Repository: containerd/nerdctl Branch: main Commit: d5bc0f0cda0a Files: 916 Total size: 4.1 MB Directory structure: gitextract_yfhpsu0l/ ├── .dockerignore ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yaml │ │ ├── config.yml │ │ └── feature_request.yaml │ ├── dependabot.yml │ └── workflows/ │ ├── ghcr-image-build-and-publish.yml │ ├── job-build.yml │ ├── job-lint-go.yml │ ├── job-lint-other.yml │ ├── job-lint-project.yml │ ├── job-test-dependencies.yml │ ├── job-test-in-container.yml │ ├── job-test-in-host.yml │ ├── job-test-in-lima.yml │ ├── job-test-in-vagrant.yml │ ├── job-test-unit.yml │ ├── release.yml │ ├── workflow-flaky.yml │ ├── workflow-lint.yml │ ├── workflow-test.yml │ └── workflow-tigron.yml ├── .gitignore ├── .golangci.yml ├── .yamllint ├── BUILDING.md ├── Dockerfile ├── Dockerfile.d/ │ ├── SHA256SUMS.d/ │ │ ├── SHA256SUMS │ │ ├── buildg-v0.5.3 │ │ ├── buildkit-v0.26.3 │ │ ├── cni-plugins-v1.9.0 │ │ ├── containerd-fuse-overlayfs-v2.1.7 │ │ ├── fuse-overlayfs-v1.16 │ │ ├── rootlesskit-v1.1.1 │ │ ├── rootlesskit-v2.3.6 │ │ ├── slirp4netns-v1.3.3 │ │ ├── stargz-snapshotter-v0.18.1 │ │ └── tini-v0.19.0 │ ├── etc_buildkit_buildkitd.toml │ ├── etc_containerd_config.toml │ ├── etc_systemd_system_user@.service.d_delegate.conf │ ├── home_rootless_.config_systemd_user_containerd.service.d_port-slirp4netns.conf │ ├── test-integration-buildkit-nerdctl-test.service │ ├── test-integration-etc_containerd-stargz-grpc_config.toml │ ├── test-integration-etc_containerd_config.toml │ ├── test-integration-ipfs-offline.service │ ├── test-integration-rootless.sh │ └── test-integration-soci-snapshotter.service ├── EMERITUS.md ├── LICENSE ├── MAINTAINERS ├── MAINTAINERS_GUIDE.md ├── Makefile ├── NOTICE ├── README.md ├── SECURITY.md ├── Vagrantfile.freebsd ├── cmd/ │ └── nerdctl/ │ ├── apparmor/ │ │ ├── apparmor_inspect_linux.go │ │ ├── apparmor_linux.go │ │ ├── apparmor_linux_test.go │ │ ├── apparmor_list_linux.go │ │ ├── apparmor_load_linux.go │ │ └── apparmor_unload_linux.go │ ├── builder/ │ │ ├── builder.go │ │ ├── builder_build.go │ │ ├── builder_build_oci_layout_test.go │ │ ├── builder_build_test.go │ │ ├── builder_builder_test.go │ │ └── builder_test.go │ ├── checkpoint/ │ │ ├── checkpoint.go │ │ ├── checkpoint_create.go │ │ ├── checkpoint_create_linux_test.go │ │ ├── checkpoint_list.go │ │ ├── checkpoint_list_linux_test.go │ │ ├── checkpoint_remove.go │ │ ├── checkpoint_remove_linux_test.go │ │ └── checkpoint_test.go │ ├── completion/ │ │ ├── completion.go │ │ ├── completion_linux.go │ │ ├── completion_test.go │ │ ├── completion_unix.go │ │ ├── completion_unix_nolinux.go │ │ └── completion_windows.go │ ├── compose/ │ │ ├── compose.go │ │ ├── compose_build.go │ │ ├── compose_build_linux_test.go │ │ ├── compose_config.go │ │ ├── compose_config_test.go │ │ ├── compose_cp.go │ │ ├── compose_cp_linux_test.go │ │ ├── compose_create.go │ │ ├── compose_create_linux_test.go │ │ ├── compose_down.go │ │ ├── compose_down_linux_test.go │ │ ├── compose_exec.go │ │ ├── compose_exec_linux_test.go │ │ ├── compose_images.go │ │ ├── compose_images_linux_test.go │ │ ├── compose_kill.go │ │ ├── compose_kill_linux_test.go │ │ ├── compose_logs.go │ │ ├── compose_pause.go │ │ ├── compose_pause_linux_test.go │ │ ├── compose_port.go │ │ ├── compose_port_linux_test.go │ │ ├── compose_ps.go │ │ ├── compose_ps_linux_test.go │ │ ├── compose_pull.go │ │ ├── compose_pull_linux_test.go │ │ ├── compose_push.go │ │ ├── compose_restart.go │ │ ├── compose_restart_linux_test.go │ │ ├── compose_rm.go │ │ ├── compose_rm_linux_test.go │ │ ├── compose_run.go │ │ ├── compose_run_linux_test.go │ │ ├── compose_start.go │ │ ├── compose_start_linux_test.go │ │ ├── compose_stop.go │ │ ├── compose_stop_linux_test.go │ │ ├── compose_test.go │ │ ├── compose_top.go │ │ ├── compose_top_linux_test.go │ │ ├── compose_up.go │ │ ├── compose_up_linux_test.go │ │ ├── compose_up_test.go │ │ ├── compose_version.go │ │ └── compose_version_test.go │ ├── container/ │ │ ├── container.go │ │ ├── container_attach.go │ │ ├── container_attach_linux_test.go │ │ ├── container_commit.go │ │ ├── container_commit_linux_test.go │ │ ├── container_commit_test.go │ │ ├── container_cp_acid_linux_test.go │ │ ├── container_cp_linux.go │ │ ├── container_cp_linux_test.go │ │ ├── container_cp_nolinux.go │ │ ├── container_create.go │ │ ├── container_create_linux_test.go │ │ ├── container_create_test.go │ │ ├── container_diff.go │ │ ├── container_diff_test.go │ │ ├── container_exec.go │ │ ├── container_exec_linux_test.go │ │ ├── container_exec_test.go │ │ ├── container_export.go │ │ ├── container_export_test.go │ │ ├── container_health_check.go │ │ ├── container_health_check_linux_test.go │ │ ├── container_inspect.go │ │ ├── container_inspect_linux_test.go │ │ ├── container_inspect_windows_test.go │ │ ├── container_kill.go │ │ ├── container_kill_linux_test.go │ │ ├── container_list.go │ │ ├── container_list_linux_test.go │ │ ├── container_list_test.go │ │ ├── container_list_windows_test.go │ │ ├── container_logs.go │ │ ├── container_logs_test.go │ │ ├── container_pause.go │ │ ├── container_port.go │ │ ├── container_prune.go │ │ ├── container_prune_linux_test.go │ │ ├── container_remove.go │ │ ├── container_remove_linux_test.go │ │ ├── container_remove_test.go │ │ ├── container_remove_windows_test.go │ │ ├── container_rename.go │ │ ├── container_rename_linux_test.go │ │ ├── container_rename_windows_test.go │ │ ├── container_restart.go │ │ ├── container_restart_linux_test.go │ │ ├── container_run.go │ │ ├── container_run_cgroup_linux_test.go │ │ ├── container_run_gpus_test.go │ │ ├── container_run_linux.go │ │ ├── container_run_linux_test.go │ │ ├── container_run_log_driver_syslog_test.go │ │ ├── container_run_mount_linux_test.go │ │ ├── container_run_mount_windows_test.go │ │ ├── container_run_network.go │ │ ├── container_run_network_base_test.go │ │ ├── container_run_network_linux_test.go │ │ ├── container_run_network_windows_test.go │ │ ├── container_run_nolinux.go │ │ ├── container_run_restart_linux_test.go │ │ ├── container_run_runtime_linux_test.go │ │ ├── container_run_security_linux_test.go │ │ ├── container_run_soci_linux_test.go │ │ ├── container_run_stargz_linux_test.go │ │ ├── container_run_systemd_linux_test.go │ │ ├── container_run_test.go │ │ ├── container_run_user_linux_test.go │ │ ├── container_run_user_windows_test.go │ │ ├── container_run_verify_linux_test.go │ │ ├── container_run_windows_test.go │ │ ├── container_start.go │ │ ├── container_start_linux_test.go │ │ ├── container_start_test.go │ │ ├── container_stats.go │ │ ├── container_stats_test.go │ │ ├── container_stop.go │ │ ├── container_stop_linux_test.go │ │ ├── container_test.go │ │ ├── container_top.go │ │ ├── container_top_test.go │ │ ├── container_unpause.go │ │ ├── container_update.go │ │ ├── container_update_linux_test.go │ │ ├── container_wait.go │ │ ├── container_wait_test.go │ │ └── multi_platform_linux_test.go │ ├── helpers/ │ │ ├── cobra.go │ │ ├── consts.go │ │ ├── flagutil.go │ │ ├── prompt.go │ │ ├── testing.go │ │ └── testing_linux.go │ ├── image/ │ │ ├── image.go │ │ ├── image_convert.go │ │ ├── image_convert_linux_test.go │ │ ├── image_cryptutil.go │ │ ├── image_decrypt.go │ │ ├── image_encrypt.go │ │ ├── image_encrypt_linux_test.go │ │ ├── image_history.go │ │ ├── image_history_test.go │ │ ├── image_import.go │ │ ├── image_import_linux_test.go │ │ ├── image_inspect.go │ │ ├── image_inspect_test.go │ │ ├── image_list.go │ │ ├── image_list_test.go │ │ ├── image_load.go │ │ ├── image_load_test.go │ │ ├── image_prune.go │ │ ├── image_prune_test.go │ │ ├── image_pull.go │ │ ├── image_pull_linux_test.go │ │ ├── image_push.go │ │ ├── image_push_linux_test.go │ │ ├── image_remove.go │ │ ├── image_remove_test.go │ │ ├── image_save.go │ │ ├── image_save_test.go │ │ ├── image_tag.go │ │ └── image_test.go │ ├── inspect/ │ │ ├── inspect.go │ │ └── inspect_test.go │ ├── internal/ │ │ ├── internal.go │ │ └── internal_oci_hook.go │ ├── ipfs/ │ │ ├── ipfs.go │ │ ├── ipfs_compose_linux_test.go │ │ ├── ipfs_kubo_linux_test.go │ │ ├── ipfs_registry.go │ │ ├── ipfs_registry_linux_test.go │ │ ├── ipfs_registry_serve.go │ │ ├── ipfs_simple_linux_test.go │ │ └── ipfs_test.go │ ├── issues/ │ │ ├── issues_linux_test.go │ │ └── main_linux_test.go │ ├── login/ │ │ ├── login.go │ │ ├── login_linux_test.go │ │ ├── login_test.go │ │ └── logout.go │ ├── main.go │ ├── main_linux.go │ ├── main_nolinux.go │ ├── main_test.go │ ├── main_test_test.go │ ├── manifest/ │ │ ├── manifest.go │ │ ├── manifest_annotate.go │ │ ├── manifest_annotate_linux_test.go │ │ ├── manifest_create.go │ │ ├── manifest_create_linux_test.go │ │ ├── manifest_inspect.go │ │ ├── manifest_inspect_linux_test.go │ │ ├── manifest_push.go │ │ ├── manifest_push_linux_test.go │ │ ├── manifest_remove.go │ │ ├── manifest_remove_linux_test.go │ │ └── manifest_test.go │ ├── namespace/ │ │ ├── namespace.go │ │ ├── namespace_create.go │ │ ├── namespace_inspect.go │ │ ├── namespace_list.go │ │ ├── namespace_remove.go │ │ ├── namespace_test.go │ │ └── namespace_update.go │ ├── network/ │ │ ├── network.go │ │ ├── network_create.go │ │ ├── network_create_linux_test.go │ │ ├── network_create_unix.go │ │ ├── network_create_windows.go │ │ ├── network_inspect.go │ │ ├── network_inspect_test.go │ │ ├── network_list.go │ │ ├── network_list_linux_test.go │ │ ├── network_prune.go │ │ ├── network_prune_linux_test.go │ │ ├── network_remove.go │ │ ├── network_remove_linux_test.go │ │ └── network_test.go │ ├── search/ │ │ ├── search.go │ │ ├── search_linux_test.go │ │ └── search_test.go │ ├── system/ │ │ ├── system.go │ │ ├── system_events.go │ │ ├── system_events_linux_test.go │ │ ├── system_info.go │ │ ├── system_info_test.go │ │ ├── system_prune.go │ │ ├── system_prune_linux_test.go │ │ └── system_test.go │ ├── version.go │ └── volume/ │ ├── volume.go │ ├── volume_create.go │ ├── volume_create_test.go │ ├── volume_inspect.go │ ├── volume_inspect_test.go │ ├── volume_list.go │ ├── volume_list_test.go │ ├── volume_namespace_test.go │ ├── volume_prune.go │ ├── volume_prune_linux_test.go │ ├── volume_remove.go │ ├── volume_remove_linux_test.go │ └── volume_test.go ├── docs/ │ ├── build.md │ ├── builder-debug.md │ ├── cni.md │ ├── command-reference.md │ ├── compose.md │ ├── config.md │ ├── cosign.md │ ├── cvmfs.md │ ├── dev/ │ │ ├── auditing_dockerfile.md │ │ └── store.md │ ├── dir.md │ ├── experimental.md │ ├── faq.md │ ├── freebsd.md │ ├── gpu.md │ ├── healthchecks.md │ ├── ipfs.md │ ├── multi-platform.md │ ├── notation.md │ ├── nydus.md │ ├── ocicrypt.md │ ├── overlaybd.md │ ├── registry.md │ ├── rootless.md │ ├── soci.md │ ├── stargz.md │ └── testing/ │ ├── README.md │ └── tools.md ├── examples/ │ ├── compose-multi-platform/ │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── docker-compose.yaml │ │ └── index.php │ ├── compose-wordpress/ │ │ ├── README.md │ │ ├── docker-compose.stargz.yaml │ │ └── docker-compose.yaml │ ├── nerdctl-as-a-library/ │ │ ├── README.md │ │ └── run-container/ │ │ └── main.go │ └── nerdctl-ipfs-registry-kubernetes/ │ ├── README.md │ ├── ipfs/ │ │ ├── README.md │ │ ├── bootstrap.yaml.sh │ │ └── nerdctl-ipfs-registry.yaml │ ├── ipfs-cluster/ │ │ ├── README.md │ │ ├── bootstrap.yaml.sh │ │ └── nerdctl-ipfs-registry.yaml │ └── ipfs-stargz-snapshotter/ │ ├── README.md │ ├── bootstrap.yaml.sh │ └── nerdctl-ipfs-registry.yaml ├── extras/ │ └── rootless/ │ ├── containerd-rootless-setuptool.sh │ └── containerd-rootless.sh ├── go.mod ├── go.sum ├── hack/ │ ├── build-integration-canary.sh │ ├── generate-release-note.sh │ ├── git-checkout-tag-with-hash.sh │ ├── github/ │ │ ├── action-helpers.sh │ │ └── gotestsum-reporter.sh │ ├── provisioning/ │ │ ├── README.md │ │ ├── gpg/ │ │ │ ├── docker │ │ │ └── hashicorp │ │ ├── kube/ │ │ │ ├── kind.sh │ │ │ └── kind.yaml │ │ ├── linux/ │ │ │ ├── cni.sh │ │ │ └── containerd.sh │ │ ├── version/ │ │ │ └── fetch.sh │ │ └── windows/ │ │ ├── cni.sh │ │ └── containerd.ps1 │ ├── scripts/ │ │ └── lib.sh │ └── test-integration.sh ├── mod/ │ └── tigron/ │ ├── .golangci.yml │ ├── .yamllint │ ├── LICENSE │ ├── Makefile │ ├── README.md │ ├── expect/ │ │ ├── comparators.go │ │ ├── comparators_test.go │ │ ├── doc.go │ │ ├── doc.md │ │ └── exit.go │ ├── go.mod │ ├── go.sum │ ├── hack/ │ │ ├── dev-setup-linux.sh │ │ ├── dev-setup-macos.sh │ │ └── headers/ │ │ ├── bash.txt │ │ ├── dockerfile.txt │ │ ├── go.txt │ │ └── makefile.txt │ ├── internal/ │ │ ├── assertive/ │ │ │ ├── assertive.go │ │ │ ├── assertive_test.go │ │ │ └── doc.go │ │ ├── com/ │ │ │ ├── command.go │ │ │ ├── command_other.go │ │ │ ├── command_test.go │ │ │ ├── command_windows.go │ │ │ ├── doc.go │ │ │ ├── package_benchmark_test.go │ │ │ ├── package_example_test.go │ │ │ ├── package_test.go │ │ │ └── pipes.go │ │ ├── doc.go │ │ ├── exit.go │ │ ├── formatter/ │ │ │ ├── doc.go │ │ │ ├── formatter.go │ │ │ └── osc8.go │ │ ├── highk/ │ │ │ ├── doc.go │ │ │ ├── fileleak.go │ │ │ └── goroutines.go │ │ ├── logger/ │ │ │ ├── doc.go │ │ │ └── logger.go │ │ ├── mimicry/ │ │ │ ├── doc.go │ │ │ ├── doc.md │ │ │ ├── mimicry.go │ │ │ ├── print.go │ │ │ └── stack.go │ │ ├── mocks/ │ │ │ ├── doc.go │ │ │ └── t.go │ │ └── pty/ │ │ └── pty.go │ ├── require/ │ │ ├── doc.go │ │ ├── doc.md │ │ ├── requirement.go │ │ └── requirement_test.go │ ├── test/ │ │ ├── case.go │ │ ├── command.go │ │ ├── config.go │ │ ├── config_test.go │ │ ├── consts.go │ │ ├── data.go │ │ ├── data_test.go │ │ ├── doc.go │ │ ├── expected.go │ │ ├── funct.go │ │ ├── helpers.go │ │ ├── interfaces.go │ │ ├── package_test.go │ │ ├── test.go │ │ └── types.go │ ├── tig/ │ │ ├── doc.go │ │ └── t.go │ └── utils/ │ ├── doc.go │ ├── testca/ │ │ └── ca.go │ └── utilities.go └── pkg/ ├── annotations/ │ └── annotations.go ├── api/ │ └── types/ │ ├── apparmor_types.go │ ├── builder_types.go │ ├── checkpoint_types.go │ ├── container_network_types.go │ ├── container_types.go │ ├── cri/ │ │ └── metadata_types.go │ ├── global.go │ ├── image_types.go │ ├── import_types.go │ ├── ipfs_types.go │ ├── load_types.go │ ├── login_types.go │ ├── manifest_types.go │ ├── namespace_types.go │ ├── network_types.go │ ├── search_types.go │ ├── system_types.go │ └── volume_types.go ├── apparmorutil/ │ ├── apparmorutil.go │ └── apparmorutil_linux.go ├── buildkitutil/ │ ├── buildkitutil.go │ ├── buildkitutil_linux.go │ ├── buildkitutil_test.go │ ├── buildkitutil_unix.go │ ├── buildkitutil_unix_nolinux.go │ ├── buildkitutil_windows.go │ └── types.go ├── bypass4netnsutil/ │ ├── bypass.go │ └── bypass4netnsutil.go ├── checkpointutil/ │ └── checkpointutil.go ├── cioutil/ │ ├── container_io.go │ ├── container_io_unix.go │ └── container_io_windows.go ├── clientutil/ │ └── client.go ├── cmd/ │ ├── apparmor/ │ │ ├── inspect_linux.go │ │ ├── list_linux.go │ │ ├── load_linux.go │ │ └── unload_linux.go │ ├── builder/ │ │ ├── build.go │ │ ├── build_test.go │ │ └── prune.go │ ├── checkpoint/ │ │ ├── create.go │ │ ├── list.go │ │ └── remove.go │ ├── compose/ │ │ └── compose.go │ ├── container/ │ │ ├── attach.go │ │ ├── commit.go │ │ ├── cp_linux.go │ │ ├── create.go │ │ ├── create_userns_opts_darwin.go │ │ ├── create_userns_opts_freebsd.go │ │ ├── create_userns_opts_linux.go │ │ ├── create_userns_opts_linux_test.go │ │ ├── create_userns_opts_windows.go │ │ ├── exec.go │ │ ├── exec_linux.go │ │ ├── exec_nolinux.go │ │ ├── export.go │ │ ├── health_check.go │ │ ├── idmap.go │ │ ├── inspect.go │ │ ├── kill.go │ │ ├── list.go │ │ ├── list_util.go │ │ ├── logs.go │ │ ├── pause.go │ │ ├── prune.go │ │ ├── remove.go │ │ ├── rename.go │ │ ├── restart.go │ │ ├── run_blkio_linux.go │ │ ├── run_cdi.go │ │ ├── run_cgroup_linux.go │ │ ├── run_gpus.go │ │ ├── run_linux.go │ │ ├── run_mount.go │ │ ├── run_restart.go │ │ ├── run_runtime.go │ │ ├── run_security_linux.go │ │ ├── run_ulimit_linux.go │ │ ├── run_unix_nolinux.go │ │ ├── run_user.go │ │ ├── run_windows.go │ │ ├── start.go │ │ ├── stats.go │ │ ├── stats_linux.go │ │ ├── stats_nolinux.go │ │ ├── stop.go │ │ ├── top.go │ │ ├── top_unix.go │ │ ├── top_windows.go │ │ ├── unpause.go │ │ └── wait.go │ ├── image/ │ │ ├── convert.go │ │ ├── crypt.go │ │ ├── ensure.go │ │ ├── import.go │ │ ├── inspect.go │ │ ├── list.go │ │ ├── prune.go │ │ ├── pull.go │ │ ├── push.go │ │ ├── remove.go │ │ ├── save.go │ │ └── tag.go │ ├── ipfs/ │ │ └── registry_serve.go │ ├── login/ │ │ ├── login.go │ │ ├── prompt.go │ │ ├── prompt_unix.go │ │ └── prompt_windows.go │ ├── logout/ │ │ └── logout.go │ ├── manifest/ │ │ ├── annotate.go │ │ ├── create.go │ │ ├── inspect.go │ │ ├── push.go │ │ └── rm.go │ ├── namespace/ │ │ ├── common.go │ │ ├── create.go │ │ ├── inspect.go │ │ ├── list.go │ │ ├── namespace_linux.go │ │ ├── namespace_nolinux.go │ │ ├── remove.go │ │ └── update.go │ ├── network/ │ │ ├── create.go │ │ ├── inspect.go │ │ ├── list.go │ │ ├── prune.go │ │ └── remove.go │ ├── search/ │ │ └── search.go │ ├── system/ │ │ ├── events.go │ │ ├── info.go │ │ └── prune.go │ └── volume/ │ ├── create.go │ ├── inspect.go │ ├── list.go │ ├── prune.go │ ├── rm.go │ └── volume.go ├── composer/ │ ├── build.go │ ├── composer.go │ ├── config.go │ ├── container.go │ ├── copy.go │ ├── create.go │ ├── down.go │ ├── exec.go │ ├── kill.go │ ├── lock.go │ ├── logs.go │ ├── orphans.go │ ├── pause.go │ ├── pipetagger/ │ │ └── pipetagger.go │ ├── port.go │ ├── pull.go │ ├── push.go │ ├── restart.go │ ├── rm.go │ ├── run.go │ ├── serviceparser/ │ │ ├── build.go │ │ ├── build_test.go │ │ ├── serviceparser.go │ │ └── serviceparser_test.go │ ├── stop.go │ ├── up.go │ ├── up_network.go │ ├── up_service.go │ └── up_volume.go ├── config/ │ └── config.go ├── consoleutil/ │ ├── consoleutil.go │ ├── consoleutil_unix.go │ ├── consoleutil_windows.go │ └── detach.go ├── containerdutil/ │ ├── content.go │ ├── helpers.go │ ├── image_store.go │ ├── snapshotter.go │ └── version.go ├── containerinspector/ │ ├── containerinspector.go │ ├── containerinspector_linux.go │ ├── containerinspector_unix_nolinux.go │ └── containerinspector_windows.go ├── containerutil/ │ ├── config.go │ ├── container_network_manager.go │ ├── container_network_manager_linux.go │ ├── container_network_manager_other.go │ ├── container_network_manager_test.go │ ├── container_network_manager_windows.go │ ├── containerutil.go │ ├── containerutil_test.go │ ├── cp_linux.go │ ├── cp_resolve_linux.go │ └── lock.go ├── defaults/ │ ├── cgroup_linux.go │ ├── defaults_darwin.go │ ├── defaults_freebsd.go │ ├── defaults_linux.go │ └── defaults_windows.go ├── dnsutil/ │ ├── dnsutil.go │ ├── dnsutil_test.go │ └── hostsstore/ │ ├── hosts.go │ ├── hosts_test.go │ ├── hostsstore.go │ ├── updater.go │ └── updater_test.go ├── doc.go ├── errutil/ │ ├── errors_check.go │ └── exit_coder.go ├── eventutil/ │ └── eventutil.go ├── flagutil/ │ ├── flagutil.go │ └── flagutil_test.go ├── formatter/ │ ├── common.go │ ├── formatter.go │ └── formatter_test.go ├── fs/ │ └── fs.go ├── healthcheck/ │ ├── executor.go │ ├── health.go │ ├── healthcheck_manager_darwin.go │ ├── healthcheck_manager_freebsd.go │ ├── healthcheck_manager_linux.go │ ├── healthcheck_manager_windows.go │ └── log.go ├── identifiers/ │ └── validate.go ├── idgen/ │ └── idgen.go ├── idutil/ │ ├── containerwalker/ │ │ └── containerwalker.go │ └── imagewalker/ │ └── imagewalker.go ├── imageinspector/ │ └── imageinspector.go ├── imgutil/ │ ├── commit/ │ │ ├── commit.go │ │ ├── commit_other.go │ │ └── commit_unix.go │ ├── converter/ │ │ ├── convert.go │ │ ├── info.go │ │ └── zstd.go │ ├── dockerconfigresolver/ │ │ ├── credentialsstore.go │ │ ├── credentialsstore_test.go │ │ ├── defaults.go │ │ ├── dockerconfigresolver.go │ │ ├── hostsstore.go │ │ ├── registryurl.go │ │ └── registryurl_test.go │ ├── fetch/ │ │ └── fetch.go │ ├── filtering.go │ ├── filtering_test.go │ ├── imgutil.go │ ├── imgutil_test.go │ ├── jobs/ │ │ └── jobs.go │ ├── load/ │ │ └── load.go │ ├── pull/ │ │ └── pull.go │ ├── push/ │ │ └── push.go │ ├── snapshotter.go │ ├── snapshotter_test.go │ └── transfer.go ├── infoutil/ │ ├── infoutil.go │ ├── infoutil_darwin.go │ ├── infoutil_freebsd.go │ ├── infoutil_linux.go │ ├── infoutil_test.go │ ├── infoutil_unix.go │ ├── infoutil_unix_test.go │ ├── infoutil_windows.go │ ├── infoutil_windows_test.go │ └── infoutilmock/ │ └── infoutil_mock.go ├── inspecttypes/ │ ├── dockercompat/ │ │ ├── blkio.go │ │ ├── blkioutils_linux.go │ │ ├── blkioutils_others.go │ │ ├── dockercompat.go │ │ ├── dockercompat_test.go │ │ └── info.go │ └── native/ │ ├── container.go │ ├── image.go │ ├── info.go │ ├── namespace.go │ ├── network.go │ └── volume.go ├── internal/ │ └── filesystem/ │ ├── consts.go │ ├── errors.go │ ├── helpers.go │ ├── lock.go │ ├── lock_test.go │ ├── lock_unix.go │ ├── lock_windows.go │ ├── os.go │ ├── path.go │ ├── path_test.go │ ├── path_unix.go │ ├── path_windows.go │ ├── umask.go │ ├── umask_test.go │ ├── umask_unix.go │ ├── umask_windows.go │ ├── writefile_rename.go │ ├── writefile_rollback.go │ └── writefile_rollback_test.go ├── ipcutil/ │ ├── ipcutil.go │ ├── ipcutil_linux.go │ ├── ipcutil_other.go │ └── ipcutil_windows.go ├── ipfs/ │ ├── image_ipfs.go │ ├── image_noipfs.go │ ├── noipfs.go │ ├── registry.go │ ├── registry_ipfs.go │ └── registry_noipfs.go ├── labels/ │ ├── k8slabels/ │ │ └── k8slabels.go │ └── labels.go ├── logging/ │ ├── cri_logger.go │ ├── cri_logger_test.go │ ├── detail_writer.go │ ├── fluentd_logger.go │ ├── fluentd_logger_test.go │ ├── journald_logger.go │ ├── json_logger.go │ ├── json_logger_test.go │ ├── jsonfile/ │ │ └── jsonfile.go │ ├── log_viewer.go │ ├── logging.go │ ├── logging_test.go │ ├── logs_other.go │ ├── logs_windows.go │ ├── none_logger.go │ ├── none_logger_test.go │ ├── syslog_logger.go │ └── tail/ │ ├── tail.go │ └── tail_test.go ├── manifeststore/ │ └── manifeststore.go ├── manifesttypes/ │ └── manifesttypes.go ├── manifestutil/ │ └── manifestutils.go ├── maputil/ │ ├── maputil.go │ └── maputil_test.go ├── mountutil/ │ ├── mountutil.go │ ├── mountutil_darwin.go │ ├── mountutil_freebsd.go │ ├── mountutil_linux.go │ ├── mountutil_linux_test.go │ ├── mountutil_test.go │ ├── mountutil_unix.go │ ├── mountutil_windows.go │ ├── mountutil_windows_test.go │ └── volumestore/ │ └── volumestore.go ├── namestore/ │ ├── namestore.go │ └── namestore_test.go ├── netutil/ │ ├── cni_plugin.go │ ├── cni_plugin_unix.go │ ├── cni_plugin_windows.go │ ├── nettype/ │ │ ├── nettype.go │ │ └── nettype_test.go │ ├── netutil.go │ ├── netutil_linux_test.go │ ├── netutil_test.go │ ├── netutil_unix.go │ ├── netutil_unix_test.go │ ├── netutil_windows.go │ ├── netutil_windows_test.go │ ├── networkstore/ │ │ └── networkstore.go │ ├── store.go │ └── subnet/ │ ├── subnet.go │ └── subnet_test.go ├── ocihook/ │ ├── ocihook.go │ ├── ocihook_linux.go │ ├── ocihook_nolinux.go │ ├── rootless_linux.go │ ├── rootless_other.go │ └── state/ │ └── state.go ├── platformutil/ │ ├── binfmt.go │ ├── layers.go │ └── platformutil.go ├── portutil/ │ ├── iptable/ │ │ ├── iptables.go │ │ ├── iptables_linux.go │ │ └── iptables_test.go │ ├── port_allocate_linux.go │ ├── port_allocate_other.go │ ├── portutil.go │ ├── portutil_test.go │ └── procnet/ │ ├── procnet.go │ ├── procnet_linux.go │ └── procnetd_test.go ├── referenceutil/ │ ├── cid_ipfs.go │ ├── cid_noipfs.go │ ├── referenceutil.go │ └── referenceutil_test.go ├── reflectutil/ │ ├── reflectutil.go │ └── reflectutil_test.go ├── resolvconf/ │ ├── resolvconf.go │ └── resolvconf_linux_test.go ├── rootlessutil/ │ ├── child_linux.go │ ├── parent_linux.go │ ├── port_linux.go │ ├── rootlessutil_linux.go │ ├── rootlessutil_other.go │ └── xdg_linux.go ├── signalutil/ │ ├── signals.go │ ├── signals_linux.go │ └── signals_other.go ├── signutil/ │ ├── cosignutil.go │ ├── notationutil.go │ └── signutil.go ├── snapshotterutil/ │ ├── socisource.go │ └── sociutil.go ├── statsutil/ │ ├── stats.go │ └── stats_linux.go ├── store/ │ ├── filestore.go │ ├── filestore_test.go │ └── store.go ├── strutil/ │ ├── strutil.go │ └── strutil_test.go ├── systemutil/ │ ├── socket_unix.go │ └── socket_windows.go ├── tabutil/ │ ├── tabutil.go │ └── tabutil_test.go ├── tarutil/ │ └── tarutil.go ├── taskutil/ │ └── taskutil.go ├── testutil/ │ ├── compose.go │ ├── images.yaml │ ├── images_linux.go │ ├── iptables/ │ │ └── iptables_linux.go │ ├── nerdtest/ │ │ ├── ambient.go │ │ ├── command.go │ │ ├── hoststoml/ │ │ │ └── hoststoml.go │ │ ├── platform/ │ │ │ ├── platform_darwin.go │ │ │ ├── platform_freebsd.go │ │ │ ├── platform_linux.go │ │ │ └── platform_windows.go │ │ ├── registry/ │ │ │ ├── cesanta.go │ │ │ ├── common.go │ │ │ ├── docker.go │ │ │ └── kubo.go │ │ ├── requirements.go │ │ ├── requirements_other.go │ │ ├── requirements_windows.go │ │ ├── test.go │ │ ├── third-party.go │ │ ├── utilities.go │ │ └── utilities_linux.go │ ├── nettestutil/ │ │ └── nettestutil.go │ ├── portlock/ │ │ └── portlock.go │ ├── testca/ │ │ └── testca.go │ ├── testregistry/ │ │ ├── certsd_linux.go │ │ └── testregistry_linux.go │ ├── testsyslog/ │ │ └── testsyslog.go │ ├── testutil.go │ ├── testutil_darwin.go │ ├── testutil_freebsd.go │ ├── testutil_linux.go │ └── testutil_windows.go ├── transferutil/ │ └── progress.go └── version/ └── version.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ # artifacts /nerdctl _output *.gomodjail # golangci-lint /build # vagrant /.vagrant ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yaml ================================================ name: Bug report description: Create a bug report to help improve nerdctl labels: kind/unconfirmed-bug-claim body: - type: markdown attributes: value: | If you are reporting a new issue, make sure that we do not have any duplicates already open. You can ensure this by searching the issue list for this repository. If there is a duplicate, please close your issue and add a comment to the existing issue instead. Please also see [the FAQs and Troubleshooting](https://github.com/containerd/nerdctl/blob/main/docs/faq.md). - type: textarea attributes: label: Description description: | Briefly describe the problem you are having in a few paragraphs. validations: required: true - type: textarea attributes: label: Steps to reproduce the issue value: | 1. 2. 3. - type: textarea attributes: label: Describe the results you received and expected validations: required: true - type: textarea attributes: label: What version of nerdctl are you using? placeholder: nerdctl version validations: required: true - type: dropdown attributes: label: Are you using a variant of nerdctl? (e.g., Rancher Desktop) options: - Rancher Desktop for Windows - Rancher Desktop for macOS - Lima - Colima - Others - type: textarea attributes: label: Host information placeholder: nerdctl info ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: true contact_links: - name: Ask a question (GitHub Discussions) url: https://github.com/containerd/nerdctl/discussions about: | Please do not submit "a bug report" for asking a question. In most cases, GitHub Discussions is the best place to ask a question. If you are not sure whether you are going to report a bug or ask a question, please consider asking in GitHub Discussions first. - name: Chat with containerd/nerdctl users and developers url: https://slack.cncf.io/ about: CNCF slack has `#containerd` and `#containerd-dev` channels ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yaml ================================================ name: Feature request description: Suggest an idea for nerdctl labels: kind/feature body: - type: textarea attributes: label: What is the problem you're trying to solve description: | A clear and concise description of what the problem is. validations: required: true - type: textarea attributes: label: Describe the solution you'd like description: | A clear and concise description of what you'd like to happen. validations: required: true - type: textarea attributes: label: Additional context description: | Add any other context about the feature request here. ================================================ FILE: .github/dependabot.yml ================================================ # ----------------------------------------------------------------------------- # Forked from https://raw.githubusercontent.com/opencontainers/runc/2888e6e54339e52ae45710daa9e47cdb2e1926f9/.github/dependabot.yml # Copyright The runc Authors. # Licensed under the Apache License, Version 2.0 # ----------------------------------------------------------------------------- # Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: # Dependencies listed in go.mod - package-ecosystem: "gomod" directory: "/" # Location of package manifests schedule: interval: "daily" groups: golang-x: patterns: - "golang.org/x/*" moby-sys: patterns: - "github.com/moby/sys/*" docker: patterns: - "github.com/docker/docker" - "github.com/docker/cli" containerd: patterns: - "github.com/containerd/containerd" - "github.com/containerd/containerd/api" stargz: patterns: - "github.com/containerd/stargz-snapshotter" - "github.com/containerd/stargz-snapshotter/estargz" - "github.com/containerd/stargz-snapshotter/ipfs" # Dependencies listed in .github/workflows/*.yml - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" # Dependencies listed in Dockerfile - package-ecosystem: "docker" directory: "/" schedule: interval: "daily" ================================================ FILE: .github/workflows/ghcr-image-build-and-publish.yml ================================================ name: image # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. on: push: branches: [main] # Publish semver tags as releases. tags: ['v*.*.*'] pull_request: branches: [main] paths-ignore: - '**.md' env: # Use docker.io for Docker Hub if empty REGISTRY: ghcr.io # github.repository as / IMAGE_NAME: ${{ github.repository }} jobs: build: runs-on: ubuntu-24.04 permissions: contents: read packages: write steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: Set up QEMU uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 # Login against a Docker registry except on PR # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # Extract metadata (tags, labels) for Docker # https://github.com/docker/metadata-action - name: Extract Docker metadata id: meta uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} # Build and push Docker image with Buildx (don't push on PR) # https://github.com/docker/build-push-action - name: Build and push Docker image uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: context: . platforms: linux/amd64,linux/arm64 push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} secrets: | github_token=${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/job-build.yml ================================================ # This job just builds nerdctl for the golang versions we support (as a smoke test) name: job-build on: workflow_call: inputs: timeout: required: true type: number go-version: required: true type: string runner: required: true type: string canary: required: false default: false type: boolean env: GOTOOLCHAIN: local jobs: build-all-targets: name: ${{ format('go {0}', inputs.canary && 'canary' || inputs.go-version ) }} timeout-minutes: ${{ inputs.timeout }} runs-on: "${{ inputs.runner }}" defaults: run: shell: bash env: GO_VERSION: ${{ inputs.go-version }} steps: - name: "Init: checkout" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 - if: ${{ inputs.canary }} name: "Init (canary): retrieve GO_VERSION" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | . ./hack/github/action-helpers.sh latest_go="$(. ./hack/provisioning/version/fetch.sh; go::canary::for::go-setup)" printf "GO_VERSION=%s\n" "$latest_go" >> "$GITHUB_ENV" [ "$latest_go" != "" ] || \ github::log::warning "No canary go" "There is currently no canary go version to test. Steps will not run." - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true - if: ${{ env.GO_VERSION != '' }} name: "Run: make binaries" run: | . ./hack/github/action-helpers.sh github::md::table::header "OS" "Arch" "Result" "Time" >> $GITHUB_STEP_SUMMARY failure= build(){ local goos="$1" local goarch="${2:-amd64}" local goarm="${3:-}" local result GOOS="$goos" GOARCH="$goarch" GOARM="$goarm" go build ./examples/... github::timer::begin GOOS="$goos" GOARCH="$goarch" GOARM="$goarm" make binaries \ && result="$decorator_success" \ || { failure=true result="$decorator_failure" } [ ! "$goarm" ] || goarch="$goarch/v$goarm" github::md::table::line "$goos" "$goarch" "$result" "$(github::timer::format <(github::timer::tick))" >> $GITHUB_STEP_SUMMARY } # We officially support these build linux build linux arm64 build windows build freebsd # These architectures are not released, but we still verify that we can at least compile build darwin build linux arm 6 build linux loong64 build linux ppc64le build linux riscv64 build linux s390x [ ! "$failure" ] || exit 1 - if: ${{ env.GO_VERSION != '' }} name: "Run: make binaries with custom BUILDTAGS" run: | set -eux # no_ipfs: make sure it does not incur any IPFS-related dependency go mod vendor rm -rf vendor/github.com/ipfs vendor/github.com/multiformats BUILDTAGS=no_ipfs make binaries ================================================ FILE: .github/workflows/job-lint-go.yml ================================================ # This job runs golangci-lint # Note that technically, `make lint-go-all` would run the linter for all targets, and could be called once, on a single instance. # The point of running it on a matrix instead, each GOOS separately, is to verify that the tooling itself is working on the target OS. name: job-lint-go on: workflow_call: inputs: timeout: required: true type: number go-version: required: true type: string runner: required: true type: string canary: required: false default: false type: boolean goos: required: true type: string env: GOTOOLCHAIN: local jobs: lint-go: name: ${{ format('{0}{1}', inputs.goos, inputs.canary && ' (go canary)' || '') }} timeout-minutes: ${{ inputs.timeout }} runs-on: "${{ inputs.runner }}" defaults: run: shell: bash env: GO_VERSION: ${{ inputs.go-version }} steps: - name: "Init: checkout" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 - if: ${{ inputs.canary }} name: "Init (canary): retrieve GO_VERSION" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | latest_go="$(. ./hack/provisioning/version/fetch.sh; go::canary::for::go-setup)" printf "GO_VERSION=%s\n" "$latest_go" >> "$GITHUB_ENV" [ "$latest_go" != "" ] || \ echo "::warning title=No canary go::There is currently no canary go version to test. Steps will not run." - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true - if: ${{ env.GO_VERSION != '' }} name: "Init: install dev-tools" run: | echo "::group:: make install-dev-tools" make install-dev-tools echo "::endgroup::" - if: ${{ env.GO_VERSION != '' }} name: "Run" run: | # On canary, lint for all supported targets if [ "${{ inputs.canary }}" == "true" ]; then NO_COLOR=true make lint-go-all else NO_COLOR=true GOOS="${{ inputs.goos }}" make lint-go fi ================================================ FILE: .github/workflows/job-lint-other.yml ================================================ # This job runs any subsidiary linter not part of golangci (shell, yaml, etc) name: job-lint-other on: workflow_call: inputs: timeout: required: true type: number runner: required: true type: string env: GOTOOLCHAIN: local jobs: lint-other: name: "yaml | shell" timeout-minutes: ${{ inputs.timeout }} runs-on: ${{ inputs.runner }} defaults: run: shell: bash steps: - name: "Init: checkout" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 - name: "Run: yaml" run: | make lint-yaml - name: "Run: shell" run: | make lint-shell ================================================ FILE: .github/workflows/job-lint-project.yml ================================================ # This job runs containerd shared project-checks, that verifies licenses, headers, and commits. # To run locally, you may just use `make lint` instead, that does the same thing # (albeit `make lint` uses more modern versions). name: job-lint-project on: workflow_call: inputs: timeout: required: true type: number go-version: required: true type: string runner: required: true type: string env: GOTOOLCHAIN: local jobs: project: name: "commits, licenses..." timeout-minutes: ${{ inputs.timeout }} runs-on: ${{ inputs.runner }} defaults: run: shell: bash steps: - name: "Init: checkout" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 100 path: src/github.com/containerd/nerdctl - name: "Init: install go" uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: ${{ inputs.go-version }} check-latest: true cache-dependency-path: src/github.com/containerd/nerdctl - name: "Run" uses: containerd/project-checks@d7751f3c375b8fe4a84c02a068184ee4c1f59bc4 # v1.2.2 with: working-directory: src/github.com/containerd/nerdctl repo-access-token: ${{ secrets.GITHUB_TOKEN }} # go-licenses-ignore is set because go-licenses cannot detect the license of the following package: # * go-base36: Apache-2.0 OR MIT (https://github.com/multiformats/go-base36/blob/master/LICENSE.md) # * filepath-securejoin: MPL-2.0 AND BSD-3-Clause, exceptionally approved by CNCF # (https://github.com/cncf/foundation/issues/1154#issuecomment-3562385979) # # The list of the CNCF-approved licenses can be found here: # https://github.com/cncf/foundation/blob/main/allowed-third-party-license-policy.md go-licenses-ignore: | github.com/multiformats/go-base36 github.com/cyphar/filepath-securejoin ================================================ FILE: .github/workflows/job-test-dependencies.yml ================================================ # This job pre-heats the cache for the test image by building all dependencies name: job-test-dependencies on: workflow_call: inputs: timeout: required: true type: number runner: required: true type: string containerd-version: required: false default: '' type: string env: GOTOOLCHAIN: local jobs: # This job builds the dependency target of the test docker image for all supported architectures and cache it in GHA build-dependencies: # Note: for whatever reason, you cannot access env.RUNNER_ARCH here name: "${{ contains(inputs.runner, 'arm') && 'arm64' || 'amd64' }}${{ inputs.containerd-version && format(' | {0}', inputs.containerd-version) || ''}}" timeout-minutes: ${{ inputs.timeout }} runs-on: "${{ inputs.runner }}" defaults: run: shell: bash steps: - name: "Init: checkout" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 - name: "Init: expose GitHub Runtime variables for gha" uses: crazy-max/ghaction-github-runtime@04d248b84655b509d8c44dc1d6f990c879747487 # v4.0.0 - name: "Run: build dependencies for the integration test environment image" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # Cache is sharded per-architecture arch=${{ env.RUNNER_ARCH == 'ARM64' && 'arm64' || 'amd64' }} docker buildx create --name with-gha --use # Honor old containerd if requested args=() if [ "${{ inputs.containerd-version }}" != "" ]; then args=(--build-arg CONTAINERD_VERSION=${{ inputs.containerd-version }}) fi docker buildx build \ --secret id=github_token,env=GITHUB_TOKEN \ --cache-to type=gha,compression=zstd,mode=max,scope=test-integration-dependencies-"$arch" \ --cache-from type=gha,scope=test-integration-dependencies-"$arch" \ --target build-dependencies "${args[@]}" . ================================================ FILE: .github/workflows/job-test-in-container.yml ================================================ # This job runs integration tests inside a container, for all supported variants (ipv6, canary, etc) # Note that it is linux and nerdctl (+/- gomodjail) only. name: job-test-in-container on: workflow_call: inputs: timeout: required: true type: number runner: required: true type: string canary: required: false default: false type: boolean target: required: false default: '' type: string binary: required: false default: nerdctl type: string containerd-version: required: false default: '' type: string rootlesskit-version: required: false default: '' type: string ipv6: required: false default: false type: boolean skip-flaky: required: false default: false type: boolean env: GOTOOLCHAIN: local jobs: test: name: | ${{ inputs.binary != 'nerdctl' && format('{0} < ', inputs.binary) || '' }} ${{ inputs.target }} ${{ contains(inputs.runner, 'arm') && '(arm)' || '' }} ${{ contains(inputs.runner, '22.04') && '(old ubuntu)' || '' }} ${{ inputs.ipv6 && ' (ipv6)' || '' }} ${{ inputs.canary && ' (canary)' || '' }} ${{ inputs.containerd-version && format(' (ctd: {0})', inputs.containerd-version) || '' }} ${{ inputs.rootlesskit-version && format(' (rlk: {0})', inputs.rootlesskit-version) || '' }} timeout-minutes: ${{ inputs.timeout }} runs-on: ${{ inputs.runner }} defaults: run: shell: bash env: # https://github.com/containerd/nerdctl/issues/622 # The only case when rootlesskit-version is force-specified is when we downgrade explicitly to v1 WORKAROUND_ISSUE_622: ${{ inputs.rootlesskit-version }} steps: - name: "Init: checkout" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 - name: "Init: expose GitHub Runtime variables for gha" uses: crazy-max/ghaction-github-runtime@04d248b84655b509d8c44dc1d6f990c879747487 # v4.0.0 - name: "Init: install br-netfilter" run: | # This ensures that bridged traffic goes through netfilter sudo modprobe br-netfilter - name: "Init: register QEMU (tonistiigi/binfmt)" run: | # `--install all` will only install emulation for architectures that cannot be natively executed # Since some arm64 platforms do provide native fallback execution for 32 bits, # armv7 emulation may or may not be installed, causing variance in the result of `uname -m`. # To avoid that, we explicitly list the architectures we do want emulation for. docker run --privileged --rm tonistiigi/binfmt --install linux/amd64 docker run --privileged --rm tonistiigi/binfmt --install linux/arm64 docker run --privileged --rm tonistiigi/binfmt --install linux/arm/v7 - if: ${{ inputs.canary }} name: "Init (canary): prepare updated test image" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | . ./hack/build-integration-canary.sh canary::build::integration - if: ${{ ! inputs.canary }} name: "Init: prepare test image" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | buildargs=() # If the runner is old, use old ubuntu inside the container as well [ "${{ contains(inputs.runner, '22.04') }}" != "true" ] || buildargs=(--build-arg UBUNTU_VERSION=22.04) # Honor if we want old containerd [ "${{ inputs.containerd-version }}" == "" ] || buildargs+=(--build-arg CONTAINERD_VERSION=${{ inputs.containerd-version }}) # Honor custom targets and if we want old rootlesskit target=test-integration if [ "${{ inputs.target }}" != "rootful" ]; then target+=-${{ inputs.target }} if [ "${{ inputs.rootlesskit-version }}" != "" ]; then buildargs+=(--build-arg ROOTLESSKIT_VERSION=${{ inputs.rootlesskit-version }}) fi fi # Cache is sharded per-architecture arch=${{ env.RUNNER_ARCH == 'ARM64' && 'arm64' || 'amd64' }} docker buildx create --name with-gha --use docker buildx build \ --secret id=github_token,env=GITHUB_TOKEN \ --output=type=docker \ --cache-from type=gha,scope=test-integration-dependencies-"$arch" \ -t "$target" --target "$target" \ "${buildargs[@]}" \ . # Rootful needs to disable snap - if: ${{ inputs.target == 'rootful' }} name: "Init: remove snap loopback devices (conflicts with our loopback devices in TestRunDevice)" run: | sudo systemctl disable --now snapd.service snapd.socket sudo apt-get purge -qq snapd sudo losetup -Dv sudo losetup -lv # Rootless on modern ubuntu wants apparmor - if: ${{ inputs.target != 'rootful' && ! contains(inputs.runner, '22.04') }} name: "Init: prepare apparmor for rootless + ubuntu 24+" run: | cat <, include /usr/local/bin/rootlesskit flags=(unconfined) { userns, # Site-specific additions and overrides. See local/README for details. include if exists } EOT sudo systemctl restart apparmor.service # ipv6 wants... ipv6 - if: ${{ inputs.ipv6 }} name: "Init: ipv6" run: | # Enable ipv4 and ipv6 forwarding sudo sysctl -w net.ipv6.conf.all.forwarding=1 sudo sysctl -w net.ipv4.ip_forward=1 # Enable IPv6 for Docker, and configure docker to use containerd for gha sudo mkdir -p /etc/docker echo '{"ipv6": true, "fixed-cidr-v6": "2001:db8:1::/64", "ip6tables": true}' | sudo tee /etc/docker/daemon.json - name: "Init: enable Docker experimental features" run: | sudo mkdir -p /etc/docker if [ -f /etc/docker/daemon.json ]; then tmpfile="$(sudo mktemp)" sudo jq '.experimental = true' /etc/docker/daemon.json | sudo tee "$tmpfile" >/dev/null sudo mv "$tmpfile" /etc/docker/daemon.json else echo '{"experimental": true}' | sudo tee /etc/docker/daemon.json >/dev/null fi sudo systemctl restart docker - name: "Run: integration tests" run: | . ./hack/github/action-helpers.sh github::md::h2 "non-flaky" >> "$GITHUB_STEP_SUMMARY" # IPV6 note: nested IPv6 network inside docker and qemu is complex and needs a bunch of sysctl config. # Therefore, it's hard to debug why the IPv6 tests fail in such an isolation layer. # On the other side, using the host network is easier at configuration. # Besides, each job is running on a different instance, which means using host network here # is safe and has no side effects on others. [ "${{ inputs.target }}" == "rootful" ] \ && args=(test-integration ./hack/test-integration.sh -test.allow-modify-users=true) \ || args=(test-integration-${{ inputs.target }} /test-integration-rootless.sh ./hack/test-integration.sh) if [ "${{ inputs.ipv6 }}" == true ]; then docker run --network host -t --rm --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=false -test.only-ipv6 -test.target=${{ inputs.binary }} else docker run -t --rm --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=false -test.target=${{ inputs.binary }} fi # FIXME: this NEEDS to go away - name: "Run: integration tests (flaky)" if: ${{ !fromJSON(inputs.skip-flaky) }} run: | . ./hack/github/action-helpers.sh github::md::h2 "flaky" >> "$GITHUB_STEP_SUMMARY" [ "${{ inputs.target }}" == "rootful" ] \ && args=(test-integration ./hack/test-integration.sh) \ || args=(test-integration-${{ inputs.target }} /test-integration-rootless.sh ./hack/test-integration.sh) if [ "${{ inputs.ipv6 }}" == true ]; then docker run --network host -t --rm --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=true -test.only-ipv6 -test.target=${{ inputs.binary }} else docker run -t --rm --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=true -test.target=${{ inputs.binary }} fi ================================================ FILE: .github/workflows/job-test-in-host.yml ================================================ # This currently test docker and nerdctl on windows (w/o canary) # Structure is in to allow testing nerdctl on linux as well, though more work is required to make it functional. name: job-test-in-host on: workflow_call: inputs: timeout: required: true type: number runner: required: true type: string canary: required: false default: false type: boolean binary: required: false default: nerdctl type: string go-version: required: true type: string docker-version: required: true type: string containerd-version: required: true type: string containerd-sha: required: true type: string containerd-service-sha: required: true type: string windows-cni-version: required: true type: string linux-cni-version: required: true type: string linux-cni-sha: required: true type: string env: GOTOOLCHAIN: local jobs: test: name: | ${{ inputs.binary != 'nerdctl' && format('{0} < ', inputs.binary) || '' }} ${{ contains(inputs.runner, 'ubuntu') && ' linux' || ' windows' }} ${{ contains(inputs.runner, 'arm') && '(arm)' || '' }} ${{ contains(inputs.runner, '22.04') && '(old ubuntu)' || '' }} ${{ inputs.canary && ' (canary)' || '' }} timeout-minutes: ${{ inputs.timeout }} runs-on: "${{ inputs.runner }}" defaults: run: shell: bash env: SHOULD_RUN: "yes" GO_VERSION: ${{ inputs.go-version }} # Both Docker and nerdctl on linux need rootful right now WITH_SUDO: ${{ contains(inputs.runner, 'ubuntu') }} CONTAINERD_VERSION: ${{ inputs.containerd-version }} CONTAINERD_SHA: ${{ inputs.containerd-sha }} steps: - name: "Init: checkout" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 - if: ${{ inputs.canary }} name: "Init (canary): retrieve latest go and containerd" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | latest_go="$(. ./hack/provisioning/version/fetch.sh; go::canary::for::go-setup)" latest_containerd="$(. ./hack/provisioning/version/fetch.sh; github::project::latest "containerd/containerd")" [ "$latest_go" == "" ] || \ printf "GO_VERSION=%s\n" "$latest_go" >> "$GITHUB_ENV" [ "${latest_containerd:1}" == "$CONTAINERD_VERSION" ] || { printf "CONTAINERD_VERSION=%s\n" "${latest_containerd:1}" >> "$GITHUB_ENV" printf "CONTAINERD_SHA=canary is volatile and I accept the risk\n" >> "$GITHUB_ENV" } if [ "$latest_go" == "" ] && [ "${latest_containerd:1}" == "$CONTAINERD_VERSION" ]; then echo "::warning title=No canary::There is currently no canary versions to test. Steps will not run."; printf "SHOULD_RUN=no\n" >> "$GITHUB_ENV" fi - if: ${{ env.SHOULD_RUN == 'yes' }} name: "Init: install go" uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true # XXX RUNNER_OS and generally env is too unreliable # - if: ${{ env.RUNNER_OS == 'Linux' }} - if: ${{ contains(inputs.runner, 'ubuntu') && env.SHOULD_RUN == 'yes' }} name: "Init (linux): prepare host" run: | if [ "${{ contains(inputs.binary, 'docker') }}" == true ]; then echo "::group:: configure cdi and experimental for docker" sudo mkdir -p /etc/docker sudo jq -n '.features.cdi = true | .experimental = true' | sudo tee /etc/docker/daemon.json echo "::endgroup::" echo "::group:: downgrade docker to the specific version we want to test (${{ inputs.docker-version }})" sudo apt-get update -qq sudo apt-get install -qq ca-certificates curl sudo install -m 0755 -d /etc/apt/keyrings sudo cp ./hack/provisioning/gpg/docker /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" \ | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update -qq sudo apt-get install -qq --allow-downgrades docker-ce=${{ inputs.docker-version }} docker-ce-cli=${{ inputs.docker-version }} sudo systemctl restart docker echo "::endgroup::" else # FIXME: this is missing runc (see top level workflow note about the state of this) echo "::group:: install dependencies" sudo ./hack/provisioning/linux/containerd.sh uninstall ./hack/provisioning/linux/containerd.sh rootful "$CONTAINERD_VERSION" "amd64" "$CONTAINERD_SHA" "${{ inputs.containerd-service-sha }}" sudo ./hack/provisioning/linux/cni.sh uninstall ./hack/provisioning/linux/cni.sh install "${{ inputs.linux-cni-version }}" "amd64" "${{ inputs.linux-cni-sha }}" echo "::endgroup::" echo "::group:: build nerctl" go install ./cmd/nerdctl echo "$HOME/go/bin" >> "$GITHUB_PATH" # Since tests are going to run root, we need nerdctl to be in a PATH that will survive `sudo` sudo cp "$(which nerdctl)" /usr/local/bin echo "::endgroup::" fi # Register QEMU (tonistiigi/binfmt) # `--install all` will only install emulation for architectures that cannot be natively executed # Since some arm64 platforms do provide native fallback execution for 32 bits, # armv7 emulation may or may not be installed, causing variance in the result of `uname -m`. # To avoid that, we explicitly list the architectures we do want emulation for. echo "::group:: install binfmt" docker run --quiet --privileged --rm tonistiigi/binfmt --install linux/amd64 docker run --quiet --privileged --rm tonistiigi/binfmt --install linux/arm64 docker run --quiet --privileged --rm tonistiigi/binfmt --install linux/arm/v7 echo "::endgroup::" # FIXME: remove expect when we are done removing unbuffer from tests echo "::group:: installing test dependencies" sudo add-apt-repository ppa:criu/ppa -y sudo apt-get install -qq expect criu echo "::endgroup::" # This ensures that bridged traffic goes through netfilter sudo modprobe br-netfilter - if: ${{ contains(inputs.runner, 'windows') && env.SHOULD_RUN == 'yes' }} name: "Init (windows): prepare host" env: ctrdVersion: ${{ env.CONTAINERD_VERSION }} run: | # Install WinCNI echo "::group:: install wincni" GOPATH=$(go env GOPATH) WINCNI_VERSION=${{ inputs.windows-cni-version }} ./hack/provisioning/windows/cni.sh echo "::endgroup::" # Install containerd echo "::group:: install containerd" powershell hack/provisioning/windows/containerd.ps1 echo "::endgroup::" # Install nerdctl echo "::group:: build nerctl" go install ./cmd/nerdctl echo "::endgroup::" choco install jq - if: ${{ env.SHOULD_RUN == 'yes' }} name: "Init: install dev tools" run: | echo "::group:: make install-dev-tools" make install-dev-tools echo "::endgroup::" # ipv6 is tested only on linux - if: ${{ contains(inputs.runner, 'ubuntu') && env.SHOULD_RUN == 'yes' }} name: "Run (linux): integration tests (IPv6)" run: | . ./hack/github/action-helpers.sh github::md::h2 "ipv6" >> "$GITHUB_STEP_SUMMARY" ./hack/test-integration.sh -test.target=${{ inputs.binary }} -test.only-ipv6 - if: ${{ env.SHOULD_RUN == 'yes' }} name: "Run: integration tests" run: | . ./hack/github/action-helpers.sh github::md::h2 "non-flaky" >> "$GITHUB_STEP_SUMMARY" ./hack/test-integration.sh -test.target=${{ inputs.binary }} -test.only-flaky=false # FIXME: this must go - if: ${{ env.SHOULD_RUN == 'yes' }} name: "Run: integration tests (flaky)" run: | . ./hack/github/action-helpers.sh github::md::h2 "flaky" >> "$GITHUB_STEP_SUMMARY" ./hack/test-integration.sh -test.target=${{ inputs.binary }} -test.only-flaky=true ================================================ FILE: .github/workflows/job-test-in-lima.yml ================================================ # Currently, Lima job test only for EL, though in the future it could be used to also test FreeBSD or other linux-es name: job-test-in-lima on: workflow_call: inputs: timeout: required: true type: number runner: required: true type: string target: required: true type: string guest: required: true type: string skip-flaky: required: false default: false type: boolean jobs: test: name: "${{ inputs.guest }} ${{ inputs.target }}" timeout-minutes: ${{ inputs.timeout }} runs-on: "${{ inputs.runner }}" env: TARGET: ${{ inputs.target }} steps: - name: "Init: checkout" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 - name: "Init: lima" uses: lima-vm/lima-actions/setup@55627e31b78637bf254a8b2a14da8ea7d12564e5 # v1.1.0 id: lima-actions-setup - name: "Init: Cache" uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: ~/.cache/lima key: lima-${{ steps.lima-actions-setup.outputs.version }} - name: "Init: start the guest VM" run: | set -eux # containerd=none is set because the built-in containerd support conflicts with Docker limactl start \ --name=default \ --cpus=4 \ --memory=12 \ --containerd=none \ --set '.mounts=null | .portForwards=[{"guestSocket":"/var/run/docker.sock","hostSocket":"{{.Dir}}/sock/docker.sock"}]' \ template://${{ inputs.guest }} # FIXME: the tests should be directly executed in the VM without nesting Docker inside it # https://github.com/containerd/nerdctl/issues/3858 - name: "Init: install dockerd in the guest VM" run: | set -eux lima sudo mkdir -p /etc/systemd/system/docker.socket.d cat <<-EOF | lima sudo tee /etc/systemd/system/docker.socket.d/override.conf [Socket] SocketUser=$(whoami) EOF lima sudo dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo lima sudo dnf -q -y install docker-ce --nobest lima sudo systemctl enable --now docker - name: "Init: configure the host to use dockerd in the guest VM" run: | set -eux sudo systemctl disable --now docker.service docker.socket export DOCKER_HOST="unix://$(limactl ls --format '{{.Dir}}/sock/docker.sock' default)" echo "DOCKER_HOST=${DOCKER_HOST}" >>$GITHUB_ENV docker info docker version - name: "Init: install br-netfilter in the guest VM" run: | lima sudo modprobe br-netfilter - name: "Init: expose GitHub Runtime variables for gha" uses: crazy-max/ghaction-github-runtime@04d248b84655b509d8c44dc1d6f990c879747487 # v4.0.0 - name: "Init: prepare integration tests" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eux sudo losetup -Dv sudo losetup -lv [ "$TARGET" = "rootless" ] && TARGET=test-integration-rootless || TARGET=test-integration docker buildx create --name with-gha --use docker buildx build \ --secret id=github_token,env=GITHUB_TOKEN \ --output=type=docker \ --cache-from type=gha,scope=test-integration-dependencies-amd64 \ -t test-integration --target "${TARGET}" \ . - name: "Run integration tests" # Presumably, something is broken with the way docker exposes /dev to the container, as it appears to only # randomly work. Mounting /dev does workaround the issue. # This might be due to the old kernel shipped with Alma (4.18), or something else between centos/docker. run: | set -eux if [ "$TARGET" = "rootless" ]; then echo "rootless" docker run -t -v /dev:/dev --rm --privileged test-integration /test-integration-rootless.sh ./hack/test-integration.sh -test.only-flaky=false else echo "rootful" docker run -t -v /dev:/dev --rm --privileged test-integration ./hack/test-integration.sh -test.only-flaky=false fi - name: "Run: integration tests (flaky)" if: ${{ !fromJSON(inputs.skip-flaky) }} run: | set -eux if [ "$TARGET" = "rootless" ]; then echo "rootless" docker run -t -v /dev:/dev --rm --privileged test-integration /test-integration-rootless.sh ./hack/test-integration.sh -test.only-flaky=true else echo "rootful" docker run -t -v /dev:/dev --rm --privileged test-integration ./hack/test-integration.sh -test.only-flaky=true fi ================================================ FILE: .github/workflows/job-test-in-vagrant.yml ================================================ # Right now, this is testing solely FreeBSD, but could be used to test other targets. # Alternatively, this might get replaced entirely by Lima eventually. name: job-test-in-vagrant on: workflow_call: inputs: timeout: required: true type: number runner: required: true type: string jobs: test: # Will appear as freebsd / 14 in GitHub UI name: "14" timeout-minutes: ${{ inputs.timeout }} runs-on: "${{ inputs.runner }}" steps: - name: "Init: checkout" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 - name: "Init: setup cache" uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: /root/.vagrant.d key: vagrant - name: "Init: set up vagrant" run: | # from https://github.com/containerd/containerd/blob/v2.0.2/.github/workflows/ci.yml#L583-L596 # which is based on https://github.com/opencontainers/runc/blob/v1.1.8/.cirrus.yml#L41-L49 # FIXME: https://github.com/containerd/nerdctl/issues/4163 cat ./hack/provisioning/gpg/hashicorp | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list sudo sed -i 's/^Types: deb$/Types: deb deb-src/' /etc/apt/sources.list.d/ubuntu.sources sudo apt-get update -qq sudo apt-get install -qq libvirt-daemon libvirt-daemon-system vagrant ovmf # https://github.com/vagrant-libvirt/vagrant-libvirt/issues/1725#issuecomment-1454058646 sudo cp /usr/share/OVMF/OVMF_VARS_4M.fd /var/lib/libvirt/qemu/nvram/ sudo systemctl enable --now libvirtd sudo apt-get build-dep -qq ruby-libvirt sudo apt-get install -qq --no-install-recommends libxslt-dev libxml2-dev libvirt-dev ruby-bundler ruby-dev zlib1g-dev # Disable strict dependency enforcement to bypass gem version conflicts during the installation of the vagrant-libvirt plugin. sudo env VAGRANT_DISABLE_STRICT_DEPENDENCY_ENFORCEMENT=1 vagrant plugin install vagrant-libvirt - name: "Init: boot VM" run: | ln -sf Vagrantfile.freebsd Vagrantfile sudo vagrant up --no-tty - name: "Run: test-unit" run: sudo vagrant up --provision-with=test-unit - name: "Run: test-integration" run: sudo vagrant up --provision-with=test-integration ================================================ FILE: .github/workflows/job-test-unit.yml ================================================ # Note: freebsd tests are not ran here (see integration instead) name: job-test-unit on: workflow_call: inputs: timeout: required: true type: number go-version: required: true type: string runner: required: true type: string canary: required: false default: false type: boolean windows-cni-version: required: true type: string linux-cni-version: required: true type: string linux-cni-sha: required: true type: string env: GOTOOLCHAIN: local # Windows fails without this CGO_ENABLED: 0 jobs: test-unit: name: ${{ format('{0}{1}', inputs.runner, inputs.canary && ' (go canary)' || '') }} timeout-minutes: ${{ inputs.timeout }} runs-on: "${{ inputs.runner }}" defaults: run: shell: bash env: GO_VERSION: ${{ inputs.go-version }} steps: - name: "Init: checkout" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 # If canary is requested, check for the latest unstable release - if: ${{ inputs.canary }} name: "Init (canary): retrieve GO_VERSION" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | latest_go="$(. ./hack/provisioning/version/fetch.sh; go::canary::for::go-setup)" printf "GO_VERSION=%s\n" "$latest_go" >> "$GITHUB_ENV" [ "$latest_go" != "" ] || \ echo "::warning title=No canary go::There is currently no canary go version to test. Following steps will not run." - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true # Install CNI and CRIU - if: ${{ env.GO_VERSION != '' }} name: "Init: set up CNI and CRIU" run: | if [ "$RUNNER_OS" == "Windows" ]; then GOPATH=$(go env GOPATH) WINCNI_VERSION=${{ inputs.windows-cni-version }} ./hack/provisioning/windows/cni.sh elif [ "$RUNNER_OS" == "Linux" ]; then ./hack/provisioning/linux/cni.sh install "${{ inputs.linux-cni-version }}" "amd64" "${{ inputs.linux-cni-sha }}" sudo apt-get update -qq sudo add-apt-repository ppa:criu/ppa -y sudo apt-get install -qq criu fi - if: ${{ env.GO_VERSION != '' }} name: "Run" run: | make test-unit # On linux, also run with root - if: ${{ env.GO_VERSION != '' && env.RUNNER_OS == 'Linux' }} name: "Run: with root" run: | sudo make test-unit ================================================ FILE: .github/workflows/release.yml ================================================ # See https://github.com/containerd/nerdctl/blob/main/MAINTAINERS_GUIDE.md for how to make a release. name: Release on: push: tags: - 'v*' - 'test-action-release-*' pull_request: paths-ignore: - '**.md' env: GOTOOLCHAIN: local jobs: release: runs-on: ubuntu-24.04 timeout-minutes: 40 # The maximum access is "read" for PRs from public forked repos # https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token permissions: contents: write # for releases id-token: write # for provenances attestations: write # for provenances steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: "Set up QEMU" uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - name: "Install go" uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: "1.25" check-latest: true - name: "Compile binaries" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: make artifacts - name: "SHA256SUMS" run: | ( cd _output; sha256sum nerdctl-* ) | tee /tmp/SHA256SUMS mv /tmp/SHA256SUMS _output/SHA256SUMS - name: "The sha256sum of the SHA256SUMS file" run: (cd _output; sha256sum SHA256SUMS) - name: "Prepare the release note" run: | shasha=$(sha256sum _output/SHA256SUMS | awk '{print $1}') cat <<-EOF | tee /tmp/release-note.txt $(hack/generate-release-note.sh) - - - The binaries were built automatically on GitHub Actions. The build log is available for 90 days: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} The sha256sum of the SHA256SUMS file itself is \`${shasha}\` . - - - Release manager: [ADD YOUR NAME HERE] (@[ADD YOUR GITHUB ID HERE]) EOF - name: "Generate artifact attestation" uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') with: subject-path: _output/* - name: "Create release" if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | tag="${GITHUB_REF##*/}" gh release create -F /tmp/release-note.txt --draft --title "${tag}" "${tag}" _output/* ================================================ FILE: .github/workflows/workflow-flaky.yml ================================================ # This workflow puts together all known "flaky" and experimental targets name: "[flaky, see #3988]" on: push: branches: - main - 'release/**' pull_request: paths-ignore: - '**.md' jobs: test-integration-el: name: "EL${{ inputs.hack }}" uses: ./.github/workflows/job-test-in-lima.yml strategy: fail-fast: false # EL8 is used for testing compatibility with cgroup v1. # Unfortunately, EL8 is hard to debug for ARM Mac users (as Lima+ARM Mac+EL8 is not runnable because of page size), # and it currently shows numerous issues. # ARM Mac users may use oraclelinux-8 instead for debugging cgroup v1 issues, although its kernel is different from # other EL8 variants. matrix: guest: ["almalinux-8"] target: ["rootful", "rootless"] with: timeout: 60 runner: ubuntu-24.04 guest: ${{ matrix.guest }} target: ${{ matrix.target }} skip-flaky: true # skip the most flaky ones for now test-integration-freebsd: name: "FreeBSD" uses: ./.github/workflows/job-test-in-vagrant.yml with: timeout: 15 runner: ubuntu-24.04 kube: name: "kubernetes" runs-on: ubuntu-24.04 timeout-minutes: 15 env: ROOTFUL: true steps: - name: "Init: checkout" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 - name: "Run" run: | # FIXME: this should be a bit more elegant to use. ./hack/provisioning/kube/kind.sh # See https://github.com/containerd/nerdctl/blob/main/docs/testing/README.md#about-parallelization sudo ./_output/nerdctl exec nerdctl-test-control-plane bash -c -- 'export TMPDIR="$HOME"/tmp; mkdir -p "$TMPDIR"; cd /nerdctl-source; /usr/local/go/bin/go test -p 1 ./cmd/nerdctl/... -test.only-kubernetes' ================================================ FILE: .github/workflows/workflow-lint.yml ================================================ name: lint on: push: branches: - main - 'release/**' pull_request: jobs: # Runs golangci to ensure that: # 1. the tooling is working on the target platform # 2. the linter is happy # 3. for canary (if there is a canary go version), does lint for all supported goos lint-go: name: "go${{ inputs.hack }}" uses: ./.github/workflows/job-lint-go.yml strategy: fail-fast: false matrix: include: - runner: ubuntu-24.04 goos: linux - runner: ubuntu-24.04 goos: freebsd - runner: macos-15 goos: darwin # FIXME: this is currently failing in a nonsensical way, so, running on linux instead... # - runner: windows-2022 - runner: ubuntu-24.04 goos: windows # Additionally lint for canary - runner: ubuntu-24.04 goos: linux canary: true with: timeout: 10 go-version: "1.25" runner: ubuntu-24.04 # Note: in GitHub yaml world, if `matrix.canary` is undefined, and is passed to `inputs.canary`, the job # will not run. However, if you test it, it will coerce to `false`, hence: canary: ${{ matrix.canary && true || false }} goos: ${{ matrix.goos }} # Run common project checks (commits, licenses, etc) lint-project-checks: name: "project checks" uses: ./.github/workflows/job-lint-project.yml with: timeout: 5 go-version: "1.25" runner: ubuntu-24.04 # Lint for shell and yaml files lint-other: name: "other" uses: ./.github/workflows/job-lint-other.yml with: timeout: 5 runner: ubuntu-24.04 # Verify we can actually build on all supported platforms, and a bunch of architectures build-for-go: name: "build for${{ inputs.hack }}" uses: ./.github/workflows/job-build.yml strategy: fail-fast: false matrix: include: # Build for both old and stable go - go-version: "1.24" - go-version: "1.25" # Additionally build for canary - go-version: "1.25" canary: true with: timeout: 10 go-version: ${{ matrix.go-version }} runner: ubuntu-24.04 canary: ${{ matrix.canary && true || false }} ================================================ FILE: .github/workflows/workflow-test.yml ================================================ name: test on: push: branches: - main - 'release/**' pull_request: paths-ignore: - '**.md' jobs: test-unit: # Note: inputs.hack is undefined - its purpose is to prevent GitHub Actions from displaying all matrix variants as part of the name. name: "unit${{ inputs.hack }}" uses: ./.github/workflows/job-test-unit.yml strategy: fail-fast: false matrix: # Run on all supported platforms but freebsd # Additionally run on canary for linux include: - runner: "ubuntu-24.04" - runner: "macos-15" - runner: "windows-2025" - runner: "ubuntu-24.04" canary: true with: runner: ${{ matrix.runner }} canary: ${{ matrix.canary && true || false }} # Windows routinely go over 5 minutes timeout: 10 go-version: 1.25 windows-cni-version: v0.3.1 linux-cni-version: v1.7.1 linux-cni-sha: 1a28a0506bfe5bcdc981caf1a49eeab7e72da8321f1119b7be85f22621013098 # This job builds the dependency target of the test-image for all supported architectures and cache it in GHA build-dependencies: name: "dependencies${{ inputs.hack }}" uses: ./.github/workflows/job-test-dependencies.yml strategy: fail-fast: false matrix: include: # Build for arm & amd, current containerd - runner: ubuntu-24.04 - runner: ubuntu-24.04-arm # Additionally build for old containerd on amd - runner: ubuntu-24.04 containerd-version: v1.7.30 with: runner: ${{ matrix.runner }} containerd-version: ${{ matrix.containerd-version }} timeout: 20 test-integration-container: name: "in-container${{ inputs.hack }}" uses: ./.github/workflows/job-test-in-container.yml needs: build-dependencies strategy: fail-fast: false matrix: include: ###### Rootless # amd64 - runner: ubuntu-24.04 target: rootless # arm64 - runner: ubuntu-24.04-arm target: rootless skip-flaky: true # port-slirp4netns - runner: ubuntu-24.04 target: rootless-port-slirp4netns skip-flaky: true # old containerd + old ubuntu + old rootlesskit - runner: ubuntu-22.04 target: rootless containerd-version: v1.7.30 rootlesskit-version: v1.1.1 # gomodjail - runner: ubuntu-24.04 target: rootless binary: "nerdctl.gomodjail" ###### Rootful # amd64 - runner: ubuntu-24.04 target: rootful # arm64 - runner: ubuntu-24.04-arm target: rootful skip-flaky: true # old containerd + old ubuntu - runner: ubuntu-22.04 target: rootful containerd-version: v1.7.30 # ipv6 - runner: ubuntu-24.04 target: rootful ipv6: true skip-flaky: true # all canary - runner: ubuntu-24.04 target: rootful canary: true with: timeout: 80 runner: ${{ matrix.runner }} target: ${{ matrix.target }} binary: ${{ matrix.binary && matrix.binary || 'nerdctl' }} containerd-version: ${{ matrix.containerd-version }} rootlesskit-version: ${{ matrix.rootlesskit-version }} ipv6: ${{ matrix.ipv6 && true || false }} canary: ${{ matrix.canary && true || false }} skip-flaky: ${{ matrix.skip-flaky && true || false }} test-integration-host: name: "in-host${{ inputs.hack }}" uses: ./.github/workflows/job-test-in-host.yml strategy: fail-fast: false matrix: include: # Test on windows w/o canary - runner: windows-2022 - runner: windows-2025 canary: true # Test docker on linux - runner: ubuntu-24.04 binary: docker # FIXME: running nerdctl on the host is work in progress # (we miss runc to be installed on the host - and obviously other deps) # Plan is to pause this for now and first consolidate dependencies management (wrt Dockerfile vs. host-testing CI) # before we can really start testing linux nerdctl on the host. # - runner: ubuntu-24.04 # - runner: ubuntu-24.04 # canary: true with: timeout: 45 runner: ${{ matrix.runner }} binary: ${{ matrix.binary != '' && matrix.binary || 'nerdctl' }} canary: ${{ matrix.canary && true || false }} go-version: 1.25 windows-cni-version: v0.3.1 docker-version: 5:28.0.4-1~ubuntu.24.04~noble containerd-version: 2.2.1 # Note: these as for amd64 containerd-sha: f5d8e90ecb6c1c7e33ecddf8cc268a93b9e5b54e0e850320d765511d76624f41 containerd-service-sha: 1941362cbaa89dd591b99c32b050d82c583d3cd2e5fa63085d7017457ec5fca8 linux-cni-version: v1.9.0 linux-cni-sha: 58c03705426e929658f45a851df15a86d06ef680cacbf3f2dc127731ca265c28 ================================================ FILE: .github/workflows/workflow-tigron.yml ================================================ name: tigron on: push: branches: - main - 'release/**' pull_request: paths: 'mod/tigron/**' env: GO_VERSION: "1.25" GOTOOLCHAIN: local jobs: lint: timeout-minutes: 15 name: "${{ matrix.goos }} ${{ matrix.runner }} | go ${{ matrix.canary }}" runs-on: ${{ matrix.runner }} defaults: run: shell: bash strategy: matrix: include: - runner: ubuntu-24.04 - runner: macos-15 - runner: windows-2022 - runner: ubuntu-24.04 goos: freebsd - runner: ubuntu-24.04 canary: go-canary steps: - name: "Checkout project" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 100 - if: ${{ matrix.canary }} name: "Init (canary): retrieve GO_VERSION" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | latest_go="$(. ./hack/provisioning/version/fetch.sh; go::canary::for::go-setup)" printf "GO_VERSION=%s\n" "$latest_go" >> "$GITHUB_ENV" [ "$latest_go" != "" ] || \ echo "::warning title=No canary go::There is currently no canary go version to test. Steps will not run." - if: ${{ env.GO_VERSION != '' }} name: "Install go" uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true - if: ${{ env.GO_VERSION != '' }} name: "Install tools" run: | cd mod/tigron echo "::group:: make install-dev-tools" make install-dev-tools if [ "$RUNNER_OS" == macOS ]; then brew install yamllint shellcheck fi echo "::endgroup::" - if: ${{ env.GO_VERSION != '' && matrix.goos == '' }} name: "lint" env: NO_COLOR: true run: | if [ "$RUNNER_OS" == Linux ]; then echo "::group:: lint" cd mod/tigron export LINT_COMMIT_RANGE="$(jq -r '.after + "..HEAD"' ${GITHUB_EVENT_PATH})" make lint echo "::endgroup::" else echo "Lint is disabled on $RUNNER_OS" fi - if: ${{ env.GO_VERSION != '' }} name: "test-unit" run: | echo "::group:: unit test" cd mod/tigron make test-unit echo "::endgroup::" - if: ${{ env.GO_VERSION != '' }} name: "test-unit-race" run: | echo "::group:: race test" cd mod/tigron make test-unit-race echo "::endgroup::" - if: ${{ env.GO_VERSION != '' }} name: "test-unit-bench" run: | echo "::group:: bench" cd mod/tigron make test-unit-bench echo "::endgroup::" ================================================ FILE: .gitignore ================================================ # artifacts /nerdctl _output *.gomodjail # golangci-lint /build # vagrant /.vagrant Vagrantfile ================================================ FILE: .golangci.yml ================================================ version: "2" run: modules-download-mode: readonly issues: max-issues-per-linter: 0 max-same-issues: 0 linters: default: none enable: # 1. This is the default enabled set of golanci # We should consider enabling errcheck # - errcheck - govet - ineffassign - staticcheck - unused # 2. These are not part of the default set # Important to prevent import of certain packages - depguard # Removes unnecessary conversions - unconvert # Flag common typos - misspell # A meta-linter seen as a good replacement for golint - revive # Gocritic - gocritic - forbidigo # 3. We used to use these, but have now removed them # Use of prealloc is generally premature optimization and performance profiling should be done instead # https://golangci-lint.run/usage/linters/#prealloc # - prealloc # Provided by revive in a better way # - nakedret settings: forbidigo: forbid: # FIXME: there are still calls to os.WriteFile in tests under `cmd` - pattern: ^os\.WriteFile.*$ pkg: github.com/containerd/nerdctl/v2/pkg msg: os.WriteFile is neither atomic nor durable - use nerdctl filesystem.WriteFile instead - pattern: ^os\.ReadFile.*$ pkg: github.com/containerd/nerdctl/v2/pkg msg: use filesystem.ReadFile instead of os.ReadFile staticcheck: checks: # Below is the default set - "all" - "-ST1000" - "-ST1003" - "-ST1016" - "-ST1020" - "-ST1021" - "-ST1022" ##### TODO: fix and enable these # 6 occurrences. # Apply De Morgan’s law https://staticcheck.dev/docs/checks#QF1001 - "-QF1001" # 10 occurrences. # Convert if/else-if chain to tagged switch https://staticcheck.dev/docs/checks#QF1003 - "-QF1003" ##### These have been vetted to be disabled. # 55 occurrences. Omit embedded fields from selector expression https://staticcheck.dev/docs/checks#QF1008 # Usefulness is questionable. - "-QF1008" revive: enable-all-rules: true rules: # See https://revive.run/r ##### P0: we should do it ASAP. - name: max-control-nesting # 10 occurences (at default 5). Deep nesting hurts readibility. arguments: [7] - name: deep-exit # 11 occurrences. Do not exit in random places. disabled: true - name: unchecked-type-assertion # 14 occurrences. This is generally risky and encourages bad coding for newcomers. disabled: true - name: bare-return # 31 occurrences. Bare returns are just evil, very unfriendly, and make reading and editing much harder. disabled: true - name: import-shadowing # 44 occurrences. Shadowing makes things prone to errors / confusing to read. disabled: true - name: use-errors-new # 84 occurrences. Improves error testing. disabled: true - name: struct-tag # 2 occurrences. disabled: true ##### P1: consider making a dent on these, but not critical. - name: argument-limit # 4 occurrences (at default 8). Long windy arguments list for functions are hard to read. Use structs instead. arguments: [12] - name: unnecessary-stmt # 5 occurrences. Increase readability. disabled: true - name: defer # 7 occurrences. Confusing to read for newbies. disabled: true - name: confusing-naming # 10 occurrences. Hurts readability. disabled: true - name: early-return # 10 occurrences. Would improve readability. disabled: true - name: function-result-limit # 12 occurrences (at default 3). A function returning many results is probably too big. arguments: [7] - name: function-length # 155 occurrences (at default 0, 75). Really long functions should really be broken up in most cases. arguments: [0, 500] - name: cyclomatic # 204 occurrences (at default 10) arguments: [100] - name: unhandled-error # 222 occurrences. Could indicate failure to handle broken conditions. disabled: true - name: cognitive-complexity arguments: [205] # 441 occurrences (at default 7). We should try to lower it (involves significant refactoring). - name: var-naming # 1 occurrence. disabled: true ##### P2: nice to have. - name: max-public-structs # 7 occurrences (at default 5). Might indicate overcrowding of public API. arguments: [25] - name: confusing-results # 13 occurrences. Have named returns when the type stutters. # Makes it a bit easier to figure out function behavior just looking at signature. disabled: true - name: comment-spacings # 50 occurrences. Makes code look less wonky / ease readability. disabled: true - name: use-any # 30 occurrences. `any` instead of `interface{}`. Cosmetic. disabled: true - name: empty-lines # 85 occurrences. Makes code look less wonky / ease readability. disabled: true - name: package-comments # 100 occurrences. Better for documentation... disabled: true - name: exported # 577 occurrences. Forces documentation of any exported symbol. disabled: true - name: unnecessary-format # Many occurrences. disabled: true ###### Permanently disabled. Below have been reviewed and vetted to be unnecessary. - name: line-length-limit # Formatter `golines` takes care of this. disabled: true - name: nested-structs # 5 occurrences. Trivial. This is not that hard to read. disabled: true - name: flag-parameter # 52 occurrences. Not sure if this is valuable. disabled: true - name: unused-parameter # 505 occurrences. A lot of work for a marginal improvement. disabled: true - name: unused-receiver # 31 occurrences. Ibid. disabled: true - name: add-constant # 2605 occurrences. Kind of useful in itself, but unacceptable amount of effort to fix disabled: true - name: enforce-switch-style # Many occurrences. disabled: true depguard: rules: no-patent: # do not link in golang-lru anywhere (problematic patent) deny: - pkg: github.com/hashicorp/golang-lru/arc/v2 desc: patented (https://github.com/hashicorp/golang-lru/blob/arc/v2.0.7/arc/arc.go#L18) pkg: # pkg files must not depend on cobra nor anything in cmd files: - '**/pkg/**/*.go' deny: - pkg: github.com/spf13/cobra desc: pkg must not depend on cobra - pkg: github.com/spf13/pflag desc: pkg must not depend on pflag - pkg: github.com/spf13/viper desc: pkg must not depend on viper - pkg: github.com/containerd/nerdctl/v2/cmd desc: pkg must not depend on any cmd files gocritic: disabled-checks: # Below are normally enabled by default, but we do not pass - appendAssign - ifElseChain - unslice - badCall - assignOp - commentFormatting - captLocal - singleCaseSwitch - wrapperFunc - elseif - regexpMust enabled-checks: # Below used to be enabled, but we do not pass anymore # - paramTypeCombine # - octalLiteral # - unnamedResult # - equalFold # - sloppyReassign # - emptyStringTest # - hugeParam # - appendCombine # - stringXbytes # - ptrToRefParam # - commentedOutCode # - rangeValCopy # - methodExprCall # - yodaStyleExpr # - typeUnparen # We enabled these and we pass - nilValReturn - weakCond - indexAlloc - rangeExprCopy - boolExprSimplify - commentedOutImport - docStub - emptyFallthrough - hexLiteral - typeAssertChain - unlabelStmt - builtinShadow - initClause - nestingReduce - unnecessaryBlock exclusions: generated: disable formatters: settings: gci: sections: - standard - default - prefix(github.com/containerd) - localmodule no-inline-comments: true no-prefix-comments: true custom-order: true gofumpt: extra-rules: true golines: max-len: 500 tab-len: 4 shorten-comments: true enable: - gci - gofmt # We might consider enabling the following: # - gofumpt - golines exclusions: generated: disable ================================================ FILE: .yamllint ================================================ --- extends: default rules: indentation: spaces: 2 indent-sequences: consistent truthy: allowed-values: ['true', 'false', 'on', 'off'] comments-indentation: disable document-start: disable line-length: disable ================================================ FILE: BUILDING.md ================================================ # Building nerdctl To build nerdctl, use `make`: ```bash make sudo make install ``` Alternatively, nerdctl can be also built with `go build ./cmd/nerdctl`. However, this is not recommended as it does not populate the version string (`nerdctl -v`). ## Customization To specify build tags, set the `BUILDTAGS` variable as follows: ```bash BUILDTAGS=no_ipfs make ``` The following build tags are supported: * `no_ipfs` (since v2.1.3): Disable IPFS ================================================ FILE: Dockerfile ================================================ # Copyright The containerd Authors. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ----------------------------------------------------------------------------- # Usage: `docker run -it --privileged `. Make sure to add `-t` and `--privileged`. # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- ARG CONTAINERD_VERSION=v2.2.1@dea7da592f5d1d2b7755e3a161be07f43fad8f75 ARG RUNC_VERSION=v1.4.0@8bd78a9977e604c4d5f67a7415d7b8b8c109cdc4 ARG CNI_PLUGINS_VERSION=v1.9.0@BINARY # Extra deps: Build ARG BUILDKIT_VERSION=v0.26.3@BINARY # Extra deps: Lazy-pulling ARG STARGZ_SNAPSHOTTER_VERSION=v0.18.1@BINARY # Extra deps: Encryption ARG IMGCRYPT_VERSION=v2.0.2@6892f4df2405cd15acbefd1dca970f53ba38bfda # Extra deps: Rootless ARG ROOTLESSKIT_VERSION=v2.3.6@BINARY ARG SLIRP4NETNS_VERSION=v1.3.3@BINARY # Extra deps: bypass4netns ARG BYPASS4NETNS_VERSION=v0.4.2@aa04bd3dcc48c6dae6d7327ba219bda8fe2a4634 # Extra deps: FUSE-OverlayFS ARG FUSE_OVERLAYFS_VERSION=v1.16@BINARY ARG CONTAINERD_FUSE_OVERLAYFS_VERSION=v2.1.7@BINARY # Extra deps: Init ARG TINI_VERSION=v0.19.0@BINARY # Extra deps: Debug ARG BUILDG_VERSION=v0.5.3@BINARY # Extra deps: gomodjail ARG GOMODJAIL_VERSION=v0.1.3@cea529ddd971b677c67d8af7e936fbc62b35b98c # Test deps # Currently, the Docker Official Images and the test deps are not pinned by the hash ARG GO_VERSION=1.25 ARG UBUNTU_VERSION=24.04 ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.1 ARG GOTESTSUM_VERSION=v1.13.0 ARG NYDUS_VERSION=v2.3.9 ARG SOCI_SNAPSHOTTER_VERSION=0.12.1 ARG KUBO_VERSION=v0.39.0 FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0@sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707 AS xx FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-trixie AS build-base COPY --from=xx / / ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update -qq && apt-get install -qq --no-install-recommends \ make \ git \ jq \ curl \ dpkg-dev ARG TARGETARCH # libbtrfs: for containerd # libseccomp: for runc and bypass4netns RUN xx-apt-get update -qq && xx-apt-get install -qq --no-install-recommends \ binutils \ gcc \ libc6-dev \ libbtrfs-dev \ libseccomp-dev \ pkg-config RUN git config --global advice.detachedHead false ADD hack/git-checkout-tag-with-hash.sh /usr/local/bin/ ADD hack/scripts/lib.sh /usr/local/bin/http::helper FROM build-base AS build-containerd ARG TARGETARCH ARG CONTAINERD_VERSION RUN git clone --quiet --depth 1 --branch "${CONTAINERD_VERSION%%@*}" https://github.com/containerd/containerd.git /go/src/github.com/containerd/containerd WORKDIR /go/src/github.com/containerd/containerd RUN git-checkout-tag-with-hash.sh ${CONTAINERD_VERSION} && \ mkdir -p /out /out/$TARGETARCH && \ cp -a containerd.service /out RUN GO=xx-go make STATIC=1 && \ cp -a bin/containerd bin/containerd-shim-runc-v2 bin/ctr /out/$TARGETARCH FROM build-base AS build-runc ARG RUNC_VERSION ARG TARGETARCH RUN git clone --quiet --depth 1 --branch "${RUNC_VERSION%%@*}" https://github.com/opencontainers/runc.git /go/src/github.com/opencontainers/runc WORKDIR /go/src/github.com/opencontainers/runc RUN git-checkout-tag-with-hash.sh ${RUNC_VERSION} && \ mkdir -p /out ENV CGO_ENABLED=1 # FIXME: avoid omitting libpathrs RUN set -x ; GO=xx-go CC=$(xx-info)-gcc STRIP=$(xx-info)-strip make BUILDTAGS="$(grep -oP "^BUILDTAGS := \K.*" Makefile | sed -e s/libpathrs//)" static && \ xx-verify --static runc && cp -v -a runc /out/runc.${TARGETARCH} FROM build-base AS build-bypass4netns ARG BYPASS4NETNS_VERSION ARG TARGETARCH RUN git clone --quiet --depth 1 --branch "${BYPASS4NETNS_VERSION%%@*}" https://github.com/rootless-containers/bypass4netns.git /go/src/github.com/rootless-containers/bypass4netns WORKDIR /go/src/github.com/rootless-containers/bypass4netns RUN git-checkout-tag-with-hash.sh ${BYPASS4NETNS_VERSION} && \ mkdir -p /out/${TARGETARCH} ENV CGO_ENABLED=1 RUN GO=xx-go make static && \ xx-verify --static bypass4netns && cp -a bypass4netns bypass4netnsd /out/${TARGETARCH} FROM build-base AS build-gomodjail ARG GOMODJAIL_VERSION ARG TARGETARCH RUN git clone --quiet --depth 1 --branch "${GOMODJAIL_VERSION%%@*}" https://github.com/AkihiroSuda/gomodjail.git /go/src/github.com/AkihiroSuda/gomodjail WORKDIR /go/src/github.com/AkihiroSuda/gomodjail RUN git-checkout-tag-with-hash.sh ${GOMODJAIL_VERSION} && \ mkdir -p /out/${TARGETARCH} RUN GO=xx-go make STATIC=1 && \ xx-verify --static _output/bin/gomodjail && cp -a _output/bin/gomodjail /out/${TARGETARCH} FROM build-base AS build-kubo ARG KUBO_VERSION ARG TARGETARCH RUN git clone --quiet --depth 1 --branch "${KUBO_VERSION%%@*}" https://github.com/ipfs/kubo.git /go/src/github.com/ipfs/kubo WORKDIR /go/src/github.com/ipfs/kubo RUN git-checkout-tag-with-hash.sh ${KUBO_VERSION} && \ mkdir -p /out/${TARGETARCH} ENV CGO_ENABLED=0 RUN xx-go --wrap && \ make build && \ xx-verify --static cmd/ipfs/ipfs && cp -a cmd/ipfs/ipfs /out/${TARGETARCH} FROM build-base AS build-minimal RUN BINDIR=/out/bin make binaries install # We do not set CMD to `go test` here, because it requires systemd FROM build-base AS build-dependencies ARG TARGETARCH ENV GOARCH=${TARGETARCH} COPY ./Dockerfile.d/SHA256SUMS.d/ /SHA256SUMS.d WORKDIR /nowhere RUN echo "${TARGETARCH:-amd64}" | sed -e s/amd64/x86_64/ -e s/arm64/aarch64/ | tee /target_uname_m RUN mkdir -p /out/share/doc/nerdctl-full && touch /out/share/doc/nerdctl-full/README.md ARG CONTAINERD_VERSION COPY --from=build-containerd /out/${TARGETARCH:-amd64}/* /out/bin/ COPY --from=build-containerd /out/containerd.service /out/lib/systemd/system/containerd.service RUN echo "- containerd: ${CONTAINERD_VERSION%%@*}" >> /out/share/doc/nerdctl-full/README.md ARG RUNC_VERSION COPY --from=build-runc /out/runc.${TARGETARCH:-amd64} /out/bin/runc RUN echo "- runc: ${RUNC_VERSION%%@*}" >> /out/share/doc/nerdctl-full/README.md ARG CNI_PLUGINS_VERSION RUN CNI_PLUGINS_VERSION=${CNI_PLUGINS_VERSION%%@*}; \ fname="cni-plugins-${TARGETOS:-linux}-${TARGETARCH:-amd64}-${CNI_PLUGINS_VERSION}.tgz" && \ curl -o "${fname}" -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/containernetworking/plugins/releases/download/${CNI_PLUGINS_VERSION}/${fname}" && \ grep "${fname}" "/SHA256SUMS.d/cni-plugins-${CNI_PLUGINS_VERSION}" | sha256sum -c && \ mkdir -p /out/libexec/cni && \ tar xzf "${fname}" -C /out/libexec/cni && \ rm -f "${fname}" && \ echo "- CNI plugins: ${CNI_PLUGINS_VERSION}" >> /out/share/doc/nerdctl-full/README.md ARG BUILDKIT_VERSION RUN BUILDKIT_VERSION=${BUILDKIT_VERSION%%@*}; \ fname="buildkit-${BUILDKIT_VERSION}.${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \ curl -o "${fname}" -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/moby/buildkit/releases/download/${BUILDKIT_VERSION}/${fname}" && \ grep "${fname}" "/SHA256SUMS.d/buildkit-${BUILDKIT_VERSION}" | sha256sum -c && \ tar xzf "${fname}" -C /out && \ rm -f "${fname}" /out/bin/buildkit-qemu-* /out/bin/buildkit-cni-* /out/bin/buildkit-runc && \ for f in /out/libexec/cni/*; do [ -x "$f" ] && [ -f "$f" ] && ln -s ../libexec/cni/$(basename $f) /out/bin/buildkit-cni-$(basename $f); done && \ echo "- BuildKit: ${BUILDKIT_VERSION}" >> /out/share/doc/nerdctl-full/README.md # NOTE: github.com/moby/buildkit/examples/systemd is not included in BuildKit v0.8.x, will be included in v0.9.x RUN cd /out/lib/systemd/system && \ sedcomm='s@bin/containerd@bin/buildkitd@g; s@(Description|Documentation)=.*@@' && \ sed -E "${sedcomm}" containerd.service > buildkit.service && \ echo "" >> buildkit.service && \ echo "# This file was converted from containerd.service, with \`sed -E '${sedcomm}'\`" >> buildkit.service ARG STARGZ_SNAPSHOTTER_VERSION RUN --mount=type=secret,id=github_token,env=GITHUB_TOKEN \ STARGZ_SNAPSHOTTER_VERSION=${STARGZ_SNAPSHOTTER_VERSION%%@*}; \ fname="stargz-snapshotter-${STARGZ_SNAPSHOTTER_VERSION}-${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \ curl -o "${fname}" -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/containerd/stargz-snapshotter/releases/download/${STARGZ_SNAPSHOTTER_VERSION}/${fname}" && \ http::helper github::file containerd/stargz-snapshotter script/config/etc/systemd/system/stargz-snapshotter.service "${STARGZ_SNAPSHOTTER_VERSION}" > "stargz-snapshotter.service" && \ grep "${fname}" "/SHA256SUMS.d/stargz-snapshotter-${STARGZ_SNAPSHOTTER_VERSION}" | sha256sum -c - && \ grep "stargz-snapshotter.service" "/SHA256SUMS.d/stargz-snapshotter-${STARGZ_SNAPSHOTTER_VERSION}" | sha256sum -c - && \ tar xzf "${fname}" -C /out/bin && \ rm -f "${fname}" /out/bin/stargz-store && \ mv stargz-snapshotter.service /out/lib/systemd/system/stargz-snapshotter.service && \ echo "- Stargz Snapshotter: ${STARGZ_SNAPSHOTTER_VERSION}" >> /out/share/doc/nerdctl-full/README.md ARG IMGCRYPT_VERSION RUN git clone --quiet --depth 1 --branch "${IMGCRYPT_VERSION%%@*}" https://github.com/containerd/imgcrypt.git /go/src/github.com/containerd/imgcrypt && \ cd /go/src/github.com/containerd/imgcrypt && \ git-checkout-tag-with-hash.sh "${IMGCRYPT_VERSION}" && \ CGO_ENABLED=0 make && DESTDIR=/out make install && \ echo "- imgcrypt: ${IMGCRYPT_VERSION%%@*}" >> /out/share/doc/nerdctl-full/README.md ARG SLIRP4NETNS_VERSION RUN SLIRP4NETNS_VERSION=${SLIRP4NETNS_VERSION%%@*}; \ fname="slirp4netns-$(cat /target_uname_m)" && \ curl -o "${fname}" -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/rootless-containers/slirp4netns/releases/download/${SLIRP4NETNS_VERSION}/${fname}" && \ grep "${fname}" "/SHA256SUMS.d/slirp4netns-${SLIRP4NETNS_VERSION}" | sha256sum -c && \ mv "${fname}" /out/bin/slirp4netns && \ chmod +x /out/bin/slirp4netns && \ echo "- slirp4netns: ${SLIRP4NETNS_VERSION}" >> /out/share/doc/nerdctl-full/README.md ARG BYPASS4NETNS_VERSION COPY --from=build-bypass4netns /out/${TARGETARCH:-amd64}/* /out/bin/ RUN echo "- bypass4netns: ${BYPASS4NETNS_VERSION%%@*}" >> /out/share/doc/nerdctl-full/README.md ARG FUSE_OVERLAYFS_VERSION RUN FUSE_OVERLAYFS_VERSION=${FUSE_OVERLAYFS_VERSION%%@*}; \ fname="fuse-overlayfs-$(cat /target_uname_m)" && \ curl -o "${fname}" -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/containers/fuse-overlayfs/releases/download/${FUSE_OVERLAYFS_VERSION}/${fname}" && \ grep "${fname}" "/SHA256SUMS.d/fuse-overlayfs-${FUSE_OVERLAYFS_VERSION}" | sha256sum -c && \ mv "${fname}" /out/bin/fuse-overlayfs && \ chmod +x /out/bin/fuse-overlayfs && \ echo "- fuse-overlayfs: ${FUSE_OVERLAYFS_VERSION}" >> /out/share/doc/nerdctl-full/README.md ARG CONTAINERD_FUSE_OVERLAYFS_VERSION RUN CONTAINERD_FUSE_OVERLAYFS_VERSION=${CONTAINERD_FUSE_OVERLAYFS_VERSION%%@*}; \ fname="containerd-fuse-overlayfs-${CONTAINERD_FUSE_OVERLAYFS_VERSION##*v}-${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \ curl -o "${fname}" -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/containerd/fuse-overlayfs-snapshotter/releases/download/${CONTAINERD_FUSE_OVERLAYFS_VERSION}/${fname}" && \ grep "${fname}" "/SHA256SUMS.d/containerd-fuse-overlayfs-${CONTAINERD_FUSE_OVERLAYFS_VERSION}" | sha256sum -c && \ tar xzf "${fname}" -C /out/bin && \ rm -f "${fname}" && \ echo "- containerd-fuse-overlayfs: ${CONTAINERD_FUSE_OVERLAYFS_VERSION}" >> /out/share/doc/nerdctl-full/README.md ARG TINI_VERSION RUN TINI_VERSION=${TINI_VERSION%%@*}; \ fname="tini-static-${TARGETARCH:-amd64}" && \ curl -o "${fname}" -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/${fname}" && \ grep "${fname}" "/SHA256SUMS.d/tini-${TINI_VERSION}" | sha256sum -c && \ cp -a "${fname}" /out/bin/tini && chmod +x /out/bin/tini && \ echo "- Tini: ${TINI_VERSION}" >> /out/share/doc/nerdctl-full/README.md ARG BUILDG_VERSION # FIXME: this is a mildly-confusing approach. Buildkit will perform some "smart" replacement at build time and output # confusing debugging information, eg: BUILDG_VERSION will appear as if the original ARG value was used. RUN BUILDG_VERSION=${BUILDG_VERSION%%@*}; \ fname="buildg-${BUILDG_VERSION}-${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \ curl -o "${fname}" -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/ktock/buildg/releases/download/${BUILDG_VERSION}/${fname}" && \ grep "${fname}" "/SHA256SUMS.d/buildg-${BUILDG_VERSION}" | sha256sum -c && \ tar xzf "${fname}" -C /out/bin && \ rm -f "${fname}" && \ echo "- buildg: ${BUILDG_VERSION}" >> /out/share/doc/nerdctl-full/README.md ARG ROOTLESSKIT_VERSION RUN ROOTLESSKIT_VERSION=${ROOTLESSKIT_VERSION%%@*}; \ fname="rootlesskit-$(cat /target_uname_m).tar.gz" && \ curl -o "${fname}" -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/rootless-containers/rootlesskit/releases/download/${ROOTLESSKIT_VERSION}/${fname}" && \ grep "${fname}" "/SHA256SUMS.d/rootlesskit-${ROOTLESSKIT_VERSION}" | sha256sum -c && \ tar xzf "${fname}" -C /out/bin && \ rm -f "${fname}" /out/bin/rootlesskit-docker-proxy && \ echo "- RootlessKit: ${ROOTLESSKIT_VERSION}" >> /out/share/doc/nerdctl-full/README.md ARG GOMODJAIL_VERSION COPY --from=build-gomodjail /out/${TARGETARCH:-amd64}/* /out/bin/ RUN echo "- gomodjail: ${GOMODJAIL_VERSION}" >> /out/share/doc/nerdctl-full/README.md ARG CONTAINERIZED_SYSTEMD_VERSION RUN --mount=type=secret,id=github_token,env=GITHUB_TOKEN \ http::helper github::file AkihiroSuda/containerized-systemd docker-entrypoint.sh "${CONTAINERIZED_SYSTEMD_VERSION}" > /docker-entrypoint.sh && \ chmod +x /docker-entrypoint.sh RUN echo "" >> /out/share/doc/nerdctl-full/README.md && \ echo "## License" >> /out/share/doc/nerdctl-full/README.md && \ echo "- bin/slirp4netns: [GNU GENERAL PUBLIC LICENSE, Version 2](https://github.com/rootless-containers/slirp4netns/blob/${SLIRP4NETNS_VERSION%%@*}/COPYING)" >> /out/share/doc/nerdctl-full/README.md && \ echo "- bin/fuse-overlayfs: [GNU GENERAL PUBLIC LICENSE, Version 2](https://github.com/containers/fuse-overlayfs/blob/${FUSE_OVERLAYFS_VERSION%%@*}/COPYING)" >> /out/share/doc/nerdctl-full/README.md && \ echo "- bin/{runc,bypass4netns,bypass4netnsd}: Apache License 2.0, statically linked with libseccomp ([LGPL 2.1](https://github.com/seccomp/libseccomp/blob/main/LICENSE), source code available at https://github.com/seccomp/libseccomp/)" >> /out/share/doc/nerdctl-full/README.md && \ echo "- bin/tini: [MIT License](https://github.com/krallin/tini/blob/${TINI_VERSION%%@*}/LICENSE)" >> /out/share/doc/nerdctl-full/README.md && \ echo "- Other files: [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0)" >> /out/share/doc/nerdctl-full/README.md FROM build-dependencies AS build-full COPY . /go/src/github.com/containerd/nerdctl RUN { echo "# nerdctl (full distribution)"; echo "- nerdctl: $(cd /go/src/github.com/containerd/nerdctl && git describe --tags)"; cat /out/share/doc/nerdctl-full/README.md; } > /out/share/doc/nerdctl-full/README.md.new; mv /out/share/doc/nerdctl-full/README.md.new /out/share/doc/nerdctl-full/README.md WORKDIR /go/src/github.com/containerd/nerdctl RUN BINDIR=/out/bin make binaries install # FIXME: `gomodjail pack` depends on QEMU for non-native architecture # TODO: gomodjail should provide a plain shell script that utilizes `zip(1)` for packing the self-extract archive, without running `gomodjail pack`.. RUN /out/bin/gomodjail pack --go-mod=/go/src/github.com/containerd/nerdctl/go.mod /out/bin/nerdctl && \ cp -a nerdctl.gomodjail /out/bin/ COPY README.md /out/share/doc/nerdctl/ COPY docs /out/share/doc/nerdctl/docs RUN (cd /out && find ! -type d | sort | xargs sha256sum > /tmp/SHA256SUMS ) && \ mv /tmp/SHA256SUMS /out/share/doc/nerdctl-full/SHA256SUMS && \ chown -R 0:0 /out FROM scratch AS out-full COPY --from=build-full /out / FROM ubuntu:${UBUNTU_VERSION} AS base # fuse3 is required by stargz snapshotter RUN apt-get update -qq && apt-get install -qq -y --no-install-recommends \ apparmor \ bash-completion \ ca-certificates curl \ iproute2 iptables \ dbus dbus-user-session systemd systemd-sysv \ fuse3 COPY --from=build-full /docker-entrypoint.sh /docker-entrypoint.sh COPY --from=out-full / /usr/local/ RUN perl -pi -e 's/multi-user.target/docker-entrypoint.target/g' /usr/local/lib/systemd/system/*.service && \ systemctl enable containerd buildkit stargz-snapshotter && \ mkdir -p /etc/bash_completion.d && \ nerdctl completion bash >/etc/bash_completion.d/nerdctl && \ mkdir -p -m 0755 /etc/cni COPY ./Dockerfile.d/etc_containerd_config.toml /etc/containerd/config.toml COPY ./Dockerfile.d/etc_buildkit_buildkitd.toml /etc/buildkit/buildkitd.toml VOLUME /var/lib/containerd VOLUME /var/lib/buildkit VOLUME /var/lib/containerd-stargz-grpc VOLUME /var/lib/nerdctl ENTRYPOINT ["/docker-entrypoint.sh"] CMD ["bash", "--login", "-i"] FROM base AS test-integration ARG DEBIAN_FRONTEND=noninteractive # `expect` package contains `unbuffer(1)`, which is used for emulating TTY for testing # `jq` is required to generate test summaries RUN apt-get update -qq && apt-get install -qq --no-install-recommends \ software-properties-common \ gnupg \ gpg-agent \ ca-certificates && \ add-apt-repository ppa:criu/ppa && \ apt-get update -qq && apt-get install -qq --no-install-recommends \ expect \ jq \ git \ make \ criu # We wouldn't need this if Docker Hub could have "golang:${GO_VERSION}-ubuntu" COPY --from=build-base /usr/local/go /usr/local/go ARG TARGETARCH ENV PATH=/usr/local/go/bin:$PATH ARG GOTESTSUM_VERSION RUN GOBIN=/usr/local/bin go install gotest.tools/gotestsum@${GOTESTSUM_VERSION} COPY . /go/src/github.com/containerd/nerdctl WORKDIR /go/src/github.com/containerd/nerdctl VOLUME /tmp ENV CGO_ENABLED=0 # copy cosign binary for integration test COPY --from=ghcr.io/sigstore/cosign/cosign:v2.2.3@sha256:8fc9cad121611e8479f65f79f2e5bea58949e8a87ffac2a42cb99cf0ff079ba7 /ko-app/cosign /usr/local/bin/cosign # installing soci for integration test ARG SOCI_SNAPSHOTTER_VERSION RUN fname="soci-snapshotter-${SOCI_SNAPSHOTTER_VERSION}-${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \ curl -o "${fname}" -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/awslabs/soci-snapshotter/releases/download/v${SOCI_SNAPSHOTTER_VERSION}/${fname}" && \ tar -C /usr/local/bin -xvf "${fname}" soci soci-snapshotter-grpc && \ mkdir -p /etc/soci-snapshotter-grpc && \ touch /etc/soci-snapshotter-grpc/config.toml && \ echo "\n[pull_modes]\n [pull_modes.soci_v1]\n enable = true" >> /etc/soci-snapshotter-grpc/config.toml # enable offline ipfs for integration test COPY --from=build-kubo /out/${TARGETARCH:-amd64}/* /usr/local/bin/ COPY ./Dockerfile.d/test-integration-etc_containerd-stargz-grpc_config.toml /etc/containerd-stargz-grpc/config.toml COPY ./Dockerfile.d/test-integration-ipfs-offline.service /usr/local/lib/systemd/system/ COPY ./Dockerfile.d/test-integration-buildkit-nerdctl-test.service /usr/local/lib/systemd/system/ COPY ./Dockerfile.d/test-integration-soci-snapshotter.service /usr/local/lib/systemd/system/ RUN cp /usr/local/bin/tini /usr/local/bin/tini-custom # using test integration containerd config COPY ./Dockerfile.d/test-integration-etc_containerd_config.toml /etc/containerd/config.toml # install ipfs service. avoid using 5001(api)/8080(gateway) which are reserved by tests. RUN systemctl enable test-integration-ipfs-offline test-integration-buildkit-nerdctl-test test-integration-soci-snapshotter && \ ipfs init && \ ipfs config Addresses.API "/ip4/127.0.0.1/tcp/5888" && \ ipfs config Addresses.Gateway "/ip4/127.0.0.1/tcp/5889" # install nydus components ARG NYDUS_VERSION RUN curl -o nydus-static.tgz -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/dragonflyoss/image-service/releases/download/${NYDUS_VERSION}/nydus-static-${NYDUS_VERSION}-linux-${TARGETARCH}.tgz" && \ tar xzf nydus-static.tgz && \ mv nydus-static/nydus-image nydus-static/nydusd nydus-static/nydusify /usr/bin/ && \ rm nydus-static.tgz CMD ["./hack/test-integration.sh"] FROM test-integration AS test-integration-rootless # Install SSH for creating systemd user session. # (`sudo` does not work for this purpose, # OTOH `machinectl shell` can create the session but does not propagate exit code) RUN apt-get update -qq && apt-get install -qq --no-install-recommends \ uidmap \ openssh-server \ openssh-client # TODO: update containerized-systemd to enable sshd by default, or allow `systemctl wants ssh` here RUN ssh-keygen -q -t rsa -f /root/.ssh/id_rsa -N '' && \ useradd -m -s /bin/bash rootless && \ mkdir -p -m 0700 /home/rootless/.ssh && \ cp -a /root/.ssh/id_rsa.pub /home/rootless/.ssh/authorized_keys && \ mkdir -p /home/rootless/.local/share && \ chown -R rootless:rootless /home/rootless COPY ./Dockerfile.d/etc_systemd_system_user@.service.d_delegate.conf /etc/systemd/system/user@.service.d/delegate.conf # ipfs daemon for rootless containerd will be enabled in /test-integration-rootless.sh RUN systemctl disable test-integration-ipfs-offline VOLUME /home/rootless/.local/share COPY ./Dockerfile.d/test-integration-rootless.sh / RUN chmod a+rx /test-integration-rootless.sh CMD ["/test-integration-rootless.sh", "./hack/test-integration.sh"] # test for CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns FROM test-integration-rootless AS test-integration-rootless-port-slirp4netns COPY ./Dockerfile.d/home_rootless_.config_systemd_user_containerd.service.d_port-slirp4netns.conf /home/rootless/.config/systemd/user/containerd.service.d/port-slirp4netns.conf RUN chown -R rootless:rootless /home/rootless/.config FROM base AS demo ================================================ FILE: Dockerfile.d/SHA256SUMS.d/SHA256SUMS ================================================ 3edc52986c442576da856a66b59a61d16cf765359712c5ecf2d147c69f0df6e9 rootlesskit-aarch64.tar.gz 6ce9eed50f9e12f18f3e5197cf93d226bc9290185880a626ab186244593d2eed rootlesskit-armv7l.tar.gz 730ef884439e2fe15551218b05d5c4f96d96d6945db8ad7e89b1d12946408a8d rootlesskit-ppc64le.tar.gz 05da5803d0f023ec51112bbdf8967a3e12ae19544f8c101a7f08f3bb9c6548fd rootlesskit-riscv64.tar.gz 199f6bfcd0495d0b944d95f70e6fa1177ace16d801e2693fdd86fdaafa69b01a rootlesskit-s390x.tar.gz afc52e9fa2f7a2d4bb692f675cf3d2f70f3a184f02593e8b18cfbbbc34cbfd41 rootlesskit-x86_64.tar.gz ================================================ FILE: Dockerfile.d/SHA256SUMS.d/buildg-v0.5.3 ================================================ cf4c40c58ca795eeb6e75e2c6a0e5bb3a6a9c0623d51bc3b85163e5d483eeade buildg-full-v0.5.3-linux-amd64.tar.gz 47c479f2e5150c9c76294fa93a03ad20e5928f4315bf52ca8432bfb6707d4276 buildg-full-v0.5.3-linux-arm64.tar.gz c289a454ae8673ff99acf56dec9ba97274c20d2015e80f7ac3b8eb8e4f77888f buildg-v0.5.3-linux-amd64.tar.gz b2e244250ce7ea5c090388f2025a9c546557861d25bba7b0666aa512f01fa6cd buildg-v0.5.3-linux-arm64.tar.gz ================================================ FILE: Dockerfile.d/SHA256SUMS.d/buildkit-v0.26.3 ================================================ 249ae16ba4be59fadb51a49ff4d632bbf37200e2b6e187fa8574f0f1bce8166b buildkit-v0.26.3.linux-amd64.tar.gz a98829f1b1b9ec596eb424dd03f03b9c7b596edac83e6700adf83ba0cb0d5f80 buildkit-v0.26.3.linux-arm64.tar.gz ================================================ FILE: Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.9.0 ================================================ 58c03705426e929658f45a851df15a86d06ef680cacbf3f2dc127731ca265c28 cni-plugins-linux-amd64-v1.9.0.tgz 2596ef56329dd1269026f46b8df262f09ba43c92dbfb940e1e69fbccccd30a29 cni-plugins-linux-arm64-v1.9.0.tgz ================================================ FILE: Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.7 ================================================ d54148043c22381af89cec2a167431e40668716404a1eb682ca69dfb890376f3 containerd-fuse-overlayfs-2.1.7-linux-amd64.tar.gz a301030391d51356065f628b5e6e5a5a8c55f1978289eb71d8f5284af7a81eda containerd-fuse-overlayfs-2.1.7-linux-arm-v7.tar.gz 94ed6c2c3bece42e0c789ea056565b64fe487de4644121ee0dfb8acd8ef9369c containerd-fuse-overlayfs-2.1.7-linux-arm64.tar.gz 1bfb1f86894b640781d837ec0f66997222b419532fae730579140dbc1c7ea858 containerd-fuse-overlayfs-2.1.7-linux-ppc64le.tar.gz 9f2ef69b06229f5357f3fc23524922cea6616663ff220979a110a7742aaffee6 containerd-fuse-overlayfs-2.1.7-linux-riscv64.tar.gz 03f61035cef5fff33c5084c55f133d0340597520d8d12112970609dff0bd1e7a containerd-fuse-overlayfs-2.1.7-linux-s390x.tar.gz ================================================ FILE: Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.16 ================================================ 6c9ee54166fe7d33ebbfb085812585441f22ebe2a24a868d0a878d3127bcb89e fuse-overlayfs-aarch64 fc2a73ace8eb6a0553204532de615d782cb98d86deeb0fa7b5d14347d0b95823 fuse-overlayfs-armv7l 3c07b76b432a5b4e5e0ccd986919b478d096701178617175b0c71bcce7c6f6a0 fuse-overlayfs-ppc64le 404fd7a762255d554e70849612fb6979639e1eb23a740487dbe3bac2bccc37c1 fuse-overlayfs-riscv64 9e96cfe091b4342b8de3e239a96d5fecfb8692fbb4a201c256790c270526fd1b fuse-overlayfs-s390x 30c6b9e192600d6854e13397974c709d7cabd980b7d1a4d67defd8eb69677e91 fuse-overlayfs-x86_64 ================================================ FILE: Dockerfile.d/SHA256SUMS.d/rootlesskit-v1.1.1 ================================================ b74c577abd6ad721e0b7e10a74f4c5ac26cb3afe005ad3d28d4d7912c356079f rootlesskit-aarch64.tar.gz 95c27e6808c942c67ab93d94e37bada3a62cfc47de848101889f8e3ba5c9f7dd rootlesskit-armv7l.tar.gz df35c74cd030e1b3978f28d1cb7c909da2ab962fb0c9369463d43a89b9f16cc2 rootlesskit-ppc64le.tar.gz 79af3e96e9d6deddc5faa4680de7e28120ae333386c48a30e79fe156f17bad9b rootlesskit-riscv64.tar.gz 32da9a11b67340ff498de8a3268673277a1e1d9e9d8d5f619bbf09305beaaa6c rootlesskit-s390x.tar.gz 3c83affbb405cafe2d32e2e24462af9b4dcfa19e3809030012ad0d4e3fd49e8f rootlesskit-x86_64.tar.gz ================================================ FILE: Dockerfile.d/SHA256SUMS.d/rootlesskit-v2.3.6 ================================================ 3edc52986c442576da856a66b59a61d16cf765359712c5ecf2d147c69f0df6e9 rootlesskit-aarch64.tar.gz 6ce9eed50f9e12f18f3e5197cf93d226bc9290185880a626ab186244593d2eed rootlesskit-armv7l.tar.gz 730ef884439e2fe15551218b05d5c4f96d96d6945db8ad7e89b1d12946408a8d rootlesskit-ppc64le.tar.gz 05da5803d0f023ec51112bbdf8967a3e12ae19544f8c101a7f08f3bb9c6548fd rootlesskit-riscv64.tar.gz 199f6bfcd0495d0b944d95f70e6fa1177ace16d801e2693fdd86fdaafa69b01a rootlesskit-s390x.tar.gz afc52e9fa2f7a2d4bb692f675cf3d2f70f3a184f02593e8b18cfbbbc34cbfd41 rootlesskit-x86_64.tar.gz ================================================ FILE: Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.3.3 ================================================ d0e6a13342efbedb8b7454629a0e9ce9b7a937c261034c85f46ed81af76307d8 SOURCE_DATE_EPOCH 1ca9d2f5f1fb4beb91f354653e5dad35b95c049afb264268d99a96ff2a10d903 slirp4netns-aarch64 3e209d1c56fccbe627a038d311b233c15e8d914b30f9b981b5ed78b98e836859 slirp4netns-armv7l 4d1003a98103ee170c0fcd4aad8a5e0ba7aa2e70fbca883cbb6a39f40447c8da slirp4netns-ppc64le 06a13b398d88120097b20dace966d7dd5e2fbfd284b95a086347808df392200e slirp4netns-riscv64 23d4a206edd6d3fc9c86f8b05c0881ff77a607b8d471f20964ad9f9c3f3176b1 slirp4netns-s390x 5618887b671a30a2f7548f2bdf7fba98a53981abc80cfd3183cd28b4dc8b2b97 slirp4netns-x86_64 ================================================ FILE: Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.1 ================================================ f8f106a61b9fc797a6336d6c06435cdbf8b896f3f49fdc5288e08e87dff6bbdf stargz-snapshotter-v0.18.1-linux-amd64.tar.gz 643d04f5e97e83606b9ee129c2c33513df13a091dbc1dc084256d13a1034b749 stargz-snapshotter-v0.18.1-linux-arm64.tar.gz f1cf855870af16a653d8acb9daa3edf84687c2c05323cb958f078fb148af3eec stargz-snapshotter.service ================================================ FILE: Dockerfile.d/SHA256SUMS.d/tini-v0.19.0 ================================================ c5b0666b4cb676901f90dfcb37106783c5fe2077b04590973b885950611b30ee tini-static-amd64 eae1d3aa50c48fb23b8cbdf4e369d0910dfc538566bfd09df89a774aa84a48b9 tini-static-arm64 ================================================ FILE: Dockerfile.d/etc_buildkit_buildkitd.toml ================================================ [worker.oci] enabled = false [worker.containerd] enabled = true namespace = "default" ================================================ FILE: Dockerfile.d/etc_containerd_config.toml ================================================ version = 2 # Enable stargz snapshotter [proxy_plugins] [proxy_plugins.stargz] type = "snapshot" address = "/run/containerd-stargz-grpc/containerd-stargz-grpc.sock" [proxy_plugins.stargz.exports] root = "/var/lib/containerd-stargz-grpc/" enable_remote_snapshot_annotations = "true" [[plugins."io.containerd.transfer.v1.local".unpack_config]] platform = "linux" snapshotter = "overlayfs" [[plugins."io.containerd.transfer.v1.local".unpack_config]] platform = "linux" snapshotter = "stargz" ================================================ FILE: Dockerfile.d/etc_systemd_system_user@.service.d_delegate.conf ================================================ [Service] Delegate=yes ================================================ FILE: Dockerfile.d/home_rootless_.config_systemd_user_containerd.service.d_port-slirp4netns.conf ================================================ [Service] # Change the port driver from "builtin" to "slirp4netns". Only used in CI. Environment="CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns" ================================================ FILE: Dockerfile.d/test-integration-buildkit-nerdctl-test.service ================================================ # Copyright The containerd Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Copied from released buildkit.service [Unit] After=network.target local-fs.target Description=buildkit daemon for integration test (namespace: nerdctl-test) [Service] ExecStartPre=-/sbin/modprobe overlay ExecStart=/usr/local/bin/buildkitd --oci-worker=false --containerd-worker=true --addr="unix:///run/buildkit-nerdctl-test/buildkitd.sock" --root=/var/lib/buildkit-nerdctl-test --containerd-worker-namespace=nerdctl-test Type=notify Delegate=yes KillMode=process Restart=always RestartSec=5 # Having non-zero Limit*s causes performance problems due to accounting overhead # in the kernel. We recommend using cgroups to do container-local accounting. LimitNPROC=infinity LimitCORE=infinity LimitNOFILE=infinity # Comment TasksMax if your systemd version does not supports it. # Only systemd 226 and above support this version. TasksMax=infinity OOMScoreAdjust=-999 [Install] WantedBy=docker-entrypoint.target ================================================ FILE: Dockerfile.d/test-integration-etc_containerd-stargz-grpc_config.toml ================================================ version = 2 # Enable IPFS ipfs = true ================================================ FILE: Dockerfile.d/test-integration-etc_containerd_config.toml ================================================ version = 2 # Enable stargz snapshotter [proxy_plugins] [proxy_plugins.stargz] type = "snapshot" address = "/run/containerd-stargz-grpc/containerd-stargz-grpc.sock" [proxy_plugins.stargz.exports] root = "/var/lib/containerd-stargz-grpc/" enable_remote_snapshot_annotations = "true" # Enable soci snapshotter [proxy_plugins.soci] type = "snapshot" address = "/run/soci-snapshotter-grpc/soci-snapshotter-grpc.sock" [proxy_plugins.soci.exports] root = "/var/lib/soci-snapshotter-grpc" enable_remote_snapshot_annotations = "true" [[plugins."io.containerd.transfer.v1.local".unpack_config]] platform = "linux" snapshotter = "overlayfs" [[plugins."io.containerd.transfer.v1.local".unpack_config]] platform = "linux" snapshotter = "soci" [[plugins."io.containerd.transfer.v1.local".unpack_config]] platform = "linux" snapshotter = "stargz" ================================================ FILE: Dockerfile.d/test-integration-ipfs-offline.service ================================================ [Unit] Description=ipfs daemon for integration test (offline) [Service] ExecStart=ipfs daemon --init --offline Environment=IPFS_PATH="%h/.ipfs" [Install] WantedBy=docker-entrypoint.target ================================================ FILE: Dockerfile.d/test-integration-rootless.sh ================================================ #!/bin/bash # Copyright The containerd Authors. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -eux -o pipefail if [[ "$(id -u)" = "0" ]]; then # Ensure securityfs is mounted for apparmor to work if ! mountpoint -q /sys/kernel/security; then mount -tsecurityfs securityfs /sys/kernel/security fi if [ -e /sys/kernel/security/apparmor/profiles ]; then # Load the "nerdctl-default" profile for TestRunApparmor nerdctl apparmor load fi : "${WORKAROUND_ISSUE_622:=}" if [[ "$WORKAROUND_ISSUE_622" != "" ]]; then touch /workaround-issue-622 fi # Switch to the rootless user via SSH systemctl start ssh exec ssh -o StrictHostKeyChecking=no rootless@localhost "$0" "$@" else containerd-rootless-setuptool.sh install if grep -q "options use-vc" /etc/resolv.conf; then containerd-rootless-setuptool.sh nsenter -- sh -euc 'echo "options use-vc" >>/etc/resolv.conf' fi if [[ -e /workaround-issue-622 ]]; then echo "WORKAROUND_ISSUE_622: Not enabling BuildKit (https://github.com/containerd/nerdctl/issues/622)" >&2 else CONTAINERD_NAMESPACE="nerdctl-test" containerd-rootless-setuptool.sh install-buildkit-containerd fi containerd-rootless-setuptool.sh install-stargz if [ ! -f "/home/rootless/.config/containerd/config.toml" ] ; then echo "version = 2" > /home/rootless/.config/containerd/config.toml fi cat <>/home/rootless/.config/containerd/config.toml [proxy_plugins] [proxy_plugins."stargz"] type = "snapshot" address = "/run/user/$(id -u)/containerd-stargz-grpc/containerd-stargz-grpc.sock" [proxy_plugins.stargz.exports] root = "/home/rootless/.local/share/containerd-stargz-grpc/" enable_remote_snapshot_annotations = "true" [[plugins."io.containerd.transfer.v1.local".unpack_config]] platform = "linux" snapshotter = "overlayfs" [[plugins."io.containerd.transfer.v1.local".unpack_config]] platform = "linux" snapshotter = "stargz" EOF systemctl --user restart containerd.service containerd-rootless-setuptool.sh -- install-ipfs --init --offline # offline ipfs daemon for testing echo "ipfs = true" >>/home/rootless/.config/containerd-stargz-grpc/config.toml systemctl --user restart stargz-snapshotter.service export IPFS_PATH="/home/rootless/.local/share/ipfs" containerd-rootless-setuptool.sh install-bypass4netnsd # Once ssh-ed, we lost the Dockerfile working dir, so, get back in the nerdctl checkout cd /go/src/github.com/containerd/nerdctl # We also lose the PATH (and SendEnv=PATH would require sshd config changes) exec env PATH="/usr/local/go/bin:$PATH" "$@" fi ================================================ FILE: Dockerfile.d/test-integration-soci-snapshotter.service ================================================ [Unit] Description=soci snapshotter containerd plugin for integration test Documentation=https://github.com/awslabs/soci-snapshotter After=network.target Before=containerd.service [Service] Type=notify ExecStartPre=/bin/bash -c 'mkdir -p /var/lib/soci-snapshotter-grpc && mount -t tmpfs none /var/lib/soci-snapshotter-grpc' ExecStart=/usr/local/bin/soci-snapshotter-grpc Restart=always RestartSec=5 [Install] WantedBy=docker-entrypoint.target ================================================ FILE: EMERITUS.md ================================================ See [`MAINTAINERS`](./MAINTAINERS) for the current active maintainers. - - - # nerdctl Emeritus Maintainers ## Committers ### Ye Sijun ([@junnplus](https://github.com/junnplus)) Ye Sijun (GitHub ID [@junnplus](https://github.com/junnplus)) served as a Committer of nerdctl from November 2022 to June 2024. Prior to his role as a Committer, Sijun served as a Reviewer since February 2022. Sijun has made [significant improvements](https://github.com/containerd/nerdctl/pulls?q=author%3Ajunnplus+) especially to `nerdctl compose`, IPAM, and cosign integration. ## Reviewers ### Hanchin Hsieh ([@yuchanns](https://github.com/yuchanns)) Hanchin Hsieh (GitHub ID [@yuchanns](https://github.com/yuchanns)) served as a Reviewer of nerdctl from November 2022 to June 2024. Hanchin has made significant contributions such as the addition of [syslog driver](https://github.com/containerd/nerdctl/pull/1377) and [IPv6 networking](https://github.com/containerd/nerdctl/pull/1558). ### Manu Gupta ([@manugupt1](https://github.com/manugupt1)) Manu Gupta (GitHub ID [@manugupt1](https://github.com/manugupt1)) served as a Reviewer of nerdctl from 2022 to August 2025. Manu has made [significant improvements](https://github.com/containerd/nerdctl/pulls?q=author%3Amanugupt1+) especially to image and volume management, container runtime features, build system enhancements, and CI/CD infrastructure. Notable contributions include image filtering capabilities, volume size inspection, Docker Compose enhancements, and multi-architecture build support. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: MAINTAINERS ================================================ # nerdctl maintainers # # As a containerd sub-project, containerd maintainers are also included from https://github.com/containerd/project/blob/main/MAINTAINERS. # See https://github.com/containerd/project/blob/main/GOVERNANCE.md for description of maintainer role # # See also MAINTAINERS_GUIDE.md # CORE COMMITTERS who regularly contribute to nerdctl # (Extracted from https://github.com/containerd/project/blob/main/MAINTAINERS for ease of reference) # GitHub ID, Name, Email address, GPG fingerprint "AkihiroSuda","Akihiro Suda","akihiro.suda.cz@hco.ntt.co.jp","C020 EA87 6CE4 E06C 7AB9 5AEF 4952 4C6F 9F63 8F1A" # COMMITTERS # GitHub ID, Name, Email address, GPG fingerprint "ktock","Kohei Tokunaga","ktokunaga.mail@gmail.com","" "fahedouch","Fahed Dorgaa","fahed.dorgaa@gmail.com","EE7A 5503 CE0D 38AC 5B95 A500 F35F F497 60A8 65FA" "Zheaoli", "Zheao Li", "me@manjusaka.me","6E0D D9FA BAD5 AF61 D884 01EE 878F 445D 9C6C E65E" "djdongjin", "Jin Dong", "djdongjin95@gmail.com","" "yankay", "Kay Yan", "kay.yan@daocloud.io", "" "ChengyuZhu6","Chengyu Zhu","hudson@cyzhu.com","" # REVIEWERS # GitHub ID, Name, Email address, GPG fingerprint "jsturtevant","James Sturtevant","jstur@microsoft.com","" "Shubhranshu153","Shubharanshu Mahapatra","shubhum@amazon.com","" "haytok","Hayato Kiwata","haytok@amazon.co.jp","B485 C5AA 6220 0A06 78FD 294D FA4F 2421 1D65 269F" # EMERITUS # See EMERITUS.md ================================================ FILE: MAINTAINERS_GUIDE.md ================================================ # Maintainers' guide ## Maintainer list - Core: https://github.com/containerd/project/blob/main/MAINTAINERS - Non-core: [`MAINTAINERS`](./MAINTAINERS) ## Governance See https://github.com/containerd/project/blob/main/GOVERNANCE.md ## Creating a release Eligibility to be a release manager: - MUST be an active Commiter (Core or Non-core) - MUST have the GPG fingerprint listed in [`MAINTAINERS`](./MAINTAINERS) - MUST upload the GPG public key to `https://github.com/USERNAME.gpg` - MUST protect the GPG key with a passphrase or a hardware token. Release steps: - Open a PR to keep the dependencies up-to-date. Update `go.mod` for Go dependencies (usually Dependabot automatically updates them). Update `Dockerfile` and relevant files under `Dockerfile.d` for `nerdctl-full` dependencies. - Open an issue to propose making a new release. The proposal should be public, with an exception for vulnerability fixes. If this is the first time for you to take a role of release management, you SHOULD make a beta (or alpha, RC) release as an exercise before releasing GA. - Make sure that all the merged PRs are associated with the correct [Milestone](https://github.com/containerd/nerdctl/milestones). - Run `git tag --sign vX.Y.Z-beta.W` . - Run `git push UPSTREAM vX.Y.Z-beta.W` . - Wait for the `Release` action on GitHub Actions to complete. A draft release will appear in https://github.com/containerd/nerdctl/releases . - Download `SHA256SUMS` from the draft release, and confirm that it corresponds to the hashes printed in the build logs on the `Release` action. - Sign `SHA256SUMS` with `gpg --detach-sign -a SHA256SUMS` to produce `SHA256SUMS.asc`, and upload it to the draft release. - Add release notes in the draft release, to explain the changes and show appreciation to the contributors. Make sure to fulfill the `Release manager: [ADD YOUR NAME HERE] (@[ADD YOUR GITHUB ID HERE])` line with your name. e.g., `Release manager: Akihiro Suda (@AkihiroSuda)` . - Click the `Set as a pre-release` checkbox if this release is a beta (or alpha, RC). - Click the `Publish release` button. - Close the [Milestone](https://github.com/containerd/nerdctl/milestones). ================================================ FILE: Makefile ================================================ # Copyright The containerd Authors. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ----------------------------------------------------------------------------- # Portions from https://github.com/kubernetes-sigs/cri-tools/blob/v1.19.0/Makefile # Copyright The Kubernetes Authors. # Licensed under the Apache License, Version 2.0 # ----------------------------------------------------------------------------- ########################## # Configuration ########################## PACKAGE := "github.com/containerd/nerdctl/v2" DOCKER ?= docker GO ?= go GOOS ?= $(shell $(GO) env GOOS) GOARCH ?= $(shell $(GO) env GOARCH) ifeq ($(GOOS),windows) BIN_EXT := .exe endif # distro builders might want to override these PREFIX ?= /usr/local BINDIR ?= $(PREFIX)/bin DATADIR ?= $(PREFIX)/share DOCDIR ?= $(DATADIR)/doc BINARY ?= "nerdctl" MAKEFILE_DIR := $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST))))) VERSION ?= $(shell git -C $(MAKEFILE_DIR) describe --match 'v[0-9]*' --dirty='.m' --always --tags 2>/dev/null || echo no_git_information) VERSION_TRIMMED := $(VERSION:v%=%) REVISION ?= $(shell git -C $(MAKEFILE_DIR) rev-parse HEAD 2>/dev/null || echo no_git_information)$(shell if ! git -C $(MAKEFILE_DIR) diff --no-ext-diff --quiet --exit-code 2>/dev/null; then echo .m; fi) LINT_COMMIT_RANGE ?= main..HEAD GO_BUILD_LDFLAGS ?= -s -w GO_BUILD_FLAGS ?= BUILDTAGS ?= GO_TAGS=$(if $(BUILDTAGS),-tags "$(strip $(BUILDTAGS))",) ########################## # Helpers ########################## ifdef VERBOSE VERBOSE_FLAG := -v VERBOSE_FLAG_LONG := --verbose endif export GO_BUILD=CGO_ENABLED=0 GOOS=$(GOOS) $(GO) -C $(MAKEFILE_DIR) build $(GO_TAGS) -ldflags "$(GO_BUILD_LDFLAGS) $(VERBOSE_FLAG) -X $(PACKAGE)/pkg/version.Version=$(VERSION) -X $(PACKAGE)/pkg/version.Revision=$(REVISION)" ifndef NO_COLOR NC := \033[0m GREEN := \033[1;32m ORANGE := \033[1;33m endif recursive_wildcard=$(wildcard $1$2) $(foreach e,$(wildcard $1*),$(call recursive_wildcard,$e/,$2)) define title @printf "$(GREEN)____________________________________________________________________________________________________\n" @printf "$(GREEN)%*s\n" $$(( ( $(shell echo "🤓$(1) 🤓" | wc -c ) + 100 ) / 2 )) "🤓$(1) 🤓" @printf "$(GREEN)____________________________________________________________________________________________________\n$(ORANGE)" endef define footer @printf "$(GREEN)> %s: done!\n" "$(1)" @printf "$(GREEN)____________________________________________________________________________________________________\n$(NC)" endef ########################## # High-level tasks definitions ########################## all: binaries lint: lint-go-all lint-yaml lint-shell lint-commits lint-mod lint-licenses-all fix: fix-mod fix-go-all # TODO: fix race task and add it test: test-unit # test-unit-race test-unit-bench help: @echo "Usage: make " @echo @echo " * 'lint' - Run linters against codebase." @echo " * 'fix' - Automatically fixes imports, modules, and simple formatting." @echo " * 'test' - Run basic unit testing." @echo " * 'binaries' - Build nerdctl." @echo " * 'install' - Install binaries to system locations." @echo " * 'clean' - Clean artifacts." ########################## # Building and installation tasks ########################## binaries: $(CURDIR)/_output/$(BINARY)$(BIN_EXT) $(CURDIR)/_output/$(BINARY)$(BIN_EXT): $(call title, $@: $(GOOS)/$(GOARCH)) $(GO_BUILD) $(GO_BUILD_FLAGS) $(VERBOSE_FLAG) -o $(CURDIR)/_output/$(BINARY)$(BIN_EXT) ./cmd/nerdctl $(call footer, $@) install: $(call title, $@) install -D -m 755 $(CURDIR)/_output/$(BINARY) $(DESTDIR)$(BINDIR)/$(BINARY) install -D -m 755 $(MAKEFILE_DIR)/extras/rootless/containerd-rootless.sh $(DESTDIR)$(BINDIR)/containerd-rootless.sh install -D -m 755 $(MAKEFILE_DIR)/extras/rootless/containerd-rootless-setuptool.sh $(DESTDIR)$(BINDIR)/containerd-rootless-setuptool.sh install -D -m 644 -t $(DESTDIR)$(DOCDIR)/nerdctl $(MAKEFILE_DIR)/docs/*.md $(call footer, $@) clean: $(call title, $@) find . -name \*~ -delete find . -name \#\* -delete rm -rf $(CURDIR)/_output/* $(MAKEFILE_DIR)/vendor $(call footer, $@) ########################## # Linting tasks ########################## lint-go: $(call title, $@: $(GOOS)) @cd $(MAKEFILE_DIR) \ && golangci-lint run $(VERBOSE_FLAG_LONG) ./... $(call footer, $@) lint-go-all: $(call title, $@) @cd $(MAKEFILE_DIR) \ && GOOS=linux make lint-go \ && GOOS=windows make lint-go \ && GOOS=freebsd make lint-go \ && GOOS=darwin make lint-go $(call footer, $@) lint-yaml: $(call title, $@) cd $(MAKEFILE_DIR) \ && yamllint . $(call footer, $@) lint-shell: $(call recursive_wildcard,$(MAKEFILE_DIR)/,*.sh) $(call title, $@) shellcheck -a -x $^ $(call footer, $@) lint-commits: $(call title, $@) @cd $(MAKEFILE_DIR) \ && git-validation $(VERBOSE_FLAG) -run DCO,short-subject,dangling-whitespace -range "$(LINT_COMMIT_RANGE)" $(call footer, $@) lint-mod: $(call title, $@) @cd $(MAKEFILE_DIR) \ && go mod tidy --diff $(call footer, $@) # FIXME: go-licenses cannot find LICENSE from root of repo when submodule is imported: # https://github.com/google/go-licenses/issues/186 # This is impacting gotest.tools # FIXME: go-base36 is multi-license (MIT/Apache), using a custom boilerplate file that go-licenses fails to understand lint-licenses: $(call title, $@: $(GOOS)) @cd $(MAKEFILE_DIR) \ && go-licenses check --include_tests --allowed_licenses=Apache-2.0,BSD-2-Clause,BSD-2-Clause-FreeBSD,BSD-3-Clause,MIT,ISC,Python-2.0,PostgreSQL,X11,Zlib \ --ignore gotest.tools \ --ignore github.com/multiformats/go-base36 \ ./... $(call footer, $@) lint-licenses-all: $(call title, $@) @cd $(MAKEFILE_DIR) \ && GOOS=linux make lint-licenses \ && GOOS=windows make lint-licenses \ && GOOS=freebsd make lint-licenses \ && GOOS=darwin make lint-licenses $(call footer, $@) ########################## # Automated fixing tasks ########################## fix-go: $(call title, $@: $(GOOS)) @cd $(MAKEFILE_DIR) \ && golangci-lint run --fix $(call footer, $@) fix-go-all: $(call title, $@) @cd $(MAKEFILE_DIR) \ && GOOS=linux make fix-go \ && GOOS=windows make fix-go \ && GOOS=freebsd make fix-go \ && GOOS=darwin make fix-go $(call footer, $@) fix-mod: $(call title, $@) @cd $(MAKEFILE_DIR) \ && go mod tidy $(call footer, $@) ########################## # Development tools installation ########################## install-dev-tools: $(call title, $@) # golangci: v2.4.0 (2025-08-14) # git-validation: main (2025-02-25) # ltag: main (2025-03-04) # go-licenses: v2.0.0-alpha.1 (2024-06-27) # stubbing go-licenses with dependency upgrade due to non-compatibility with golang 1.25rc1 # Issue: https://github.com/google/go-licenses/issues/312 @cd $(MAKEFILE_DIR) \ && go install github.com/Shubhranshu153/go-licenses/v2@f8c503d1357dffb6c97ed3b94e912ab294dde24a \ && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@43d03392d7dc3746fa776dbddd66dfcccff70651 \ && go install github.com/vbatts/git-validation@7b60e35b055dd2eab5844202ffffad51d9c93922 \ && go install github.com/containerd/ltag@66e6a514664ee2d11a470735519fa22b1a9eaabd \ && go install gotest.tools/gotestsum@0d9599e513d70e5792bb9334869f82f6e8b53d4d @echo "Remember to add \$$HOME/go/bin to your path" $(call footer, $@) ########################## # Testing tasks ########################## test-unit: $(call title, $@) @go test $(VERBOSE_FLAG) $(MAKEFILE_DIR)/pkg/... $(call footer, $@) test-unit-bench: $(call title, $@) @go test $(VERBOSE_FLAG) $(MAKEFILE_DIR)/pkg/... -bench=. $(call footer, $@) test-unit-race: $(call title, $@) @go test $(VERBOSE_FLAG) $(MAKEFILE_DIR)/pkg/... -race $(call footer, $@) ########################## # Release tasks ########################## # Note that these options will not work on macOS - unless you use gnu-tar instead of tar TAR_OWNER0_FLAGS=--owner=0 --group=0 TAR_FLATTEN_FLAGS=--transform 's/.*\///g' define make_artifact_full_linux $(DOCKER) build --secret id=github_token,env=GITHUB_TOKEN --output type=tar,dest=$(CURDIR)/_output/nerdctl-full-$(VERSION_TRIMMED)-linux-$(1).tar --target out-full --platform $(1) --build-arg GO_VERSION -f $(MAKEFILE_DIR)/Dockerfile $(MAKEFILE_DIR) gzip -9 $(CURDIR)/_output/nerdctl-full-$(VERSION_TRIMMED)-linux-$(1).tar endef artifacts: clean $(call title, $@) GOOS=linux GOARCH=amd64 make -C $(CURDIR) -f $(MAKEFILE_DIR)/Makefile binaries tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-linux-amd64.tar.gz $(CURDIR)/_output/nerdctl $(MAKEFILE_DIR)/extras/rootless/* GOOS=linux GOARCH=arm64 make -C $(CURDIR) -f $(MAKEFILE_DIR)/Makefile binaries tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-linux-arm64.tar.gz $(CURDIR)/_output/nerdctl $(MAKEFILE_DIR)/extras/rootless/* GOOS=linux GOARCH=arm GOARM=7 make -C $(CURDIR) -f $(MAKEFILE_DIR)/Makefile binaries tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-linux-arm-v7.tar.gz $(CURDIR)/_output/nerdctl $(MAKEFILE_DIR)/extras/rootless/* GOOS=linux GOARCH=loong64 make -C $(CURDIR) -f $(MAKEFILE_DIR)/Makefile binaries tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-linux-loong64.tar.gz $(CURDIR)/_output/nerdctl $(MAKEFILE_DIR)/extras/rootless/* GOOS=linux GOARCH=ppc64le make -C $(CURDIR) -f $(MAKEFILE_DIR)/Makefile binaries tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-linux-ppc64le.tar.gz $(CURDIR)/_output/nerdctl $(MAKEFILE_DIR)/extras/rootless/* GOOS=linux GOARCH=riscv64 make -C $(CURDIR) -f $(MAKEFILE_DIR)/Makefile binaries tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-linux-riscv64.tar.gz $(CURDIR)/_output/nerdctl $(MAKEFILE_DIR)/extras/rootless/* GOOS=linux GOARCH=s390x make -C $(CURDIR) -f $(MAKEFILE_DIR)/Makefile binaries tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-linux-s390x.tar.gz $(CURDIR)/_output/nerdctl $(MAKEFILE_DIR)/extras/rootless/* GOOS=windows GOARCH=amd64 make -C $(CURDIR) -f $(MAKEFILE_DIR)/Makefile binaries tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-windows-amd64.tar.gz $(CURDIR)/_output/nerdctl.exe GOOS=freebsd GOARCH=amd64 make -C $(CURDIR) -f $(MAKEFILE_DIR)/Makefile binaries tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-freebsd-amd64.tar.gz $(CURDIR)/_output/nerdctl rm -f $(CURDIR)/_output/nerdctl $(CURDIR)/_output/nerdctl.exe $(call make_artifact_full_linux,amd64) $(call make_artifact_full_linux,arm64) $(GO) -C $(MAKEFILE_DIR) mod vendor tar $(TAR_OWNER0_FLAGS) -czf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-go-mod-vendor.tar.gz $(MAKEFILE_DIR)/go.mod $(MAKEFILE_DIR)/go.sum $(MAKEFILE_DIR)/vendor $(call footer, $@) .PHONY: \ all \ lint \ fix \ test \ help \ binaries \ install \ clean \ lint-go lint-go-all lint-yaml lint-shell lint-commits lint-mod lint-licenses lint-licenses-all \ fix-go fix-go-all fix-mod \ install-dev-tools \ test-unit test-unit-race test-unit-bench \ artifacts ================================================ FILE: NOTICE ================================================ nerdctl Copyright The containerd Authors. This project contains portions of other projects that are licensed under the terms of Apache License 2.0. The NOTICE files of those projects are replicated here. === https://github.com/moby/moby , https://github.com/docker/cli === https://github.com/moby/moby/blob/v20.10.14/LICENSE , https://github.com/docker/cli/blob/v20.10.14/LICENSE https://github.com/moby/moby/blob/v20.10.14/NOTICE , https://github.com/docker/cli/blob/v20.10.14/NOTICE > Docker > Copyright 2012-2017 Docker, Inc. > > This product includes software developed at Docker, Inc. (https://www.docker.com). > > This product contains software (https://github.com/creack/pty) developed > by Keith Rarick, licensed under the MIT License. > > The following is courtesy of our legal counsel: > > > Use and transfer of Docker may be subject to certain restrictions by the > United States and other governments. > It is your responsibility to ensure that your use and/or transfer does not > violate applicable laws. > > For more information, please see https://www.bis.doc.gov > > See also https://www.apache.org/dev/crypto.html and/or seek legal counsel. === https://github.com/docker/compose === https://github.com/docker/compose/blob/v2.4.1/LICENSE https://github.com/docker/compose/blob/v2.4.1/NOTICE > Docker Compose V2 > Copyright 2020 Docker Compose authors > > This product includes software developed at Docker, Inc. (https://www.docker.com). ================================================ FILE: README.md ================================================ [[⬇️ **Download]**](https://github.com/containerd/nerdctl/releases) [[📖 **Command reference]**](./docs/command-reference.md) [[❓**FAQs & Troubleshooting]**](./docs/faq.md) [[📚 **Additional documents]**](#additional-documents) # nerdctl: Docker-compatible CLI for containerd logo `nerdctl` is a Docker-compatible CLI for [contai**nerd**](https://containerd.io). ✅ Same UI/UX as `docker` ✅ Supports Docker Compose (`nerdctl compose up`) ✅ [Optional] Supports [rootless mode, without slirp overhead (bypass4netns)](./docs/rootless.md) ✅ [Optional] Supports lazy-pulling ([Stargz](./docs/stargz.md), [Nydus](./docs/nydus.md), [OverlayBD](./docs/overlaybd.md)) ✅ [Optional] Supports [encrypted images (ocicrypt)](./docs/ocicrypt.md) ✅ [Optional] Supports [P2P image distribution (IPFS)](./docs/ipfs.md) (\*1) ✅ [Optional] Supports [container image signing and verifying (cosign)](./docs/cosign.md) nerdctl is a **non-core** sub-project of containerd. \*1: P2P image distribution (IPFS) is completely optional. Your host is NOT connected to any P2P network, unless you opt in to [install and run IPFS daemon](https://docs.ipfs.io/install/). ## Examples ### Basic usage To run a container with the default `bridge` CNI network (10.4.0.0/24): ```console # nerdctl run -it --rm alpine ``` To build an image using BuildKit: ```console # nerdctl build -t foo /some-dockerfile-directory # nerdctl run -it --rm foo ``` To build and send output to a local directory using BuildKit: ```console # nerdctl build -o type=local,dest=. /some-dockerfile-directory ``` To run containers from `docker-compose.yaml`: ```console # nerdctl compose -f ./examples/compose-wordpress/docker-compose.yaml up ``` See also [`./examples/compose-wordpress`](./examples/compose-wordpress). ### Debugging Kubernetes To list local Kubernetes containers: ```console # nerdctl --namespace k8s.io ps -a ``` To build an image for local Kubernetes without using registry: ```console # nerdctl --namespace k8s.io build -t foo /some-dockerfile-directory # kubectl apply -f - < In addition to containerd, the following components should be installed: - [CNI plugins](https://github.com/containernetworking/plugins): for using `nerdctl run`. - v1.1.0 or later is highly recommended. - [BuildKit](https://github.com/moby/buildkit) (OPTIONAL): for using `nerdctl build`. BuildKit daemon (`buildkitd`) needs to be running. See also [the document about setting up BuildKit](./docs/build.md). - v0.11.0 or later is highly recommended. Some features, such as pruning caches with `nerdctl system prune`, do not work with older versions. - [RootlessKit](https://github.com/rootless-containers/rootlesskit) and [slirp4netns](https://github.com/rootless-containers/slirp4netns) (OPTIONAL): for [Rootless mode](./docs/rootless.md) - RootlessKit needs to be v0.10.0 or later. v2.0.0 or later is recommended. - slirp4netns needs to be v0.4.0 or later. v1.1.7 or later is recommended. These dependencies are included in `nerdctl-full---.tar.gz`, but not included in `nerdctl---.tar.gz`. ### Brew On Linux systems you can install nerdctl via [brew](https://brew.sh): ```bash brew install nerdctl ``` This is currently not supported for macOS. The section below shows how to install on macOS using brew. ### macOS [Lima](https://github.com/lima-vm/lima) project provides Linux virtual machines for macOS, with built-in integration for nerdctl. ```console $ brew install lima $ limactl start $ lima nerdctl run -d --name nginx -p 127.0.0.1:8080:80 nginx:alpine ``` ### FreeBSD See [`./docs/freebsd.md`](docs/freebsd.md). ### Windows - Linux containers: Known to work on WSL2 - Windows containers: experimental support for Windows (see below for features that are currently known to work) ### Docker To run containerd and nerdctl inside Docker: ```bash docker build -t nerdctl . docker run -it --rm --privileged nerdctl ``` ## Motivation The goal of `nerdctl` is to facilitate experimenting the cutting-edge features of containerd that are not present in Docker (see below). Note that competing with Docker is _not_ the goal of `nerdctl`. Those cutting-edge features are expected to be eventually available in Docker as well. Also, `nerdctl` might be potentially useful for debugging Kubernetes clusters, but it is not the primary goal. ## Features present in `nerdctl` but not present in Docker Major: - On-demand image pulling (lazy-pulling) using [Stargz](./docs/stargz.md)/[Nydus](./docs/nydus.md)/[OverlayBD](./docs/overlaybd.md)/[SOCI](./docs/soci.md) Snapshotter: `nerdctl --snapshotter=stargz|nydus|overlaybd|soci run IMAGE` . - [Image encryption and decryption using ocicrypt (imgcrypt)](./docs/ocicrypt.md): `nerdctl image (encrypt|decrypt) SRC DST` - [P2P image distribution using IPFS](./docs/ipfs.md): `nerdctl run ipfs://CID` . P2P image distribution (IPFS) is completely optional. Your host is NOT connected to any P2P network, unless you opt in to [install and run IPFS daemon](https://docs.ipfs.io/install/). - [Cosign integration](./docs/cosign.md): `nerdctl pull --verify=cosign` and `nerdctl push --sign=cosign`, and [in Compose](./docs/cosign.md#cosign-in-compose) - [Accelerated rootless containers using bypass4netns](./docs/rootless.md): `nerdctl run --annotation nerdctl/bypass4netns=true` Minor: - Namespacing: `nerdctl --namespace= ps` . (NOTE: All Kubernetes containers are in the `k8s.io` containerd namespace regardless to Kubernetes namespaces) - Exporting Docker/OCI dual-format archives: `nerdctl save` . - Importing OCI archives as well as Docker archives: `nerdctl load` . - Specifying a non-image rootfs: `nerdctl run -it --rootfs /bin/sh` . The CLI syntax conforms to Podman convention. - Connecting a container to multiple networks at once: `nerdctl run --net foo --net bar` - Running [FreeBSD jails](./docs/freebsd.md). - Better multi-platform support, e.g., `nerdctl pull --all-platforms IMAGE` - Applying an (existing) AppArmor profile to rootless containers: `nerdctl run --security-opt apparmor=`. Use `sudo nerdctl apparmor load` to load the `nerdctl-default` profile. - Systemd compatibility support: `nerdctl run --systemd=always` Trivial: - Inspecting raw OCI config: `nerdctl container inspect --mode=native` . ## Features implemented in `nerdctl` ahead of Docker - Recursive read-only (RRO) bind-mount: `nerdctl run -v /mnt:/mnt:rro` (make children such as `/mnt/usb` to be read-only, too). Requires kernel >= 5.12. The same feature was later introduced in Docker v25 with a different syntax. nerdctl will support Docker v25 syntax too in the future. ## Similar tools - [`ctr`](https://github.com/containerd/containerd/tree/main/cmd/ctr): incompatible with Docker CLI, and not friendly to users. Notably, `ctr` lacks the equivalents of the following nerdctl commands: - `nerdctl run -p ` - `nerdctl run --restart=always --net=bridge` - `nerdctl pull` with `~/.docker/config.json` and credential helper binaries such as `docker-credential-ecr-login` - `nerdctl logs` - `nerdctl build` - `nerdctl compose up` - [`crictl`](https://github.com/kubernetes-sigs/cri-tools): incompatible with Docker CLI, not friendly to users, and does not support non-CRI features - [k3c v0.2 (abandoned)](https://github.com/rancher/k3c/tree/v0.2.1): needs an extra daemon, and does not support non-CRI features - [Rancher Kim (nee k3c v0.3)](https://github.com/rancher/kim): needs Kubernetes, and only focuses on image management commands such as `kim build` and `kim push` - [PouchContainer (abandoned?)](https://github.com/alibaba/pouch): needs an extra daemon ## Developer guide nerdctl is a containerd **non-core** sub-project, licensed under the [Apache 2.0 license](./LICENSE). As a containerd non-core sub-project, you will find the: - [Project governance](https://github.com/containerd/project/blob/main/GOVERNANCE.md), - [Maintainers](./MAINTAINERS), - and [Contributing guidelines](https://github.com/containerd/project/blob/main/CONTRIBUTING.md) information in our [`containerd/project`](https://github.com/containerd/project) repository. ### Compiling nerdctl from source Run `make && sudo make install`. See the header of [`go.mod`](./go.mod) for the minimum supported version of Go. Using `go install github.com/containerd/nerdctl/v2/cmd/nerdctl` is possible, but unrecommended because it does not fill version strings printed in `nerdctl version` ### Testing See [testing nerdctl](docs/testing/README.md). ### Contributing to nerdctl Lots of commands and flags are currently missing. Pull requests are highly welcome. Please certify your [Developer Certificate of Origin (DCO)](https://developercertificate.org/), by signing off your commit with `git commit -s` and with your real name. # Command reference Moved to [`./docs/command-reference.md`](./docs/command-reference.md) # Additional documents Configuration guide: - [`./docs/config.md`](./docs/config.md): Configuration (`/etc/nerdctl/nerdctl.toml`, `~/.config/nerdctl/nerdctl.toml`) - [`./docs/registry.md`](./docs/registry.md): Registry authentication (`~/.docker/config.json`) Basic features: - [`./docs/compose.md`](./docs/compose.md): Compose - [`./docs/rootless.md`](./docs/rootless.md): Rootless mode - [`./docs/cni.md`](./docs/cni.md): CNI for containers network - [`./docs/build.md`](./docs/build.md): `nerdctl build` with BuildKit Advanced features: - [`./docs/stargz.md`](./docs/stargz.md): Lazy-pulling using Stargz Snapshotter - [`./docs/nydus.md`](./docs/nydus.md): Lazy-pulling using Nydus Snapshotter - [`./docs/soci.md`](./docs/soci.md): Lazy-pulling using SOCI Snapshotter - [`./docs/overlaybd.md`](./docs/overlaybd.md): Lazy-pulling using OverlayBD Snapshotter - [`./docs/ocicrypt.md`](./docs/ocicrypt.md): Running encrypted images - [`./docs/gpu.md`](./docs/gpu.md): Using GPUs inside containers - [`./docs/multi-platform.md`](./docs/multi-platform.md): Multi-platform mode Experimental features: - [`./docs/experimental.md`](./docs/experimental.md): Experimental features - [`./docs/freebsd.md`](./docs/freebsd.md): Running FreeBSD jails - [`./docs/ipfs.md`](./docs/ipfs.md): Distributing images on IPFS - [`./docs/builder-debug.md`](./docs/builder-debug.md): Interactive debugging of Dockerfile Implementation details: - [`./docs/dir.md`](./docs/dir.md): Directory layout (`/var/lib/nerdctl`) Misc: - [`./docs/faq.md`](./docs/faq.md): FAQs and Troubleshooting ================================================ FILE: SECURITY.md ================================================ See https://github.com/containerd/project/blob/main/SECURITY.md for reporting a vulnerability. ================================================ FILE: Vagrantfile.freebsd ================================================ # -*- mode: ruby -*- # vi: set ft=ruby : # Copyright The containerd Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Vagrantfile for FreeBSD Vagrant.configure("2") do |config| config.vm.box = "generic/freebsd14" memory = 2048 cpus = 1 config.vm.provider :virtualbox do |v, o| v.memory = memory v.cpus = cpus end config.vm.provider :libvirt do |v| v.memory = memory v.cpus = cpus end config.vm.synced_folder ".", "/vagrant", type: "rsync" config.vm.provision "install", type: "shell", run: "once" do |sh| sh.inline = <<~SHELL #!/usr/bin/env bash set -eux -o pipefail freebsd-version -kru # switching to "release_2" ensures compatibility with the current Vagrant box # https://github.com/moby/buildkit/pull/5893 sed -i '' 's/latest/release_2/' /usr/local/etc/pkg/repos/FreeBSD.conf # `pkg install go` still installs Go 1.20 (March 2024) pkg install -y go122 containerd runj ln -s go122 /usr/local/bin/go cd /vagrant go install ./cmd/nerdctl SHELL end config.vm.provision "test-unit", type: "shell", run: "never" do |sh| sh.inline = <<~SHELL #!/usr/bin/env bash set -eux -o pipefail cd /vagrant go test -v ./pkg/... SHELL end config.vm.provision "test-integration", type: "shell", run: "never" do |sh| sh.inline = <<~SHELL #!/usr/bin/env bash set -eux -o pipefail daemon -o containerd.out containerd sleep 3 CONTAINERD_ADDRESS=/run/containerd/containerd.sock /root/go/bin/nerdctl run --rm --quiet --net=none dougrabson/freebsd-minimal:13 echo "Nerdctl is up and running." SHELL end end ================================================ FILE: cmd/nerdctl/apparmor/apparmor_inspect_linux.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package apparmor import ( "fmt" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/cmd/apparmor" "github.com/containerd/nerdctl/v2/pkg/defaults" ) func inspectCommand() *cobra.Command { cmd := &cobra.Command{ Use: "inspect", Short: fmt.Sprintf("Display the default AppArmor profile %q. Other profiles cannot be displayed with this command.", defaults.AppArmorProfileName), Args: cobra.NoArgs, RunE: inspectAction, SilenceUsage: true, SilenceErrors: true, } return cmd } func inspectAction(cmd *cobra.Command, args []string) error { return apparmor.Inspect(types.ApparmorInspectOptions{ Stdout: cmd.OutOrStdout(), }) } ================================================ FILE: cmd/nerdctl/apparmor/apparmor_linux.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package apparmor import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" ) func Command() *cobra.Command { cmd := &cobra.Command{ Annotations: map[string]string{helpers.Category: helpers.Management}, Use: "apparmor", Short: "Manage AppArmor profiles", RunE: helpers.UnknownSubcommandAction, SilenceUsage: true, SilenceErrors: true, } cmd.AddCommand( listCommand(), inspectCommand(), loadCommand(), unloadCommand(), ) return cmd } ================================================ FILE: cmd/nerdctl/apparmor/apparmor_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package apparmor import ( "testing" "github.com/containerd/nerdctl/v2/pkg/testutil" ) func TestMain(m *testing.M) { testutil.M(m) } ================================================ FILE: cmd/nerdctl/apparmor/apparmor_list_linux.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package apparmor import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/cmd/apparmor" ) func listCommand() *cobra.Command { cmd := &cobra.Command{ Use: "ls", Aliases: []string{"list"}, Short: "List the loaded AppArmor profiles", Args: cobra.NoArgs, RunE: listAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().BoolP("quiet", "q", false, "Only display profile names") // Alias "-f" is reserved for "--filter" cmd.Flags().String("format", "", "Format the output using the given go template") cmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"json", "table", "wide"}, cobra.ShellCompDirectiveNoFileComp }) return cmd } func listOptions(cmd *cobra.Command) (types.ApparmorListOptions, error) { quiet, err := cmd.Flags().GetBool("quiet") if err != nil { return types.ApparmorListOptions{}, err } format, err := cmd.Flags().GetString("format") if err != nil { return types.ApparmorListOptions{}, err } return types.ApparmorListOptions{ Quiet: quiet, Format: format, Stdout: cmd.OutOrStdout(), }, nil } func listAction(cmd *cobra.Command, args []string) error { options, err := listOptions(cmd) if err != nil { return err } return apparmor.List(options) } ================================================ FILE: cmd/nerdctl/apparmor/apparmor_load_linux.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package apparmor import ( "fmt" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/pkg/cmd/apparmor" "github.com/containerd/nerdctl/v2/pkg/defaults" ) func loadCommand() *cobra.Command { cmd := &cobra.Command{ Use: "load", Short: fmt.Sprintf("Load the default AppArmor profile %q. Requires root.", defaults.AppArmorProfileName), Args: cobra.NoArgs, RunE: loadAction, SilenceUsage: true, SilenceErrors: true, } return cmd } func loadAction(cmd *cobra.Command, args []string) error { return apparmor.Load() } ================================================ FILE: cmd/nerdctl/apparmor/apparmor_unload_linux.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package apparmor import ( "fmt" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/pkg/cmd/apparmor" "github.com/containerd/nerdctl/v2/pkg/defaults" ) func unloadCommand() *cobra.Command { cmd := &cobra.Command{ Use: "unload [PROFILE]", Short: fmt.Sprintf("Unload an AppArmor profile. The target profile name defaults to %q. Requires root.", defaults.AppArmorProfileName), Args: cobra.MaximumNArgs(1), RunE: unloadAction, ValidArgsFunction: unloadShellComplete, SilenceUsage: true, SilenceErrors: true, } return cmd } func unloadAction(cmd *cobra.Command, args []string) error { target := defaults.AppArmorProfileName if len(args) > 0 { target = args[0] } return apparmor.Unload(target) } func unloadShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.ApparmorProfiles(cmd) } ================================================ FILE: cmd/nerdctl/builder/builder.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package builder import ( "fmt" "os" "os/exec" "strings" "time" "github.com/docker/go-units" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/cmd/builder" ) func Command() *cobra.Command { var cmd = &cobra.Command{ Annotations: map[string]string{helpers.Category: helpers.Management}, Use: "builder", Short: "Manage builds", RunE: helpers.UnknownSubcommandAction, SilenceUsage: true, SilenceErrors: true, } cmd.AddCommand( BuildCommand(), pruneCommand(), debugCommand(), ) return cmd } func pruneCommand() *cobra.Command { shortHelp := `Clean up BuildKit build cache` var cmd = &cobra.Command{ Use: "prune", Args: cobra.NoArgs, Short: shortHelp, RunE: pruneAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().String("buildkit-host", "", "BuildKit address") cmd.Flags().BoolP("all", "a", false, "Remove all unused build cache, not just dangling ones") cmd.Flags().BoolP("force", "f", false, "Do not prompt for confirmation") return cmd } func pruneAction(cmd *cobra.Command, _ []string) error { options, err := pruneOptions(cmd) if err != nil { return err } if !options.Force { var msg string if options.All { msg = "This will remove all build cache." } else { msg = "This will remove any dangling build cache." } if confirmed, err := helpers.Confirm(cmd, fmt.Sprintf("WARNING! %s.", msg)); err != nil || !confirmed { return err } } prunedObjects, err := builder.Prune(cmd.Context(), options) if err != nil { return err } var totalReclaimedSpace int64 for _, prunedObject := range prunedObjects { totalReclaimedSpace += prunedObject.Size } fmt.Fprintf(cmd.OutOrStdout(), "Total: %s\n", units.BytesSize(float64(totalReclaimedSpace))) return nil } func pruneOptions(cmd *cobra.Command) (types.BuilderPruneOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.BuilderPruneOptions{}, err } buildkitHost, err := GetBuildkitHost(cmd, globalOptions.Namespace) if err != nil { return types.BuilderPruneOptions{}, err } all, err := cmd.Flags().GetBool("all") if err != nil { return types.BuilderPruneOptions{}, err } force, err := cmd.Flags().GetBool("force") if err != nil { return types.BuilderPruneOptions{}, err } return types.BuilderPruneOptions{ Stderr: cmd.OutOrStderr(), GOptions: globalOptions, BuildKitHost: buildkitHost, All: all, Force: force, }, nil } func debugCommand() *cobra.Command { shortHelp := `Debug Dockerfile` var cmd = &cobra.Command{ Use: "debug", Short: shortHelp, PreRunE: helpers.CheckExperimental("`nerdctl builder debug`"), RunE: debugAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().StringP("file", "f", "", "Name of the Dockerfile") cmd.Flags().String("target", "", "Set the target build stage to build") cmd.Flags().StringArray("build-arg", nil, "Set build-time variables") cmd.Flags().String("image", "", "Image to use for debugging stage") cmd.Flags().StringArray("ssh", nil, "Allow forwarding SSH agent to the build. Format: default|[=|[,]]") cmd.Flags().StringArray("secret", nil, "Expose secret value to the build. Format: id=secretname,src=filepath") helpers.AddDurationFlag(cmd, "buildg-startup-timeout", nil, 1*time.Minute, "", "Timeout for starting up buildg") return cmd } func debugAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } if len(args) < 1 { return fmt.Errorf("context needs to be specified") } buildgBinary, err := exec.LookPath("buildg") if err != nil { return err } buildgArgs := []string{"debug"} if globalOptions.Debug { buildgArgs = append([]string{"--debug"}, buildgArgs...) } startupTimeout, err := cmd.Flags().GetDuration("buildg-startup-timeout") if err != nil { return err } buildgArgs = append(buildgArgs, "--startup-timeout="+startupTimeout.String()) if file, err := cmd.Flags().GetString("file"); err != nil { return err } else if file != "" { buildgArgs = append(buildgArgs, "--file="+file) } if target, err := cmd.Flags().GetString("target"); err != nil { return err } else if target != "" { buildgArgs = append(buildgArgs, "--target="+target) } if buildArgsValue, err := cmd.Flags().GetStringArray("build-arg"); err != nil { return err } else if len(buildArgsValue) > 0 { for _, v := range buildArgsValue { arr := strings.Split(v, "=") if len(arr) == 1 && len(arr[0]) > 0 { // Avoid masking default build arg value from Dockerfile if environment variable is not set // https://github.com/moby/moby/issues/24101 val, ok := os.LookupEnv(arr[0]) if ok { buildgArgs = append(buildgArgs, fmt.Sprintf("--build-arg=%s=%s", v, val)) } } else if len(arr) > 1 && len(arr[0]) > 0 { buildgArgs = append(buildgArgs, "--build-arg="+v) } else { return fmt.Errorf("invalid build arg %q", v) } } } if imageValue, err := cmd.Flags().GetString("image"); err != nil { return err } else if imageValue != "" { buildgArgs = append(buildgArgs, "--image="+imageValue) } if sshValue, err := cmd.Flags().GetStringArray("ssh"); err != nil { return err } else if len(sshValue) > 0 { for _, v := range sshValue { buildgArgs = append(buildgArgs, "--ssh="+v) } } if secretValue, err := cmd.Flags().GetStringArray("secret"); err != nil { return err } else if len(secretValue) > 0 { for _, v := range secretValue { buildgArgs = append(buildgArgs, "--secret="+v) } } buildgCmd := exec.Command(buildgBinary, append(buildgArgs, args[0])...) buildgCmd.Env = os.Environ() buildgCmd.Stdin = cmd.InOrStdin() buildgCmd.Stdout = cmd.OutOrStdout() buildgCmd.Stderr = cmd.ErrOrStderr() if err := buildgCmd.Start(); err != nil { return err } return buildgCmd.Wait() } ================================================ FILE: cmd/nerdctl/builder/builder_build.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package builder import ( "errors" "fmt" "strconv" "strings" "github.com/spf13/cobra" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/buildkitutil" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/builder" "github.com/containerd/nerdctl/v2/pkg/strutil" ) func BuildCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "build [flags] PATH", Short: "Build an image from a Dockerfile. Needs buildkitd to be running.", Long: `Build an image from a Dockerfile. Needs buildkitd to be running. If Dockerfile is not present and -f is not specified, it will look for Containerfile and build with it. `, RunE: buildAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().String("buildkit-host", "", "BuildKit address") cmd.Flags().StringArray("add-host", nil, "Add a custom host-to-IP mapping (format: \"host:ip\")") cmd.Flags().StringArrayP("tag", "t", nil, "Name and optionally a tag in the 'name:tag' format") cmd.Flags().StringP("file", "f", "", "Name of the Dockerfile") cmd.Flags().String("target", "", "Set the target build stage to build") cmd.Flags().StringArray("build-arg", nil, "Set build-time variables") cmd.Flags().Bool("no-cache", false, "Do not use cache when building the image") cmd.Flags().StringP("output", "o", "", "Output destination (format: type=local,dest=path)") cmd.Flags().String("progress", "auto", "Set type of progress output (auto, plain, tty). Use plain to show container output") cmd.Flags().String("provenance", "", "Shorthand for \"--attest=type=provenance\"") cmd.Flags().Bool("pull", false, "On true, always attempt to pull latest image version from remote. Default uses buildkit's default.") cmd.Flags().StringArray("secret", nil, "Secret file to expose to the build: id=mysecret,src=/local/secret") cmd.Flags().StringArray("allow", nil, "Allow extra privileged entitlement, e.g. network.host, security.insecure") cmd.RegisterFlagCompletionFunc("allow", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"network.host", "security.insecure"}, cobra.ShellCompDirectiveNoFileComp }) cmd.Flags().StringArray("attest", nil, "Attestation parameters (format: \"type=sbom,generator=image\")") cmd.Flags().StringArray("ssh", nil, "SSH agent socket or keys to expose to the build (format: default|[=|[,]])") cmd.Flags().BoolP("quiet", "q", false, "Suppress the build output and print image ID on success") cmd.Flags().String("sbom", "", "Shorthand for \"--attest=type=sbom\"") cmd.Flags().StringArray("cache-from", nil, "External cache sources (eg. user/app:cache, type=local,src=path/to/dir)") cmd.Flags().StringArray("cache-to", nil, "Cache export destinations (eg. user/app:cache, type=local,dest=path/to/dir)") cmd.Flags().Bool("rm", true, "Remove intermediate containers after a successful build") cmd.Flags().String("network", "default", "Set type of network for build (format:network=default|none|host)") cmd.RegisterFlagCompletionFunc("network", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"default", "host", "none"}, cobra.ShellCompDirectiveNoFileComp }) // #region platform flags // platform is defined as StringSlice, not StringArray, to allow specifying "--platform=amd64,arm64" cmd.Flags().StringSlice("platform", []string{}, "Set target platform for build (e.g., \"amd64\", \"arm64\")") cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) cmd.Flags().StringArray("build-context", []string{}, "Additional build contexts (e.g., name=path)") // #endregion cmd.Flags().String("iidfile", "", "Write the image ID to the file") cmd.Flags().StringArray("label", nil, "Set metadata for an image") cmd.Flags().String("source-policy-file", "", "BuildKit source policy file (see https://github.com/moby/buildkit/blob/master/docs/build-repro.md)") return cmd } func processBuildCommandFlag(cmd *cobra.Command, args []string) (types.BuilderBuildOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.BuilderBuildOptions{}, err } buildKitHost, err := GetBuildkitHost(cmd, globalOptions.Namespace) if err != nil { return types.BuilderBuildOptions{}, err } extraHosts, err := cmd.Flags().GetStringArray("add-host") if err != nil { return types.BuilderBuildOptions{}, err } platform, err := cmd.Flags().GetStringSlice("platform") if err != nil { return types.BuilderBuildOptions{}, err } platform = strutil.DedupeStrSlice(platform) if len(args) < 1 { return types.BuilderBuildOptions{}, errors.New("context needs to be specified") } buildContext := args[0] if buildContext == "-" || strings.Contains(buildContext, "://") { return types.BuilderBuildOptions{}, fmt.Errorf("unsupported build context: %q", buildContext) } output, err := cmd.Flags().GetString("output") if err != nil { return types.BuilderBuildOptions{}, err } tagValue, err := cmd.Flags().GetStringArray("tag") if err != nil { return types.BuilderBuildOptions{}, err } progress, err := cmd.Flags().GetString("progress") if err != nil { return types.BuilderBuildOptions{}, err } filename, err := cmd.Flags().GetString("file") if err != nil { return types.BuilderBuildOptions{}, err } target, err := cmd.Flags().GetString("target") if err != nil { return types.BuilderBuildOptions{}, err } buildArgs, err := cmd.Flags().GetStringArray("build-arg") if err != nil { return types.BuilderBuildOptions{}, err } label, err := cmd.Flags().GetStringArray("label") if err != nil { return types.BuilderBuildOptions{}, err } noCache, err := cmd.Flags().GetBool("no-cache") if err != nil { return types.BuilderBuildOptions{}, err } var pull *bool if cmd.Flags().Changed("pull") { pullFlag, err := cmd.Flags().GetBool("pull") if err != nil { return types.BuilderBuildOptions{}, err } pull = &pullFlag } secret, err := cmd.Flags().GetStringArray("secret") if err != nil { return types.BuilderBuildOptions{}, err } allow, err := cmd.Flags().GetStringArray("allow") if err != nil { return types.BuilderBuildOptions{}, err } ssh, err := cmd.Flags().GetStringArray("ssh") if err != nil { return types.BuilderBuildOptions{}, err } cacheFrom, err := cmd.Flags().GetStringArray("cache-from") if err != nil { return types.BuilderBuildOptions{}, err } cacheTo, err := cmd.Flags().GetStringArray("cache-to") if err != nil { return types.BuilderBuildOptions{}, err } rm, err := cmd.Flags().GetBool("rm") if err != nil { return types.BuilderBuildOptions{}, err } iidfile, err := cmd.Flags().GetString("iidfile") if err != nil { return types.BuilderBuildOptions{}, err } quiet, err := cmd.Flags().GetBool("quiet") if err != nil { return types.BuilderBuildOptions{}, err } network, err := cmd.Flags().GetString("network") if err != nil { return types.BuilderBuildOptions{}, err } attest, err := cmd.Flags().GetStringArray("attest") if err != nil { return types.BuilderBuildOptions{}, err } sbom, err := cmd.Flags().GetString("sbom") if err != nil { return types.BuilderBuildOptions{}, err } if sbom != "" { attest = append(attest, canonicalizeAttest("sbom", sbom)) } provenance, err := cmd.Flags().GetString("provenance") if err != nil { return types.BuilderBuildOptions{}, err } if provenance != "" { attest = append(attest, canonicalizeAttest("provenance", provenance)) } extendedBuildCtx, err := cmd.Flags().GetStringArray("build-context") if err != nil { return types.BuilderBuildOptions{}, err } sourcePolicyFile, err := cmd.Flags().GetString("source-policy-file") if err != nil { return types.BuilderBuildOptions{}, err } usernsRemap, err := cmd.Flags().GetString("userns-remap") if err != nil { return types.BuilderBuildOptions{}, err } else if usernsRemap != "" { log.L.Warn("userns remap is not supported with nerdctl build. dropping the config.") } return types.BuilderBuildOptions{ GOptions: globalOptions, BuildKitHost: buildKitHost, BuildContext: buildContext, Output: output, Tag: tagValue, Progress: progress, File: filename, Target: target, BuildArgs: buildArgs, Label: label, NoCache: noCache, Pull: pull, Secret: secret, Allow: allow, Attest: attest, SSH: ssh, CacheFrom: cacheFrom, CacheTo: cacheTo, Rm: rm, IidFile: iidfile, Quiet: quiet, Platform: platform, Stdout: cmd.OutOrStdout(), Stderr: cmd.OutOrStderr(), Stdin: cmd.InOrStdin(), NetworkMode: network, ExtendedBuildContext: extendedBuildCtx, ExtraHosts: extraHosts, SourcePolicyFile: sourcePolicyFile, }, nil } func GetBuildkitHost(cmd *cobra.Command, namespace string) (string, error) { if cmd.Flags().Changed("buildkit-host") { // If address is explicitly specified, use it. buildkitHost, err := cmd.Flags().GetString("buildkit-host") if err != nil { return "", err } if err := buildkitutil.PingBKDaemon(buildkitHost); err != nil { return "", err } return buildkitHost, nil } return buildkitutil.GetBuildkitHost(namespace) } func buildAction(cmd *cobra.Command, args []string) error { options, err := processBuildCommandFlag(cmd, args) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return builder.Build(ctx, client, options) } // canonicalizeAttest is from https://github.com/docker/buildx/blob/v0.12/util/buildflags/attests.go##L13-L21 func canonicalizeAttest(attestType string, in string) string { if in == "" { return "" } if b, err := strconv.ParseBool(in); err == nil { return fmt.Sprintf("type=%s,disabled=%t", attestType, !b) } return fmt.Sprintf("type=%s,%s", attestType, in) } ================================================ FILE: cmd/nerdctl/builder/builder_build_oci_layout_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package builder import ( "fmt" "path/filepath" "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestBuildContextWithOCILayout(t *testing.T) { nerdtest.Setup() var dockerBuilderArgs []string testCase := &test.Case{ Require: require.All( nerdtest.Build, require.Not(require.Windows), ), Cleanup: func(data test.Data, helpers test.Helpers) { if nerdtest.IsDocker() { helpers.Anyhow("buildx", "stop", data.Identifier("container")) helpers.Anyhow("buildx", "rm", "--force", data.Identifier("container")) } helpers.Anyhow("rmi", "-f", data.Identifier("parent")) helpers.Anyhow("rmi", "-f", data.Identifier("child")) }, Setup: func(data test.Data, helpers test.Helpers) { // Default docker driver does not support OCI exporter. // Reference: https://docs.docker.com/build/exporters/oci-docker/ if nerdtest.IsDocker() { name := data.Identifier("container") helpers.Ensure("buildx", "create", "--name", name, "--driver=docker-container") dockerBuilderArgs = []string{"buildx", "--builder", name} } dockerfile := fmt.Sprintf(`FROM %s LABEL layer=oci-layout-parent CMD ["echo", "test-nerdctl-build-context-oci-layout-parent"]`, testutil.CommonImage) data.Temp().Save(dockerfile, "Dockerfile") dest := data.Temp().Dir("parent") tarPath := data.Temp().Path("parent.tar") helpers.Ensure("build", data.Temp().Path(), "--tag", data.Identifier("parent")) helpers.Ensure("image", "save", "--output", tarPath, data.Identifier("parent")) helpers.Custom("tar", "Cxf", dest, tarPath).Run(&test.Expected{ ExitCode: expect.ExitCodeSuccess, }) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { dockerfile := `FROM parent CMD ["echo", "test-nerdctl-build-context-oci-layout"]` data.Temp().Save(dockerfile, "Dockerfile") var cmd test.TestableCommand if nerdtest.IsDocker() { cmd = helpers.Command(dockerBuilderArgs...) } else { cmd = helpers.Command() } cmd.WithArgs( "build", data.Temp().Path(), fmt.Sprintf("--build-context=parent=oci-layout://%s", filepath.Join(data.Temp().Path(), "parent")), "--tag", data.Identifier("child"), ) if nerdtest.IsDocker() { // Need to load the container image from the builder to be able to run it. cmd.WithArgs("--load") } return cmd }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { assert.Assert( t, strings.Contains( helpers.Capture("run", "--rm", data.Identifier("child")), "test-nerdctl-build-context-oci-layout", ), ) }, } }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/builder/builder_build_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package builder import ( "errors" "fmt" "os" "path/filepath" "regexp" "runtime" "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/buildkitutil" "github.com/containerd/nerdctl/v2/pkg/platformutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestBuildBasics(t *testing.T) { nerdtest.Setup() dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-test-string"]`, testutil.CommonImage) testCase := &test.Case{ Require: nerdtest.Build, Setup: func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerfile, "Dockerfile") data.Labels().Set("buildCtx", data.Temp().Path()) }, SubTests: []*test.Case{ { Description: "Successfully build with 'tag first', 'buildctx second'", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("build", "-t", data.Identifier(), data.Labels().Get("buildCtx")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("nerdctl-build-test-string\n")), }, { Description: "Successfully build with 'buildctx first', 'tag second'", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("build", data.Labels().Get("buildCtx"), "-t", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("nerdctl-build-test-string\n")), }, { Description: "Successfully build with output docker, main tag still works", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure( "build", data.Labels().Get("buildCtx"), "-t", data.Identifier(), "--output=type=docker,name="+data.Identifier("ignored"), ) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("nerdctl-build-test-string\n")), }, { Description: "Successfully build with output docker, name cannot be used", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure( "build", data.Labels().Get("buildCtx"), "-t", data.Identifier(), "--output=type=docker,name="+data.Identifier("ignored"), ) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier("ignored")) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier("ignored")) helpers.Anyhow("rmi", "-f", data.Identifier()) }, Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, }, } testCase.Run(t) } func TestCanBuildOnOtherPlatform(t *testing.T) { nerdtest.Setup() requireEmulation := &test.Requirement{ Check: func(data test.Data, helpers test.Helpers) (bool, string) { candidateArch := "arm64" if runtime.GOARCH == "arm64" { candidateArch = "amd64" } can, err := platformutil.CanExecProbably("linux/" + candidateArch) assert.NilError(helpers.T(), err) data.Labels().Set("OS", "linux") data.Labels().Set("Architecture", candidateArch) return can, "Current environment does not support emulation" }, } dockerfile := fmt.Sprintf(`FROM %s RUN echo hello > /hello CMD ["echo", "nerdctl-build-test-string"]`, testutil.CommonImage) testCase := &test.Case{ Require: require.All( nerdtest.Build, requireEmulation, ), Setup: func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerfile, "Dockerfile") data.Labels().Set("buildCtx", data.Temp().Path()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command( "build", data.Labels().Get("buildCtx"), "--platform", fmt.Sprintf("%s/%s", data.Labels().Get("OS"), data.Labels().Get("Architecture")), "-t", data.Identifier(), ) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), } testCase.Run(t) } // TestBuildBaseImage tests if an image can be built on the previously built image. // This isn't currently supported by nerdctl with BuildKit OCI worker. func TestBuildBaseImage(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: nerdtest.Build, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier("first")) helpers.Anyhow("rmi", "-f", data.Identifier("second")) }, Setup: func(data test.Data, helpers test.Helpers) { dockerfile := fmt.Sprintf(`FROM %s RUN echo hello > /hello CMD ["echo", "nerdctl-build-test-string"]`, testutil.CommonImage) data.Temp().Save(dockerfile, "Dockerfile") helpers.Ensure("build", "-t", data.Identifier("first"), data.Temp().Path()) dockerfileSecond := fmt.Sprintf(`FROM %s RUN echo hello2 > /hello2 CMD ["cat", "/hello2"]`, data.Identifier("first")) data.Temp().Save(dockerfileSecond, "Dockerfile") helpers.Ensure("build", "-t", data.Identifier("second"), data.Temp().Path()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier("second")) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("hello2\n")), } testCase.Run(t) } // TestBuildFromContainerd tests if an image can be built on an image pulled by nerdctl. // This isn't currently supported by nerdctl with BuildKit OCI worker. func TestBuildFromContainerd(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.All( nerdtest.Build, require.Not(nerdtest.Docker), ), Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier("first")) helpers.Anyhow("rmi", "-f", data.Identifier("second")) }, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) helpers.Ensure("tag", testutil.CommonImage, data.Identifier("first")) dockerfile := fmt.Sprintf(`FROM %s RUN echo hello2 > /hello2 CMD ["cat", "/hello2"]`, data.Identifier("first")) data.Temp().Save(dockerfile, "Dockerfile") helpers.Ensure("build", "-t", data.Identifier("second"), data.Temp().Path()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier("second")) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("hello2\n")), } testCase.Run(t) } func TestBuildFromStdin(t *testing.T) { nerdtest.Setup() dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-test-stdin"]`, testutil.CommonImage) testCase := &test.Case{ Require: nerdtest.Build, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("build", "-t", data.Identifier(), "-f", "-", ".") cmd.Feed(strings.NewReader(dockerfile)) return cmd }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Errors: []error{errors.New(data.Identifier())}, } }, } testCase.Run(t) } func TestBuildWithDockerfile(t *testing.T) { nerdtest.Setup() dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-test-dockerfile"] `, testutil.CommonImage) testCase := &test.Case{ Require: nerdtest.Build, Setup: func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerfile, "test", "Dockerfile") data.Labels().Set("buildCtx", data.Temp().Path("test")) }, SubTests: []*test.Case{ { Description: "Dockerfile ..", Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("build", "-t", data.Identifier(), "-f", "Dockerfile", "..") cmd.WithCwd(data.Labels().Get("buildCtx")) return cmd }, Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, { Description: "Dockerfile .", Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("build", "-t", data.Identifier(), "-f", "Dockerfile", ".") cmd.WithCwd(data.Labels().Get("buildCtx")) return cmd }, Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, { Description: "../Dockerfile .", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("build", "-t", data.Identifier(), "-f", "../Dockerfile", ".") cmd.WithCwd(data.Labels().Get("buildCtx")) return cmd }, Expected: test.Expects(1, nil, nil), }, }, } testCase.Run(t) } func TestBuildLocal(t *testing.T) { nerdtest.Setup() const testFileName = "nerdctl-build-test" const testContent = "nerdctl" dockerfile := fmt.Sprintf(`FROM scratch COPY %s /`, testFileName) testCase := &test.Case{ Require: nerdtest.Build, Setup: func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerfile, "Dockerfile") data.Temp().Save(testContent, testFileName) data.Labels().Set("buildCtx", data.Temp().Path()) }, SubTests: []*test.Case{ { // GOTCHA: avoid comma and = in the test name, or buildctl will misparse the destination direction Description: "-o type local destination DIR: verify the file copied from context is in the output directory", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("build", "-o", fmt.Sprintf("type=local,dest=%s", data.Temp().Path()), data.Labels().Get("buildCtx")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { // Expecting testFileName to exist inside the output target directory assert.Equal(t, data.Temp().Load(testFileName), testContent, "file content is identical") }, } }, }, { Description: "-o DIR: verify the file copied from context is in the output directory", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("build", "-o", data.Temp().Path(), data.Labels().Get("buildCtx")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { assert.Equal(t, data.Temp().Load(testFileName), testContent, "file content is identical") }, } }, }, }, } testCase.Run(t) } func TestBuildWithBuildArg(t *testing.T) { nerdtest.Setup() dockerfile := fmt.Sprintf(`FROM %s ARG TEST_STRING=1 ENV TEST_STRING=$TEST_STRING CMD echo $TEST_STRING `, testutil.CommonImage) testCase := &test.Case{ Require: nerdtest.Build, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Setup: func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerfile, "Dockerfile") data.Labels().Set("buildCtx", data.Temp().Path()) }, SubTests: []*test.Case{ { Description: "No args", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("build", data.Labels().Get("buildCtx"), "-t", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("1\n")), }, { Description: "ArgValueOverridesDefault", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("build", data.Labels().Get("buildCtx"), "--build-arg", "TEST_STRING=2", "-t", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("2\n")), }, { Description: "EmptyArgValueOverridesDefault", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("build", data.Labels().Get("buildCtx"), "--build-arg", "TEST_STRING=", "-t", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("\n")), }, { Description: "UnsetArgKeyPreservesDefault", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("build", data.Labels().Get("buildCtx"), "--build-arg", "TEST_STRING", "-t", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("1\n")), }, { Description: "EnvValueOverridesDefault", Env: map[string]string{ "TEST_STRING": "3", }, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("build", data.Labels().Get("buildCtx"), "--build-arg", "TEST_STRING", "-t", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("3\n")), }, { Description: "EmptyEnvValueOverridesDefault", Env: map[string]string{ "TEST_STRING": "", }, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("build", data.Labels().Get("buildCtx"), "--build-arg", "TEST_STRING", "-t", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("\n")), }, }, } testCase.Run(t) } func TestBuildWithIIDFile(t *testing.T) { nerdtest.Setup() dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-test-string"] `, testutil.CommonImage) testCase := &test.Case{ Require: nerdtest.Build, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Setup: func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerfile, "Dockerfile") helpers.Ensure("build", data.Temp().Path(), "--iidfile", data.Temp().Path("id.txt"), "-t", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Temp().Load("id.txt")) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("nerdctl-build-test-string\n")), } testCase.Run(t) } func TestBuildWithLabels(t *testing.T) { nerdtest.Setup() dockerfile := fmt.Sprintf(`FROM %s LABEL name=nerdctl-build-test-label `, testutil.CommonImage) testCase := &test.Case{ Require: nerdtest.Build, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Setup: func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerfile, "Dockerfile") helpers.Ensure("build", data.Temp().Path(), "--label", "label=test", "-t", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("inspect", data.Identifier(), "--format", "{{json .Config.Labels }}") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("{\"label\":\"test\",\"name\":\"nerdctl-build-test-label\"}\n")), } testCase.Run(t) } func TestBuildMultipleTags(t *testing.T) { nerdtest.Setup() dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-test-string"] `, testutil.CommonImage) testCase := &test.Case{ Require: nerdtest.Build, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Labels().Get("i1")) helpers.Anyhow("rmi", "-f", data.Labels().Get("i2")) helpers.Anyhow("rmi", "-f", data.Labels().Get("i3")) }, Setup: func(data test.Data, helpers test.Helpers) { data.Labels().Set("i1", data.Identifier("image")) data.Labels().Set("i2", data.Identifier("image2")) data.Labels().Set("i3", data.Identifier("image3")+":hello") data.Temp().Save(dockerfile, "Dockerfile") helpers.Ensure( "build", data.Temp().Path(), "-t", data.Labels().Get("i1"), "-t", data.Labels().Get("i2"), "-t", data.Labels().Get("i3"), ) }, SubTests: []*test.Case{ { Description: "i1", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Labels().Get("i1")) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("nerdctl-build-test-string\n")), }, { Description: "i2", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Labels().Get("i2")) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("nerdctl-build-test-string\n")), }, { Description: "i3", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Labels().Get("i3")) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("nerdctl-build-test-string\n")), }, }, } testCase.Run(t) } func TestBuildWithContainerfile(t *testing.T) { nerdtest.Setup() dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-test-string"] `, testutil.CommonImage) testCase := &test.Case{ Require: require.All( nerdtest.Build, require.Not(nerdtest.Docker), ), Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Setup: func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerfile, "Dockerfile") helpers.Ensure("build", data.Temp().Path(), "-t", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier()) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("nerdctl-build-test-string\n")), } testCase.Run(t) } func TestBuildWithDockerFileAndContainerfile(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: nerdtest.Build, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Setup: func(data test.Data, helpers test.Helpers) { dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "dockerfile"] `, testutil.CommonImage) data.Temp().Save(dockerfile, "Dockerfile") dockerfile = fmt.Sprintf(`FROM %s CMD ["echo", "containerfile"] `, testutil.CommonImage) data.Temp().Save(dockerfile, "Containerfile") helpers.Ensure("build", data.Temp().Path(), "-t", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier()) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("dockerfile\n")), } testCase.Run(t) } func TestBuildNoTag(t *testing.T) { nerdtest.Setup() dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-test-string"] `, testutil.CommonImage) // FIXME: this test should be rewritten and instead get the image id from the build, then query the image explicitly - instead of pruning / noparallel testCase := &test.Case{ NoParallel: true, Require: nerdtest.Build, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("image", "prune", "--force", "--all") }, Setup: func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerfile, "Dockerfile") // XXX FIXME helpers.Capture("build", data.Temp().Path()) }, Command: test.Command("images"), Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("")), } testCase.Run(t) } func TestBuildContextDockerImageAlias(t *testing.T) { nerdtest.Setup() dockerfile := `FROM myorg/myapp CMD ["echo", "nerdctl-build-myorg/myapp"]` testCase := &test.Case{ Require: nerdtest.Build, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Setup: func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerfile, "Dockerfile") }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command( "build", "-t", data.Identifier(), data.Temp().Path(), fmt.Sprintf("--build-context=myorg/myapp=docker-image://%s", testutil.CommonImage), ) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), } testCase.Run(t) } func TestBuildContextWithCopyFromDir(t *testing.T) { nerdtest.Setup() content := "hello_from_dir_2" filename := "hello.txt" dockerfile := fmt.Sprintf(`FROM %s COPY --from=dir2 /%s /hello_from_dir2.txt RUN ["cat", "/hello_from_dir2.txt"]`, testutil.CommonImage, filename) testCase := &test.Case{ Require: require.All( nerdtest.Build, require.Not(nerdtest.Docker), ), Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Setup: func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerfile, "context", "Dockerfile") data.Temp().Save(content, "other-directory", filename) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command( "build", "-t", data.Identifier(), data.Temp().Path("context"), fmt.Sprintf("--build-context=dir2=%s", data.Temp().Path("other-directory")), ) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), } testCase.Run(t) } // TestBuildSourceDateEpoch tests that $SOURCE_DATE_EPOCH is propagated from the client env // https://github.com/docker/buildx/pull/1482 func TestBuildSourceDateEpoch(t *testing.T) { nerdtest.Setup() dockerfile := fmt.Sprintf(`FROM %s ARG SOURCE_DATE_EPOCH RUN echo $SOURCE_DATE_EPOCH >/source-date-epoch CMD ["cat", "/source-date-epoch"] `, testutil.CommonImage) testCase := &test.Case{ Require: require.All( nerdtest.Build, require.Not(nerdtest.Docker), ), Setup: func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerfile, "Dockerfile") data.Labels().Set("buildCtx", data.Temp().Path()) }, SubTests: []*test.Case{ { Description: "1111111111", Env: map[string]string{ "SOURCE_DATE_EPOCH": "1111111111", }, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("build", data.Labels().Get("buildCtx"), "-t", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier()) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("1111111111\n")), }, { Description: "2222222222", Env: map[string]string{ "SOURCE_DATE_EPOCH": "1111111111", }, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("build", data.Labels().Get("buildCtx"), "--build-arg", "SOURCE_DATE_EPOCH=2222222222", "-t", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier()) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("2222222222\n")), }, }, } testCase.Run(t) } func TestBuildNetwork(t *testing.T) { nerdtest.Setup() dockerfile := fmt.Sprintf(`FROM %s RUN apk add --no-cache curl RUN curl -I http://google.com `, testutil.CommonImage) testCase := &test.Case{ Require: require.All( nerdtest.Build, require.Not(nerdtest.Docker), ), Setup: func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerfile, "Dockerfile") data.Labels().Set("buildCtx", data.Temp().Path()) }, SubTests: []*test.Case{ { Description: "none", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("build", data.Labels().Get("buildCtx"), "-t", data.Identifier(), "--no-cache", "--network", "none") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Expected: test.Expects(1, nil, nil), }, { Description: "empty", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("build", data.Labels().Get("buildCtx"), "-t", data.Identifier(), "--no-cache", "--network", "") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, { Description: "default", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("build", data.Labels().Get("buildCtx"), "-t", data.Identifier(), "--no-cache", "--network", "default") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, }, } testCase.Run(t) } func TestBuildAttestation(t *testing.T) { nerdtest.Setup() // Using regex patterns to match SBOM and provenance files with optional platform suffix const testSBOMFilePattern = `sbom\.spdx(?:\.[a-z0-9_]+)?\.json` const testProvenanceFilePattern = `provenance(?:\.[a-z0-9_]+)?\.json` dockerfile := fmt.Sprintf(`FROM %s`, testutil.CommonImage) testCase := &test.Case{ Require: require.All( nerdtest.Build, require.Not(nerdtest.Docker), ), Cleanup: func(data test.Data, helpers test.Helpers) { if nerdtest.IsDocker() { helpers.Anyhow("buildx", "rm", data.Identifier("builder")) } }, Setup: func(data test.Data, helpers test.Helpers) { if nerdtest.IsDocker() { helpers.Anyhow("buildx", "create", "--name", data.Identifier("builder"), "--bootstrap", "--use") } data.Temp().Save(dockerfile, "Dockerfile") data.Labels().Set("buildCtx", data.Temp().Path()) }, SubTests: []*test.Case{ { Description: "SBOM", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("build") if nerdtest.IsDocker() { cmd.WithArgs("--builder", data.Identifier("builder")) } cmd.WithArgs( "--sbom=true", "-o", fmt.Sprintf("type=local,dest=%s", data.Temp().Path("dir-for-bom")), data.Labels().Get("buildCtx"), ) return cmd }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { files, err := os.ReadDir(data.Temp().Path("dir-for-bom")) assert.NilError(t, err, "failed to read directory") found := false for _, file := range files { if !file.IsDir() && regexp.MustCompile(testSBOMFilePattern).MatchString(file.Name()) { found = true break } } assert.Assert(t, found, "no SBOM file matching pattern %s found", testSBOMFilePattern) }, } }, }, { Description: "Provenance", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("build") if nerdtest.IsDocker() { cmd.WithArgs("--builder", data.Identifier("builder")) } cmd.WithArgs( "--provenance=mode=min", "-o", fmt.Sprintf("type=local,dest=%s", data.Temp().Path("dir-for-prov")), data.Labels().Get("buildCtx"), ) return cmd }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { files, err := os.ReadDir(data.Temp().Path("dir-for-prov")) assert.NilError(t, err, "failed to read directory") found := false for _, file := range files { if !file.IsDir() && regexp.MustCompile(testProvenanceFilePattern).MatchString(file.Name()) { found = true break } } assert.Assert(t, found, "no provenance file matching pattern %s found", testProvenanceFilePattern) }, } }, }, { Description: "Attestation", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("build") if nerdtest.IsDocker() { cmd.WithArgs("--builder", data.Identifier("builder")) } cmd.WithArgs( "--attest=type=provenance,mode=min", "--attest=type=sbom", "-o", fmt.Sprintf("type=local,dest=%s", data.Temp().Path("dir-for-attest")), data.Labels().Get("buildCtx"), ) return cmd }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { // Check if any file in the directory matches the SBOM file pattern files, err := os.ReadDir(data.Temp().Path("dir-for-attest")) assert.NilError(t, err, "failed to read directory") sbomFound := false for _, file := range files { if !file.IsDir() && regexp.MustCompile(testSBOMFilePattern).MatchString(file.Name()) { sbomFound = true break } } assert.Assert(t, sbomFound, "no SBOM file matching pattern %s found", testSBOMFilePattern) // Check if any file in the directory matches the provenance file pattern provenanceFound := false for _, file := range files { if !file.IsDir() && regexp.MustCompile(testProvenanceFilePattern).MatchString(file.Name()) { provenanceFound = true break } } assert.Assert(t, provenanceFound, "no provenance file matching pattern %s found", testProvenanceFilePattern) }, } }, }, }, } testCase.Run(t) } func TestBuildAddHost(t *testing.T) { nerdtest.Setup() dockerfile := fmt.Sprintf(`FROM %s RUN ping -c 5 alpha RUN ping -c 5 beta `, testutil.CommonImage) testCase := &test.Case{ Require: require.All( nerdtest.Build, ), Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Setup: func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerfile, "Dockerfile") }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command( "build", data.Temp().Path(), "-t", data.Identifier(), "--add-host", "alpha:127.0.0.1", "--add-host", "beta:127.0.0.1", ) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), } testCase.Run(t) } func TestBuildWithBuildkitConfig(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.All( nerdtest.Build, require.Not(nerdtest.Docker), ), Setup: func(data test.Data, helpers test.Helpers) { dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-test-string"]`, testutil.CommonImage) data.Temp().Save(dockerfile, "Dockerfile") data.Labels().Set("buildCtx", data.Temp().Path()) }, SubTests: []*test.Case{ { Description: "build with buildkit-host", Setup: func(data test.Data, helpers test.Helpers) { // Get BuildkitAddr buildkitAddr, err := buildkitutil.GetBuildkitHost(testutil.Namespace) assert.NilError(helpers.T(), err) buildkitAddr = strings.TrimPrefix(buildkitAddr, "unix://") // Symlink the buildkit Socket for testing symlinkedBuildkitAddr := filepath.Join(data.Temp().Path(), "buildkit.sock") // Do a negative test to check the setup helpers.Fail("build", "-t", data.Identifier(), "--buildkit-host", fmt.Sprintf("unix://%s", symlinkedBuildkitAddr), data.Labels().Get("buildCtx")) // Test build with the symlinked socket cmd := helpers.Custom("ln", "-s", buildkitAddr, symlinkedBuildkitAddr) cmd.Run(&test.Expected{ ExitCode: 0, }) helpers.Ensure("build", "-t", data.Identifier(), "--buildkit-host", fmt.Sprintf("unix://%s", symlinkedBuildkitAddr), data.Labels().Get("buildCtx")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Expected: test.Expects(0, nil, expect.Equals("nerdctl-build-test-string\n")), }, { Description: "build with env specified", Setup: func(data test.Data, helpers test.Helpers) { // Get BuildkitAddr buildkitAddr, err := buildkitutil.GetBuildkitHost(testutil.Namespace) assert.NilError(helpers.T(), err) buildkitAddr = strings.TrimPrefix(buildkitAddr, "unix://") // Symlink the buildkit Socket for testing symlinkedBuildkitAddr := filepath.Join(data.Temp().Path(), "buildkit-env.sock") // Do a negative test to ensure setting up the env variable is effective cmd := helpers.Command("build", "-t", data.Identifier(), data.Labels().Get("buildCtx")) cmd.Setenv("BUILDKIT_HOST", fmt.Sprintf("unix://%s", symlinkedBuildkitAddr)) cmd.Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) // Symlink the buildkit socket for testing cmd = helpers.Custom("ln", "-s", buildkitAddr, symlinkedBuildkitAddr) cmd.Run(&test.Expected{ ExitCode: 0, }) cmd = helpers.Command("build", "-t", data.Identifier(), data.Labels().Get("buildCtx")) cmd.Setenv("BUILDKIT_HOST", fmt.Sprintf("unix://%s", symlinkedBuildkitAddr)) cmd.Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Expected: test.Expects(0, nil, expect.Equals("nerdctl-build-test-string\n")), }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/builder/builder_builder_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package builder import ( "errors" "fmt" "path/filepath" "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/buildkitutil" "github.com/containerd/nerdctl/v2/pkg/referenceutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestBuilder(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ NoParallel: true, Require: require.All( nerdtest.Build, require.Not(require.Windows), ), SubTests: []*test.Case{ { Description: "PruneForce", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-test-builder-prune"]`, testutil.CommonImage) data.Temp().Save(dockerfile, "Dockerfile") helpers.Ensure("build", data.Temp().Path()) }, Command: test.Command("builder", "prune", "--force"), Expected: test.Expects(0, nil, nil), }, { Description: "PruneForceAll", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-test-builder-prune"]`, testutil.CommonImage) data.Temp().Save(dockerfile, "Dockerfile") helpers.Ensure("build", data.Temp().Path()) }, Command: test.Command("builder", "prune", "--force", "--all"), Expected: test.Expects(0, nil, nil), }, { Description: "builder with buildkit-host", NoParallel: true, Require: require.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { // Get BuildkitAddr buildkitAddr, err := buildkitutil.GetBuildkitHost(testutil.Namespace) assert.NilError(helpers.T(), err) buildkitAddr = strings.TrimPrefix(buildkitAddr, "unix://") // Symlink the buildkit Socket for testing symlinkedBuildkitAddr := filepath.Join(data.Temp().Path(), "buildkit.sock") data.Labels().Set("symlinkedBuildkitAddr", symlinkedBuildkitAddr) // Do a negative test to check the setup helpers.Fail("builder", "prune", "--force", "--buildkit-host", fmt.Sprintf("unix://%s", symlinkedBuildkitAddr)) // Test build with the symlinked socket cmd := helpers.Custom("ln", "-s", buildkitAddr, symlinkedBuildkitAddr) cmd.Run(&test.Expected{ ExitCode: 0, }) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("builder", "prune", "--force", "--buildkit-host", fmt.Sprintf("unix://%s", data.Labels().Get("symlinkedBuildkitAddr"))) }, Expected: test.Expects(0, nil, nil), }, { Description: "builder with env", NoParallel: true, Require: require.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { // Get BuildkitAddr buildkitAddr, err := buildkitutil.GetBuildkitHost(testutil.Namespace) assert.NilError(helpers.T(), err) buildkitAddr = strings.TrimPrefix(buildkitAddr, "unix://") // Symlink the buildkit Socket for testing symlinkedBuildkitAddr := filepath.Join(data.Temp().Path(), "buildkit-env.sock") data.Labels().Set("symlinkedBuildkitAddr", symlinkedBuildkitAddr) // Do a negative test to ensure setting up the env variable is effective cmd := helpers.Command("builder", "prune", "--force") cmd.Setenv("BUILDKIT_HOST", fmt.Sprintf("unix://%s", symlinkedBuildkitAddr)) cmd.Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) // Symlink the buildkit socket for testing cmd = helpers.Custom("ln", "-s", buildkitAddr, symlinkedBuildkitAddr) cmd.Run(&test.Expected{ ExitCode: 0, }) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { symlinkedBuildkitAddr := data.Labels().Get("symlinkedBuildkitAddr") cmd := helpers.Command("builder", "prune", "--force") cmd.Setenv("BUILDKIT_HOST", fmt.Sprintf("unix://%s", symlinkedBuildkitAddr)) return cmd }, Expected: test.Expects(0, nil, nil), }, { Description: "Debug", // `nerdctl builder debug` is currently incompatible with `docker buildx debug`. Require: require.All(require.Not(nerdtest.Docker)), NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-builder-debug-test-string"]`, testutil.CommonImage) data.Temp().Save(dockerfile, "Dockerfile") cmd := helpers.Command("builder", "debug", data.Temp().Path()) cmd.Feed(strings.NewReader("c\n")) return cmd }, Expected: test.Expects(0, nil, nil), }, { Description: "WithPull", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { // FIXME: this test should be rewritten to dynamically retrieve the ids, and use images // available on all platforms oldImage := testutil.BusyboxImage parsedOldImage, err := referenceutil.Parse(oldImage) assert.NilError(helpers.T(), err) oldImageSha := parsedOldImage.Digest.String() newImage := testutil.AlpineImage parsedNewImage, err := referenceutil.Parse(newImage) assert.NilError(helpers.T(), err) newImageSha := parsedNewImage.Digest.String() helpers.Ensure("pull", "--quiet", oldImage) helpers.Ensure("tag", oldImage, parsedNewImage.Domain+"/"+parsedNewImage.Path+":"+parsedNewImage.Tag) dockerfile := fmt.Sprintf(`FROM %s`, parsedNewImage.Domain+"/"+parsedNewImage.Path+":"+parsedNewImage.Tag) data.Temp().Save(dockerfile, "Dockerfile") data.Labels().Set("oldImageSha", oldImageSha) data.Labels().Set("newImageSha", newImageSha) data.Labels().Set("base", data.Temp().Dir()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", testutil.AlpineImage) }, SubTests: []*test.Case{ { Description: "pull false", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("build", data.Labels().Get("base"), "--pull=false") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Errors: []error{errors.New(data.Labels().Get("oldImageSha"))}, } }, }, { Description: "pull true", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("build", data.Labels().Get("base"), "--pull=true") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Errors: []error{errors.New(data.Labels().Get("newImageSha"))}, } }, }, { Description: "no pull", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("build", data.Labels().Get("base")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Errors: []error{errors.New(data.Labels().Get("newImageSha"))}, } }, }, }, }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/builder/builder_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package builder import ( "testing" "github.com/containerd/nerdctl/v2/pkg/testutil" ) func TestMain(m *testing.M) { testutil.M(m) } ================================================ FILE: cmd/nerdctl/checkpoint/checkpoint.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package checkpoint import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" ) func Command() *cobra.Command { cmd := &cobra.Command{ Annotations: map[string]string{helpers.Category: helpers.Management}, Use: "checkpoint", Short: "Manage checkpoints.", RunE: helpers.UnknownSubcommandAction, SilenceUsage: true, SilenceErrors: true, } cmd.AddCommand( createCommand(), lsCommand(), rmCommand(), ) return cmd } func lsCommand() *cobra.Command { x := listCommand() x.Use = "ls" x.Aliases = []string{"list"} return x } func rmCommand() *cobra.Command { x := removeCommand() x.Use = "rm" x.Aliases = []string{"remove"} return x } ================================================ FILE: cmd/nerdctl/checkpoint/checkpoint_create.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package checkpoint import ( "path/filepath" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/checkpoint" ) func createCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "create [OPTIONS] CONTAINER CHECKPOINT", Short: "Create a checkpoint from a running container", Args: cobra.ExactArgs(2), RunE: createAction, ValidArgsFunction: createShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().Bool("leave-running", false, "Leave the container running after checkpointing") cmd.Flags().String("checkpoint-dir", "", "Checkpoint directory") return cmd } func processCreateFlags(cmd *cobra.Command) (types.CheckpointCreateOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.CheckpointCreateOptions{}, err } leaveRunning, err := cmd.Flags().GetBool("leave-running") if err != nil { return types.CheckpointCreateOptions{}, err } checkpointDir, err := cmd.Flags().GetString("checkpoint-dir") if err != nil { return types.CheckpointCreateOptions{}, err } if checkpointDir == "" { checkpointDir = filepath.Join(globalOptions.DataRoot, "checkpoints") } return types.CheckpointCreateOptions{ Stdout: cmd.OutOrStdout(), GOptions: globalOptions, LeaveRunning: leaveRunning, CheckpointDir: checkpointDir, }, nil } func createAction(cmd *cobra.Command, args []string) error { createOptions, err := processCreateFlags(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), createOptions.GOptions.Namespace, createOptions.GOptions.Address) if err != nil { return err } defer cancel() err = checkpoint.Create(ctx, client, args[0], args[1], createOptions) if err != nil { return err } return nil } func createShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.ImageNames(cmd) } ================================================ FILE: cmd/nerdctl/checkpoint/checkpoint_create_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package checkpoint import ( "errors" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestCheckpointCreateErrors(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.All( require.Not(nerdtest.Rootless), // Docker version 28.x has a known regression that breaks Checkpoint/Restore functionality. // The issue is tracked in the moby/moby project as https://github.com/moby/moby/issues/50750. require.Not(nerdtest.Docker), ) testCase.SubTests = []*test.Case{ { Description: "too-few-arguments", Command: test.Command("checkpoint", "create", "too-few-arguments"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, } }, }, { Description: "too-many-arguments", Command: test.Command("checkpoint", "create", "too", "many", "arguments"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, } }, }, { Description: "invalid-container-id", Command: test.Command("checkpoint", "create", "foo", "bar"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New("error creating checkpoint for container: foo")}, } }, }, } testCase.Run(t) } func TestCheckpointCreate(t *testing.T) { const ( checkpointName = "checkpoint-bar" checkpointDir = "/dir/foo" ) testCase := nerdtest.Setup() testCase.Require = require.All( require.Not(nerdtest.Rootless), // Docker version 28.x has a known regression that breaks Checkpoint/Restore functionality. // The issue is tracked in the moby/moby project as https://github.com/moby/moby/issues/50750. require.Not(nerdtest.Docker), ) testCase.NoParallel = true testCase.SubTests = []*test.Case{ { Description: "leave-running=true", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier("container-running"), testutil.CommonImage, "sleep", "infinity") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("container-running")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("checkpoint", "create", "--leave-running", "--checkpoint-dir", checkpointDir, data.Identifier("container-running"), checkpointName+"running") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.Equals(checkpointName + "running\n"), } }, }, { Description: "leave-running=false", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier("container-exit"), testutil.CommonImage, "sleep", "infinity") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("container-exit")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("checkpoint", "create", "--checkpoint-dir", checkpointDir, data.Identifier("container-exit"), checkpointName+"exit") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.Equals(checkpointName + "exit\n"), } }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/checkpoint/checkpoint_list.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package checkpoint import ( "fmt" "text/tabwriter" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/checkpoint" ) func listCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "list [OPTIONS] CONTAINER", Short: "List checkpoints for a container", Args: cobra.ExactArgs(1), RunE: listAction, ValidArgsFunction: listShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().String("checkpoint-dir", "", "Checkpoint directory") return cmd } func processListFlags(cmd *cobra.Command) (types.CheckpointListOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.CheckpointListOptions{}, err } checkpointDir, err := cmd.Flags().GetString("checkpoint-dir") if err != nil { return types.CheckpointListOptions{}, err } if checkpointDir == "" { checkpointDir = globalOptions.DataRoot + "/checkpoints" } return types.CheckpointListOptions{ Stdout: cmd.OutOrStdout(), GOptions: globalOptions, CheckpointDir: checkpointDir, }, nil } func listAction(cmd *cobra.Command, args []string) error { listOptions, err := processListFlags(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), listOptions.GOptions.Namespace, listOptions.GOptions.Address) if err != nil { return err } defer cancel() checkpoints, err := checkpoint.List(ctx, client, args[0], listOptions) if err != nil { return err } w := tabwriter.NewWriter(listOptions.Stdout, 4, 8, 4, ' ', 0) fmt.Fprintln(w, "CHECKPOINT NAME") for _, cp := range checkpoints { fmt.Fprintln(w, cp.Name) } return w.Flush() } func listShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.ImageNames(cmd) } ================================================ FILE: cmd/nerdctl/checkpoint/checkpoint_list_linux_test.go ================================================ //go:build linux /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package checkpoint import ( "errors" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestCheckpointListErrors(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.All( require.Not(nerdtest.Rootless), // Docker version 28.x has a known regression that breaks Checkpoint/Restore functionality. // The issue is tracked in the moby/moby project as https://github.com/moby/moby/issues/50750. require.Not(nerdtest.Docker), ) testCase.SubTests = []*test.Case{ { Description: "too-few-arguments", Command: test.Command("checkpoint", "list"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ExitCode: 1} }, }, { Description: "too-many-arguments", Command: test.Command("checkpoint", "list", "too", "many", "arguments"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ExitCode: 1} }, }, { Description: "invalid-container-id", Command: test.Command("checkpoint", "list", "no-such-container"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New("error list checkpoint for container: no-such-container")}, } }, }, } testCase.Run(t) } func TestCheckpointList(t *testing.T) { const checkpointName = "checkpoint-list" testCase := nerdtest.Setup() testCase.Require = require.All( require.Not(nerdtest.Rootless), // Docker version 28.x has a known regression that breaks Checkpoint/Restore functionality. // The issue is tracked in the moby/moby project as https://github.com/moby/moby/issues/50750. require.Not(nerdtest.Docker), ) testCase.NoParallel = true testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") helpers.Ensure("checkpoint", "create", data.Identifier(), checkpointName) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("checkpoint", "list", data.Identifier()) } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, // First line is header, second should include the checkpoint name Output: expect.Contains("CHECKPOINT NAME\n" + checkpointName + "\n"), } } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/checkpoint/checkpoint_remove.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package checkpoint import ( "path/filepath" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/checkpoint" ) func removeCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "rm [OPTIONS] CONTAINER CHECKPOINT", Short: "Remove a checkpoint", Args: cobra.ExactArgs(2), RunE: removeAction, ValidArgsFunction: removeShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().String("checkpoint-dir", "", "Checkpoint directory") return cmd } func processRemoveFlags(cmd *cobra.Command) (types.CheckpointRemoveOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.CheckpointRemoveOptions{}, err } checkpointDir, err := cmd.Flags().GetString("checkpoint-dir") if err != nil { return types.CheckpointRemoveOptions{}, err } if checkpointDir == "" { checkpointDir = filepath.Join(globalOptions.DataRoot, "checkpoints") } return types.CheckpointRemoveOptions{ Stdout: cmd.OutOrStdout(), GOptions: globalOptions, CheckpointDir: checkpointDir, }, nil } func removeAction(cmd *cobra.Command, args []string) error { removeOptions, err := processRemoveFlags(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), removeOptions.GOptions.Namespace, removeOptions.GOptions.Address) if err != nil { return err } defer cancel() err = checkpoint.Remove(ctx, client, args[0], args[1], removeOptions) if err != nil { return err } return nil } func removeShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.ImageNames(cmd) } ================================================ FILE: cmd/nerdctl/checkpoint/checkpoint_remove_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package checkpoint import ( "errors" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestCheckpointRemoveErrors(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.All( require.Not(nerdtest.Rootless), // Docker version 28.x has a known regression that breaks Checkpoint/Restore functionality. // The issue is tracked in the moby/moby project as https://github.com/moby/moby/issues/50750. require.Not(nerdtest.Docker), ) testCase.SubTests = []*test.Case{ { Description: "too-few-arguments", Command: test.Command("checkpoint", "rm", "too-few-arguments"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, } }, }, { Description: "too-many-arguments", Command: test.Command("checkpoint", "rm", "too", "many", "arguments"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, } }, }, { Description: "invalid-container-id", Command: test.Command("checkpoint", "rm", "foo", "bar"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New("error removing checkpoint for container: foo")}, } }, }, } testCase.Run(t) } func TestCheckpointRemove(t *testing.T) { const ( checkpointName = "checkpoint-remove" checkpointDir = "/dir/remove" ) testCase := nerdtest.Setup() testCase.Require = require.All( require.Not(nerdtest.Rootless), // Docker version 28.x has a known regression that breaks Checkpoint/Restore functionality. // The issue is tracked in the moby/moby project as https://github.com/moby/moby/issues/50750. require.Not(nerdtest.Docker), ) testCase.NoParallel = true testCase.SubTests = []*test.Case{ { Description: "remove-existing", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier("container-running-remove"), testutil.CommonImage, "sleep", "infinity") helpers.Ensure("checkpoint", "create", "--checkpoint-dir", checkpointDir, data.Identifier("container-running-remove"), checkpointName) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("container-running-remove")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("checkpoint", "rm", "--checkpoint-dir", checkpointDir, data.Identifier("container-running-remove"), checkpointName) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.Equals(""), } }, }, { Description: "remove-nonexistent-checkpoint", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier("container-clean-remove"), testutil.CommonImage, "sleep", "infinity") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("container-clean-remove")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("checkpoint", "rm", "--checkpoint-dir", checkpointDir, data.Identifier("container-clean-remove"), checkpointName) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New("checkpoint " + checkpointName + " does not exist for container")}, } }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/checkpoint/checkpoint_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package checkpoint import ( "testing" "github.com/containerd/nerdctl/v2/pkg/testutil" ) func TestMain(m *testing.M) { testutil.M(m) } ================================================ FILE: cmd/nerdctl/completion/completion.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package completion import ( "context" "time" "github.com/spf13/cobra" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/volume" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" "github.com/containerd/nerdctl/v2/pkg/labels" "github.com/containerd/nerdctl/v2/pkg/netutil" ) func ImageNames(cmd *cobra.Command) ([]string, cobra.ShellCompDirective) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return nil, cobra.ShellCompDirectiveError } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return nil, cobra.ShellCompDirectiveError } defer cancel() imageList, err := client.ImageService().List(ctx, "") if err != nil { return nil, cobra.ShellCompDirectiveError } candidates := []string{} for _, img := range imageList { candidates = append(candidates, img.Name) } return candidates, cobra.ShellCompDirectiveNoFileComp } func ContainerNames(cmd *cobra.Command, filterFunc func(containerd.ProcessStatus) bool) ([]string, cobra.ShellCompDirective) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return nil, cobra.ShellCompDirectiveError } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return nil, cobra.ShellCompDirectiveError } defer cancel() containers, err := client.Containers(ctx) if err != nil { return nil, cobra.ShellCompDirectiveError } getStatus := func(c containerd.Container) containerd.ProcessStatus { ctx2, cancel2 := context.WithTimeout(ctx, 100*time.Millisecond) defer cancel2() task, err := c.Task(ctx2, nil) if err != nil { return containerd.Unknown } st, err := task.Status(ctx2) if err != nil { return containerd.Unknown } return st.Status } candidates := []string{} for _, c := range containers { if filterFunc != nil { if !filterFunc(getStatus(c)) { continue } } lab, err := c.Labels(ctx) if err != nil { continue } name := lab[labels.Name] if name != "" { candidates = append(candidates, name) continue } candidates = append(candidates, c.ID()) } return candidates, cobra.ShellCompDirectiveNoFileComp } // NetworkNames includes {"bridge","host","none"} func NetworkNames(cmd *cobra.Command, exclude []string) ([]string, cobra.ShellCompDirective) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return nil, cobra.ShellCompDirectiveError } excludeMap := make(map[string]struct{}, len(exclude)) for _, ex := range exclude { excludeMap[ex] = struct{}{} } e, err := netutil.NewCNIEnv(globalOptions.CNIPath, globalOptions.CNINetConfPath, netutil.WithNamespace(globalOptions.Namespace)) if err != nil { return nil, cobra.ShellCompDirectiveError } candidates := []string{} netConfigs, err := e.NetworkMap() if err != nil { return nil, cobra.ShellCompDirectiveError } for netName, network := range netConfigs { if _, ok := excludeMap[netName]; !ok { candidates = append(candidates, netName) if network.NerdctlID != nil { candidates = append(candidates, *network.NerdctlID) candidates = append(candidates, (*network.NerdctlID)[0:12]) } } } for _, s := range []string{"host", "none"} { if _, ok := excludeMap[s]; !ok { candidates = append(candidates, s) } } return candidates, cobra.ShellCompDirectiveNoFileComp } func VolumeNames(cmd *cobra.Command) ([]string, cobra.ShellCompDirective) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return nil, cobra.ShellCompDirectiveError } vols, err := getVolumes(cmd, globalOptions) if err != nil { return nil, cobra.ShellCompDirectiveError } candidates := []string{} for _, v := range vols { candidates = append(candidates, v.Name) } return candidates, cobra.ShellCompDirectiveNoFileComp } func Platforms(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { candidates := []string{ "amd64", "arm64", "riscv64", "ppc64le", "s390x", "loong64", "386", "arm", // alias of "linux/arm/v7" "linux/arm/v6", // "arm/v6" is invalid (interpreted as OS="arm", Arch="v7") } return candidates, cobra.ShellCompDirectiveNoFileComp } func getVolumes(cmd *cobra.Command, globalOptions types.GlobalCommandOptions) (map[string]native.Volume, error) { volumeSize, err := cmd.Flags().GetBool("size") if err != nil { // The `nerdctl volume rm` does not have the flag `size`, so set it to false as the default value. volumeSize = false } return volume.Volumes(globalOptions.Namespace, globalOptions.DataRoot, globalOptions.Address, volumeSize, nil) } ================================================ FILE: cmd/nerdctl/completion/completion_linux.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package completion import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/pkg/apparmorutil" ncdefaults "github.com/containerd/nerdctl/v2/pkg/defaults" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" ) func ApparmorProfiles(cmd *cobra.Command) ([]string, cobra.ShellCompDirective) { profiles, err := apparmorutil.Profiles() if err != nil { return nil, cobra.ShellCompDirectiveError } var names []string // nolint: prealloc for _, f := range profiles { names = append(names, f.Name) } return names, cobra.ShellCompDirectiveNoFileComp } func CgroupManagerNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { candidates := []string{"cgroupfs"} if ncdefaults.IsSystemdAvailable() { candidates = append(candidates, "systemd") } if rootlessutil.IsRootless() { candidates = append(candidates, "none") } return candidates, cobra.ShellCompDirectiveNoFileComp } ================================================ FILE: cmd/nerdctl/completion/completion_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package completion import ( "os/exec" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestMain(m *testing.M) { testutil.M(m) } func TestCompletion(t *testing.T) { nerdtest.Setup() // Note: some functions need to be tested without the automatic --namespace nerdctl-test argument, so we need // to retrieve the binary name. // Note that we know this works already, so no need to assert err. bin, _ := exec.LookPath(testutil.GetTarget()) testCase := &test.Case{ Require: require.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() helpers.Ensure("pull", "--quiet", testutil.CommonImage) helpers.Ensure("network", "create", identifier) helpers.Ensure("volume", "create", identifier) data.Labels().Set("identifier", identifier) }, Cleanup: func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() helpers.Anyhow("network", "rm", identifier) helpers.Anyhow("volume", "rm", identifier) }, SubTests: []*test.Case{ { Description: "--cgroup-manager", Require: require.Not(require.Windows), Command: test.Command("__complete", "--cgroup-manager", ""), Expected: test.Expects(0, nil, expect.Contains("cgroupfs\n")), }, { Description: "--snapshotter", Require: require.Not(require.Windows), Command: test.Command("__complete", "--snapshotter", ""), Expected: test.Expects(0, nil, expect.Contains("native\n")), }, { Description: "empty", Command: test.Command("__complete", ""), Expected: test.Expects(0, nil, expect.Contains("run\t")), }, { Description: "build --network", Command: test.Command("__complete", "build", "--network", ""), Expected: test.Expects(0, nil, expect.Contains("default\n")), }, { Description: "run -", Command: test.Command("__complete", "run", "-"), Expected: test.Expects(0, nil, expect.Contains("--network\t")), }, { Description: "run --n", Command: test.Command("__complete", "run", "--n"), Expected: test.Expects(0, nil, expect.Contains("--network\t")), }, { Description: "run --ne", Command: test.Command("__complete", "run", "--ne"), Expected: test.Expects(0, nil, expect.Contains("--network\t")), }, { Description: "run --net", Command: test.Command("__complete", "run", "--net", ""), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.Contains("host\n", data.Labels().Get("identifier")+"\n"), } }, }, { Description: "run -it --net", Command: test.Command("__complete", "run", "-it", "--net", ""), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.Contains("host\n", data.Labels().Get("identifier")+"\n"), } }, }, { Description: "run -ti --rm --net", Command: test.Command("__complete", "run", "-it", "--rm", "--net", ""), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.Contains("host\n", data.Labels().Get("identifier")+"\n"), } }, }, { Description: "run --restart", Command: test.Command("__complete", "run", "--restart", ""), Expected: test.Expects(0, nil, expect.Contains("always\n")), }, { Description: "network --rm", Command: test.Command("__complete", "network", "rm", ""), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( expect.DoesNotContain("host\n"), expect.Contains(data.Labels().Get("identifier")+"\n"), ), } }, }, { Description: "run --cap-add", Require: require.Not(require.Windows), Command: test.Command("__complete", "run", "--cap-add", ""), Expected: test.Expects(0, nil, expect.All( expect.Contains("sys_admin\n"), expect.DoesNotContain("CAP_SYS_ADMIN\n"), )), }, { Description: "volume inspect", Command: test.Command("__complete", "volume", "inspect", ""), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.Contains(data.Labels().Get("identifier") + "\n"), } }, }, { Description: "volume rm", Command: test.Command("__complete", "volume", "rm", ""), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.Contains(data.Labels().Get("identifier") + "\n"), } }, }, { Description: "--cgroup-manager", Require: require.Not(require.Windows), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("__complete", "--cgroup-manager", "") }, Expected: test.Expects(0, nil, expect.Contains("cgroupfs\n")), }, { Description: "empty", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("__complete", "") }, Expected: test.Expects(0, nil, expect.Contains("run\t")), }, { Description: "namespace space empty", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { // mind {"--namespace=nerdctl-test"} vs {"--namespace", "nerdctl-test"} return helpers.Custom(bin, "__complete", "--namespace", string(helpers.Read(nerdtest.Namespace)), "") }, Expected: test.Expects(0, nil, expect.Contains("run\t")), }, { Description: "run -i", Command: test.Command("__complete", "run", "-i", ""), Expected: test.Expects(0, nil, expect.Contains(testutil.CommonImage)), }, { Description: "run -it", Command: test.Command("__complete", "run", "-it", ""), Expected: test.Expects(0, nil, expect.Contains(testutil.CommonImage)), }, { Description: "run -it --rm", Command: test.Command("__complete", "run", "-it", "--rm", ""), Expected: test.Expects(0, nil, expect.Contains(testutil.CommonImage)), }, { Description: "namespace run -i", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { // mind {"--namespace=nerdctl-test"} vs {"--namespace", "nerdctl-test"} return helpers.Custom(bin, "__complete", "--namespace", string(helpers.Read(nerdtest.Namespace)), "run", "-i", "") }, Expected: test.Expects(0, nil, expect.Contains(testutil.CommonImage+"\n")), }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/completion/completion_unix.go ================================================ //go:build unix /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package completion import ( "github.com/spf13/cobra" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/infoutil" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" ) func NetworkDrivers(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { candidates := []string{"bridge", "macvlan", "ipvlan"} return candidates, cobra.ShellCompDirectiveNoFileComp } func IPAMDrivers(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"default", "host-local", "dhcp"}, cobra.ShellCompDirectiveNoFileComp } func NetworkOptions(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { driver, _ := cmd.Flags().GetString("driver") if driver == "" { driver = "bridge" } var candidates []string switch driver { case "bridge": candidates = []string{ "mtu=", "com.docker.network.driver.mtu=", "ip-masq=", "com.docker.network.bridge.enable_ip_masquerade=", } case "macvlan": candidates = []string{ "mtu=", "com.docker.network.driver.mtu=", "mode=bridge", "macvlan_mode=bridge", "parent=", } case "ipvlan": candidates = []string{ "mtu=", "com.docker.network.driver.mtu=", "mode=l2", "mode=l3", "ipvlan_mode=l2", "ipvlan_mode=l3", "parent=", } default: candidates = []string{ "mtu=", "com.docker.network.driver.mtu=", "parent=", } } return candidates, cobra.ShellCompDirectiveNoSpace } func NamespaceNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return nil, cobra.ShellCompDirectiveError } if rootlessutil.IsRootlessParent() { _ = rootlessutil.ParentMain(globalOptions.HostGatewayIP) return nil, cobra.ShellCompDirectiveNoFileComp } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return nil, cobra.ShellCompDirectiveError } defer cancel() nsService := client.NamespaceService() nsList, err := nsService.List(ctx) if err != nil { log.L.Warn(err) return nil, cobra.ShellCompDirectiveError } var candidates []string candidates = append(candidates, nsList...) return candidates, cobra.ShellCompDirectiveNoFileComp } func SnapshotterNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return nil, cobra.ShellCompDirectiveError } if rootlessutil.IsRootlessParent() { _ = rootlessutil.ParentMain(globalOptions.HostGatewayIP) return nil, cobra.ShellCompDirectiveNoFileComp } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return nil, cobra.ShellCompDirectiveError } defer cancel() snapshotterPlugins, err := infoutil.GetSnapshotterNames(ctx, client.IntrospectionService()) if err != nil { return nil, cobra.ShellCompDirectiveError } var candidates []string candidates = append(candidates, snapshotterPlugins...) return candidates, cobra.ShellCompDirectiveNoFileComp } ================================================ FILE: cmd/nerdctl/completion/completion_unix_nolinux.go ================================================ //go:build unix && !linux /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package completion import "github.com/spf13/cobra" func CgroupManagerNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return nil, cobra.ShellCompDirectiveNoFileComp } ================================================ FILE: cmd/nerdctl/completion/completion_windows.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package completion import "github.com/spf13/cobra" func NamespaceNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return nil, cobra.ShellCompDirectiveNoFileComp } func SnapshotterNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return nil, cobra.ShellCompDirectiveNoFileComp } func CgroupManagerNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return nil, cobra.ShellCompDirectiveNoFileComp } func NetworkDrivers(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { candidates := []string{"nat"} return candidates, cobra.ShellCompDirectiveNoFileComp } func IPAMDrivers(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"default"}, cobra.ShellCompDirectiveNoFileComp } func NetworkOptions(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { driver, _ := cmd.Flags().GetString("driver") if driver == "" { driver = "nat" } var candidates []string switch driver { case "nat": candidates = []string{ "mtu=", "com.docker.network.driver.mtu=", } default: candidates = []string{ "mtu=", "com.docker.network.driver.mtu=", } } return candidates, cobra.ShellCompDirectiveNoSpace } ================================================ FILE: cmd/nerdctl/compose/compose.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/composer" ) func Command() *cobra.Command { var cmd = &cobra.Command{ Use: "compose [flags] COMMAND", Short: "Compose", RunE: helpers.UnknownSubcommandAction, SilenceUsage: true, SilenceErrors: true, TraverseChildren: true, // required for global short hands like -f } // `-f` is a nonPersistentAlias, as it conflicts with `nerdctl compose logs --follow` helpers.AddPersistentStringArrayFlag(cmd, "file", nil, []string{"f"}, nil, "", "Specify an alternate compose file") cmd.PersistentFlags().String("project-directory", "", "Specify an alternate working directory") cmd.PersistentFlags().StringP("project-name", "p", "", "Specify an alternate project name") cmd.PersistentFlags().String("env-file", "", "Specify an alternate environment file") cmd.PersistentFlags().String("ipfs-address", "", "multiaddr of IPFS API (default uses $IPFS_PATH env variable if defined or local directory ~/.ipfs)") cmd.PersistentFlags().StringArray("profile", []string{}, "Specify a profile to enable") cmd.AddCommand( upCommand(), logsCommand(), configCommand(), copyCommand(), buildCommand(), execCommand(), imagesCommand(), portCommand(), pushCommand(), pullCommand(), downCommand(), psCommand(), killCommand(), restartCommand(), removeCommand(), runCommand(), versionCommand(), startCommand(), stopCommand(), pauseCommand(), unpauseCommand(), topCommand(), createCommand(), ) return cmd } func getComposeOptions(cmd *cobra.Command, debugFull, experimental bool) (composer.Options, error) { nerdctlCmd, nerdctlArgs := helpers.GlobalFlags(cmd) projectDirectory, err := cmd.Flags().GetString("project-directory") if err != nil { return composer.Options{}, err } envFile, err := cmd.Flags().GetString("env-file") if err != nil { return composer.Options{}, err } projectName, err := cmd.Flags().GetString("project-name") if err != nil { return composer.Options{}, err } files, err := cmd.Flags().GetStringArray("file") if err != nil { return composer.Options{}, err } ipfsAddressStr, err := cmd.Flags().GetString("ipfs-address") if err != nil { return composer.Options{}, err } profiles, err := cmd.Flags().GetStringArray("profile") if err != nil { return composer.Options{}, err } return composer.Options{ Project: projectName, ProjectDirectory: projectDirectory, ConfigPaths: files, Profiles: profiles, EnvFile: envFile, NerdctlCmd: nerdctlCmd, NerdctlArgs: nerdctlArgs, DebugPrintFull: debugFull, Experimental: experimental, IPFSAddress: ipfsAddressStr, }, nil } ================================================ FILE: cmd/nerdctl/compose/compose_build.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/compose" "github.com/containerd/nerdctl/v2/pkg/composer" ) func buildCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "build [flags] [SERVICE...]", Short: "Build or rebuild services", RunE: buildAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().StringArray("build-arg", nil, "Set build-time variables for services.") cmd.Flags().Bool("no-cache", false, "Do not use cache when building the image.") cmd.Flags().String("progress", "", "Set type of progress output (auto, plain, tty). Use plain to show container output") return cmd } func buildAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } buildArg, err := cmd.Flags().GetStringArray("build-arg") if err != nil { return err } noCache, err := cmd.Flags().GetBool("no-cache") if err != nil { return err } progress, err := cmd.Flags().GetString("progress") if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() options, err := getComposeOptions(cmd, globalOptions.DebugFull, globalOptions.Experimental) if err != nil { return err } c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr()) if err != nil { return err } bo := composer.BuildOptions{ Args: buildArg, NoCache: noCache, Progress: progress, } return c.Build(ctx, bo, args) } ================================================ FILE: cmd/nerdctl/compose/compose_build_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "errors" "fmt" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeBuild(t *testing.T) { dockerfile := "FROM " + testutil.CommonImage testCase := nerdtest.Setup() testCase.Require = nerdtest.Build testCase.Setup = func(data test.Data, helpers test.Helpers) { // Make sure we shard the image name to something unique to the test to avoid conflicts with other tests imageSvc0 := data.Identifier("svc0") imageSvc1 := data.Identifier("svc1") imageSvc2 := data.Identifier("svc2") // We are not going to run them, so, ports conflicts should not matter here dockerComposeYAML := fmt.Sprintf(` services: svc0: build: . image: %s depends_on: - svc1 svc1: build: . image: %s svc2: image: %s build: context: . dockerfile_inline: | FROM %s `, imageSvc0, imageSvc1, imageSvc2, testutil.CommonImage) data.Temp().Save(dockerComposeYAML, "compose.yaml") data.Temp().Save(dockerfile, "Dockerfile") data.Labels().Set("composeYaml", data.Temp().Path("compose.yaml")) data.Labels().Set("imageSvc0", imageSvc0) data.Labels().Set("imageSvc1", imageSvc1) data.Labels().Set("imageSvc2", imageSvc2) } testCase.SubTests = []*test.Case{ { Description: "build svc0", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("compose", "-f", data.Labels().Get("composeYaml"), "build", "svc0") }, Command: test.Command("images"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( expect.Contains(data.Labels().Get("imageSvc0")), expect.DoesNotContain(data.Labels().Get("imageSvc1")), expect.DoesNotContain(data.Labels().Get("imageSvc2")), ), } }, }, { Description: "build svc2", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("compose", "-f", data.Labels().Get("composeYaml"), "build", "svc2") }, Command: test.Command("images"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( expect.Contains(data.Labels().Get("imageSvc2")), expect.DoesNotContain(data.Labels().Get("imageSvc1")), ), } }, }, { Description: "build svc0, svc1, svc2", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("compose", "-f", data.Labels().Get("composeYaml"), "build", "svc0", "svc1", "svc2") }, Command: test.Command("images"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.Contains(data.Labels().Get("imageSvc0"), data.Labels().Get("imageSvc1"), data.Labels().Get("imageSvc2")), } }, }, { Description: "build no arg", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "build") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, { Description: "build bogus", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command( "compose", "-f", data.Labels().Get("composeYaml"), "build", "svc0", "svc100", ) }, Expected: test.Expects(1, []error{errors.New("no such service: svc100")}, nil), }, } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if data.Labels().Get("imageSvc0") != "" { helpers.Anyhow("rmi", data.Labels().Get("imageSvc0"), data.Labels().Get("imageSvc1"), data.Labels().Get("imageSvc2")) } } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/compose/compose_config.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "fmt" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/compose" "github.com/containerd/nerdctl/v2/pkg/composer" ) func configCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "config", Short: "Validate and view the Compose file", RunE: configAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().BoolP("quiet", "q", false, "Only validate the configuration, don't print anything.") cmd.Flags().Bool("services", false, "Print the service names, one per line.") cmd.Flags().Bool("volumes", false, "Print the volume names, one per line.") cmd.Flags().String("hash", "", "Print the service config hash, one per line.") cmd.RegisterFlagCompletionFunc("hash", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"\"*\""}, cobra.ShellCompDirectiveNoFileComp }) return cmd } func configAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } if len(args) != 0 { // TODO: support specifying service names as args return fmt.Errorf("arguments %v not supported", args) } quiet, err := cmd.Flags().GetBool("quiet") if err != nil { return err } services, err := cmd.Flags().GetBool("services") if err != nil { return err } volumes, err := cmd.Flags().GetBool("volumes") if err != nil { return err } hash, err := cmd.Flags().GetString("hash") if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() options, err := getComposeOptions(cmd, globalOptions.DebugFull, globalOptions.Experimental) if err != nil { return err } c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr()) if err != nil { return err } if quiet { return nil } co := composer.ConfigOptions{ Services: services, Volumes: volumes, Hash: hash, } return c.Config(ctx, cmd.OutOrStdout(), co) } ================================================ FILE: cmd/nerdctl/compose/compose_config_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "fmt" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeConfig(t *testing.T) { dockerComposeYAML := fmt.Sprintf(` services: hello: image: %s `, testutil.CommonImage) testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerComposeYAML, "compose.yaml") data.Labels().Set("composeYaml", data.Temp().Path("compose.yaml")) } testCase.SubTests = []*test.Case{ { Description: "config contains service name", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "config") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("hello:")), }, { Description: "config --services is exactly service name", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command( "compose", "-f", data.Labels().Get("composeYaml"), "config", "--services", ) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("hello\n")), }, { Description: "config --hash=* contains service name", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "config", "--hash=*") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("hello")), }, } testCase.Run(t) } func TestComposeConfigWithPrintServiceHash(t *testing.T) { const dockerComposeYAML = ` services: hello: image: alpine:%s ` testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { data.Temp().Save(fmt.Sprintf(dockerComposeYAML, "3.13"), "compose.yaml") hash := helpers.Capture( "compose", "-f", data.Temp().Path("compose.yaml"), "config", "--hash=hello", ) data.Labels().Set("hash", hash) data.Temp().Save(fmt.Sprintf(dockerComposeYAML, "3.14"), "compose.yaml") } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command( "compose", "-f", data.Temp().Path("compose.yaml"), "config", "--hash=hello", ) } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { assert.Assert(t, data.Labels().Get("hash") != stdout, "hash should be different") }, } } testCase.Run(t) } func TestComposeConfigWithMultipleFile(t *testing.T) { const dockerComposeBase = ` services: hello1: image: alpine:3.13 ` const dockerComposeTest = ` services: hello2: image: alpine:3.14 ` const dockerComposeOverride = ` services: hello1: image: alpine:3.14 ` testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerComposeBase, "compose.yaml") data.Temp().Save(dockerComposeTest, "docker-compose.test.yml") data.Temp().Save(dockerComposeOverride, "docker-compose.override.yml") data.Labels().Set("composeDir", data.Temp().Path()) data.Labels().Set("composeYaml", data.Temp().Path("compose.yaml")) data.Labels().Set("composeYamlTest", data.Temp().Path("docker-compose.test.yml")) } testCase.SubTests = []*test.Case{ { Description: "config override", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command( "compose", "-f", data.Labels().Get("composeYaml"), "-f", data.Labels().Get("composeYamlTest"), "config", ) }, Expected: test.Expects( expect.ExitCodeSuccess, nil, expect.Contains("alpine:3.13", "alpine:3.14", "hello1", "hello2"), ), }, { Description: "project dir", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command( "compose", "--project-directory", data.Labels().Get("composeDir"), "config", ) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("alpine:3.14")), }, { Description: "project dir services", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command( "compose", "--project-directory", data.Labels().Get("composeDir"), "config", "--services", ) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("hello1\n")), }, } testCase.Run(t) } func TestComposeConfigWithComposeFileEnv(t *testing.T) { const dockerComposeBase = ` services: hello1: image: alpine:3.13 ` const dockerComposeTest = ` services: hello2: image: alpine:3.14 ` testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerComposeBase, "compose.yaml") data.Temp().Save(dockerComposeTest, "docker-compose.test.yml") data.Labels().Set("composeDir", data.Temp().Path()) data.Labels().Set("composeYaml", data.Temp().Path("compose.yaml")) data.Labels().Set("composeYamlTest", data.Temp().Path("docker-compose.test.yml")) } testCase.SubTests = []*test.Case{ { Description: "env config", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command( "compose", "config", ) cmd.Setenv("COMPOSE_FILE", data.Labels().Get("composeYaml")+","+data.Labels().Get("composeYamlTest")) cmd.Setenv("COMPOSE_PATH_SEPARATOR", ",") return cmd }, Expected: test.Expects( expect.ExitCodeSuccess, nil, expect.Contains("alpine:3.13", "alpine:3.14", "hello1", "hello2"), ), }, { Description: "env with project dir", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command( "compose", "--project-directory", data.Labels().Get("composeDir"), "config", ) cmd.Setenv("COMPOSE_FILE", data.Labels().Get("composeYaml")+","+data.Labels().Get("composeYamlTest")) cmd.Setenv("COMPOSE_PATH_SEPARATOR", ",") return cmd }, Expected: test.Expects( expect.ExitCodeSuccess, nil, expect.Contains("alpine:3.13", "alpine:3.14", "hello1", "hello2"), ), }, } testCase.Run(t) } func TestComposeConfigWithEnvFile(t *testing.T) { const dockerComposeYAML = ` services: hello: image: ${image} ` const envFileContent = ` image: hello-world ` testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerComposeYAML, "compose.yaml") data.Temp().Save(envFileContent, "env") } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Temp().Path("compose.yaml"), "--env-file", data.Temp().Path("env"), "config", ) } testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("image: hello-world")) testCase.Run(t) } ================================================ FILE: cmd/nerdctl/compose/compose_cp.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "errors" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/compose" "github.com/containerd/nerdctl/v2/pkg/composer" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" ) func copyCommand() *cobra.Command { usage := `cp [OPTIONS] SERVICE:SRC_PATH DEST_PATH|- nerdctl compose cp [OPTIONS] SRC_PATH|- SERVICE:DEST_PATH` var cmd = &cobra.Command{ Use: usage, Short: "Copy files/folders between a service container and the local filesystem", Args: cobra.ExactArgs(2), RunE: copyAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().Bool("dry-run", false, "Execute command in dry run mode") cmd.Flags().BoolP("follow-link", "L", false, "Always follow symbol link in SRC_PATH") cmd.Flags().Int("index", 0, "index of the container if service has multiple replicas") return cmd } func copyAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } source := args[0] if source == "" { return errors.New("source can not be empty") } destination := args[1] if destination == "" { return errors.New("destination can not be empty") } dryRun, err := cmd.Flags().GetBool("dry-run") if err != nil { return err } followLink, err := cmd.Flags().GetBool("follow-link") if err != nil { return err } index, err := cmd.Flags().GetInt("index") if err != nil { return err } // rootless cp runs in the host namespaces, so the address is different if rootlessutil.IsRootless() { globalOptions.Address, err = rootlessutil.RootlessContainredSockAddress() if err != nil { return err } } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() options, err := getComposeOptions(cmd, globalOptions.DebugFull, globalOptions.Experimental) if err != nil { return err } c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr()) if err != nil { return err } co := composer.CopyOptions{ Source: source, Destination: destination, Index: index, FollowLink: followLink, DryRun: dryRun, } return c.Copy(ctx, co) } ================================================ FILE: cmd/nerdctl/compose/compose_cp_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "fmt" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeCopy(t *testing.T) { var dockerComposeYAML = fmt.Sprintf(` services: svc0: image: %s command: "sleep infinity" `, testutil.CommonImage) const testFileContent = "test-file-content" testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { compYamlPath := data.Temp().Save(dockerComposeYAML, "compose.yaml") helpers.Ensure("compose", "-f", compYamlPath, "up", "-d") srcFilePath := data.Temp().Save(testFileContent, "test-file") data.Labels().Set("composeYaml", compYamlPath) data.Labels().Set("srcFile", srcFilePath) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") } testCase.SubTests = []*test.Case{ { Description: "test copy to service /dest-no-exist-no-slash", // These are expected to run in sequence NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "cp", data.Labels().Get("srcFile"), "svc0:/dest-no-exist-no-slash") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, { Description: "test copy from service test-file2", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "cp", "svc0:/dest-no-exist-no-slash", data.Temp().Path("test-file2")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { copied := data.Temp().Load("test-file2") assert.Equal(t, copied, testFileContent) }, } }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/compose/compose_create.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "errors" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/compose" "github.com/containerd/nerdctl/v2/pkg/composer" ) func createCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "create [flags] [SERVICE...]", Short: "Creates containers for one or more services", RunE: createAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().Bool("build", false, "Build images before starting containers.") cmd.Flags().Bool("no-build", false, "Don't build an image even if it's missing, conflict with --build.") cmd.Flags().Bool("force-recreate", false, "Recreate containers even if their configuration and image haven't changed.") cmd.Flags().Bool("no-recreate", false, "Don't recreate containers if they exist, conflict with --force-recreate.") cmd.Flags().String("pull", "missing", "Pull images before running. (support always|missing|never)") return cmd } func createAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } build, err := cmd.Flags().GetBool("build") if err != nil { return err } noBuild, err := cmd.Flags().GetBool("no-build") if err != nil { return err } if build && noBuild { return errors.New("flag --build and --no-build cannot be specified together") } forceRecreate, err := cmd.Flags().GetBool("force-recreate") if err != nil { return err } noRecreate, err := cmd.Flags().GetBool("no-recreate") if err != nil { return err } if forceRecreate && noRecreate { return errors.New("flag --force-recreate and --no-recreate cannot be specified together") } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() options, err := getComposeOptions(cmd, globalOptions.DebugFull, globalOptions.Experimental) if err != nil { return err } c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr()) if err != nil { return err } opt := composer.CreateOptions{ Build: build, NoBuild: noBuild, ForceRecreate: forceRecreate, NoRecreate: noRecreate, } if cmd.Flags().Changed("pull") { pull, err := cmd.Flags().GetString("pull") if err != nil { return err } opt.Pull = &pull } return c.Create(ctx, opt, args) } ================================================ FILE: cmd/nerdctl/compose/compose_create_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "errors" "fmt" "path/filepath" "regexp" "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeCreate(t *testing.T) { var dockerComposeYAML = fmt.Sprintf(` services: svc0: image: %s `, testutil.CommonImage) testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { compYamlPath := data.Temp().Save(dockerComposeYAML, "compose.yaml") data.Labels().Set("composeYaml", compYamlPath) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") } testCase.SubTests = []*test.Case{ { Description: "`compose create` should work", // These are sequential NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "create") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, { Description: "`compose create` should have created service container (in `created` status)", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "ps", "svc0", "-a") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(stdout, "created") || strings.Contains(stdout, "Created"), "stdout should contain `created`") }), }, { Description: "`created container can be started by `compose start`", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "start") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, } testCase.Run(t) } func TestComposeCreateDependency(t *testing.T) { var dockerComposeYAML = fmt.Sprintf(` services: svc0: image: %s depends_on: - "svc1" svc1: image: %s `, testutil.CommonImage, testutil.CommonImage) testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { compYamlPath := data.Temp().Save(dockerComposeYAML, "compose.yaml") data.Labels().Set("composeYaml", compYamlPath) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") } testCase.SubTests = []*test.Case{ { Description: "`compose create` should work", // These are sequential NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "create") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, { Description: "`compose create` should have created svc0 (in `created` status)", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "ps", "svc0", "-a") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(stdout, "created") || strings.Contains(stdout, "Created"), "stdout should contain `created`") }), }, { Description: "`compose create` should have created svc1 (in `created` status)", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "ps", "svc1", "-a") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(stdout, "created") || strings.Contains(stdout, "Created"), "stdout should contain `created`") }), }, } testCase.Run(t) } func TestComposeCreatePull(t *testing.T) { testCase := nerdtest.Setup() testCase.NoParallel = true testCase.Require = nerdtest.Private testCase.Setup = func(data test.Data, helpers test.Helpers) { composeYAML := fmt.Sprintf(` services: svc0: image: %s `, testutil.CommonImage) composePath := data.Temp().Save(composeYAML, "compose.yaml") projectName := filepath.Base(filepath.Dir(composePath)) t.Logf("projectName=%q", projectName) data.Labels().Set("composeYAML", composePath) } testCase.SubTests = []*test.Case{ { Description: "compose create --pull never fails when image missing", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("rmi", "-f", testutil.CommonImage) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "create", "--pull", "never") }, Expected: test.Expects(1, nil, nil), }, { Description: "compose create --pull missing (default) pulls and creates a container", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("rmi", "-f", testutil.CommonImage) helpers.Ensure("compose", "-f", data.Labels().Get("composeYAML"), "create") }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "ps", "svc0", "-a") }, Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile(`Created|created`))), }, { Description: "compose create --pull always pulls and creates a container", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("rmi", "-f", testutil.CommonImage) helpers.Ensure("compose", "-f", data.Labels().Get("composeYAML"), "create", "--pull", "always") }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "ps", "svc0", "-a") }, Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile(`Created|created`))), }, } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if data.Labels().Get("composeYAML") != "" { helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") } } testCase.Run(t) } func TestComposeCreatePullInvalidOption(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { composeYAML := fmt.Sprintf(` services: svc0: image: %s `, testutil.CommonImage) composePath := data.Temp().Save(composeYAML, "compose.yaml") data.Labels().Set("composeYAML", composePath) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { // nerver isn't never. return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "create", "--pull", "nerver") } testCase.Expected = test.Expects(1, []error{errors.New(`invalid --pull option \"nerver\"`)}, nil) testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if path := data.Labels().Get("composeYAML"); path != "" { helpers.Anyhow("compose", "-f", path, "down", "-v") } } testCase.Run(t) } func TestComposeCreateBuild(t *testing.T) { testCase := nerdtest.Setup() testCase.NoParallel = true testCase.Require = require.All( nerdtest.Private, nerdtest.Build, ) testCase.Setup = func(data test.Data, helpers test.Helpers) { imageSvc0 := data.Identifier("composebuild_svc0") composeYAML := fmt.Sprintf(` services: svc0: build: . image: %s `, imageSvc0) dockerfile := fmt.Sprintf(`FROM %s`, testutil.CommonImage) composePath := data.Temp().Save(composeYAML, "compose.yaml") data.Temp().Save(dockerfile, "Dockerfile") projectName := filepath.Base(filepath.Dir(composePath)) t.Logf("projectName=%q", projectName) data.Labels().Set("composeYAML", composePath) data.Labels().Set("imageName", imageSvc0) } testCase.SubTests = []*test.Case{ { Description: "compose create --no-build fails when image needs to be built", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "create", "--no-build") }, Expected: test.Expects(1, nil, nil), }, { Description: "compose create --build builds image and creates container", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("compose", "-f", data.Labels().Get("composeYAML"), "create", "--build") helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "images", "svc0").Run( &test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(stdout, data.Labels().Get("imageName"))) }, }, ) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "ps", "svc0", "-a") }, Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile(`Created|created`))), }, } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if data.Labels().Get("composeYAML") != "" { helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") } helpers.Anyhow("rmi", "-f", data.Labels().Get("imageName")) helpers.Anyhow("builder", "prune", "--all", "--force") } testCase.Run(t) } func TestComposeCreateWritesConfigHashLabel(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { var composeYAML = fmt.Sprintf(` services: svc0: image: %s `, testutil.CommonImage) composePath := data.Temp().Save(composeYAML, "compose.yaml") projectName := filepath.Base(filepath.Dir(composePath)) t.Logf("projectName=%q", projectName) data.Labels().Set("composeYAML", composePath) data.Labels().Set("containerName", serviceparser.DefaultContainerName(projectName, "svc0", "1")) helpers.Ensure("compose", "-f", composePath, "create") } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("inspect", "--format", "{{json .Config.Labels}}", data.Labels().Get("containerName")) } testCase.Expected = test.Expects(0, nil, expect.Contains("com.docker.compose.config-hash")) testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if path := data.Labels().Get("composeYAML"); path != "" { helpers.Anyhow("compose", "-f", path, "down", "-v") } } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/compose/compose_down.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/compose" "github.com/containerd/nerdctl/v2/pkg/composer" ) func downCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "down", Short: "Remove containers and associated resources", Args: cobra.NoArgs, RunE: downAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().BoolP("volumes", "v", false, "Remove named volumes declared in the `volumes` section of the Compose file and anonymous volumes attached to containers.") cmd.Flags().Bool("remove-orphans", false, "Remove containers for services not defined in the Compose file.") return cmd } func downAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } volumes, err := cmd.Flags().GetBool("volumes") if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } removeOrphans, err := cmd.Flags().GetBool("remove-orphans") if err != nil { return err } defer cancel() options, err := getComposeOptions(cmd, globalOptions.DebugFull, globalOptions.Experimental) if err != nil { return err } c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr()) if err != nil { return err } downOpts := composer.DownOptions{ RemoveVolumes: volumes, RemoveOrphans: removeOrphans, } return c.Down(ctx, downOpts) } ================================================ FILE: cmd/nerdctl/compose/compose_down_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "fmt" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeDownRemoveUsedNetwork(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { dockerComposeYAMLOrphan := fmt.Sprintf(` services: test: image: %s command: "sleep infinity" `, testutil.CommonImage) dockerComposeYAMLFull := fmt.Sprintf(` %s orphan: image: %s command: "sleep infinity" `, dockerComposeYAMLOrphan, testutil.CommonImage) composeOrphanPath := data.Temp().Save(dockerComposeYAMLOrphan, "compose-orphan.yaml") composeFullPath := data.Temp().Save(dockerComposeYAMLFull, "compose-full.yaml") projectName := data.Identifier("project") t.Logf("projectName=%q", projectName) testContainer := serviceparser.DefaultContainerName(projectName, "test", "1") orphanContainer := serviceparser.DefaultContainerName(projectName, "orphan", "1") data.Labels().Set("composeOrphan", composeOrphanPath) data.Labels().Set("composeFull", composeFullPath) data.Labels().Set("projectName", projectName) helpers.Ensure("compose", "-p", projectName, "-f", composeFullPath, "up", "-d") nerdtest.EnsureContainerStarted(helpers, testContainer) nerdtest.EnsureContainerStarted(helpers, orphanContainer) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-p", data.Labels().Get("projectName"), "-f", data.Labels().Get("composeOrphan"), "down", "-v") } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Errors: []error{ fmt.Errorf("in use"), }, } } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if composeFull := data.Labels().Get("composeFull"); composeFull != "" { helpers.Anyhow("compose", "-p", data.Labels().Get("projectName"), "-f", composeFull, "down", "--remove-orphans") } } testCase.Run(t) } func TestComposeDownRemoveOrphans(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { dockerComposeYAMLOrphan := fmt.Sprintf(` services: test: image: %s command: "sleep infinity" `, testutil.CommonImage) dockerComposeYAMLFull := fmt.Sprintf(` %s orphan: image: %s command: "sleep infinity" `, dockerComposeYAMLOrphan, testutil.CommonImage) composeOrphanPath := data.Temp().Save(dockerComposeYAMLOrphan, "compose-orphan.yaml") composeFullPath := data.Temp().Save(dockerComposeYAMLFull, "compose-full.yaml") projectName := data.Identifier("project") t.Logf("projectName=%q", projectName) testContainer := serviceparser.DefaultContainerName(projectName, "test", "1") orphanContainer := serviceparser.DefaultContainerName(projectName, "orphan", "1") data.Labels().Set("composeOrphan", composeOrphanPath) data.Labels().Set("composeFull", composeFullPath) data.Labels().Set("projectName", projectName) data.Labels().Set("orphanContainer", orphanContainer) helpers.Ensure("compose", "-p", projectName, "-f", composeFullPath, "up", "-d") nerdtest.EnsureContainerStarted(helpers, testContainer) nerdtest.EnsureContainerStarted(helpers, orphanContainer) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-p", data.Labels().Get("projectName"), "-f", data.Labels().Get("composeOrphan"), "down", "--remove-orphans") } testCase.Expected = test.Expects(0, nil, nil) testCase.SubTests = []*test.Case{ { Description: "orphan container removed", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-p", data.Labels().Get("projectName"), "-f", data.Labels().Get("composeFull"), "ps", "-a") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.DoesNotContain(data.Labels().Get("orphanContainer")), } }, }, } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if composeFull := data.Labels().Get("composeFull"); composeFull != "" { helpers.Anyhow("compose", "-p", data.Labels().Get("projectName"), "-f", composeFull, "down", "-v") } } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/compose/compose_exec.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "errors" "os" "github.com/moby/term" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/compose" "github.com/containerd/nerdctl/v2/pkg/composer" ) func execCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "exec [flags] SERVICE COMMAND [ARGS...]", Short: "Execute a command in a running container of the service", Args: cobra.MinimumNArgs(2), RunE: execAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().SetInterspersed(false) _, isTerminal := term.GetFdInfo(os.Stdout) cmd.Flags().BoolP("no-TTY", "T", !isTerminal, "Disable pseudo-TTY allocation. By default nerdctl compose exec allocates a TTY.") cmd.Flags().BoolP("detach", "d", false, "Detached mode: Run containers in the background") cmd.Flags().StringP("workdir", "w", "", "Working directory inside the container") // env needs to be StringArray, not StringSlice, to prevent "FOO=foo1,foo2" from being split to {"FOO=foo1", "foo2"} cmd.Flags().StringArrayP("env", "e", nil, "Set environment variables") cmd.Flags().Bool("privileged", false, "Give extended privileges to the command") cmd.Flags().StringP("user", "u", "", "Username or UID (format: [:])") cmd.Flags().Int("index", 1, "index of the container if the service has multiple instances.") cmd.Flags().BoolP("interactive", "i", true, "Keep STDIN open even if not attached") cmd.Flags().MarkHidden("interactive") // The -t does not has effect to keep the compatibility with docker. // The proposal of -t is to keep "muscle memory" with compose v1: https://github.com/docker/compose/issues/9207 // FYI: https://github.com/docker/compose/blob/v2.23.1/cmd/compose/exec.go#L77 cmd.Flags().BoolP("tty", "t", true, "Allocate a pseudo-TTY") cmd.Flags().MarkHidden("tty") return cmd } func execAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } interactive, err := cmd.Flags().GetBool("interactive") if err != nil { return err } noTty, err := cmd.Flags().GetBool("no-TTY") if err != nil { return err } detach, err := cmd.Flags().GetBool("detach") if err != nil { return err } workdir, err := cmd.Flags().GetString("workdir") if err != nil { return err } env, err := cmd.Flags().GetStringArray("env") if err != nil { return err } privileged, err := cmd.Flags().GetBool("privileged") if err != nil { return err } user, err := cmd.Flags().GetString("user") if err != nil { return err } index, err := cmd.Flags().GetInt("index") if err != nil { return err } if index < 1 { return errors.New("index starts from 1 and should be equal or greater than 1") } // https://github.com/containerd/nerdctl/blob/v1.0.0/cmd/nerdctl/exec.go#L116 if interactive && detach { return errors.New("currently flag -i and -d cannot be specified together (FIXME)") } // https://github.com/containerd/nerdctl/blob/v1.0.0/cmd/nerdctl/exec.go#L122 if !noTty && detach { return errors.New("currently flag -d should be specified with --no-TTY (FIXME)") } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() options, err := getComposeOptions(cmd, globalOptions.DebugFull, globalOptions.Experimental) if err != nil { return err } c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr()) if err != nil { return err } eo := composer.ExecOptions{ ServiceName: args[0], Index: index, Interactive: interactive, Tty: !noTty, Detach: detach, WorkDir: workdir, Env: env, Privileged: privileged, User: user, Args: args[1:], } return c.Exec(ctx, eo) } ================================================ FILE: cmd/nerdctl/compose/compose_exec_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "fmt" "net" "path/filepath" "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeExec(t *testing.T) { dockerComposeYAML := fmt.Sprintf(` services: svc0: image: %s command: "sleep infinity" svc1: image: %s command: "sleep infinity" `, testutil.CommonImage, testutil.CommonImage) testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { yamlPath := data.Temp().Save(dockerComposeYAML, "compose.yaml") data.Labels().Set("YAMLPath", yamlPath) helpers.Ensure("compose", "-f", yamlPath, "up", "-d", "svc0") } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") } testCase.SubTests = []*test.Case{ { Description: "exec no tty", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command( "compose", "-f", data.Labels().Get("YAMLPath"), "exec", "-i=false", "--no-TTY", "svc0", "echo", "success", ) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("success\n")), }, { Description: "exec no tty with workdir", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command( "compose", "-f", data.Labels().Get("YAMLPath"), "exec", "-i=false", "--no-TTY", "--workdir", "/tmp", "svc0", "pwd", ) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("/tmp\n")), }, { Description: "cannot exec on non-running service", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("YAMLPath"), "exec", "svc1", "echo", "success") }, Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, { Description: "with env", Env: map[string]string{ "CORGE": "corge-value-in-host", "GARPLY": "garply-value-in-host", }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command( "compose", "-f", data.Labels().Get("YAMLPath"), "exec", "-i=false", "--no-TTY", "--env", "FOO=foo1,foo2", "--env", "BAR=bar1 bar2", "--env", "BAZ=", "--env", "QUX", // not exported in OS "--env", "QUUX=quux1", "--env", "QUUX=quux2", "--env", "CORGE", // OS exported "--env", "GRAULT=grault_key=grault_value", // value contains `=` char "--env", "GARPLY=", // OS exported "--env", "WALDO=", // not exported in OS "svc0", "env") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( expect.Contains( "\nFOO=foo1,foo2\n", "\nBAR=bar1 bar2\n", "\nBAZ=\n", "\nQUUX=quux2\n", "\nCORGE=corge-value-in-host\n", "\nGRAULT=grault_key=grault_value\n", "\nGARPLY=\n", "\nWALDO=\n"), expect.DoesNotContain("QUX"), )), }, } userSubTest := &test.Case{ Description: "with user", SubTests: []*test.Case{}, } userCasesMap := map[string]string{ "": "uid=0(root) gid=0(root)", "1000": "uid=1000 gid=0(root)", "1000:users": "uid=1000 gid=100(users)", "guest": "uid=405(guest) gid=100(users)", "nobody": "uid=65534(nobody) gid=65534(nobody)", "nobody:users": "uid=65534(nobody) gid=100(users)", } for k, v := range userCasesMap { userSubTest.SubTests = append(userSubTest.SubTests, &test.Case{ Description: k + " " + v, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { args := []string{"compose", "-f", data.Labels().Get("YAMLPath"), "exec", "-i=false", "--no-TTY"} if k != "" { args = append(args, "--user", k) } args = append(args, "svc0", "id") return helpers.Command(args...) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(v)), }) } testCase.SubTests = append(testCase.SubTests, userSubTest) testCase.Run(t) } func TestComposeExecTTY(t *testing.T) { const expectedOutput = "speed 38400 baud" dockerComposeYAML := fmt.Sprintf(` services: svc0: image: %s svc1: image: %s `, testutil.CommonImage, testutil.CommonImage) testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { yamlPath := data.Temp().Save(dockerComposeYAML, "compose.yaml") data.Labels().Set("YAMLPath", yamlPath) helpers.Ensure( "compose", "-f", yamlPath, "run", "-d", "-i=false", "--name", data.Identifier(), "svc0", "sleep", "1h", ) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { // FIXME? // similar, other test does *also* remove the container helpers.Anyhow("compose", "-f", data.Labels().Get("YAMLPath"), "down", "-v") } testCase.SubTests = []*test.Case{ { Description: "stty exec", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("compose", "-f", data.Labels().Get("YAMLPath"), "exec", "svc0", "stty") cmd.WithPseudoTTY() return cmd }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(expectedOutput)), }, { Description: "-i=false stty exec", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("compose", "-f", data.Labels().Get("YAMLPath"), "exec", "-i=false", "svc0", "stty") cmd.WithPseudoTTY() return cmd }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(expectedOutput)), }, { Description: "--no-TTY stty exec", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("compose", "-f", data.Labels().Get("YAMLPath"), "exec", "--no-TTY", "svc0", "stty") cmd.WithPseudoTTY() return cmd }, Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, { Description: "-i=false --no-TTY stty exec", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command( "compose", "-f", data.Labels().Get("YAMLPath"), "exec", "-i=false", "--no-TTY", "svc0", "stty", ) cmd.WithPseudoTTY() return cmd }, Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, } testCase.Run(t) } func TestComposeExecWithIndex(t *testing.T) { dockerComposeYAML := fmt.Sprintf(` services: svc0: image: %s command: "sleep infinity" deploy: replicas: 3 `, testutil.CommonImage) testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { yamlPath := data.Temp().Save(dockerComposeYAML, "compose.yaml") data.Labels().Set("YAMLPath", yamlPath) data.Labels().Set("projectName", strings.ToLower(filepath.Base(data.Temp().Dir()))) helpers.Ensure("compose", "-f", yamlPath, "up", "-d", "svc0") // Make sure all containers are started so that /etc/hosts is consistent. for _, index := range []string{"1", "2", "3"} { nerdtest.EnsureContainerStarted(helpers, fmt.Sprintf("%s-svc0-%s", data.Labels().Get("projectName"), index)) } } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") } for _, index := range []string{"1", "2", "3"} { testCase.SubTests = append(testCase.SubTests, &test.Case{ Description: index, Setup: func(data test.Data, helpers test.Helpers) { // try 5 times to ensure that results are stable for range 5 { cmds := []string{ "compose", "-f", data.Labels().Get("YAMLPath"), "exec", "-i=false", "--no-TTY", "--index", index, "svc0", } hsts := helpers.Capture(append(cmds, "cat", "/etc/hosts")...) ips := helpers.Capture(append(cmds, "ip", "addr", "show", "dev", "eth0")...) var ( expectIP string realIP string ) name := fmt.Sprintf("%s-svc0-%s", data.Labels().Get("projectName"), index) host := fmt.Sprintf("%s.%s_default", name, data.Labels().Get("projectName")) if nerdtest.IsDocker() { host = strings.TrimSpace(helpers.Capture("ps", "--filter", "name="+name, "--format", "{{.ID}}")) } lines := strings.Split(hsts, "\n") for _, line := range lines { if !strings.Contains(line, host) { continue } fields := strings.Fields(line) if len(fields) == 0 { continue } expectIP = fields[0] } var ip string lines = strings.Split(ips, "\n") for _, line := range lines { if !strings.Contains(line, "inet ") { continue } fields := strings.Fields(line) if len(fields) <= 1 { continue } ip = strings.Split(fields[1], "/")[0] break } pip := net.ParseIP(ip) assert.Assert(helpers.T(), pip != nil, "fail to get the real ip address") realIP = pip.String() assert.Equal(helpers.T(), realIP, expectIP) } }, }) } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/compose/compose_images.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "context" "fmt" "strings" "text/tabwriter" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/core/snapshots" "github.com/containerd/containerd/v2/pkg/progress" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/compose" "github.com/containerd/nerdctl/v2/pkg/formatter" "github.com/containerd/nerdctl/v2/pkg/imgutil" "github.com/containerd/nerdctl/v2/pkg/labels" "github.com/containerd/nerdctl/v2/pkg/strutil" ) func imagesCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "images [flags] [SERVICE...]", Short: "List images used by created containers in services", RunE: imagesAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().String("format", "", "Format the output. Supported values: [json]") cmd.Flags().BoolP("quiet", "q", false, "Only show numeric image IDs") return cmd } func imagesAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } quiet, err := cmd.Flags().GetBool("quiet") if err != nil { return err } format, err := cmd.Flags().GetString("format") if err != nil { return err } if format != "json" && format != "" { return fmt.Errorf("unsupported format %s, supported formats are: [json]", format) } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() options, err := getComposeOptions(cmd, globalOptions.DebugFull, globalOptions.Experimental) if err != nil { return err } c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr()) if err != nil { return err } serviceNames, err := c.ServiceNames(args...) if err != nil { return err } containers, err := c.Containers(ctx, serviceNames...) if err != nil { return err } if quiet { return printComposeImageIDs(ctx, containers) } sn := client.SnapshotService(globalOptions.Snapshotter) return printComposeImages(ctx, cmd, containers, sn, format) } func printComposeImageIDs(ctx context.Context, containers []containerd.Container) error { ids := []string{} for _, c := range containers { image, err := c.Image(ctx) if err != nil { return err } metaImage := image.Metadata() id := metaImage.Target.Digest.String() if !strutil.InStringSlice(ids, id) { ids = append(ids, id) } } for _, id := range ids { // always truncate image ids. fmt.Println(strings.Split(id, ":")[1][:12]) } return nil } func printComposeImages(ctx context.Context, cmd *cobra.Command, containers []containerd.Container, sn snapshots.Snapshotter, format string) error { type composeImagePrintable struct { ContainerName string Repository string Tag string ImageID string Size string } imagePrintables := make([]composeImagePrintable, len(containers)) eg, ctx := errgroup.WithContext(ctx) for i, c := range containers { i, c := i, c eg.Go(func() error { info, err := c.Info(ctx, containerd.WithoutRefreshedMetadata) if err != nil { return err } containerName := info.Labels[labels.Name] image, err := c.Image(ctx) if err != nil { return err } size, err := imgutil.UnpackedImageSize(ctx, sn, image) if err != nil { return err } metaImage := image.Metadata() repository, tag := imgutil.ParseRepoTag(metaImage.Name) imageID := metaImage.Target.Digest.String() if repository == "" { repository = "" } if tag == "" { tag = "" } if format != "json" { imageID = strings.Split(imageID, ":")[1][:12] } // no race condition since each goroutine accesses different `i` imagePrintables[i] = composeImagePrintable{ ContainerName: containerName, Repository: repository, Tag: tag, ImageID: imageID, Size: progress.Bytes(size).String(), } return nil }) } if err := eg.Wait(); err != nil { return err } if format == "json" { outJSON, err := formatter.ToJSON(imagePrintables, "", "") if err != nil { return err } _, err = fmt.Fprint(cmd.OutOrStdout(), outJSON) return err } w := tabwriter.NewWriter(cmd.OutOrStdout(), 4, 8, 4, ' ', 0) fmt.Fprintln(w, "Container\tRepository\tTag\tImage Id\tSize") for _, p := range imagePrintables { if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", p.ContainerName, p.Repository, p.Tag, p.ImageID, p.Size, ); err != nil { return err } } return w.Flush() } ================================================ FILE: cmd/nerdctl/compose/compose_images_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "fmt" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/referenceutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeImages(t *testing.T) { var dockerComposeYAML = fmt.Sprintf(` services: wordpress: image: %s container_name: wordpress environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser WORDPRESS_DB_PASSWORD: examplepass WORDPRESS_DB_NAME: exampledb volumes: - wordpress:/var/www/html db: image: %s container_name: db environment: MYSQL_DATABASE: exampledb MYSQL_USER: exampleuser MYSQL_PASSWORD: examplepass MYSQL_RANDOM_ROOT_PASSWORD: '1' volumes: - db:/var/lib/mysql volumes: wordpress: db: `, testutil.WordpressImage, testutil.MariaDBImage) wordpressImageName, _ := referenceutil.Parse(testutil.WordpressImage) dbImageName, _ := referenceutil.Parse(testutil.MariaDBImage) testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerComposeYAML, "compose.yaml") data.Labels().Set("composeYaml", data.Temp().Path("compose.yaml")) helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "up", "-d") } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down") } testCase.SubTests = []*test.Case{ { Description: "images db", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "images", "db") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( expect.Contains(dbImageName.Name()), expect.DoesNotContain(wordpressImageName.Name()), )), }, { Description: "images", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "images") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(dbImageName.Name(), wordpressImageName.Name())), }, { Description: "images --format yaml", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "images", "--format", "yaml") }, Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, { Description: "images --format json", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "images", "--format", "json") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( expect.JSON([]composeContainerPrintable{}, func(printables []composeContainerPrintable, t tig.T) { assert.Equal(t, len(printables), 2) }), expect.Contains(`"ContainerName":"wordpress"`, `"ContainerName":"db"`), )), }, { Description: "images --format json wordpress", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "images", "--format", "json", "wordpress") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( expect.JSON([]composeContainerPrintable{}, func(printables []composeContainerPrintable, t tig.T) { assert.Equal(t, len(printables), 1) }), expect.Contains(`"ContainerName":"wordpress"`), )), }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/compose/compose_kill.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/compose" "github.com/containerd/nerdctl/v2/pkg/composer" ) func killCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "kill [flags] [SERVICE...]", Short: "Force stop service containers", RunE: killAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().StringP("signal", "s", "SIGKILL", "SIGNAL to send to the container.") return cmd } func killAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } signal, err := cmd.Flags().GetString("signal") if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() options, err := getComposeOptions(cmd, globalOptions.DebugFull, globalOptions.Experimental) if err != nil { return err } c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr()) if err != nil { return err } killOpts := composer.KillOptions{ Signal: signal, } return c.Kill(ctx, killOpts, args) } ================================================ FILE: cmd/nerdctl/compose/compose_kill_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "fmt" "path/filepath" "regexp" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeKill(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { dockerComposeYAML := fmt.Sprintf(` services: wordpress: image: %s environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser WORDPRESS_DB_PASSWORD: examplepass WORDPRESS_DB_NAME: exampledb volumes: - wordpress:/var/www/html db: image: %s environment: MYSQL_DATABASE: exampledb MYSQL_USER: exampleuser MYSQL_PASSWORD: examplepass MYSQL_RANDOM_ROOT_PASSWORD: '1' volumes: - db:/var/lib/mysql volumes: wordpress: db: `, testutil.WordpressImage, testutil.MariaDBImage) composePath := data.Temp().Save(dockerComposeYAML, "compose.yaml") projectName := filepath.Base(filepath.Dir(composePath)) t.Logf("projectName=%q", projectName) wordpressContainerName := serviceparser.DefaultContainerName(projectName, "wordpress", "1") dbContainerName := serviceparser.DefaultContainerName(projectName, "db", "1") data.Labels().Set("composeYAML", composePath) data.Labels().Set("wordpressContainer", wordpressContainerName) data.Labels().Set("dbContainer", dbContainerName) helpers.Ensure("compose", "-f", composePath, "up", "-d") nerdtest.EnsureContainerStarted(helpers, wordpressContainerName) nerdtest.EnsureContainerStarted(helpers, dbContainerName) } testCase.SubTests = []*test.Case{ { Description: "kill db container and exit with 137", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("compose", "-f", data.Labels().Get("composeYAML"), "kill", "db") nerdtest.EnsureContainerExited(helpers, data.Labels().Get("dbContainer"), expect.ExitCodeSigkill) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "ps", "db", "-a") }, // Docker Compose v1: "Exit 137", v2: "exited (137)" Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Match(regexp.MustCompile(` 137|\(137\)`))), }, { Description: "wordpress container is still running", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "ps", "wordpress") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Match(regexp.MustCompile("Up|running"))), }, } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if data.Labels().Get("composeYAML") != "" { helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") } } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/compose/compose_logs.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/compose" "github.com/containerd/nerdctl/v2/pkg/composer" ) func logsCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "logs [flags] [SERVICE...]", Short: "Show logs of running containers", RunE: logsAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().BoolP("follow", "f", false, "Follow log output.") cmd.Flags().BoolP("timestamps", "t", false, "Show timestamps") cmd.Flags().String("tail", "all", "Number of lines to show from the end of the logs") cmd.Flags().Bool("no-color", false, "Produce monochrome output") cmd.Flags().Bool("no-log-prefix", false, "Don't print prefix in logs") return cmd } func logsAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } follow, err := cmd.Flags().GetBool("follow") if err != nil { return err } timestamps, err := cmd.Flags().GetBool("timestamps") if err != nil { return err } tail, err := cmd.Flags().GetString("tail") if err != nil { return err } noColor, err := cmd.Flags().GetBool("no-color") if err != nil { return err } noLogPrefix, err := cmd.Flags().GetBool("no-log-prefix") if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() options, err := getComposeOptions(cmd, globalOptions.DebugFull, globalOptions.Experimental) if err != nil { return err } c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr()) if err != nil { return err } lo := composer.LogsOptions{ Follow: follow, Timestamps: timestamps, Tail: tail, NoColor: noColor, NoLogPrefix: noLogPrefix, } return c.Logs(ctx, lo, args) } ================================================ FILE: cmd/nerdctl/compose/compose_pause.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/compose" ) func pauseCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "pause [SERVICE...]", Short: "Pause all processes within containers of service(s). They can be unpaused with nerdctl compose unpause", RunE: pauseAction, SilenceUsage: true, SilenceErrors: true, DisableFlagsInUseLine: true, } return cmd } func pauseAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() options, err := getComposeOptions(cmd, globalOptions.DebugFull, globalOptions.Experimental) if err != nil { return err } c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr()) if err != nil { return err } return c.Pause(ctx, args, cmd.OutOrStdout()) } func unpauseCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "unpause [SERVICE...]", Short: "Unpause all processes within containers of service(s).", RunE: unpauseAction, SilenceUsage: true, SilenceErrors: true, DisableFlagsInUseLine: true, } return cmd } func unpauseAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() options, err := getComposeOptions(cmd, globalOptions.DebugFull, globalOptions.Experimental) if err != nil { return err } c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr()) if err != nil { return err } return c.Unpause(ctx, args, cmd.OutOrStdout()) } ================================================ FILE: cmd/nerdctl/compose/compose_pause_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "fmt" "path/filepath" "regexp" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposePauseAndUnpause(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = nerdtest.CGroup testCase.Setup = func(data test.Data, helpers test.Helpers) { dockerComposeYAML := fmt.Sprintf(` services: svc0: image: %s command: "sleep infinity" svc1: image: %s command: "sleep infinity" `, testutil.CommonImage, testutil.CommonImage) composePath := data.Temp().Save(dockerComposeYAML, "compose.yaml") projectName := filepath.Base(filepath.Dir(composePath)) t.Logf("projectName=%q", projectName) svc0Container := serviceparser.DefaultContainerName(projectName, "svc0", "1") svc1Container := serviceparser.DefaultContainerName(projectName, "svc1", "1") data.Labels().Set("composeYAML", composePath) helpers.Ensure("compose", "-f", composePath, "up", "-d") nerdtest.EnsureContainerStarted(helpers, svc0Container) nerdtest.EnsureContainerStarted(helpers, svc1Container) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { // pause a service should (only) pause its own container return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "pause", "svc0") } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, Output: func(stdout string, t tig.T) { svc0Paused := helpers.Capture("compose", "-f", data.Labels().Get("composeYAML"), "ps", "svc0", "-a") expect.Match(regexp.MustCompile("Paused|paused"))(svc0Paused, t) svc1Running := helpers.Capture("compose", "-f", data.Labels().Get("composeYAML"), "ps", "svc1") expect.Match(regexp.MustCompile("Up|running"))(svc1Running, t) // unpause should be able to recover the paused service container helpers.Ensure("compose", "-f", data.Labels().Get("composeYAML"), "unpause", "svc0") svc0Running := helpers.Capture("compose", "-f", data.Labels().Get("composeYAML"), "ps", "svc0") expect.Match(regexp.MustCompile("Up|running"))(svc0Running, t) }, } } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if data.Labels().Get("composeYAML") != "" { helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") } } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/compose/compose_port.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "fmt" "strconv" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/compose" "github.com/containerd/nerdctl/v2/pkg/composer" ) func portCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "port [flags] SERVICE PRIVATE_PORT", Short: "Print the public port for a port binding", Args: cobra.ExactArgs(2), RunE: portAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().Int("index", 1, "index of the container if the service has multiple instances.") cmd.Flags().String("protocol", "tcp", "protocol of the port (tcp|udp)") return cmd } func portAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } index, err := cmd.Flags().GetInt("index") if err != nil { return err } if index < 1 { return fmt.Errorf("index starts from 1 and should be equal or greater than 1, given index: %d", index) } protocol, err := cmd.Flags().GetString("protocol") if err != nil { return err } switch protocol { case "tcp", "udp": default: return fmt.Errorf("unsupported protocol: %s (only tcp and udp are supported)", protocol) } port, err := strconv.Atoi(args[1]) if err != nil { return err } if port <= 0 { return fmt.Errorf("unexpected port: %d", port) } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() options, err := getComposeOptions(cmd, globalOptions.DebugFull, globalOptions.Experimental) if err != nil { return err } c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr()) if err != nil { return err } dataStore, err := clientutil.DataStore(globalOptions.DataRoot, globalOptions.Address) if err != nil { return err } po := composer.PortOptions{ ServiceName: args[0], Index: index, Port: port, Protocol: protocol, DataStore: dataStore, Namespace: globalOptions.Namespace, } return c.Port(ctx, cmd.OutOrStdout(), po) } ================================================ FILE: cmd/nerdctl/compose/compose_port_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "fmt" "path/filepath" "strconv" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/portlock" ) func TestComposePort(t *testing.T) { const portCount = 2 testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { for i := 0; i < portCount; i++ { port, err := portlock.Acquire(0) if err != nil { helpers.T().Log(fmt.Sprintf("Failed to acquire port: %v", err)) helpers.T().FailNow() } data.Labels().Set(fmt.Sprintf("hostPort%d", i), strconv.Itoa(port)) } dockerComposeYAML := fmt.Sprintf(` services: svc0: image: %s command: "sleep infinity" ports: - "%s:10000" - "%s:10001/udp" `, testutil.CommonImage, data.Labels().Get("hostPort0"), data.Labels().Get("hostPort1")) compYamlPath := data.Temp().Save(dockerComposeYAML, "compose.yaml") data.Labels().Set("composeYaml", compYamlPath) projectName := filepath.Base(filepath.Dir(compYamlPath)) t.Logf("projectName=%q", projectName) helpers.Ensure("compose", "-f", compYamlPath, "up", "-d") } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") for i := 0; i < portCount; i++ { port, _ := strconv.Atoi(data.Labels().Get(fmt.Sprintf("hostPort%d", i))) _ = portlock.Release(port) } } testCase.SubTests = []*test.Case{ { Description: "port should return host port for TCP", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "port", "svc0", "10000") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, Output: expect.Equals(fmt.Sprintf("0.0.0.0:%s\n", data.Labels().Get("hostPort0"))), } }, }, { Description: "port should return host port for UDP", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "port", "--protocol", "udp", "svc0", "10001") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, Output: expect.Equals(fmt.Sprintf("0.0.0.0:%s\n", data.Labels().Get("hostPort1"))), } }, }, } testCase.Run(t) } func TestComposePortFailure(t *testing.T) { const portCount = 2 testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { for i := 0; i < portCount; i++ { port, err := portlock.Acquire(0) if err != nil { helpers.T().Log(fmt.Sprintf("Failed to acquire port: %v", err)) helpers.T().FailNow() } data.Labels().Set(fmt.Sprintf("hostPort%d", i), strconv.Itoa(port)) } dockerComposeYAML := fmt.Sprintf(` services: svc0: image: %s command: "sleep infinity" ports: - "%s:10000" - "%s:10001/udp" `, testutil.CommonImage, data.Labels().Get("hostPort0"), data.Labels().Get("hostPort1")) compYamlPath := data.Temp().Save(dockerComposeYAML, "compose.yaml") data.Labels().Set("composeYaml", compYamlPath) projectName := filepath.Base(filepath.Dir(compYamlPath)) t.Logf("projectName=%q", projectName) helpers.Ensure("compose", "-f", compYamlPath, "up", "-d") } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") for i := 0; i < portCount; i++ { port, _ := strconv.Atoi(data.Labels().Get(fmt.Sprintf("hostPort%d", i))) _ = portlock.Release(port) } } testCase.SubTests = []*test.Case{ { Description: "port should fail for non-existent port", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "port", "svc0", "9999") }, Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, { Description: "port should fail for wrong protocol (UDP on TCP port)", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "port", "--protocol", "udp", "svc0", "10000") }, Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, { Description: "port should fail for wrong protocol (TCP on UDP port)", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "port", "--protocol", "tcp", "svc0", "10001") }, Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, } testCase.Run(t) } // TestComposeMultiplePorts tests whether it is possible to allocate a large // number of ports. (https://github.com/containerd/nerdctl/issues/4027) func TestComposeMultiplePorts(t *testing.T) { testCase := nerdtest.Setup() testCase.NoParallel = true testCase.Setup = func(data test.Data, helpers test.Helpers) { dockerComposeYAML := fmt.Sprintf(` services: svc0: image: %s command: "sleep infinity" ports: - '32000-32060:32000-32060' `, testutil.AlpineImage) compYamlPath := data.Temp().Save(dockerComposeYAML, "compose.yaml") data.Labels().Set("composeYaml", compYamlPath) projectName := filepath.Base(filepath.Dir(compYamlPath)) t.Logf("projectName=%q", projectName) helpers.Ensure("compose", "-f", compYamlPath, "up", "-d") } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") } testCase.SubTests = []*test.Case{ { Description: "Issue #4027 - Allocate a large number of ports.", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "port", "svc0", "32000") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("0.0.0.0:32000")), }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/compose/compose_ps.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "context" "fmt" "strings" "text/tabwriter" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/core/runtime/restart" "github.com/containerd/errdefs" "github.com/containerd/go-cni" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/compose" "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/formatter" "github.com/containerd/nerdctl/v2/pkg/labels" "github.com/containerd/nerdctl/v2/pkg/portutil" ) func psCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "ps [flags] [SERVICE...]", Short: "List containers of services", RunE: psAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().String("format", "table", "Format the output. Supported values: [table|json]") cmd.Flags().String("filter", "", "Filter matches containers based on given conditions") cmd.Flags().StringArray("status", []string{}, "Filter services by status. Values: [paused | restarting | removing | running | dead | created | exited]") cmd.Flags().BoolP("quiet", "q", false, "Only display container IDs") cmd.Flags().Bool("services", false, "Display services") cmd.Flags().BoolP("all", "a", false, "Show all containers (default shows just running)") return cmd } type composeContainerPrintable struct { ID string Name string Image string Command string Project string Service string State string Health string // placeholder, lack containerd support. ExitCode uint32 // `Publishers` stores docker-compatible ports and used for json output. // `Ports` stores formatted ports and only used for console output. Publishers []PortPublisher Ports string `json:"-"` } func psAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } format, err := cmd.Flags().GetString("format") if err != nil { return err } if format != "json" && format != "table" { return fmt.Errorf("unsupported format %s, supported formats are: [table|json]", format) } status, err := cmd.Flags().GetStringArray("status") if err != nil { return err } quiet, err := cmd.Flags().GetBool("quiet") if err != nil { return err } displayServices, err := cmd.Flags().GetBool("services") if err != nil { return err } filter, err := cmd.Flags().GetString("filter") if err != nil { return err } if filter != "" { splited := strings.SplitN(filter, "=", 2) if len(splited) != 2 { return fmt.Errorf("invalid argument \"%s\" for \"-f, --filter\": bad format of filter (expected name=value)", filter) } // currently only the 'status' filter is supported if splited[0] != "status" { return fmt.Errorf("invalid filter '%s'", splited[0]) } status = append(status, splited[1]) } all, err := cmd.Flags().GetBool("all") if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() options, err := getComposeOptions(cmd, globalOptions.DebugFull, globalOptions.Experimental) if err != nil { return err } c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr()) if err != nil { return err } serviceNames, err := c.ServiceNames(args...) if err != nil { return err } containers, err := c.Containers(ctx, serviceNames...) if err != nil { return err } if !all { var upContainers []containerd.Container for _, container := range containers { // cStatus := formatter.ContainerStatus(ctx, c) cStatus, err := containerutil.ContainerStatus(ctx, container) if err != nil { continue } if cStatus.Status == containerd.Running { upContainers = append(upContainers, container) } } containers = upContainers } if len(status) != 0 { var filterdContainers []containerd.Container for _, container := range containers { cStatus := statusForFilter(ctx, container) for _, s := range status { if cStatus == s { filterdContainers = append(filterdContainers, container) } } } containers = filterdContainers } if quiet { for _, c := range containers { fmt.Fprintln(cmd.OutOrStdout(), c.ID()) } return nil } containersPrintable := make([]composeContainerPrintable, len(containers)) eg, ctx := errgroup.WithContext(ctx) for i, container := range containers { i, container := i, container eg.Go(func() error { var p composeContainerPrintable var err error if format == "json" { p, err = composeContainerPrintableJSON(ctx, container, globalOptions) } else { p, err = composeContainerPrintableTab(ctx, container, globalOptions) } if err != nil { return err } containersPrintable[i] = p return nil }) } if err := eg.Wait(); err != nil { return err } if displayServices { for _, p := range containersPrintable { fmt.Fprintln(cmd.OutOrStdout(), p.Service) } return nil } if format == "json" { outJSON, err := formatter.ToJSON(containersPrintable, "", "") if err != nil { return err } _, err = fmt.Fprint(cmd.OutOrStdout(), outJSON) return err } w := tabwriter.NewWriter(cmd.OutOrStdout(), 4, 8, 4, ' ', 0) fmt.Fprintln(w, "NAME\tIMAGE\tCOMMAND\tSERVICE\tSTATUS\tPORTS") for _, p := range containersPrintable { if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", p.Name, p.Image, p.Command, p.Service, p.State, p.Ports, ); err != nil { return err } } return w.Flush() } // composeContainerPrintableTab constructs composeContainerPrintable with fields // only for console output. func composeContainerPrintableTab(ctx context.Context, container containerd.Container, gOptions types.GlobalCommandOptions) (composeContainerPrintable, error) { info, err := container.Info(ctx, containerd.WithoutRefreshedMetadata) if err != nil { return composeContainerPrintable{}, err } spec, err := container.Spec(ctx) if err != nil { return composeContainerPrintable{}, err } status := formatter.ContainerStatus(ctx, container) if status == "Up" { status = "running" // corresponds to Docker Compose v2.0.1 } image, err := container.Image(ctx) if err != nil { return composeContainerPrintable{}, err } dataStore, err := clientutil.DataStore(gOptions.DataRoot, gOptions.Address) if err != nil { return composeContainerPrintable{}, err } containerLabels, err := container.Labels(ctx) if err != nil { return composeContainerPrintable{}, err } ports, err := portutil.LoadPortMappings(dataStore, gOptions.Namespace, info.ID, containerLabels) if err != nil { return composeContainerPrintable{}, err } return composeContainerPrintable{ Name: info.Labels[labels.Name], Image: image.Metadata().Name, Command: formatter.InspectContainerCommandTrunc(spec), Service: info.Labels[labels.ComposeService], State: status, Ports: formatter.FormatPorts(ports), }, nil } // composeContainerPrintableJSON constructs composeContainerPrintable with fields // only for json output and compatible docker output. func composeContainerPrintableJSON(ctx context.Context, container containerd.Container, gOptions types.GlobalCommandOptions) (composeContainerPrintable, error) { info, err := container.Info(ctx, containerd.WithoutRefreshedMetadata) if err != nil { return composeContainerPrintable{}, err } spec, err := container.Spec(ctx) if err != nil { return composeContainerPrintable{}, err } var ( state string exitCode uint32 ) status, err := containerutil.ContainerStatus(ctx, container) if err == nil { // show exitCode only when container is exited/stopped if status.Status == containerd.Stopped { state = "exited" exitCode = status.ExitStatus } else { state = string(status.Status) } } else { state = string(containerd.Unknown) } image, err := container.Image(ctx) if err != nil { return composeContainerPrintable{}, err } dataStore, err := clientutil.DataStore(gOptions.DataRoot, gOptions.Address) if err != nil { return composeContainerPrintable{}, err } containerLabels, err := container.Labels(ctx) if err != nil { return composeContainerPrintable{}, err } portMappings, err := portutil.LoadPortMappings(dataStore, gOptions.Namespace, info.ID, containerLabels) if err != nil { return composeContainerPrintable{}, err } return composeContainerPrintable{ ID: container.ID(), Name: info.Labels[labels.Name], Image: image.Metadata().Name, Command: formatter.InspectContainerCommand(spec, false, false), Project: info.Labels[labels.ComposeProject], Service: info.Labels[labels.ComposeService], State: state, Health: "", ExitCode: exitCode, Publishers: formatPublishers(portMappings), }, nil } // PortPublisher hold status about published port // Use this to match the json output with docker compose // FYI: https://github.com/docker/compose/blob/v2.13.0/pkg/api/api.go#L305C27-L311 type PortPublisher struct { URL string TargetPort int PublishedPort int Protocol string } // formatPublishers parses and returns docker-compatible []PortPublisher from // label map. If an error happens, an empty slice is returned. func formatPublishers(portMappings []cni.PortMapping) []PortPublisher { mapper := func(pm cni.PortMapping) PortPublisher { return PortPublisher{ URL: pm.HostIP, TargetPort: int(pm.ContainerPort), PublishedPort: int(pm.HostPort), Protocol: pm.Protocol, } } var dockerPorts []PortPublisher for _, p := range portMappings { dockerPorts = append(dockerPorts, mapper(p)) } return dockerPorts } // statusForFilter returns the status value to be matched with the 'status' filter func statusForFilter(ctx context.Context, c containerd.Container) string { task, err := c.Task(ctx, nil) if err != nil { // NOTE: NotFound doesn't mean that container hasn't started. // In docker/CRI-containerd plugin, the task will be deleted // when it exits. So, the status will be "created" for this // case. if errdefs.IsNotFound(err) { return string(containerd.Created) } return string(containerd.Unknown) } status, err := task.Status(ctx) if err != nil { return string(containerd.Unknown) } labels, err := c.Labels(ctx) if err != nil { return string(containerd.Unknown) } switch s := status.Status; s { case containerd.Stopped: if labels[restart.StatusLabel] == string(containerd.Running) && restart.Reconcile(status, labels) { return "restarting" } return "exited" default: return string(s) } } ================================================ FILE: cmd/nerdctl/compose/compose_ps_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "encoding/json" "fmt" "strings" "testing" "time" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/v2/pkg/tabutil" "github.com/containerd/nerdctl/v2/pkg/testutil" ) func TestComposePs(t *testing.T) { base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` services: wordpress: image: %s container_name: wordpress_container environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser WORDPRESS_DB_PASSWORD: examplepass WORDPRESS_DB_NAME: exampledb volumes: - wordpress:/var/www/html db: image: %s container_name: db_container environment: MYSQL_DATABASE: exampledb MYSQL_USER: exampleuser MYSQL_PASSWORD: examplepass MYSQL_RANDOM_ROOT_PASSWORD: '1' volumes: - db:/var/lib/mysql alpine: image: %s container_name: alpine_container volumes: wordpress: db: `, testutil.WordpressImage, testutil.MariaDBImage, testutil.CommonImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() projectName := comp.ProjectName() t.Logf("projectName=%q", projectName) base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() assertHandler := func(expectedName, expectedImage string) func(stdout string) error { return func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) < 2 { return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) } tab := tabutil.NewReader("NAME\tIMAGE\tCOMMAND\tSERVICE\tSTATUS\tPORTS") err := tab.ParseHeader(lines[0]) if err != nil { return fmt.Errorf("failed to parse header: %v", err) } container, _ := tab.ReadRow(lines[1], "NAME") assert.Equal(t, container, expectedName) image, _ := tab.ReadRow(lines[1], "IMAGE") assert.Equal(t, image, expectedImage) return nil } } time.Sleep(3 * time.Second) base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "wordpress").AssertOutWithFunc(assertHandler("wordpress_container", testutil.WordpressImage)) base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutWithFunc(assertHandler("db_container", testutil.MariaDBImage)) base.ComposeCmd("-f", comp.YAMLFullPath(), "ps").AssertOutNotContains(testutil.CommonImage) base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "alpine", "-a").AssertOutWithFunc(assertHandler("alpine_container", testutil.CommonImage)) base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "-a", "--filter", "status=exited").AssertOutWithFunc(assertHandler("alpine_container", testutil.CommonImage)) base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "--services", "-a").AssertOutContainsAll("wordpress\n", "db\n", "alpine\n") } func TestComposePsJSON(t *testing.T) { // docker parses unknown 'format' as a Go template and won't output an error testutil.DockerIncompatible(t) base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` services: wordpress: image: %s ports: - 8080:80 environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser WORDPRESS_DB_PASSWORD: examplepass WORDPRESS_DB_NAME: exampledb volumes: - wordpress:/var/www/html db: image: %s environment: MYSQL_DATABASE: exampledb MYSQL_USER: exampleuser MYSQL_PASSWORD: examplepass MYSQL_RANDOM_ROOT_PASSWORD: '1' volumes: - db:/var/lib/mysql volumes: wordpress: db: `, testutil.WordpressImage, testutil.MariaDBImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() projectName := comp.ProjectName() t.Logf("projectName=%q", projectName) base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() assertHandler := func(svc string, count int, fields ...string) func(stdout string) error { return func(stdout string) error { // 1. check json output can be unmarshalled back to printables. var printables []composeContainerPrintable if err := json.Unmarshal([]byte(stdout), &printables); err != nil { return fmt.Errorf("[service: %s]failed to unmarshal json output from `compose ps`: %s", svc, stdout) } // 2. check #printables matches expected count. if len(printables) != count { return fmt.Errorf("[service: %s]unmarshal generates %d printables, expected %d: %s", svc, len(printables), count, stdout) } // 3. check marshalled json string has all expected substrings. for _, field := range fields { if !strings.Contains(stdout, field) { return fmt.Errorf("[service: %s]marshalled json output doesn't have expected string (%s): %s", svc, field, stdout) } } return nil } } // check other formats are not supported base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "--format", "yaml").AssertFail() // check all services are up (can be marshalled and unmarshalled) and check Image field exists base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "--format", "json"). AssertOutWithFunc(assertHandler("all", 2, `"Service":"wordpress"`, `"Service":"db"`, fmt.Sprintf(`"Image":"%s"`, testutil.WordpressImage), fmt.Sprintf(`"Image":"%s"`, testutil.MariaDBImage))) // check wordpress is running base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "--format", "json", "wordpress"). AssertOutWithFunc(assertHandler("wordpress", 1, `"Service":"wordpress"`, `"State":"running"`, `"TargetPort":80`, `"PublishedPort":8080`)) // check wordpress is stopped base.ComposeCmd("-f", comp.YAMLFullPath(), "stop", "wordpress").AssertOK() base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "--format", "json", "wordpress", "-a"). AssertOutWithFunc(assertHandler("wordpress", 1, `"Service":"wordpress"`, `"State":"exited"`)) // check wordpress is removed base.ComposeCmd("-f", comp.YAMLFullPath(), "rm", "-f", "wordpress").AssertOK() base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "--format", "json", "wordpress"). AssertOutWithFunc(assertHandler("wordpress", 0)) } ================================================ FILE: cmd/nerdctl/compose/compose_pull.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/compose" "github.com/containerd/nerdctl/v2/pkg/composer" ) func pullCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "pull [flags] [SERVICE...]", Short: "Pull service images", RunE: pullAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().BoolP("quiet", "q", false, "Pull without printing progress information") return cmd } func pullAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() options, err := getComposeOptions(cmd, globalOptions.DebugFull, globalOptions.Experimental) if err != nil { return err } c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr()) if err != nil { return err } quiet, err := cmd.Flags().GetBool("quiet") if err != nil { return err } po := composer.PullOptions{ Quiet: quiet, } return c.Pull(ctx, po, args) } ================================================ FILE: cmd/nerdctl/compose/compose_pull_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "fmt" "testing" "github.com/containerd/nerdctl/v2/pkg/testutil" ) func TestComposePullWithService(t *testing.T) { base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` services: wordpress: image: %s environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser WORDPRESS_DB_PASSWORD: examplepass WORDPRESS_DB_NAME: exampledb volumes: - wordpress:/var/www/html db: image: %s environment: MYSQL_DATABASE: exampledb MYSQL_USER: exampleuser MYSQL_PASSWORD: examplepass MYSQL_RANDOM_ROOT_PASSWORD: '1' volumes: - db:/var/lib/mysql volumes: wordpress: db: `, testutil.WordpressImage, testutil.MariaDBImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() projectName := comp.ProjectName() t.Logf("projectName=%q", projectName) base.ComposeCmd("-f", comp.YAMLFullPath(), "pull", "db").AssertOutNotContains("wordpress") } ================================================ FILE: cmd/nerdctl/compose/compose_push.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/compose" "github.com/containerd/nerdctl/v2/pkg/composer" ) func pushCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "push [flags] [SERVICE...]", Short: "Push service images", RunE: pushAction, SilenceUsage: true, SilenceErrors: true, } return cmd } func pushAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() options, err := getComposeOptions(cmd, globalOptions.DebugFull, globalOptions.Experimental) if err != nil { return err } c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr()) if err != nil { return err } po := composer.PushOptions{} return c.Push(ctx, po, args) } ================================================ FILE: cmd/nerdctl/compose/compose_restart.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/compose" "github.com/containerd/nerdctl/v2/pkg/composer" ) func restartCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "restart [flags] [SERVICE...]", Short: "Restart containers of given (or all) services", RunE: restartAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().UintP("timeout", "t", 10, "Seconds to wait before restarting them") return cmd } func restartAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } var opt composer.RestartOptions if cmd.Flags().Changed("timeout") { timeValue, err := cmd.Flags().GetUint("timeout") if err != nil { return err } opt.Timeout = &timeValue } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() options, err := getComposeOptions(cmd, globalOptions.DebugFull, globalOptions.Experimental) if err != nil { return err } c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr()) if err != nil { return err } return c.Restart(ctx, opt, args) } ================================================ FILE: cmd/nerdctl/compose/compose_restart_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "fmt" "testing" "github.com/containerd/nerdctl/v2/pkg/testutil" ) func TestComposeRestart(t *testing.T) { base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` services: wordpress: image: %s environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser WORDPRESS_DB_PASSWORD: examplepass WORDPRESS_DB_NAME: exampledb volumes: - wordpress:/var/www/html db: image: %s environment: MYSQL_DATABASE: exampledb MYSQL_USER: exampleuser MYSQL_PASSWORD: examplepass MYSQL_RANDOM_ROOT_PASSWORD: '1' volumes: - db:/var/lib/mysql volumes: wordpress: db: `, testutil.WordpressImage, testutil.MariaDBImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() projectName := comp.ProjectName() t.Logf("projectName=%q", projectName) base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() // stop and restart a single service. base.ComposeCmd("-f", comp.YAMLFullPath(), "stop", "db").AssertOK() base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db", "-a").AssertOutContainsAny("Exit", "exited") base.ComposeCmd("-f", comp.YAMLFullPath(), "restart", "db").AssertOK() base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutContainsAny("Up", "running") // stop one service and restart all (also check `--timeout` arg). base.ComposeCmd("-f", comp.YAMLFullPath(), "stop", "db").AssertOK() base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db", "-a").AssertOutContainsAny("Exit", "exited") base.ComposeCmd("-f", comp.YAMLFullPath(), "restart", "--timeout", "5").AssertOK() base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutContainsAny("Up", "running") base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "wordpress").AssertOutContainsAny("Up", "running") } ================================================ FILE: cmd/nerdctl/compose/compose_rm.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "fmt" "strings" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/compose" "github.com/containerd/nerdctl/v2/pkg/composer" ) func removeCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "rm [flags] [SERVICE...]", Short: "Remove stopped service containers", RunE: removeAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().BoolP("force", "f", false, "Do not prompt for confirmation") cmd.Flags().BoolP("stop", "s", false, "Stop containers before removing") cmd.Flags().BoolP("volumes", "v", false, "Remove anonymous volumes associated with containers") return cmd } func removeAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } force, err := cmd.Flags().GetBool("force") if err != nil { return err } if !force { services := "all" if len(args) != 0 { services = strings.Join(args, ",") } msg := fmt.Sprintf("This will remove all stopped containers from services: %s.", services) if confirmed, err := helpers.Confirm(cmd, fmt.Sprintf("WARNING! %s.", msg)); err != nil || !confirmed { return err } } stop, err := cmd.Flags().GetBool("stop") if err != nil { return err } volumes, err := cmd.Flags().GetBool("volumes") if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() options, err := getComposeOptions(cmd, globalOptions.DebugFull, globalOptions.Experimental) if err != nil { return err } c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr()) if err != nil { return err } rmOpts := composer.RemoveOptions{ Stop: stop, Volumes: volumes, } return c.Remove(ctx, rmOpts, args) } ================================================ FILE: cmd/nerdctl/compose/compose_rm_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "fmt" "regexp" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeRemove(t *testing.T) { var dockerComposeYAML = fmt.Sprintf(` services: wordpress: image: %s environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser WORDPRESS_DB_PASSWORD: examplepass WORDPRESS_DB_NAME: exampledb volumes: - wordpress:/var/www/html db: image: %s environment: MYSQL_DATABASE: exampledb MYSQL_USER: exampleuser MYSQL_PASSWORD: examplepass MYSQL_RANDOM_ROOT_PASSWORD: '1' volumes: - db:/var/lib/mysql volumes: wordpress: db: `, testutil.WordpressImage, testutil.MariaDBImage) testCase := nerdtest.Setup() testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down") } testCase.Setup = func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerComposeYAML, "compose.yaml") helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "up", "-d") data.Labels().Set("yamlPath", data.Temp().Path("compose.yaml")) } testCase.SubTests = []*test.Case{ { Description: "All services are still up", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "rm", "-f") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { wp := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "wordpress") db := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "db") comp := expect.Match(regexp.MustCompile("Up|running")) comp(wp, t) comp(db, t) }, } }, }, { Description: "Remove stopped service", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { helpers.Ensure("compose", "-f", data.Labels().Get("yamlPath"), "stop", "wordpress") return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "rm", "-f") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { wp := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "wordpress") db := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "db") expect.DoesNotContain("wordpress")(wp, t) expect.Match(regexp.MustCompile("Up|running"))(db, t) }, } }, }, { Description: "Remove all services with stop", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "rm", "-f", "-s") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { db := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "db") expect.DoesNotContain("db")(db, t) }, } }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/compose/compose_run.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "errors" "fmt" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/compose" "github.com/containerd/nerdctl/v2/pkg/composer" ) func runCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "run [flags] SERVICE [COMMAND] [ARGS...]", Short: "Run a one-off command on a service", Args: cobra.MinimumNArgs(1), RunE: runAction, SilenceUsage: true, SilenceErrors: true, DisableFlagsInUseLine: true, } cmd.Flags().SetInterspersed(false) cmd.Flags().BoolP("detach", "d", false, "Detached mode: Run containers in the background") cmd.Flags().Bool("no-build", false, "Don't build an image, even if it's missing.") cmd.Flags().Bool("no-color", false, "Produce monochrome output") cmd.Flags().Bool("no-log-prefix", false, "Don't print prefix in logs") cmd.Flags().Bool("build", false, "Build images before starting containers.") cmd.Flags().Bool("quiet-pull", false, "Pull without printing progress information") cmd.Flags().Bool("remove-orphans", false, "Remove containers for services not defined in the Compose file.") cmd.Flags().String("name", "", "Assign a name to the container") cmd.Flags().Bool("no-deps", false, "Don't start dependencies") // TODO: no-TTY flag // In docker-compose's documentation, no-TTY is automatically detected // But, it follows `-i` flag because currently `run` command needs `-it` simultaneously. cmd.Flags().BoolP("interactive", "i", true, "Keep STDIN open even if not attached") cmd.Flags().Bool("rm", false, "Automatically remove the container when it exits") cmd.Flags().StringP("user", "u", "", "Username or UID (format: [:])") cmd.Flags().StringArrayP("volume", "v", nil, "Bind mount a volume") cmd.Flags().StringArray("entrypoint", nil, "Overwrite the default ENTRYPOINT of the image") cmd.Flags().StringArrayP("env", "e", nil, "Set environment variables") cmd.Flags().StringArrayP("label", "l", nil, "Set metadata on container") cmd.Flags().StringP("workdir", "w", "", "Working directory inside the container") // FIXME: `-p` conflicts with the `--project-name` in PersistentFlags of parent command `compose` // For docker compatibility, it should be fixed. cmd.Flags().StringSlice("publish", nil, "Publish a container's port(s) to the host") cmd.Flags().Bool("service-ports", false, "Run command with the service's ports enabled and mapped to the host") // TODO: use-aliases return cmd } func runAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } detach, err := cmd.Flags().GetBool("detach") if err != nil { return err } noBuild, err := cmd.Flags().GetBool("no-build") if err != nil { return err } noColor, err := cmd.Flags().GetBool("no-color") if err != nil { return err } noLogPrefix, err := cmd.Flags().GetBool("no-log-prefix") if err != nil { return err } build, err := cmd.Flags().GetBool("build") if err != nil { return err } if build && noBuild { return errors.New("--build and --no-build can not be combined") } quietPull, err := cmd.Flags().GetBool("quiet-pull") if err != nil { return err } removeOrphans, err := cmd.Flags().GetBool("remove-orphans") if err != nil { return err } name, err := cmd.Flags().GetString("name") if err != nil { return err } nodeps, err := cmd.Flags().GetBool("no-deps") if err != nil { return err } interactive, err := cmd.Flags().GetBool("interactive") if err != nil { return err } // FIXME : https://github.com/containerd/nerdctl/blob/v0.22.2/cmd/nerdctl/run.go#L100 tty := interactive rm, err := cmd.Flags().GetBool("rm") if err != nil { return err } user, err := cmd.Flags().GetString("user") if err != nil { return err } volume, err := cmd.Flags().GetStringArray("volume") if err != nil { return err } entrypoint, err := cmd.Flags().GetStringArray("entrypoint") if err != nil { return err } env, err := cmd.Flags().GetStringArray("env") if err != nil { return err } label, err := cmd.Flags().GetStringArray("label") if err != nil { return err } workdir, err := cmd.Flags().GetString("workdir") if err != nil { return err } publish, err := cmd.Flags().GetStringSlice("publish") if err != nil { return err } servicePorts, err := cmd.Flags().GetBool("service-ports") if err != nil { return err } if servicePorts && publish != nil && len(publish) > 0 { return fmt.Errorf("--service-ports and --publish(-p) cannot exist simultaneously") } // https://github.com/containerd/nerdctl/blob/v0.22.2/cmd/nerdctl/run.go#L475 if interactive && detach { return errors.New("currently flag -i and -d cannot be specified together (FIXME)") } // https://github.com/containerd/nerdctl/blob/v0.22.2/cmd/nerdctl/run.go#L479 if tty && detach { return errors.New("currently flag -t and -d cannot be specified together (FIXME)") } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() options, err := getComposeOptions(cmd, globalOptions.DebugFull, globalOptions.Experimental) if err != nil { return err } c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr()) if err != nil { return err } ro := composer.RunOptions{ Detach: detach, NoBuild: noBuild, NoColor: noColor, NoLogPrefix: noLogPrefix, ForceBuild: build, QuietPull: quietPull, RemoveOrphans: removeOrphans, ServiceName: args[0], Args: args[1:], Name: name, NoDeps: nodeps, Tty: tty, Interactive: interactive, Rm: rm, User: user, Volume: volume, Entrypoint: entrypoint, Env: env, Label: label, WorkDir: workdir, ServicePorts: servicePorts, Publish: publish, } return c.Run(ctx, ro) } ================================================ FILE: cmd/nerdctl/compose/compose_run_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "fmt" "io" "strings" "testing" "time" "gotest.tools/v3/assert" "github.com/containerd/log" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" ) func TestComposeRun(t *testing.T) { const expectedOutput = "speed 38400 baud" dockerComposeYAML := fmt.Sprintf(` services: alpine: image: %s entrypoint: - stty `, testutil.CommonImage) testCase := nerdtest.Setup() testCase.SubTests = []*test.Case{ { Description: "pty run", Setup: func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerComposeYAML, "compose.yaml") }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command( "compose", "-f", data.Temp().Path("compose.yaml"), "run", "--name", data.Identifier(), "alpine", ) cmd.WithPseudoTTY() return cmd }, Expected: test.Expects(0, nil, expect.Contains(expectedOutput)), Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", "-v", data.Identifier()) helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") }, }, { Description: "pty run with --rm", Setup: func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerComposeYAML, "compose.yaml") }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command( "compose", "-f", data.Temp().Path("compose.yaml"), "run", "--name", data.Identifier(), "--rm", "alpine", ) cmd.WithPseudoTTY() return cmd }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { // Ensure the container has been removed capt := helpers.Capture("ps", "-a", "--format=\"{{.Names}}\"") assert.Assert(t, !strings.Contains(capt, data.Identifier()), capt) return &test.Expected{ Output: expect.Contains(expectedOutput), } }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", "-v", data.Identifier()) helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") }, }, } testCase.Run(t) } func TestComposeRunWithServicePorts(t *testing.T) { base := testutil.NewBase(t) // specify the name of container in order to remove // TODO: when `compose rm` is implemented, replace it. containerName := testutil.Identifier(t) dockerComposeYAML := fmt.Sprintf(` services: web: image: %s ports: - 8080:80 `, testutil.NginxAlpineImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() projectName := comp.ProjectName() t.Logf("projectName=%q", projectName) defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() defer base.Cmd("rm", "-f", "-v", containerName).Run() go func() { // unbuffer(1) emulates tty, which is required by `nerdctl run -t`. // unbuffer(1) can be installed with `apt-get install expect`. unbuffer := []string{"unbuffer"} base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "run", "--service-ports", "--name", containerName, "web").Run() }() checkNginx := func() error { resp, err := nettestutil.HTTPGet("http://127.0.0.1:8080", 5, false) if err != nil { return err } respBody, err := io.ReadAll(resp.Body) if err != nil { return err } if !strings.Contains(string(respBody), testutil.NginxAlpineIndexHTMLSnippet) { t.Logf("respBody=%q", respBody) return fmt.Errorf("respBody does not contain %q", testutil.NginxAlpineIndexHTMLSnippet) } return nil } var nginxWorking bool for i := 0; i < 30; i++ { t.Logf("(retry %d)", i) err := checkNginx() if err == nil { nginxWorking = true break } t.Log(err) time.Sleep(3 * time.Second) } if !nginxWorking { t.Fatal("nginx is not working") } t.Log("nginx seems functional") } func TestComposeRunWithPublish(t *testing.T) { base := testutil.NewBase(t) // specify the name of container in order to remove // TODO: when `compose rm` is implemented, replace it. containerName := testutil.Identifier(t) dockerComposeYAML := fmt.Sprintf(` services: web: image: %s `, testutil.NginxAlpineImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() projectName := comp.ProjectName() t.Logf("projectName=%q", projectName) defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() defer base.Cmd("rm", "-f", "-v", containerName).Run() go func() { // unbuffer(1) emulates tty, which is required by `nerdctl run -t`. // unbuffer(1) can be installed with `apt-get install expect`. unbuffer := []string{"unbuffer"} base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "run", "--publish", "8080:80", "--name", containerName, "web").Run() }() checkNginx := func() error { resp, err := nettestutil.HTTPGet("http://127.0.0.1:8080", 5, false) if err != nil { return err } respBody, err := io.ReadAll(resp.Body) if err != nil { return err } if !strings.Contains(string(respBody), testutil.NginxAlpineIndexHTMLSnippet) { t.Logf("respBody=%q", respBody) return fmt.Errorf("respBody does not contain %q", testutil.NginxAlpineIndexHTMLSnippet) } return nil } var nginxWorking bool for i := 0; i < 30; i++ { t.Logf("(retry %d)", i) err := checkNginx() if err == nil { nginxWorking = true break } t.Log(err) time.Sleep(3 * time.Second) } if !nginxWorking { t.Fatal("nginx is not working") } t.Log("nginx seems functional") } func TestComposeRunWithEnv(t *testing.T) { base := testutil.NewBase(t) // specify the name of container in order to remove // TODO: when `compose rm` is implemented, replace it. containerName := testutil.Identifier(t) dockerComposeYAML := fmt.Sprintf(` services: alpine: image: %s entrypoint: - sh - -c - "echo $$FOO" `, testutil.CommonImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() projectName := comp.ProjectName() t.Logf("projectName=%q", projectName) defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() defer base.Cmd("rm", "-f", "-v", containerName).Run() const partialOutput = "bar" // unbuffer(1) emulates tty, which is required by `nerdctl run -t`. // unbuffer(1) can be installed with `apt-get install expect`. unbuffer := []string{"unbuffer"} base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "run", "-e", "FOO=bar", "--name", containerName, "alpine").AssertOutContains(partialOutput) } func TestComposeRunWithUser(t *testing.T) { base := testutil.NewBase(t) // specify the name of container in order to remove // TODO: when `compose rm` is implemented, replace it. containerName := testutil.Identifier(t) dockerComposeYAML := fmt.Sprintf(` services: alpine: image: %s entrypoint: - id - -u `, testutil.CommonImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() projectName := comp.ProjectName() t.Logf("projectName=%q", projectName) defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() defer base.Cmd("rm", "-f", "-v", containerName).Run() const partialOutput = "5000" // unbuffer(1) emulates tty, which is required by `nerdctl run -t`. // unbuffer(1) can be installed with `apt-get install expect`. unbuffer := []string{"unbuffer"} base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "run", "--user", "5000", "--name", containerName, "alpine").AssertOutContains(partialOutput) } func TestComposeRunWithLabel(t *testing.T) { base := testutil.NewBase(t) containerName := testutil.Identifier(t) dockerComposeYAML := fmt.Sprintf(` services: alpine: image: %s entrypoint: - echo - "dummy log" labels: - "foo=bar" `, testutil.CommonImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() projectName := comp.ProjectName() t.Logf("projectName=%q", projectName) defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() defer base.Cmd("rm", "-f", "-v", containerName).Run() // unbuffer(1) emulates tty, which is required by `nerdctl run -t`. // unbuffer(1) can be installed with `apt-get install expect`. unbuffer := []string{"unbuffer"} base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "run", "--label", "foo=rab", "--label", "x=y", "--name", containerName, "alpine").AssertOK() container := base.InspectContainer(containerName) if container.Config == nil { log.L.Errorf("test failed, cannot fetch container config") t.Fail() } assert.Equal(t, container.Config.Labels["foo"], "rab") assert.Equal(t, container.Config.Labels["x"], "y") } func TestComposeRunWithArgs(t *testing.T) { base := testutil.NewBase(t) containerName := testutil.Identifier(t) dockerComposeYAML := fmt.Sprintf(` services: alpine: image: %s entrypoint: - echo `, testutil.CommonImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() projectName := comp.ProjectName() t.Logf("projectName=%q", projectName) defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() defer base.Cmd("rm", "-f", "-v", containerName).Run() const partialOutput = "hello world" // unbuffer(1) emulates tty, which is required by `nerdctl run -t`. // unbuffer(1) can be installed with `apt-get install expect`. unbuffer := []string{"unbuffer"} base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "run", "--name", containerName, "alpine", partialOutput).AssertOutContains(partialOutput) } func TestComposeRunWithEntrypoint(t *testing.T) { base := testutil.NewBase(t) // specify the name of container in order to remove // TODO: when `compose rm` is implemented, replace it. containerName := testutil.Identifier(t) dockerComposeYAML := fmt.Sprintf(` services: alpine: image: %s entrypoint: - stty # should be changed `, testutil.CommonImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() projectName := comp.ProjectName() t.Logf("projectName=%q", projectName) defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() defer base.Cmd("rm", "-f", "-v", containerName).Run() const partialOutput = "hello world" // unbuffer(1) emulates tty, which is required by `nerdctl run -t`. // unbuffer(1) can be installed with `apt-get install expect`. unbuffer := []string{"unbuffer"} base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "run", "--entrypoint", "echo", "--name", containerName, "alpine", partialOutput).AssertOutContains(partialOutput) } func TestComposeRunWithVolume(t *testing.T) { base := testutil.NewBase(t) containerName := testutil.Identifier(t) dockerComposeYAML := fmt.Sprintf(` services: alpine: image: %s entrypoint: - stty # no meaning, just put any command `, testutil.CommonImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() projectName := comp.ProjectName() t.Logf("projectName=%q", projectName) defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() // The directory is automatically removed by Cleanup tmpDir := t.TempDir() destinationDir := "/data" volumeFlagStr := fmt.Sprintf("%s:%s", tmpDir, destinationDir) defer base.Cmd("rm", "-f", "-v", containerName).Run() // unbuffer(1) emulates tty, which is required by `nerdctl run -t`. // unbuffer(1) can be installed with `apt-get install expect`. unbuffer := []string{"unbuffer"} base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "run", "--volume", volumeFlagStr, "--name", containerName, "alpine").AssertOK() container := base.InspectContainer(containerName) errMsg := fmt.Sprintf("test failed, cannot find volume: %v", container.Mounts) assert.Assert(t, container.Mounts != nil, errMsg) assert.Assert(t, len(container.Mounts) == 1, errMsg) assert.Assert(t, container.Mounts[0].Source == tmpDir, errMsg) assert.Assert(t, container.Mounts[0].Destination == destinationDir, errMsg) } func TestComposePushAndPullWithCosignVerify(t *testing.T) { testutil.RequireExecutable(t, "cosign") testutil.DockerIncompatible(t) testutil.RequiresBuild(t) testutil.RegisterBuildCacheCleanup(t) t.Parallel() base := testutil.NewBase(t) base.Env = append(base.Env, "COSIGN_PASSWORD=1") keyPair := helpers.NewCosignKeyPair(t, "cosign-key-pair", "1") reg := testregistry.NewWithNoAuth(base, 0, false) t.Cleanup(func() { keyPair.Cleanup() reg.Cleanup(nil) }) tID := testutil.Identifier(t) testImageRefPrefix := fmt.Sprintf("127.0.0.1:%d/%s/", reg.Port, tID) var ( imageSvc0 = testImageRefPrefix + "composebuild_svc0" imageSvc1 = testImageRefPrefix + "composebuild_svc1" imageSvc2 = testImageRefPrefix + "composebuild_svc2" ) dockerComposeYAML := fmt.Sprintf(` services: svc0: build: . image: %s x-nerdctl-verify: cosign x-nerdctl-cosign-public-key: %s x-nerdctl-sign: cosign x-nerdctl-cosign-private-key: %s entrypoint: - stty svc1: build: . image: %s x-nerdctl-verify: cosign x-nerdctl-cosign-public-key: dummy_pub_key x-nerdctl-sign: cosign x-nerdctl-cosign-private-key: %s entrypoint: - stty svc2: build: . image: %s x-nerdctl-verify: none x-nerdctl-sign: none entrypoint: - stty `, imageSvc0, keyPair.PublicKey, keyPair.PrivateKey, imageSvc1, keyPair.PrivateKey, imageSvc2) dockerfile := fmt.Sprintf(`FROM %s`, testutil.CommonImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() comp.WriteFile("Dockerfile", dockerfile) projectName := comp.ProjectName() t.Logf("projectName=%q", projectName) defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() // 1. build both services/images base.ComposeCmd("-f", comp.YAMLFullPath(), "build").AssertOK() // 2. compose push with cosign for svc0/svc1, (and none for svc2) base.ComposeCmd("-f", comp.YAMLFullPath(), "push").AssertOK() // 3. compose pull with cosign base.ComposeCmd("-f", comp.YAMLFullPath(), "pull", "svc0").AssertOK() // key match base.ComposeCmd("-f", comp.YAMLFullPath(), "pull", "svc1").AssertFail() // key mismatch base.ComposeCmd("-f", comp.YAMLFullPath(), "pull", "svc2").AssertOK() // verify passed // 4. compose run const sttyPartialOutput = "speed 38400 baud" // unbuffer(1) emulates tty, which is required by `nerdctl run -t`. // unbuffer(1) can be installed with `apt-get install expect`. unbuffer := []string{"unbuffer"} base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "run", "svc0").AssertOutContains(sttyPartialOutput) // key match base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "run", "svc1").AssertFail() // key mismatch base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "run", "svc2").AssertOutContains(sttyPartialOutput) // verify passed // 5. compose up base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "svc0").AssertOK() // key match base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "svc1").AssertFail() // key mismatch base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "svc2").AssertOK() // verify passed } ================================================ FILE: cmd/nerdctl/compose/compose_start.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "context" "fmt" "os" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/errdefs" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/compose" "github.com/containerd/nerdctl/v2/pkg/config" "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/labels" ) func startCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "start [SERVICE...]", Short: "Start existing containers for service(s)", RunE: startAction, SilenceUsage: true, SilenceErrors: true, DisableFlagsInUseLine: true, } return cmd } func startAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } nerdctlCmd, nerdctlArgs := helpers.GlobalFlags(cmd) client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() options, err := getComposeOptions(cmd, globalOptions.DebugFull, globalOptions.Experimental) if err != nil { return err } c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr()) if err != nil { return err } // TODO(djdongjin): move to `pkg/composer` and rewrite `c.Services + for-loop` // with `c.project.WithServices` after refactor (#1639) is done. services, err := c.Services(ctx, args...) if err != nil { return err } for _, svc := range services { svcName := svc.Unparsed.Name containers, err := c.Containers(ctx, svcName) if err != nil { return err } // return error if no containers and service replica is not zero if len(containers) == 0 { if len(svc.Containers) == 0 { continue } return fmt.Errorf("service %q has no container to start", svcName) } if err := startContainers(ctx, client, containers, &globalOptions, nerdctlCmd, nerdctlArgs); err != nil { return err } } return nil } func startContainers(ctx context.Context, client *containerd.Client, containers []containerd.Container, globalOptions *types.GlobalCommandOptions, nerdctlCmd string, nerdctlArgs []string) error { eg, ctx := errgroup.WithContext(ctx) for _, c := range containers { c := c eg.Go(func() error { if cStatus, err := containerutil.ContainerStatus(ctx, c); err != nil { // NOTE: NotFound doesn't mean that container hasn't started. // In docker/CRI-containerd plugin, the task will be deleted // when it exits. So, the status will be "created" for this // case. if !errdefs.IsNotFound(err) { return err } } else if cStatus.Status == containerd.Running { return nil } // in compose, always disable attach if err := containerutil.Start(ctx, c, false, false, client, "", "", (*config.Config)(globalOptions), nerdctlCmd, nerdctlArgs); err != nil { return err } info, err := c.Info(ctx, containerd.WithoutRefreshedMetadata) if err != nil { return err } _, err = fmt.Fprintf(os.Stdout, "Container %s started\n", info.Labels[labels.Name]) return err }) } return eg.Wait() } ================================================ FILE: cmd/nerdctl/compose/compose_start_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "fmt" "regexp" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeStart(t *testing.T) { var dockerComposeYAML = fmt.Sprintf(` services: svc0: image: %s command: "sleep infinity" svc1: image: %s command: "sleep infinity" `, testutil.CommonImage, testutil.CommonImage) testCase := nerdtest.Setup() testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down") } testCase.Setup = func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerComposeYAML, "compose.yaml") helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "up", "-d") helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "start") helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "stop", "--timeout", "1", "svc0") helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "kill", "svc1") } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Temp().Path("compose.yaml"), "start") } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Errors: nil, Output: func(stdout string, t tig.T) { svc0 := helpers.Capture("compose", "-f", data.Temp().Path("compose.yaml"), "ps", "svc0") svc1 := helpers.Capture("compose", "-f", data.Temp().Path("compose.yaml"), "ps", "svc1") comp := expect.Match(regexp.MustCompile("Up|running")) comp(svc0, t) comp(svc1, t) }, } } testCase.Run(t) } func TestComposeStartFailWhenServicePause(t *testing.T) { var dockerComposeYAML = fmt.Sprintf(` services: svc0: image: %s command: "sleep infinity" `, testutil.CommonImage) testCase := nerdtest.Setup() testCase.Require = nerdtest.CGroup testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down") } testCase.Setup = func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerComposeYAML, "compose.yaml") helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "up", "-d") helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "pause", "svc0") } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Temp().Path("compose.yaml"), "start") } testCase.Expected = test.Expects(expect.ExitCodeGenericFail, nil, nil) testCase.Run(t) } ================================================ FILE: cmd/nerdctl/compose/compose_stop.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/compose" "github.com/containerd/nerdctl/v2/pkg/composer" ) func stopCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "stop [flags] [SERVICE...]", Short: "Stop running containers without removing them.", RunE: stopAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().UintP("timeout", "t", 10, "Seconds to wait for stop before killing them") return cmd } func stopAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } var opt composer.StopOptions if cmd.Flags().Changed("timeout") { timeValue, err := cmd.Flags().GetUint("timeout") if err != nil { return err } opt.Timeout = &timeValue } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() options, err := getComposeOptions(cmd, globalOptions.DebugFull, globalOptions.Experimental) if err != nil { return err } c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr()) if err != nil { return err } return c.Stop(ctx, opt, args) } ================================================ FILE: cmd/nerdctl/compose/compose_stop_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "fmt" "regexp" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeStop(t *testing.T) { var dockerComposeYAML = fmt.Sprintf(` services: wordpress: image: %s environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser WORDPRESS_DB_PASSWORD: examplepass WORDPRESS_DB_NAME: exampledb volumes: - wordpress:/var/www/html db: image: %s environment: MYSQL_DATABASE: exampledb MYSQL_USER: exampleuser MYSQL_PASSWORD: examplepass MYSQL_RANDOM_ROOT_PASSWORD: '1' volumes: - db:/var/lib/mysql volumes: wordpress: db: `, testutil.WordpressImage, testutil.MariaDBImage) testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerComposeYAML, "compose.yaml") helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "up", "-d") data.Labels().Set("yamlPath", data.Temp().Path("compose.yaml")) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down") } testCase.SubTests = []*test.Case{ { Description: "stop db", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("compose", "-f", data.Labels().Get("yamlPath"), "stop", "db") }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "ps", "db", "-a") }, Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile("Exit|exited"))), }, { Description: "wordpress is still running", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "ps", "wordpress") }, Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile("Up|running"))), }, { Description: "stop wordpress", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("compose", "-f", data.Labels().Get("yamlPath"), "stop", "--timeout", "5", "wordpress") }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "ps", "wordpress", "-a") }, Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile("Exit|exited"))), }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/compose/compose_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "testing" "github.com/containerd/nerdctl/v2/pkg/testutil" ) func TestMain(m *testing.M) { testutil.M(m) } ================================================ FILE: cmd/nerdctl/compose/compose_top.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "fmt" "github.com/spf13/cobra" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/compose" "github.com/containerd/nerdctl/v2/pkg/cmd/container" "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/labels" ) func topCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "top [SERVICE...]", Short: "Display the running processes of service containers", RunE: topAction, SilenceUsage: true, SilenceErrors: true, DisableFlagsInUseLine: true, } return cmd } func topAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() options, err := getComposeOptions(cmd, globalOptions.DebugFull, globalOptions.Experimental) if err != nil { return err } c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr()) if err != nil { return err } serviceNames, err := c.ServiceNames(args...) if err != nil { return err } containers, err := c.Containers(ctx, serviceNames...) if err != nil { return err } stdout := cmd.OutOrStdout() for _, c := range containers { cStatus, err := containerutil.ContainerStatus(ctx, c) if err != nil { return err } if cStatus.Status != containerd.Running { continue } info, err := c.Info(ctx, containerd.WithoutRefreshedMetadata) if err != nil { return err } fmt.Fprintln(stdout, info.Labels[labels.Name]) // `compose ps` uses empty ps args err = container.Top(ctx, client, []string{c.ID()}, types.ContainerTopOptions{ Stdout: cmd.OutOrStdout(), GOptions: globalOptions, }) if err != nil { return err } fmt.Fprintln(stdout) } return nil } ================================================ FILE: cmd/nerdctl/compose/compose_top_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "fmt" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeTop(t *testing.T) { var dockerComposeYAML = fmt.Sprintf(` services: svc0: image: %s command: "sleep infinity" svc1: image: %s `, testutil.CommonImage, testutil.NginxAlpineImage) testCase := nerdtest.Setup() testCase.Require = require.All(nerdtest.CgroupsAccessible) testCase.Setup = func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerComposeYAML, "compose.yaml") helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "up", "-d") data.Labels().Set("yamlPath", data.Temp().Path("compose.yaml")) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down") } testCase.SubTests = []*test.Case{ { Description: "svc0 contains sleep infinity", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "top", "svc0") }, Expected: test.Expects(0, nil, expect.Contains("sleep infinity")), }, { Description: "svc1 contains sleep nginx", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "top", "svc1") }, Expected: test.Expects(0, nil, expect.Contains("nginx")), }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/compose/compose_up.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "errors" "fmt" "strconv" "strings" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/compose" "github.com/containerd/nerdctl/v2/pkg/composer" ) func upCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "up [flags] [SERVICE...]", Short: "Create and start containers", RunE: upAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().Bool("abort-on-container-exit", false, "Stops all containers if any container was stopped. Incompatible with -d.") cmd.Flags().BoolP("detach", "d", false, "Detached mode: Run containers in the background. Incompatible with --abort-on-container-exit.") cmd.Flags().Bool("no-build", false, "Don't build an image, even if it's missing.") cmd.Flags().Bool("no-color", false, "Produce monochrome output") cmd.Flags().Bool("no-log-prefix", false, "Don't print prefix in logs") cmd.Flags().Bool("build", false, "Build images before starting containers.") cmd.Flags().Bool("ipfs", false, "Allow pulling base images from IPFS during build") cmd.Flags().Bool("quiet-pull", false, "Pull without printing progress information") cmd.Flags().Bool("remove-orphans", false, "Remove containers for services not defined in the Compose file.") cmd.Flags().Bool("force-recreate", false, "Recreate containers even if their configuration and image haven't changed.") cmd.Flags().Bool("no-recreate", false, "Don't recreate containers if they exist, conflict with --force-recreate.") cmd.Flags().StringArray("scale", []string{}, "Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.") cmd.Flags().String("pull", "", "Pull image before running (\"always\"|\"missing\"|\"never\")") return cmd } func upAction(cmd *cobra.Command, services []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } detach, err := cmd.Flags().GetBool("detach") if err != nil { return err } abortOnContainerExit, err := cmd.Flags().GetBool("abort-on-container-exit") if detach && abortOnContainerExit { return fmt.Errorf("--abort-on-container-exit flag is incompatible with flag --detach") } if err != nil { return err } noBuild, err := cmd.Flags().GetBool("no-build") if err != nil { return err } noColor, err := cmd.Flags().GetBool("no-color") if err != nil { return err } noLogPrefix, err := cmd.Flags().GetBool("no-log-prefix") if err != nil { return err } build, err := cmd.Flags().GetBool("build") if err != nil { return err } if build && noBuild { return errors.New("--build and --no-build can not be combined") } enableIPFS, err := cmd.Flags().GetBool("ipfs") if err != nil { return err } quietPull, err := cmd.Flags().GetBool("quiet-pull") if err != nil { return err } pull, err := cmd.Flags().GetString("pull") if err != nil { return err } removeOrphans, err := cmd.Flags().GetBool("remove-orphans") if err != nil { return err } scaleSlice, err := cmd.Flags().GetStringArray("scale") if err != nil { return err } forceRecreate, err := cmd.Flags().GetBool("force-recreate") if err != nil { return err } noRecreate, err := cmd.Flags().GetBool("no-recreate") if err != nil { return err } if forceRecreate && noRecreate { return errors.New("flag --force-recreate and --no-recreate cannot be specified together") } scale := make(map[string]int) for _, s := range scaleSlice { parts := strings.Split(s, "=") if len(parts) != 2 { return fmt.Errorf("invalid --scale option %q. Should be SERVICE=NUM", s) } replicas, err := strconv.Atoi(parts[1]) if err != nil { return err } scale[parts[0]] = replicas } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() options, err := getComposeOptions(cmd, globalOptions.DebugFull, globalOptions.Experimental) if err != nil { return err } options.Services = services c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr()) if err != nil { return err } uo := composer.UpOptions{ AbortOnContainerExit: abortOnContainerExit, Detach: detach, NoBuild: noBuild, NoColor: noColor, NoLogPrefix: noLogPrefix, ForceBuild: build, IPFS: enableIPFS, QuietPull: quietPull, RemoveOrphans: removeOrphans, Scale: scale, Pull: pull, ForceRecreate: forceRecreate, NoRecreate: noRecreate, } return c.Up(ctx, uo, services) } ================================================ FILE: cmd/nerdctl/compose/compose_up_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "fmt" "io" "path/filepath" "strconv" "strings" "testing" "github.com/docker/go-connections/nat" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" "github.com/containerd/nerdctl/v2/pkg/testutil/portlock" ) func TestComposeUp(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { hostPort, err := portlock.Acquire(0) if err != nil { helpers.T().Log(fmt.Sprintf("Failed to acquire port: %v", err)) helpers.T().FailNow() } composeYAML := fmt.Sprintf(` services: wordpress: image: %s restart: always ports: - %d:80 environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser WORDPRESS_DB_PASSWORD: examplepass WORDPRESS_DB_NAME: exampledb volumes: - wordpress:/var/www/html db: image: %s restart: always environment: MYSQL_DATABASE: exampledb MYSQL_USER: exampleuser MYSQL_PASSWORD: examplepass MYSQL_RANDOM_ROOT_PASSWORD: '1' volumes: - db:/var/lib/mysql volumes: wordpress: db: `, testutil.WordpressImage, hostPort, testutil.MariaDBImage) composePath := data.Temp().Save(composeYAML, "compose.yaml") projectName := filepath.Base(filepath.Dir(composePath)) t.Logf("projectName=%q", projectName) wordpressContainerName := serviceparser.DefaultContainerName(projectName, "wordpress", "1") dbContainerName := serviceparser.DefaultContainerName(projectName, "db", "1") data.Labels().Set("hostPort", strconv.Itoa(hostPort)) data.Labels().Set("composeYAML", composePath) data.Labels().Set("projectName", projectName) data.Labels().Set("wordpressContainerName", wordpressContainerName) data.Labels().Set("dbContainerName", dbContainerName) helpers.Ensure("compose", "-f", composePath, "up", "-d") nerdtest.EnsureContainerStarted(helpers, wordpressContainerName) nerdtest.EnsureContainerStarted(helpers, dbContainerName) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "ps") } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All( expect.Contains(data.Labels().Get("wordpressContainerName")), expect.Contains(data.Labels().Get("dbContainerName")), ), } } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if data.Labels().Get("composeYAML") != "" { helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") } if p := data.Labels().Get("hostPort"); p != "" { if port, err := strconv.Atoi(p); err == nil { _ = portlock.Release(port) } } if projectName := data.Labels().Get("projectName"); projectName != "" { helpers.Command("volume", "inspect", fmt.Sprintf("%s_db", projectName)).Run(&test.Expected{ExitCode: 1}) helpers.Command("network", "inspect", fmt.Sprintf("%s_default", projectName)).Run(&test.Expected{ExitCode: 1}) } } testCase.Run(t) } func TestComposeUpBuild(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = nerdtest.Build testCase.Setup = func(data test.Data, helpers test.Helpers) { hostPort, err := portlock.Acquire(0) if err != nil { helpers.T().Log(fmt.Sprintf("Failed to acquire port: %v", err)) helpers.T().FailNow() } composeYAML := fmt.Sprintf(` services: web: build: . ports: - %d:80 `, hostPort) dockerfile := fmt.Sprintf(`FROM %s COPY index.html /usr/share/nginx/html/index.html `, testutil.NginxAlpineImage) indexHTML := data.Identifier("indexHTML") composePath := data.Temp().Save(composeYAML, "compose.yaml") data.Temp().Save(dockerfile, "Dockerfile") data.Temp().Save(indexHTML, "index.html") projectName := filepath.Base(filepath.Dir(composePath)) t.Logf("projectName=%q", projectName) data.Labels().Set("hostPort", strconv.Itoa(hostPort)) data.Labels().Set("composeYAML", composePath) data.Labels().Set("indexHTML", data.Temp().Path("index.html")) helpers.Ensure("compose", "-f", composePath, "up", "-d", "--build") nerdtest.EnsureContainerStarted(helpers, serviceparser.DefaultContainerName(projectName, "web", "1")) } testCase.SubTests = []*test.Case{ { Description: "HTTP request to the web container", Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { host := fmt.Sprintf("http://127.0.0.1:%s", data.Labels().Get("hostPort")) resp, err := nettestutil.HTTPGet(host, 5, false) assert.NilError(t, err) respBody, err := io.ReadAll(resp.Body) assert.NilError(t, err) t.Log(fmt.Sprintf("respBody=%q", respBody)) assert.Assert(t, strings.Contains(string(respBody), data.Labels().Get("indexHTML"))) }, } }, }, } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if data.Labels().Get("composeYAML") != "" { helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") } helpers.Anyhow("builder", "prune", "--all", "--force") if portStr := data.Labels().Get("hostPort"); portStr != "" { port, _ := strconv.Atoi(portStr) _ = portlock.Release(port) } } testCase.Run(t) } func TestComposeUpNetWithStaticIP(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.All( require.Not(nerdtest.Rootless), ) testCase.Setup = func(data test.Data, helpers test.Helpers) { staticIP := "10.4.255.254" subnet := "10.4.255.0/24" var composeYAML = fmt.Sprintf(` services: svc0: image: %s networks: net0: ipv4_address: %s networks: net0: ipam: config: - subnet: %s `, testutil.NginxAlpineImage, staticIP, subnet) composePath := data.Temp().Save(composeYAML, "compose.yaml") projectName := filepath.Base(filepath.Dir(composePath)) t.Logf("projectName=%q", projectName) containerName := serviceparser.DefaultContainerName(projectName, "svc0", "1") data.Labels().Set("staticIP", staticIP) data.Labels().Set("composeYAML", composePath) data.Labels().Set("containerName", containerName) helpers.Ensure("compose", "-f", composePath, "up", "-d") nerdtest.EnsureContainerStarted(helpers, containerName) } testCase.SubTests = []*test.Case{ { Description: "static IP is assigned to container", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("inspect", data.Labels().Get("containerName"), "--format", "\"{{range .NetworkSettings.Networks}} {{.IPAddress}}{{end}}\"") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(stdout, data.Labels().Get("staticIP"))) }, } }, }, } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if data.Labels().Get("composeYAML") != "" { helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") } } testCase.Run(t) } func TestComposeUpMultiNet(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { var composeYAML = fmt.Sprintf(` services: svc0: image: %s networks: - net0 - net1 - net2 svc1: image: %s networks: - net0 - net1 svc2: image: %s networks: - net2 networks: net0: {} net1: {} net2: {} `, testutil.NginxAlpineImage, testutil.NginxAlpineImage, testutil.NginxAlpineImage) composePath := data.Temp().Save(composeYAML, "compose.yaml") projectName := filepath.Base(filepath.Dir(composePath)) t.Logf("projectName=%q", projectName) svc0 := serviceparser.DefaultContainerName(projectName, "svc0", "1") svc1 := serviceparser.DefaultContainerName(projectName, "svc1", "1") svc2 := serviceparser.DefaultContainerName(projectName, "svc2", "1") data.Labels().Set("composeYAML", composePath) data.Labels().Set("svc0", svc0) data.Labels().Set("svc1", svc1) data.Labels().Set("svc2", svc2) helpers.Ensure("compose", "-f", composePath, "up", "-d") nerdtest.EnsureContainerStarted(helpers, svc0) nerdtest.EnsureContainerStarted(helpers, svc1) nerdtest.EnsureContainerStarted(helpers, svc2) } testCase.SubTests = []*test.Case{ { Description: "svc0 can ping itself", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Labels().Get("svc0"), "ping", "-c", "1", "svc0") }, Expected: test.Expects(0, nil, nil), }, { Description: "svc0 can ping svc1", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Labels().Get("svc0"), "ping", "-c", "1", "svc1") }, Expected: test.Expects(0, nil, nil), }, { Description: "svc0 can ping svc2", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Labels().Get("svc0"), "ping", "-c", "1", "svc2") }, Expected: test.Expects(0, nil, nil), }, { Description: "svc1 can ping svc0", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Labels().Get("svc1"), "ping", "-c", "1", "svc0") }, Expected: test.Expects(0, nil, nil), }, { Description: "svc2 can ping svc0", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Labels().Get("svc2"), "ping", "-c", "1", "svc0") }, Expected: test.Expects(0, nil, nil), }, { Description: "svc1 cannot ping svc2", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Labels().Get("svc1"), "ping", "-c", "1", "svc2") }, Expected: test.Expects(1, nil, nil), }, } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if data.Labels().Get("composeYAML") != "" { helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") } } testCase.Run(t) } func TestComposeUpOsEnvVar(t *testing.T) { testCase := nerdtest.Setup() testCase.Env = map[string]string{ "ADDRESS": "0.0.0.0", } testCase.Setup = func(data test.Data, helpers test.Helpers) { const containerName = "nginxAlpine" hostPort, err := portlock.Acquire(0) if err != nil { helpers.T().Log(fmt.Sprintf("Failed to acquire port: %v", err)) helpers.T().FailNow() } var composeYAML = fmt.Sprintf(` services: svc1: image: %s container_name: %s ports: - ${ADDRESS:-127.0.0.1}:%d:80 `, testutil.NginxAlpineImage, containerName, hostPort) composePath := data.Temp().Save(composeYAML, "compose.yaml") projectName := filepath.Base(filepath.Dir(composePath)) t.Logf("projectName=%q", projectName) data.Labels().Set("containerName", containerName) data.Labels().Set("hostPort", strconv.Itoa(hostPort)) data.Labels().Set("composeYAML", composePath) helpers.Ensure("compose", "-f", composePath, "up", "-d") nerdtest.EnsureContainerStarted(helpers, containerName) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("container", "inspect", data.Labels().Get("containerName")) } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.JSON([]dockercompat.Container{}, func(dc []dockercompat.Container, t tig.T) { assert.Equal(t, 1, len(dc), "unexpected number of containers") inspect80TCP := (*dc[0].NetworkSettings.Ports)["80/tcp"] assert.Assert(t, len(inspect80TCP) > 0, "no host bindings for 80/tcp") expected := nat.PortBinding{ HostIP: "0.0.0.0", HostPort: data.Labels().Get("hostPort"), } assert.Equal(t, expected, inspect80TCP[0]) }), } } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if data.Labels().Get("composeYAML") != "" { helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") } } testCase.Run(t) } func TestComposeUpDotEnvFile(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { var composeYAML = ` services: svc3: image: ghcr.io/stargz-containers/nginx:$TAG ` composePath := data.Temp().Save(composeYAML, "compose.yaml") data.Temp().Save(`TAG=1.19-alpine-org`, ".env") projectName := filepath.Base(filepath.Dir(composePath)) t.Logf("projectName=%q", projectName) data.Labels().Set("composeYAML", composePath) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "up", "-d") } testCase.Expected = test.Expects(0, nil, nil) testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") } testCase.Run(t) } func TestComposeUpEnvFileNotFoundError(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { var composeYAML = ` services: svc4: image: ghcr.io/stargz-containers/nginx:$TAG ` composePath := data.Temp().Save(composeYAML, "compose.yaml") data.Temp().Save(`TAG=1.19-alpine-org`, "envFile") projectName := filepath.Base(filepath.Dir(composePath)) t.Logf("projectName=%q", projectName) data.Labels().Set("composeYAML", composePath) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { // env-file is relative to the current working directory and not the project directory return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "--env-file", "envFile", "up", "-d") } testCase.Expected = test.Expects(1, nil, nil) testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") } testCase.Run(t) } func TestComposeUpWithScale(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { var composeYAML = fmt.Sprintf(` services: test: image: %s command: "sleep infinity" `, testutil.CommonImage) composePath := data.Temp().Save(composeYAML, "compose.yaml") projectName := filepath.Base(filepath.Dir(composePath)) t.Logf("projectName=%q", projectName) test1 := serviceparser.DefaultContainerName(projectName, "test", "1") test2 := serviceparser.DefaultContainerName(projectName, "test", "2") data.Labels().Set("composeYAML", composePath) data.Labels().Set("test1", test1) data.Labels().Set("test2", test2) helpers.Ensure("compose", "-f", composePath, "up", "-d", "--scale", "test=2") nerdtest.EnsureContainerStarted(helpers, test1) nerdtest.EnsureContainerStarted(helpers, test2) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "ps") } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All( expect.Contains(data.Labels().Get("test1")), expect.Contains(data.Labels().Get("test2")), ), } } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if data.Labels().Get("composeYAML") != "" { helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") } } testCase.Run(t) } func TestComposeIPAMConfig(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { var composeYAML = fmt.Sprintf(` services: foo: image: %s command: "sleep infinity" networks: default: ipam: config: - subnet: 10.1.100.0/24 `, testutil.CommonImage) composePath := data.Temp().Save(composeYAML, "compose.yaml") projectName := filepath.Base(filepath.Dir(composePath)) t.Logf("projectName=%q", projectName) fooContainer := serviceparser.DefaultContainerName(projectName, "foo", "1") data.Labels().Set("composeYAML", composePath) data.Labels().Set("fooContainer", fooContainer) helpers.Ensure("compose", "-f", composePath, "up", "-d") nerdtest.EnsureContainerStarted(helpers, fooContainer) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("inspect", "-f", "{{json .NetworkSettings.Networks }}", data.Labels().Get("fooContainer")) } testCase.Expected = test.Expects(0, nil, expect.Contains("10.1.100.")) testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if data.Labels().Get("composeYAML") != "" { helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") } } testCase.Run(t) } func TestComposeUpRemoveOrphans(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { var ( dockerComposeYAMLOrphan = fmt.Sprintf(` services: test: image: %s command: "sleep infinity" `, testutil.CommonImage) dockerComposeYAMLFull = fmt.Sprintf(` %s orphan: image: %s command: "sleep infinity" `, dockerComposeYAMLOrphan, testutil.CommonImage) ) composeOrphanPath := data.Temp().Save(dockerComposeYAMLOrphan, "compose-orphan.yaml") composeFullPath := data.Temp().Save(dockerComposeYAMLFull, "compose-full.yaml") projectName := data.Identifier("project") t.Logf("projectName=%q", projectName) testContainer := serviceparser.DefaultContainerName(projectName, "test", "1") orphanContainer := serviceparser.DefaultContainerName(projectName, "orphan", "1") data.Labels().Set("composeOrphanPath", composeOrphanPath) data.Labels().Set("composeFullPath", composeFullPath) data.Labels().Set("projectName", projectName) data.Labels().Set("orphanContainer", orphanContainer) helpers.Ensure("compose", "-p", projectName, "-f", composeFullPath, "up", "-d") helpers.Ensure("compose", "-p", projectName, "-f", composeOrphanPath, "up", "-d") nerdtest.EnsureContainerStarted(helpers, testContainer) nerdtest.EnsureContainerStarted(helpers, orphanContainer) helpers.Command("compose", "-p", projectName, "-f", composeFullPath, "ps").Run( &test.Expected{ ExitCode: 0, Output: expect.Contains(orphanContainer), }, ) helpers.Ensure("compose", "-p", projectName, "-f", composeOrphanPath, "up", "-d", "--remove-orphans") } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-p", data.Labels().Get("projectName"), "-f", data.Labels().Get("composeFullPath"), "ps") } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.DoesNotContain(data.Labels().Get("orphanContainer")), } } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if data.Labels().Get("composeOrphanPath") != "" { helpers.Anyhow("compose", "-p", data.Labels().Get("projectName"), "-f", data.Labels().Get("composeOrphanPath"), "down", "-v") } if data.Labels().Get("composeFullPath") != "" { helpers.Anyhow("compose", "-p", data.Labels().Get("projectName"), "-f", data.Labels().Get("composeFullPath"), "down", "-v") } } testCase.Run(t) } func TestComposeUpIdempotent(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { composeYAML := fmt.Sprintf(` services: test: image: %s command: "sleep infinity" `, testutil.CommonImage) composePath := data.Temp().Save(composeYAML, "compose.yaml") projectName := filepath.Base(filepath.Dir(composePath)) t.Logf("projectName=%q", projectName) data.Labels().Set("composeYAML", composePath) helpers.Ensure("compose", "-f", composePath, "up", "-d") nerdtest.EnsureContainerStarted(helpers, serviceparser.DefaultContainerName(projectName, "test", "1")) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "up", "-d") } testCase.Expected = test.Expects(0, nil, nil) testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if data.Labels().Get("composeYAML") != "" { helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") } } testCase.Run(t) } func TestComposeUpNoRecreateDependencies(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { var composeYAML = fmt.Sprintf(` services: foo: image: %s command: "sleep infinity" bar: image: %s command: "sleep infinity" depends_on: - foo `, testutil.CommonImage, testutil.CommonImage) composePath := data.Temp().Save(composeYAML, "compose.yaml") projectName := filepath.Base(filepath.Dir(composePath)) t.Logf("projectName=%q", projectName) fooContainer := serviceparser.DefaultContainerName(projectName, "foo", "1") barContainer := serviceparser.DefaultContainerName(projectName, "bar", "1") data.Labels().Set("composeYAML", composePath) data.Labels().Set("projectName", projectName) data.Labels().Set("fooContainer", fooContainer) data.Labels().Set("barContainer", barContainer) } testCase.SubTests = []*test.Case{ { Description: "foo is not recreated when starting bar", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("compose", "-f", data.Labels().Get("composeYAML"), "up", "-d", "foo") nerdtest.EnsureContainerStarted(helpers, data.Labels().Get("fooContainer")) helpers.Command("inspect", data.Labels().Get("fooContainer"), "--format", "{{.Id}}").Run( &test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { data.Labels().Set("fooContainerID", strings.TrimSpace(stdout)) }, }, ) // Bring up dependent service; ensure foo is not recreated (ID unchanged) helpers.Ensure("compose", "-f", data.Labels().Get("composeYAML"), "up", "-d", "bar") nerdtest.EnsureContainerStarted(helpers, data.Labels().Get("barContainer")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("inspect", data.Labels().Get("fooContainer"), "--format", "{{.Id}}") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { assert.Equal(t, strings.TrimSpace(stdout), data.Labels().Get("fooContainerID")) }, } }, }, } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if data.Labels().Get("composeYAML") != "" { helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") } } testCase.Run(t) } func TestComposeUpWithExternalNetwork(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { var dockerComposeYaml1 = fmt.Sprintf(` services: %s: image: %s container_name: %s networks: %s: aliases: - nginx-1 networks: %s: external: true `, data.Identifier("con-1"), testutil.NginxAlpineImage, data.Identifier("con-1"), data.Identifier("network"), data.Identifier("network")) var dockerComposeYaml2 = fmt.Sprintf(` services: %s: image: %s container_name: %s networks: %s: aliases: - nginx-2 networks: %s: external: true `, data.Identifier("con-2"), testutil.NginxAlpineImage, data.Identifier("con-2"), data.Identifier("network"), data.Identifier("network")) tmp := data.Temp() tmp.Save(dockerComposeYaml1, "project-1", "compose.yaml") tmp.Save(dockerComposeYaml2, "project-2", "compose.yaml") helpers.Ensure("network", "create", data.Identifier("network")) helpers.Ensure("compose", "-f", tmp.Path("project-1", "compose.yaml"), "up", "-d") helpers.Ensure("compose", "-f", tmp.Path("project-2", "compose.yaml"), "up", "-d") helpers.Ensure("compose", "-f", tmp.Path("project-2", "compose.yaml"), "down", "-v") helpers.Ensure("compose", "-f", tmp.Path("project-2", "compose.yaml"), "up", "-d") nerdtest.EnsureContainerStarted(helpers, data.Identifier("con-2")) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { helpers.Ensure("exec", data.Identifier("con-1"), "cat", "/etc/hosts") return helpers.Command("exec", data.Identifier("con-1"), "wget", "-qO-", "http://"+data.Identifier("con-2")) } testCase.Expected = test.Expects(0, nil, expect.Contains(testutil.NginxAlpineIndexHTMLSnippet)) testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Temp().Path("project-1", "compose.yaml"), "down", "-v") helpers.Anyhow("compose", "-f", data.Temp().Path("project-2", "compose.yaml"), "down", "-v") helpers.Anyhow("network", "rm", data.Identifier("network")) } testCase.Run(t) } func TestComposeUpWithBypass4netns(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.All( require.Not(nerdtest.Docker), nerdtest.Rootless, ) testCase.Setup = func(data test.Data, helpers test.Helpers) { testutil.RequireKernelVersion(t, ">= 5.9.0-0") testutil.RequireSystemService(t, "bypass4netnsd") hostPort, err := portlock.Acquire(0) if err != nil { helpers.T().Log(fmt.Sprintf("Failed to acquire port: %v", err)) helpers.T().FailNow() } composeYAML := fmt.Sprintf(` services: wordpress: image: %s restart: always ports: - %d:80 environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser WORDPRESS_DB_PASSWORD: examplepass WORDPRESS_DB_NAME: exampledb volumes: - wordpress:/var/www/html annotations: - nerdctl/bypass4netns=1 db: image: %s restart: always environment: MYSQL_DATABASE: exampledb MYSQL_USER: exampleuser MYSQL_PASSWORD: examplepass MYSQL_RANDOM_ROOT_PASSWORD: '1' volumes: - db:/var/lib/mysql annotations: - nerdctl/bypass4netns=1 volumes: wordpress: db: `, testutil.WordpressImage, hostPort, testutil.MariaDBImage) composePath := data.Temp().Save(composeYAML, "compose.yaml") projectName := filepath.Base(filepath.Dir(composePath)) t.Logf("projectName=%q", projectName) data.Labels().Set("hostPort", strconv.Itoa(hostPort)) data.Labels().Set("composeYAML", composePath) data.Labels().Set("projectName", projectName) helpers.Ensure("compose", "-f", composePath, "up", "-d") nerdtest.EnsureContainerStarted(helpers, serviceparser.DefaultContainerName(projectName, "wordpress", "1")) nerdtest.EnsureContainerStarted(helpers, serviceparser.DefaultContainerName(projectName, "db", "1")) helpers.Command("volume", "inspect", fmt.Sprintf("%s_db", projectName)).Run(&test.Expected{ExitCode: 0}) helpers.Command("network", "inspect", fmt.Sprintf("%s_default", projectName)).Run(&test.Expected{ExitCode: 0}) } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(_ string, tt tig.T) { host := fmt.Sprintf("http://127.0.0.1:%s", data.Labels().Get("hostPort")) resp, err := nettestutil.HTTPGet(host, 5, false) assert.NilError(tt, err) body, err := io.ReadAll(resp.Body) assert.NilError(tt, err) _ = resp.Body.Close() assert.Assert(tt, strings.Contains(string(body), testutil.WordpressIndexHTMLSnippet)) t.Log("wordpress seems functional") }, } } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if data.Labels().Get("composeYAML") != "" { helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") } if p := data.Labels().Get("hostPort"); p != "" { if port, err := strconv.Atoi(p); err == nil { _ = portlock.Release(port) } } if projectName := data.Labels().Get("projectName"); projectName != "" { helpers.Command("volume", "inspect", fmt.Sprintf("%s_db", projectName)).Run(&test.Expected{ExitCode: 1}) helpers.Command("network", "inspect", fmt.Sprintf("%s_default", projectName)).Run(&test.Expected{ExitCode: 1}) } } testCase.Run(t) } func TestComposeUpProfile(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { serviceRegular := data.Identifier("regular") serviceProfiled := data.Identifier("profiled") envFilePath := data.Temp().Save(`TEST_ENV_INJECTION=WORKS\n`, "env.common") composeYAML := fmt.Sprintf(` services: %s: image: %[3]s %[2]s: image: %[3]s profiles: - test-profile env_file: - %[4]s `, serviceRegular, serviceProfiled, testutil.NginxAlpineImage, envFilePath) composePath := data.Temp().Save(composeYAML, "compose.yaml") projectName := filepath.Base(filepath.Dir(composePath)) t.Logf("projectName=%q", projectName) data.Labels().Set("serviceRegular", serviceRegular) data.Labels().Set("serviceProfiled", serviceProfiled) data.Labels().Set("composeYAML", composePath) data.Labels().Set("regularContainer", serviceparser.DefaultContainerName(projectName, serviceRegular, "1")) data.Labels().Set("profiledContainer", serviceparser.DefaultContainerName(projectName, serviceProfiled, "1")) } testCase.SubTests = []*test.Case{ { Description: "with profile", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("compose", "-f", data.Labels().Get("composeYAML"), "--profile", "test-profile", "up", "-d") nerdtest.EnsureContainerStarted(helpers, data.Labels().Get("regularContainer")) nerdtest.EnsureContainerStarted(helpers, data.Labels().Get("profiledContainer")) helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "exec", data.Labels().Get("serviceProfiled"), "env"). Run(&test.Expected{ ExitCode: 0, Output: expect.Contains("TEST_ENV_INJECTION=WORKS"), }) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("ps", "-a", "--format={{.Names}}") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All( expect.Contains(data.Labels().Get("serviceRegular")), expect.Contains(data.Labels().Get("serviceProfiled")), ), } }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "--profile", "test-profile", "down", "-v") }, }, { Description: "profiled not started without profile flag", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("compose", "-f", data.Labels().Get("composeYAML"), "up", "-d") nerdtest.EnsureContainerStarted(helpers, data.Labels().Get("regularContainer")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("ps", "-a", "--format={{.Names}}") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All( expect.Contains(data.Labels().Get("serviceRegular")), expect.DoesNotContain(data.Labels().Get("serviceProfiled")), ), } }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") }, }, } testCase.Run(t) } func TestComposeUpAbortOnContainerExit(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { serviceRegular := data.Identifier("regular") serviceProfiled := data.Identifier("exited") composeYAML := fmt.Sprintf(` services: %s: image: %s %s: image: %s entrypoint: /bin/sh -c "exit 1" `, serviceRegular, testutil.NginxAlpineImage, serviceProfiled, testutil.BusyboxImage) composePath := data.Temp().Save(composeYAML, "compose.yaml") projectName := filepath.Base(filepath.Dir(composePath)) t.Logf("projectName=%q", projectName) data.Labels().Set("serviceRegular", serviceRegular) data.Labels().Set("serviceProfiled", serviceProfiled) data.Labels().Set("composeYAML", composePath) data.Labels().Set("regularContainer", serviceparser.DefaultContainerName(projectName, serviceRegular, "1")) } testCase.SubTests = []*test.Case{ { Description: "abort on container exit", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "up", "--abort-on-container-exit").Run( &test.Expected{ ExitCode: 1, }, ) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("ps", "-a", "--format={{.Names}}", "--filter", "status=exited") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All( expect.Contains(data.Labels().Get("serviceRegular")), expect.Contains(data.Labels().Get("serviceProfiled")), ), } }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") }, }, { Description: "no abort flag keeps other services running", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("compose", "-f", data.Labels().Get("composeYAML"), "up", "-d") nerdtest.EnsureContainerStarted(helpers, data.Labels().Get("regularContainer")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("ps", "-a", "--format={{.Names}}", "--filter", "status=exited") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All( expect.DoesNotContain(data.Labels().Get("serviceRegular")), expect.Contains(data.Labels().Get("serviceProfiled")), ), } }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") }, }, // in this sub-test we are ensuring that flags '-d' and '--abort-on-container-exit' cannot be ran together { Description: "flag -d incompatible with --abort-on-container-exit", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "up", "-d", "--abort-on-container-exit") }, Expected: test.Expects(1, nil, nil), }, } testCase.Run(t) } func TestComposeUpPull(t *testing.T) { testCase := nerdtest.Setup() testCase.NoParallel = true testCase.Require = nerdtest.Private testCase.Setup = func(data test.Data, helpers test.Helpers) { composeYAML := fmt.Sprintf(` services: test: image: %s command: sh -euxc "echo hi" `, testutil.CommonImage) composePath := data.Temp().Save(composeYAML, "compose.yaml") data.Labels().Set("composeYAML", composePath) } testCase.SubTests = []*test.Case{ { Description: "pull=missing", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("rmi", "-f", testutil.CommonImage) helpers.Command("images").Run( &test.Expected{ ExitCode: 0, Output: expect.DoesNotContain(testutil.CommonImage), }, ) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "up", "--pull", "missing") }, Expected: test.Expects(0, nil, expect.Contains("hi")), Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down") }, }, { Description: "pull=always", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("rmi", "-f", testutil.CommonImage) helpers.Command("images").Run( &test.Expected{ ExitCode: 0, Output: expect.DoesNotContain(testutil.CommonImage), }, ) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "up", "--pull", "always") }, Expected: test.Expects(0, nil, expect.Contains("hi")), Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down") }, }, { Description: "pull=never, no pull", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("rmi", "-f", testutil.CommonImage) helpers.Command("images").Run( &test.Expected{ ExitCode: 0, Output: expect.DoesNotContain(testutil.CommonImage), }, ) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "up", "--pull", "never") }, Expected: test.Expects(1, nil, nil), Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down") }, }, } testCase.Run(t) } func TestComposeUpServicePullPolicy(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = nerdtest.Private testCase.Setup = func(data test.Data, helpers test.Helpers) { var composeYAML = fmt.Sprintf(` services: test: image: %s command: sh -euxc "echo hi" pull_policy: "never" `, testutil.CommonImage) composePath := data.Temp().Save(composeYAML, "compose.yaml") data.Labels().Set("composeYAML", composePath) helpers.Ensure("rmi", "-f", testutil.CommonImage) helpers.Command("images").Run( &test.Expected{ ExitCode: 0, Output: expect.DoesNotContain(testutil.CommonImage), }, ) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "up") } testCase.Expected = test.Expects(1, nil, nil) testCase.Run(t) } func TestComposeTmpfsVolume(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { containerName := data.Identifier("tmpfs") composeYAML := fmt.Sprintf(` services: tmpfs: container_name: %s image: %s command: sleep infinity volumes: - type: tmpfs target: /target-rw tmpfs: size: 64m - type: tmpfs target: /target-ro read_only: true tmpfs: size: 64m mode: 0o1770 `, containerName, testutil.CommonImage) composeYAMLPath := data.Temp().Save(composeYAML, "compose.yaml") helpers.Ensure("compose", "-f", composeYAMLPath, "up", "-d") nerdtest.EnsureContainerStarted(helpers, containerName) data.Labels().Set("composeYAML", composeYAMLPath) data.Labels().Set("containerName", containerName) } testCase.SubTests = []*test.Case{ { Description: "rw tmpfs mount", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Labels().Get("containerName"), "grep", "/target-rw", "/proc/mounts") }, Expected: test.Expects(0, nil, expect.All( expect.Contains("/target-rw"), expect.Contains("rw"), expect.Contains("size=65536k"), ), ), }, { Description: "ro tmpfs mount with mode", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Labels().Get("containerName"), "grep", "/target-ro", "/proc/mounts") }, Expected: test.Expects(0, nil, expect.All( expect.Contains("/target-ro"), expect.Contains("ro"), expect.Contains("size=65536k"), expect.Contains("mode=1770"), ), ), }, } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down") } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/compose/compose_up_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "errors" "fmt" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) // https://github.com/containerd/nerdctl/issues/1942 func TestComposeUpDetailedError(t *testing.T) { dockerComposeYAML := fmt.Sprintf(` services: foo: image: %s runtime: invalid `, testutil.CommonImage) testCase := nerdtest.Setup() // "FIXME: test does not work on Windows yet (runtime \"io.containerd.runc.v2\" binary not installed \"containerd-shim-runc-v2.exe\": file does not exist) testCase.Require = require.Not(require.Windows) testCase.Setup = func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerComposeYAML, "compose.yaml") } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Temp().Path("compose.yaml"), "up", "-d") } testCase.Expected = test.Expects( 1, []error{errors.New(`invalid runtime name`)}, nil, ) testCase.Run(t) } // https://github.com/containerd/nerdctl/issues/1652 func TestComposeUpBindCreateHostPath(t *testing.T) { testCase := nerdtest.Setup() // `FIXME: no support for Windows path: (error: "volume target must be an absolute path, got \"/mnt\")` testCase.Require = require.Not(require.Windows) testCase.Setup = func(data test.Data, helpers test.Helpers) { var dockerComposeYAML = fmt.Sprintf(` services: test: image: %s command: sh -euxc "echo hi >/mnt/test" volumes: # tempdir/foo should be automatically created - %s:/mnt `, testutil.CommonImage, data.Temp().Path("foo")) data.Temp().Save(dockerComposeYAML, "compose.yaml") } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Temp().Path("compose.yaml"), "up") } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down") } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Errors: nil, Output: func(stdout string, t tig.T) { assert.Equal(t, data.Temp().Load("foo", "test"), "hi\n") }, } } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/compose/compose_version.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "fmt" "strings" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/pkg/version" ) func versionCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "version", Short: "Show the Compose version information", Args: cobra.NoArgs, RunE: versionAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().StringP("format", "f", "pretty", "Format the output. Values: [pretty | json]") cmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"json", "pretty"}, cobra.ShellCompDirectiveNoFileComp }) cmd.Flags().Bool("short", false, "Shows only Compose's version number") return cmd } func versionAction(cmd *cobra.Command, args []string) error { short, err := cmd.Flags().GetBool("short") if err != nil { return err } if short { fmt.Fprintln(cmd.OutOrStdout(), strings.TrimPrefix(version.GetVersion(), "v")) return nil } format, err := cmd.Flags().GetString("format") if err != nil { return err } switch format { case "pretty": fmt.Fprintln(cmd.OutOrStdout(), "nerdctl Compose version "+version.GetVersion()) case "json": fmt.Fprintf(cmd.OutOrStdout(), "{\"version\":\"%v\"}\n", version.Version) default: return fmt.Errorf("format can be either pretty or json, not %v", format) } return nil } ================================================ FILE: cmd/nerdctl/compose/compose_version_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package compose import ( "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeVersion(t *testing.T) { testCase := nerdtest.Setup() testCase.Command = test.Command("compose", "version") testCase.Expected = test.Expects(0, nil, expect.Contains("Compose version ")) testCase.Run(t) } func TestComposeVersionShort(t *testing.T) { testCase := nerdtest.Setup() testCase.Command = test.Command("compose", "version", "--short") testCase.Expected = test.Expects(0, nil, nil) testCase.Run(t) } func TestComposeVersionJson(t *testing.T) { testCase := nerdtest.Setup() testCase.Command = test.Command("compose", "version", "--format", "json") testCase.Expected = test.Expects(0, nil, expect.Contains("{\"version\":\"")) testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" ) func Command() *cobra.Command { cmd := &cobra.Command{ Annotations: map[string]string{helpers.Category: helpers.Management}, Use: "container", Short: "Manage containers", RunE: helpers.UnknownSubcommandAction, SilenceUsage: true, SilenceErrors: true, } cmd.AddCommand( CreateCommand(), RunCommand(), UpdateCommand(), ExecCommand(), listCommand(), inspectCommand(), LogsCommand(), PortCommand(), RemoveCommand(), StopCommand(), StartCommand(), RestartCommand(), KillCommand(), PauseCommand(), DiffCommand(), WaitCommand(), UnpauseCommand(), CommitCommand(), RenameCommand(), pruneCommand(), StatsCommand(), AttachCommand(), HealthCheckCommand(), ExportCommand(), ) AddCpCommand(cmd) return cmd } func listCommand() *cobra.Command { x := PsCommand() x.Use = "ls" x.Aliases = []string{"list"} return x } ================================================ FILE: cmd/nerdctl/container/container_attach.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "io" "github.com/spf13/cobra" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/container" "github.com/containerd/nerdctl/v2/pkg/consoleutil" ) func AttachCommand() *cobra.Command { const shortHelp = "Attach stdin, stdout, and stderr to a running container." const longHelp = `Attach stdin, stdout, and stderr to a running container. For example: 1. 'nerdctl run -it --name test busybox' to start a container with a pty 2. 'ctrl-p ctrl-q' to detach from the container 3. 'nerdctl attach test' to attach to the container Caveats: - Currently only one attach session is allowed. When the second session tries to attach, currently no error will be returned from nerdctl. However, since behind the scenes, there's only one FIFO for stdin, stdout, and stderr respectively, if there are multiple sessions, all the sessions will be reading from and writing to the same 3 FIFOs, which will result in mixed input and partial output. - Until dual logging (issue #1946) is implemented, a container that is spun up by either 'nerdctl run -d' or 'nerdctl start' (without '--attach') cannot be attached to.` var cmd = &cobra.Command{ Use: "attach [flags] CONTAINER", Args: cobra.ExactArgs(1), Short: shortHelp, Long: longHelp, RunE: attachAction, ValidArgsFunction: attachShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().String("detach-keys", consoleutil.DefaultDetachKeys, "Override the default detach keys") cmd.Flags().Bool("no-stdin", false, "Do not attach STDIN") return cmd } func attachOptions(cmd *cobra.Command) (types.ContainerAttachOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ContainerAttachOptions{}, err } detachKeys, err := cmd.Flags().GetString("detach-keys") if err != nil { return types.ContainerAttachOptions{}, err } noStdin, err := cmd.Flags().GetBool("no-stdin") if err != nil { return types.ContainerAttachOptions{}, err } var stdin io.Reader if !noStdin { stdin = cmd.InOrStdin() } return types.ContainerAttachOptions{ GOptions: globalOptions, Stdin: stdin, Stdout: cmd.OutOrStdout(), Stderr: cmd.ErrOrStderr(), DetachKeys: detachKeys, }, nil } func attachAction(cmd *cobra.Command, args []string) error { options, err := attachOptions(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return container.Attach(ctx, client, args[0], options) } func attachShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { statusFilterFn := func(st containerd.ProcessStatus) bool { return st == containerd.Running } return completion.ContainerNames(cmd, statusFilterFn) } ================================================ FILE: cmd/nerdctl/container/container_attach_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "bytes" "errors" "io" "strings" "testing" "time" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) /* Important notes: - for both docker and nerdctl, you can run+detach of a container and exit 0, while the container would actually fail starting - nerdctl (not docker): on run, detach will race anything on stdin before the detach sequence from reaching the container - nerdctl AND docker: on attach ^ - exit code variants: https://github.com/containerd/nerdctl/issues/3571 */ func TestAttach(t *testing.T) { // In nerdctl the detach return code from the container after attach is 0, but in docker the return code is 1. // This behaviour is reported in https://github.com/containerd/nerdctl/issues/3571 ex := 0 if nerdtest.IsDocker() { ex = 1 } testCase := nerdtest.Setup() testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Setup = func(data test.Data, helpers test.Helpers) { cmd := helpers.Command("run", "--rm", "-it", "--name", data.Identifier(), testutil.CommonImage) cmd.WithPseudoTTY() // ctrl+p and ctrl+q (see https://en.wikipedia.org/wiki/C0_and_C1_control_codes) cmd.Feed(bytes.NewReader([]byte{16, 17})) cmd.Run(&test.Expected{ ExitCode: 0, Errors: []error{errors.New("read detach keys")}, Output: func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true")) }, }) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { // Run interactively and detach cmd := helpers.Command("attach", data.Identifier()) cmd.WithPseudoTTY() cmd.Feed(strings.NewReader("echo mark${NON}mark\n")) cmd.WithFeeder(func() io.Reader { // Interestingly, and unlike with run, on attach, docker (like nerdctl) ALSO needs a pause so that the // container can read stdin before we detach time.Sleep(time.Second) // ctrl+p and ctrl+q (see https://en.wikipedia.org/wiki/C0_and_C1_control_codes) return bytes.NewReader([]byte{16, 17}) }) return cmd } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: ex, Errors: []error{errors.New("read detach keys")}, Output: expect.All( expect.Contains("markmark"), func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true")) }, ), } } testCase.Run(t) } func TestAttachDetachKeys(t *testing.T) { // In nerdctl the detach return code from the container after attach is 0, but in docker the return code is 1. // This behaviour is reported in https://github.com/containerd/nerdctl/issues/3571 ex := 0 if nerdtest.IsDocker() { ex = 1 } testCase := nerdtest.Setup() testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Setup = func(data test.Data, helpers test.Helpers) { cmd := helpers.Command("run", "--rm", "-it", "--detach-keys=ctrl-q", "--name", data.Identifier(), testutil.CommonImage) cmd.WithPseudoTTY() cmd.Feed(bytes.NewReader([]byte{17})) cmd.Run(&test.Expected{ ExitCode: 0, Errors: []error{errors.New("read detach keys")}, Output: func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true")) }, }) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { // Run interactively and detach cmd := helpers.Command("attach", "--detach-keys=ctrl-a,ctrl-b", data.Identifier()) cmd.WithPseudoTTY() cmd.Feed(strings.NewReader("echo mark${NON}mark\n")) cmd.WithFeeder(func() io.Reader { // Interestingly, and unlike with run, on attach, docker (like nerdctl) ALSO needs a pause so that the // container can read stdin before we detach time.Sleep(time.Second) // ctrl+p and ctrl+q (see https://en.wikipedia.org/wiki/C0_and_C1_control_codes) return bytes.NewReader([]byte{1, 2}) }) return cmd } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: ex, Errors: []error{errors.New("read detach keys")}, Output: expect.All( expect.Contains("markmark"), func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true")) }, ), } } testCase.Run(t) } // TestIssue3568 tests https://github.com/containerd/nerdctl/issues/3568 func TestAttachForAutoRemovedContainer(t *testing.T) { testCase := nerdtest.Setup() testCase.Description = "Issue #3568 - A container should be deleted when detaching and attaching a container started with the --rm option." testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Setup = func(data test.Data, helpers test.Helpers) { cmd := helpers.Command("run", "--rm", "-it", "--detach-keys=ctrl-a,ctrl-b", "--name", data.Identifier(), testutil.CommonImage) cmd.WithPseudoTTY() // ctrl+a and ctrl+b (see https://en.wikipedia.org/wiki/C0_and_C1_control_codes) cmd.Feed(bytes.NewReader([]byte{1, 2})) cmd.Run(&test.Expected{ ExitCode: 0, Errors: []error{errors.New("read detach keys")}, Output: func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true")) }, }) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { // Run interactively and detach cmd := helpers.Command("attach", data.Identifier()) cmd.WithPseudoTTY() cmd.Feed(strings.NewReader("echo mark${NON}mark\nexit 42\n")) return cmd } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 42, Output: expect.All( expect.Contains("markmark"), func(stdout string, t tig.T) { assert.Assert(t, !strings.Contains(helpers.Capture("ps", "-a"), data.Identifier())) }, ), } } testCase.Run(t) } func TestAttachNoStdin(t *testing.T) { testCase := nerdtest.Setup() testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Setup = func(data test.Data, helpers test.Helpers) { cmd := helpers.Command("run", "-it", "--detach-keys=ctrl-p,ctrl-q", "--name", data.Identifier(), testutil.CommonImage, "sleep", "5") cmd.WithPseudoTTY() cmd.Feed(bytes.NewReader([]byte{16, 17})) // Ctrl-p, Ctrl-q to detach (https://en.wikipedia.org/wiki/C0_and_C1_control_codes) cmd.Run(&test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "{{.State.Running}}", data.Identifier()), "true")) }, }) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("attach", "--no-stdin", data.Identifier()) cmd.WithPseudoTTY() cmd.Feed(strings.NewReader("should-not-appear\n")) cmd.Feed(bytes.NewReader([]byte{16, 17})) return cmd } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, // Since it's a normal exit and not detach. Output: func(stdout string, t tig.T) { logs := helpers.Capture("logs", data.Identifier()) assert.Assert(t, !strings.Contains(logs, "should-not-appear")) }, } } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_commit.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "errors" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/container" ) func CommitCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "commit [flags] CONTAINER REPOSITORY[:TAG]", Short: "Create a new image from a container's changes", Args: helpers.IsExactArgs(2), RunE: commitAction, ValidArgsFunction: commitShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().StringP("author", "a", "", `Author (e.g., "nerdctl contributor ")`) cmd.Flags().StringP("message", "m", "", "Commit message") cmd.Flags().StringArrayP("change", "c", nil, "Apply Dockerfile instruction to the created image (supported directives: [CMD, ENTRYPOINT])") cmd.Flags().BoolP("pause", "p", true, "Pause container during commit") cmd.Flags().StringP("compression", "", "gzip", "commit compression algorithm (zstd or gzip)") cmd.Flags().String("format", "docker", "Format of the committed image (docker or oci)") cmd.Flags().Bool("estargz", false, "Convert the committed layer to eStargz for lazy pulling") cmd.Flags().Int("estargz-compression-level", 9, "eStargz compression level (1-9)") cmd.Flags().Int("estargz-chunk-size", 0, "eStargz chunk size") cmd.Flags().Int("estargz-min-chunk-size", 0, "The minimal number of bytes of data must be written in one gzip stream") cmd.Flags().Bool("zstdchunked", false, "Convert the committed layer to zstd:chunked for lazy pulling") cmd.Flags().Int("zstdchunked-compression-level", 3, "zstd:chunked compression level") cmd.Flags().Int("zstdchunked-chunk-size", 0, "zstd:chunked chunk size") return cmd } func commitOptions(cmd *cobra.Command) (types.ContainerCommitOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ContainerCommitOptions{}, err } author, err := cmd.Flags().GetString("author") if err != nil { return types.ContainerCommitOptions{}, err } message, err := cmd.Flags().GetString("message") if err != nil { return types.ContainerCommitOptions{}, err } pause, err := cmd.Flags().GetBool("pause") if err != nil { return types.ContainerCommitOptions{}, err } change, err := cmd.Flags().GetStringArray("change") if err != nil { return types.ContainerCommitOptions{}, err } com, err := cmd.Flags().GetString("compression") if err != nil { return types.ContainerCommitOptions{}, err } if com != string(types.Zstd) && com != string(types.Gzip) { return types.ContainerCommitOptions{}, errors.New("--compression param only supports zstd or gzip") } format, err := cmd.Flags().GetString("format") if err != nil { return types.ContainerCommitOptions{}, err } if format != string(types.ImageFormatDocker) && format != string(types.ImageFormatOCI) { return types.ContainerCommitOptions{}, errors.New("--format param only supports docker or oci") } estargz, err := cmd.Flags().GetBool("estargz") if err != nil { return types.ContainerCommitOptions{}, err } estargzCompressionLevel, err := cmd.Flags().GetInt("estargz-compression-level") if err != nil { return types.ContainerCommitOptions{}, err } estargzChunkSize, err := cmd.Flags().GetInt("estargz-chunk-size") if err != nil { return types.ContainerCommitOptions{}, err } estargzMinChunkSize, err := cmd.Flags().GetInt("estargz-min-chunk-size") if err != nil { return types.ContainerCommitOptions{}, err } zstdchunked, err := cmd.Flags().GetBool("zstdchunked") if err != nil { return types.ContainerCommitOptions{}, err } zstdchunkedCompressionLevel, err := cmd.Flags().GetInt("zstdchunked-compression-level") if err != nil { return types.ContainerCommitOptions{}, err } zstdchunkedChunkSize, err := cmd.Flags().GetInt("zstdchunked-chunk-size") if err != nil { return types.ContainerCommitOptions{}, err } // estargz and zstdchunked are mutually exclusive if estargz && zstdchunked { return types.ContainerCommitOptions{}, errors.New("options --estargz and --zstdchunked lead to conflict, only one of them can be used") } return types.ContainerCommitOptions{ Stdout: cmd.OutOrStdout(), GOptions: globalOptions, Author: author, Message: message, Pause: pause, Change: change, Compression: types.CompressionType(com), Format: types.ImageFormat(format), EstargzOptions: types.EstargzOptions{ Estargz: estargz, EstargzCompressionLevel: estargzCompressionLevel, EstargzChunkSize: estargzChunkSize, EstargzMinChunkSize: estargzMinChunkSize, }, ZstdChunkedOptions: types.ZstdChunkedOptions{ ZstdChunked: zstdchunked, ZstdChunkedCompressionLevel: zstdchunkedCompressionLevel, ZstdChunkedChunkSize: zstdchunkedChunkSize, }, }, nil } func commitAction(cmd *cobra.Command, args []string) error { options, err := commitOptions(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return container.Commit(ctx, client, args[1], args[0], options) } func commitShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) == 0 { return completion.ContainerNames(cmd, nil) } return nil, cobra.ShellCompDirectiveNoFileComp } ================================================ FILE: cmd/nerdctl/container/container_commit_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "strings" "testing" "time" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestKubeCommitSave(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = nerdtest.OnlyKubernetes testCase.Setup = func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() containerID := "" // NOTE: kubectl namespaces are not the same as containerd namespaces. // We still want kube test objects segregated in their own Kube API namespace. nerdtest.KubeCtlCommand(helpers, "create", "namespace", "nerdctl-test-k8s").Run(&test.Expected{}) nerdtest.KubeCtlCommand(helpers, "run", "--image", testutil.CommonImage, identifier, "--", "sleep", nerdtest.Infinity).Run(&test.Expected{}) nerdtest.KubeCtlCommand(helpers, "wait", "pod", identifier, "--for=condition=ready", "--timeout=1m").Run(&test.Expected{}) nerdtest.KubeCtlCommand(helpers, "exec", identifier, "--", "mkdir", "-p", "/tmp/whatever").Run(&test.Expected{}) nerdtest.KubeCtlCommand(helpers, "get", "pods", identifier, "-o", "jsonpath={ .status.containerStatuses[0].containerID }").Run(&test.Expected{ Output: func(stdout string, t tig.T) { containerID = strings.TrimPrefix(stdout, "containerd://") }, }) data.Labels().Set("containerID", containerID) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { nerdtest.KubeCtlCommand(helpers, "delete", "pod", "--all").Run(nil) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { helpers.Ensure("commit", data.Labels().Get("containerID"), data.Identifier("testcommitsave")) // Wait for the image to show up for range 5 { found := false cmd := helpers.Command("images", data.Identifier("testcommitsave"), "--format", "json") cmd.Run(&test.Expected{ Output: func(stdout string, t tig.T) { found = strings.TrimSpace(stdout) != "" }, }) if found { break } time.Sleep(1 * time.Second) } return helpers.Command("save", data.Identifier("testcommitsave")) } testCase.Expected = test.Expects(0, nil, nil) testCase.Run(t) // This below is missing configuration to allow for plain http communication // This is left here for future work to successfully start a registry usable in the cluster /* // Start a registry nerdtest.KubeCtlCommand(helpers, "run", "--port", "5000", "--image", testutil.RegistryImageStable, "testregistry"). Run(&test.Expected{}) nerdtest.KubeCtlCommand(helpers, "wait", "pod", "testregistry", "--for=condition=ready", "--timeout=1m"). AssertOK() cmd = nerdtest.KubeCtlCommand(helpers, "get", "pods", tID, "-o", "jsonpath={ .status.hostIPs[0].ip }") cmd.Run(&test.Expected{ Output: func(stdout string, t tig.T) { registryIP = stdout }, }) cmd = nerdtest.KubeCtlCommand(helpers, "apply", "-f", "-", fmt.Sprintf(`apiVersion: v1 kind: ConfigMap metadata: name: local-registry namespace: nerdctl-test data: localRegistryHosting.v1: | host: "%s:5000" help: "https://kind.sigs.k8s.io/docs/user/local-registry/" `, registryIP)) */ } ================================================ FILE: cmd/nerdctl/container/container_commit_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestCommit(t *testing.T) { testCase := nerdtest.Setup() testCase.SubTests = []*test.Case{ { Description: "with pause", Require: nerdtest.CGroup, Cleanup: func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() helpers.Anyhow("rm", "-f", identifier) helpers.Anyhow("rmi", "-f", identifier) }, Setup: func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() helpers.Ensure("run", "-d", "--name", identifier, testutil.CommonImage, "sleep", nerdtest.Infinity) helpers.Ensure("exec", identifier, "sh", "-euxc", `echo hello-test-commit > /foo`) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { identifier := data.Identifier() helpers.Ensure( "commit", "-c", `CMD ["/foo"]`, "-c", `ENTRYPOINT ["cat"]`, "--pause=true", identifier, identifier) return helpers.Command("run", "--rm", identifier) }, Expected: test.Expects(0, nil, expect.Equals("hello-test-commit\n")), }, { Description: "no pause", Require: require.Not(require.Windows), Cleanup: func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() helpers.Anyhow("rm", "-f", identifier) helpers.Anyhow("rmi", "-f", identifier) }, Setup: func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() helpers.Ensure("run", "-d", "--name", identifier, testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, identifier) helpers.Ensure("exec", identifier, "sh", "-euxc", `echo hello-test-commit > /foo`) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { identifier := data.Identifier() helpers.Ensure( "commit", "-c", `CMD ["/foo"]`, "-c", `ENTRYPOINT ["cat"]`, "--pause=false", identifier, identifier) return helpers.Command("run", "--rm", identifier) }, Expected: test.Expects(0, nil, expect.Equals("hello-test-commit\n")), }, } testCase.Run(t) } func TestZstdCommit(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.All( // FIXME: Docker does not support compression require.Not(nerdtest.Docker), nerdtest.ContainerdVersion("2.0.0"), nerdtest.CGroup, ) testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("rmi", "-f", data.Identifier("image")) } testCase.Setup = func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() helpers.Ensure("run", "-d", "--name", identifier, testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, identifier) helpers.Ensure("exec", identifier, "sh", "-euxc", `echo hello-test-commit > /foo`) helpers.Ensure("commit", identifier, data.Identifier("image"), "--compression=zstd") data.Labels().Set("image", data.Identifier("image")) } testCase.SubTests = []*test.Case{ { Description: "verify zstd has been used", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("image", "inspect", "--mode=native", data.Labels().Get("image")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.JSON([]native.Image{}, func(images []native.Image, t tig.T) { assert.Equal(t, len(images), 1) assert.Equal(helpers.T(), images[0].Manifest.Layers[len(images[0].Manifest.Layers)-1].MediaType, "application/vnd.docker.image.rootfs.diff.tar.zstd") }), } }, }, { Description: "verify the image is working", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Labels().Get("image"), "sh", "-c", "--", "cat /foo") }, Expected: test.Expects(0, nil, expect.Equals("hello-test-commit\n")), }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_cp_acid_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "errors" "fmt" "os" "path/filepath" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) // This is a separate set of tests for cp specifically meant to test corner or extreme cases that do not fit in the normal testing rig // because of their complexity func TestCopyAcid(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { testID := data.Identifier() tempDir := t.TempDir() sourceFile := filepath.Join(tempDir, "hostfile") sourceFileContent := []byte(testID) roContainer := testID + "-ro" rwContainer := testID + "-rw" data.Labels().Set("sourceFile", sourceFile) data.Labels().Set("sourceFileContent", string(sourceFileContent)) data.Labels().Set("roContainer", roContainer) data.Labels().Set("rwContainer", rwContainer) helpers.Ensure("volume", "create", testID+"-1-ro") helpers.Ensure("volume", "create", testID+"-2-ro") helpers.Ensure("volume", "create", testID+"-3-ro") helpers.Ensure("run", "-d", "-w", containerCwd, "--name", roContainer, "--read-only", "-v", fmt.Sprintf("%s:%s:ro", testID+"-1-ro", "/vol1/dir1/ro"), "-v", fmt.Sprintf("%s:%s", testID+"-2-rw", "/vol2/dir2/rw"), testutil.CommonImage, "sleep", nerdtest.Infinity, ) nerdtest.EnsureContainerStarted(helpers, roContainer) helpers.Ensure("run", "-d", "-w", containerCwd, "--name", rwContainer, "-v", fmt.Sprintf("%s:%s:ro", testID+"-1-ro", "/vol1/dir1/ro"), "-v", fmt.Sprintf("%s:%s", testID+"-3-rw", "/vol3/dir3/rw"), testutil.CommonImage, "sleep", nerdtest.Infinity, ) nerdtest.EnsureContainerStarted(helpers, rwContainer) helpers.Ensure("exec", rwContainer, "sh", "-euxc", "cd /vol3/dir3/rw; ln -s ../../../ relativelinktoroot") helpers.Ensure("exec", rwContainer, "sh", "-euxc", "cd /vol3/dir3/rw; ln -s / absolutelinktoroot") helpers.Ensure("exec", roContainer, "sh", "-euxc", "cd /vol2/dir2/rw; ln -s ../../../ relativelinktoroot") helpers.Ensure("exec", roContainer, "sh", "-euxc", "cd /vol2/dir2/rw; ln -s / absolutelinktoroot") // Create file on the host err := os.WriteFile(sourceFile, sourceFileContent, filePerm) assert.NilError(t, err) expectedErr := containerutil.ErrTargetIsReadOnly.Error() if nerdtest.IsDocker() { expectedErr = "" } data.Labels().Set("expectedErr", expectedErr) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { testID := data.Identifier() helpers.Anyhow("rm", "-f", testID+"-ro") helpers.Anyhow("rm", "-f", testID+"-rw") helpers.Anyhow("volume", "rm", testID+"-1-ro") helpers.Anyhow("volume", "rm", testID+"-2-rw") helpers.Anyhow("volume", "rm", testID+"-3-rw") } testCase.SubTests = []*test.Case{ { Description: "Cannot copy into a read-only root", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("cp", data.Labels().Get("sourceFile"), data.Labels().Get("roContainer")+":/") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New(data.Labels().Get("expectedErr"))}, } }, }, { Description: "Cannot copy into a read-only mount, in a rw container", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("cp", data.Labels().Get("sourceFile"), data.Labels().Get("rwContainer")+":/vol1/dir1/ro") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New(data.Labels().Get("expectedErr"))}, } }, }, { Description: "Can copy into a read-write mount in a read-only container", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("cp", data.Labels().Get("sourceFile"), data.Labels().Get("roContainer")+":/vol2/dir2/rw") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, } }, }, { Description: "Traverse read-only locations to a read-write location", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("cp", data.Labels().Get("sourceFile"), data.Labels().Get("roContainer")+":/vol1/dir1/ro/../../../vol2/dir2/rw") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, } }, }, { Description: "Follow an absolute symlink inside a read-write mount to a read-only root", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("cp", data.Labels().Get("sourceFile"), data.Labels().Get("roContainer")+":/vol2/dir2/rw/absolutelinktoroot") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New(data.Labels().Get("expectedErr"))}, } }, }, { Description: "Follow am absolute symlink inside a read-write mount to a read-only mount", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("cp", data.Labels().Get("sourceFile"), data.Labels().Get("rwContainer")+":/vol3/dir3/rw/absolutelinktoroot/vol1/dir1/ro") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New(data.Labels().Get("expectedErr"))}, } }, }, { Description: "Follow a relative symlink inside a read-write location to a read-only root", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("cp", data.Labels().Get("sourceFile"), data.Labels().Get("roContainer")+":/vol2/dir2/rw/relativelinktoroot") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New(data.Labels().Get("expectedErr"))}, } }, }, { Description: "Follow a relative symlink inside a read-write location to a read-only mount", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("cp", data.Labels().Get("sourceFile"), data.Labels().Get("rwContainer")+":/vol3/dir3/rw/relativelinktoroot/vol1/dir1/ro") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New(data.Labels().Get("expectedErr"))}, } }, }, { Description: "Cannot copy into a HOST read-only location", Require: nerdtest.Rootless, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { tempDir := t.TempDir() err := os.MkdirAll(filepath.Join(tempDir, "rotest"), 0o000) assert.NilError(t, err) return helpers.Command("cp", data.Labels().Get("roContainer")+":/etc/issue", filepath.Join(tempDir, "rotest")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New(data.Labels().Get("expectedErr"))}, } }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_cp_linux.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "errors" "fmt" "path/filepath" "strings" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/container" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" ) func copyCommand() *cobra.Command { shortHelp := "Copy files/folders between a running container and the local filesystem." longHelp := shortHelp + ` This command requires 'tar' to be installed on the host (not in the container). Using GNU tar is recommended. The path of the 'tar' binary can be specified with an environment variable '$TAR'. WARNING: 'nerdctl cp' is designed only for use with trusted, cooperating containers. Using 'nerdctl cp' with untrusted or malicious containers is unsupported and may not provide protection against unexpected behavior. ` usage := `cp [flags] CONTAINER:SRC_PATH DEST_PATH|- nerdctl cp [flags] SRC_PATH|- CONTAINER:DEST_PATH` var cmd = &cobra.Command{ Use: usage, Args: helpers.IsExactArgs(2), Short: shortHelp, Long: longHelp, RunE: copyAction, ValidArgsFunction: copyShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().BoolP("follow-link", "L", false, "Always follow symbolic link in SRC_PATH.") return cmd } func copyAction(cmd *cobra.Command, args []string) error { options, err := copyOptions(cmd, args) if err != nil { return err } if rootlessutil.IsRootless() { options.GOptions.Address, err = rootlessutil.RootlessContainredSockAddress() if err != nil { return err } } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return container.Cp(ctx, client, options) } func copyOptions(cmd *cobra.Command, args []string) (types.ContainerCpOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ContainerCpOptions{}, err } flagL, err := cmd.Flags().GetBool("follow-link") if err != nil { return types.ContainerCpOptions{}, err } srcSpec, err := parseCpFileSpec(args[0]) if err != nil { return types.ContainerCpOptions{}, err } destSpec, err := parseCpFileSpec(args[1]) if err != nil { return types.ContainerCpOptions{}, err } if (srcSpec.Container != nil && destSpec.Container != nil) || (len(srcSpec.Path) == 0 && len(destSpec.Path) == 0) { return types.ContainerCpOptions{}, fmt.Errorf("one of src or dest must be a local file specification") } if srcSpec.Container == nil && destSpec.Container == nil { return types.ContainerCpOptions{}, fmt.Errorf("one of src or dest must be a container file specification") } container2host := srcSpec.Container != nil var containerReq string if container2host { containerReq = *srcSpec.Container } else { containerReq = *destSpec.Container } return types.ContainerCpOptions{ GOptions: globalOptions, Container2Host: container2host, ContainerReq: containerReq, DestPath: destSpec.Path, SrcPath: srcSpec.Path, FollowSymLink: flagL, FromStdin: srcSpec.Path == "-", ToStdout: destSpec.Path == "-", }, nil } func AddCpCommand(rootCmd *cobra.Command) { rootCmd.AddCommand(copyCommand()) } var errFileSpecDoesntMatchFormat = errors.New("filespec must match the canonical format: [container:]file/path") func parseCpFileSpec(arg string) (*copyFileSpec, error) { if arg == "" { return ©FileSpec{ Path: "-", }, nil } i := strings.Index(arg, ":") // filespec starting with a semicolon is invalid if i == 0 { return nil, errFileSpecDoesntMatchFormat } if filepath.IsAbs(arg) { // Explicit local absolute path, e.g., `C:\foo` or `/foo`. return ©FileSpec{ Container: nil, Path: arg, }, nil } parts := strings.SplitN(arg, ":", 2) if len(parts) == 1 || strings.HasPrefix(parts[0], ".") { // Either there's no `:` in the arg // OR it's an explicit local relative path like `./file:name.txt`. return ©FileSpec{ Path: arg, }, nil } return ©FileSpec{ Container: &parts[0], Path: parts[1], }, nil } type copyFileSpec struct { Container *string Path string } func copyShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return nil, cobra.ShellCompDirectiveFilterFileExt } ================================================ FILE: cmd/nerdctl/container/container_cp_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "fmt" "os" "os/exec" "path/filepath" "strings" "syscall" "testing" "gotest.tools/v3/assert" "gotest.tools/v3/icmd" "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/tarutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) // For the test matrix, see https://docs.docker.com/engine/reference/commandline/cp/ // Obviously, none of this is fully windows ready - obviously `nerdctl cp` itself is not either, so, ok for now. const ( // Use this to poke the testing rig for improper path handling // TODO: fuzz this more seriously // FIXME: the following will break the test (anything that will evaluate on the shell, obviously): // - ` // - $a, ${a}, etc complexify = "" // = "-~a0-_.(){}[]*#! \"'∞" pathDoesNotExistRelative = "does-not-exist" + complexify pathDoesNotExistAbsolute = string(os.PathSeparator) + "does-not-exist" + complexify pathIsAFileRelative = "is-a-file" + complexify pathIsAFileAbsolute = string(os.PathSeparator) + "is-a-file" + complexify pathIsADirRelative = "is-a-dir" + complexify pathIsADirAbsolute = string(os.PathSeparator) + "is-a-dir" + complexify pathIsAVolumeMount = string(os.PathSeparator) + "is-a-volume-mount" + complexify srcFileName = "test-file" + complexify tarballName = "test-tar" + complexify cpFolderName = "nerdctl-cp-test" // Since nerdctl cp must NOT obey container wd, but instead resolve paths against the root, we set this // explicitly to ensure we do the right thing wrt that. containerCwd = "/nerdctl/cp/test" dirPerm = 0o755 filePerm = 0o644 ) var srcDirName = filepath.Join("three-levels-src-dir", "test-dir", "dir"+complexify) type testgroup struct { description string // parent test description toContainer bool // copying to, or from container // sourceSpec as specified by the user (without the container: part) - can be relative or absolute - // if sourceSpec points to a file, you must use srcFileName for filename sourceSpec string sourceIsAFile bool // whether the provided sourceSpec points to a file or a dir testCases []testcases // testcases } type testcases struct { description string // textual description of what the test is doing destinationSpec string // destination path as specified by the user (without the container: part) - can be relative or absolute expect icmd.Expected // expectation // Optional catFile string // path that we "cat" - defaults to destinationSpec if not specified setup func(base *testutil.Base, container string, destPath string) // additional test setup if needed tearDown func() // additional cleanup if needed volume func(base *testutil.Base, id string) (string, string, bool) // volume creation function if needed (should return the volume name, mountPoint, readonly flag) } func TestCopyToContainer(t *testing.T) { t.Parallel() testGroups := []*testgroup{ { description: "Copying to container, SRC_PATH is a file, absolute", sourceSpec: filepath.Join(string(os.PathSeparator), srcDirName, srcFileName), sourceIsAFile: true, toContainer: true, testCases: []testcases{ { description: "DEST_PATH does not exist, relative", destinationSpec: pathDoesNotExistRelative, expect: icmd.Expected{ ExitCode: 0, }, }, { description: "DEST_PATH does not exist, absolute", destinationSpec: pathDoesNotExistAbsolute, expect: icmd.Expected{ ExitCode: 0, }, }, { description: "DEST_PATH does not exist, relative, and ends with " + string(os.PathSeparator), destinationSpec: pathDoesNotExistRelative + string(os.PathSeparator), expect: icmd.Expected{ ExitCode: 1, Err: containerutil.ErrDestinationDirMustExist.Error(), }, }, { description: "DEST_PATH does not exist, absolute, and ends with " + string(os.PathSeparator), destinationSpec: pathDoesNotExistAbsolute + string(os.PathSeparator), expect: icmd.Expected{ ExitCode: 1, Err: containerutil.ErrDestinationDirMustExist.Error(), }, }, { description: "DEST_PATH is a file, relative", destinationSpec: pathIsAFileRelative, expect: icmd.Expected{ ExitCode: 0, }, setup: func(base *testutil.Base, container string, destPath string) { base.Cmd("exec", container, "touch", destPath).AssertOK() }, }, { description: "DEST_PATH is a file, absolute", destinationSpec: pathIsAFileAbsolute, expect: icmd.Expected{ ExitCode: 0, }, setup: func(base *testutil.Base, container string, destPath string) { base.Cmd("exec", container, "touch", destPath).AssertOK() }, }, { description: "DEST_PATH is a file, relative, ends with improper " + string(os.PathSeparator), destinationSpec: pathIsAFileRelative + string(os.PathSeparator), expect: icmd.Expected{ ExitCode: 1, Err: containerutil.ErrDestinationIsNotADir.Error(), }, setup: func(base *testutil.Base, container string, destPath string) { base.Cmd("exec", container, "touch", destPath).AssertOK() }, }, { description: "DEST_PATH is a file, absolute, ends with improper " + string(os.PathSeparator), destinationSpec: pathIsAFileAbsolute + string(os.PathSeparator), expect: icmd.Expected{ ExitCode: 1, // FIXME: it is unclear why the code path with absolute (this test) versus relative (just above) // yields a different error. Both should ideally be ErrCannotCopyDirToFile // This is probably happening somewhere in resolve. // This is not a deal killer, as both DO error with a reasonable explanation, but a bit // frustrating Err: containerutil.ErrDestinationIsNotADir.Error(), }, setup: func(base *testutil.Base, container string, destPath string) { base.Cmd("exec", container, "touch", destPath).AssertOK() }, }, { description: "DEST_PATH is a directory, relative", destinationSpec: pathIsADirRelative, catFile: filepath.Join(pathIsADirRelative, srcFileName), expect: icmd.Expected{ ExitCode: 0, }, setup: func(base *testutil.Base, container string, destPath string) { base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() }, }, { description: "DEST_PATH is a directory, absolute", destinationSpec: pathIsADirAbsolute, catFile: filepath.Join(pathIsADirAbsolute, srcFileName), expect: icmd.Expected{ ExitCode: 0, }, setup: func(base *testutil.Base, container string, destPath string) { base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() }, }, { description: "DEST_PATH is a directory, relative, ends with " + string(os.PathSeparator), destinationSpec: pathIsADirRelative + string(os.PathSeparator), catFile: filepath.Join(pathIsADirRelative, srcFileName), expect: icmd.Expected{ ExitCode: 0, }, setup: func(base *testutil.Base, container string, destPath string) { base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() }, }, { description: "DEST_PATH is a directory, absolute, ends with " + string(os.PathSeparator), destinationSpec: pathIsADirAbsolute + string(os.PathSeparator), catFile: filepath.Join(pathIsADirAbsolute, srcFileName), expect: icmd.Expected{ ExitCode: 0, }, setup: func(base *testutil.Base, container string, destPath string) { base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() }, }, { description: "DEST_PATH is a volume mount-point", destinationSpec: pathIsAVolumeMount, catFile: filepath.Join(pathIsAVolumeMount, srcFileName), expect: icmd.Expected{ ExitCode: 0, }, // FIXME the way we handle volume is not right - too complicated for the test author volume: func(base *testutil.Base, id string) (string, string, bool) { base.Cmd("volume", "create", id).Run() return id, pathIsAVolumeMount, false }, }, { description: "DEST_PATH is a read-only volume mount-point", destinationSpec: pathIsAVolumeMount, expect: icmd.Expected{ ExitCode: 1, Err: containerutil.ErrTargetIsReadOnly.Error(), }, volume: func(base *testutil.Base, id string) (string, string, bool) { base.Cmd("volume", "create", id).Run() return id, pathIsAVolumeMount, true }, }, }, }, { description: "Copying to container, SRC_PATH is a directory", sourceSpec: srcDirName, toContainer: true, testCases: []testcases{ { description: "DEST_PATH does not exist, relative", destinationSpec: pathDoesNotExistRelative, catFile: filepath.Join(pathDoesNotExistRelative, srcFileName), expect: icmd.Expected{ ExitCode: 0, }, }, { description: "DEST_PATH does not exist, absolute", destinationSpec: pathDoesNotExistAbsolute, catFile: filepath.Join(pathDoesNotExistAbsolute, srcFileName), expect: icmd.Expected{ ExitCode: 0, }, }, { description: "DEST_PATH does not exist, relative, and ends with " + string(os.PathSeparator), destinationSpec: pathDoesNotExistRelative + string(os.PathSeparator), catFile: filepath.Join(pathDoesNotExistRelative, srcFileName), expect: icmd.Expected{ ExitCode: 0, }, }, { description: "DEST_PATH does not exist, absolute, and ends with " + string(os.PathSeparator), destinationSpec: pathDoesNotExistAbsolute + string(os.PathSeparator), catFile: filepath.Join(pathDoesNotExistAbsolute, srcFileName), expect: icmd.Expected{ ExitCode: 0, }, }, { description: "DEST_PATH is a file, relative", destinationSpec: pathIsAFileRelative, expect: icmd.Expected{ ExitCode: 1, Err: containerutil.ErrCannotCopyDirToFile.Error(), }, setup: func(base *testutil.Base, container string, destPath string) { base.Cmd("exec", container, "touch", destPath).AssertOK() }, }, { description: "DEST_PATH is a file, absolute", destinationSpec: pathIsAFileAbsolute, expect: icmd.Expected{ ExitCode: 1, Err: containerutil.ErrCannotCopyDirToFile.Error(), }, setup: func(base *testutil.Base, container string, destPath string) { base.Cmd("exec", container, "touch", destPath).AssertOK() }, }, { description: "DEST_PATH is a file, relative, ends with improper " + string(os.PathSeparator), destinationSpec: pathIsAFileRelative + string(os.PathSeparator), expect: icmd.Expected{ ExitCode: 1, Err: containerutil.ErrDestinationIsNotADir.Error(), }, setup: func(base *testutil.Base, container string, destPath string) { base.Cmd("exec", container, "touch", destPath).AssertOK() }, }, { description: "DEST_PATH is a file, absolute, ends with improper " + string(os.PathSeparator), destinationSpec: pathIsAFileAbsolute + string(os.PathSeparator), expect: icmd.Expected{ ExitCode: 1, // FIXME: it is unclear why the code path with absolute (this test) versus relative (just above) // yields a different error. Both should ideally be ErrCannotCopyDirToFile // This is probably happening somewhere in resolve. // This is not a deal killer, as both DO error with a reasonable explanation, but a bit // frustrating Err: containerutil.ErrDestinationIsNotADir.Error(), }, setup: func(base *testutil.Base, container string, destPath string) { base.Cmd("exec", container, "touch", destPath).AssertOK() }, }, { description: "DEST_PATH is a directory, relative", destinationSpec: pathIsADirRelative, catFile: filepath.Join(pathIsADirRelative, filepath.Base(srcDirName), srcFileName), expect: icmd.Expected{ ExitCode: 0, }, setup: func(base *testutil.Base, container string, destPath string) { base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() }, }, { description: "DEST_PATH is a directory, absolute", destinationSpec: pathIsADirAbsolute, catFile: filepath.Join(pathIsADirAbsolute, filepath.Base(srcDirName), srcFileName), expect: icmd.Expected{ ExitCode: 0, }, setup: func(base *testutil.Base, container string, destPath string) { base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() }, }, { description: "DEST_PATH is a directory, relative, ends with " + string(os.PathSeparator), destinationSpec: pathIsADirRelative + string(os.PathSeparator), catFile: filepath.Join(pathIsADirRelative, filepath.Base(srcDirName), srcFileName), expect: icmd.Expected{ ExitCode: 0, }, setup: func(base *testutil.Base, container string, destPath string) { base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() }, }, { description: "DEST_PATH is a directory, absolute, ends with " + string(os.PathSeparator), destinationSpec: pathIsADirAbsolute + string(os.PathSeparator), catFile: filepath.Join(pathIsADirAbsolute, filepath.Base(srcDirName), srcFileName), expect: icmd.Expected{ ExitCode: 0, }, setup: func(base *testutil.Base, container string, destPath string) { base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() }, }, }, }, { description: "Copying to container, SRC_PATH is a directory ending with /.", sourceSpec: srcDirName + string(os.PathSeparator) + ".", toContainer: true, testCases: []testcases{ { description: "DEST_PATH is a directory, relative", destinationSpec: pathIsADirRelative, catFile: filepath.Join(pathIsADirRelative, srcFileName), setup: func(base *testutil.Base, container string, destPath string) { base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() }, }, { description: "DEST_PATH is a directory, absolute", destinationSpec: pathIsADirAbsolute, catFile: filepath.Join(pathIsADirAbsolute, srcFileName), setup: func(base *testutil.Base, container string, destPath string) { base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() }, }, }, }, { description: "Copying to container, SRC_PATH is stdin", sourceSpec: "-", sourceIsAFile: true, toContainer: true, testCases: []testcases{ { description: "DEST_PATH is a directory, relative", destinationSpec: pathIsADirRelative, catFile: filepath.Join(pathIsADirRelative, srcFileName), setup: func(base *testutil.Base, container string, destPath string) { base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() }, }, { description: "DEST_PATH is a directory, absolute", destinationSpec: pathIsADirAbsolute, catFile: filepath.Join(pathIsADirAbsolute, srcFileName), setup: func(base *testutil.Base, container string, destPath string) { base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() }, }, { description: "DEST_PATH is stdout", destinationSpec: "-", expect: icmd.Expected{ ExitCode: 1, Err: "one of src or dest must be a container file specification", }, }, { description: "DEST_PATH is a file", destinationSpec: pathIsAFileAbsolute, setup: func(base *testutil.Base, container string, destPath string) { base.Cmd("exec", container, "touch", destPath).AssertOK() }, expect: icmd.Expected{ ExitCode: 1, Err: containerutil.ErrCannotCopyDirToFile.Error(), }, }, }, }, } for _, tg := range testGroups { cpTestHelper(t, tg) } } func TestCopyFromContainer(t *testing.T) { t.Parallel() testGroups := []*testgroup{ { description: "Copying from container, SRC_PATH specifies a file", sourceSpec: srcFileName, sourceIsAFile: true, testCases: []testcases{ { description: "DEST_PATH does not exist, relative", destinationSpec: pathDoesNotExistRelative, expect: icmd.Expected{ ExitCode: 0, }, }, { description: "DEST_PATH does not exist, absolute", destinationSpec: pathDoesNotExistAbsolute, expect: icmd.Expected{ ExitCode: 0, }, }, { description: "DEST_PATH does not exist, relative, and ends with a path separator", destinationSpec: pathDoesNotExistRelative + string(os.PathSeparator), expect: icmd.Expected{ ExitCode: 1, Err: containerutil.ErrDestinationDirMustExist.Error(), }, }, { description: "DEST_PATH does not exist, absolute, and ends with a path separator", destinationSpec: pathDoesNotExistAbsolute + string(os.PathSeparator), expect: icmd.Expected{ ExitCode: 1, Err: containerutil.ErrDestinationDirMustExist.Error(), }, }, { description: "DEST_PATH is a file, relative", destinationSpec: pathIsAFileRelative, expect: icmd.Expected{ ExitCode: 0, }, setup: func(base *testutil.Base, container string, destPath string) { err := os.WriteFile(destPath, []byte(""), filePerm) assert.NilError(t, err) }, }, { description: "DEST_PATH is a file, absolute", destinationSpec: pathIsAFileAbsolute, expect: icmd.Expected{ ExitCode: 0, }, setup: func(base *testutil.Base, container string, destPath string) { err := os.WriteFile(destPath, []byte(""), filePerm) assert.NilError(t, err) }, }, { description: "DEST_PATH is a file, relative, improperly ends with a separator", destinationSpec: pathIsAFileRelative + string(os.PathSeparator), expect: icmd.Expected{ ExitCode: 1, Err: containerutil.ErrDestinationIsNotADir.Error(), }, setup: func(base *testutil.Base, container string, destPath string) { err := os.WriteFile(destPath, []byte(""), filePerm) assert.NilError(t, err) }, }, { description: "DEST_PATH is a file, absolute, improperly ends with a separator", destinationSpec: pathIsAFileAbsolute + string(os.PathSeparator), expect: icmd.Expected{ ExitCode: 1, Err: containerutil.ErrDestinationIsNotADir.Error(), }, setup: func(base *testutil.Base, container string, destPath string) { err := os.WriteFile(destPath, []byte(""), filePerm) assert.NilError(t, err) }, }, { description: "DEST_PATH is a directory, relative", destinationSpec: pathIsADirRelative, catFile: filepath.Join(pathIsADirRelative, srcFileName), expect: icmd.Expected{ ExitCode: 0, }, setup: func(base *testutil.Base, container string, destPath string) { err := os.MkdirAll(destPath, dirPerm) assert.NilError(t, err) }, }, { description: "DEST_PATH is a directory, absolute", destinationSpec: pathIsADirAbsolute, catFile: filepath.Join(pathIsADirAbsolute, srcFileName), expect: icmd.Expected{ ExitCode: 0, }, setup: func(base *testutil.Base, container string, destPath string) { err := os.MkdirAll(destPath, dirPerm) assert.NilError(t, err) }, }, { description: "DEST_PATH is a directory, relative, ending with a path separator", destinationSpec: pathIsADirRelative + string(os.PathSeparator), catFile: filepath.Join(pathIsADirRelative, srcFileName), expect: icmd.Expected{ ExitCode: 0, }, setup: func(base *testutil.Base, container string, destPath string) { err := os.MkdirAll(destPath, dirPerm) assert.NilError(t, err) }, }, { description: "DEST_PATH is a directory, absolute, ending with a path separator", destinationSpec: pathIsADirAbsolute + string(os.PathSeparator), catFile: filepath.Join(pathIsADirAbsolute, srcFileName), expect: icmd.Expected{ ExitCode: 0, }, setup: func(base *testutil.Base, container string, destPath string) { err := os.MkdirAll(destPath, dirPerm) assert.NilError(t, err) }, }, { description: "DEST_PATH is stdout", destinationSpec: "-", // Extra dir to account for folder created from extracted tar file catFile: filepath.Join(pathIsADirAbsolute, filepath.Base(srcDirName), srcFileName), expect: icmd.Expected{ ExitCode: 0, }, setup: func(base *testutil.Base, container string, destPath string) { err := os.MkdirAll(destPath, dirPerm) assert.NilError(t, err) }, }, }, }, { description: "Copying from container, SRC_PATH specifies a dir", sourceSpec: srcDirName, testCases: []testcases{ { description: "DEST_PATH does not exist, relative", destinationSpec: pathDoesNotExistRelative, catFile: filepath.Join(pathDoesNotExistRelative, srcFileName), expect: icmd.Expected{ ExitCode: 0, }, }, { description: "DEST_PATH does not exist, absolute", destinationSpec: pathDoesNotExistAbsolute, catFile: filepath.Join(pathDoesNotExistAbsolute, srcFileName), expect: icmd.Expected{ ExitCode: 0, }, }, { description: "DEST_PATH does not exist, relative, ends with path separator", destinationSpec: pathDoesNotExistRelative + string(os.PathSeparator), catFile: filepath.Join(pathDoesNotExistRelative, srcFileName), expect: icmd.Expected{ ExitCode: 0, }, }, { description: "DEST_PATH does not exist, absolute, ends with path separator", destinationSpec: pathDoesNotExistAbsolute + string(os.PathSeparator), catFile: filepath.Join(pathDoesNotExistAbsolute, srcFileName), expect: icmd.Expected{ ExitCode: 0, }, }, { description: "DEST_PATH is a file, relative", destinationSpec: pathIsAFileRelative, expect: icmd.Expected{ ExitCode: 1, Err: containerutil.ErrCannotCopyDirToFile.Error(), }, setup: func(base *testutil.Base, container string, destPath string) { err := os.MkdirAll(filepath.Dir(destPath), dirPerm) assert.NilError(t, err) err = os.WriteFile(destPath, []byte(""), filePerm) assert.NilError(t, err) }, }, { description: "DEST_PATH is a file, absolute", destinationSpec: pathIsAFileAbsolute, expect: icmd.Expected{ ExitCode: 1, Err: containerutil.ErrCannotCopyDirToFile.Error(), }, setup: func(base *testutil.Base, container string, destPath string) { err := os.MkdirAll(filepath.Dir(destPath), dirPerm) assert.NilError(t, err) err = os.WriteFile(destPath, []byte(""), filePerm) assert.NilError(t, err) }, }, { description: "DEST_PATH is a file, relative, improperly ends with path separator", destinationSpec: pathIsAFileRelative + string(os.PathSeparator), expect: icmd.Expected{ ExitCode: 1, Err: containerutil.ErrDestinationIsNotADir.Error(), }, setup: func(base *testutil.Base, container string, destPath string) { err := os.MkdirAll(filepath.Dir(destPath), dirPerm) assert.NilError(t, err) err = os.WriteFile(destPath, []byte(""), filePerm) assert.NilError(t, err) }, }, { description: "DEST_PATH is a file, absolute, improperly ends with path separator", destinationSpec: pathIsAFileAbsolute + string(os.PathSeparator), expect: icmd.Expected{ ExitCode: 1, Err: containerutil.ErrDestinationIsNotADir.Error(), }, setup: func(base *testutil.Base, container string, destPath string) { err := os.MkdirAll(filepath.Dir(destPath), dirPerm) assert.NilError(t, err) err = os.WriteFile(destPath, []byte(""), filePerm) assert.NilError(t, err) }, }, { description: "DEST_PATH is a directory, relative", destinationSpec: pathIsADirRelative, catFile: filepath.Join(pathIsADirRelative, filepath.Base(srcDirName), srcFileName), expect: icmd.Expected{ ExitCode: 0, }, setup: func(base *testutil.Base, container string, destPath string) { err := os.MkdirAll(destPath, dirPerm) assert.NilError(t, err) }, }, { description: "DEST_PATH is a directory, absolute", destinationSpec: pathIsADirAbsolute, catFile: filepath.Join(pathIsADirAbsolute, filepath.Base(srcDirName), srcFileName), expect: icmd.Expected{ ExitCode: 0, }, setup: func(base *testutil.Base, container string, destPath string) { err := os.MkdirAll(destPath, dirPerm) assert.NilError(t, err) }, }, { description: "DEST_PATH is a directory, relative, ends with path separator", destinationSpec: pathIsADirRelative + string(os.PathSeparator), catFile: filepath.Join(pathIsADirRelative, filepath.Base(srcDirName), srcFileName), expect: icmd.Expected{ ExitCode: 0, }, setup: func(base *testutil.Base, container string, destPath string) { err := os.MkdirAll(destPath, dirPerm) assert.NilError(t, err) }, }, { description: "DEST_PATH is a directory, absolute, ends with path separator", destinationSpec: pathIsADirAbsolute + string(os.PathSeparator), catFile: filepath.Join(pathIsADirAbsolute, filepath.Base(srcDirName), srcFileName), expect: icmd.Expected{ ExitCode: 0, }, setup: func(base *testutil.Base, container string, destPath string) { err := os.MkdirAll(destPath, dirPerm) assert.NilError(t, err) }, }, { description: "DEST_PATH is stdout", destinationSpec: "-", catFile: filepath.Join(pathIsADirAbsolute, srcDirName, srcFileName), expect: icmd.Expected{ ExitCode: 0, }, setup: func(base *testutil.Base, container string, destPath string) { // Don't make the topmost dir as this is where the tarball must extract err := os.MkdirAll(filepath.Dir(destPath), dirPerm) assert.NilError(t, err) }, }, }, }, { description: "SRC_PATH is a dir, with a trailing slash/dot", sourceSpec: srcDirName + string(os.PathSeparator) + ".", testCases: []testcases{ { description: "DEST_PATH is a directory, relative", destinationSpec: pathIsADirRelative, catFile: filepath.Join(pathIsADirRelative, srcFileName), expect: icmd.Expected{ ExitCode: 0, }, setup: func(base *testutil.Base, container string, destPath string) { err := os.MkdirAll(destPath, dirPerm) assert.NilError(t, err) }, }, { description: "DEST_PATH is a directory, absolute", destinationSpec: pathIsADirAbsolute, catFile: filepath.Join(pathIsADirAbsolute, srcFileName), expect: icmd.Expected{ ExitCode: 0, }, setup: func(base *testutil.Base, container string, destPath string) { err := os.MkdirAll(destPath, dirPerm) assert.NilError(t, err) }, }, { description: "DEST_PATH is stdout", destinationSpec: "-", catFile: filepath.Join(pathIsADirAbsolute, srcFileName), expect: icmd.Expected{ ExitCode: 0, }, setup: func(base *testutil.Base, container string, destPath string) { err := os.MkdirAll(destPath, dirPerm) assert.NilError(t, err) }, }, }, }, } for _, tg := range testGroups { cpTestHelper(t, tg) } } func assertCatHelper(base *testutil.Base, catPath string, fileContent []byte, container string, expectedUID int, containerIsStopped bool) { base.T.Logf("catPath=%q", catPath) if container != "" && containerIsStopped { base.Cmd("start", container).AssertOK() defer base.Cmd("stop", container).AssertOK() } if container == "" { got, err := os.ReadFile(catPath) assert.NilError(base.T, err, "Failed reading from file") assert.DeepEqual(base.T, fileContent, got) st, err := os.Stat(catPath) assert.NilError(base.T, err) stSys := st.Sys().(*syscall.Stat_t) expected := uint32(expectedUID) actual := stSys.Uid assert.DeepEqual(base.T, expected, actual) } else { base.Cmd("exec", container, "sh", "-c", "--", fmt.Sprintf("ls -lA /; echo %q; cat %q", catPath, catPath)).AssertOutContains(string(fileContent)) base.Cmd("exec", container, "stat", "-c", "%u", catPath).AssertOutExactly(fmt.Sprintf("%d\n", expectedUID)) } } func cpTestHelper(t *testing.T, tg *testgroup) { // Get the source path groupSourceSpec := tg.sourceSpec groupSourceDir := groupSourceSpec fromStdin := false if tg.sourceSpec == "-" { groupSourceSpec = filepath.Join(srcDirName, tarballName) groupSourceDir = srcDirName fromStdin = true } else if tg.sourceIsAFile { groupSourceDir = filepath.Dir(groupSourceSpec) } // Copy direction copyToContainer := tg.toContainer // Description description := tg.description // Test cases testCases := tg.testCases // Compute UIDs dependent on cp direction var srcUID, destUID int if copyToContainer { srcUID = os.Geteuid() destUID = srcUID } else { srcUID = 42 destUID = os.Geteuid() } t.Run(description, func(t *testing.T) { t.Parallel() for _, tc := range testCases { testCase := tc t.Run(testCase.description, func(t *testing.T) { t.Parallel() // Compute test-specific values testID := testutil.Identifier(t) containerRunning := testID + "-r" containerStopped := testID + "-s" sourceFileContent := []byte(testID) tempDir := t.TempDir() base := testutil.NewBase(t) // Change working directory for commands to execute to the newly created temp directory on the host // Note that ChDir won't do in a parallel context - and that setup func on the host below // has to deal with that problem separately by making sure relative paths are resolved against temp base.Dir = tempDir // Prepare the specs and derived variables sourceSpec := groupSourceSpec catFile := testCase.catFile destinationSpec := testCase.destinationSpec toStdout := false // tarball destination just sets up the dir to extract to if destinationSpec == "-" { toStdout = true destinationSpec = filepath.Dir(catFile) } // If the test case does not specify a catFile, start with the destination spec if catFile == "" { catFile = destinationSpec } sourceFile := filepath.Join(groupSourceDir, srcFileName) if copyToContainer { if !filepath.IsAbs(catFile) { catFile = filepath.Join(string(os.PathSeparator), catFile) } if fromStdin { sourceFile = filepath.Join(tempDir, groupSourceDir, tarballName) } else { // Use an absolute path for evaluation // If the sourceFile is still relative, make it absolute to the temp sourceFile = filepath.Join(tempDir, sourceFile) // If the spec path for source on the host was absolute, make sure we put that under tempDir if filepath.IsAbs(sourceSpec) { sourceSpec = tempDir + sourceSpec } } } else { // If we are copying to host, we need to make sure we have an absolute path to cat, relative to temp, // whether it is relative, or "absolute" catFile = filepath.Join(tempDir, catFile) // If the spec for destination on the host was absolute, make sure we put that under tempDir if filepath.IsAbs(destinationSpec) { destinationSpec = tempDir + destinationSpec } } // Teardown: clean-up containers and optional volume tearDown := func() { base.Cmd("rm", "-f", containerRunning).Run() base.Cmd("rm", "-f", containerStopped).Run() if testCase.volume != nil { volID, _, _ := testCase.volume(base, testID) base.Cmd("volume", "rm", volID).Run() } } createFileOnHost := func() { switch fromStdin { case true: d := filepath.Dir(sourceFile) tarCpFolder := filepath.Join(d, cpFolderName) tarBinary, _, err := tarutil.FindTarBinary() assert.NilError(t, err) err = os.MkdirAll(tarCpFolder, dirPerm) assert.NilError(t, err) err = os.WriteFile(filepath.Join(tarCpFolder, srcFileName), sourceFileContent, filePerm) assert.NilError(t, err) err = exec.Command(tarBinary, "-cf", sourceFile, "-C", tarCpFolder, ".").Run() assert.NilError(t, err) err = os.RemoveAll(tarCpFolder) assert.NilError(t, err) case false: // Create file on the host err := os.MkdirAll(filepath.Dir(sourceFile), dirPerm) assert.NilError(t, err) err = os.WriteFile(sourceFile, sourceFileContent, filePerm) assert.NilError(t, err) } } // Setup: create volume, containers, create the source file setup := func() { args := []string{"run", "-d", "-w", containerCwd} if testCase.volume != nil { vol, mount, ro := testCase.volume(base, testID) volArg := fmt.Sprintf("%s:%s", vol, mount) if ro { volArg += ":ro" } args = append(args, "-v", volArg) } base.Cmd(append(args, "--name", containerRunning, testutil.CommonImage, "sleep", "Inf")...).AssertOK() base.Cmd(append(args, "--name", containerStopped, testutil.CommonImage, "sleep", "Inf")...).AssertOK() if copyToContainer { createFileOnHost() } else { // Create file content in the container // Note: cd /, otherwise we end-up in the container cwd, which is NOT obeyed by cp mkSrcScript := fmt.Sprintf("cd /; mkdir -p %q && echo -n %q >%q && chown %d %q", filepath.Dir(sourceFile), sourceFileContent, sourceFile, srcUID, sourceFile) base.Cmd("exec", containerRunning, "sh", "-euc", mkSrcScript).AssertOK() base.Cmd("exec", containerStopped, "sh", "-euc", mkSrcScript).AssertOK() } // If we have optional setup, run that now if testCase.setup != nil { // Some specs may come with a trailing slash (proper or improper) // Setup should still work in all cases (including if its a file), and get through to the actual test setupDest := destinationSpec setupDest = strings.TrimSuffix(setupDest, string(os.PathSeparator)) if !filepath.IsAbs(setupDest) { if copyToContainer { setupDest = filepath.Join(string(os.PathSeparator), setupDest) } else { setupDest = filepath.Join(tempDir, setupDest) } } testCase.setup(base, containerRunning, setupDest) testCase.setup(base, containerStopped, setupDest) } // Stop the "stopped" container base.Cmd("stop", containerStopped).AssertOK() } tearDown() t.Cleanup(tearDown) // If we have custom teardown, do that if testCase.tearDown != nil { testCase.tearDown() t.Cleanup(testCase.tearDown) } // Do the setup setup() // If Docker, removes the err part of expectation if nerdtest.IsDocker() { testCase.expect.Err = "" } // Build the final src and dest specifiers, including `containerXYZ:` container := "" if copyToContainer { if fromStdin { if toStdout { nerdctlCmd := base.Cmd("cp", "-", "-") nerdctlCmd.Run() nerdctlCmd.Assert(testCase.expect) } else { sourceSpec = "-" f, err := os.Open(sourceFile) assert.NilError(t, err) nerdctlCmd := base.Cmd("cp", sourceSpec, containerRunning+":"+destinationSpec) nerdctlCmd.Stdin = f nerdctlCmd.Run() nerdctlCmd.Assert(testCase.expect) f.Close() } } else { base.Cmd("cp", sourceSpec, containerRunning+":"+destinationSpec).Assert(testCase.expect) } container = containerRunning } else { nerdctlCmd := base.Cmd("cp", containerRunning+":"+sourceSpec, destinationSpec) if toStdout { out := nerdctlCmd.Out() nerdctlCmd.Assert(testCase.expect) // Since we can't check tar file directly easily, extract to the same destination tarDst := filepath.Dir(catFile) tarBinary, _, err := tarutil.FindTarBinary() assert.NilError(t, err) tarCmd := exec.Command(tarBinary, "-C", tarDst, "-xf", "-") tarCmd.Stdin = strings.NewReader(out) tarCmd.Stdout = os.Stdout tarCmd.Run() assert.NilError(t, tarCmd.Err) } else { nerdctlCmd.Assert(testCase.expect) } } // Run the actual test for the running container // If we expect the op to be a success, also check the destination file if testCase.expect.ExitCode == 0 { assertCatHelper(base, catFile, sourceFileContent, container, destUID, false) } // When copying container > host, we get shadowing from the previous container, possibly hiding failures // Solution: clear-up the tempDir if copyToContainer { err := os.RemoveAll(tempDir) assert.NilError(t, err) err = os.MkdirAll(tempDir, dirPerm) assert.NilError(t, err) createFileOnHost() defer os.RemoveAll(tempDir) } // ... and for the stopped container container = "" var cmd *testutil.Cmd if fromStdin && toStdout { cmd = base.Cmd("cp", "-", "-") } else if copyToContainer { container = containerStopped cmd = base.Cmd("cp", sourceSpec, containerStopped+":"+destinationSpec) if fromStdin { f, err := os.Open(sourceFile) assert.NilError(t, err) defer f.Close() cmd.Stdin = f } } else { cmd = base.Cmd("cp", containerStopped+":"+sourceSpec, destinationSpec) } if rootlessutil.IsRootless() && !nerdtest.IsDocker() { if fromStdin && toStdout { // Regular assert test case should work fine if src and dst are invalid cmd.Assert(testCase.expect) } else { cmd.Assert( icmd.Expected{ ExitCode: 1, Err: containerutil.ErrRootlessCannotCp.Error(), }) } return } cmd.Assert(testCase.expect) if testCase.expect.ExitCode == 0 { assertCatHelper(base, catFile, sourceFileContent, container, destUID, true) } }) } }) } ================================================ FILE: cmd/nerdctl/container/container_cp_nolinux.go ================================================ //go:build !linux /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import "github.com/spf13/cobra" func AddCpCommand(rootCmd *cobra.Command) { // NOP } ================================================ FILE: cmd/nerdctl/container/container_create.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "fmt" "runtime" "github.com/spf13/cobra" cdiparser "tags.cncf.io/container-device-interface/pkg/parser" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/container" "github.com/containerd/nerdctl/v2/pkg/containerutil" ) func CreateCommand() *cobra.Command { shortHelp := "Create a new container. Optionally specify \"ipfs://\" or \"ipns://\" scheme to pull image from IPFS." longHelp := shortHelp switch runtime.GOOS { case "windows": longHelp += "\n" longHelp += "WARNING: `nerdctl create` is experimental on Windows and currently broken (https://github.com/containerd/nerdctl/issues/28)" case "freebsd": longHelp += "\n" longHelp += "WARNING: `nerdctl create` is experimental on FreeBSD and currently requires `--net=none` (https://github.com/containerd/nerdctl/blob/main/docs/freebsd.md)" } var cmd = &cobra.Command{ Use: "create [flags] IMAGE [COMMAND] [ARG...]", Args: cobra.MinimumNArgs(1), Short: shortHelp, Long: longHelp, RunE: createAction, ValidArgsFunction: runShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().SetInterspersed(false) setCreateFlags(cmd) return cmd } //revive:disable:function-length func createOptions(cmd *cobra.Command) (types.ContainerCreateOptions, error) { var err error opt := types.ContainerCreateOptions{ Stdout: cmd.OutOrStdout(), Stderr: cmd.ErrOrStderr(), } opt.GOptions, err = helpers.ProcessRootCmdFlags(cmd) if err != nil { return opt, err } opt.NerdctlCmd, opt.NerdctlArgs = helpers.GlobalFlags(cmd) // #region for basic flags // The command `container start` doesn't support the flag `--interactive`. Set the default value of `opt.Interactive` false. opt.Interactive = false opt.TTY, err = cmd.Flags().GetBool("tty") if err != nil { return opt, err } // The nerdctl create command similar to nerdctl run -d except the container is never started. // So we keep the default value of `opt.Detach` true. opt.Detach = true opt.Restart, err = cmd.Flags().GetString("restart") if err != nil { return opt, err } opt.Rm, err = cmd.Flags().GetBool("rm") if err != nil { return opt, err } opt.Pull, err = cmd.Flags().GetString("pull") if err != nil { return opt, err } opt.Pid, err = cmd.Flags().GetString("pid") if err != nil { return opt, err } opt.StopSignal, err = cmd.Flags().GetString("stop-signal") if err != nil { return opt, err } opt.StopTimeout, err = cmd.Flags().GetInt("stop-timeout") if err != nil { return opt, err } // #endregion // #region for platform flags opt.Platform, err = cmd.Flags().GetString("platform") if err != nil { return opt, err } // #endregion // #region for init process flags opt.InitProcessFlag, err = cmd.Flags().GetBool("init") if err != nil { return opt, err } if opt.InitProcessFlag || cmd.Flags().Changed("init-binary") { var initBinary string initBinary, err = cmd.Flags().GetString("init-binary") if err != nil { return opt, err } opt.InitBinary = &initBinary } // #endregion // #region for isolation flags opt.Isolation, err = cmd.Flags().GetString("isolation") if err != nil { return opt, err } // #endregion // #region for resource flags opt.CPUs, err = cmd.Flags().GetFloat64("cpus") if err != nil { return opt, err } opt.CPUQuota, err = cmd.Flags().GetInt64("cpu-quota") if err != nil { return opt, err } opt.CPUPeriod, err = cmd.Flags().GetUint64("cpu-period") if err != nil { return opt, err } opt.CPUShares, err = cmd.Flags().GetUint64("cpu-shares") if err != nil { return opt, err } opt.CPUSetCPUs, err = cmd.Flags().GetString("cpuset-cpus") if err != nil { return opt, err } opt.CPUSetMems, err = cmd.Flags().GetString("cpuset-mems") if err != nil { return opt, err } opt.CPURealtimePeriod, err = cmd.Flags().GetUint64("cpu-rt-period") if err != nil { return opt, err } opt.CPURealtimeRuntime, err = cmd.Flags().GetUint64("cpu-rt-runtime") if err != nil { return opt, err } opt.Memory, err = cmd.Flags().GetString("memory") if err != nil { return opt, err } opt.MemoryReservationChanged = cmd.Flags().Changed("memory-reservation") opt.MemoryReservation, err = cmd.Flags().GetString("memory-reservation") if err != nil { return opt, err } opt.MemorySwap, err = cmd.Flags().GetString("memory-swap") if err != nil { return opt, err } opt.MemorySwappiness64Changed = cmd.Flags().Changed("memory-swappiness") opt.MemorySwappiness64, err = cmd.Flags().GetInt64("memory-swappiness") if err != nil { return opt, err } opt.KernelMemoryChanged = cmd.Flag("kernel-memory").Changed opt.KernelMemory, err = cmd.Flags().GetString("kernel-memory") if err != nil { return opt, err } opt.OomKillDisable, err = cmd.Flags().GetBool("oom-kill-disable") if err != nil { return opt, err } opt.OomScoreAdjChanged = cmd.Flags().Changed("oom-score-adj") opt.OomScoreAdj, err = cmd.Flags().GetInt("oom-score-adj") if err != nil { return opt, err } opt.PidsLimit, err = cmd.Flags().GetInt64("pids-limit") if err != nil { return opt, err } opt.CgroupConf, err = cmd.Flags().GetStringSlice("cgroup-conf") if err != nil { return opt, err } opt.Cgroupns, err = cmd.Flags().GetString("cgroupns") if err != nil { return opt, err } opt.CgroupParent, err = cmd.Flags().GetString("cgroup-parent") if err != nil { return opt, err } allDevices, err := cmd.Flags().GetStringSlice("device") if err != nil { return opt, err } for _, device := range allDevices { if cdiparser.IsQualifiedName(device) { opt.CDIDevices = append(opt.CDIDevices, device) } else { opt.Device = append(opt.Device, device) } } // #endregion // #region for blkio flags opt.BlkioWeight, err = cmd.Flags().GetUint16("blkio-weight") if err != nil { return opt, err } opt.BlkioWeightDevice, err = cmd.Flags().GetStringArray("blkio-weight-device") if err != nil { return opt, err } opt.BlkioDeviceReadBps, err = cmd.Flags().GetStringArray("device-read-bps") if err != nil { return opt, err } opt.BlkioDeviceWriteBps, err = cmd.Flags().GetStringArray("device-write-bps") if err != nil { return opt, err } opt.BlkioDeviceReadIOps, err = cmd.Flags().GetStringArray("device-read-iops") if err != nil { return opt, err } opt.BlkioDeviceWriteIOps, err = cmd.Flags().GetStringArray("device-write-iops") if err != nil { return opt, err } // #endregion // #region for healthcheck flags opt.HealthCmd, err = cmd.Flags().GetString("health-cmd") if err != nil { return opt, err } opt.HealthInterval, err = cmd.Flags().GetDuration("health-interval") if err != nil { return opt, err } opt.HealthTimeout, err = cmd.Flags().GetDuration("health-timeout") if err != nil { return opt, err } opt.HealthRetries, err = cmd.Flags().GetInt("health-retries") if err != nil { return opt, err } opt.HealthStartPeriod, err = cmd.Flags().GetDuration("health-start-period") if err != nil { return opt, err } opt.NoHealthcheck, err = cmd.Flags().GetBool("no-healthcheck") if err != nil { return opt, err } if err := helpers.ValidateHealthcheckFlags(opt); err != nil { return opt, err } // #endregion // #region for intel RDT flags opt.RDTClass, err = cmd.Flags().GetString("rdt-class") if err != nil { return opt, err } // #endregion // #region for user flags // If user is set we will attempt to start container with that user (must be present on the host) // Otherwise we will inherit permissions from the user that the containerd process is running as opt.User, err = cmd.Flags().GetString("user") if err != nil { return opt, err } opt.Umask = "" if cmd.Flags().Changed("umask") { opt.Umask, err = cmd.Flags().GetString("umask") if err != nil { return opt, err } } opt.GroupAdd, err = cmd.Flags().GetStringSlice("group-add") if err != nil { return opt, err } // #endregion // #region for security flags opt.SecurityOpt, err = cmd.Flags().GetStringArray("security-opt") if err != nil { return opt, err } opt.CapAdd, err = cmd.Flags().GetStringSlice("cap-add") if err != nil { return opt, err } opt.CapDrop, err = cmd.Flags().GetStringSlice("cap-drop") if err != nil { return opt, err } opt.Privileged, err = cmd.Flags().GetBool("privileged") if err != nil { return opt, err } opt.Systemd, err = cmd.Flags().GetString("systemd") if err != nil { return opt, err } // #endregion // #region for runtime flags opt.Runtime, err = cmd.Flags().GetString("runtime") if err != nil { return opt, err } opt.Sysctl, err = cmd.Flags().GetStringArray("sysctl") if err != nil { return opt, err } // #endregion // #region for volume flags opt.Volume, err = cmd.Flags().GetStringArray("volume") if err != nil { return opt, err } // tmpfs needs to be StringArray, not StringSlice, to prevent "/foo:size=64m,exec" from being split to {"/foo:size=64m", "exec"} opt.Tmpfs, err = cmd.Flags().GetStringArray("tmpfs") if err != nil { return opt, err } opt.Mount, err = cmd.Flags().GetStringArray("mount") if err != nil { return opt, err } opt.VolumesFrom, err = cmd.Flags().GetStringArray("volumes-from") if err != nil { return opt, err } // #endregion // #region for rootfs flags opt.ReadOnly, err = cmd.Flags().GetBool("read-only") if err != nil { return opt, err } opt.Rootfs, err = cmd.Flags().GetBool("rootfs") if err != nil { return opt, err } // #endregion // #region for env flags opt.EntrypointChanged = cmd.Flags().Changed("entrypoint") opt.Entrypoint, err = cmd.Flags().GetStringArray("entrypoint") if err != nil { return opt, err } opt.Workdir, err = cmd.Flags().GetString("workdir") if err != nil { return opt, err } opt.Env, err = cmd.Flags().GetStringArray("env") if err != nil { return opt, err } opt.EnvFile, err = cmd.Flags().GetStringSlice("env-file") if err != nil { return opt, err } // #endregion // #region for metadata flags opt.Name, err = cmd.Flags().GetString("name") if err != nil { return opt, err } opt.Label, err = cmd.Flags().GetStringArray("label") if err != nil { return opt, err } opt.LabelFile, err = cmd.Flags().GetStringSlice("label-file") if err != nil { return opt, err } opt.Annotations, err = cmd.Flags().GetStringArray("annotation") if err != nil { return opt, err } opt.CidFile, err = cmd.Flags().GetString("cidfile") if err != nil { return opt, err } opt.PidFile = "" if cmd.Flags().Changed("pidfile") { opt.PidFile, err = cmd.Flags().GetString("pidfile") if err != nil { return opt, err } } // #endregion // #region for logging flags // json-file is the built-in and default log driver for nerdctl opt.LogDriver, err = cmd.Flags().GetString("log-driver") if err != nil { return opt, err } opt.LogOpt, err = cmd.Flags().GetStringArray("log-opt") if err != nil { return opt, err } // #endregion // #region for shared memory flags opt.IPC, err = cmd.Flags().GetString("ipc") if err != nil { return opt, err } opt.ShmSize, err = cmd.Flags().GetString("shm-size") if err != nil { return opt, err } // #endregion // #region for gpu flags opt.GPUs, err = cmd.Flags().GetStringArray("gpus") if err != nil { return opt, err } // #endregion // #region for ulimit flags opt.Ulimit, err = cmd.Flags().GetStringSlice("ulimit") if err != nil { return opt, err } // #endregion // #region for ipfs flags opt.IPFSAddress, err = cmd.Flags().GetString("ipfs-address") if err != nil { return opt, err } // #endregion // #region for image pull and verify options imageVerifyOpt, err := helpers.VerifyOptions(cmd) if err != nil { return opt, err } quiet, err := cmd.Flags().GetBool("quiet") if err != nil { return opt, err } opt.ImagePullOpt = types.ImagePullOptions{ GOptions: opt.GOptions, VerifyOptions: imageVerifyOpt, IPFSAddress: opt.IPFSAddress, Stdout: opt.Stdout, Stderr: opt.Stderr, Quiet: quiet, } // #endregion // #region for UserNS opt.UserNS, err = cmd.Flags().GetString("userns-remap") if err != nil { return opt, err } userns, err := cmd.Flags().GetString("userns") if err != nil { return opt, err } if userns == "host" { opt.UserNS = "" } else if userns != "" { return opt, fmt.Errorf("invalid user mode") } if opt.Privileged && opt.UserNS != "" { //userns-remap is not supported with privileged flag. // Ref: https://docs.docker.com/engine/security/userns-remap/ return opt, fmt.Errorf("privileged flag cannot be used with userns-remap") } // #endregion return opt, nil } func createAction(cmd *cobra.Command, args []string) error { createOpt, err := createOptions(cmd) if err != nil { return err } if (createOpt.Platform == "windows" || createOpt.Platform == "freebsd") && !createOpt.GOptions.Experimental { return fmt.Errorf("%s requires experimental mode to be enabled", createOpt.Platform) } client, ctx, cancel, err := clientutil.NewClientWithPlatform(cmd.Context(), createOpt.GOptions.Namespace, createOpt.GOptions.Address, createOpt.Platform) if err != nil { return err } defer cancel() netFlags, err := loadNetworkFlags(cmd, createOpt.GOptions) if err != nil { return fmt.Errorf("failed to load networking flags: %w", err) } netManager, err := containerutil.NewNetworkingOptionsManager(createOpt.GOptions, netFlags, client) if err != nil { return err } c, gc, err := container.Create(ctx, client, args, netManager, createOpt) if err != nil { if gc != nil { gc() } return err } // defer setting `nerdctl/error` label in case of error defer func() { if err != nil { containerutil.UpdateErrorLabel(ctx, c, err) } }() fmt.Fprintln(createOpt.Stdout, c.ID()) return nil } ================================================ FILE: cmd/nerdctl/container/container_create_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "errors" "fmt" "io" "os" "path/filepath" "strconv" "strings" "syscall" "testing" "github.com/opencontainers/go-digest" "gotest.tools/v3/assert" "github.com/containerd/containerd/v2/defaults" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" ) func TestCreateWithLabel(t *testing.T) { t.Parallel() base := testutil.NewBase(t) tID := testutil.Identifier(t) base.Cmd("create", "--name", tID, "--label", "foo=bar", testutil.NginxAlpineImage, "echo", "foo").AssertOK() defer base.Cmd("rm", "-f", tID).Run() inspect := base.InspectContainer(tID) assert.Equal(base.T, "bar", inspect.Config.Labels["foo"]) // the label `maintainer`` is defined by image assert.Equal(base.T, "NGINX Docker Maintainers ", inspect.Config.Labels["maintainer"]) } func TestCreateWithMACAddress(t *testing.T) { base := testutil.NewBase(t) tID := testutil.Identifier(t) networkBridge := "testNetworkBridge" + tID networkMACvlan := "testNetworkMACvlan" + tID networkIPvlan := "testNetworkIPvlan" + tID tearDown := func() { base.Cmd("network", "rm", networkBridge).Run() base.Cmd("network", "rm", networkMACvlan).Run() base.Cmd("network", "rm", networkIPvlan).Run() } tearDown() t.Cleanup(tearDown) base.Cmd("network", "create", networkBridge, "--driver", "bridge").AssertOK() base.Cmd("network", "create", networkMACvlan, "--driver", "macvlan").AssertOK() base.Cmd("network", "create", networkIPvlan, "--driver", "ipvlan").AssertOK() defaultMac := base.Cmd("run", "--rm", "-i", "--network", "host", testutil.CommonImage). CmdOption(testutil.WithStdin(strings.NewReader("ip addr show eth0 | grep ether | awk '{printf $2}'"))). Run().Stdout() passedMac := "we expect the generated mac on the output" tests := []struct { Network string WantErr bool Expect string }{ {"host", false, defaultMac}, // anything but the actual address being passed {"none", false, ""}, {"container:whatever" + tID, true, "container"}, // "No such container" vs. "could not find container" {"bridge", false, passedMac}, {networkBridge, false, passedMac}, {networkMACvlan, false, passedMac}, {networkIPvlan, true, "not support"}, } for i, test := range tests { containerName := fmt.Sprintf("%s_%d", tID, i) testName := fmt.Sprintf("%s_container:%s_network:%s_expect:%s", tID, containerName, test.Network, test.Expect) expect := test.Expect network := test.Network wantErr := test.WantErr t.Run(testName, func(tt *testing.T) { tt.Parallel() macAddress, err := nettestutil.GenerateMACAddress() if err != nil { tt.Errorf("failed to generate MAC address: %s", err) } if expect == passedMac { expect = macAddress } tearDown := func() { base.Cmd("rm", "-f", containerName).Run() } tearDown() tt.Cleanup(tearDown) // This is currently blocked by https://github.com/containerd/nerdctl/pull/3104 // res := base.Cmd("create", "-i", "--network", network, "--mac-address", macAddress, testutil.CommonImage).Run() res := base.Cmd("create", "--network", network, "--name", containerName, "--mac-address", macAddress, testutil.CommonImage, "sh", "-c", "--", "ip addr show").Run() if !wantErr { assert.Assert(t, res.ExitCode == 0, "Command should have succeeded", res) // This is currently blocked by: https://github.com/containerd/nerdctl/pull/3104 // res = base.Cmd("start", "-i", containerName). // CmdOption(testutil.WithStdin(strings.NewReader("ip addr show eth0 | grep ether | awk '{printf $2}'"))).Run() res = base.Cmd("start", "-a", containerName).Run() // FIXME: flaky - this has failed on the CI once, with the output NOT containing anything // https://github.com/containerd/nerdctl/actions/runs/11392051487/job/31697214002?pr=3535#step:7:271 assert.Assert(t, strings.Contains(res.Stdout(), expect), fmt.Sprintf("expected output to contain %q: %q", expect, res.Stdout())) assert.Assert(t, res.ExitCode == 0, "Command should have succeeded") } else { if nerdtest.IsDocker() && (network == networkIPvlan || network == "container:whatever"+tID) { // unlike nerdctl // when using network ipvlan or container in Docker // it delays fail on executing start command assert.Assert(t, res.ExitCode == 0, "Command should have succeeded", res) res = base.Cmd("start", "-i", "-a", containerName). CmdOption(testutil.WithStdin(strings.NewReader("ip addr show eth0 | grep ether | awk '{printf $2}'"))).Run() } // See https://github.com/containerd/nerdctl/issues/3101 if nerdtest.IsDocker() && (network == networkBridge) { expect = "" } if expect != "" { assert.Assert(t, strings.Contains(res.Combined(), expect), fmt.Sprintf("expected output to contain %q: %q", expect, res.Combined())) } else { assert.Assert(t, res.Combined() == "", fmt.Sprintf("expected output to be empty: %q", res.Combined())) } assert.Assert(t, res.ExitCode != 0, "Command should have failed", res) } }) } } func TestCreateWithTty(t *testing.T) { base := testutil.NewBase(t) imageName := testutil.CommonImage withoutTtyContainerName := "without-terminal-" + testutil.Identifier(t) withTtyContainerName := "with-terminal-" + testutil.Identifier(t) // without -t, fail base.Cmd("create", "--name", withoutTtyContainerName, imageName, "stty").AssertOK() base.Cmd("start", withoutTtyContainerName).AssertOK() defer base.Cmd("container", "rm", "-f", withoutTtyContainerName).AssertOK() base.Cmd("logs", withoutTtyContainerName).AssertCombinedOutContains("stty: standard input: Not a tty") withoutTtyContainer := base.InspectContainer(withoutTtyContainerName) assert.Equal(base.T, 1, withoutTtyContainer.State.ExitCode) // with -t, success base.Cmd("create", "-t", "--name", withTtyContainerName, imageName, "stty").AssertOK() base.Cmd("start", withTtyContainerName).AssertOK() defer base.Cmd("container", "rm", "-f", withTtyContainerName).AssertOK() base.Cmd("logs", withTtyContainerName).AssertCombinedOutContains("speed 38400 baud; line = 0;") withTtyContainer := base.InspectContainer(withTtyContainerName) assert.Equal(base.T, 0, withTtyContainer.State.ExitCode) } // TestIssue2993 tests https://github.com/containerd/nerdctl/issues/2993 func TestIssue2993(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.Not(nerdtest.Docker) const ( containersPathKey = "containersPath" etchostsPathKey = "etchostsPath" ) getAddrHash := func(addr string) string { const addrHashLen = 8 d := digest.SHA256.FromString(addr) h := d.Encoded()[0:addrHashLen] return h } testCase.SubTests = []*test.Case{ { Description: "Issue #2993 - nerdctl no longer leaks containers and etchosts directories and files when container creation fails.", Setup: func(data test.Data, helpers test.Helpers) { dataRoot := data.Temp().Path() helpers.Ensure("run", "--data-root", dataRoot, "--name", data.Identifier(), "-d", testutil.AlpineImage, "sleep", nerdtest.Infinity) h := getAddrHash(defaults.DefaultAddress) dataStore := filepath.Join(dataRoot, h) namespace := string(helpers.Read(nerdtest.Namespace)) containersPath := filepath.Join(dataStore, "containers", namespace) containersDirs, err := os.ReadDir(containersPath) assert.NilError(t, err) assert.Equal(t, len(containersDirs), 1) etchostsPath := filepath.Join(dataStore, "etchosts", namespace) etchostsDirs, err := os.ReadDir(etchostsPath) assert.NilError(t, err) assert.Equal(t, len(etchostsDirs), 1) data.Labels().Set(containersPathKey, containersPath) data.Labels().Set(etchostsPathKey, etchostsPath) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "--data-root", data.Temp().Path(), "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--data-root", data.Temp().Path(), "--name", data.Identifier(), "-d", testutil.AlpineImage, "sleep", nerdtest.Infinity) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New("is already used by ID")}, Output: func(stdout string, t tig.T) { containersDirs, err := os.ReadDir(data.Labels().Get(containersPathKey)) assert.NilError(t, err) assert.Equal(t, len(containersDirs), 1) etchostsDirs, err := os.ReadDir(data.Labels().Get(etchostsPathKey)) assert.NilError(t, err) assert.Equal(t, len(etchostsDirs), 1) }, } }, }, { Description: "Issue #2993 - nerdctl no longer leaks containers and etchosts directories and files when containers are removed.", Setup: func(data test.Data, helpers test.Helpers) { dataRoot := data.Temp().Path() helpers.Ensure("run", "--data-root", dataRoot, "--name", data.Identifier(), "-d", testutil.AlpineImage, "sleep", nerdtest.Infinity) h := getAddrHash(defaults.DefaultAddress) dataStore := filepath.Join(dataRoot, h) namespace := string(helpers.Read(nerdtest.Namespace)) containersPath := filepath.Join(dataStore, "containers", namespace) containersDirs, err := os.ReadDir(containersPath) assert.NilError(t, err) assert.Equal(t, len(containersDirs), 1) etchostsPath := filepath.Join(dataStore, "etchosts", namespace) etchostsDirs, err := os.ReadDir(etchostsPath) assert.NilError(t, err) assert.Equal(t, len(etchostsDirs), 1) data.Labels().Set(containersPathKey, containersPath) data.Labels().Set(etchostsPathKey, etchostsPath) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("--data-root", data.Temp().Path(), "rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("--data-root", data.Temp().Path(), "rm", "-f", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Errors: []error{}, Output: func(stdout string, t tig.T) { containersDirs, err := os.ReadDir(data.Labels().Get(containersPathKey)) assert.NilError(t, err) assert.Equal(t, len(containersDirs), 0) etchostsDirs, err := os.ReadDir(data.Labels().Get(etchostsPathKey)) assert.NilError(t, err) assert.Equal(t, len(etchostsDirs), 0) }, } }, }, } testCase.Run(t) } func TestCreateFromOCIArchive(t *testing.T) { testutil.RequiresBuild(t) testutil.RegisterBuildCacheCleanup(t) // Docker does not support creating containers from OCI archive. testutil.DockerIncompatible(t) base := testutil.NewBase(t) imageName := testutil.Identifier(t) containerName := testutil.Identifier(t) teardown := func() { base.Cmd("rm", "-f", containerName).Run() base.Cmd("rmi", "-f", imageName).Run() } defer teardown() teardown() const sentinel = "test-nerdctl-create-from-oci-archive" dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "%s"]`, testutil.CommonImage, sentinel) buildCtx := helpers.CreateBuildContext(t, dockerfile) tag := fmt.Sprintf("%s:latest", imageName) tarPath := fmt.Sprintf("%s/%s.tar", buildCtx, imageName) base.Cmd("build", "--tag", tag, fmt.Sprintf("--output=type=oci,dest=%s", tarPath), buildCtx).AssertOK() base.Cmd("create", "--rm", "--name", containerName, fmt.Sprintf("oci-archive://%s", tarPath)).AssertOK() base.Cmd("start", "--attach", containerName).AssertOutContains("test-nerdctl-create-from-oci-archive") } func TestUsernsMappingCreateCmd(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.All( nerdtest.AllowModifyUserns, nerdtest.RemapIDs, require.Not(nerdtest.Docker)), NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { data.Labels().Set("validUserns", "nerdctltestuser") data.Labels().Set("expectedHostUID", "123456789") data.Labels().Set("invalidUserns", "invaliduser") }, SubTests: []*test.Case{ { Description: "Test container create with valid Userns", NoParallel: true, // Changes system config so running in non parallel mode Setup: func(data test.Data, helpers test.Helpers) { err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers) assert.NilError(t, err, "Failed to append Userns config") }, Cleanup: func(data test.Data, helpers test.Helpers) { removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers) helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { helpers.Ensure("create", "--tty", "--userns-remap", data.Labels().Get("validUserns"), "--name", data.Identifier(), testutil.NginxAlpineImage) return helpers.Command("start", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) assert.NilError(t, err, "Failed to get container host UID") assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID")) }, } }, }, { Description: "Test container create failure with valid Userns and privileged flag", NoParallel: true, // Changes system config so running in non parallel mode Setup: func(data test.Data, helpers test.Helpers) { err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers) assert.NilError(t, err, "Failed to append Userns config") }, Cleanup: func(data test.Data, helpers test.Helpers) { removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("create", "--tty", "--privileged", "--userns-remap", data.Labels().Get("validUserns"), "--name", data.Identifier(), testutil.NginxAlpineImage) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, } }, }, { Description: "Test container create with invalid Userns", NoParallel: true, // Changes system config so running in non parallel mode Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("create", "--tty", "--userns-remap", data.Labels().Get("invalidUserns"), "--name", data.Identifier(), testutil.NginxAlpineImage) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, } }, }, }, } testCase.Run(t) } func getContainerHostUID(helpers test.Helpers, containerName string) (string, error) { result := helpers.Capture("inspect", "--format", "{{.State.Pid}}", containerName) pidStr := strings.TrimSpace(result) pid, err := strconv.Atoi(pidStr) if err != nil { return "", fmt.Errorf("invalid PID: %v", err) } stat, err := os.Stat(fmt.Sprintf("/proc/%d", pid)) if err != nil { return "", fmt.Errorf("failed to stat process: %v", err) } uid := int(stat.Sys().(*syscall.Stat_t).Uid) return strconv.Itoa(uid), nil } func appendUsernsConfig(userns string, hostUID string, helpers test.Helpers) error { addUser(userns, hostUID, helpers) entry := fmt.Sprintf("%s:%s:65536\n", userns, hostUID) tempDir := helpers.T().TempDir() files := []string{"subuid", "subgid"} for _, file := range files { fileBak := filepath.Join(tempDir, file) defer os.Remove(fileBak) d, err := os.Create(fileBak) if err != nil { return fmt.Errorf("failed to create %s: %w", fileBak, err) } s, err := os.Open(filepath.Join("/etc", file)) if err != nil { return fmt.Errorf("failed to open %s: %w", file, err) } defer s.Close() _, err = io.Copy(d, s) if err != nil { return fmt.Errorf("failed to copy %s to %s: %w", file, fileBak, err) } f, err := os.OpenFile(fmt.Sprintf("/etc/%s", file), os.O_APPEND|os.O_WRONLY, 0644) if err != nil { return fmt.Errorf("failed to open %s: %w", file, err) } defer f.Close() if _, err := f.WriteString(entry); err != nil { return fmt.Errorf("failed to write to %s: %w", file, err) } } return nil } func addUser(username string, hostID string, helpers test.Helpers) { helpers.Custom("groupadd", "-g", hostID, username).Run(&test.Expected{ ExitCode: 0}) helpers.Custom("useradd", "-u", hostID, "-g", hostID, "-s", "/bin/false", username).Run(&test.Expected{ ExitCode: 0}) } func removeUsernsConfig(t *testing.T, userns string, helpers test.Helpers) { delUser(userns, helpers) delGroup(userns, helpers) tempDir := helpers.T().TempDir() files := []string{"subuid", "subgid"} for _, file := range files { fileBak := filepath.Join(tempDir, file) s, err := os.Open(fileBak) if err != nil { t.Logf("failed to open %s, Error: %s", fileBak, err) continue } defer s.Close() d, err := os.Open(filepath.Join("/etc/%s", file)) if err != nil { t.Logf("failed to open %s, Error: %s", file, err) continue } defer d.Close() _, err = io.Copy(d, s) if err != nil { t.Logf("failed to restore. Copy %s to %s failed, Error %s", fileBak, file, err) continue } } } func delUser(username string, helpers test.Helpers) { helpers.Custom("userdel", username).Run(&test.Expected{ExitCode: expect.ExitCodeNoCheck}) } func delGroup(groupname string, helpers test.Helpers) { helpers.Custom("groupdel", groupname).Run(&test.Expected{ExitCode: expect.ExitCodeNoCheck}) } ================================================ FILE: cmd/nerdctl/container/container_create_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "encoding/json" "fmt" "testing" "time" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestCreate(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("create", "--name", data.Identifier("container"), testutil.CommonImage, "echo", "foo") data.Labels().Set("cID", data.Identifier("container")) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("container")) } testCase.SubTests = []*test.Case{ { Description: "ps -a", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("ps", "-a", "--filter", "status=created", "--filter", fmt.Sprintf("name=%s", data.Labels().Get("cID"))) }, Expected: test.Expects(0, nil, expect.Contains("Created")), }, { Description: "start", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("start", "-a", data.Labels().Get("cID")) }, Expected: test.Expects(0, nil, expect.Contains("foo")), }, } testCase.Run(t) } func TestCreateHyperVContainer(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = nerdtest.HyperV testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("create", "--isolation", "hyperv", "--name", data.Identifier("container"), testutil.CommonImage, "echo", "foo") data.Labels().Set("cID", data.Identifier("container")) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("container")) } testCase.SubTests = []*test.Case{ { Description: "ps -a", NoParallel: true, Command: test.Command("ps", "-a"), // FIXME: this might get a false positive if other tests have created a container Expected: test.Expects(0, nil, expect.Contains("Created")), }, { Description: "start", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("start", data.Labels().Get("cID")) ran := false for i := 0; i < 10 && !ran; i++ { helpers.Command("container", "inspect", data.Labels().Get("cID")). Run(&test.Expected{ ExitCode: expect.ExitCodeNoCheck, Output: func(stdout string, t tig.T) { var dc []dockercompat.Container err := json.Unmarshal([]byte(stdout), &dc) if err != nil || len(dc) == 0 { return } assert.Equal(t, len(dc), 1, "Unexpectedly got multiple results\n") ran = dc[0].State.Status == "exited" }, }) time.Sleep(time.Second) } assert.Assert(t, ran, "container did not ran after 10 seconds") }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", data.Labels().Get("cID")) }, Expected: test.Expects(0, nil, expect.Contains("foo")), }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_diff.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "context" "fmt" "os" "path/filepath" "time" "github.com/opencontainers/image-spec/identity" "github.com/spf13/cobra" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/core/leases" "github.com/containerd/containerd/v2/core/mount" "github.com/containerd/continuity/fs" "github.com/containerd/log" "github.com/containerd/platforms" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/idgen" "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" "github.com/containerd/nerdctl/v2/pkg/imgutil" "github.com/containerd/nerdctl/v2/pkg/labels" ) func DiffCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "diff [CONTAINER]", Short: "Inspect changes to files or directories on a container's filesystem", Args: cobra.MinimumNArgs(1), RunE: diffAction, ValidArgsFunction: diffShellComplete, SilenceUsage: true, SilenceErrors: true, } return cmd } func diffOptions(cmd *cobra.Command) (types.ContainerDiffOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ContainerDiffOptions{}, err } return types.ContainerDiffOptions{ Stdout: cmd.OutOrStdout(), GOptions: globalOptions, }, nil } func diffAction(cmd *cobra.Command, args []string) error { options, err := diffOptions(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() walker := &containerwalker.ContainerWalker{ Client: client, OnFound: func(ctx context.Context, found containerwalker.Found) error { if found.MatchCount > 1 { return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) } changes, err := getChanges(ctx, client, found.Container) if err != nil { return err } for _, change := range changes { switch change.Kind { case fs.ChangeKindAdd: fmt.Fprintln(options.Stdout, "A", change.Path) case fs.ChangeKindModify: fmt.Fprintln(options.Stdout, "C", change.Path) case fs.ChangeKindDelete: fmt.Fprintln(options.Stdout, "D", change.Path) default: } } return nil }, } container := args[0] n, err := walker.Walk(ctx, container) if err != nil { return err } else if n == 0 { return fmt.Errorf("no such container %s", container) } return nil } func getChanges(ctx context.Context, client *containerd.Client, container containerd.Container) ([]fs.Change, error) { id := container.ID() info, err := container.Info(ctx) if err != nil { return nil, err } var ( snName = info.Snapshotter sn = client.SnapshotService(snName) ) mounts, err := sn.Mounts(ctx, id) if err != nil { return nil, err } // NOTE: Moby uses provided rootfs to run container. It doesn't support // to commit container created by moby. baseImgWithoutPlatform, err := client.ImageService().Get(ctx, info.Image) if err != nil { return nil, fmt.Errorf("container %q lacks image (wasn't created by nerdctl?): %w", id, err) } platformLabel := info.Labels[labels.Platform] if platformLabel == "" { platformLabel = platforms.DefaultString() log.G(ctx).Warnf("Image lacks label %q, assuming the platform to be %q", labels.Platform, platformLabel) } ocispecPlatform, err := platforms.Parse(platformLabel) if err != nil { return nil, err } log.G(ctx).Debugf("ocispecPlatform=%q", platforms.Format(ocispecPlatform)) platformMC := platforms.Only(ocispecPlatform) baseImg := containerd.NewImageWithPlatform(client, baseImgWithoutPlatform, platformMC) baseImgConfig, _, err := imgutil.ReadImageConfig(ctx, baseImg) if err != nil { return nil, err } // Don't gc me and clean the dirty data after 1 hour! ctx, done, err := client.WithLease(ctx, leases.WithRandomID(), leases.WithExpiration(1*time.Hour)) if err != nil { return nil, fmt.Errorf("failed to create lease for diff: %w", err) } defer done(ctx) rootfsID := identity.ChainID(baseImgConfig.RootFS.DiffIDs).String() randomID := idgen.GenerateID() parent, err := sn.View(ctx, randomID, rootfsID) if err != nil { return nil, err } defer sn.Remove(ctx, randomID) var changes []fs.Change err = mount.WithReadonlyTempMount(ctx, parent, func(lower string) error { return mount.WithReadonlyTempMount(ctx, mounts, func(upper string) error { return fs.Changes(ctx, lower, upper, func(ck fs.ChangeKind, s string, fi os.FileInfo, err error) error { if err != nil { return err } changes = appendChanges(changes, fs.Change{ Kind: ck, Path: s, }) return nil }) }) }) if err != nil { return nil, err } return changes, err } func appendChanges(changes []fs.Change, fsChange fs.Change) []fs.Change { newDir, _ := filepath.Split(fsChange.Path) newDirPath := filepath.SplitList(newDir) if len(changes) == 0 { for i := 1; i < len(newDirPath); i++ { changes = append(changes, fs.Change{ Kind: fs.ChangeKindModify, Path: filepath.Join(newDirPath[:i+1]...), }) } return append(changes, fsChange) } last := changes[len(changes)-1] lastDir, _ := filepath.Split(last.Path) lastDirPath := filepath.SplitList(lastDir) for i := range newDirPath { if len(lastDirPath) > i && lastDirPath[i] == newDirPath[i] { continue } changes = append(changes, fs.Change{ Kind: fs.ChangeKindModify, Path: filepath.Join(newDirPath[:i+1]...), }) } return append(changes, fsChange) } func diffShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show container names return completion.ContainerNames(cmd, nil) } ================================================ FILE: cmd/nerdctl/container/container_diff_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestDiff(t *testing.T) { testCase := nerdtest.Setup() // It is unclear why this is failing with docker when run in parallel // Obviously some other container test is interfering if nerdtest.IsDocker() { testCase.NoParallel = true } testCase.Require = require.Not(require.Windows) testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "--name", data.Identifier(), testutil.CommonImage, "sh", "-euxc", "touch /a; touch /bin/b; rm /bin/base64") } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("diff", data.Identifier()) } testCase.Expected = test.Expects( 0, nil, expect.Contains( "A /a", "C /bin", "A /bin/b", "D /bin/base64"), ) testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_exec.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "errors" "github.com/spf13/cobra" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/container" ) func ExecCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "exec [flags] CONTAINER COMMAND [ARG...]", Args: cobra.MinimumNArgs(2), Short: "Run a command in a running container", RunE: execAction, ValidArgsFunction: execShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().SetInterspersed(false) cmd.Flags().BoolP("tty", "t", false, "Allocate a pseudo-TTY") cmd.Flags().BoolP("interactive", "i", false, "Keep STDIN open even if not attached") cmd.Flags().BoolP("detach", "d", false, "Detached mode: run command in the background") cmd.Flags().StringP("workdir", "w", "", "Working directory inside the container") // env needs to be StringArray, not StringSlice, to prevent "FOO=foo1,foo2" from being split to {"FOO=foo1", "foo2"} cmd.Flags().StringArrayP("env", "e", nil, "Set environment variables") // env-file is defined as StringSlice, not StringArray, to allow specifying "--env-file=FILE1,FILE2" (compatible with Podman) cmd.Flags().StringSlice("env-file", nil, "Set environment variables from file") cmd.Flags().Bool("privileged", false, "Give extended privileges to the command") cmd.Flags().StringP("user", "u", "", "Username or UID (format: [:])") return cmd } func execOptions(cmd *cobra.Command) (types.ContainerExecOptions, error) { // We do not check if we have a terminal here, as container.Exec calling console.Current will ensure that globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ContainerExecOptions{}, err } isInteractive, err := cmd.Flags().GetBool("interactive") if err != nil { return types.ContainerExecOptions{}, err } isTerminal, err := cmd.Flags().GetBool("tty") if err != nil { return types.ContainerExecOptions{}, err } isDetach, err := cmd.Flags().GetBool("detach") if err != nil { return types.ContainerExecOptions{}, err } if isInteractive { if isDetach { return types.ContainerExecOptions{}, errors.New("currently flag -i and -d cannot be specified together (FIXME)") } } if isTerminal { if isDetach { return types.ContainerExecOptions{}, errors.New("currently flag -t and -d cannot be specified together (FIXME)") } } workdir, err := cmd.Flags().GetString("workdir") if err != nil { return types.ContainerExecOptions{}, err } envFile, err := cmd.Flags().GetStringSlice("env-file") if err != nil { return types.ContainerExecOptions{}, err } env, err := cmd.Flags().GetStringArray("env") if err != nil { return types.ContainerExecOptions{}, err } privileged, err := cmd.Flags().GetBool("privileged") if err != nil { return types.ContainerExecOptions{}, err } user, err := cmd.Flags().GetString("user") if err != nil { return types.ContainerExecOptions{}, err } return types.ContainerExecOptions{ GOptions: globalOptions, TTY: isTerminal, Interactive: isInteractive, Detach: isDetach, Workdir: workdir, Env: env, EnvFile: envFile, Privileged: privileged, User: user, }, nil } func execAction(cmd *cobra.Command, args []string) error { options, err := execOptions(cmd) if err != nil { return err } // simulate the behavior of double dash newArg := []string{} if len(args) >= 2 && args[1] == "--" { newArg = append(newArg, args[:1]...) newArg = append(newArg, args[2:]...) args = newArg } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return container.Exec(ctx, client, args, options) } func execShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) == 0 { // show running container names statusFilterFn := func(st containerd.ProcessStatus) bool { return st == containerd.Running } return completion.ContainerNames(cmd, statusFilterFn) } return nil, cobra.ShellCompDirectiveNoFileComp } ================================================ FILE: cmd/nerdctl/container/container_exec_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestExecWithUser(t *testing.T) { testCase := nerdtest.Setup() testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("rm", "-f", data.Identifier()) } testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) data.Labels().Set("container_name", data.Identifier()) } testCase.SubTests = []*test.Case{ { Description: "with no user flag", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Labels().Get("container_name"), "id") }, Expected: test.Expects(0, nil, expect.Contains("uid=0(root) gid=0(root)")), }, { Description: "with --user 1000", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", "--user", "1000", data.Labels().Get("container_name"), "id") }, Expected: test.Expects(0, nil, expect.Contains("uid=1000 gid=0(root)")), }, { Description: "with --user 1000:users", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", "--user", "1000:users", data.Labels().Get("container_name"), "id") }, Expected: test.Expects(0, nil, expect.Contains("uid=1000 gid=100(users)")), }, { Description: "with --user guest", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", "--user", "guest", data.Labels().Get("container_name"), "id") }, Expected: test.Expects(0, nil, expect.Contains("uid=405(guest) gid=100(users)")), }, { Description: "with --user nobody", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", "--user", "nobody", data.Labels().Get("container_name"), "id") }, Expected: test.Expects(0, nil, expect.Contains("uid=65534(nobody) gid=65534(nobody)")), }, { Description: "with --user nobody:users", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", "--user", "nobody:users", data.Labels().Get("container_name"), "id") }, Expected: test.Expects(0, nil, expect.Contains("uid=65534(nobody) gid=100(users)")), }, } testCase.Run(t) } func TestExecTTY(t *testing.T) { const sttyPartialOutput = "speed 38400 baud" testCase := nerdtest.Setup() testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("rm", "-f", data.Identifier()) } testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) data.Labels().Set("container_name", data.Identifier()) } testCase.SubTests = []*test.Case{ { Description: "stty with -it", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("exec", "-it", data.Labels().Get("container_name"), "stty") cmd.WithPseudoTTY() return cmd }, Expected: test.Expects(0, nil, expect.Contains(sttyPartialOutput)), }, { Description: "stty with -t", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("exec", "-t", data.Labels().Get("container_name"), "stty") cmd.WithPseudoTTY() return cmd }, Expected: test.Expects(0, nil, expect.Contains(sttyPartialOutput)), }, { Description: "stty with -i", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("exec", "-i", data.Labels().Get("container_name"), "stty") cmd.WithPseudoTTY() return cmd }, Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, { Description: "stty without params", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("exec", data.Labels().Get("container_name"), "stty") cmd.WithPseudoTTY() return cmd }, Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_exec_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "runtime" "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestExec(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Identifier(), "echo", "success") }, Expected: test.Expects(0, nil, expect.Equals("success\n")), } testCase.Run(t) } func TestExecWithDoubleDash(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Identifier(), "--", "echo", "success") }, Expected: test.Expects(0, nil, expect.Equals("success\n")), } testCase.Run(t) } func TestExecStdin(t *testing.T) { nerdtest.Setup() const testStr = "test-exec-stdin" testCase := &test.Case{ Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("exec", "-i", data.Identifier(), "cat") cmd.Feed(strings.NewReader(testStr)) return cmd }, Expected: test.Expects(0, nil, expect.Equals(testStr)), } testCase.Run(t) } // FYI: https://github.com/containerd/nerdctl/blob/e4b2b6da56555dc29ed66d0fd8e7094ff2bc002d/cmd/nerdctl/run_test.go#L177 func TestExecEnv(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Env: map[string]string{ "CORGE": "corge-value-in-host", "GARPLY": "garply-value-in-host", }, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", "--env", "FOO=foo1,foo2", "--env", "BAR=bar1 bar2", "--env", "BAZ=", "--env", "QUX", // not exported in OS "--env", "QUUX=quux1", "--env", "QUUX=quux2", "--env", "CORGE", // OS exported "--env", "GRAULT=grault_key=grault_value", // value contains `=` char "--env", "GARPLY=", // OS exported "--env", "WALDO=", // not exported in OS data.Identifier(), "env") }, Expected: test.Expects(0, nil, func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(stdout, "\nFOO=foo1,foo2\n"), "got bad FOO") assert.Assert(t, strings.Contains(stdout, "\nBAR=bar1 bar2\n"), "got bad BAR") if runtime.GOOS != "windows" { assert.Assert(t, strings.Contains(stdout, "\nBAZ=\n"), "got bad BAZ") } assert.Assert(t, !strings.Contains(stdout, "QUX"), "got bad QUX (should not be set)") assert.Assert(t, strings.Contains(stdout, "\nQUUX=quux2\n"), "got bad QUUX") assert.Assert(t, strings.Contains(stdout, "\nCORGE=corge-value-in-host\n"), "got bad CORGE") assert.Assert(t, strings.Contains(stdout, "\nGRAULT=grault_key=grault_value\n"), "got bad GRAULT") if runtime.GOOS != "windows" { assert.Assert(t, strings.Contains(stdout, "\nGARPLY=\n"), "got bad GARPLY") assert.Assert(t, strings.Contains(stdout, "\nWALDO=\n"), "got bad WALDO") } }), } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_export.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "fmt" "os" "github.com/mattn/go-isatty" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/container" ) func ExportCommand() *cobra.Command { var exportCommand = &cobra.Command{ Use: "export [OPTIONS] CONTAINER", Args: cobra.ExactArgs(1), Short: "Export a containers filesystem as a tar archive", Long: "Export a containers filesystem as a tar archive", RunE: exportAction, ValidArgsFunction: exportShellComplete, SilenceUsage: true, SilenceErrors: true, } exportCommand.Flags().StringP("output", "o", "", "Write to a file, instead of STDOUT") return exportCommand } func exportAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } if len(args) == 0 { return fmt.Errorf("requires at least 1 argument") } output, err := cmd.Flags().GetString("output") if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() writer := cmd.OutOrStdout() if output != "" { f, err := os.OpenFile(output, os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return err } defer f.Close() writer = f } else { if isatty.IsTerminal(os.Stdout.Fd()) { return fmt.Errorf("cowardly refusing to save to a terminal. Use the -o flag or redirect") } } options := types.ContainerExportOptions{ Stdout: writer, GOptions: globalOptions, } return container.Export(ctx, client, args[0], options) } func exportShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show container names return completion.ContainerNames(cmd, nil) } ================================================ FILE: cmd/nerdctl/container/container_export_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "archive/tar" "io" "os" "path/filepath" "runtime" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) // validateExportedTar checks that the tar file exists and contains /bin/busybox func validateExportedTar(outFile string) test.Comparator { return func(stdout string, t tig.T) { // Check if the tar file was created _, err := os.Stat(outFile) assert.Assert(t, !os.IsNotExist(err), "exported tar file %s was not created", outFile) // Open and read the tar file to check for /bin/busybox file, err := os.Open(outFile) assert.NilError(t, err, "failed to open tar file %s", outFile) defer file.Close() tarReader := tar.NewReader(file) busyboxFound := false for { header, err := tarReader.Next() if err == io.EOF { break } assert.NilError(t, err, "failed to read tar entry") if header.Name == "bin/busybox" || header.Name == "./bin/busybox" { busyboxFound = true break } } assert.Assert(t, busyboxFound, "exported tar file %s does not contain /bin/busybox", outFile) t.Log("Export validation passed: tar file exists and contains /bin/busybox") } } func TestExportStoppedContainer(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("export is not supported on Windows") } testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { identifier := data.Identifier("container") helpers.Ensure("create", "--name", identifier, testutil.CommonImage) data.Labels().Set("cID", identifier) data.Labels().Set("outFile", filepath.Join(os.TempDir(), identifier+".tar")) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("container", "rm", "-f", data.Labels().Get("cID")) helpers.Anyhow("rm", "-f", data.Labels().Get("cID")) os.Remove(data.Labels().Get("outFile")) } testCase.SubTests = []*test.Case{ { Description: "export command succeeds", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("export", "-o", data.Labels().Get("outFile"), data.Labels().Get("cID")) }, Expected: test.Expects(0, nil, nil), }, { Description: "tar file exists and has content", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { // Use a simple command that always succeeds to trigger the validation return helpers.Custom("echo", "validating tar file") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: validateExportedTar(data.Labels().Get("outFile")), } }, }, } testCase.Run(t) } func TestExportRunningContainer(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("export is not supported on Windows") } testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { identifier := data.Identifier("container") helpers.Ensure("run", "-d", "--name", identifier, testutil.CommonImage, "sleep", nerdtest.Infinity) data.Labels().Set("cID", identifier) data.Labels().Set("outFile", filepath.Join(os.TempDir(), identifier+".tar")) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Labels().Get("cID")) os.Remove(data.Labels().Get("outFile")) } testCase.SubTests = []*test.Case{ { Description: "export command succeeds", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("export", "-o", data.Labels().Get("outFile"), data.Labels().Get("cID")) }, Expected: test.Expects(0, nil, nil), }, { Description: "tar file exists and has content", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { // Use a simple command that always succeeds to trigger the validation return helpers.Custom("echo", "validating tar file") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: validateExportedTar(data.Labels().Get("outFile")), } }, }, } testCase.Run(t) } func TestExportNonexistentContainer(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("export is not supported on Windows") } testCase := nerdtest.Setup() testCase.Command = test.Command("export", "nonexistent-container") testCase.Expected = test.Expects(1, nil, nil) testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_health_check.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "context" "fmt" "github.com/spf13/cobra" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/container" "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" ) // HealthCheckCommand returns a cobra command for `nerdctl container healthcheck` func HealthCheckCommand() *cobra.Command { var healthCheckCommand = &cobra.Command{ Use: "healthcheck [flags] CONTAINER", Short: "Execute the health check command in a container", Args: cobra.ExactArgs(1), RunE: healthCheckAction, ValidArgsFunction: healthCheckShellComplete, SilenceUsage: true, SilenceErrors: true, } return healthCheckCommand } func healthCheckAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() containerID := args[0] walker := &containerwalker.ContainerWalker{ Client: client, OnFound: func(ctx context.Context, found containerwalker.Found) error { if found.MatchCount > 1 { return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) } return container.HealthCheck(ctx, client, found.Container) }, } n, err := walker.Walk(ctx, containerID) if err != nil { return err } else if n == 0 { return fmt.Errorf("no such container %s", containerID) } return nil } func healthCheckShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.ContainerNames(cmd, func(status containerd.ProcessStatus) bool { return status == containerd.Running }) } ================================================ FILE: cmd/nerdctl/container/container_health_check_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "encoding/json" "errors" "fmt" "strings" "testing" "time" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/healthcheck" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestContainerHealthCheckBasic(t *testing.T) { testCase := nerdtest.Setup() // Docker CLI does not provide a standalone healthcheck command. testCase.Require = require.Not(nerdtest.Docker) // Skip systemd tests in rootless environment to bypass dbus permission issues if rootlessutil.IsRootless() { t.Skip("systemd healthcheck tests are skipped in rootless environment") } testCase.SubTests = []*test.Case{ { Description: "Container does not exist", Command: test.Command("container", "healthcheck", "non-existent"), Expected: test.Expects(1, []error{errors.New("no such container non-existent")}, nil), }, { Description: "Missing health check config", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("container", "healthcheck", data.Identifier()) }, Expected: test.Expects(1, []error{errors.New("container has no health check configured")}, nil), }, { Description: "Basic health check success", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--health-cmd", "echo healthy", "--health-interval", "45s", "--health-timeout", "30s", testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("container", "healthcheck", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health debug, _ := json.MarshalIndent(h, "", " ") t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state to be present") assert.Equal(t, healthcheck.Healthy, h.Status) assert.Equal(t, 0, h.FailingStreak) assert.Assert(t, len(h.Log) > 0, "expected at least one health check log entry") }), } }, }, { Description: "Health check on stopped container", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--health-cmd", "echo healthy", "--health-interval", "3s", testutil.CommonImage, "sleep", "2") nerdtest.EnsureContainerStarted(helpers, data.Identifier()) helpers.Ensure("stop", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("container", "healthcheck", data.Identifier()) }, Expected: test.Expects(1, []error{errors.New("container is not running (status: stopped)")}, nil), }, { Description: "Health check without task", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("create", "--name", data.Identifier(), "--health-cmd", "echo healthy", testutil.CommonImage, "sleep", nerdtest.Infinity) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("container", "healthcheck", data.Identifier()) }, Expected: test.Expects(1, []error{errors.New("failed to get container task: no running task found")}, nil), }, } testCase.Run(t) } func TestContainerHealthCheckDefaults(t *testing.T) { testCase := nerdtest.Setup() // Docker CLI does not provide a standalone healthcheck command. testCase.Require = require.Not(nerdtest.Docker) // Skip systemd tests in rootless environment to bypass dbus permission issues if rootlessutil.IsRootless() { t.Skip("systemd healthcheck tests are skipped in rootless environment") } testCase.SubTests = []*test.Case{ { Description: "Health check applies default values when not explicitly set", Setup: func(data test.Data, helpers test.Helpers) { // Create container with only --health-cmd, no other health flags helpers.Ensure("run", "-d", "--name", data.Identifier(), "--health-cmd", "echo healthy", testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("inspect", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) // Parse the healthcheck config from container labels hcLabel := inspect.Config.Labels["nerdctl/healthcheck"] assert.Assert(t, hcLabel != "", "expected healthcheck label to be present") var hc healthcheck.Healthcheck err := json.Unmarshal([]byte(hcLabel), &hc) assert.NilError(t, err, "failed to parse healthcheck config") // Verify default values are applied assert.Equal(t, hc.Interval, 30*time.Second, "expected default interval of 30s") assert.Equal(t, hc.Timeout, 30*time.Second, "expected default timeout of 30s") assert.Equal(t, hc.Retries, 3, "expected default retries of 3") assert.Equal(t, hc.StartPeriod, 0*time.Second, "expected default start period of 0s") // Verify the command was set correctly assert.DeepEqual(t, hc.Test, []string{"CMD-SHELL", "echo healthy"}) }), } }, }, { Description: "CLI flags override default values correctly", Setup: func(data test.Data, helpers test.Helpers) { // Create container with custom health flags that override defaults helpers.Ensure("run", "-d", "--name", data.Identifier(), "--health-cmd", "echo custom", "--health-interval", "45s", "--health-timeout", "15s", "--health-retries", "5", "--health-start-period", "10s", testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("inspect", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) // Parse the healthcheck config from container labels hcLabel := inspect.Config.Labels["nerdctl/healthcheck"] assert.Assert(t, hcLabel != "", "expected healthcheck label to be present") var hc healthcheck.Healthcheck err := json.Unmarshal([]byte(hcLabel), &hc) assert.NilError(t, err, "failed to parse healthcheck config") // Verify CLI overrides are applied (not defaults) assert.Equal(t, hc.Interval, 45*time.Second, "expected custom interval of 45s") assert.Equal(t, hc.Timeout, 15*time.Second, "expected custom timeout of 15s") assert.Equal(t, hc.Retries, 5, "expected custom retries of 5") assert.Equal(t, hc.StartPeriod, 10*time.Second, "expected custom start period of 10s") // Verify the command was set correctly assert.DeepEqual(t, hc.Test, []string{"CMD-SHELL", "echo custom"}) }), } }, }, { Description: "No defaults applied when no healthcheck is configured", Setup: func(data test.Data, helpers test.Helpers) { // Create container without any health flags helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("inspect", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) // Verify no healthcheck label is present hcLabel := inspect.Config.Labels["nerdctl/healthcheck"] assert.Equal(t, hcLabel, "", "expected no healthcheck label when no healthcheck is configured") // Verify no health state assert.Assert(t, inspect.State.Health == nil, "expected no health state when no healthcheck is configured") }), } }, }, } testCase.Run(t) } func TestContainerHealthCheckAdvance(t *testing.T) { testCase := nerdtest.Setup() // Docker CLI does not provide a standalone healthcheck command. testCase.Require = require.Not(nerdtest.Docker) // Skip systemd tests in rootless environment to bypass dbus permission issues if rootlessutil.IsRootless() { t.Skip("systemd healthcheck tests are skipped in rootless environment") } testCase.SubTests = []*test.Case{ { Description: "Health check timeout scenario", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--health-cmd", "sleep 10", "--health-timeout", "2s", "--health-interval", "1s", testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("container", "healthcheck", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health debug, _ := json.MarshalIndent(h, "", " ") t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Assert(t, h.FailingStreak >= 1, "expected at least one failing streak") assert.Assert(t, len(inspect.State.Health.Log) > 0, "expected health log to have entries") last := inspect.State.Health.Log[0] assert.Equal(t, -1, last.ExitCode) }), } }, }, { Description: "Health check failing streak behavior", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--health-cmd", "exit 1", "--health-interval", "1s", "--health-retries", "2", testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { // Run healthcheck twice to ensure failing streak for i := 0; i < 2; i++ { helpers.Ensure("container", "healthcheck", data.Identifier()) time.Sleep(2 * time.Second) } return helpers.Command("inspect", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health debug, _ := json.MarshalIndent(h, "", " ") t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Unhealthy) assert.Assert(t, h.FailingStreak >= 1, "expected atleast one FailingStreak") }), } }, }, { Description: "Health check with start period", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--health-cmd", "exit 1", "--health-interval", "1s", "--health-start-period", "60s", "--health-retries", "2", testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("container", "healthcheck", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health debug, _ := json.MarshalIndent(h, "", " ") t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Starting) assert.Equal(t, h.FailingStreak, 0) }), } }, }, { Description: "Health check with invalid command", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--health-cmd", "not-a-real-cmd", "--health-interval", "1s", "--health-retries", "1", testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("container", "healthcheck", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health debug, _ := json.MarshalIndent(h, "", " ") t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Unhealthy) assert.Assert(t, h.FailingStreak >= 1, "expected at least one failing streak") }), } }, }, { Description: "No healthcheck flag disables health status", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--no-healthcheck", testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("inspect", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) assert.Assert(t, inspect.State.Health == nil, "expected health to be nil with --no-healthcheck") }), } }, }, { Description: "Healthcheck using CMD-SHELL format", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--health-cmd", "echo shell-format", "--health-interval", "1s", testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("container", "healthcheck", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All(func(_ string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health debug, _ := json.MarshalIndent(h, "", " ") t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Healthy) assert.Assert(t, len(h.Log) > 0) assert.Assert(t, strings.Contains(h.Log[0].Output, "shell-format")) }), } }, }, { Description: "Health check uses container environment variables", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--env", "MYVAR=test-value", "--health-cmd", "echo $MYVAR", "--health-interval", "1s", "--health-timeout", "1s", testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("container", "healthcheck", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health debug, _ := json.MarshalIndent(h, "", " ") t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Healthy) assert.Assert(t, h.FailingStreak == 0) assert.Assert(t, strings.Contains(h.Log[0].Output, "test"), "expected health log output to contain 'test'") }), } }, }, { Description: "Health check respects container WorkingDir", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--workdir", "/tmp", "--health-cmd", "pwd", "--health-interval", "1s", "--health-timeout", "1s", testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("container", "healthcheck", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health debug, _ := json.MarshalIndent(h, "", " ") t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Healthy) assert.Equal(t, h.FailingStreak, 0) assert.Assert(t, strings.Contains(h.Log[0].Output, "/tmp"), "expected health log output to contain '/tmp'") }), } }, }, { Description: "Healthcheck emits large output repeatedly", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--health-cmd", "yes X | head -c 60000", "--health-interval", "1s", "--health-timeout", "2s", testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { for i := 0; i < 3; i++ { helpers.Ensure("container", "healthcheck", data.Identifier()) time.Sleep(2 * time.Second) } return helpers.Command("inspect", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All(func(_ string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health debug, _ := json.MarshalIndent(h, "", " ") t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Healthy) assert.Assert(t, len(h.Log) >= 3, "expected at least 3 health log entries") for _, log := range h.Log { assert.Assert(t, len(log.Output) >= 1024, fmt.Sprintf("each output should be >= 1024 bytes, was: %s", log.Output)) } }), } }, }, { Description: "Health log in inspect keeps only the latest 5 entries", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--health-cmd", "exit 1", "--health-interval", "1s", "--health-retries", "1", testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { for i := 0; i < 7; i++ { helpers.Ensure("container", "healthcheck", data.Identifier()) time.Sleep(1 * time.Second) } return helpers.Command("inspect", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All(func(_ string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health debug, _ := json.MarshalIndent(h, "", " ") t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Unhealthy) assert.Assert(t, len(h.Log) <= 5, "expected health log to contain at most 5 entries") }), } }, }, { Description: "Healthcheck with large output gets truncated in health log", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--health-cmd", "yes X | head -c 1048576", // 1MB output "--health-interval", "1s", "--health-timeout", "2s", testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("container", "healthcheck", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All(func(_ string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health debug, _ := json.MarshalIndent(h, "", " ") t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Healthy) assert.Equal(t, h.FailingStreak, 0) assert.Assert(t, len(h.Log) >= 1, "expected at least one log entry") output := h.Log[0].Output assert.Assert(t, strings.HasSuffix(output, "[truncated]"), "expected output to be truncated with '[truncated]'") }), } }, }, { Description: "Health status transitions from healthy to unhealthy after retries", Setup: func(data test.Data, helpers test.Helpers) { containerName := data.Identifier() helpers.Ensure("run", "-d", "--name", containerName, "--health-cmd", "exit 1", "--health-timeout", "10s", "--health-retries", "3", testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { for i := 0; i < 4; i++ { helpers.Ensure("container", "healthcheck", data.Identifier()) time.Sleep(2 * time.Second) } return helpers.Command("inspect", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health debug, _ := json.MarshalIndent(h, "", " ") t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Unhealthy) assert.Assert(t, h.FailingStreak >= 3) }), } }, }, { Description: "Failed healthchecks in start-period do not change status", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--health-cmd", "ls /foo || exit 1", "--health-retries", "2", "--health-start-period", "30s", // long enough to stay in "starting" testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { // Run healthcheck 3 times (should still be in start period) for i := 0; i < 3; i++ { helpers.Ensure("container", "healthcheck", data.Identifier()) time.Sleep(1 * time.Second) } return helpers.Command("inspect", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health debug, _ := json.MarshalIndent(h, "", " ") t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Starting) assert.Equal(t, h.FailingStreak, 0, "failing streak should not increase during start period") }), } }, }, { Description: "Successful healthcheck in start-period sets status to healthy", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--health-cmd", "ls || exit 1", "--health-retries", "2", testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { helpers.Ensure("container", "healthcheck", data.Identifier()) time.Sleep(1 * time.Second) return helpers.Command("inspect", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health debug, _ := json.MarshalIndent(h, "", " ") t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Healthy, "expected healthy status even during start-period") assert.Equal(t, h.FailingStreak, 0) }), } }, }, } testCase.Run(t) } func TestHealthCheck_SystemdIntegration_Basic(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.Not(nerdtest.Docker) // Skip systemd tests in rootless environment to bypass dbus permission issues if rootlessutil.IsRootless() { t.Skip("systemd healthcheck tests are skipped in rootless environment") } testCase.SubTests = []*test.Case{ { Description: "Basic healthy container with systemd-triggered healthcheck", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--health-cmd", "echo healthy", "--health-interval", "2s", testutil.CommonImage, "sleep", "30") nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { // Ensure proper cleanup of systemd units helpers.Anyhow("stop", data.Identifier()) helpers.Anyhow("rm", "-f", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All(func(stdout string, t tig.T) { var h *healthcheck.Health // Poll up to 5 times for health status maxAttempts := 5 var finalStatus string for i := 0; i < maxAttempts; i++ { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h = inspect.State.Health assert.Assert(t, h != nil, "expected health state to be present") finalStatus = h.Status // If healthy, break and pass the test if finalStatus == "healthy" { t.Log(fmt.Sprintf("Container became healthy on attempt %d/%d", i+1, maxAttempts)) break } // If unhealthy, fail immediately if finalStatus == "unhealthy" { assert.Assert(t, false, fmt.Sprintf("Container became unhealthy on attempt %d/%d, status: %s", i+1, maxAttempts, finalStatus)) return } // If not the last attempt, wait before retrying if i < maxAttempts-1 { t.Log(fmt.Sprintf("Attempt %d/%d: status is '%s', waiting 1 second before retry", i+1, maxAttempts, finalStatus)) time.Sleep(1 * time.Second) } } if finalStatus != "healthy" { assert.Assert(t, false, fmt.Sprintf("Container did not become healthy after %d attempts, final status: %s", maxAttempts, finalStatus)) return } assert.Assert(t, len(h.Log) > 0, "expected at least one health check log entry") }), } }, }, { Description: "Kill stops healthcheck execution and cleans up systemd timer", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--health-cmd", "echo healthy", "--health-interval", "1s", testutil.CommonImage, "sleep", "30") nerdtest.EnsureContainerStarted(helpers, data.Identifier()) helpers.Ensure("kill", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { // Container is already killed, just remove it helpers.Anyhow("rm", "-f", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeNoCheck, Output: func(stdout string, t tig.T) { // Get container info for verification inspect := nerdtest.InspectContainer(helpers, data.Identifier()) containerID := inspect.ID h := inspect.State.Health // Verify health state and logs exist assert.Assert(t, h != nil, "expected health state to be present") assert.Assert(t, len(h.Log) > 0, "expected at least one health check log entry") // Ensure systemd timers are removed result := helpers.Custom("systemctl", "list-timers", "--all", "--no-pager") result.Run(&test.Expected{ ExitCode: expect.ExitCodeNoCheck, Output: func(stdout string, _ tig.T) { assert.Assert(t, !strings.Contains(stdout, containerID), "expected nerdctl healthcheck timer for container ID %s to be removed after container stop", containerID) }, }) }, } }, }, { Description: "Remove cleans up systemd timer", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--health-cmd", "echo healthy", "--health-interval", "1s", testutil.CommonImage, "sleep", "30") nerdtest.EnsureContainerStarted(helpers, data.Identifier()) helpers.Ensure("rm", "-f", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { // Container is already removed, no cleanup needed helpers.Anyhow("rm", "-f", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeNoCheck, Output: func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) containerID := inspect.ID // Check systemd timers to ensure cleanup result := helpers.Custom("systemctl", "list-timers", "--all", "--no-pager") result.Run(&test.Expected{ ExitCode: expect.ExitCodeNoCheck, Output: func(stdout string, _ tig.T) { // Verify systemd timer has been cleaned up by checking systemctl output // We check that no timer contains our test identifier assert.Assert(t, !strings.Contains(stdout, containerID), "expected nerdctl healthcheck timer for container ID %s to be removed after container removal", containerID) }, }) }, } }, }, { Description: "Stop cleans up systemd timer", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--health-cmd", "echo healthy", "--health-interval", "1s", testutil.CommonImage, "sleep", "30") nerdtest.EnsureContainerStarted(helpers, data.Identifier()) helpers.Ensure("stop", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { // Container is already stopped, just remove it helpers.Anyhow("rm", "-f", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeNoCheck, Output: func(stdout string, t tig.T) { // Get container info for verification inspect := nerdtest.InspectContainer(helpers, data.Identifier()) containerID := inspect.ID // Ensure systemd timers are removed result := helpers.Custom("systemctl", "list-timers", "--all", "--no-pager") result.Run(&test.Expected{ ExitCode: expect.ExitCodeNoCheck, Output: func(stdout string, _ tig.T) { assert.Assert(t, !strings.Contains(stdout, containerID), "expected nerdctl healthcheck timer for container ID %s to be removed after container stop", containerID) }, }) }, } }, }, } testCase.Run(t) } func TestHealthCheck_GlobalFlags(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.Not(nerdtest.Docker) // Skip systemd tests in rootless environment to bypass dbus permission issues if rootlessutil.IsRootless() { t.Skip("systemd healthcheck tests are skipped in rootless environment") } testCase.SubTests = []*test.Case{ { Description: "Healthcheck works with custom namespace flag", Setup: func(data test.Data, helpers test.Helpers) { // Create container in custom namespace with healthcheck helpers.Ensure("--namespace=healthcheck-test", "run", "-d", "--name", data.Identifier(), "--health-cmd", "echo healthy", "--health-interval", "2s", testutil.CommonImage, "sleep", "30") // Wait a bit to ensure container is running (can't use EnsureContainerStarted with custom namespace) time.Sleep(1 * time.Second) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("--namespace=healthcheck-test", "rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { // Wait a bit for healthcheck to run time.Sleep(3 * time.Second) // Verify container is accessible in the custom namespace return helpers.Command("--namespace=healthcheck-test", "inspect", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { var inspectResults []dockercompat.Container err := json.Unmarshal([]byte(stdout), &inspectResults) assert.NilError(t, err, "failed to parse inspect output") assert.Assert(t, len(inspectResults) > 0, "expected at least one container in inspect results") inspect := inspectResults[0] h := inspect.State.Health assert.Assert(t, h != nil, "expected health state to be present") assert.Assert(t, h.Status == healthcheck.Healthy || h.Status == healthcheck.Starting, "expected health status to be healthy or starting, got: %s", h.Status) assert.Assert(t, len(h.Log) > 0, "expected at least one health check log entry") }, } }, }, { Description: "Healthcheck works correctly with namespace after container restart", Setup: func(data test.Data, helpers test.Helpers) { // Create container in custom namespace helpers.Ensure("--namespace=restart-test", "run", "-d", "--name", data.Identifier(), "--health-cmd", "echo healthy", "--health-interval", "2s", testutil.CommonImage, "sleep", "60") // Wait a bit to ensure container is running (can't use EnsureContainerStarted with custom namespace) time.Sleep(1 * time.Second) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("--namespace=restart-test", "rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { // Wait for initial healthcheck time.Sleep(3 * time.Second) // Stop and restart the container helpers.Ensure("--namespace=restart-test", "stop", data.Identifier()) helpers.Ensure("--namespace=restart-test", "start", data.Identifier()) // Wait a bit to ensure container is running after restart time.Sleep(1 * time.Second) // Wait for healthcheck to run after restart time.Sleep(3 * time.Second) return helpers.Command("--namespace=restart-test", "inspect", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { // Parse the inspect JSON output directly since we're in a custom namespace var inspectResults []dockercompat.Container err := json.Unmarshal([]byte(stdout), &inspectResults) assert.NilError(t, err, "failed to parse inspect output") assert.Assert(t, len(inspectResults) > 0, "expected at least one container in inspect results") inspect := inspectResults[0] h := inspect.State.Health assert.Assert(t, h != nil, "expected health state after restart") assert.Assert(t, h.Status == healthcheck.Healthy || h.Status == healthcheck.Starting, "expected health status to be healthy or starting after restart, got: %s", h.Status) assert.Assert(t, len(h.Log) > 0, "expected health check logs after restart") }, } }, }, } testCase.Run(t) } func TestHealthCheck_SystemdIntegration_Advanced(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.Not(nerdtest.Docker) // Skip systemd tests in rootless environment to bypass dbus permission issues if rootlessutil.IsRootless() { t.Skip("systemd healthcheck tests are skipped in rootless environment") } testCase.SubTests = []*test.Case{ { // Tests that CreateTimer() successfully creates systemd timer units and // RemoveTransientHealthCheckFiles() properly cleans up units when container stops. Description: "Systemd timer unit creation and cleanup", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--health-cmd", "echo healthy", "--health-interval", "1s", testutil.CommonImage, "sleep", "30") nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("inspect", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All(func(stdout string, t tig.T) { // Get container ID and check systemd timer containerInspect := nerdtest.InspectContainer(helpers, data.Identifier()) containerID := containerInspect.ID // Check systemd timer result := helpers.Custom("systemctl", "list-timers", "--all", "--no-pager") result.Run(&test.Expected{ ExitCode: expect.ExitCodeNoCheck, Output: func(stdout string, _ tig.T) { // Verify that a timer exists for this specific container assert.Assert(t, strings.Contains(stdout, containerID), "expected to find nerdctl healthcheck timer containing container ID: %s", containerID) }, }) // Stop container and verify cleanup helpers.Ensure("stop", data.Identifier()) // Check that timer is gone result = helpers.Custom("systemctl", "list-timers", "--all", "--no-pager") result.Run(&test.Expected{ ExitCode: expect.ExitCodeNoCheck, Output: func(stdout string, _ tig.T) { assert.Assert(t, !strings.Contains(stdout, containerID), "expected nerdctl healthcheck timer for container ID %s to be removed after container stop", containerID) }, }) }), } }, }, { Description: "Container restart recreates systemd timer", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--health-cmd", "echo restart-test", "--health-interval", "2s", testutil.CommonImage, "sleep", "60") nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { // Get container ID for verification containerInspect := nerdtest.InspectContainer(helpers, data.Identifier()) containerID := containerInspect.ID // Step 1: Verify timer exists initially result := helpers.Custom("systemctl", "list-timers", "--all", "--no-pager") result.Run(&test.Expected{ ExitCode: expect.ExitCodeNoCheck, Output: func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(stdout, containerID), "expected timer for container %s to exist initially", containerID) }, }) // Step 2: Stop container helpers.Ensure("stop", data.Identifier()) // Step 3: Verify timer is removed after stop result = helpers.Custom("systemctl", "list-timers", "--all", "--no-pager") result.Run(&test.Expected{ ExitCode: expect.ExitCodeNoCheck, Output: func(stdout string, t tig.T) { assert.Assert(t, !strings.Contains(stdout, containerID), "expected timer for container %s to be removed after stop", containerID) }, }) // Step 4: Restart container helpers.Ensure("start", data.Identifier()) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) // Step 5: Verify timer is recreated after restart - this is our final verification return helpers.Custom("systemctl", "list-timers", "--all", "--no-pager") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeNoCheck, Output: func(stdout string, t tig.T) { containerInspect := nerdtest.InspectContainer(helpers, data.Identifier()) containerID := containerInspect.ID assert.Assert(t, strings.Contains(stdout, containerID), "expected timer for container %s to be recreated after restart", containerID) }, } }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_inspect.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "fmt" "github.com/spf13/cobra" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/container" "github.com/containerd/nerdctl/v2/pkg/formatter" ) func inspectCommand() *cobra.Command { cmd := &cobra.Command{ Use: "inspect [flags] CONTAINER [CONTAINER, ...]", Short: "Display detailed information on one or more containers.", Long: "Hint: set `--mode=native` for showing the full output", Args: cobra.MinimumNArgs(1), RunE: inspectAction, ValidArgsFunction: containerInspectShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().String("mode", "dockercompat", `Inspect mode, "dockercompat" for Docker-compatible output, "native" for containerd-native output`) cmd.Flags().BoolP("size", "s", false, "Display total file sizes") cmd.RegisterFlagCompletionFunc("mode", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"dockercompat", "native"}, cobra.ShellCompDirectiveNoFileComp }) cmd.Flags().StringP("format", "f", "", "Format the output using the given Go template, e.g, '{{json .}}'") cmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"json"}, cobra.ShellCompDirectiveNoFileComp }) return cmd } var validModeType = map[string]bool{ "native": true, "dockercompat": true, } func InspectOptions(cmd *cobra.Command) (opt types.ContainerInspectOptions, err error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return } mode, err := cmd.Flags().GetString("mode") if err != nil { return } if len(mode) > 0 && !validModeType[mode] { err = fmt.Errorf("%q is not a valid value for --mode", mode) return } format, err := cmd.Flags().GetString("format") if err != nil { return } size, err := cmd.Flags().GetBool("size") if err != nil { return } return types.ContainerInspectOptions{ GOptions: globalOptions, Format: format, Mode: mode, Size: size, Stdout: cmd.OutOrStdout(), }, nil } func inspectAction(cmd *cobra.Command, args []string) error { opt, err := InspectOptions(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), opt.GOptions.Namespace, opt.GOptions.Address) if err != nil { return err } defer cancel() entries, err := container.Inspect(ctx, client, args, opt) if err != nil { return err } // Display if len(entries) > 0 { if formatErr := formatter.FormatSlice(opt.Format, opt.Stdout, entries); formatErr != nil { log.G(ctx).Error(formatErr) } } return err } func containerInspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show container names return completion.ContainerNames(cmd, nil) } ================================================ FILE: cmd/nerdctl/container/container_inspect_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "encoding/json" "fmt" "os" "slices" "strings" "testing" "github.com/docker/go-connections/nat" "gotest.tools/v3/assert" "github.com/containerd/continuity/testutil/loopback" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/infoutil" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/labels" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestContainerInspectContainsPortConfig(t *testing.T) { testContainer := testutil.Identifier(t) base := testutil.NewBase(t) defer base.Cmd("rm", "-f", testContainer).Run() base.Cmd("run", "-d", "--name", testContainer, "-p", "8080:80", testutil.NginxAlpineImage).AssertOK() inspect := base.InspectContainer(testContainer) inspect80TCP := (*inspect.NetworkSettings.Ports)["80/tcp"] expected := nat.PortBinding{ HostIP: "0.0.0.0", HostPort: "8080", } assert.Equal(base.T, expected, inspect80TCP[0]) } func TestContainerInspectContainsMounts(t *testing.T) { testContainer := testutil.Identifier(t) base := testutil.NewBase(t) testVolume := testutil.Identifier(t) defer base.Cmd("volume", "rm", "-f", testVolume).Run() base.Cmd("volume", "create", "--label", "tag=testVolume", testVolume).AssertOK() inspectVolume := base.InspectVolume(testVolume) namedVolumeSource := inspectVolume.Mountpoint defer base.Cmd("rm", "-f", testContainer).Run() base.Cmd("run", "-d", "--privileged", "--name", testContainer, "--network", "none", "-v", "/anony-vol", "--tmpfs", "/app1:size=64m", "--mount", "type=bind,src=/tmp,dst=/app2,ro", "--mount", fmt.Sprintf("type=volume,src=%s,dst=/app3,readonly=false", testVolume), testutil.NginxAlpineImage).AssertOK() inspect := base.InspectContainer(testContainer) // convert array to map to get by key of Destination actual := make(map[string]dockercompat.MountPoint) for i := range inspect.Mounts { actual[inspect.Mounts[i].Destination] = inspect.Mounts[i] } t.Logf("actual in TestContainerInspectContainsMounts: %+v", actual) const localDriver = "local" expected := []struct { dest string mountPoint dockercompat.MountPoint }{ // anonymous volume { dest: "/anony-vol", mountPoint: dockercompat.MountPoint{ Type: "volume", Name: "", Source: "", // source of anonymous volume is a generated path, so here will not check it. Destination: "/anony-vol", Driver: localDriver, RW: true, }, }, // bind { dest: "/app2", mountPoint: dockercompat.MountPoint{ Type: "bind", Name: "", Source: "/tmp", Destination: "/app2", Driver: "", RW: false, }, }, // named volume { dest: "/app3", mountPoint: dockercompat.MountPoint{ Type: "volume", Name: testVolume, Source: namedVolumeSource, Destination: "/app3", Driver: localDriver, RW: true, }, }, } for i := range expected { testCase := expected[i] t.Logf("test volume[dest=%q]", testCase.dest) mountPoint, ok := actual[testCase.dest] assert.Assert(base.T, ok) assert.Equal(base.T, testCase.mountPoint.Type, mountPoint.Type) assert.Equal(base.T, testCase.mountPoint.Driver, mountPoint.Driver) assert.Equal(base.T, testCase.mountPoint.RW, mountPoint.RW) assert.Equal(base.T, testCase.mountPoint.Destination, mountPoint.Destination) if testCase.mountPoint.Source != "" { assert.Equal(base.T, testCase.mountPoint.Source, mountPoint.Source) } if testCase.mountPoint.Name != "" { assert.Equal(base.T, testCase.mountPoint.Name, mountPoint.Name) } } } func TestContainerInspectContainsLabel(t *testing.T) { t.Parallel() testContainer := testutil.Identifier(t) base := testutil.NewBase(t) defer base.Cmd("rm", "-f", testContainer).Run() base.Cmd("run", "-d", "--name", testContainer, "--label", "foo=foo", "--label", "bar=bar", testutil.NginxAlpineImage).AssertOK() base.EnsureContainerStarted(testContainer) inspect := base.InspectContainer(testContainer) lbs := inspect.Config.Labels assert.Equal(base.T, "foo", lbs["foo"]) assert.Equal(base.T, "bar", lbs["bar"]) } func TestContainerInspectContainsInternalLabel(t *testing.T) { testutil.DockerIncompatible(t) t.Parallel() testContainer := testutil.Identifier(t) base := testutil.NewBase(t) defer base.Cmd("rm", "-f", testContainer).Run() base.Cmd("run", "-d", "--name", testContainer, "--mount", "type=bind,src=/tmp,dst=/app,readonly=false,bind-propagation=rprivate", testutil.NginxAlpineImage).AssertOK() base.EnsureContainerStarted(testContainer) inspect := base.InspectContainer(testContainer) lbs := inspect.Config.Labels // TODO: add more internal labels testcases labelMount := lbs[labels.Mounts] expectedLabelMount := "[{\"Type\":\"bind\",\"Source\":\"/tmp\",\"Destination\":\"/app\",\"Mode\":\"rprivate,rbind\",\"RW\":true,\"Propagation\":\"rprivate\"}]" assert.Equal(base.T, expectedLabelMount, labelMount) } func TestContainerInspectConfigImage(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Description: "Container inspect contains Config.Image field", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.AlpineImage, "sleep", "infinity") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("inspect", data.Identifier()) }, Expected: test.Expects(0, nil, func(stdout string, t tig.T) { var containers []dockercompat.Container err := json.Unmarshal([]byte(stdout), &containers) assert.NilError(t, err, "Unable to unmarshal output\n") assert.Equal(t, 1, len(containers), "Expected exactly one container in inspect output") container := containers[0] assert.Assert(t, container.Config != nil, "container Config should not be nil") assert.Assert(t, container.Config.Image != "", "Config.Image should not be empty") }), } testCase.Run(t) } func TestContainerInspectState(t *testing.T) { t.Parallel() testContainer := testutil.Identifier(t) base := testutil.NewBase(t) type testCase struct { name, containerName, cmd string want dockercompat.ContainerState } // nerdctl: run error produces a nil Task, so the Status is empty because Status comes from Task. // docker : run error gives => `Status=created` as in docker there is no a separation between container and Task. errStatus := "" if nerdtest.IsDocker() { errStatus = "created" } testCases := []testCase{ { name: "inspect State with error", containerName: fmt.Sprintf("%s-fail", testContainer), cmd: "aa", want: dockercompat.ContainerState{ Error: "executable file not found in $PATH", Status: errStatus, }, }, { name: "inspect State without error", containerName: fmt.Sprintf("%s-success", testContainer), cmd: "ls", want: dockercompat.ContainerState{ Error: "", Status: "exited", }, }, } for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { defer base.Cmd("rm", "-f", tc.containerName).Run() if tc.want.Error != "" { base.Cmd("run", "--name", tc.containerName, testutil.AlpineImage, tc.cmd).AssertFail() } else { base.Cmd("run", "--name", tc.containerName, testutil.AlpineImage, tc.cmd).AssertOK() } inspect := base.InspectContainer(tc.containerName) assert.Assert(t, strings.Contains(inspect.State.Error, tc.want.Error), fmt.Sprintf("expected: %s, actual: %s", tc.want.Error, inspect.State.Error)) assert.Equal(base.T, inspect.State.Status, tc.want.Status) }) } } func TestContainerInspectHostConfig(t *testing.T) { testContainer := testutil.Identifier(t) if rootlessutil.IsRootless() && infoutil.CgroupsVersion() == "1" { t.Skip("test skipped for rootless containers on cgroup v1") } base := testutil.NewBase(t) defer base.Cmd("rm", "-f", testContainer).Run() // Run a container with various HostConfig options base.Cmd("run", "-d", "--name", testContainer, "--cpuset-cpus", "0-1", "--cpuset-mems", "0", "--cpu-shares", "1024", "--cpu-quota", "100000", "--group-add", "1000", "--group-add", "2000", "--add-host", "host1:10.0.0.1", "--add-host", "host2:10.0.0.2", "--ipc", "host", "--memory", "512m", "--read-only", "--shm-size", "256m", "--uts", "host", "--runtime", "io.containerd.runc.v2", testutil.AlpineImage, "sleep", "infinity").AssertOK() inspect := base.InspectContainer(testContainer) assert.Equal(t, "0-1", inspect.HostConfig.CPUSetCPUs) assert.Equal(t, "0", inspect.HostConfig.CPUSetMems) assert.Equal(t, uint64(1024), inspect.HostConfig.CPUShares) assert.Equal(t, int64(100000), inspect.HostConfig.CPUQuota) assert.Assert(t, slices.Contains(inspect.HostConfig.GroupAdd, "1000"), "Expected '1000' to be in GroupAdd") assert.Assert(t, slices.Contains(inspect.HostConfig.GroupAdd, "2000"), "Expected '2000' to be in GroupAdd") expectedExtraHosts := []string{"host1:10.0.0.1", "host2:10.0.0.2"} assert.DeepEqual(t, expectedExtraHosts, inspect.HostConfig.ExtraHosts) assert.Equal(t, "host", inspect.HostConfig.IpcMode) assert.Equal(t, int64(536870912), inspect.HostConfig.Memory) assert.Equal(t, int64(1073741824), inspect.HostConfig.MemorySwap) assert.Equal(t, true, inspect.HostConfig.ReadonlyRootfs) assert.Equal(t, "host", inspect.HostConfig.UTSMode) assert.Equal(t, int64(268435456), inspect.HostConfig.ShmSize) } func TestContainerInspectHostConfigDefaults(t *testing.T) { testContainer := testutil.Identifier(t) base := testutil.NewBase(t) defer base.Cmd("rm", "-f", testContainer).Run() var hc hostConfigValues // Hostconfig default values differ with Docker. // This is because we directly retrieve the configured values instead of using preset defaults. if nerdtest.IsDocker() { hc.Driver = "" hc.GroupAddSize = 0 hc.ShmSize = int64(67108864) // Docker default 64M hc.Runtime = "runc" } else { hc.GroupAddSize = 10 hc.Driver = "json-file" hc.ShmSize = int64(0) hc.Runtime = "io.containerd.runc.v2" } // Run a container without specifying HostConfig options base.Cmd("run", "-d", "--name", testContainer, testutil.AlpineImage, "sleep", "infinity").AssertOK() inspect := base.InspectContainer(testContainer) t.Logf("HostConfig in TestContainerInspectHostConfigDefaults: %+v", inspect.HostConfig) assert.Equal(t, "", inspect.HostConfig.CPUSetCPUs) assert.Equal(t, "", inspect.HostConfig.CPUSetMems) assert.Equal(t, uint16(0), inspect.HostConfig.BlkioWeight) assert.Equal(t, 0, len(inspect.HostConfig.BlkioWeightDevice)) assert.Equal(t, 0, len(inspect.HostConfig.BlkioDeviceReadBps)) assert.Equal(t, 0, len(inspect.HostConfig.BlkioDeviceReadIOps)) assert.Equal(t, 0, len(inspect.HostConfig.BlkioDeviceWriteBps)) assert.Equal(t, 0, len(inspect.HostConfig.BlkioDeviceWriteIOps)) assert.Equal(t, uint64(0), inspect.HostConfig.CPUShares) assert.Equal(t, int64(0), inspect.HostConfig.CPUQuota) assert.Equal(t, hc.GroupAddSize, len(inspect.HostConfig.GroupAdd)) assert.Equal(t, 0, len(inspect.HostConfig.ExtraHosts)) assert.Equal(t, "private", inspect.HostConfig.IpcMode) assert.Equal(t, hc.Driver, inspect.HostConfig.LogConfig.Driver) assert.Equal(t, int64(0), inspect.HostConfig.Memory) assert.Equal(t, int64(0), inspect.HostConfig.MemorySwap) assert.Equal(t, bool(false), inspect.HostConfig.OomKillDisable) assert.Equal(t, bool(false), inspect.HostConfig.ReadonlyRootfs) assert.Equal(t, "", inspect.HostConfig.UTSMode) assert.Equal(t, hc.ShmSize, inspect.HostConfig.ShmSize) assert.Equal(t, hc.Runtime, inspect.HostConfig.Runtime) assert.Equal(t, 0, len(inspect.HostConfig.Devices)) // Sysctls can be empty or contain "net.ipv4.ip_unprivileged_port_start" depending on the environment. got := len(inspect.HostConfig.Sysctls) if got != 0 && got != 1 { t.Fatalf("unexpected number of Sysctls entries: %d (want 0 or 1)", got) } } func TestContainerInspectHostConfigDNS(t *testing.T) { testContainer := testutil.Identifier(t) base := testutil.NewBase(t) defer base.Cmd("rm", "-f", testContainer).Run() // Run a container with DNS options base.Cmd("run", "-d", "--name", testContainer, "--dns", "8.8.8.8", "--dns", "1.1.1.1", "--dns-search", "example.com", "--dns-search", "test.local", "--dns-option", "ndots:5", "--dns-option", "timeout:3", testutil.AlpineImage, "sleep", "infinity").AssertOK() inspect := base.InspectContainer(testContainer) // Check DNS servers expectedDNSServers := []string{"8.8.8.8", "1.1.1.1"} assert.DeepEqual(t, expectedDNSServers, inspect.HostConfig.DNS) // Check DNS search domains expectedDNSSearch := []string{"example.com", "test.local"} assert.DeepEqual(t, expectedDNSSearch, inspect.HostConfig.DNSSearch) // Check DNS options expectedDNSOptions := []string{"ndots:5", "timeout:3"} assert.DeepEqual(t, expectedDNSOptions, inspect.HostConfig.DNSOptions) } func TestContainerInspectHostConfigDNSDefaults(t *testing.T) { testContainer := testutil.Identifier(t) base := testutil.NewBase(t) defer base.Cmd("rm", "-f", testContainer).Run() // Run a container without specifying DNS options base.Cmd("run", "-d", "--name", testContainer, testutil.AlpineImage, "sleep", "infinity").AssertOK() inspect := base.InspectContainer(testContainer) // Check that DNS settings are empty by default assert.Equal(t, 0, len(inspect.HostConfig.DNS)) assert.Equal(t, 0, len(inspect.HostConfig.DNSSearch)) assert.Equal(t, 0, len(inspect.HostConfig.DNSOptions)) } func TestContainerInspectHostConfigPID(t *testing.T) { testContainer1 := testutil.Identifier(t) + "-container1" testContainer2 := testutil.Identifier(t) + "-container2" base := testutil.NewBase(t) defer base.Cmd("rm", "-f", testContainer1, testContainer2).Run() // Run the first container base.Cmd("run", "-d", "--name", testContainer1, testutil.AlpineImage, "sleep", "infinity").AssertOK() containerID1 := strings.TrimSpace(base.Cmd("inspect", "-f", "{{.Id}}", testContainer1).Out()) var hc hostConfigValues if nerdtest.IsDocker() { hc.PidMode = "container:" + containerID1 } else { hc.PidMode = containerID1 } base.Cmd("run", "-d", "--name", testContainer2, "--pid", fmt.Sprintf("container:%s", testContainer1), testutil.AlpineImage, "sleep", "infinity").AssertOK() inspect := base.InspectContainer(testContainer2) assert.Equal(t, hc.PidMode, inspect.HostConfig.PidMode) } func TestContainerInspectHostConfigPIDDefaults(t *testing.T) { testContainer := testutil.Identifier(t) base := testutil.NewBase(t) defer base.Cmd("rm", "-f", testContainer).Run() base.Cmd("run", "-d", "--name", testContainer, testutil.AlpineImage, "sleep", "infinity").AssertOK() inspect := base.InspectContainer(testContainer) assert.Equal(t, "", inspect.HostConfig.PidMode) } func TestContainerInspectDevices(t *testing.T) { testContainer := testutil.Identifier(t) base := testutil.NewBase(t) defer base.Cmd("rm", "-f", testContainer).Run() if rootlessutil.IsRootless() && infoutil.CgroupsVersion() == "1" { t.Skip("test skipped for rootless containers on cgroup v1") } // Create a temporary directory dir, err := os.MkdirTemp(t.TempDir(), "device-dir") if err != nil { t.Fatal(err) } if nerdtest.IsDocker() { dir = "/dev/zero" } // Run the container with the directory mapped as a device base.Cmd("run", "-d", "--name", testContainer, "--device", dir+":/dev/xvda", testutil.AlpineImage, "sleep", "infinity").AssertOK() inspect := base.InspectContainer(testContainer) expectedDevices := []dockercompat.DeviceMapping{ { PathOnHost: dir, PathInContainer: "/dev/xvda", CgroupPermissions: "rwm", }, } assert.DeepEqual(t, expectedDevices, inspect.HostConfig.Devices) } func TestContainerInspectBlkioSettings(t *testing.T) { testutil.DockerIncompatible(t) testContainer := testutil.Identifier(t) // Some of the blkio settings are not supported in cgroup v1. // So skip this test if running on cgroup v1 if infoutil.CgroupsVersion() == "1" { t.Skip("test skipped for rootless containers or if running with cgroup v1") } if rootlessutil.IsRootless() { t.Skip("test requires root privilege to create a dummy device") } // See https://github.com/containerd/nerdctl/issues/4185 // It is unclear if this is truly a kernel version problem, a runc issue, or a distro (EL9) issue. // For now, disable the test unless on a recent kernel. testutil.RequireKernelVersion(t, ">= 6.0.0-0") lo, err := loopback.New(4096) if err != nil { err = fmt.Errorf("cannot find a loop device: %w", err) t.Fatal(err) } defer lo.Close() base := testutil.NewBase(t) defer base.Cmd("rm", "-f", testContainer).AssertOK() const ( weight = 500 readBps = 1048576 readIops = 1000 writeBps = 2097152 writeIops = 2000 ) base.Cmd("run", "-d", "--name", testContainer, "--blkio-weight", fmt.Sprintf("%d", weight), "--blkio-weight-device", fmt.Sprintf("%s:%d", lo.Device, weight), "--device-read-bps", fmt.Sprintf("%s:%d", lo.Device, readBps), "--device-read-iops", fmt.Sprintf("%s:%d", lo.Device, readIops), "--device-write-bps", fmt.Sprintf("%s:%d", lo.Device, writeBps), "--device-write-iops", fmt.Sprintf("%s:%d", lo.Device, writeIops), testutil.AlpineImage, "sleep", "infinity").AssertOK() inspect := base.InspectContainer(testContainer) assert.Equal(t, uint16(weight), inspect.HostConfig.BlkioWeight) assert.Equal(t, 1, len(inspect.HostConfig.BlkioWeightDevice)) assert.Equal(t, lo.Device, inspect.HostConfig.BlkioWeightDevice[0].Path) assert.Equal(t, uint16(weight), inspect.HostConfig.BlkioWeightDevice[0].Weight) assert.Equal(t, 1, len(inspect.HostConfig.BlkioDeviceReadBps)) assert.Equal(t, uint64(readBps), inspect.HostConfig.BlkioDeviceReadBps[0].Rate) assert.Equal(t, 1, len(inspect.HostConfig.BlkioDeviceWriteBps)) assert.Equal(t, uint64(writeBps), inspect.HostConfig.BlkioDeviceWriteBps[0].Rate) assert.Equal(t, 1, len(inspect.HostConfig.BlkioDeviceReadIOps)) assert.Equal(t, uint64(readIops), inspect.HostConfig.BlkioDeviceReadIOps[0].Rate) assert.Equal(t, 1, len(inspect.HostConfig.BlkioDeviceWriteIOps)) assert.Equal(t, uint64(writeIops), inspect.HostConfig.BlkioDeviceWriteIOps[0].Rate) } func TestContainerInspectUser(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Description: "Container inspect contains User", Require: nerdtest.Build, Setup: func(data test.Data, helpers test.Helpers) { dockerfile := fmt.Sprintf(` FROM %s RUN groupadd -r test && useradd -r -g test test USER test `, testutil.UbuntuImage) data.Temp().Save(dockerfile, "Dockerfile") helpers.Ensure("build", "-t", data.Identifier(), data.Temp().Path()) helpers.Ensure("create", "--name", data.Identifier(), "--user", "test", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("inspect", "--format", "{{.Config.User}}", data.Identifier()) }, Expected: test.Expects(0, nil, expect.Equals("test\n")), } testCase.Run(t) } type hostConfigValues struct { Driver string ShmSize int64 PidMode string GroupAddSize int Runtime string } ================================================ FILE: cmd/nerdctl/container/container_inspect_windows_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "encoding/json" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestInspectProcessContainerContainsLabel(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { containerName := testutil.Identifier(t) data.Labels().Set("containerName", containerName) helpers.Ensure("run", "-d", "--name", containerName, "--label", "foo=foo", "--label", "bar=bar", testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, containerName) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { containerName := data.Labels().Get("containerName") helpers.Anyhow("rm", "-f", containerName) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { containerName := data.Labels().Get("containerName") return helpers.Command("inspect", containerName) } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, Output: func(stdout string, t tig.T) { var dc []dockercompat.Container err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err) assert.Equal(t, 1, len(dc)) assert.Equal(t, "foo", dc[0].Config.Labels["foo"]) assert.Equal(t, "bar", dc[0].Config.Labels["bar"]) }, } } testCase.Run(t) } func TestInspectHyperVContainerContainsLabel(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = nerdtest.HyperV testCase.Setup = func(data test.Data, helpers test.Helpers) { containerName := testutil.Identifier(t) data.Labels().Set("containerName", containerName) helpers.Ensure("run", "-d", "--name", containerName, "--isolation", "hyperv", "--label", "foo=foo", "--label", "bar=bar", testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, containerName) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { containerName := data.Labels().Get("containerName") helpers.Anyhow("rm", "-f", containerName) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { containerName := data.Labels().Get("containerName") return helpers.Command("inspect", containerName) } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, Output: func(stdout string, t tig.T) { var dc []dockercompat.Container err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err) assert.Equal(t, 1, len(dc)) //check with HCS if the container is ineed a VM isHypervContainer, err := testutil.HyperVContainer(dc[0]) assert.NilError(t, err) assert.Equal(t, true, isHypervContainer) assert.Equal(t, "foo", dc[0].Config.Labels["foo"]) assert.Equal(t, "bar", dc[0].Config.Labels["bar"]) }, } } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_kill.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "github.com/spf13/cobra" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/container" ) func KillCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "kill [flags] CONTAINER [CONTAINER, ...]", Short: "Kill one or more running containers", Args: cobra.MinimumNArgs(1), RunE: killAction, ValidArgsFunction: killShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().StringP("signal", "s", "KILL", "Signal to send to the container") return cmd } func killAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } killSignal, err := cmd.Flags().GetString("signal") if err != nil { return err } options := types.ContainerKillOptions{ GOptions: globalOptions, KillSignal: killSignal, Stdout: cmd.OutOrStdout(), Stderr: cmd.ErrOrStderr(), } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return container.Kill(ctx, client, args, options) } func killShellComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { // show non-stopped container names statusFilterFn := func(st containerd.ProcessStatus) bool { return st != containerd.Stopped && st != containerd.Created && st != containerd.Unknown } return completion.ContainerNames(cmd, statusFilterFn) } ================================================ FILE: cmd/nerdctl/container/container_kill_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "fmt" "strings" "testing" "github.com/coreos/go-iptables/iptables" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" iptablesutil "github.com/containerd/nerdctl/v2/pkg/testutil/iptables" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) // TestKillCleanupForwards runs a container that exposes a port and then kill it. // The test checks that the kill command effectively clean up // the iptables forwards creted from the run. func TestKillCleanupForwards(t *testing.T) { const ( hostPort = 9999 testContainerName = "ngx" ) base := testutil.NewBase(t) defer func() { base.Cmd("rm", "-f", testContainerName).Run() }() // skip if rootless if rootlessutil.IsRootless() { t.Skip("pkg/testutil/iptables does not support rootless") } ipt, err := iptables.New() assert.NilError(t, err) containerID := base.Cmd("run", "-d", "--restart=no", "--name", testContainerName, "-p", fmt.Sprintf("127.0.0.1:%d:80", hostPort), testutil.NginxAlpineImage).Run().Stdout() containerID = strings.TrimSuffix(containerID, "\n") containerIP := base.Cmd("inspect", "-f", "'{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}'", testContainerName).Run().Stdout() containerIP = strings.ReplaceAll(containerIP, "'", "") containerIP = strings.TrimSuffix(containerIP, "\n") // define iptables chain name depending on the target (docker/nerdctl) var chain string if nerdtest.IsDocker() { chain = "DOCKER" } else { redirectChain := "CNI-HOSTPORT-DNAT" chain = iptablesutil.GetRedirectedChain(t, ipt, redirectChain, testutil.Namespace, containerID) } assert.Equal(t, iptablesutil.ForwardExists(t, ipt, chain, containerIP, hostPort), true) base.Cmd("kill", testContainerName).AssertOK() assert.Equal(t, iptablesutil.ForwardExists(t, ipt, chain, containerIP, hostPort), false) } ================================================ FILE: cmd/nerdctl/container/container_list.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "bytes" "errors" "fmt" "io" "text/tabwriter" "text/template" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/container" "github.com/containerd/nerdctl/v2/pkg/formatter" ) func PsCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "ps", Args: cobra.NoArgs, Short: "List containers", RunE: psAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().BoolP("all", "a", false, "Show all containers (default shows just running)") cmd.Flags().IntP("last", "n", -1, "Show n last created containers (includes all states)") cmd.Flags().BoolP("latest", "l", false, "Show the latest created container (includes all states)") cmd.Flags().Bool("no-trunc", false, "Don't truncate output") cmd.Flags().BoolP("quiet", "q", false, "Only display container IDs") cmd.Flags().BoolP("size", "s", false, "Display total file sizes") // Alias "-f" is reserved for "--filter" cmd.Flags().String("format", "", "Format the output using the given Go template, e.g, '{{json .}}', 'wide'") cmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"json", "table", "wide"}, cobra.ShellCompDirectiveNoFileComp }) cmd.Flags().StringSliceP("filter", "f", nil, "Filter matches containers based on given conditions. When specifying the condition 'status', it filters all containers") return cmd } func processOptions(cmd *cobra.Command) (types.ContainerListOptions, FormattingAndPrintingOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ContainerListOptions{}, FormattingAndPrintingOptions{}, err } all, err := cmd.Flags().GetBool("all") if err != nil { return types.ContainerListOptions{}, FormattingAndPrintingOptions{}, err } latest, err := cmd.Flags().GetBool("latest") if err != nil { return types.ContainerListOptions{}, FormattingAndPrintingOptions{}, err } lastN, err := cmd.Flags().GetInt("last") if err != nil { return types.ContainerListOptions{}, FormattingAndPrintingOptions{}, err } if lastN == -1 && latest { lastN = 1 } filters, err := cmd.Flags().GetStringSlice("filter") if err != nil { return types.ContainerListOptions{}, FormattingAndPrintingOptions{}, err } noTrunc, err := cmd.Flags().GetBool("no-trunc") if err != nil { return types.ContainerListOptions{}, FormattingAndPrintingOptions{}, err } trunc := !noTrunc quiet, err := cmd.Flags().GetBool("quiet") if err != nil { return types.ContainerListOptions{}, FormattingAndPrintingOptions{}, err } format, err := cmd.Flags().GetString("format") if err != nil { return types.ContainerListOptions{}, FormattingAndPrintingOptions{}, err } size := false if !quiet { size, err = cmd.Flags().GetBool("size") if err != nil { return types.ContainerListOptions{}, FormattingAndPrintingOptions{}, err } } return types.ContainerListOptions{ GOptions: globalOptions, All: all, LastN: lastN, Truncate: trunc, Size: size || (format == "wide" && !quiet), Filters: filters, }, FormattingAndPrintingOptions{ Stdout: cmd.OutOrStdout(), Quiet: quiet, Format: format, Size: size, }, nil } func psAction(cmd *cobra.Command, args []string) error { clOpts, fpOpts, err := processOptions(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), clOpts.GOptions.Namespace, clOpts.GOptions.Address) if err != nil { return err } defer cancel() containers, err := container.List(ctx, client, clOpts) if err != nil { return err } return formatAndPrintContainerInfo(containers, fpOpts) } // FormattingAndPrintingOptions specifies options for formatting and printing of `nerdctl (container) list`. type FormattingAndPrintingOptions struct { Stdout io.Writer // Only display container IDs. Quiet bool // Format the output using the given Go template (e.g., '{{json .}}', 'table', 'wide'). Format string // Display total file sizes. Size bool } func formatAndPrintContainerInfo(containers []container.ListItem, options FormattingAndPrintingOptions) error { w := options.Stdout var ( wide bool tmpl *template.Template ) switch options.Format { case "", "table": w = tabwriter.NewWriter(w, 4, 8, 4, ' ', 0) if !options.Quiet { printHeader := "CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES" if options.Size { printHeader += "\tSIZE" } fmt.Fprintln(w, printHeader) } case "raw": return errors.New("unsupported format: \"raw\"") case "wide": w = tabwriter.NewWriter(w, 4, 8, 4, ' ', 0) if !options.Quiet { fmt.Fprintln(w, "CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES\tRUNTIME\tPLATFORM\tSIZE") wide = true } default: if options.Quiet { return errors.New("format and quiet must not be specified together") } var err error tmpl, err = formatter.ParseTemplate(options.Format) if err != nil { return err } } for _, c := range containers { if tmpl != nil { var b bytes.Buffer if err := tmpl.Execute(&b, &c); err != nil { return err } if _, err := fmt.Fprintln(w, b.String()); err != nil { return err } } else if options.Quiet { if _, err := fmt.Fprintln(w, c.ID); err != nil { return err } } else { format := "%s\t%s\t%s\t%s\t%s\t%s\t%s" args := []interface{}{ c.ID, c.Image, c.Command, formatter.TimeSinceInHuman(c.CreatedAt), c.Status, c.Ports, c.Names, } if wide { format += "\t%s\t%s\t%s\n" args = append(args, c.Runtime, c.Platform, c.Size) } else if options.Size { format += "\t%s\n" args = append(args, c.Size) } else { format += "\n" } if _, err := fmt.Fprintf(w, format, args...); err != nil { return err } } } if f, ok := w.(formatter.Flusher); ok { return f.Flush() } return nil } ================================================ FILE: cmd/nerdctl/container/container_list_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "errors" "fmt" "os" "slices" "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/formatter" "github.com/containerd/nerdctl/v2/pkg/strutil" "github.com/containerd/nerdctl/v2/pkg/tabutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) type psTestContainer struct { name string labels map[string]string volumes []string network string } // When keepAlive is false, the container will exit immediately with status 1. func preparePsTestContainer(t *testing.T, identity string, keepAlive bool) (*testutil.Base, psTestContainer) { base := testutil.NewBase(t) base.Cmd("pull", "--quiet", testutil.CommonImage).AssertOK() testContainerName := testutil.Identifier(t) + identity rwVolName := testContainerName + "-rw" // A container can mount named and anonymous volumes rwDir, err := os.MkdirTemp(t.TempDir(), "rw") if err != nil { t.Fatal(err) } base.Cmd("network", "create", testContainerName).AssertOK() t.Cleanup(func() { base.Cmd("rm", "-f", testContainerName).AssertOK() base.Cmd("volume", "rm", "-f", rwVolName).Run() base.Cmd("network", "rm", testContainerName).Run() os.RemoveAll(rwDir) }) // A container can have multiple labels. // Therefore, this test container has multiple labels to check it. testLabels := make(map[string]string) keys := []string{ testutil.Identifier(t) + identity, testutil.Identifier(t) + identity, } // fill the value of testLabels for _, k := range keys { testLabels[k] = k } base.Cmd("volume", "create", rwVolName).AssertOK() mnt1 := fmt.Sprintf("%s:/%s_mnt1", rwDir, identity) mnt2 := fmt.Sprintf("%s:/%s_mnt3", rwVolName, identity) args := []string{ "run", "-d", "--name", testContainerName, "--label", formatter.FormatLabels(testLabels), "-v", mnt1, "-v", mnt2, "--net", testContainerName, } if keepAlive { args = append(args, testutil.CommonImage, "top") } else { args = append(args, "--restart=no", testutil.CommonImage, "false") } base.Cmd(args...).AssertOK() if keepAlive { base.EnsureContainerStarted(testContainerName) } else { base.EnsureContainerExited(testContainerName, 1) } // dd if=/dev/zero of=test_file bs=1M count=25 // let the container occupy 25MiB space. if keepAlive { base.Cmd("exec", testContainerName, "dd", "if=/dev/zero", "of=/test_file", "bs=1M", "count=25").AssertOK() } volumes := []string{} volumes = append(volumes, strings.Split(mnt1, ":")...) volumes = append(volumes, strings.Split(mnt2, ":")...) return base, psTestContainer{ name: testContainerName, labels: testLabels, volumes: volumes, network: testContainerName, } } func TestContainerList(t *testing.T) { base, testContainer := preparePsTestContainer(t, "list", true) // hope there are no tests running parallel base.Cmd("ps", "-n", "1", "-s").AssertOutWithFunc(func(stdout string) error { // An example of nerdctl/docker ps -n 1 -s // CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES SIZE // be8d386c991e docker.io/library/busybox:latest "top" 1 second ago Up c1 16.0 KiB (virtual 1.3 MiB) lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) < 2 { return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) } tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES\tSIZE") err := tab.ParseHeader(lines[0]) if err != nil { return fmt.Errorf("failed to parse header: %v", err) } container, _ := tab.ReadRow(lines[1], "NAMES") assert.Equal(t, container, testContainer.name) image, _ := tab.ReadRow(lines[1], "IMAGE") assert.Equal(t, image, testutil.CommonImage) size, _ := tab.ReadRow(lines[1], "SIZE") // there is some difference between nerdctl and docker in calculating the size of the container expectedSize := "26.2MB (virtual " if !nerdtest.IsDocker() { expectedSize = "25.0 MiB (virtual " } if !strings.Contains(size, expectedSize) { return fmt.Errorf("expect container size %s, but got %s", expectedSize, size) } return nil }) } func TestContainerListWideMode(t *testing.T) { testutil.DockerIncompatible(t) base, testContainer := preparePsTestContainer(t, "listWithMode", true) // hope there are no tests running parallel base.Cmd("ps", "-n", "1", "--format", "wide").AssertOutWithFunc(func(stdout string) error { // An example of nerdctl ps --format wide // CONTAINER ID IMAGE PLATFORM COMMAND CREATED STATUS PORTS NAMES RUNTIME SIZE // 17181f208b61 docker.io/library/busybox:latest linux/amd64 "top" About an hour ago Up busybox-17181 io.containerd.runc.v2 16.0 KiB (virtual 1.3 MiB) lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) < 2 { return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) } tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES\tRUNTIME\tPLATFORM\tSIZE") err := tab.ParseHeader(lines[0]) if err != nil { return fmt.Errorf("failed to parse header: %v", err) } container, _ := tab.ReadRow(lines[1], "NAMES") assert.Equal(t, container, testContainer.name) image, _ := tab.ReadRow(lines[1], "IMAGE") assert.Equal(t, image, testutil.CommonImage) runtime, _ := tab.ReadRow(lines[1], "RUNTIME") assert.Equal(t, runtime, "io.containerd.runc.v2") size, _ := tab.ReadRow(lines[1], "SIZE") expectedSize := "25.0 MiB (virtual " if !strings.Contains(size, expectedSize) { return fmt.Errorf("expect container size %s, but got %s", expectedSize, size) } return nil }) } func TestContainerListWithLabels(t *testing.T) { base, testContainer := preparePsTestContainer(t, "listWithLabels", true) // hope there are no tests running parallel base.Cmd("ps", "-n", "1", "--format", "{{.Labels}}").AssertOutWithFunc(func(stdout string) error { // An example of nerdctl ps --format "{{.Labels}}" // key1=value1,key2=value2,key3=value3 lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) != 1 { return fmt.Errorf("expected 1 line, got %d", len(lines)) } // check labels using map // 1. the results has no guarantee to show the same order. // 2. the results has no guarantee to show only configured labels. labelsMap, err := strutil.ParseCSVMap(lines[0]) if err != nil { return fmt.Errorf("failed to parse labels: %v", err) } for i := range testContainer.labels { if value, ok := labelsMap[i]; ok { assert.Equal(t, value, testContainer.labels[i]) } } return nil }) } func TestContainerListWithNames(t *testing.T) { base, testContainer := preparePsTestContainer(t, "listWithNames", true) // hope there are no tests running parallel base.Cmd("ps", "-n", "1", "--format", "{{.Names}}").AssertOutWithFunc(func(stdout string) error { // An example of nerdctl ps --format "{{.Names}}" lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) != 1 { return fmt.Errorf("expected 1 line, got %d", len(lines)) } assert.Equal(t, lines[0], testContainer.name) return nil }) } func TestContainerListWithFilter(t *testing.T) { base, testContainerA := preparePsTestContainer(t, "listWithFilterA", true) _, testContainerB := preparePsTestContainer(t, "listWithFilterB", true) _, testContainerC := preparePsTestContainer(t, "listWithFilterC", false) base.Cmd("ps", "--filter", "name="+testContainerA.name).AssertOutWithFunc(func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) < 2 { return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) } tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") err := tab.ParseHeader(lines[0]) if err != nil { return fmt.Errorf("failed to parse header: %v", err) } containerName, _ := tab.ReadRow(lines[1], "NAMES") assert.Equal(t, containerName, testContainerA.name) id, _ := tab.ReadRow(lines[1], "CONTAINER ID") base.Cmd("ps", "-q", "--filter", "id="+id).AssertOutWithFunc(func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) != 1 { return fmt.Errorf("expected 1 line, got %d", len(lines)) } if lines[0] != id { return errors.New("failed to filter by id") } return nil }) base.Cmd("ps", "-q", "--filter", "id="+id+id).AssertOutWithFunc(func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) > 0 { for _, line := range lines { if line != "" { return fmt.Errorf("unexpected container found: %s", line) } } } return nil }) base.Cmd("ps", "-q", "--filter", "id=").AssertOutWithFunc(func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) > 0 { for _, line := range lines { if line != "" { return fmt.Errorf("unexpected container found: %s", line) } } } return nil }) return nil }) // should support regexp base.Cmd("ps", "--filter", "name=.*"+testContainerA.name+".*").AssertOutWithFunc(func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) < 2 { return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) } tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") err := tab.ParseHeader(lines[0]) if err != nil { return fmt.Errorf("failed to parse header: %v", err) } containerName, _ := tab.ReadRow(lines[1], "NAMES") assert.Equal(t, containerName, testContainerA.name) return nil }) // fully anchored regexp base.Cmd("ps", "--filter", "name=^"+testContainerA.name+"$").AssertOutWithFunc(func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) < 2 { return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) } tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") err := tab.ParseHeader(lines[0]) if err != nil { return fmt.Errorf("failed to parse header: %v", err) } containerName, _ := tab.ReadRow(lines[1], "NAMES") assert.Equal(t, containerName, testContainerA.name) return nil }) base.Cmd("ps", "-q", "--filter", "name="+testContainerA.name+testContainerA.name).AssertOutWithFunc(func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) > 0 { for _, line := range lines { if line != "" { return fmt.Errorf("unexpected container found: %s", line) } } } return nil }) base.Cmd("ps", "-q", "--filter", "name=").AssertOutWithFunc(func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) == 0 { return errors.New("expect at least 1 container, got 0") } return nil }) base.Cmd("ps", "--filter", "name=listWithFilter").AssertOutWithFunc(func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) < 3 { return fmt.Errorf("expected at least 3 lines, got %d", len(lines)) } tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") err := tab.ParseHeader(lines[0]) if err != nil { return fmt.Errorf("failed to parse header: %v", err) } containerNames := map[string]struct{}{ testContainerA.name: {}, testContainerB.name: {}, } for idx, line := range lines { if idx == 0 { continue } containerName, _ := tab.ReadRow(line, "NAMES") if _, ok := containerNames[containerName]; !ok { return fmt.Errorf("unexpected container %s found", containerName) } } return nil }) // docker filter by id only support full ID no truncate // https://github.com/docker/for-linux/issues/258 // yet nerdctl also support truncate ID base.Cmd("ps", "--no-trunc", "--filter", "since="+testContainerA.name).AssertOutWithFunc(func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) < 2 { return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) } tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") err := tab.ParseHeader(lines[0]) if err != nil { return fmt.Errorf("failed to parse header: %v", err) } var id string for idx, line := range lines { if idx == 0 { continue } containerName, _ := tab.ReadRow(line, "NAMES") if containerName != testContainerB.name { return fmt.Errorf("unexpected container %s found", containerName) } id, _ = tab.ReadRow(line, "CONTAINER ID") } base.Cmd("ps", "--filter", "before="+id).AssertOutWithFunc(func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) < 2 { return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) } tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") err := tab.ParseHeader(lines[0]) if err != nil { return fmt.Errorf("failed to parse header: %v", err) } foundA := false for idx, line := range lines { if idx == 0 { continue } containerName, _ := tab.ReadRow(line, "NAMES") if containerName == testContainerA.name { foundA = true break } } // there are other containers such as **wordpress** could be listed since // their created times are ahead of testContainerB too if !foundA { return fmt.Errorf("expected container %s not found", testContainerA.name) } return nil }) return nil }) // docker filter by id only support full ID no truncate // https://github.com/docker/for-linux/issues/258 // yet nerdctl also support truncate ID base.Cmd("ps", "--no-trunc", "--filter", "before="+testContainerB.name).AssertOutWithFunc(func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) < 2 { return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) } tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") err := tab.ParseHeader(lines[0]) if err != nil { return fmt.Errorf("failed to parse header: %v", err) } foundA := false var id string for idx, line := range lines { if idx == 0 { continue } containerName, _ := tab.ReadRow(line, "NAMES") if containerName == testContainerA.name { foundA = true id, _ = tab.ReadRow(line, "CONTAINER ID") break } } // there are other containers such as **wordpress** could be listed since // their created times are ahead of testContainerB too if !foundA { return fmt.Errorf("expected container %s not found", testContainerA.name) } base.Cmd("ps", "--filter", "since="+id).AssertOutWithFunc(func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) < 2 { return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) } tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") err := tab.ParseHeader(lines[0]) if err != nil { return fmt.Errorf("failed to parse header: %v", err) } for idx, line := range lines { if idx == 0 { continue } containerName, _ := tab.ReadRow(line, "NAMES") if containerName != testContainerB.name { return fmt.Errorf("unexpected container %s found", containerName) } } return nil }) return nil }) for _, testContainer := range []psTestContainer{testContainerA, testContainerB} { for _, volume := range testContainer.volumes { base.Cmd("ps", "--filter", "volume="+volume).AssertOutWithFunc(func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) < 2 { return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) } tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") err := tab.ParseHeader(lines[0]) if err != nil { return fmt.Errorf("failed to parse header: %v", err) } containerName, _ := tab.ReadRow(lines[1], "NAMES") assert.Equal(t, containerName, testContainer.name) return nil }) } } base.Cmd("ps", "--filter", "network="+testContainerA.network).AssertOutWithFunc(func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) < 2 { return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) } tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") err := tab.ParseHeader(lines[0]) if err != nil { return fmt.Errorf("failed to parse header: %v", err) } containerName, _ := tab.ReadRow(lines[1], "NAMES") assert.Equal(t, containerName, testContainerA.name) return nil }) for key, value := range testContainerB.labels { base.Cmd("ps", "--filter", "label="+key+"="+value).AssertOutWithFunc(func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) < 2 { return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) } tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") err := tab.ParseHeader(lines[0]) if err != nil { return fmt.Errorf("failed to parse header: %v", err) } containerNames := map[string]struct{}{ testContainerB.name: {}, } for idx, line := range lines { if idx == 0 { continue } containerName, _ := tab.ReadRow(line, "NAMES") if _, ok := containerNames[containerName]; !ok { return fmt.Errorf("unexpected container %s found", containerName) } } return nil }) } base.Cmd("ps", "-a", "--filter", "exited=1").AssertOutWithFunc(func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) < 2 { return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) } tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") err := tab.ParseHeader(lines[0]) if err != nil { return fmt.Errorf("failed to parse header: %v", err) } containerNames := map[string]struct{}{ testContainerC.name: {}, } for idx, line := range lines { if idx == 0 { continue } containerName, _ := tab.ReadRow(line, "NAMES") if _, ok := containerNames[containerName]; !ok { return fmt.Errorf("unexpected container %s found", containerName) } } return nil }) base.Cmd("ps", "-a", "--filter", "status=exited").AssertOutWithFunc(func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) < 2 { return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) } tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") err := tab.ParseHeader(lines[0]) if err != nil { return fmt.Errorf("failed to parse header: %v", err) } containerNames := map[string]struct{}{ testContainerC.name: {}, } for idx, line := range lines { if idx == 0 { continue } containerName, _ := tab.ReadRow(line, "NAMES") if _, ok := containerNames[containerName]; !ok { return fmt.Errorf("unexpected container %s found", containerName) } } return nil }) // filter container state without option "-a". base.Cmd("ps", "--filter", "status=exited").AssertOutWithFunc(func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) < 2 { return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) } tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") err := tab.ParseHeader(lines[0]) if err != nil { return fmt.Errorf("failed to parse header: %v", err) } containerNames := map[string]struct{}{ testContainerC.name: {}, } for idx, line := range lines { if idx == 0 { continue } containerName, _ := tab.ReadRow(line, "NAMES") if _, ok := containerNames[containerName]; !ok { return fmt.Errorf("unexpected container %s found", containerName) } } return nil }) } func TestContainerListCheckCreatedTime(t *testing.T) { base, _ := preparePsTestContainer(t, "checkCreatedTimeA", true) preparePsTestContainer(t, "checkCreatedTimeB", true) preparePsTestContainer(t, "checkCreatedTimeC", false) preparePsTestContainer(t, "checkCreatedTimeD", false) var createdTimes []string base.Cmd("ps", "--format", "'{{json .CreatedAt}}'", "-a").AssertOutWithFunc(func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) < 4 { return fmt.Errorf("expected at least 4 lines, got %d", len(lines)) } createdTimes = append(createdTimes, lines...) return nil }) slices.Reverse(createdTimes) if !slices.IsSorted(createdTimes) { t.Errorf("expected containers in decending order") } } func TestContainerListStatusFilter(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("create", "--name", data.Identifier("container"), testutil.CommonImage, "echo", "foo") data.Labels().Set("cID", data.Identifier("container")) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("container")) } testCase.SubTests = []*test.Case{ // TODO: Refactor other filter tests { Description: "ps filter with status=created", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("ps", "-a", "--filter", "status=created", "--filter", fmt.Sprintf("name=%s", data.Labels().Get("cID"))) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(stdout, data.Labels().Get("cID")), "No container found with status created") }, } }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_list_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "fmt" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) // https://github.com/containerd/nerdctl/issues/2598 func TestContainerListWithFormatLabel(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Setup: func(data test.Data, helpers test.Helpers) { labelK := "label-key-" + data.Identifier() labelV := "label-value-" + data.Identifier() helpers.Ensure("run", "-d", "--name", data.Identifier(), "--label", labelK+"="+labelV, testutil.CommonImage, "sleep", nerdtest.Infinity) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { labelK := "label-key-" + data.Identifier() return helpers.Command("ps", "-a", "--filter", "label="+labelK, "--format", fmt.Sprintf("{{.Label %q}}", labelK)) //nolint:dupামিটার }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { labelV := "label-value-" + data.Identifier() return test.Expects(0, nil, expect.Equals(labelV+"\n"))(data, helpers) }, } testCase.Run(t) } func TestContainerListWithJsonFormatLabel(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Setup: func(data test.Data, helpers test.Helpers) { labelK := "label-key-" + data.Identifier() labelV := "label-value-" + data.Identifier() helpers.Ensure("run", "-d", "--name", data.Identifier(), "--label", labelK+"="+labelV, testutil.CommonImage, "sleep", nerdtest.Infinity) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { labelK := "label-key-" + data.Identifier() return helpers.Command("ps", "-a", "--filter", "label="+labelK, "--format", "json") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { labelK := "label-key-" + data.Identifier() labelV := "label-value-" + data.Identifier() return test.Expects(0, nil, expect.Contains(fmt.Sprintf("%s=%s", labelK, labelV)))(data, helpers) }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_list_windows_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "fmt" "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/v2/pkg/formatter" "github.com/containerd/nerdctl/v2/pkg/strutil" "github.com/containerd/nerdctl/v2/pkg/tabutil" "github.com/containerd/nerdctl/v2/pkg/testutil" ) type psTestContainer struct { name string labels map[string]string network string } func preparePsTestContainer(t *testing.T, identity string, restart bool, hyperv bool) (*testutil.Base, psTestContainer) { base := testutil.NewBase(t) base.Cmd("pull", "--quiet", testutil.NginxAlpineImage).AssertOK() testContainerName := testutil.Identifier(t) + identity t.Cleanup(func() { base.Cmd("rm", "-f", testContainerName).AssertOK() }) // A container can have multiple labels. // Therefore, this test container has multiple labels to check it. testLabels := make(map[string]string) keys := []string{ testutil.Identifier(t) + identity, testutil.Identifier(t) + identity, } // fill the value of testLabels for _, k := range keys { testLabels[k] = k } args := []string{ "run", "-d", "--name", testContainerName, "--label", formatter.FormatLabels(testLabels), testutil.NginxAlpineImage, } if !restart { args = append(args, "--restart=no") } if hyperv { args = append(args[:3], args[1:]...) args[1], args[2] = "--isolation", "hyperv" } base.Cmd(args...).AssertOK() if restart { base.EnsureContainerStarted(testContainerName) } return base, psTestContainer{ name: testContainerName, labels: testLabels, network: testContainerName, } } func TestListProcessContainer(t *testing.T) { base, testContainer := preparePsTestContainer(t, "list", true, false) // hope there are no tests running parallel base.Cmd("ps", "-n", "1", "-s").AssertOutWithFunc(func(stdout string) error { // An example of nerdctl/docker ps -n 1 -s // CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES SIZE // be8d386c991e docker.io/library/busybox:latest "top" 1 second ago Up c1 16.0 KiB (virtual 1.3 MiB) lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) < 2 { return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) } tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES\tSIZE") err := tab.ParseHeader(lines[0]) if err != nil { return fmt.Errorf("failed to parse header: %v", err) } container, _ := tab.ReadRow(lines[1], "NAMES") assert.Equal(t, container, testContainer.name) image, _ := tab.ReadRow(lines[1], "IMAGE") assert.Equal(t, image, testutil.NginxAlpineImage) size, _ := tab.ReadRow(lines[1], "SIZE") // there is some difference between nerdctl and docker in calculating the size of the container expectedSize := "36.0 MiB (virtual " if !strings.Contains(size, expectedSize) { return fmt.Errorf("expect container size %s, but got %s", expectedSize, size) } return nil }) } func TestListHyperVContainer(t *testing.T) { if !testutil.HyperVSupported() { t.Skip("HyperV is not enabled, skipping test") } base, testContainer := preparePsTestContainer(t, "list", true, true) inspect := base.InspectContainer(testContainer.name) //check with HCS if the container is ineed a VM isHypervContainer, err := testutil.HyperVContainer(inspect) if err != nil { t.Fatalf("unable to list HCS containers: %s", err) } assert.Assert(t, isHypervContainer, true) // hope there are no tests running parallel base.Cmd("ps", "-n", "1", "-s").AssertOutWithFunc(func(stdout string) error { // An example of nerdctl/docker ps -n 1 -s // CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES SIZE // be8d386c991e docker.io/library/busybox:latest "top" 1 second ago Up c1 16.0 KiB (virtual 1.3 MiB) lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) < 2 { return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) } tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES\tSIZE") err := tab.ParseHeader(lines[0]) if err != nil { return fmt.Errorf("failed to parse header: %v", err) } container, _ := tab.ReadRow(lines[1], "NAMES") assert.Equal(t, container, testContainer.name) image, _ := tab.ReadRow(lines[1], "IMAGE") assert.Equal(t, image, testutil.NginxAlpineImage) size, _ := tab.ReadRow(lines[1], "SIZE") // there is some difference between nerdctl and docker in calculating the size of the container expectedSize := "72.0 MiB (virtual " if !strings.Contains(size, expectedSize) { return fmt.Errorf("expect container size %s, but got %s", expectedSize, size) } return nil }) } func TestListProcessContainerWideMode(t *testing.T) { testutil.DockerIncompatible(t) base, testContainer := preparePsTestContainer(t, "listWithMode", true, false) // hope there are no tests running parallel base.Cmd("ps", "-n", "1", "--format", "wide").AssertOutWithFunc(func(stdout string) error { // An example of nerdctl ps --format wide // CONTAINER ID IMAGE PLATFORM COMMAND CREATED STATUS PORTS NAMES RUNTIME SIZE // 17181f208b61 docker.io/library/busybox:latest linux/amd64 "top" About an hour ago Up busybox-17181 io.containerd.runc.v2 16.0 KiB (virtual 1.3 MiB) lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) < 2 { return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) } tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES\tRUNTIME\tPLATFORM\tSIZE") err := tab.ParseHeader(lines[0]) if err != nil { return fmt.Errorf("failed to parse header: %v", err) } container, _ := tab.ReadRow(lines[1], "NAMES") assert.Equal(t, container, testContainer.name) image, _ := tab.ReadRow(lines[1], "IMAGE") assert.Equal(t, image, testutil.NginxAlpineImage) runtime, _ := tab.ReadRow(lines[1], "RUNTIME") assert.Equal(t, runtime, "io.containerd.runhcs.v1") size, _ := tab.ReadRow(lines[1], "SIZE") expectedSize := "36.0 MiB (virtual " if !strings.Contains(size, expectedSize) { return fmt.Errorf("expect container size %s, but got %s", expectedSize, size) } return nil }) } func TestListProcessContainerWithLabels(t *testing.T) { base, testContainer := preparePsTestContainer(t, "listWithLabels", true, false) // hope there are no tests running parallel base.Cmd("ps", "-n", "1", "--format", "{{.Labels}}").AssertOutWithFunc(func(stdout string) error { // An example of nerdctl ps --format "{{.Labels}}" // key1=value1,key2=value2,key3=value3 lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) != 1 { return fmt.Errorf("expected 1 line, got %d", len(lines)) } // check labels using map // 1. the results has no guarantee to show the same order. // 2. the results has no guarantee to show only configured labels. labelsMap, err := strutil.ParseCSVMap(lines[0]) if err != nil { return fmt.Errorf("failed to parse labels: %v", err) } for i := range testContainer.labels { if value, ok := labelsMap[i]; ok { assert.Equal(t, value, testContainer.labels[i]) } } return nil }) } ================================================ FILE: cmd/nerdctl/container/container_logs.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "fmt" "strconv" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/container" ) func LogsCommand() *cobra.Command { const shortUsage = "Fetch the logs of a container. Expected to be used with 'nerdctl run -d'." const longUsage = `Fetch the logs of a container. The following containers are supported: - Containers created with 'nerdctl run -d'. The log is currently empty for containers created without '-d'. - Containers created with 'nerdctl compose'. - Containers created with Kubernetes (EXPERIMENTAL). ` var cmd = &cobra.Command{ Use: "logs [flags] CONTAINER", Args: helpers.IsExactArgs(1), Short: shortUsage, Long: longUsage, RunE: logsAction, ValidArgsFunction: logsShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().BoolP("follow", "f", false, "Follow log output") cmd.Flags().BoolP("timestamps", "t", false, "Show timestamps") cmd.Flags().StringP("tail", "n", "all", "Number of lines to show from the end of the logs") cmd.Flags().String("since", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)") cmd.Flags().String("until", "", "Show logs before a timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)") cmd.Flags().Bool("details", false, "Show extra details provided to logs") return cmd } func logsOptions(cmd *cobra.Command) (types.ContainerLogsOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ContainerLogsOptions{}, err } follow, err := cmd.Flags().GetBool("follow") if err != nil { return types.ContainerLogsOptions{}, err } tailArg, err := cmd.Flags().GetString("tail") if err != nil { return types.ContainerLogsOptions{}, err } var tail uint if tailArg != "" { tail, err = getTailArgAsUint(tailArg) if err != nil { return types.ContainerLogsOptions{}, err } } timestamps, err := cmd.Flags().GetBool("timestamps") if err != nil { return types.ContainerLogsOptions{}, err } since, err := cmd.Flags().GetString("since") if err != nil { return types.ContainerLogsOptions{}, err } until, err := cmd.Flags().GetString("until") if err != nil { return types.ContainerLogsOptions{}, err } details, err := cmd.Flags().GetBool("details") if err != nil { return types.ContainerLogsOptions{}, err } return types.ContainerLogsOptions{ Stdout: cmd.OutOrStdout(), Stderr: cmd.OutOrStderr(), GOptions: globalOptions, Follow: follow, Timestamps: timestamps, Tail: tail, Since: since, Until: until, Details: details, }, nil } func logsAction(cmd *cobra.Command, args []string) error { options, err := logsOptions(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return container.Logs(ctx, client, args[0], options) } func logsShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show container names (TODO: only show containers with logs) return completion.ContainerNames(cmd, nil) } // Attempts to parse the argument given to `-n/--tail` as a uint. func getTailArgAsUint(arg string) (uint, error) { if arg == "all" { return 0, nil } num, err := strconv.Atoi(arg) if err != nil { return 0, fmt.Errorf("failed to parse `-n/--tail` argument %q: %w", arg, err) } if num < 0 { return 0, fmt.Errorf("`-n/--tail` argument must be positive, got: %d", num) } return uint(num), nil } ================================================ FILE: cmd/nerdctl/container/container_logs_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "errors" "fmt" "regexp" "runtime" "strconv" "strings" "testing" "time" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestLogs(t *testing.T) { const expected = `foo bar ` testCase := nerdtest.Setup() if runtime.GOOS == "windows" { testCase.Require = nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/4237") } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "--quiet", "--name", data.Identifier(), testutil.CommonImage, "sh", "-euxc", "echo foo; echo bar;") data.Labels().Set("cID", data.Identifier()) } testCase.SubTests = []*test.Case{ { Description: "since 1s", Setup: func(data test.Data, helpers test.Helpers) { // Ensure at least 2 seconds have elapsed since the container ran, // so that --since 1s does not include the container's output. time.Sleep(2 * time.Second) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", "--since", "1s", data.Labels().Get("cID")) }, Expected: test.Expects(0, nil, expect.DoesNotContain(expected)), }, { Description: "since 60s", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", "--since", "60s", data.Labels().Get("cID")) }, Expected: test.Expects(0, nil, expect.Equals(expected)), }, { Description: "until 60s", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", "--until", "60s", data.Labels().Get("cID")) }, Expected: test.Expects(0, nil, expect.DoesNotContain(expected)), }, { Description: "until 1s", Setup: func(data test.Data, helpers test.Helpers) { // Ensure at least 2 seconds have elapsed since the container ran, // so that --until 1s includes the container's output. time.Sleep(2 * time.Second) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", "--until", "1s", data.Labels().Get("cID")) }, Expected: test.Expects(0, nil, expect.Equals(expected)), }, { Description: "follow", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", "-f", data.Labels().Get("cID")) }, Expected: test.Expects(0, nil, expect.Equals(expected)), }, { Description: "timestamp", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", "-t", data.Labels().Get("cID")) }, Expected: test.Expects(0, nil, expect.Contains(time.Now().UTC().Format("2006-01-02"))), }, { Description: "tail flag", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", "-n", "all", data.Labels().Get("cID")) }, Expected: test.Expects(0, nil, expect.Equals(expected)), }, { Description: "tail flag", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", "-n", "1", data.Labels().Get("cID")) }, // FIXME: why? Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile("^(?:bar\n|)$"))), }, } testCase.Run(t) } // Tests whether `nerdctl logs` properly separates stdout/stderr output // streams for containers using the jsonfile logging driver: func TestLogsOutStreamsSeparated(t *testing.T) { testCase := nerdtest.Setup() if runtime.GOOS == "windows" { // Logging seems broken on windows. testCase.Require = nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/4237") } testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "--name", data.Identifier(), testutil.CommonImage, "sh", "-euc", "echo stdout1; echo stderr1 >&2; echo stdout2; echo stderr2 >&2") } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", data.Identifier()) } testCase.Expected = test.Expects(expect.ExitCodeSuccess, []error{ //revive:disable:error-strings errors.New("stderr1\nstderr2\n"), }, expect.Equals("stdout1\nstdout2\n")) testCase.Run(t) } func TestLogsWithInheritedFlags(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.Not(nerdtest.Docker) testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("-n="+testutil.Namespace, "run", "--name", data.Identifier(), testutil.CommonImage, "sh", "-euxc", "echo foo; echo bar") } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("-n="+testutil.Namespace, "logs", "-n", "1", data.Identifier()) } // FIXME: why? testCase.Expected = test.Expects(0, nil, expect.Match(regexp.MustCompile("^(?:bar\n|)$"))) testCase.Run(t) } func TestLogsOfJournaldDriver(t *testing.T) { const expected = `foo bar ` testCase := nerdtest.Setup() testCase.Require = require.All( require.Binary("journalctl"), &test.Requirement{ Check: func(data test.Data, helpers test.Helpers) (bool, string) { works := false cmd := helpers.Custom("journalctl", "-xe") cmd.Run(&test.Expected{ ExitCode: expect.ExitCodeNoCheck, Output: func(stdout string, t tig.T) { if stdout != "" { works = true } }, }) return works, "Journactl to return data for the current user" }, }, ) testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "--network", "none", "--log-driver", "journald", "--name", data.Identifier(), testutil.CommonImage, "sh", "-euxc", "echo foo; echo bar") data.Labels().Set("cID", data.Identifier()) } testCase.SubTests = []*test.Case{ { Description: "logs", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", data.Labels().Get("cID")) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals(expected)), }, { Description: "logs --since 60s", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", "--since", "60s", data.Labels().Get("cID")) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.DoesNotContain("foo", "bar")), }, } } func TestLogsWithFailingContainer(t *testing.T) { const expected = `foo bar ` testCase := nerdtest.Setup() if runtime.GOOS == "windows" { // Logging seems broken on windows. testCase.Require = nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/4237") } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("run", "--name", data.Identifier(), testutil.CommonImage, "sh", "-euxc", "echo foo; echo bar; exit 42; echo baz") } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", data.Identifier()) } testCase.Expected = test.Expects(0, nil, expect.Equals(expected)) testCase.Run(t) } func TestLogsWithRunningContainer(t *testing.T) { expected := make([]string, 10) for i := 0; i < 10; i++ { expected[i] = fmt.Sprint(i + 1) } testCase := nerdtest.Setup() if runtime.GOOS == "windows" { // Logging seems broken on windows. testCase.Require = nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/4237") } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "--name", data.Identifier(), testutil.CommonImage, "sh", "-euc", "for i in `seq 1 10`; do echo $i; sleep 1; done") } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", data.Identifier()) } testCase.Expected = test.Expects(0, nil, expect.Contains(expected[0], expected[1:]...)) testCase.Run(t) } func TestLogsWithoutNewlineOrEOF(t *testing.T) { testCase := nerdtest.Setup() // FIXME: test does not work on Windows yet because containerd doesn't send an exit event appropriately after task exit on Windows") // FIXME: nerdctl behavior does not match docker - test disabled for nerdctl until we fix testCase.Require = require.All( require.Linux, ) testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "--name", data.Identifier(), testutil.CommonImage, "printf", "'Hello World!\nThere is no newline'") } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", "-f", data.Identifier()) } testCase.Expected = test.Expects(0, nil, expect.Equals("'Hello World!\nThere is no newline'")) testCase.Run(t) } func TestLogsAfterRestartingContainer(t *testing.T) { if runtime.GOOS != "linux" { t.Skip("FIXME: test does not work on Windows yet. Restarting a container fails with: failed to create shim task: hcs::CreateComputeSystem : The requested operation for attach namespace failed.: unknown") } testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "--name", data.Identifier(), testutil.CommonImage, "printf", "'Hello World!\nThere is no newline'") data.Labels().Set("cID", data.Identifier()) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.SubTests = []*test.Case{ { Description: "logs -f works", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", "-f", data.Labels().Get("cID")) }, Expected: test.Expects(0, nil, expect.Equals("'Hello World!\nThere is no newline'")), }, { Description: "logs -f works after restart", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("start", data.Labels().Get("cID")) // FIXME: this is inherently flaky time.Sleep(5 * time.Second) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", "-f", data.Labels().Get("cID")) }, Expected: test.Expects(0, nil, expect.Equals("'Hello World!\nThere is no newline''Hello World!\nThere is no newline'")), }, } testCase.Run(t) } func TestLogsWithForegroundContainers(t *testing.T) { testCase := nerdtest.Setup() // dual logging is not supported on Windows testCase.Require = require.Not(require.Windows) testCase.Run(t) testCase.SubTests = []*test.Case{ { Description: "foreground", Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "--name", data.Identifier(), testutil.CommonImage, "sh", "-euxc", "echo foo; echo bar") }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", data.Identifier()) }, Expected: test.Expects(0, nil, expect.Equals("foo\nbar\n")), }, { Description: "interactive", Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-i", "--name", data.Identifier(), testutil.CommonImage, "sh", "-euxc", "echo foo; echo bar") }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", data.Identifier()) }, Expected: test.Expects(0, nil, expect.Equals("foo\nbar\n")), }, { Description: "PTY", Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Setup: func(data test.Data, helpers test.Helpers) { cmd := helpers.Command("run", "-t", "--name", data.Identifier(), testutil.CommonImage, "sh", "-euxc", "echo foo; echo bar") cmd.WithPseudoTTY() cmd.Run(&test.Expected{ExitCode: 0}) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", data.Identifier()) }, Expected: test.Expects(0, nil, expect.Equals("foo\nbar\n")), }, { Description: "interactivePTY", Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Setup: func(data test.Data, helpers test.Helpers) { cmd := helpers.Command("run", "-i", "-t", "--name", data.Identifier(), testutil.CommonImage, "sh", "-euxc", "echo foo; echo bar") cmd.WithPseudoTTY() cmd.Run(&test.Expected{ExitCode: 0}) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", data.Identifier()) }, Expected: test.Expects(0, nil, expect.Equals("foo\nbar\n")), }, } } func TestLogsTailFollowRotate(t *testing.T) { // FIXME this is flaky by nature... the number of lines is arbitrary, the wait is arbitrary, // and both are some sort of educated guess that things will mostly always kinda work maybe... const sampleJSONLog = `{"log":"A\n","stream":"stdout","time":"2024-04-11T12:01:09.800288974Z"}` const linesPerFile = 200 testCase := nerdtest.Setup() // tail log is not supported on Windows testCase.Require = require.Not(require.Windows) testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--log-driver", "json-file", "--log-opt", fmt.Sprintf("max-size=%d", len(sampleJSONLog)*linesPerFile), "--log-opt", "max-file=10", "--name", data.Identifier(), testutil.CommonImage, "sh", "-euc", "while true; do echo A; usleep 100; done") // FIXME: ... inherently racy... time.Sleep(5 * time.Second) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("logs", "-f", data.Identifier()) // FIXME: this is flaky by nature. We assume that the container has started and will output enough in 5 seconds. cmd.WithTimeout(5 * time.Second) return cmd } testCase.Expected = test.Expects(expect.ExitCodeTimeout, nil, func(stdout string, t tig.T) { tailLogs := strings.Split(strings.TrimSpace(stdout), "\n") for _, line := range tailLogs { if line != "" { assert.Equal(t, "A", line) } } assert.Assert(t, len(tailLogs) > linesPerFile, fmt.Sprintf("expected %d lines or more, found %d", linesPerFile, len(tailLogs))) }) testCase.Run(t) } func TestLogsNoneLoggerHasNoLogURI(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "--name", data.Identifier(), "--log-driver", "none", testutil.CommonImage, "sh", "-euxc", "echo foo") } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", data.Identifier()) } testCase.Expected = test.Expects(1, nil, nil) testCase.Run(t) } func TestLogsWithDetails(t *testing.T) { testCase := nerdtest.Setup() // FIXME: this is not working on windows. There is some deep issue with windows logs: // https://github.com/containerd/nerdctl/issues/4237 if runtime.GOOS == "windows" { testCase.Require = nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/4237") } testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "--log-driver", "json-file", "--log-opt", "max-size=10m", "--log-opt", "max-file=3", "--log-opt", "env=ENV", "--env", "ENV=foo", "--log-opt", "labels=LABEL", "--label", "LABEL=bar", "--name", data.Identifier(), testutil.CommonImage, "sh", "-ec", "echo baz") } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", "--details", data.Identifier()) } testCase.Expected = test.Expects(0, nil, expect.Contains("ENV=foo", "LABEL=bar", "baz")) testCase.Run(t) } func TestLogsFollowNoExtraneousLineFeed(t *testing.T) { testCase := nerdtest.Setup() // This test verifies that `nerdctl logs -f` does not add extraneous line feeds testCase.Require = require.Not(require.Windows) testCase.Setup = func(data test.Data, helpers test.Helpers) { // Create a container that outputs a message without a trailing newline helpers.Ensure("run", "--name", data.Identifier(), testutil.CommonImage, "sh", "-c", "printf 'Hello without newline'") } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { // Use logs -f to follow the logs return helpers.Command("logs", "-f", data.Identifier()) } // Verify that the output is exactly "Hello without newline" without any additional line feeds testCase.Expected = test.Expects(0, nil, expect.Equals("Hello without newline")) testCase.Run(t) } func TestLogsWithStartContainer(t *testing.T) { testCase := nerdtest.Setup() // Windows does not support dual logging. testCase.Require = require.Not(require.Windows) testCase.SubTests = []*test.Case{ { Description: "Test logs are directed correctly for container start of a interactive container", Setup: func(data test.Data, helpers test.Helpers) { cmd := helpers.Command("run", "-it", "--name", data.Identifier(), testutil.CommonImage) cmd.WithPseudoTTY() cmd.Feed(strings.NewReader("echo foo\nexit\n")) cmd.Run(&test.Expected{ ExitCode: 0, }) cmd = helpers.Command("start", "-ia", data.Identifier()) cmd.WithPseudoTTY() cmd.Feed(strings.NewReader("echo bar\nexit\n")) cmd.Run(&test.Expected{ ExitCode: 0, }) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", data.Identifier()) }, Expected: test.Expects(0, nil, expect.Contains("foo", "bar")), }, { // FIXME: is this test safe or could it be racy? Description: "Test logs are captured after stopping and starting a non-interactive container and continue capturing new logs", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sh", "-c", "while true; do echo foo; sleep 1; done") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { helpers.Ensure("stop", data.Identifier()) initialLogs := helpers.Capture("logs", data.Identifier()) initialFooCount := strings.Count(initialLogs, "foo") data.Labels().Set("initialFooCount", strconv.Itoa(initialFooCount)) helpers.Ensure("start", data.Identifier()) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) return helpers.Command("logs", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { finalLogsCount := strings.Count(stdout, "foo") initialFooCount, _ := strconv.Atoi(data.Labels().Get("initialFooCount")) assert.Assert(t, finalLogsCount > initialFooCount, "Expected 'foo' count to increase after restart") }, } }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_pause.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "github.com/spf13/cobra" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/container" ) func PauseCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "pause [flags] CONTAINER [CONTAINER, ...]", Args: cobra.MinimumNArgs(1), Short: "Pause all processes within one or more containers", RunE: pauseAction, ValidArgsFunction: pauseShellComplete, SilenceUsage: true, SilenceErrors: true, } return cmd } func pauseOptions(cmd *cobra.Command) (types.ContainerPauseOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ContainerPauseOptions{}, err } return types.ContainerPauseOptions{ GOptions: globalOptions, Stdout: cmd.OutOrStdout(), }, nil } func pauseAction(cmd *cobra.Command, args []string) error { options, err := pauseOptions(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return container.Pause(ctx, client, args, options) } func pauseShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show running container names statusFilterFn := func(st containerd.ProcessStatus) bool { return st == containerd.Running } return completion.ContainerNames(cmd, statusFilterFn) } ================================================ FILE: cmd/nerdctl/container/container_port.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "context" "fmt" "strconv" "strings" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" "github.com/containerd/nerdctl/v2/pkg/portutil" ) func PortCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "port [flags] CONTAINER [PRIVATE_PORT[/PROTO]]", Args: cobra.RangeArgs(1, 2), Short: "List port mappings or a specific mapping for the container", RunE: portAction, ValidArgsFunction: portShellComplete, SilenceUsage: true, SilenceErrors: true, } return cmd } func portAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } argPort := -1 argProto := "" portProto := "" if len(args) == 2 { portProto = args[1] } if portProto != "" { splitBySlash := strings.Split(portProto, "/") var err error argPort, err = strconv.Atoi(splitBySlash[0]) if err != nil { return err } if argPort <= 0 { return fmt.Errorf("unexpected port %d", argPort) } switch len(splitBySlash) { case 1: argProto = "tcp" case 2: argProto = strings.ToLower(splitBySlash[1]) default: return fmt.Errorf("failed to parse %q", portProto) } } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() dataStore, err := clientutil.DataStore(globalOptions.DataRoot, globalOptions.Address) if err != nil { return err } walker := &containerwalker.ContainerWalker{ Client: client, OnFound: func(ctx context.Context, found containerwalker.Found) error { if found.MatchCount > 1 { return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) } containerLabels, err := found.Container.Labels(ctx) if err != nil { return err } ports, err := portutil.LoadPortMappings(dataStore, globalOptions.Namespace, found.Container.ID(), containerLabels) if err != nil { return err } return containerutil.PrintHostPort(ctx, cmd.OutOrStdout(), found.Container, argPort, argProto, ports) }, } req := args[0] n, err := walker.Walk(ctx, req) if err != nil { return err } else if n == 0 { return fmt.Errorf("no such container %s", req) } return nil } func portShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.ContainerNames(cmd, nil) } ================================================ FILE: cmd/nerdctl/container/container_prune.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/container" ) func pruneCommand() *cobra.Command { cmd := &cobra.Command{ Use: "prune [flags]", Short: "Remove all stopped containers", Args: cobra.NoArgs, RunE: pruneAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().BoolP("force", "f", false, "Do not prompt for confirmation") return cmd } func pruneOptions(cmd *cobra.Command) (types.ContainerPruneOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ContainerPruneOptions{}, err } return types.ContainerPruneOptions{ GOptions: globalOptions, Stdout: cmd.OutOrStdout(), }, nil } func grantPrunePermission(cmd *cobra.Command) (bool, error) { force, err := cmd.Flags().GetBool("force") if err != nil { return false, err } if !force { return helpers.Confirm(cmd, "WARNING! This will remove all stopped containers.") } return true, nil } func pruneAction(cmd *cobra.Command, _ []string) error { options, err := pruneOptions(cmd) if err != nil { return err } if ok, err := grantPrunePermission(cmd); err != nil { return err } else if !ok { return nil } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return container.Prune(ctx, client, options) } ================================================ FILE: cmd/nerdctl/container/container_prune_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "testing" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestPruneContainer(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = nerdtest.Private testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("1")) helpers.Anyhow("rm", "-f", data.Identifier("2")) } testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier("1"), "-v", "/anonymous", testutil.CommonImage, "sleep", nerdtest.Infinity) helpers.Ensure("exec", data.Identifier("1"), "touch", "/anonymous/foo") helpers.Ensure("create", "--name", data.Identifier("2"), testutil.CommonImage, "sleep", nerdtest.Infinity) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { helpers.Ensure("container", "prune", "-f") helpers.Ensure("inspect", data.Identifier("1")) helpers.Fail("inspect", data.Identifier("2")) // https://github.com/containerd/nerdctl/issues/3134 helpers.Ensure("exec", data.Identifier("1"), "ls", "-lA", "/anonymous/foo") helpers.Ensure("kill", data.Identifier("1")) helpers.Ensure("container", "prune", "-f") return helpers.Command("inspect", data.Identifier("1")) } testCase.Expected = test.Expects(1, nil, nil) } ================================================ FILE: cmd/nerdctl/container/container_remove.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/container" ) func RemoveCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "rm [flags] CONTAINER [CONTAINER, ...]", Args: cobra.MinimumNArgs(1), Short: "Remove one or more containers", RunE: removeAction, ValidArgsFunction: rmShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Aliases = []string{"remove"} cmd.Flags().BoolP("force", "f", false, "Force the removal of a running|paused|unknown container (uses SIGKILL)") cmd.Flags().BoolP("volumes", "v", false, "Remove volumes associated with the container") return cmd } func removeAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } force, err := cmd.Flags().GetBool("force") if err != nil { return err } removeAnonVolumes, err := cmd.Flags().GetBool("volumes") if err != nil { return err } options := types.ContainerRemoveOptions{ GOptions: globalOptions, Force: force, Volumes: removeAnonVolumes, Stdout: cmd.OutOrStdout(), } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return container.Remove(ctx, client, args, options) } func rmShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show container names return completion.ContainerNames(cmd, nil) } ================================================ FILE: cmd/nerdctl/container/container_remove_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "fmt" "strconv" "testing" "time" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/portlock" ) // iptablesCheckCommand is the shell command to check iptables rules const iptablesCheckCommand = "iptables -t nat -S && iptables -t filter -S && iptables -t mangle -S" // testContainerRmIptablesExecutor is a common executor function for testing iptables rules cleanup func testContainerRmIptablesExecutor(data test.Data, helpers test.Helpers) test.TestableCommand { t := helpers.T() // Get the container ID from the label containerID := data.Labels().Get("containerID") // Remove the container helpers.Ensure("rm", "-f", containerID) time.Sleep(1 * time.Second) // Create a TestableCommand using helpers.Custom if rootlessutil.IsRootless() { // In rootless mode, we need to enter the rootlesskit network namespace if netns, err := rootlessutil.DetachedNetNS(); err != nil { t.Log(fmt.Sprintf("Failed to get detached network namespace: %v", err)) t.FailNow() } else { if netns != "" { // Use containerd-rootless-setuptool.sh to enter the RootlessKit namespace return helpers.Custom("containerd-rootless-setuptool.sh", "nsenter", "--", "nsenter", "--net="+netns, "sh", "-ec", iptablesCheckCommand) } // Enter into :RootlessKit namespace using containerd-rootless-setuptool.sh return helpers.Custom("containerd-rootless-setuptool.sh", "nsenter", "--", "sh", "-ec", iptablesCheckCommand) } } // In non-rootless mode, check iptables rules directly on the host return helpers.Custom("sh", "-ec", iptablesCheckCommand) } // TestContainerRmIptables tests that iptables rules are cleared after container deletion func TestContainerRmIptables(t *testing.T) { testCase := nerdtest.Setup() // Require iptables and containerd-rootless-setuptool.sh commands to be available testCase.Require = require.All( require.Binary("iptables"), require.Binary("containerd-rootless-setuptool.sh"), require.Not(require.Windows), require.Not(nerdtest.Docker), ) testCase.SubTests = []*test.Case{ { Description: "Test iptables rules are cleared after container deletion", Setup: func(data test.Data, helpers test.Helpers) { // Get a free port using portlock port, err := portlock.Acquire(0) if err != nil { helpers.T().Log(fmt.Sprintf("Failed to acquire port: %v", err)) helpers.T().FailNow() } data.Labels().Set("port", strconv.Itoa(port)) // Create a container with port mapping to ensure iptables rules are created containerID := helpers.Capture("run", "-d", "--name", data.Identifier(), "-p", fmt.Sprintf("%d:80", port), testutil.NginxAlpineImage) data.Labels().Set("containerID", containerID) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { // Make sure container is removed even if test fails helpers.Anyhow("rm", "-f", data.Identifier()) // Release the acquired port if portStr := data.Labels().Get("port"); portStr != "" { port, _ := strconv.Atoi(portStr) _ = portlock.Release(port) } }, Command: testContainerRmIptablesExecutor, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { // Get the container ID from the label containerID := data.Labels().Get("containerID") return &test.Expected{ ExitCode: expect.ExitCodeSuccess, // Verify that the iptables output does not contain the container ID Output: expect.DoesNotContain(containerID), } }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_remove_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "testing" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRemoveContainer(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { containerID := data.Identifier() helpers.Fail("rm", containerID) // FIXME: should (re-)evaluate this // `kill` seems to return before the container actually stops helpers.Ensure("stop", containerID) return helpers.Command("rm", containerID) } testCase.Expected = test.Expects(0, nil, nil) } ================================================ FILE: cmd/nerdctl/container/container_remove_windows_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRemoveHyperVContainer(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = nerdtest.HyperV testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--isolation", "hyperv", "--name", testutil.Identifier(t), testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, testutil.Identifier(t)) inspect := nerdtest.InspectContainer(helpers, testutil.Identifier(t)) //check with HCS if the container is ineed a VM isHypervContainer, err := testutil.HyperVContainer(inspect) assert.NilError(t, err) assert.Assert(t, isHypervContainer, true) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", testutil.Identifier(t)) } testCase.SubTests = []*test.Case{ { Description: "should fail to remove when still running", NoParallel: true, Command: test.Command("rm", testutil.Identifier(t)), Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, { Description: "should kill the container", NoParallel: true, Command: test.Command("kill", testutil.Identifier(t)), Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, { Description: "should remove the container when terminated", NoParallel: true, Command: test.Command("rm", testutil.Identifier(t)), Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_rename.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/container" ) func RenameCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "rename [flags] CONTAINER NEW_NAME", Args: helpers.IsExactArgs(2), Short: "rename a container", RunE: renameAction, ValidArgsFunction: renameShellComplete, SilenceUsage: true, SilenceErrors: true, } return cmd } func renameOptions(cmd *cobra.Command) (types.ContainerRenameOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ContainerRenameOptions{}, err } return types.ContainerRenameOptions{ GOptions: globalOptions, Stdout: cmd.OutOrStdout(), }, nil } func renameAction(cmd *cobra.Command, args []string) error { options, err := renameOptions(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return container.Rename(ctx, client, args[0], args[1], options) } func renameShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.ContainerNames(cmd, nil) } ================================================ FILE: cmd/nerdctl/container/container_rename_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRename(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { testContainerName := data.Identifier() data.Labels().Set("containerName", testContainerName) helpers.Ensure("run", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, testContainerName) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { testContainerName := data.Labels().Get("containerName") helpers.Anyhow("rm", "-f", testContainerName) helpers.Anyhow("rm", "-f", testContainerName+"_new") } testCase.SubTests = []*test.Case{ { Description: "`rename` should work", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { testContainerName := data.Labels().Get("containerName") return helpers.Command("rename", testContainerName, testContainerName+"_new") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, { Description: "`rename` should have updated container name", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("ps", "-a") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { testContainerName := data.Labels().Get("containerName") return test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(testContainerName+"_new"))(data, helpers) }, }, { Description: "`rename` should fail to rename not existing container", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { testContainerName := data.Labels().Get("containerName") return helpers.Command("rename", testContainerName, testContainerName+"_new") }, Expected: test.Expects(1, nil, nil), }, { Description: "`rename` should fail to rename to existing name", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { testContainerName := data.Labels().Get("containerName") return helpers.Command("rename", testContainerName+"_new", testContainerName+"_new") }, Expected: test.Expects(1, nil, nil), }, } testCase.Run(t) } func TestRenameUpdateHosts(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.Not(nerdtest.Docker) testCase.Setup = func(data test.Data, helpers test.Helpers) { testContainerName := data.Identifier() data.Labels().Set("containerName", testContainerName) helpers.Ensure("run", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", nerdtest.Infinity) helpers.Ensure("run", "-d", "--name", testContainerName+"_1", testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, testContainerName) nerdtest.EnsureContainerStarted(helpers, testContainerName+"_1") } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { testContainerName := data.Labels().Get("containerName") helpers.Anyhow("rm", "-f", testContainerName) helpers.Anyhow("rm", "-f", testContainerName+"_1") helpers.Anyhow("rm", "-f", testContainerName+"_new") } testCase.SubTests = []*test.Case{ { Description: "check '/etc/hosts' for sibling container", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { testContainerName := data.Labels().Get("containerName") return helpers.Command("exec", testContainerName, "cat", "/etc/hosts") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { testContainerName := data.Labels().Get("containerName") return test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(testContainerName+"_1"))(data, helpers) }, }, { Description: "rename container", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { testContainerName := data.Labels().Get("containerName") return helpers.Command("rename", testContainerName, testContainerName+"_new") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, { Description: "check '/etc/hosts' for renamed container", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { testContainerName := data.Labels().Get("containerName") return helpers.Command("exec", testContainerName+"_new", "cat", "/etc/hosts") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { testContainerName := data.Labels().Get("containerName") return test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(testContainerName+"_new"))(data, helpers) }, }, { Description: "check sibling's '/etc/hosts' for renamed container", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { testContainerName := data.Labels().Get("containerName") return helpers.Command("exec", testContainerName+"_1", "cat", "/etc/hosts") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { testContainerName := data.Labels().Get("containerName") return test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(testContainerName+"_new"))(data, helpers) }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_rename_windows_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRenameProcessContainer(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { testContainerName := testutil.Identifier(t) data.Labels().Set("containerName", testContainerName) helpers.Ensure("run", "--isolation", "process", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, testContainerName) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { testContainerName := data.Labels().Get("containerName") helpers.Anyhow("rm", "-f", testContainerName) helpers.Anyhow("rm", "-f", testContainerName+"_new") } testCase.SubTests = []*test.Case{ { Description: "`rename` should work", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { testContainerName := data.Labels().Get("containerName") return helpers.Command("rename", testContainerName, testContainerName+"_new") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, { Description: "`rename` should have updated container name", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("ps", "-a") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { testContainerName := data.Labels().Get("containerName") return test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(testContainerName+"_new"))(data, helpers) }, }, { Description: "`rename` should fail to rename not existing container", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { testContainerName := data.Labels().Get("containerName") return helpers.Command("rename", testContainerName, testContainerName+"_new") }, Expected: test.Expects(1, nil, nil), }, { Description: "`rename` should fail to rename to existing name", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { testContainerName := data.Labels().Get("containerName") return helpers.Command("rename", testContainerName+"_new", testContainerName+"_new") }, Expected: test.Expects(1, nil, nil), }, } testCase.Run(t) } func TestRenameHyperVContainer(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = nerdtest.HyperV testCase.Setup = func(data test.Data, helpers test.Helpers) { testContainerName := testutil.Identifier(t) data.Labels().Set("containerName", testContainerName) helpers.Ensure("run", "--isolation", "hyperv", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, testContainerName) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { testContainerName := data.Labels().Get("containerName") helpers.Anyhow("rm", "-f", testContainerName) helpers.Anyhow("rm", "-f", testContainerName+"_new") } testCase.SubTests = []*test.Case{ { Description: "`rename` should work", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { testContainerName := data.Labels().Get("containerName") return helpers.Command("rename", testContainerName, testContainerName+"_new") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, { Description: "`rename` should have updated container name", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("ps", "-a") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { testContainerName := data.Labels().Get("containerName") return test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(testContainerName+"_new"))(data, helpers) }, }, { Description: "`rename` should fail to rename not existing container", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { testContainerName := data.Labels().Get("containerName") return helpers.Command("rename", testContainerName, testContainerName+"_new") }, Expected: test.Expects(1, nil, nil), }, { Description: "`rename` should fail to rename to existing name", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { testContainerName := data.Labels().Get("containerName") return helpers.Command("rename", testContainerName+"_new", testContainerName+"_new") }, Expected: test.Expects(1, nil, nil), }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_restart.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "time" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/container" ) func RestartCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "restart [flags] CONTAINER [CONTAINER, ...]", Args: cobra.MinimumNArgs(1), Short: "Restart one or more running containers", RunE: restartAction, ValidArgsFunction: startShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().UintP("time", "t", 10, "Seconds to wait for stop before killing it") cmd.Flags().StringP("signal", "s", "", "Signal to send to stop the container, before killing it") return cmd } func restartOptions(cmd *cobra.Command) (types.ContainerRestartOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ContainerRestartOptions{}, err } // Call GlobalFlags function here nerdctlCmd, nerdctlArgs := helpers.GlobalFlags(cmd) var timeout *time.Duration if cmd.Flags().Changed("time") { // Seconds to wait for stop before killing it timeValue, err := cmd.Flags().GetUint("time") if err != nil { return types.ContainerRestartOptions{}, err } t := time.Duration(timeValue) * time.Second timeout = &t } var signal string if cmd.Flags().Changed("signal") { // Signal to send to stop the container, before killing it sig, err := cmd.Flags().GetString("signal") if err != nil { return types.ContainerRestartOptions{}, err } signal = sig } return types.ContainerRestartOptions{ Stdout: cmd.OutOrStdout(), GOption: globalOptions, Timeout: timeout, Signal: signal, NerdctlCmd: nerdctlCmd, NerdctlArgs: nerdctlArgs, }, err } func restartAction(cmd *cobra.Command, args []string) error { options, err := restartOptions(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOption.Namespace, options.GOption.Address) if err != nil { return err } defer cancel() return container.Restart(ctx, client, args, options) } ================================================ FILE: cmd/nerdctl/container/container_restart_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "encoding/json" "fmt" "strconv" "strings" "testing" "time" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRestart(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", testutil.Identifier(t), testutil.NginxAlpineImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, testutil.Identifier(t)) inspect := nerdtest.InspectContainer(helpers, testutil.Identifier(t)) data.Labels().Set("pid", strconv.Itoa(inspect.State.Pid)) helpers.Ensure("restart", testutil.Identifier(t)) nerdtest.EnsureContainerStarted(helpers, testutil.Identifier(t)) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", testutil.Identifier(t)) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("inspect", testutil.Identifier(t)) } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, Output: func(stdout string, t tig.T) { var dc []dockercompat.Container err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err) assert.Equal(t, 1, len(dc)) assert.Assert(t, data.Labels().Get("pid") != strconv.Itoa(dc[0].State.Pid)) }, } } testCase.Run(t) } func TestRestartPIDContainer(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { baseContainerName := testutil.Identifier(t) helpers.Ensure("run", "-d", "--name", baseContainerName, testutil.AlpineImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, baseContainerName) sharedContainerName := fmt.Sprintf("%s-shared", baseContainerName) helpers.Ensure("run", "-d", "--name", sharedContainerName, fmt.Sprintf("--pid=container:%s", baseContainerName), testutil.AlpineImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, sharedContainerName) helpers.Ensure("restart", baseContainerName) nerdtest.EnsureContainerStarted(helpers, baseContainerName) helpers.Ensure("restart", sharedContainerName) nerdtest.EnsureContainerStarted(helpers, sharedContainerName) // output format : /proc/1/ns/pid // example output: 4026532581 /proc/1/ns/pid basePSResult := helpers.Capture("exec", baseContainerName, "ls", "-Li", "/proc/1/ns/pid") baseOutput := strings.TrimSpace(basePSResult) data.Labels().Set("baseContainerName", baseContainerName) data.Labels().Set("sharedContainerName", sharedContainerName) data.Labels().Set("baseOutput", baseOutput) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Labels().Get("baseContainerName")) helpers.Anyhow("rm", "-f", data.Labels().Get("sharedContainerName")) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Labels().Get("sharedContainerName"), "ls", "-Li", "/proc/1/ns/pid") } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, Output: func(stdout string, t tig.T) { assert.Equal(t, strings.TrimSpace(stdout), data.Labels().Get("baseOutput")) }, } } testCase.Run(t) } func TestRestartIPCContainer(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { const shmSize = "32m" baseContainerName := testutil.Identifier(t) helpers.Ensure("run", "-d", "--shm-size", shmSize, "--ipc", "shareable", "--name", baseContainerName, testutil.AlpineImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, baseContainerName) sharedContainerName := fmt.Sprintf("%s-shared", baseContainerName) helpers.Ensure("run", "-d", "--name", sharedContainerName, fmt.Sprintf("--ipc=container:%s", baseContainerName), testutil.AlpineImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, sharedContainerName) helpers.Ensure("stop", baseContainerName) helpers.Ensure("stop", sharedContainerName) helpers.Ensure("restart", baseContainerName) nerdtest.EnsureContainerStarted(helpers, baseContainerName) helpers.Ensure("restart", sharedContainerName) nerdtest.EnsureContainerStarted(helpers, sharedContainerName) baseShmSizeResult := helpers.Capture("exec", baseContainerName, "/bin/grep", "shm", "/proc/self/mounts") baseOutput := strings.TrimSpace(baseShmSizeResult) data.Labels().Set("baseContainerName", baseContainerName) data.Labels().Set("sharedContainerName", sharedContainerName) data.Labels().Set("baseOutput", baseOutput) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Labels().Get("baseContainerName")) helpers.Anyhow("rm", "-f", data.Labels().Get("sharedContainerName")) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Labels().Get("sharedContainerName"), "/bin/grep", "shm", "/proc/self/mounts") } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, Output: func(stdout string, t tig.T) { assert.Equal(t, strings.TrimSpace(stdout), data.Labels().Get("baseOutput")) }, } } testCase.Run(t) } func TestRestartWithTime(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { containerName := testutil.Identifier(t) helpers.Ensure("run", "-d", "--name", containerName, testutil.AlpineImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, containerName) inspect := nerdtest.InspectContainer(helpers, containerName) pid := inspect.State.Pid data.Labels().Set("containerName", containerName) data.Labels().Set("pid", strconv.Itoa(pid)) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Labels().Get("containerName")) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { data.Labels().Set("timePreRestart", time.Now().Format(time.RFC3339)) return helpers.Command("restart", "-t", "5", data.Labels().Get("containerName")) } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, Output: func(stdout string, t tig.T) { timePostRestart := time.Now() timePreRestart, err := time.Parse(time.RFC3339, data.Labels().Get("timePreRestart")) assert.NilError(t, err) // ensure that stop took at least 5 seconds assert.Assert(t, timePostRestart.Sub(timePreRestart) >= time.Second*5) inspect := nerdtest.InspectContainer(helpers, data.Labels().Get("containerName")) assert.Assert(t, strconv.Itoa(inspect.State.Pid) != data.Labels().Get("pid")) }, } } testCase.Run(t) } func TestRestartWithSignal(t *testing.T) { testCase := nerdtest.Setup() // FIXME: gomodjail signal handling is not working yet: https://github.com/AkihiroSuda/gomodjail/issues/51 testCase.Require = require.Not(nerdtest.Gomodjail) testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := nerdtest.RunSigProxyContainer(nerdtest.SigUsr1, false, nil, data, helpers) // Capture the current pid data.Labels().Set("oldpid", strconv.Itoa(nerdtest.InspectContainer(helpers, data.Identifier()).State.Pid)) // Send the signal helpers.Ensure("restart", "--signal", "SIGUSR1", data.Identifier()) return cmd } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ // Check the container did indeed exit ExitCode: 137, Output: expect.All( // Check that we saw SIGUSR1 inside the container expect.Contains(nerdtest.SignalCaught), func(stdout string, t tig.T) { // Ensure the container was restarted nerdtest.EnsureContainerStarted(helpers, data.Identifier()) // Check the new pid is different newpid := strconv.Itoa(nerdtest.InspectContainer(helpers, data.Identifier()).State.Pid) assert.Assert(helpers.T(), newpid != data.Labels().Get("oldpid")) }, ), } } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_run.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "errors" "fmt" "runtime" "strings" "github.com/spf13/cobra" "golang.org/x/term" "github.com/containerd/console" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/pkg/annotations" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/container" "github.com/containerd/nerdctl/v2/pkg/config" "github.com/containerd/nerdctl/v2/pkg/consoleutil" "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/defaults" "github.com/containerd/nerdctl/v2/pkg/errutil" "github.com/containerd/nerdctl/v2/pkg/healthcheck" "github.com/containerd/nerdctl/v2/pkg/labels" "github.com/containerd/nerdctl/v2/pkg/logging" "github.com/containerd/nerdctl/v2/pkg/netutil" "github.com/containerd/nerdctl/v2/pkg/signalutil" "github.com/containerd/nerdctl/v2/pkg/taskutil" ) const ( tiniInitBinary = "tini" ) func RunCommand() *cobra.Command { shortHelp := "Run a command in a new container. Optionally specify \"ipfs://\" or \"ipns://\" scheme to pull image from IPFS." longHelp := shortHelp switch runtime.GOOS { case "windows": longHelp += "\n" longHelp += "WARNING: `nerdctl run` is experimental on Windows and currently broken (https://github.com/containerd/nerdctl/issues/28)" case "freebsd": longHelp += "\n" longHelp += "WARNING: `nerdctl run` is experimental on FreeBSD and currently requires `--net=none` (https://github.com/containerd/nerdctl/blob/main/docs/freebsd.md)" } var cmd = &cobra.Command{ Use: "run [flags] IMAGE [COMMAND] [ARG...]", Args: cobra.MinimumNArgs(1), Short: shortHelp, Long: longHelp, RunE: runAction, ValidArgsFunction: runShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().SetInterspersed(false) setCreateFlags(cmd) cmd.Flags().BoolP("detach", "d", false, "Run container in background and print container ID") cmd.Flags().StringSliceP("attach", "a", []string{}, "Attach STDIN, STDOUT, or STDERR") return cmd } func setCreateFlags(cmd *cobra.Command) { // No "-h" alias for "--help", because "-h" for "--hostname". cmd.Flags().Bool("help", false, "show help") cmd.Flags().BoolP("tty", "t", false, "Allocate a pseudo-TTY") cmd.Flags().Bool("sig-proxy", true, "Proxy received signals to the process (default true)") cmd.Flags().BoolP("interactive", "i", false, "Keep STDIN open even if not attached") cmd.Flags().String("restart", "no", `Restart policy to apply when a container exits (implemented values: "no"|"always|on-failure:n|unless-stopped")`) cmd.RegisterFlagCompletionFunc("restart", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"no", "always", "on-failure", "unless-stopped"}, cobra.ShellCompDirectiveNoFileComp }) cmd.Flags().Bool("rm", false, "Automatically remove the container when it exits") cmd.Flags().String("pull", "missing", `Pull image before running ("always"|"missing"|"never")`) cmd.Flags().BoolP("quiet", "q", false, "Suppress the pull output") cmd.RegisterFlagCompletionFunc("pull", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"always", "missing", "never"}, cobra.ShellCompDirectiveNoFileComp }) cmd.Flags().String("stop-signal", "SIGTERM", "Signal to stop a container") cmd.Flags().Int("stop-timeout", 0, "Timeout (in seconds) to stop a container") cmd.Flags().String("detach-keys", consoleutil.DefaultDetachKeys, "Override the default detach keys") // #region for init process cmd.Flags().Bool("init", false, "Run an init process inside the container, Default to use tini") cmd.Flags().String("init-binary", tiniInitBinary, "The custom binary to use as the init process") // #endregion // #region platform flags cmd.Flags().String("platform", "", "Set platform (e.g. \"amd64\", \"arm64\")") // not a slice, and there is no --all-platforms cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) // #endregion // #region network flags // network (net) is defined as StringSlice, not StringArray, to allow specifying "--network=cni1,cni2" cmd.Flags().StringSlice("network", []string{netutil.DefaultNetworkName}, `Connect a container to a network ("bridge"|"host"|"none"|"container:"|"ns:"|)`) cmd.RegisterFlagCompletionFunc("network", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.NetworkNames(cmd, []string{}) }) cmd.Flags().StringSlice("net", []string{netutil.DefaultNetworkName}, `Connect a container to a network ("bridge"|"host"|"none"|"container:"|"ns:"|)`) cmd.RegisterFlagCompletionFunc("net", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.NetworkNames(cmd, []string{}) }) // dns is defined as StringSlice, not StringArray, to allow specifying "--dns=1.1.1.1,8.8.8.8" (compatible with Podman) cmd.Flags().StringSlice("dns", nil, "Set custom DNS servers") cmd.Flags().StringSlice("dns-search", nil, "Set custom DNS search domains") // We allow for both "--dns-opt" and "--dns-option", although the latter is the recommended way. cmd.Flags().StringSlice("dns-opt", nil, "Set DNS options") cmd.Flags().StringSlice("dns-option", nil, "Set DNS options") // publish is defined as StringSlice, not StringArray, to allow specifying "--publish=80:80,443:443" (compatible with Podman) cmd.Flags().StringSliceP("publish", "p", nil, "Publish a container's port(s) to the host") cmd.Flags().String("ip", "", "IPv4 address to assign to the container") cmd.Flags().String("ip6", "", "IPv6 address to assign to the container") cmd.Flags().StringP("hostname", "h", "", "Container host name") cmd.Flags().String("domainname", "", "Container domain name") cmd.Flags().String("mac-address", "", "MAC address to assign to the container") // #endregion cmd.Flags().String("ipc", "", `IPC namespace to use ("host"|"private"|"shareable"|"container:")`) cmd.RegisterFlagCompletionFunc("ipc", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if strings.HasPrefix(toComplete, "container:") { names, directive := completion.ContainerNames(cmd, func(st containerd.ProcessStatus) bool { return st == containerd.Running }) var candidates []string for _, name := range names { candidates = append(candidates, "container:"+name) } return candidates, directive } return []string{"host", "private", "shareable", "container:"}, cobra.ShellCompDirectiveNoSpace }) // #region cgroups, namespaces, and ulimits flags cmd.Flags().Float64("cpus", 0.0, "Number of CPUs") cmd.Flags().StringP("memory", "m", "", "Memory limit") cmd.Flags().String("memory-reservation", "", "Memory soft limit") cmd.Flags().String("memory-swap", "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") cmd.Flags().Int64("memory-swappiness", -1, "Tune container memory swappiness (0 to 100) (default -1)") cmd.Flags().String("kernel-memory", "", "Kernel memory limit (deprecated)") cmd.Flags().Bool("oom-kill-disable", false, "Disable OOM Killer") cmd.Flags().Int("oom-score-adj", 0, "Tune container’s OOM preferences (-1000 to 1000, rootless: 100 to 1000)") cmd.Flags().String("pid", "", "PID namespace to use") cmd.Flags().String("uts", "", "UTS namespace to use") cmd.RegisterFlagCompletionFunc("pid", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"host"}, cobra.ShellCompDirectiveNoFileComp }) cmd.Flags().Int64("pids-limit", -1, "Tune container pids limit (set -1 for unlimited)") cmd.Flags().StringSlice("cgroup-conf", nil, "Configure cgroup v2 (key=value)") cmd.Flags().String("cgroupns", defaults.CgroupnsMode(), `Cgroup namespace to use, the default depends on the cgroup version ("host"|"private")`) cmd.Flags().String("cgroup-parent", "", "Optional parent cgroup for the container") cmd.RegisterFlagCompletionFunc("cgroupns", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"host", "private"}, cobra.ShellCompDirectiveNoFileComp }) cmd.Flags().String("cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)") cmd.Flags().String("cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)") cmd.Flags().Uint64("cpu-shares", 0, "CPU shares (relative weight)") cmd.Flags().Int64("cpu-quota", -1, "Limit CPU CFS (Completely Fair Scheduler) quota") cmd.Flags().Uint64("cpu-period", 0, "Limit CPU CFS (Completely Fair Scheduler) period") cmd.Flags().Uint64("cpu-rt-period", 0, "Limit CPU real-time period in microseconds") cmd.Flags().Uint64("cpu-rt-runtime", 0, "Limit CPU real-time runtime in microseconds") // device is defined as StringSlice, not StringArray, to allow specifying "--device=DEV1,DEV2" (compatible with Podman) cmd.Flags().StringSlice("device", nil, "Add a host device to the container") // ulimit is defined as StringSlice, not StringArray, to allow specifying "--ulimit=ULIMIT1,ULIMIT2" (compatible with Podman) cmd.Flags().StringSlice("ulimit", nil, "Ulimit options") cmd.Flags().String("rdt-class", "", "Name of the RDT class (or CLOS) to associate the container with") // #endregion // #region blkio flags cmd.Flags().Uint16("blkio-weight", 0, "Block IO (relative weight), between 10 and 1000, or 0 to disable (default 0)") cmd.Flags().StringArray("blkio-weight-device", []string{}, "Block IO weight (relative device weight) (default [])") cmd.Flags().StringArray("device-read-bps", []string{}, "Limit read rate (bytes per second) from a device (default [])") cmd.Flags().StringArray("device-read-iops", []string{}, "Limit read rate (IO per second) from a device (default [])") cmd.Flags().StringArray("device-write-bps", []string{}, "Limit write rate (bytes per second) to a device (default [])") cmd.Flags().StringArray("device-write-iops", []string{}, "Limit write rate (IO per second) to a device (default [])") // #endregion // user flags cmd.Flags().StringP("user", "u", "", "Username or UID (format: [:])") cmd.Flags().String("umask", "", "Set the umask inside the container. Defaults to 0022") cmd.Flags().StringSlice("group-add", []string{}, "Add additional groups to join") // #region security flags cmd.Flags().StringArray("security-opt", []string{}, "Security options") cmd.RegisterFlagCompletionFunc("security-opt", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{ "seccomp=", "seccomp=" + defaults.SeccompProfileName, "seccomp=unconfined", "apparmor=", "apparmor=" + defaults.AppArmorProfileName, "apparmor=unconfined", "no-new-privileges", "systempaths=unconfined", "privileged-without-host-devices"}, cobra.ShellCompDirectiveNoFileComp }) // cap-add and cap-drop are defined as StringSlice, not StringArray, to allow specifying "--cap-add=CAP_SYS_ADMIN,CAP_NET_ADMIN" (compatible with Podman) cmd.Flags().StringSlice("cap-add", []string{}, "Add Linux capabilities") cmd.RegisterFlagCompletionFunc("cap-add", capShellComplete) cmd.Flags().StringSlice("cap-drop", []string{}, "Drop Linux capabilities") cmd.RegisterFlagCompletionFunc("cap-drop", capShellComplete) cmd.Flags().Bool("privileged", false, "Give extended privileges to this container") cmd.Flags().String("systemd", "false", "Allow running systemd in this container (default: false)") // #endregion // #region runtime flags cmd.Flags().String("runtime", defaults.Runtime, "Runtime to use for this container, e.g. \"crun\", or \"io.containerd.runsc.v1\"") // sysctl needs to be StringArray, not StringSlice, to prevent "foo=foo1,foo2" from being split to {"foo=foo1", "foo2"} cmd.Flags().StringArray("sysctl", nil, "Sysctl options") // gpus needs to be StringArray, not StringSlice, to prevent "capabilities=utility,device=DEV" from being split to {"capabilities=utility", "device=DEV"} cmd.Flags().StringArray("gpus", nil, "GPU devices to add to the container ('all' to pass all GPUs)") cmd.RegisterFlagCompletionFunc("gpus", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"all"}, cobra.ShellCompDirectiveNoFileComp }) // #endregion // #region mount flags // volume needs to be StringArray, not StringSlice, to prevent "/foo:/foo:ro,Z" from being split to {"/foo:/foo:ro", "Z"} cmd.Flags().StringArrayP("volume", "v", nil, "Bind mount a volume") // tmpfs needs to be StringArray, not StringSlice, to prevent "/foo:size=64m,exec" from being split to {"/foo:size=64m", "exec"} cmd.Flags().StringArray("tmpfs", nil, "Mount a tmpfs directory") cmd.Flags().StringArray("mount", nil, "Attach a filesystem mount to the container") // volumes-from needs to be StringArray, not StringSlice, to prevent "id1,id2" from being split to {"id1", "id2"} (compatible with Docker) cmd.Flags().StringArray("volumes-from", nil, "Mount volumes from the specified container(s)") // #endregion // rootfs flags cmd.Flags().Bool("read-only", false, "Mount the container's root filesystem as read only") // rootfs flags (from Podman) cmd.Flags().Bool("rootfs", false, "The first argument is not an image but the rootfs to the exploded container") // Health check flags cmd.Flags().String("health-cmd", "", "Command to run to check health") cmd.Flags().Duration("health-interval", 0, "Time between running the check (default: 30s)") cmd.Flags().Duration("health-timeout", 0, "Maximum time to allow one check to run (default: 30s)") cmd.Flags().Int("health-retries", 0, "Consecutive failures needed to report unhealthy (default: 3)") cmd.Flags().Duration("health-start-period", 0, "Start period for the container to initialize before starting health-retries countdown") cmd.Flags().Bool("no-healthcheck", false, "Disable any container-specified HEALTHCHECK") // #region env flags // entrypoint needs to be StringArray, not StringSlice, to prevent "FOO=foo1,foo2" from being split to {"FOO=foo1", "foo2"} // entrypoint StringArray is an internal implementation to support `nerdctl compose` entrypoint yaml filed with multiple strings // users are not expected to specify multiple --entrypoint flags manually. cmd.Flags().StringArray("entrypoint", nil, "Overwrite the default ENTRYPOINT of the image") cmd.Flags().StringP("workdir", "w", "", "Working directory inside the container") // env needs to be StringArray, not StringSlice, to prevent "FOO=foo1,foo2" from being split to {"FOO=foo1", "foo2"} cmd.Flags().StringArrayP("env", "e", nil, "Set environment variables") // add-host is defined as StringSlice, not StringArray, to allow specifying "--add-host=HOST1:IP1,HOST2:IP2" (compatible with Podman) cmd.Flags().StringSlice("add-host", nil, "Add a custom host-to-IP mapping (host:ip)") // env-file is defined as StringSlice, not StringArray, to allow specifying "--env-file=FILE1,FILE2" (compatible with Podman) cmd.Flags().StringSlice("env-file", nil, "Set environment variables from file") // #region metadata flags cmd.Flags().String("name", "", "Assign a name to the container") // label needs to be StringArray, not StringSlice, to prevent "foo=foo1,foo2" from being split to {"foo=foo1", "foo2"} cmd.Flags().StringArrayP("label", "l", nil, "Set metadata on container") // annotation needs to be StringArray, not StringSlice, to prevent "foo=foo1,foo2" from being split to {"foo=foo1", "foo2"} cmd.Flags().StringArray("annotation", nil, "Add an annotation to the container (passed through to the OCI runtime)") cmd.RegisterFlagCompletionFunc("annotation", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return annotations.ShellCompletions, cobra.ShellCompDirectiveNoFileComp }) // label-file is defined as StringSlice, not StringArray, to allow specifying "--env-file=FILE1,FILE2" (compatible with Podman) cmd.Flags().StringSlice("label-file", nil, "Set metadata on container from file") cmd.Flags().String("cidfile", "", "Write the container ID to the file") // #endregion // #region logging flags // log-opt needs to be StringArray, not StringSlice, to prevent "env=os,customer" from being split to {"env=os", "customer"} cmd.Flags().String("log-driver", "json-file", "Logging driver for the container. Default is json-file. It also supports logURI (eg: --log-driver binary://)") cmd.RegisterFlagCompletionFunc("log-driver", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return logging.Drivers(), cobra.ShellCompDirectiveNoFileComp }) cmd.Flags().StringArray("log-opt", nil, "Log driver options") // #endregion // shared memory flags cmd.Flags().String("shm-size", "", "Size of /dev/shm") cmd.Flags().String("pidfile", "", "file path to write the task's pid") // #region verify flags cmd.Flags().String("verify", "none", "Verify the image (none|cosign|notation)") cmd.RegisterFlagCompletionFunc("verify", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"none", "cosign", "notation"}, cobra.ShellCompDirectiveNoFileComp }) cmd.Flags().String("cosign-key", "", "Path to the public key file, KMS, URI or Kubernetes Secret for --verify=cosign") cmd.Flags().String("cosign-certificate-identity", "", "The identity expected in a valid Fulcio certificate for --verify=cosign. Valid values include email address, DNS names, IP addresses, and URIs. Either --cosign-certificate-identity or --cosign-certificate-identity-regexp must be set for keyless flows") cmd.Flags().String("cosign-certificate-identity-regexp", "", "A regular expression alternative to --cosign-certificate-identity for --verify=cosign. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either --cosign-certificate-identity or --cosign-certificate-identity-regexp must be set for keyless flows") cmd.Flags().String("cosign-certificate-oidc-issuer", "", "The OIDC issuer expected in a valid Fulcio certificate for --verify=cosign, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth. Either --cosign-certificate-oidc-issuer or --cosign-certificate-oidc-issuer-regexp must be set for keyless flows") cmd.Flags().String("cosign-certificate-oidc-issuer-regexp", "", "A regular expression alternative to --certificate-oidc-issuer for --verify=cosign. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either --cosign-certificate-oidc-issuer or --cosign-certificate-oidc-issuer-regexp must be set for keyless flows") // #endregion cmd.Flags().String("ipfs-address", "", "multiaddr of IPFS API (default uses $IPFS_PATH env variable if defined or local directory ~/.ipfs)") cmd.Flags().String("isolation", "default", "Specify isolation technology for container. On Linux the only valid value is default. Windows options are host, process and hyperv with process isolation as the default") cmd.RegisterFlagCompletionFunc("isolation", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if runtime.GOOS == "windows" { return []string{"default", "host", "process", "hyperv"}, cobra.ShellCompDirectiveNoFileComp } return []string{"default"}, cobra.ShellCompDirectiveNoFileComp }) cmd.Flags().String("userns", "", "Specify host to disable userns-remap") } func processCreateCommandFlagsInRun(cmd *cobra.Command) (types.ContainerCreateOptions, error) { opt, err := createOptions(cmd) if err != nil { return opt, err } opt.InRun = true opt.SigProxy, err = cmd.Flags().GetBool("sig-proxy") if err != nil { return opt, err } opt.Interactive, err = cmd.Flags().GetBool("interactive") if err != nil { return opt, err } opt.Detach, err = cmd.Flags().GetBool("detach") if err != nil { return opt, err } opt.DetachKeys, err = cmd.Flags().GetString("detach-keys") if err != nil { return opt, err } opt.Attach, err = cmd.Flags().GetStringSlice("attach") if err != nil { return opt, err } validAttachFlag := true for i, str := range opt.Attach { opt.Attach[i] = strings.ToUpper(str) if opt.Attach[i] != "STDIN" && opt.Attach[i] != "STDOUT" && opt.Attach[i] != "STDERR" { validAttachFlag = false } } if !validAttachFlag { return opt, fmt.Errorf("invalid stream specified with -a flag. Valid streams are STDIN, STDOUT, and STDERR") } return opt, nil } // runAction is heavily based on ctr implementation: // https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/run/run.go func runAction(cmd *cobra.Command, args []string) error { var isDetached bool createOpt, err := processCreateCommandFlagsInRun(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClientWithPlatform(cmd.Context(), createOpt.GOptions.Namespace, createOpt.GOptions.Address, createOpt.Platform) if err != nil { return err } defer cancel() if createOpt.Rm && createOpt.Detach { return errors.New("flags -d and --rm cannot be specified together") } if len(createOpt.Attach) > 0 && createOpt.Detach { return errors.New("flags -d and -a cannot be specified together") } netFlags, err := loadNetworkFlags(cmd, createOpt.GOptions) if err != nil { return fmt.Errorf("failed to load networking flags: %w", err) } netManager, err := containerutil.NewNetworkingOptionsManager(createOpt.GOptions, netFlags, client) if err != nil { return err } c, gc, err := container.Create(ctx, client, args, netManager, createOpt) if err != nil { if gc != nil { defer gc() } return err } // defer setting `nerdctl/error` label in case of error defer func() { if err != nil { containerutil.UpdateErrorLabel(ctx, c, err) } }() id := c.ID() if createOpt.Rm { defer func() { if isDetached { return } if err := container.RemoveContainer(ctx, c, createOpt.GOptions, true, true, client); err != nil { log.L.WithError(err).Warnf("failed to remove container %s", id) } }() } var con console.Console if createOpt.TTY && !createOpt.Detach { con, err = consoleutil.Current() if err != nil { return err } defer con.Reset() if _, err := term.MakeRaw(int(con.Fd())); err != nil { return err } } lab, err := c.Labels(ctx) if err != nil { return err } logURI := lab[labels.LogURI] detachC := make(chan struct{}) task, err := taskutil.NewTask(ctx, client, c, taskutil.TaskOptions{ AttachStreamOpt: createOpt.Attach, IsInteractive: createOpt.Interactive, IsTerminal: createOpt.TTY, IsDetach: createOpt.Detach, Con: con, LogURI: logURI, DetachKeys: createOpt.DetachKeys, Namespace: createOpt.GOptions.Namespace, DetachC: detachC, CheckpointDir: "", }) if err != nil { return err } statusC, err := task.Wait(ctx) if err != nil { return err } if err := task.Start(ctx); err != nil { return err } // Setup container healthchecks. if err := healthcheck.CreateTimer(ctx, c, (*config.Config)(&createOpt.GOptions), createOpt.NerdctlCmd, createOpt.NerdctlArgs); err != nil { return fmt.Errorf("failed to create healthcheck timer: %w", err) } if err := healthcheck.StartTimer(ctx, c, (*config.Config)(&createOpt.GOptions)); err != nil { return fmt.Errorf("failed to start healthcheck timer: %w", err) } if createOpt.Detach { fmt.Fprintln(createOpt.Stdout, id) return nil } if createOpt.TTY { if err := consoleutil.HandleConsoleResize(ctx, task, con); err != nil { log.L.WithError(err).Error("console resize") } } else { if createOpt.SigProxy { sigC := signalutil.ForwardAllSignals(ctx, task) defer signalutil.StopCatch(sigC) } } select { // io.Wait() would return when either 1) the user detaches from the container OR 2) the container is about to exit. // // If we replace the `select` block with io.Wait() and // directly use task.Status() to check the status of the container after io.Wait() returns, // it can still be running even though the container is about to exit (somehow especially for Windows). // // As a result, we need a separate detachC to distinguish from the 2 cases mentioned above. case <-detachC: io := task.IO() if io == nil { return errors.New("got a nil IO from the task") } io.Wait() isDetached = true case status := <-statusC: if createOpt.Rm { if _, taskDeleteErr := task.Delete(ctx); taskDeleteErr != nil { log.L.Error(taskDeleteErr) } } code, _, err := status.Result() if err != nil { return err } if code != 0 { return errutil.NewExitCoderErr(int(code)) } } return nil } func runShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) == 0 { return completion.ImageNames(cmd) } return nil, cobra.ShellCompDirectiveNoFileComp } ================================================ FILE: cmd/nerdctl/container/container_run_cgroup_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "bytes" "context" "fmt" "os" "path/filepath" "regexp" "strconv" "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/cgroups/v3" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/continuity/testutil/loopback" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/cmd/container" "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRunCgroupV2(t *testing.T) { t.Parallel() if cgroups.Mode() != cgroups.Unified { t.Skip("test requires cgroup v2") } base := testutil.NewBase(t) info := base.Info() switch info.CgroupDriver { case "none", "": t.Skip("test requires cgroup driver") } if !info.MemoryLimit { t.Skip("test requires MemoryLimit") } if !info.SwapLimit { t.Skip("test requires SwapLimit") } if !info.CPUSet { t.Skip("test requires CPUSet") } if !info.PidsLimit { t.Skip("test requires PidsLimit") } const expected1 = `42000 100000 44040192 44040192 42 0-1 0 ` const expected2 = `42000 100000 44040192 60817408 6291456 42 0-1 0 ` base.Cmd("run", "--rm", "--cpus", "0.42", "--cpuset-mems", "0", "--memory", "42m", "--pids-limit", "42", "--cpuset-cpus", "0-1", "-w", "/sys/fs/cgroup", testutil.AlpineImage, "cat", "cpu.max", "memory.max", "memory.swap.max", "pids.max", "cpuset.cpus", "cpuset.mems").AssertOutExactly(expected1) base.Cmd("run", "--rm", "--cpu-quota", "42000", "--cpuset-mems", "0", "--cpu-period", "100000", "--memory", "42m", "--memory-reservation", "6m", "--memory-swap", "100m", "--pids-limit", "42", "--cpuset-cpus", "0-1", "-w", "/sys/fs/cgroup", testutil.AlpineImage, "cat", "cpu.max", "memory.max", "memory.swap.max", "memory.low", "pids.max", "cpuset.cpus", "cpuset.mems").AssertOutExactly(expected2) base.Cmd("run", "--name", testutil.Identifier(t)+"-testUpdate1", "-w", "/sys/fs/cgroup", "-d", testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK() defer base.Cmd("rm", "-f", testutil.Identifier(t)+"-testUpdate1").Run() update := []string{"update", "--cpu-quota", "42000", "--cpuset-mems", "0", "--cpu-period", "100000", "--memory", "42m", "--pids-limit", "42", "--cpuset-cpus", "0-1"} if nerdtest.IsDocker() && info.CgroupVersion == "2" && info.SwapLimit { // Workaround for Docker with cgroup v2: // > Error response from daemon: Cannot update container 67c13276a13dd6a091cdfdebb355aa4e1ecb15fbf39c2b5c9abee89053e88fce: // > Memory limit should be smaller than already set memoryswap limit, update the memoryswap at the same time update = append(update, "--memory-swap=84m") } update = append(update, testutil.Identifier(t)+"-testUpdate1") base.Cmd(update...).AssertOK() base.Cmd("exec", testutil.Identifier(t)+"-testUpdate1", "cat", "cpu.max", "memory.max", "memory.swap.max", "pids.max", "cpuset.cpus", "cpuset.mems").AssertOutExactly(expected1) defer base.Cmd("rm", "-f", testutil.Identifier(t)+"-testUpdate2").Run() base.Cmd("run", "--name", testutil.Identifier(t)+"-testUpdate2", "-w", "/sys/fs/cgroup", "-d", testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK() base.EnsureContainerStarted(testutil.Identifier(t) + "-testUpdate2") base.Cmd("update", "--cpu-quota", "42000", "--cpuset-mems", "0", "--cpu-period", "100000", "--memory", "42m", "--memory-reservation", "6m", "--memory-swap", "100m", "--pids-limit", "42", "--cpuset-cpus", "0-1", testutil.Identifier(t)+"-testUpdate2").AssertOK() base.Cmd("exec", testutil.Identifier(t)+"-testUpdate2", "cat", "cpu.max", "memory.max", "memory.swap.max", "memory.low", "pids.max", "cpuset.cpus", "cpuset.mems").AssertOutExactly(expected2) base.Cmd("run", "--rm", "--security-opt", "writable-cgroups=true", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/foo").AssertOK() base.Cmd("run", "--rm", "--security-opt", "writable-cgroups=false", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/foo").AssertFail() base.Cmd("run", "--rm", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/foo").AssertFail() } func TestRunCgroupV1(t *testing.T) { t.Parallel() switch cgroups.Mode() { case cgroups.Legacy, cgroups.Hybrid: default: t.Skip("test requires cgroup v1") } base := testutil.NewBase(t) info := base.Info() switch info.CgroupDriver { case "none", "": t.Skip("test requires cgroup driver") } if !info.MemoryLimit { t.Skip("test requires MemoryLimit") } if !info.CPUShares { t.Skip("test requires CPUShares") } if !info.CPUSet { t.Skip("test requires CPUSet") } if !info.PidsLimit { t.Skip("test requires PidsLimit") } quota := "/sys/fs/cgroup/cpu/cpu.cfs_quota_us" period := "/sys/fs/cgroup/cpu/cpu.cfs_period_us" cpusetMems := "/sys/fs/cgroup/cpuset/cpuset.mems" memoryLimit := "/sys/fs/cgroup/memory/memory.limit_in_bytes" memoryReservation := "/sys/fs/cgroup/memory/memory.soft_limit_in_bytes" memorySwap := "/sys/fs/cgroup/memory/memory.memsw.limit_in_bytes" memorySwappiness := "/sys/fs/cgroup/memory/memory.swappiness" pidsLimit := "/sys/fs/cgroup/pids/pids.max" cpuShare := "/sys/fs/cgroup/cpu/cpu.shares" cpusetCpus := "/sys/fs/cgroup/cpuset/cpuset.cpus" const expected = "42000\n100000\n0\n44040192\n6291456\n104857600\n0\n42\n2000\n0-1\n" base.Cmd("run", "--rm", "--cpus", "0.42", "--cpuset-mems", "0", "--memory", "42m", "--memory-reservation", "6m", "--memory-swap", "100m", "--memory-swappiness", "0", "--pids-limit", "42", "--cpu-shares", "2000", "--cpuset-cpus", "0-1", testutil.AlpineImage, "cat", quota, period, cpusetMems, memoryLimit, memoryReservation, memorySwap, memorySwappiness, pidsLimit, cpuShare, cpusetCpus).AssertOutExactly(expected) base.Cmd("run", "--rm", "--cpu-quota", "42000", "--cpu-period", "100000", "--cpuset-mems", "0", "--memory", "42m", "--memory-reservation", "6m", "--memory-swap", "100m", "--memory-swappiness", "0", "--pids-limit", "42", "--cpu-shares", "2000", "--cpuset-cpus", "0-1", testutil.AlpineImage, "cat", quota, period, cpusetMems, memoryLimit, memoryReservation, memorySwap, memorySwappiness, pidsLimit, cpuShare, cpusetCpus).AssertOutExactly(expected) base.Cmd("run", "--rm", "--security-opt", "writable-cgroups=true", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/pids/foo").AssertOK() base.Cmd("run", "--rm", "--security-opt", "writable-cgroups=false", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/pids/foo").AssertFail() base.Cmd("run", "--rm", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/pids/foo").AssertFail() } // TestIssue3781 tests https://github.com/containerd/nerdctl/issues/3781 func TestIssue3781(t *testing.T) { t.Parallel() testCase := nerdtest.Setup() testCase.Require = require.Not(nerdtest.Docker) base := testutil.NewBase(t) info := base.Info() switch info.CgroupDriver { case "none", "": t.Skip("test requires cgroup driver") } containerName := testutil.Identifier(t) base.Cmd("run", "-d", "--name", containerName, testutil.AlpineImage, "sleep", "infinity").AssertOK() defer func() { base.Cmd("rm", "-f", containerName) }() base.Cmd("update", "--cpuset-cpus", "0-1", containerName).AssertOK() addr := base.ContainerdAddress() client, err := containerd.New(addr, containerd.WithDefaultNamespace(testutil.Namespace)) assert.NilError(base.T, err) ctx := context.Background() // get container id by container name. var cid string var args []string args = append(args, containerName) walker := &containerwalker.ContainerWalker{ Client: client, OnFound: func(ctx context.Context, found containerwalker.Found) error { if found.MatchCount > 1 { return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) } cid = found.Container.ID() return nil }, } err = walker.WalkAll(ctx, args, true) assert.NilError(base.T, err) container, err := client.LoadContainer(ctx, cid) assert.NilError(base.T, err) spec, err := container.Spec(ctx) assert.NilError(base.T, err) assert.Equal(t, spec.Linux.Resources.Pids == nil, true) } func TestRunDevice(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = nerdtest.Rootful const n = 3 lo := make([]*loopback.Loopback, n) testCase.Setup = func(data test.Data, helpers test.Helpers) { for i := 0; i < n; i++ { var err error lo[i], err = loopback.New(4096) assert.NilError(t, err) t.Logf("lo[%d] = %+v", i, lo[i]) loContent := fmt.Sprintf("lo%d-content", i) assert.NilError(t, os.WriteFile(lo[i].Device, []byte(loContent), 0o700)) data.Labels().Set("loContent"+strconv.Itoa(i), loContent) } // lo0 is readable but not writable. // lo1 is readable and writable // lo2 is not accessible. helpers.Ensure("run", "-d", "--name", data.Identifier(), "--device", lo[0].Device+":r", "--device", lo[1].Device, testutil.AlpineImage, "sleep", nerdtest.Infinity) data.Labels().Set("id", data.Identifier()) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { for i := 0; i < n; i++ { if lo[i] != nil { _ = lo[i].Close() } } helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.SubTests = []*test.Case{ { Description: "can read lo0", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Labels().Get("id"), "cat", lo[0].Device) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.Contains(data.Labels().Get("locontent0")), } }, }, { Description: "cannot write lo0", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Labels().Get("id"), "sh", "-ec", "echo -n \"overwritten-lo1-content\">"+lo[0].Device) }, Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, { Description: "cannot read lo2", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Labels().Get("id"), "cat", lo[2].Device) }, Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, { Description: "can read lo1", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Labels().Get("id"), "cat", lo[1].Device) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.Contains(data.Labels().Get("locontent1")), } }, }, { Description: "can write lo1 and read back updated value", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Labels().Get("id"), "sh", "-ec", "echo -n \"overwritten-lo1-content\">"+lo[1].Device) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, t tig.T) { lo1Read, err := os.ReadFile(lo[1].Device) assert.NilError(t, err) assert.Equal(t, string(bytes.Trim(lo1Read, "\x00")), "overwritten-lo1-content") }), }, } testCase.Run(t) } func TestParseDevice(t *testing.T) { t.Parallel() type testCase struct { s string expectedDevPath string expectedContainerPath string expectedMode string err string } testCases := []testCase{ { s: "/dev/sda1", expectedDevPath: "/dev/sda1", expectedContainerPath: "/dev/sda1", expectedMode: "rwm", }, { s: "/dev/sda2:r", expectedDevPath: "/dev/sda2", expectedContainerPath: "/dev/sda2", expectedMode: "r", }, { s: "/dev/sda3:rw", expectedDevPath: "/dev/sda3", expectedContainerPath: "/dev/sda3", expectedMode: "rw", }, { s: "sda4", err: "not an absolute path", }, { s: "/dev/sda5:/dev/sda5", expectedDevPath: "/dev/sda5", expectedContainerPath: "/dev/sda5", expectedMode: "rwm", }, { s: "/dev/sda6:/dev/foo6", expectedDevPath: "/dev/sda6", expectedContainerPath: "/dev/foo6", expectedMode: "rwm", }, { s: "/dev/sda7:/dev/sda7:rwmx", err: "unexpected rune", }, } for _, tc := range testCases { t.Log(tc.s) devPath, containerPath, mode, err := container.ParseDevice(tc.s) if tc.err == "" { assert.NilError(t, err) assert.Equal(t, tc.expectedDevPath, devPath) assert.Equal(t, tc.expectedContainerPath, containerPath) assert.Equal(t, tc.expectedMode, mode) } else { assert.ErrorContains(t, err, tc.err) } } } func TestRunCgroupConf(t *testing.T) { t.Parallel() if cgroups.Mode() != cgroups.Unified { t.Skip("test requires cgroup v2") } testutil.DockerIncompatible(t) // Docker lacks --cgroup-conf base := testutil.NewBase(t) info := base.Info() switch info.CgroupDriver { case "none", "": t.Skip("test requires cgroup driver") } if !info.MemoryLimit { t.Skip("test requires MemoryLimit") } base.Cmd("run", "--rm", "--cgroup-conf", "memory.high=33554432", "-w", "/sys/fs/cgroup", testutil.AlpineImage, "cat", "memory.high").AssertOutExactly("33554432\n") } func TestRunCgroupParent(t *testing.T) { t.Parallel() base := testutil.NewBase(t) info := base.Info() switch info.CgroupDriver { case "none", "": t.Skip("test requires cgroup driver") } containerName := testutil.Identifier(t) t.Logf("Using %q cgroup driver", info.CgroupDriver) parent := "/foobarbaz" if info.CgroupDriver == "systemd" { // Path separators aren't allowed in systemd path. runc // explicitly checks for this. // https://github.com/opencontainers/runc/blob/016a0d29d1750180b2a619fc70d6fe0d80111be0/libcontainer/cgroups/systemd/common.go#L65-L68 parent = "foobarbaz.slice" } tearDown := func() { base.Cmd("rm", "-f", containerName).Run() } tearDown() t.Cleanup(tearDown) // cgroup2 without host cgroup ns will just output 0::/ which doesn't help much to verify // we got our expected path. This approach should work for both cgroup1 and 2, there will // just be many more entries for cgroup1 as there'll be an entry per controller. base.Cmd( "run", "-d", "--name", containerName, "--cgroupns=host", "--cgroup-parent", parent, testutil.AlpineImage, "sleep", "infinity", ).AssertOK() id := base.InspectContainer(containerName).ID expected := filepath.Join(parent, id) if info.CgroupDriver == "systemd" { expected = filepath.Join(parent, fmt.Sprintf("nerdctl-%s", id)) if nerdtest.IsDocker() { expected = filepath.Join(parent, fmt.Sprintf("docker-%s", id)) } } base.Cmd("exec", containerName, "cat", "/proc/self/cgroup").AssertOutContains(expected) } func TestRunBlkioWeightCgroupV2(t *testing.T) { t.Parallel() if cgroups.Mode() != cgroups.Unified { t.Skip("test requires cgroup v2") } if _, err := os.Stat("/sys/module/bfq"); err != nil { t.Skipf("test requires \"bfq\" module to be loaded: %v", err) } base := testutil.NewBase(t) info := base.Info() switch info.CgroupDriver { case "none", "": t.Skip("test requires cgroup driver") } containerName := testutil.Identifier(t) defer base.Cmd("rm", "-f", containerName).AssertOK() // when bfq io scheduler is used, the io.weight knob is exposed as io.bfq.weight base.Cmd("run", "--name", containerName, "--blkio-weight", "300", "-w", "/sys/fs/cgroup", testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK() base.Cmd("exec", containerName, "cat", "io.bfq.weight").AssertOutExactly("default 300\n") base.Cmd("update", containerName, "--blkio-weight", "400").AssertOK() base.Cmd("exec", containerName, "cat", "io.bfq.weight").AssertOutExactly("default 400\n") } func TestRunBlkioSettingCgroupV2(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = nerdtest.Rootful // See https://github.com/containerd/nerdctl/issues/4185 // It is unclear if this is truly a kernel version problem, a runc issue, or a distro (EL9) issue. // For now, disable the test unless on a recent kernel. testutil.RequireKernelVersion(t, ">= 6.0.0-0") const ( weight = "150" deviceWeight = "100" readBps = "1048576" readIops = "1000" writeBps = "2097152" writeIops = "2000" ) var lo *loopback.Loopback testCase.Setup = func(data test.Data, helpers test.Helpers) { var err error lo, err = loopback.New(4096) assert.NilError(t, err) t.Logf("loopback device: %+v", lo) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if lo != nil { _ = lo.Close() } } testCase.SubTests = []*test.Case{ { Description: "blkio-weight", Require: nerdtest.CGroupV2, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "-d", "--name", data.Identifier(), "--blkio-weight", weight, testutil.AlpineImage, "sleep", "infinity") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All( func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "{{.HostConfig.BlkioWeight}}", data.Identifier()), weight)) }, ), } }, }, { Description: "blkio-weight-device", Require: nerdtest.CGroupV2, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "-d", "--name", data.Identifier(), "--blkio-weight-device", fmt.Sprintf("%s:%s", lo.Device, deviceWeight), testutil.AlpineImage, "sleep", "infinity") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All( func(stdout string, t tig.T) { inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioWeightDevice}}{{.Weight}}{{end}}", data.Identifier()) assert.Assert(t, strings.Contains(inspectOut, deviceWeight)) }, func(stdout string, t tig.T) { inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioWeightDevice}}{{.Path}}{{end}}", data.Identifier()) assert.Assert(t, strings.Contains(inspectOut, lo.Device)) }, ), } }, }, { Description: "device-read-bps", Require: require.All( nerdtest.CGroupV2, // Docker cli (v26.1.3) available in github runners has a bug where some of the blkio options // do not work https://github.com/docker/cli/issues/5321. The fix has been merged to the latest releases // but not currently available in the v26 release. require.Not(nerdtest.Docker), ), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "-d", "--name", data.Identifier(), "--device-read-bps", fmt.Sprintf("%s:%s", lo.Device, readBps), testutil.AlpineImage, "sleep", "infinity") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All( func(stdout string, t tig.T) { inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceReadBps}}{{.Rate}}{{end}}", data.Identifier()) assert.Assert(t, strings.Contains(inspectOut, readBps)) }, func(stdout string, t tig.T) { inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceReadBps}}{{.Path}}{{end}}", data.Identifier()) assert.Assert(t, strings.Contains(inspectOut, lo.Device)) }, ), } }, }, { Description: "device-write-bps", Require: require.All( nerdtest.CGroupV2, // Docker cli (v26.1.3) available in github runners has a bug where some of the blkio options // do not work https://github.com/docker/cli/issues/5321. The fix has been merged to the latest releases // but not currently available in the v26 release. require.Not(nerdtest.Docker), ), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "-d", "--name", data.Identifier(), "--device-write-bps", fmt.Sprintf("%s:%s", lo.Device, writeBps), testutil.AlpineImage, "sleep", "infinity") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All( func(stdout string, t tig.T) { inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceWriteBps}}{{.Rate}}{{end}}", data.Identifier()) assert.Assert(t, strings.Contains(inspectOut, writeBps)) }, func(stdout string, t tig.T) { inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceWriteBps}}{{.Path}}{{end}}", data.Identifier()) assert.Assert(t, strings.Contains(inspectOut, lo.Device)) }, ), } }, }, { Description: "device-read-iops", Require: require.All( nerdtest.CGroupV2, // Docker cli (v26.1.3) available in github runners has a bug where some of the blkio options // do not work https://github.com/docker/cli/issues/5321. The fix has been merged to the latest releases // but not currently available in the v26 release. require.Not(nerdtest.Docker), ), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "-d", "--name", data.Identifier(), "--device-read-iops", fmt.Sprintf("%s:%s", lo.Device, readIops), testutil.AlpineImage, "sleep", "infinity") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All( func(stdout string, t tig.T) { inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceReadIOps}}{{.Rate}}{{end}}", data.Identifier()) assert.Assert(t, strings.Contains(inspectOut, readIops)) }, func(stdout string, t tig.T) { inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceReadIOps}}{{.Path}}{{end}}", data.Identifier()) assert.Assert(t, strings.Contains(inspectOut, lo.Device)) }, ), } }, }, { Description: "device-write-iops", Require: require.All( nerdtest.CGroupV2, // Docker cli (v26.1.3) available in github runners has a bug where some of the blkio options // do not work https://github.com/docker/cli/issues/5321. The fix has been merged to the latest releases // but not currently available in the v26 release. require.Not(nerdtest.Docker), ), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "-d", "--name", data.Identifier(), "--device-write-iops", fmt.Sprintf("%s:%s", lo.Device, writeIops), testutil.AlpineImage, "sleep", "infinity") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All( func(stdout string, t tig.T) { inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceWriteIOps}}{{.Rate}}{{end}}", data.Identifier()) assert.Assert(t, strings.Contains(inspectOut, writeIops)) }, func(stdout string, t tig.T) { inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceWriteIOps}}{{.Path}}{{end}}", data.Identifier()) assert.Assert(t, strings.Contains(inspectOut, lo.Device)) }, ), } }, }, } testCase.Run(t) } func TestRunCPURealTimeSettingCgroupV1(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Description: "cpu-rt-runtime-and-period", Require: require.All( require.Not(nerdtest.CGroupV2), nerdtest.Rootful, ), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("create", "--name", data.Identifier(), "--cpu-rt-runtime", "950000", "--cpu-rt-period", "1000000", testutil.AlpineImage, "sleep", "infinity") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All( func(stdout string, t tig.T) { rtRuntime := helpers.Capture("inspect", "--format", "{{.HostConfig.CPURealtimeRuntime}}", data.Identifier()) rtPeriod := helpers.Capture("inspect", "--format", "{{.HostConfig.CPURealtimePeriod}}", data.Identifier()) assert.Assert(t, strings.Contains(rtRuntime, "950000")) assert.Assert(t, strings.Contains(rtPeriod, "1000000")) }, ), } }, } testCase.Run(t) } func TestRunCPUSharesCgroupV2(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.All( nerdtest.CGroupV2, nerdtest.Info( func(info dockercompat.Info) error { if !info.CPUShares { return fmt.Errorf("test requires CPUShares") } return nil }, ), ), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--cpu-shares", "2000", testutil.AlpineImage, "cat", "/sys/fs/cgroup/cpu.weight") }, // The value was historically 77, but with runc v1.4.0-rc.1 it became 170. // https://github.com/opencontainers/runc/issues/4896#issuecomment-3301825811 Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile("^(77|170)\n$"))), } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_run_gpus_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "testing" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "github.com/containerd/nerdctl/v2/pkg/cmd/container" ) func TestParseGpusOptAll(t *testing.T) { t.Parallel() for _, testcase := range []string{ "all", "-1", "count=all", "count=-1", } { req, err := container.ParseGPUOptCSV(testcase) assert.NilError(t, err) assert.Equal(t, req.Count, -1) assert.Equal(t, len(req.DeviceIDs), 0) assert.Equal(t, len(req.Capabilities), 0) } } func TestParseGpusOpts(t *testing.T) { t.Parallel() for _, testcase := range []string{ "driver=nvidia,\"capabilities=compute,utility\"", "1,driver=nvidia,\"capabilities=compute,utility\"", "count=1,driver=nvidia,\"capabilities=compute,utility\"", "driver=nvidia,\"capabilities=compute,utility\",count=1", "\"capabilities=compute,utility\",count=1", } { req, err := container.ParseGPUOptCSV(testcase) assert.NilError(t, err) assert.Equal(t, req.Count, 1) assert.Equal(t, len(req.DeviceIDs), 0) assert.Check(t, is.DeepEqual(req.Capabilities, []string{"compute", "utility"})) } } ================================================ FILE: cmd/nerdctl/container/container_run_linux.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "strings" "github.com/spf13/cobra" "github.com/containerd/containerd/v2/pkg/cap" ) func capShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { candidates := []string{} for _, c := range cap.Known() { // "CAP_SYS_ADMIN" -> "sys_admin" s := strings.ToLower(strings.TrimPrefix(c, "CAP_")) candidates = append(candidates, s) } return candidates, cobra.ShellCompDirectiveNoFileComp } ================================================ FILE: cmd/nerdctl/container/container_run_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "bufio" "bytes" "context" "errors" "fmt" "io" "net/http" "os" "path/filepath" "strconv" "strings" "testing" "time" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/strutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" ) func TestRunCustomRootfs(t *testing.T) { testutil.DockerIncompatible(t) // FIXME: root issue is undiagnosed and this is very likely a containerd bug // It appears that in certain conditions, the proxy content store info method will fail on the layer of the image // Search for func (pcs *proxyContentStore) ReaderAt(ctx context.Context, desc ocispec.Descriptor) (content.ReaderAt, error) { // Note that: // - the problem is still here with containerd and nerdctl v2 // - it seems to affect images that are tagged multiple times, or that share a layer with another image // - this test is not parallelized - but the fact that namespacing it solves the problem suggest that something // happening in the default namespace BEFORE this test is run is SOMETIMES setting conditions that will make this fail // Possible suspects would be concurrent pulls somehow effing things up w. namespaces. base := testutil.NewBaseWithNamespace(t, testutil.Identifier(t)) rootfs := prepareCustomRootfs(base, testutil.AlpineImage) t.Cleanup(func() { base.Cmd("namespace", "remove", testutil.Identifier(t)).Run() }) defer os.RemoveAll(rootfs) base.Cmd("run", "--rm", "--rootfs", rootfs, "/bin/cat", "/proc/self/environ").AssertOutContains("PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin") base.Cmd("run", "--rm", "--entrypoint", "/bin/echo", "--rootfs", rootfs, "echo", "foo").AssertOutExactly("echo foo\n") } func prepareCustomRootfs(base *testutil.Base, imageName string) string { base.Cmd("pull", "--quiet", imageName).AssertOK() tmpDir, err := os.MkdirTemp(base.T.TempDir(), "test-save") assert.NilError(base.T, err) defer os.RemoveAll(tmpDir) archiveTarPath := filepath.Join(tmpDir, "a.tar") base.Cmd("save", "-o", archiveTarPath, imageName).AssertOK() rootfs, err := os.MkdirTemp(base.T.TempDir(), "rootfs") assert.NilError(base.T, err) err = helpers.ExtractDockerArchive(archiveTarPath, rootfs) assert.NilError(base.T, err) return rootfs } func TestRunShmSize(t *testing.T) { t.Parallel() base := testutil.NewBase(t) const shmSize = "32m" base.Cmd("run", "--rm", "--shm-size", shmSize, testutil.AlpineImage, "/bin/grep", "shm", "/proc/self/mounts").AssertOutContains("size=32768k") } func TestRunShmSizeIPCShareable(t *testing.T) { t.Parallel() base := testutil.NewBase(t) const shmSize = "32m" container := testutil.Identifier(t) base.Cmd("run", "--rm", "--name", container, "--ipc", "shareable", "--shm-size", shmSize, testutil.AlpineImage, "/bin/grep", "shm", "/proc/self/mounts").AssertOutContains("size=32768k") defer base.Cmd("rm", "-f", container) } func TestRunIPCShareableRemoveMount(t *testing.T) { t.Parallel() base := testutil.NewBase(t) container := testutil.Identifier(t) base.Cmd("run", "--name", container, "--ipc", "shareable", testutil.AlpineImage, "sleep", "0").AssertOK() base.Cmd("rm", container).AssertOK() } func TestRunIPCContainerNotExists(t *testing.T) { t.Parallel() base := testutil.NewBase(t) container := testutil.Identifier(t) result := base.Cmd("run", "--name", container, "--ipc", "container:abcd1234", testutil.AlpineImage, "sleep", nerdtest.Infinity).Run() defer base.Cmd("rm", "-f", container) combined := result.Combined() if !strings.Contains(strings.ToLower(combined), "no such container: abcd1234") { t.Fatalf("unexpected output: %s", combined) } } func TestRunShmSizeIPCContainer(t *testing.T) { t.Parallel() base := testutil.NewBase(t) const shmSize = "32m" sharedContainerResult := base.Cmd("run", "-d", "--ipc", "shareable", "--shm-size", shmSize, testutil.AlpineImage, "sleep", nerdtest.Infinity).Run() baseContainerID := strings.TrimSpace(sharedContainerResult.Stdout()) defer base.Cmd("rm", "-f", baseContainerID).Run() base.Cmd("run", "--rm", fmt.Sprintf("--ipc=container:%s", baseContainerID), testutil.AlpineImage, "/bin/grep", "shm", "/proc/self/mounts").AssertOutContains("size=32768k") } func TestRunIPCContainer(t *testing.T) { t.Parallel() base := testutil.NewBase(t) const shmSize = "32m" victimContainerResult := base.Cmd("run", "-d", "--ipc", "shareable", "--shm-size", shmSize, testutil.AlpineImage, "sleep", nerdtest.Infinity).Run() victimContainerID := strings.TrimSpace(victimContainerResult.Stdout()) defer base.Cmd("rm", "-f", victimContainerID).Run() base.Cmd("run", "--rm", fmt.Sprintf("--ipc=container:%s", victimContainerID), testutil.AlpineImage, "/bin/grep", "shm", "/proc/self/mounts").AssertOutContains("size=32768k") } func TestRunPidHost(t *testing.T) { t.Parallel() base := testutil.NewBase(t) pid := os.Getpid() base.Cmd("run", "--rm", "--pid=host", testutil.AlpineImage, "ps", "auxw").AssertOutContains(strconv.Itoa(pid)) } func TestRunUtsHost(t *testing.T) { t.Parallel() base := testutil.NewBase(t) // Was thinking of os.ReadLink("/proc/1/ns/uts") // but you'd get EPERM for rootless. Just validate the // hostname is the same. hostName, err := os.Hostname() assert.NilError(base.T, err) base.Cmd("run", "--rm", "--uts=host", testutil.AlpineImage, "hostname").AssertOutContains(hostName) // Validate we can't provide a hostname with uts=host base.Cmd("run", "--rm", "--uts=host", "--hostname=foobar", testutil.AlpineImage, "hostname").AssertFail() // Validate we can't provide a domainname with uts=host base.Cmd("run", "--rm", "--uts=host", "--domainname=example.com", testutil.AlpineImage, "hostname").AssertFail() } func TestRunPidContainer(t *testing.T) { t.Parallel() base := testutil.NewBase(t) sharedContainerResult := base.Cmd("run", "-d", testutil.AlpineImage, "sleep", nerdtest.Infinity).Run() baseContainerID := strings.TrimSpace(sharedContainerResult.Stdout()) defer base.Cmd("rm", "-f", baseContainerID).Run() base.Cmd("run", "--rm", fmt.Sprintf("--pid=container:%s", baseContainerID), testutil.AlpineImage, "ps", "ax").AssertOutContains("sleep " + nerdtest.Infinity) } func TestRunIpcHost(t *testing.T) { t.Parallel() base := testutil.NewBase(t) testFilePath := filepath.Join("/dev/shm", fmt.Sprintf("%s-%d-%s", testutil.Identifier(t), os.Geteuid(), base.Target)) err := os.WriteFile(testFilePath, []byte(""), 0o644) assert.NilError(base.T, err) defer os.Remove(testFilePath) base.Cmd("run", "--rm", "--ipc=host", testutil.AlpineImage, "ls", testFilePath).AssertOK() } func TestRunAddHost(t *testing.T) { // Not parallelizable (https://github.com/containerd/nerdctl/issues/1127) base := testutil.NewBase(t) base.Cmd("run", "--rm", "--add-host", "testing.example.com:10.0.0.1", testutil.AlpineImage, "cat", "/etc/hosts").AssertOutWithFunc(func(stdout string) error { var found bool sc := bufio.NewScanner(bytes.NewBufferString(stdout)) for sc.Scan() { // removing spaces and tabs separating items line := strings.ReplaceAll(sc.Text(), " ", "") line = strings.ReplaceAll(line, "\t", "") if strings.Contains(line, "10.0.0.1testing.example.com") { found = true } } if !found { return errors.New("host was not added") } return nil }) base.Cmd("run", "--rm", "--add-host", "test:10.0.0.1", "--add-host", "test1:10.0.0.1", testutil.AlpineImage, "cat", "/etc/hosts").AssertOutWithFunc(func(stdout string) error { var found int sc := bufio.NewScanner(bytes.NewBufferString(stdout)) for sc.Scan() { // removing spaces and tabs separating items line := strings.ReplaceAll(sc.Text(), " ", "") line = strings.ReplaceAll(line, "\t", "") if strutil.InStringSlice([]string{"10.0.0.1test", "10.0.0.1test1"}, line) { found++ } } if found != 2 { return fmt.Errorf("host was not added, found %d", found) } return nil }) base.Cmd("run", "--rm", "--add-host", "10.0.0.1:testing.example.com", testutil.AlpineImage, "cat", "/etc/hosts").AssertFail() response := "This is the expected response for --add-host special IP test." http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, response) }) const hostPort = 8081 s := http.Server{Addr: fmt.Sprintf(":%d", hostPort), Handler: nil, ReadTimeout: 30 * time.Second} go s.ListenAndServe() defer s.Shutdown(context.Background()) base.Cmd("run", "--rm", "--add-host", "test:host-gateway", testutil.NginxAlpineImage, "curl", fmt.Sprintf("test:%d", hostPort)).AssertOutExactly(response) } func TestRunAddHostWithCustomHostGatewayIP(t *testing.T) { // Not parallelizable (https://github.com/containerd/nerdctl/issues/1127) base := testutil.NewBase(t) testutil.DockerIncompatible(t) base.Cmd("run", "--rm", "--host-gateway-ip", "192.168.5.2", "--add-host", "test:host-gateway", testutil.AlpineImage, "cat", "/etc/hosts").AssertOutWithFunc(func(stdout string) error { var found bool sc := bufio.NewScanner(bytes.NewBufferString(stdout)) for sc.Scan() { // removing spaces and tabs separating items line := strings.ReplaceAll(sc.Text(), " ", "") line = strings.ReplaceAll(line, "\t", "") if strings.Contains(line, "192.168.5.2test") { found = true } } if !found { return errors.New("host was not added") } return nil }) } func TestRunUlimit(t *testing.T) { t.Parallel() base := testutil.NewBase(t) ulimit := "nofile=622:622" ulimit2 := "nofile=622:722" base.Cmd("run", "--rm", "--ulimit", ulimit, testutil.AlpineImage, "sh", "-c", "ulimit -Sn").AssertOutExactly("622\n") base.Cmd("run", "--rm", "--ulimit", ulimit, testutil.AlpineImage, "sh", "-c", "ulimit -Hn").AssertOutExactly("622\n") base.Cmd("run", "--rm", "--ulimit", ulimit2, testutil.AlpineImage, "sh", "-c", "ulimit -Sn").AssertOutExactly("622\n") base.Cmd("run", "--rm", "--ulimit", ulimit2, testutil.AlpineImage, "sh", "-c", "ulimit -Hn").AssertOutExactly("722\n") } func TestRunWithInit(t *testing.T) { t.Parallel() testutil.DockerIncompatible(t) testutil.RequireExecutable(t, "tini-custom") base := testutil.NewBase(t) container := testutil.Identifier(t) base.Cmd("run", "-d", "--name", container, testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK() defer base.Cmd("rm", "-f", container).Run() base.Cmd("stop", "--time=3", container).AssertOK() // Unable to handle TERM signal, be killed when timeout assert.Equal(t, base.InspectContainer(container).State.ExitCode, 137) // Test with --init-path container1 := container + "-1" base.Cmd("run", "-d", "--name", container1, "--init-binary", "tini-custom", testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK() defer base.Cmd("rm", "-f", container1).Run() base.Cmd("stop", "--time=3", container1).AssertOK() assert.Equal(t, base.InspectContainer(container1).State.ExitCode, 143) // Test with --init container2 := container + "-2" base.Cmd("run", "-d", "--name", container2, "--init", testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK() defer base.Cmd("rm", "-f", container2).Run() base.Cmd("stop", "--time=3", container2).AssertOK() assert.Equal(t, base.InspectContainer(container2).State.ExitCode, 143) } func TestRunTTY(t *testing.T) { const sttyPartialOutput = "speed 38400 baud" testCase := nerdtest.Setup() testCase.SubTests = []*test.Case{ { Description: "stty with -it", Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("run", "-it", data.Identifier(), "stty") cmd.WithPseudoTTY() return cmd }, Expected: test.Expects(0, nil, expect.Contains(sttyPartialOutput)), }, { Description: "stty with -t", Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("run", "-t", data.Identifier(), "stty") cmd.WithPseudoTTY() return cmd }, Expected: test.Expects(0, nil, expect.Contains(sttyPartialOutput)), }, { Description: "stty with -i", Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("run", "-i", data.Identifier(), "stty") cmd.WithPseudoTTY() return cmd }, Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, { Description: "stty without params", Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("run", data.Identifier(), "stty") cmd.WithPseudoTTY() return cmd }, Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, { Description: "stty with -td", Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("run", "-td", data.Identifier(), "stty") cmd.WithPseudoTTY() return cmd }, Expected: test.Expects(0, nil, nil), }, } } func TestRunSigProxy(t *testing.T) { testCase := nerdtest.Setup() // FIXME: gomodjail signal handling is not working yet: https://github.com/AkihiroSuda/gomodjail/issues/51 testCase.Require = require.Not(nerdtest.Gomodjail) testCase.SubTests = []*test.Case{ { Description: "SigProxyDefault", Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { // FIXME: os.Interrupt will likely not work on Windows cmd := nerdtest.RunSigProxyContainer(os.Interrupt, true, nil, data, helpers) err := cmd.Signal(os.Interrupt) assert.NilError(helpers.T(), err) return cmd }, Expected: test.Expects(0, nil, expect.Contains(nerdtest.SignalCaught)), }, { Description: "SigProxyTrue", Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := nerdtest.RunSigProxyContainer(os.Interrupt, true, []string{"--sig-proxy=true"}, data, helpers) err := cmd.Signal(os.Interrupt) assert.NilError(helpers.T(), err) return cmd }, Expected: test.Expects(0, nil, expect.Contains(nerdtest.SignalCaught)), }, { Description: "SigProxyFalse", // Docker behavior changed sometimes with Docker 27 // See https://github.com/containerd/nerdctl/issues/4219 for details Require: require.Not(nerdtest.Docker), Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := nerdtest.RunSigProxyContainer(os.Interrupt, true, []string{"--sig-proxy=false"}, data, helpers) err := cmd.Signal(os.Interrupt) assert.NilError(helpers.T(), err) return cmd }, Expected: test.Expects(expect.ExitCodeSignaled, nil, expect.DoesNotContain(nerdtest.SignalCaught)), }, } testCase.Run(t) } func TestRunWithFluentdLogDriver(t *testing.T) { base := testutil.NewBase(t) tempDirectory := t.TempDir() err := os.Chmod(tempDirectory, 0o777) assert.NilError(t, err) containerName := testutil.Identifier(t) base.Cmd("run", "-d", "--name", containerName, "-p", "24224:24224", "-v", fmt.Sprintf("%s:/fluentd/log", tempDirectory), testutil.FluentdImage).AssertOK() defer base.Cmd("rm", "-f", containerName).AssertOK() time.Sleep(3 * time.Second) testContainerName := containerName + "test" base.Cmd("run", "-d", "--log-driver", "fluentd", "--name", testContainerName, testutil.CommonImage, "sh", "-c", "echo test").AssertOK() defer base.Cmd("rm", "-f", testContainerName).AssertOK() inspectedContainer := base.InspectContainer(testContainerName) matches, err := filepath.Glob(tempDirectory + "/" + "data.*.log") assert.NilError(t, err) assert.Equal(t, 1, len(matches)) data, err := os.ReadFile(matches[0]) assert.NilError(t, err) logData := string(data) assert.Equal(t, true, strings.Contains(logData, "test")) assert.Equal(t, true, strings.Contains(logData, inspectedContainer.ID)) } func TestRunWithFluentdLogDriverWithLogOpt(t *testing.T) { base := testutil.NewBase(t) tempDirectory := t.TempDir() err := os.Chmod(tempDirectory, 0o777) assert.NilError(t, err) containerName := testutil.Identifier(t) base.Cmd("run", "-d", "--name", containerName, "-p", "24225:24224", "-v", fmt.Sprintf("%s:/fluentd/log", tempDirectory), testutil.FluentdImage).AssertOK() defer base.Cmd("rm", "-f", containerName).AssertOK() time.Sleep(3 * time.Second) testContainerName := containerName + "test" base.Cmd("run", "-d", "--log-driver", "fluentd", "--log-opt", "fluentd-address=127.0.0.1:24225", "--name", testContainerName, testutil.CommonImage, "sh", "-c", "echo test2").AssertOK() defer base.Cmd("rm", "-f", testContainerName).AssertOK() inspectedContainer := base.InspectContainer(testContainerName) matches, err := filepath.Glob(tempDirectory + "/" + "data.*.log") assert.NilError(t, err) assert.Equal(t, 1, len(matches)) data, err := os.ReadFile(matches[0]) assert.NilError(t, err) logData := string(data) assert.Equal(t, true, strings.Contains(logData, "test2")) assert.Equal(t, true, strings.Contains(logData, inspectedContainer.ID)) } func TestRunWithOOMScoreAdj(t *testing.T) { if rootlessutil.IsRootless() { t.Skip("test skipped for rootless containers.") } t.Parallel() base := testutil.NewBase(t) score := "-42" base.Cmd("run", "--rm", "--oom-score-adj", score, testutil.AlpineImage, "cat", "/proc/self/oom_score_adj").AssertOutContains(score) } func TestRunWithDetachKeys(t *testing.T) { testCase := nerdtest.Setup() testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { // Run interactively and detach cmd := helpers.Command("run", "-it", "--detach-keys=ctrl-a,ctrl-b", "--name", data.Identifier(), testutil.CommonImage) cmd.WithPseudoTTY() cmd.Feed(strings.NewReader("echo mark${NON}mark\n")) cmd.WithFeeder(func() io.Reader { // Because of the way we proxy stdin, we have to wait here, otherwise we detach before // the rest of the input ever reaches the container // Note that this only concerns nerdctl, as docker seems to behave ok LOCALLY. // But then, it fails for docker as well ON THE CI. It is unclear why at this point. // Arbitrary time pauses would not work: what matters is that the container has started. // if !nerdtest.IsDocker() { nerdtest.EnsureContainerStarted(helpers, data.Identifier()) // } // ctrl+a and ctrl+b (see https://en.wikipedia.org/wiki/C0_and_C1_control_codes) return bytes.NewReader([]byte{1, 2}) }) return cmd } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Errors: []error{errors.New("detach keys")}, Output: expect.All( expect.Contains("markmark"), func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true")) }, ), } } testCase.Run(t) } func TestRunWithTtyAndDetached(t *testing.T) { base := testutil.NewBase(t) imageName := testutil.CommonImage withoutTtyContainerName := "without-terminal-" + testutil.Identifier(t) withTtyContainerName := "with-terminal-" + testutil.Identifier(t) // without -t, fail base.Cmd("run", "-d", "--name", withoutTtyContainerName, imageName, "stty").AssertOK() defer base.Cmd("container", "rm", "-f", withoutTtyContainerName).AssertOK() base.Cmd("logs", withoutTtyContainerName).AssertCombinedOutContains("stty: standard input: Not a tty") withoutTtyContainer := base.InspectContainer(withoutTtyContainerName) assert.Equal(base.T, 1, withoutTtyContainer.State.ExitCode) // with -t, success base.Cmd("run", "-d", "-t", "--name", withTtyContainerName, imageName, "stty").AssertOK() defer base.Cmd("container", "rm", "-f", withTtyContainerName).AssertOK() base.Cmd("logs", withTtyContainerName).AssertCombinedOutContains("speed 38400 baud; line = 0;") withTtyContainer := base.InspectContainer(withTtyContainerName) assert.Equal(base.T, 0, withTtyContainer.State.ExitCode) } // TestIssue3568 tests https://github.com/containerd/nerdctl/issues/3568 func TestIssue3568(t *testing.T) { testCase := nerdtest.Setup() testCase.Description = "Issue #3568 - Detaching from a container started by using --rm option causes the container to be deleted." testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { // Run interactively and detach cmd := helpers.Command("run", "--rm", "-it", "--detach-keys=ctrl-a,ctrl-b", "--name", data.Identifier(), testutil.CommonImage) cmd.WithPseudoTTY() cmd.Feed(strings.NewReader("echo mark${NON}mark\n")) cmd.WithFeeder(func() io.Reader { // Because of the way we proxy stdin, we have to wait here, otherwise we detach before // the rest of the input ever reaches the container // Note that this only concerns nerdctl, as docker seems to behave ok LOCALLY. // But then, it fails for docker as well ON THE CI. It is unclear why at this point. // Arbitrary time pauses would not work: what matters is that the container has started. // if !nerdtest.IsDocker() { nerdtest.EnsureContainerStarted(helpers, data.Identifier()) // } // ctrl+a and ctrl+b (see https://en.wikipedia.org/wiki/C0_and_C1_control_codes) return bytes.NewReader([]byte{1, 2}) }) return cmd } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Errors: []error{errors.New("detach keys")}, Output: expect.All( expect.Contains("markmark"), func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true")) }, ), } } testCase.Run(t) } // TestPortBindingWithCustomHost tests https://github.com/containerd/nerdctl/issues/3539 func TestPortBindingWithCustomHost(t *testing.T) { testCase := nerdtest.Setup() const ( host = "127.0.0.2" hostPort = 8080 ) address := fmt.Sprintf("%s:%d", host, hostPort) testCase.SubTests = []*test.Case{ { Description: "Issue #3539 - Access to a container running when 127.0.0.2 is specified in -p in rootless mode.", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "-p", fmt.Sprintf("%s:80", address), testutil.NginxAlpineImage) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Errors: []error{}, Output: expect.All( func(stdout string, t tig.T) { resp, err := nettestutil.HTTPGet(address, 5, false) assert.NilError(t, err) respBody, err := io.ReadAll(resp.Body) assert.NilError(t, err) assert.Assert(t, strings.Contains(string(respBody), testutil.NginxAlpineIndexHTMLSnippet)) }, ), } }, }, } testCase.Run(t) } func TestRunDeviceCDI(t *testing.T) { t.Parallel() // Although CDI injection is supported by Docker, specifying the --cdi-spec-dirs on the command line is not. testutil.DockerIncompatible(t) cdiSpecDir := filepath.Join(t.TempDir(), "cdi") const testCDIVendor1 = ` cdiVersion: "0.3.0" kind: "vendor1.com/device" devices: - name: foo containerEdits: env: - FOO=injected ` writeTestCDISpec(t, testCDIVendor1, "vendor1.yaml", cdiSpecDir) base := testutil.NewBase(t) base.Cmd("--cdi-spec-dirs", cdiSpecDir, "run", "--rm", "--device", "vendor1.com/device=foo", testutil.AlpineImage, "env", ).AssertOutContains("FOO=injected") } func TestRunDeviceCDIWithNerdctlConfig(t *testing.T) { t.Parallel() // Although CDI injection is supported by Docker, specifying the --cdi-spec-dirs on the command line is not. testutil.DockerIncompatible(t) cdiSpecDir := filepath.Join(t.TempDir(), "cdi") const testCDIVendor1 = ` cdiVersion: "0.3.0" kind: "vendor1.com/device" devices: - name: foo containerEdits: env: - FOO=injected ` writeTestCDISpec(t, testCDIVendor1, "vendor1.yaml", cdiSpecDir) tomlPath := filepath.Join(t.TempDir(), "nerdctl.toml") err := os.WriteFile(tomlPath, []byte(fmt.Sprintf(` cdi_spec_dirs = ["%s"] `, cdiSpecDir)), 0o400) assert.NilError(t, err) base := testutil.NewBase(t) base.Env = append(base.Env, "NERDCTL_TOML="+tomlPath) base.Cmd("run", "--rm", "--device", "vendor1.com/device=foo", testutil.AlpineImage, "env", ).AssertOutContains("FOO=injected") } // TestRunGPU tests GPU injection using the --gpus flag. func TestRunGPU(t *testing.T) { t.Parallel() // Although CDI injection is supported by Docker, specifying the --cdi-spec-dirs on the command line is not. testutil.DockerIncompatible(t) const nvidiaSpec = ` cdiVersion: "0.5.0" kind: "nvidia.com/gpu" devices: - name: "0" containerEdits: env: - NVIDIA_GPU_0=injected - name: "1" containerEdits: env: - NVIDIA_GPU_1=injected ` const amdSpec = ` cdiVersion: "0.5.0" kind: "amd.com/gpu" devices: - name: "0" containerEdits: env: - AMD_GPU_0=injected - name: "1" containerEdits: env: - AMD_GPU_1=injected ` const unknownSpec = ` cdiVersion: "0.5.0" kind: "unknown.com/gpu" devices: - name: "0" containerEdits: env: - UNKNOWN_GPU_0=injected ` testCases := []struct { name string specs map[string]string gpuFlags []string expectedEnvs []string expectFail bool }{ { name: "nvidia device injection", specs: map[string]string{"nvidia.yaml": nvidiaSpec}, gpuFlags: []string{"--gpus", "2"}, expectedEnvs: []string{"NVIDIA_GPU_0=injected", "NVIDIA_GPU_1=injected"}, }, { name: "amd device injection", specs: map[string]string{"amd.yaml": amdSpec}, gpuFlags: []string{"--gpus", "2"}, expectedEnvs: []string{"AMD_GPU_0=injected", "AMD_GPU_1=injected"}, }, { name: "multiple vendors", specs: map[string]string{"nvidia.yaml": nvidiaSpec, "amd.yaml": amdSpec}, gpuFlags: []string{"--gpus", "1"}, expectedEnvs: []string{"NVIDIA_GPU_0=injected"}, }, { name: "unknown vendor fails", specs: map[string]string{"unknown.yaml": unknownSpec}, gpuFlags: []string{"--gpus", "1"}, expectFail: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { t.Parallel() tmpDir := t.TempDir() for fileName, spec := range tc.specs { writeTestCDISpec(t, spec, fileName, tmpDir) } base := testutil.NewBase(t) args := []string{"--cdi-spec-dirs", tmpDir, "run", "--rm"} args = append(args, tc.gpuFlags...) args = append(args, testutil.AlpineImage, "env") if tc.expectFail { base.Cmd(args...).AssertFail() } else { base.Cmd(args...).AssertOutWithFunc(func(stdout string) error { for _, expectedEnv := range tc.expectedEnvs { if !strings.Contains(stdout, expectedEnv) { return fmt.Errorf("%s not found", expectedEnv) } } return nil }) } }) } } // TestRunGPUWithOtherCDIDevices tests GPU CDI injection along with other CDI devices. func TestRunGPUWithOtherCDIDevices(t *testing.T) { t.Parallel() // Although CDI injection is supported by Docker, specifying the --cdi-spec-dirs on the command line is not. testutil.DockerIncompatible(t) const amdSpec = ` cdiVersion: "0.5.0" kind: "amd.com/gpu" devices: - name: "0" containerEdits: env: - AMD_GPU_0=injected - name: "1" containerEdits: env: - AMD_GPU_1=injected ` const vendor1Spec = ` cdiVersion: "0.3.0" kind: "vendor1.com/device" devices: - name: foo containerEdits: env: - FOO=injected ` tmpDir := t.TempDir() writeTestCDISpec(t, amdSpec, "amd.yaml", tmpDir) writeTestCDISpec(t, vendor1Spec, "vendor1.yaml", tmpDir) base := testutil.NewBase(t) base.Cmd("--cdi-spec-dirs", tmpDir, "run", "--rm", "--gpus", "2", "--device", "vendor1.com/device=foo", testutil.AlpineImage, "env", ).AssertOutWithFunc(func(stdout string) error { if !strings.Contains(stdout, "AMD_GPU_0=injected") { return errors.New("AMD_GPU_0=injected not found") } if !strings.Contains(stdout, "AMD_GPU_1=injected") { return errors.New("AMD_GPU_1=injected not found") } if !strings.Contains(stdout, "FOO=injected") { return errors.New("FOO=injected not found") } return nil }) } func writeTestCDISpec(t *testing.T, spec string, fileName string, cdiSpecDir string) { err := os.MkdirAll(cdiSpecDir, 0o700) assert.NilError(t, err) cdiSpecPath := filepath.Join(cdiSpecDir, fileName) err = os.WriteFile(cdiSpecPath, []byte(spec), 0o400) assert.NilError(t, err) } func TestSharedIpcSetup(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.Not(require.Windows), Setup: func(data test.Data, helpers test.Helpers) { data.Labels().Set("container1", data.Identifier("container1")) helpers.Ensure("run", "-d", "--name", data.Identifier("container1"), "--ipc=shareable", testutil.CommonImage, "sleep", "inf") nerdtest.EnsureContainerStarted(helpers, data.Identifier("container1")) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("container1")) }, SubTests: []*test.Case{ { Description: "Test ipc is shared", NoParallel: true, // The validation involves starting of the main container: container1 Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("container2")) }, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure( "run", "-d", "--name", data.Identifier("container2"), "--ipc=container:"+data.Labels().Get("container1"), testutil.NginxAlpineImage) data.Labels().Set("container2", data.Identifier("container2")) nerdtest.EnsureContainerStarted(helpers, data.Identifier("container2")) }, SubTests: []*test.Case{ { NoParallel: true, Description: "Test ipc is shared", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Labels().Get("container2"), "readlink", "/proc/1/ns/ipc") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All( func(stdout string, t tig.T) { container1IPC := strings.TrimSpace(helpers.Capture("exec", data.Labels().Get("container1"), "readlink", "/proc/1/ns/ipc")) container2IPC := strings.TrimSpace(stdout) assert.Equal(t, container1IPC, container2IPC) }, ), } }, }, { NoParallel: true, Description: "Test ipc is shared after restart", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("restart", data.Labels().Get("container1")) helpers.Ensure("stop", "--time=1", data.Labels().Get("container2")) helpers.Ensure("start", data.Labels().Get("container2")) nerdtest.EnsureContainerStarted(helpers, data.Labels().Get("container2")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Labels().Get("container2"), "readlink", "/proc/1/ns/ipc") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All( func(stdout string, t tig.T) { container1IPC := strings.TrimSpace(helpers.Capture("exec", data.Labels().Get("container1"), "readlink", "/proc/1/ns/ipc")) container2IPC := strings.TrimSpace(stdout) assert.Equal(t, container1IPC, container2IPC) }, ), } }, }, }, }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_run_log_driver_syslog_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "fmt" "os" "runtime" "strconv" "strings" "testing" "time" syslog "github.com/yuchanns/srslog" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/testca" "github.com/containerd/nerdctl/v2/pkg/testutil/testsyslog" ) func runSyslogTest(t *testing.T, networks []string, syslogFacilities map[string]syslog.Priority, fmtValidFuncs map[string]func(string, string, string, string, syslog.Priority, bool) error) { if runtime.GOOS == "windows" { t.Skip("syslog container logging is not officially supported on Windows") } base := testutil.NewBase(t) base.Cmd("pull", "--quiet", testutil.CommonImage).AssertOK() hostname, err := os.Hostname() if err != nil { t.Fatalf("Error retrieving hostname") } ca := testca.New(base.T) cert := ca.NewCert("127.0.0.1") t.Cleanup(func() { cert.Close() ca.Close() }) rI := 0 for _, network := range networks { for rFK, rFV := range syslogFacilities { fPriV := rFV // test both string and number facility for _, fPriK := range []string{rFK, strconv.Itoa(int(fPriV) >> 3)} { for fmtK, fmtValidFunc := range fmtValidFuncs { fmtKT := "empty" if fmtK != "" { fmtKT = fmtK } subTestName := fmt.Sprintf("%s_%s_%s", strings.ReplaceAll(network, "+", "_"), fPriK, fmtKT) i := rI rI++ t.Run(subTestName, func(t *testing.T) { tID := testutil.Identifier(t) tag := tID + "_syslog_driver" msg := "hello, " + tID + "_syslog_driver" if !testsyslog.TestableNetwork(network) { if rootlessutil.IsRootless() { t.Skipf("skipping on %s/%s; '%s' for rootless containers are not supported", runtime.GOOS, runtime.GOARCH, network) } t.Skipf("skipping on %s/%s; '%s' is not supported", runtime.GOOS, runtime.GOARCH, network) } testContainerName := fmt.Sprintf("%s-%d-%s", tID, i, fPriK) done := make(chan string) addr, closer := testsyslog.StartServer(network, "", done, cert) args := []string{ "run", "-d", "--name", testContainerName, "--restart=no", "--log-driver=syslog", "--log-opt=syslog-facility=" + fPriK, "--log-opt=tag=" + tag, "--log-opt=syslog-format=" + fmtK, "--log-opt=syslog-address=" + fmt.Sprintf("%s://%s", network, addr), } if network == "tcp+tls" { args = append(args, "--log-opt=syslog-tls-cert="+cert.CertPath, "--log-opt=syslog-tls-key="+cert.KeyPath, "--log-opt=syslog-tls-ca-cert="+ca.CertPath, ) } args = append(args, testutil.CommonImage, "echo", msg) base.Cmd(args...).AssertOK() t.Cleanup(func() { base.Cmd("rm", "-f", testContainerName).AssertOK() }) defer closer.Close() defer close(done) select { case rcvd := <-done: if err := fmtValidFunc(rcvd, msg, tag, hostname, fPriV, network == "tcp+tls"); err != nil { t.Error(err) } case <-time.Tick(time.Second * 3): t.Errorf("timeout with %s", subTestName) } }) } } } } } func TestSyslogNetwork(t *testing.T) { var syslogFacilities = map[string]syslog.Priority{ "user": syslog.LOG_USER, } networks := []string{ "udp", "tcp", "tcp+tls", "unix", "unixgram", } fmtValidFuncs := map[string]func(string, string, string, string, syslog.Priority, bool) error{ "rfc5424": func(rcvd, msg, tag, hostname string, pri syslog.Priority, isTLS bool) error { var parsedHostname, timestamp string var length, version, pid int if !isTLS { exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" if n, err := fmt.Sscanf(rcvd, exp, &version, ×tamp, &parsedHostname, &pid); n != 4 || err != nil || hostname != parsedHostname { return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) } } else { exp := "%d " + fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" if n, err := fmt.Sscanf(rcvd, exp, &length, &version, ×tamp, &parsedHostname, &pid); n != 5 || err != nil || hostname != parsedHostname { return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) } } return nil }, } runSyslogTest(t, networks, syslogFacilities, fmtValidFuncs) } func TestSyslogFacilities(t *testing.T) { var syslogFacilities = map[string]syslog.Priority{ "kern": syslog.LOG_KERN, "user": syslog.LOG_USER, "mail": syslog.LOG_MAIL, "daemon": syslog.LOG_DAEMON, "auth": syslog.LOG_AUTH, "syslog": syslog.LOG_SYSLOG, "lpr": syslog.LOG_LPR, "news": syslog.LOG_NEWS, "uucp": syslog.LOG_UUCP, "cron": syslog.LOG_CRON, "authpriv": syslog.LOG_AUTHPRIV, "ftp": syslog.LOG_FTP, "local0": syslog.LOG_LOCAL0, "local1": syslog.LOG_LOCAL1, "local2": syslog.LOG_LOCAL2, "local3": syslog.LOG_LOCAL3, "local4": syslog.LOG_LOCAL4, "local5": syslog.LOG_LOCAL5, "local6": syslog.LOG_LOCAL6, "local7": syslog.LOG_LOCAL7, } networks := []string{"unix"} fmtValidFuncs := map[string]func(string, string, string, string, syslog.Priority, bool) error{ "rfc5424": func(rcvd, msg, tag, hostname string, pri syslog.Priority, isTLS bool) error { var parsedHostname, timestamp string var length, version, pid int if !isTLS { exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" if n, err := fmt.Sscanf(rcvd, exp, &version, ×tamp, &parsedHostname, &pid); n != 4 || err != nil || hostname != parsedHostname { return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) } } else { exp := "%d " + fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" if n, err := fmt.Sscanf(rcvd, exp, &length, &version, ×tamp, &parsedHostname, &pid); n != 5 || err != nil || hostname != parsedHostname { return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) } } return nil }, } runSyslogTest(t, networks, syslogFacilities, fmtValidFuncs) } func TestSyslogFormat(t *testing.T) { var syslogFacilities = map[string]syslog.Priority{ "user": syslog.LOG_USER, } networks := []string{"unix"} fmtValidFuncs := map[string]func(string, string, string, string, syslog.Priority, bool) error{ "": func(rcvd, msg, tag, hostname string, pri syslog.Priority, isSTLS bool) error { var mon, day, hrs string var pid int exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%s %s %s " + tag + "[%d]: " + msg + "\n" if n, err := fmt.Sscanf(rcvd, exp, &mon, &day, &hrs, &pid); n != 4 || err != nil { return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) } return nil }, "rfc3164": func(rcvd, msg, tag, hostname string, pri syslog.Priority, isTLS bool) error { var parsedHostname, mon, day, hrs string var pid int exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%s %s %s %s " + tag + "[%d]: " + msg + "\n" if n, err := fmt.Sscanf(rcvd, exp, &mon, &day, &hrs, &parsedHostname, &pid); n != 5 || err != nil || hostname != parsedHostname { return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) } return nil }, "rfc5424": func(rcvd, msg, tag, hostname string, pri syslog.Priority, isTLS bool) error { var parsedHostname, timestamp string var length, version, pid int if !isTLS { exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" if n, err := fmt.Sscanf(rcvd, exp, &version, ×tamp, &parsedHostname, &pid); n != 4 || err != nil || hostname != parsedHostname { return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) } } else { exp := "%d " + fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" if n, err := fmt.Sscanf(rcvd, exp, &length, &version, ×tamp, &parsedHostname, &pid); n != 5 || err != nil || hostname != parsedHostname { return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) } } return nil }, "rfc5424micro": func(rcvd, msg, tag, hostname string, pri syslog.Priority, isTLS bool) error { var parsedHostname, timestamp string var length, version, pid int if !isTLS { exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" if n, err := fmt.Sscanf(rcvd, exp, &version, ×tamp, &parsedHostname, &pid); n != 4 || err != nil || hostname != parsedHostname { return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) } } else { exp := "%d " + fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" if n, err := fmt.Sscanf(rcvd, exp, &length, &version, ×tamp, &parsedHostname, &pid); n != 5 || err != nil || hostname != parsedHostname { return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) } } return nil }, } runSyslogTest(t, networks, syslogFacilities, fmtValidFuncs) } ================================================ FILE: cmd/nerdctl/container/container_run_mount_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "fmt" "os" "path/filepath" "strings" "testing" mobymount "github.com/moby/sys/mount" "gotest.tools/v3/assert" "github.com/containerd/containerd/v2/core/mount" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRunVolume(t *testing.T) { t.Parallel() base := testutil.NewBase(t) tID := testutil.Identifier(t) rwDir, err := os.MkdirTemp(t.TempDir(), "rw") if err != nil { t.Fatal(err) } roDir, err := os.MkdirTemp(t.TempDir(), "ro") if err != nil { t.Fatal(err) } rwVolName := tID + "-rw" roVolName := tID + "-ro" for _, v := range []string{rwVolName, roVolName} { defer base.Cmd("volume", "rm", "-f", v).Run() base.Cmd("volume", "create", v).AssertOK() } containerName := tID defer base.Cmd("rm", "-f", containerName).AssertOK() base.Cmd("run", "-d", "--name", containerName, "-v", fmt.Sprintf("%s:/mnt1", rwDir), "-v", fmt.Sprintf("%s:/mnt2:ro", roDir), "-v", fmt.Sprintf("%s:/mnt3", rwVolName), "-v", fmt.Sprintf("%s:/mnt4:ro", roVolName), testutil.AlpineImage, "top", ).AssertOK() base.Cmd("exec", containerName, "sh", "-exc", "echo -n str1 > /mnt1/file1").AssertOK() base.Cmd("exec", containerName, "sh", "-exc", "echo -n str2 > /mnt2/file2").AssertFail() base.Cmd("exec", containerName, "sh", "-exc", "echo -n str3 > /mnt3/file3").AssertOK() base.Cmd("exec", containerName, "sh", "-exc", "echo -n str4 > /mnt4/file4").AssertFail() base.Cmd("rm", "-f", containerName).AssertOK() base.Cmd("run", "--rm", "-v", fmt.Sprintf("%s:/mnt1", rwDir), "-v", fmt.Sprintf("%s:/mnt3", rwVolName), testutil.AlpineImage, "cat", "/mnt1/file1", "/mnt3/file3", ).AssertOutExactly("str1str3") base.Cmd("run", "--rm", "-v", fmt.Sprintf("%s:/mnt3/mnt1", rwDir), "-v", fmt.Sprintf("%s:/mnt3", rwVolName), testutil.AlpineImage, "cat", "/mnt3/mnt1/file1", "/mnt3/file3", ).AssertOutExactly("str1str3") } func TestRunAnonymousVolume(t *testing.T) { t.Parallel() base := testutil.NewBase(t) base.Cmd("run", "--rm", "-v", "/foo", testutil.AlpineImage).AssertOK() base.Cmd("run", "--rm", "-v", "TestVolume2:/foo", testutil.AlpineImage).AssertOK() base.Cmd("run", "--rm", "-v", "TestVolume", testutil.AlpineImage).AssertOK() // Destination must be an absolute path not named volume base.Cmd("run", "--rm", "-v", "TestVolume2:TestVolumes", testutil.AlpineImage).AssertFail() } func TestRunVolumeRelativePath(t *testing.T) { t.Parallel() base := testutil.NewBase(t) base.Dir = t.TempDir() base.Cmd("run", "--rm", "-v", "./foo:/mnt/foo", testutil.AlpineImage).AssertOK() base.Cmd("run", "--rm", "-v", "./foo", testutil.AlpineImage).AssertOK() // Destination must be an absolute path not a relative path base.Cmd("run", "--rm", "-v", "./foo:./foo", testutil.AlpineImage).AssertFail() } func TestRunAnonymousVolumeWithTypeMountFlag(t *testing.T) { t.Parallel() base := testutil.NewBase(t) base.Cmd("run", "--rm", "--mount", "type=volume,dst=/foo", testutil.AlpineImage, "mountpoint", "-q", "/foo").AssertOK() } func TestRunAnonymousVolumeWithBuild(t *testing.T) { t.Parallel() testutil.RequiresBuild(t) testutil.RegisterBuildCacheCleanup(t) base := testutil.NewBase(t) imageName := testutil.Identifier(t) defer base.Cmd("rmi", imageName).Run() dockerfile := fmt.Sprintf(`FROM %s VOLUME /foo `, testutil.AlpineImage) buildCtx := helpers.CreateBuildContext(t, dockerfile) base.Cmd("build", "-t", imageName, buildCtx).AssertOK() base.Cmd("run", "--rm", "-v", "/foo", testutil.AlpineImage, "mountpoint", "-q", "/foo").AssertOK() } func TestRunCopyingUpInitialContentsOnVolume(t *testing.T) { t.Parallel() testutil.RequiresBuild(t) testutil.RegisterBuildCacheCleanup(t) base := testutil.NewBase(t) imageName := testutil.Identifier(t) defer base.Cmd("rmi", imageName).Run() volName := testutil.Identifier(t) + "-vol" defer base.Cmd("volume", "rm", volName).Run() dockerfile := fmt.Sprintf(`FROM %s RUN mkdir -p /mnt && echo hi > /mnt/initial_file CMD ["cat", "/mnt/initial_file"] `, testutil.AlpineImage) buildCtx := helpers.CreateBuildContext(t, dockerfile) base.Cmd("build", "-t", imageName, buildCtx).AssertOK() //AnonymousVolume base.Cmd("run", "--rm", imageName).AssertOutExactly("hi\n") base.Cmd("run", "-v", "/mnt", "--rm", imageName).AssertOutExactly("hi\n") //NamedVolume should be automatically created base.Cmd("run", "-v", volName+":/mnt", "--rm", imageName).AssertOutExactly("hi\n") } func TestRunCopyingUpInitialContentsOnDockerfileVolume(t *testing.T) { t.Parallel() testutil.RequiresBuild(t) testutil.RegisterBuildCacheCleanup(t) base := testutil.NewBase(t) imageName := testutil.Identifier(t) defer base.Cmd("rmi", imageName).Run() volName := testutil.Identifier(t) + "-vol" defer base.Cmd("volume", "rm", volName).Run() dockerfile := fmt.Sprintf(`FROM %s RUN mkdir -p /mnt && echo hi > /mnt/initial_file VOLUME /mnt CMD ["cat", "/mnt/initial_file"] `, testutil.AlpineImage) buildCtx := helpers.CreateBuildContext(t, dockerfile) base.Cmd("build", "-t", imageName, buildCtx).AssertOK() //AnonymousVolume base.Cmd("run", "--rm", imageName).AssertOutExactly("hi\n") base.Cmd("run", "-v", "/mnt", "--rm", imageName).AssertOutExactly("hi\n") //NamedVolume base.Cmd("volume", "create", volName).AssertOK() base.Cmd("run", "-v", volName+":/mnt", "--rm", imageName).AssertOutExactly("hi\n") //mount bind tmpDir, err := os.MkdirTemp(t.TempDir(), "hostDir") assert.NilError(t, err) base.Cmd("run", "-v", fmt.Sprintf("%s:/mnt", tmpDir), "--rm", imageName).AssertFail() } func TestRunCopyingUpInitialContentsOnVolumeShouldRetainSymlink(t *testing.T) { t.Parallel() testutil.RequiresBuild(t) testutil.RegisterBuildCacheCleanup(t) base := testutil.NewBase(t) imageName := testutil.Identifier(t) defer base.Cmd("rmi", imageName).Run() dockerfile := fmt.Sprintf(`FROM %s RUN ln -s ../../../../../../../../../../../../../../../../../../etc/passwd /mnt/passwd VOLUME /mnt CMD ["readlink", "/mnt/passwd"] `, testutil.AlpineImage) const expected = "../../../../../../../../../../../../../../../../../../etc/passwd\n" buildCtx := helpers.CreateBuildContext(t, dockerfile) base.Cmd("build", "-t", imageName, buildCtx).AssertOK() base.Cmd("run", "--rm", imageName).AssertOutExactly(expected) base.Cmd("run", "-v", "/mnt", "--rm", imageName).AssertOutExactly(expected) } func TestRunCopyingUpInitialContentsShouldNotResetTheCopiedContents(t *testing.T) { t.Parallel() testutil.RequiresBuild(t) testutil.RegisterBuildCacheCleanup(t) base := testutil.NewBase(t) tID := testutil.Identifier(t) imageName := tID + "-img" volumeName := tID + "-vol" containerName := tID defer func() { base.Cmd("rm", "-f", containerName).Run() base.Cmd("volume", "rm", volumeName).Run() base.Cmd("rmi", imageName).Run() }() dockerfile := fmt.Sprintf(`FROM %s RUN echo -n "rev0" > /mnt/file `, testutil.AlpineImage) buildCtx := helpers.CreateBuildContext(t, dockerfile) base.Cmd("build", "-t", imageName, buildCtx).AssertOK() base.Cmd("volume", "create", volumeName) runContainer := func() { base.Cmd("run", "-d", "--name", containerName, "-v", volumeName+":/mnt", imageName, "sleep", nerdtest.Infinity).AssertOK() } runContainer() base.EnsureContainerStarted(containerName) base.Cmd("exec", containerName, "cat", "/mnt/file").AssertOutExactly("rev0") base.Cmd("exec", containerName, "sh", "-euc", "echo -n \"rev1\" >/mnt/file").AssertOK() base.Cmd("rm", "-f", containerName).AssertOK() runContainer() base.EnsureContainerStarted(containerName) base.Cmd("exec", containerName, "cat", "/mnt/file").AssertOutExactly("rev1") } func TestRunTmpfs(t *testing.T) { t.Parallel() base := testutil.NewBase(t) f := func(allow, deny []string) func(stdout string) error { return func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) != 1 { return fmt.Errorf("expected 1 lines, got %q", stdout) } for _, s := range allow { if !strings.Contains(stdout, s) { return fmt.Errorf("expected stdout to contain %q, got %q", s, stdout) } } for _, s := range deny { if strings.Contains(stdout, s) { return fmt.Errorf("expected stdout not to contain %q, got %q", s, stdout) } } return nil } } base.Cmd("run", "--rm", "--tmpfs", "/tmp", testutil.AlpineImage, "grep", "/tmp", "/proc/mounts").AssertOutWithFunc(f([]string{"rw", "nosuid", "nodev", "noexec"}, nil)) base.Cmd("run", "--rm", "--tmpfs", "/tmp:size=64m,exec", testutil.AlpineImage, "grep", "/tmp", "/proc/mounts").AssertOutWithFunc(f([]string{"rw", "nosuid", "nodev", "size=65536k"}, []string{"noexec"})) // for https://github.com/containerd/nerdctl/issues/594 base.Cmd("run", "--rm", "--tmpfs", "/dev/shm:rw,exec,size=1g", testutil.AlpineImage, "grep", "/dev/shm", "/proc/mounts").AssertOutWithFunc(f([]string{"rw", "nosuid", "nodev", "size=1048576k"}, []string{"noexec"})) } func TestRunBindMountTmpfs(t *testing.T) { t.Parallel() base := testutil.NewBase(t) f := func(allow []string) func(stdout string) error { return func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) != 1 { return fmt.Errorf("expected 1 lines, got %q", stdout) } for _, s := range allow { if !strings.Contains(stdout, s) { return fmt.Errorf("expected stdout to contain %q, got %q", s, stdout) } } return nil } } base.Cmd("run", "--rm", "--mount", "type=tmpfs,target=/tmp", testutil.AlpineImage, "grep", "/tmp", "/proc/mounts").AssertOutWithFunc(f([]string{"rw", "nosuid", "nodev", "noexec"})) base.Cmd("run", "--rm", "--mount", "type=tmpfs,target=/tmp,tmpfs-size=64m", testutil.AlpineImage, "grep", "/tmp", "/proc/mounts").AssertOutWithFunc(f([]string{"rw", "nosuid", "nodev", "size=65536k"})) } func mountExistsWithOpt(mountPoint, mountOpt string) test.Comparator { return func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") mountOutput := []string{} for _, line := range lines { if strings.Contains(line, mountPoint) { mountOutput = strings.Split(line, " ") break } } assert.Assert(t, len(mountOutput) > 0, "we should have found the mount point in /proc/mounts") assert.Assert(t, len(mountOutput) >= 4, "invalid format for mount line") options := strings.Split(mountOutput[3], ",") found := false for _, opt := range options { if mountOpt == opt { found = true break } } assert.Assert(t, found, "mount option %s not found", mountOpt) } } func TestRunBindMountBind(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { // Run a container with bind mount directories, one rw, the other ro rwDir := data.Temp().Dir("rw") roDir := data.Temp().Dir("ro") helpers.Ensure( "run", "-d", "--name", data.Identifier("container"), "--mount", fmt.Sprintf("type=bind,src=%s,target=/mntrw", rwDir), "--mount", fmt.Sprintf("type=bind,src=%s,target=/mntro,ro", roDir), testutil.AlpineImage, "top", ) nerdtest.EnsureContainerStarted(helpers, data.Identifier("container")) // Save host rwDir location and container id for subtests data.Labels().Set("container", data.Identifier("container")) data.Labels().Set("rwDir", rwDir) } testCase.SubTests = []*test.Case{ { Description: "ensure we cannot write to ro mount", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Labels().Get("container"), "sh", "-exc", "echo -n failure > /mntro/file") }, Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, { Description: "ensure we can write to rw, and read it back from another container mounting the same target", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("exec", data.Labels().Get("container"), "sh", "-exc", "echo -n success > /mntrw/file") }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command( "run", "--rm", "--mount", fmt.Sprintf("type=bind,src=%s,target=/mntrw", data.Labels().Get("rwDir")), testutil.AlpineImage, "cat", "/mntrw/file", ) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("success")), }, { Description: "Check that mntrw is seen in /proc/mounts", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Labels().Get("container"), "cat", "/proc/mounts") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( // Ensure we have mntrw in the mount list mountExistsWithOpt("/mntrw", "rw"), mountExistsWithOpt("/mntro", "ro"), ), } }, }, } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("container")) } testCase.Run(t) } func TestRunMountBindMode(t *testing.T) { if rootlessutil.IsRootless() { t.Skip("must be superuser to use mount") } t.Parallel() base := testutil.NewBase(t) tmpDir1, err := os.MkdirTemp(t.TempDir(), "rw") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmpDir1) tmpDir1Mnt := filepath.Join(tmpDir1, "mnt") if err := os.MkdirAll(tmpDir1Mnt, 0700); err != nil { t.Fatal(err) } tmpDir2, err := os.MkdirTemp(t.TempDir(), "ro") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmpDir2) if err := mobymount.Mount(tmpDir2, tmpDir1Mnt, "none", "bind,ro"); err != nil { t.Fatal(err) } defer func() { if err := mobymount.Unmount(tmpDir1Mnt); err != nil { t.Fatal(err) } }() base.Cmd("run", "--rm", "--mount", fmt.Sprintf("type=bind,bind-nonrecursive,src=%s,target=/mnt1", tmpDir1), testutil.AlpineImage, "sh", "-euxc", "apk add findmnt -q && findmnt -nR /mnt1", ).AssertOutWithFunc(func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) != 1 { return fmt.Errorf("expected 1 line, got %q", stdout) } if !strings.HasPrefix(lines[0], "/mnt1") { return fmt.Errorf("expected mount /mnt1, got %q", lines[0]) } return nil }) base.Cmd("run", "--rm", "--mount", fmt.Sprintf("type=bind,bind-nonrecursive=false,src=%s,target=/mnt1", tmpDir1), testutil.AlpineImage, "sh", "-euxc", "apk add findmnt -q && findmnt -nR /mnt1", ).AssertOutWithFunc(func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) != 2 { return fmt.Errorf("expected 2 line, got %q", stdout) } if !strings.HasPrefix(lines[0], "/mnt1") { return fmt.Errorf("expected mount /mnt1, got %q", lines[0]) } return nil }) } func TestRunVolumeBindMode(t *testing.T) { if rootlessutil.IsRootless() { t.Skip("must be superuser to use mount") } testutil.DockerIncompatible(t) t.Parallel() base := testutil.NewBase(t) tmpDir1, err := os.MkdirTemp(t.TempDir(), "rw") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmpDir1) tmpDir1Mnt := filepath.Join(tmpDir1, "mnt") if err := os.MkdirAll(tmpDir1Mnt, 0700); err != nil { t.Fatal(err) } tmpDir2, err := os.MkdirTemp(t.TempDir(), "ro") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmpDir2) if err := mobymount.Mount(tmpDir2, tmpDir1Mnt, "none", "bind,ro"); err != nil { t.Fatal(err) } defer func() { if err := mobymount.Unmount(tmpDir1Mnt); err != nil { t.Fatal(err) } }() base.Cmd("run", "--rm", "-v", fmt.Sprintf("%s:/mnt1:bind", tmpDir1), testutil.AlpineImage, "sh", "-euxc", "apk add findmnt -q && findmnt -nR /mnt1", ).AssertOutWithFunc(func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) != 1 { return fmt.Errorf("expected 1 line, got %q", stdout) } if !strings.HasPrefix(lines[0], "/mnt1") { return fmt.Errorf("expected mount /mnt1, got %q", lines[0]) } return nil }) base.Cmd("run", "--rm", "-v", fmt.Sprintf("%s:/mnt1:rbind", tmpDir1), testutil.AlpineImage, "sh", "-euxc", "apk add findmnt -q && findmnt -nR /mnt1", ).AssertOutWithFunc(func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) != 2 { return fmt.Errorf("expected 2 line, got %q", stdout) } if !strings.HasPrefix(lines[0], "/mnt1") { return fmt.Errorf("expected mount /mnt1, got %q", lines[0]) } return nil }) } func TestRunBindMountPropagation(t *testing.T) { t.Skip("This test is currently broken. See https://github.com/containerd/nerdctl/issues/3404") tID := testutil.Identifier(t) if !isRootfsShareableMount() { t.Skipf("rootfs doesn't support shared mount, skip test %s", tID) } t.Parallel() base := testutil.NewBase(t) testCases := []struct { propagation string assertFunc func(containerName, containerNameReplica string) }{ { propagation: "rshared", assertFunc: func(containerName, containerNameReplica string) { // replica can get sub-mounts from original base.Cmd("exec", containerNameReplica, "cat", "/mnt1/replica/foo.txt").AssertOutExactly("toreplica") // and sub-mounts from replica will be propagated to the original too base.Cmd("exec", containerName, "cat", "/mnt1/bar/bar.txt").AssertOutExactly("fromreplica") }, }, { propagation: "rslave", assertFunc: func(containerName, containerNameReplica string) { // replica can get sub-mounts from original base.Cmd("exec", containerNameReplica, "cat", "/mnt1/replica/foo.txt").AssertOutExactly("toreplica") // but sub-mounts from replica will not be propagated to the original base.Cmd("exec", containerName, "cat", "/mnt1/bar/bar.txt").AssertFail() }, }, { propagation: "rprivate", assertFunc: func(containerName, containerNameReplica string) { // replica can't get sub-mounts from original base.Cmd("exec", containerNameReplica, "cat", "/mnt1/replica/foo.txt").AssertFail() // and sub-mounts from replica will not be propagated to the original too base.Cmd("exec", containerName, "cat", "/mnt1/bar/bar.txt").AssertFail() }, }, { propagation: "", assertFunc: func(containerName, containerNameReplica string) { // replica can't get sub-mounts from original base.Cmd("exec", containerNameReplica, "cat", "/mnt1/replica/foo.txt").AssertFail() // and sub-mounts from replica will not be propagated to the original too base.Cmd("exec", containerName, "cat", "/mnt1/bar/bar.txt").AssertFail() }, }, } for _, tc := range testCases { propagationName := tc.propagation if propagationName == "" { propagationName = "default" } t.Logf("Running test propagation case %s", propagationName) rwDir, err := os.MkdirTemp(t.TempDir(), "rw") if err != nil { t.Fatal(err) } containerName := tID + "-" + propagationName containerNameReplica := containerName + "-replica" mountOption := fmt.Sprintf("type=bind,src=%s,target=/mnt1,bind-propagation=%s", rwDir, tc.propagation) if tc.propagation == "" { mountOption = fmt.Sprintf("type=bind,src=%s,target=/mnt1", rwDir) } containers := []struct { name string mountOption string }{ { name: containerName, mountOption: fmt.Sprintf("type=bind,src=%s,target=/mnt1,bind-propagation=rshared", rwDir), }, { name: containerNameReplica, mountOption: mountOption, }, } for _, c := range containers { base.Cmd("run", "-d", "--privileged", "--name", c.name, "--mount", c.mountOption, testutil.AlpineImage, "top").AssertOK() defer base.Cmd("rm", "-f", c.name).Run() } // mount in the first container base.Cmd("exec", containerName, "sh", "-exc", "mkdir /app && mkdir /mnt1/replica && mount --bind /app /mnt1/replica && echo -n toreplica > /app/foo.txt").AssertOK() base.Cmd("exec", containerName, "cat", "/mnt1/replica/foo.txt").AssertOutExactly("toreplica") // mount in the second container base.Cmd("exec", containerNameReplica, "sh", "-exc", "mkdir /bar && mkdir /mnt1/bar").AssertOK() base.Cmd("exec", containerNameReplica, "sh", "-exc", "mount --bind /bar /mnt1/bar").AssertOK() base.Cmd("exec", containerNameReplica, "sh", "-exc", "echo -n fromreplica > /bar/bar.txt").AssertOK() base.Cmd("exec", containerNameReplica, "cat", "/mnt1/bar/bar.txt").AssertOutExactly("fromreplica") // call case specific assert function tc.assertFunc(containerName, containerNameReplica) // umount mount point in the first privileged container base.Cmd("exec", containerNameReplica, "sh", "-exc", "umount /mnt1/bar").AssertOK() base.Cmd("exec", containerName, "sh", "-exc", "umount /mnt1/replica").AssertOK() } } // isRootfsShareableMount will check if /tmp or / support shareable mount func isRootfsShareableMount() bool { existFunc := func(mi mount.Info) bool { for _, opt := range strings.Split(mi.Optional, " ") { if strings.HasPrefix(opt, "shared:") { return true } } return false } mi, err := mount.Lookup("/tmp") if err == nil { return existFunc(mi) } mi, err = mount.Lookup("/") if err == nil { return existFunc(mi) } return false } func TestRunVolumesFrom(t *testing.T) { t.Parallel() base := testutil.NewBase(t) tID := testutil.Identifier(t) rwDir, err := os.MkdirTemp(t.TempDir(), "rw") if err != nil { t.Fatal(err) } roDir, err := os.MkdirTemp(t.TempDir(), "ro") if err != nil { t.Fatal(err) } rwVolName := tID + "-rw" roVolName := tID + "-ro" for _, v := range []string{rwVolName, roVolName} { defer base.Cmd("volume", "rm", "-f", v).Run() base.Cmd("volume", "create", v).AssertOK() } fromContainerName := tID + "-from" toContainerName := tID + "-to" defer base.Cmd("rm", "-f", fromContainerName).AssertOK() defer base.Cmd("rm", "-f", toContainerName).AssertOK() base.Cmd("run", "-d", "--name", fromContainerName, "-v", fmt.Sprintf("%s:/mnt1", rwDir), "-v", fmt.Sprintf("%s:/mnt2:ro", roDir), "-v", fmt.Sprintf("%s:/mnt3", rwVolName), "-v", fmt.Sprintf("%s:/mnt4:ro", roVolName), testutil.AlpineImage, "top", ).AssertOK() base.Cmd("run", "-d", "--name", toContainerName, "--volumes-from", fromContainerName, testutil.AlpineImage, "top", ).AssertOK() base.Cmd("exec", toContainerName, "sh", "-exc", "echo -n str1 > /mnt1/file1").AssertOK() base.Cmd("exec", toContainerName, "sh", "-exc", "echo -n str2 > /mnt2/file2").AssertFail() base.Cmd("exec", toContainerName, "sh", "-exc", "echo -n str3 > /mnt3/file3").AssertOK() base.Cmd("exec", toContainerName, "sh", "-exc", "echo -n str4 > /mnt4/file4").AssertFail() base.Cmd("rm", "-f", toContainerName).AssertOK() base.Cmd("run", "--rm", "--volumes-from", fromContainerName, testutil.AlpineImage, "cat", "/mnt1/file1", "/mnt3/file3", ).AssertOutExactly("str1str3") } func TestBindMountWhenHostFolderDoesNotExist(t *testing.T) { t.Parallel() base := testutil.NewBase(t) containerName := testutil.Identifier(t) + "-host-dir-not-found" hostDir, err := os.MkdirTemp(t.TempDir(), "rw") if err != nil { t.Fatal(err) } defer os.RemoveAll(hostDir) hp := filepath.Join(hostDir, "does-not-exist") base.Cmd("rm", "-f", containerName).AssertOK() base.Cmd("run", "--name", containerName, "-d", "-v", fmt.Sprintf("%s:/tmp", hp), testutil.AlpineImage).AssertOK() base.Cmd("rm", "-f", containerName).AssertOK() // Host directory should get created _, err = os.Stat(hp) assert.NilError(t, err) // Test for --mount os.RemoveAll(hp) base.Cmd("run", "--name", containerName, "-d", "--mount", fmt.Sprintf("type=bind, source=%s, target=/tmp", hp), testutil.AlpineImage).AssertFail() _, err = os.Stat(hp) assert.ErrorIs(t, err, os.ErrNotExist) } ================================================ FILE: cmd/nerdctl/container/container_run_mount_windows_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "fmt" "os" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/testutil" ) func TestRunMountVolume(t *testing.T) { t.Parallel() base := testutil.NewBase(t) tID := testutil.Identifier(t) rwDir, err := os.MkdirTemp(t.TempDir(), "rw") if err != nil { t.Fatal(err) } roDir, err := os.MkdirTemp(t.TempDir(), "ro") if err != nil { t.Fatal(err) } rwVolName := tID + "-rw" roVolName := tID + "-ro" for _, v := range []string{rwVolName, roVolName} { defer base.Cmd("volume", "rm", "-f", v).Run() base.Cmd("volume", "create", v).AssertOK() } containerName := tID defer base.Cmd("rm", "-f", containerName).AssertOK() base.Cmd("run", "-d", "--name", containerName, "-v", fmt.Sprintf("%s:C:/mnt1", rwDir), "-v", fmt.Sprintf("%s:C:/mnt2:ro", roDir), "-v", fmt.Sprintf("%s:C:/mnt3", rwVolName), "-v", fmt.Sprintf("%s:C:/mnt4:ro", roVolName), testutil.CommonImage, "ping localhost -t", ).AssertOK() base.Cmd("exec", containerName, "cmd", "/c", "echo -n str1 > C:/mnt1/file1").AssertOK() base.Cmd("exec", containerName, "cmd", "/c", "echo -n str2 > C:/mnt2/file2").AssertFail() base.Cmd("exec", containerName, "cmd", "/c", "echo -n str3 > C:/mnt3/file3").AssertOK() base.Cmd("exec", containerName, "cmd", "/c", "echo -n str4 > C:/mnt4/file4").AssertFail() base.Cmd("rm", "-f", containerName).AssertOK() base.Cmd("run", "--rm", "-v", fmt.Sprintf("%s:C:/mnt1", rwDir), "-v", fmt.Sprintf("%s:C:/mnt3", rwVolName), testutil.CommonImage, "cat", "C:/mnt1/file1", "C:/mnt3/file3", ).AssertOutContainsAll("str1", "str3") base.Cmd("run", "--rm", "-v", fmt.Sprintf("%s:C:/mnt3/mnt1", rwDir), "-v", fmt.Sprintf("%s:C:/mnt3", rwVolName), testutil.CommonImage, "cat", "C:/mnt3/mnt1/file1", "C:/mnt3/file3", ).AssertOutContainsAll("str1", "str3") } func TestRunMountVolumeInspect(t *testing.T) { base := testutil.NewBase(t) testContainer := testutil.Identifier(t) testVolume := testutil.Identifier(t) defer base.Cmd("volume", "rm", "-f", testVolume).Run() base.Cmd("volume", "create", testVolume).AssertOK() inspectVolume := base.InspectVolume(testVolume) namedVolumeSource := inspectVolume.Mountpoint base.Cmd( "run", "-d", "--name", testContainer, "-v", "C:/mnt1", "-v", "C:/mnt2:C:/mnt2", "-v", "\\\\.\\pipe\\containerd-containerd:\\\\.\\pipe\\containerd-containerd", "-v", fmt.Sprintf("%s:C:/mnt3", testVolume), testutil.CommonImage, ).AssertOK() inspect := base.InspectContainer(testContainer) // convert array to map to get by key of Destination actual := make(map[string]dockercompat.MountPoint) for i := range inspect.Mounts { actual[inspect.Mounts[i].Destination] = inspect.Mounts[i] } expected := []struct { dest string mountPoint dockercompat.MountPoint }{ // anonymous volume { dest: "C:\\mnt1", mountPoint: dockercompat.MountPoint{ Type: "volume", Source: "", // source of anonymous volume is a generated path, so here will not check it. Destination: "C:\\mnt1", }, }, // bind { dest: "C:\\mnt2", mountPoint: dockercompat.MountPoint{ Type: "bind", Source: "C:\\mnt2", Destination: "C:\\mnt2", }, }, // named pipe { dest: "\\\\.\\pipe\\containerd-containerd", mountPoint: dockercompat.MountPoint{ Type: "npipe", Source: "\\\\.\\pipe\\containerd-containerd", Destination: "\\\\.\\pipe\\containerd-containerd", }, }, // named volume { dest: "C:\\mnt3", mountPoint: dockercompat.MountPoint{ Type: "volume", Name: testVolume, Source: namedVolumeSource, Destination: "C:\\mnt3", }, }, } for i := range expected { testCase := expected[i] t.Logf("test volume[dest=%q]", testCase.dest) mountPoint, ok := actual[testCase.dest] assert.Assert(base.T, ok) assert.Equal(base.T, testCase.mountPoint.Type, mountPoint.Type) assert.Equal(base.T, testCase.mountPoint.Destination, mountPoint.Destination) if testCase.mountPoint.Source == "" { // for anonymous volumes, we want to make sure that the source is not the same as the destination assert.Assert(base.T, mountPoint.Source != testCase.mountPoint.Destination) } else { assert.Equal(base.T, testCase.mountPoint.Source, mountPoint.Source) } if testCase.mountPoint.Name != "" { assert.Equal(base.T, testCase.mountPoint.Name, mountPoint.Name) } } } func TestRunMountAnonymousVolume(t *testing.T) { t.Parallel() base := testutil.NewBase(t) base.Cmd("run", "--rm", "-v", "TestVolume:C:/mnt", testutil.CommonImage).AssertOK() // For docker-campatibility, Unrecognised volume spec: invalid volume specification: 'TestVolume' base.Cmd("run", "--rm", "-v", "TestVolume", testutil.CommonImage).AssertFail() // Destination must be an absolute path not named volume base.Cmd("run", "--rm", "-v", "TestVolume2:TestVolumes", testutil.CommonImage).AssertFail() } func TestRunMountRelativePath(t *testing.T) { t.Parallel() base := testutil.NewBase(t) base.Cmd("run", "--rm", "-v", "./mnt:C:/mnt1", testutil.CommonImage, "cmd").AssertOK() // Destination cannot be a relative path base.Cmd("run", "--rm", "-v", "./mnt", testutil.CommonImage).AssertFail() base.Cmd("run", "--rm", "-v", "./mnt:./mnt1", testutil.CommonImage, "cmd").AssertFail() } func TestRunMountNamedPipeVolume(t *testing.T) { t.Parallel() base := testutil.NewBase(t) base.Cmd("run", "--rm", "-v", `\\.\pipe\containerd-containerd`, testutil.CommonImage).AssertFail() } func TestRunMountVolumeSpec(t *testing.T) { t.Parallel() base := testutil.NewBase(t) base.Cmd("run", "--rm", "-v", `InvalidPathC:\TestVolume:C:\Mount`, testutil.CommonImage).AssertFail() base.Cmd("run", "--rm", "-v", `C:\TestVolume:C:\Mount:ro,rw:boot`, testutil.CommonImage).AssertFail() // If -v is an empty string, it will be ignored base.Cmd("run", "--rm", "-v", "", testutil.CommonImage).AssertOK() } ================================================ FILE: cmd/nerdctl/container/container_run_network.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "errors" "fmt" "net" "github.com/spf13/cobra" "github.com/containerd/go-cni" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/dnsutil" "github.com/containerd/nerdctl/v2/pkg/portutil" "github.com/containerd/nerdctl/v2/pkg/strutil" ) func loadNetworkFlags(cmd *cobra.Command, globalOpts types.GlobalCommandOptions) (types.NetworkOptions, error) { netOpts := types.NetworkOptions{} // --net/--network= ... var netSlice = []string{} var networkSet = false if cmd.Flags().Lookup("network").Changed { network, err := cmd.Flags().GetStringSlice("network") if err != nil { return netOpts, err } netSlice = append(netSlice, network...) networkSet = true } if cmd.Flags().Lookup("net").Changed { net, err := cmd.Flags().GetStringSlice("net") if err != nil { return netOpts, err } netSlice = append(netSlice, net...) networkSet = true } if !networkSet { network, err := cmd.Flags().GetStringSlice("network") if err != nil { return netOpts, err } netSlice = append(netSlice, network...) } netOpts.NetworkSlice = strutil.DedupeStrSlice(netSlice) // --mac-address= macAddress, err := cmd.Flags().GetString("mac-address") if err != nil { return netOpts, err } if macAddress != "" { if _, err := net.ParseMAC(macAddress); err != nil { return netOpts, err } } netOpts.MACAddress = macAddress // --ip= ipAddress, err := cmd.Flags().GetString("ip") if err != nil { return netOpts, err } netOpts.IPAddress = ipAddress // --ip6= ip6Address, err := cmd.Flags().GetString("ip6") if err != nil { return netOpts, err } netOpts.IP6Address = ip6Address // -h/--hostname= hostName, err := cmd.Flags().GetString("hostname") if err != nil { return netOpts, err } netOpts.Hostname = hostName // --domainname= domainname, err := cmd.Flags().GetString("domainname") if err != nil { return netOpts, err } netOpts.Domainname = domainname // --dns= ... // Use command flags if set, otherwise use global config is set var dnsSlice []string if cmd.Flags().Changed("dns") { var err error dnsSlice, err = cmd.Flags().GetStringSlice("dns") if err != nil { return netOpts, err } if len(dnsSlice) == 0 { return netOpts, errors.New("--dns flag was specified but no DNS server was provided") } for _, dns := range dnsSlice { if _, err := dnsutil.ValidateIPAddress(dns); err != nil { return netOpts, fmt.Errorf("%w with --dns flag", err) } } } else { dnsSlice = globalOpts.DNS } netOpts.DNSServers = strutil.DedupeStrSlice(dnsSlice) // --dns-search= ... // Use command flags if set, otherwise use global config is set var dnsSearchSlice []string if cmd.Flags().Changed("dns-search") { var err error dnsSearchSlice, err = cmd.Flags().GetStringSlice("dns-search") if err != nil { return netOpts, err } } else { dnsSearchSlice = globalOpts.DNSSearch } netOpts.DNSSearchDomains = strutil.DedupeStrSlice(dnsSearchSlice) // --dns-opt/--dns-option= ... // Use command flags if set, otherwise use global config if set dnsOptions := []string{} // Check if either dns-opt or dns-option flags were set dnsOptChanged := cmd.Flags().Changed("dns-opt") dnsOptionChanged := cmd.Flags().Changed("dns-option") if dnsOptChanged || dnsOptionChanged { // Use command flags dnsOptFlags, err := cmd.Flags().GetStringSlice("dns-opt") if err != nil { return netOpts, err } dnsOptions = append(dnsOptions, dnsOptFlags...) dnsOptionFlags, err := cmd.Flags().GetStringSlice("dns-option") if err != nil { return netOpts, err } dnsOptions = append(dnsOptions, dnsOptionFlags...) } else { // Use global config defaults dnsOptions = append(dnsOptions, globalOpts.DNSOpts...) } netOpts.DNSResolvConfOptions = strutil.DedupeStrSlice(dnsOptions) // --add-host= ... addHostFlags, err := cmd.Flags().GetStringSlice("add-host") if err != nil { return netOpts, err } netOpts.AddHost = addHostFlags // --uts= utsNamespace, err := cmd.Flags().GetString("uts") if err != nil { return netOpts, err } netOpts.UTSNamespace = utsNamespace // -p/--publish=127.0.0.1:80:8080/tcp ... portSlice, err := cmd.Flags().GetStringSlice("publish") if err != nil { return netOpts, err } portSlice = strutil.DedupeStrSlice(portSlice) portMappings := []cni.PortMapping{} for _, p := range portSlice { pm, err := portutil.ParseFlagP(p) if err != nil { return netOpts, err } portMappings = append(portMappings, pm...) } netOpts.PortMappings = portMappings return netOpts, nil } ================================================ FILE: cmd/nerdctl/container/container_run_network_base_test.go ================================================ //go:build linux || windows /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "fmt" "io" "net" "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" ) // Tests various port mapping argument combinations by starting an nginx container and // verifying its connectivity and that its serves its index.html from the external // host IP as well as through the loopback interface. // `loopbackIsolationEnabled` indicates whether the test should expect connections between // the loopback interface and external host interface to succeed or not. func baseTestRunPort(t *testing.T, nginxImage string, nginxIndexHTMLSnippet string, loopbackIsolationEnabled bool) { expectedIsolationErr := "" if loopbackIsolationEnabled { expectedIsolationErr = testutil.ExpectedConnectionRefusedError } hostIP, err := nettestutil.NonLoopbackIPv4() assert.NilError(t, err) type testCase struct { listenIP net.IP connectIP net.IP hostPort string containerPort string connectURLPort int runShouldSuccess bool err string } lo := net.ParseIP("127.0.0.1") zeroIP := net.ParseIP("0.0.0.0") testCases := []testCase{ { listenIP: lo, connectIP: lo, hostPort: "8080", containerPort: "80", connectURLPort: 8080, runShouldSuccess: true, }, { // for https://github.com/containerd/nerdctl/issues/88 listenIP: hostIP, connectIP: hostIP, hostPort: "8080", containerPort: "80", connectURLPort: 8080, runShouldSuccess: true, }, { listenIP: hostIP, connectIP: lo, hostPort: "8080", containerPort: "80", connectURLPort: 8080, err: expectedIsolationErr, runShouldSuccess: true, }, { listenIP: lo, connectIP: hostIP, hostPort: "8080", containerPort: "80", connectURLPort: 8080, err: expectedIsolationErr, runShouldSuccess: true, }, { listenIP: zeroIP, connectIP: lo, hostPort: "8080", containerPort: "80", connectURLPort: 8080, runShouldSuccess: true, }, { listenIP: zeroIP, connectIP: hostIP, hostPort: "8080", containerPort: "80", connectURLPort: 8080, runShouldSuccess: true, }, { listenIP: lo, connectIP: lo, hostPort: "7000-7005", containerPort: "79-84", connectURLPort: 7001, runShouldSuccess: true, }, { listenIP: hostIP, connectIP: hostIP, hostPort: "7000-7005", containerPort: "79-84", connectURLPort: 7001, runShouldSuccess: true, }, { listenIP: hostIP, connectIP: lo, hostPort: "7000-7005", containerPort: "79-84", connectURLPort: 7001, err: expectedIsolationErr, runShouldSuccess: true, }, { listenIP: lo, connectIP: hostIP, hostPort: "7000-7005", containerPort: "79-84", connectURLPort: 7001, err: expectedIsolationErr, runShouldSuccess: true, }, { listenIP: zeroIP, connectIP: hostIP, hostPort: "7000-7005", containerPort: "79-84", connectURLPort: 7001, runShouldSuccess: true, }, { listenIP: zeroIP, connectIP: lo, hostPort: "7000-7005", containerPort: "80-85", connectURLPort: 7001, err: "error after 5 attempts", runShouldSuccess: true, }, { listenIP: zeroIP, connectIP: lo, hostPort: "7000-7005", containerPort: "80", connectURLPort: 7000, runShouldSuccess: true, }, { listenIP: zeroIP, connectIP: lo, hostPort: "7000-7005", containerPort: "80", connectURLPort: 7005, err: testutil.ExpectedConnectionRefusedError, runShouldSuccess: true, }, { listenIP: zeroIP, connectIP: lo, hostPort: "7000-7005", containerPort: "79-85", connectURLPort: 7005, err: "invalid ranges specified for container and host Ports", runShouldSuccess: false, }, } tID := testutil.Identifier(t) for i, tc := range testCases { i := i tc := tc tcName := fmt.Sprintf("%+v", tc) t.Run(tcName, func(t *testing.T) { testContainerName := fmt.Sprintf("%s-%d", tID, i) base := testutil.NewBase(t) defer base.Cmd("rm", "-f", testContainerName).Run() pFlag := fmt.Sprintf("%s:%s:%s", tc.listenIP.String(), tc.hostPort, tc.containerPort) connectURL := fmt.Sprintf("http://%s:%d", tc.connectIP.String(), tc.connectURLPort) t.Logf("pFlag=%q, connectURL=%q", pFlag, connectURL) cmd := base.Cmd("run", "-d", "--name", testContainerName, "-p", pFlag, nginxImage) if tc.runShouldSuccess { cmd.AssertOK() } else { cmd.AssertFail() return } resp, err := nettestutil.HTTPGet(connectURL, 5, false) if tc.err != "" { assert.ErrorContains(t, err, tc.err) return } assert.NilError(t, err) respBody, err := io.ReadAll(resp.Body) assert.NilError(t, err) assert.Assert(t, strings.Contains(string(respBody), nginxIndexHTMLSnippet)) }) } } ================================================ FILE: cmd/nerdctl/container/container_run_network_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "fmt" "io" "net" "os" "os/exec" "path/filepath" "regexp" "strings" "testing" "time" "github.com/containernetworking/plugins/pkg/ns" "github.com/opencontainers/go-digest" "github.com/vishvananda/netlink" "gotest.tools/v3/assert" "gotest.tools/v3/icmd" "github.com/containerd/containerd/v2/defaults" "github.com/containerd/containerd/v2/pkg/netns" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" ) func extractHostPort(portMapping string, port string) (string, error) { // Regular expression to extract host port from port mapping information re := regexp.MustCompile(`(?P\d{1,5})/tcp ->.*?0.0.0.0:(?P\d{1,5}).*?`) portMappingLines := strings.Split(portMapping, "\n") for _, portMappingLine := range portMappingLines { // Find the matches matches := re.FindStringSubmatch(portMappingLine) // Check if there is a match if len(matches) >= 3 && matches[1] == port { // Extract the host port number hostPort := matches[2] return hostPort, nil } } return "", fmt.Errorf("could not extract host port from port mapping: %s", portMapping) } func valuesOfMapStringString(m map[string]string) map[string]struct{} { res := make(map[string]struct{}) for _, v := range m { res[v] = struct{}{} } return res } // TestRunInternetConnectivity tests Internet connectivity with `apk update` func TestRunInternetConnectivity(t *testing.T) { base := testutil.NewBase(t) customNet := testutil.Identifier(t) base.Cmd("network", "create", customNet).AssertOK() defer base.Cmd("network", "rm", customNet).Run() type testCase struct { args []string } customNetID := base.InspectNetwork(customNet).ID testCases := []testCase{ { args: []string{"--net", "bridge"}, }, { args: []string{"--net", customNet}, }, { args: []string{"--net", customNetID}, }, { args: []string{"--net", customNetID[:12]}, }, { args: []string{"--net", "host"}, }, } for _, tc := range testCases { tc := tc // IMPORTANT name := "default" if len(tc.args) > 0 { name = strings.Join(tc.args, "_") } t.Run(name, func(t *testing.T) { args := []string{"run", "--rm"} args = append(args, tc.args...) args = append(args, testutil.AlpineImage, "apk", "update") cmd := base.Cmd(args...) cmd.AssertOutContains("OK") }) } } // TestRunHostLookup tests hostname lookup func TestRunHostLookup(t *testing.T) { base := testutil.NewBase(t) // key: container name, val: network name m := map[string]string{ "c0-in-n0": "n0", "c1-in-n0": "n0", "c2-in-n1": "n1", "c3-in-bridge": "bridge", } customNets := valuesOfMapStringString(m) defer func() { for name := range m { base.Cmd("rm", "-f", name).Run() } for netName := range customNets { if netName == "bridge" { continue } base.Cmd("network", "rm", netName).Run() } }() // Create networks for netName := range customNets { if netName == "bridge" { continue } base.Cmd("network", "create", netName).AssertOK() } // Create nginx containers for name, netName := range m { cmd := base.Cmd("run", "-d", "--name", name, "--hostname", name+"-foobar", "--net", netName, testutil.NginxAlpineImage, ) t.Logf("creating host lookup testing container with command: %q", strings.Join(cmd.Command, " ")) cmd.AssertOK() } testWget := func(srcContainer, targetHostname string, expected bool) { t.Logf("resolving %q in container %q (should success: %+v)", targetHostname, srcContainer, expected) cmd := base.Cmd("exec", srcContainer, "wget", "-qO-", "http://"+targetHostname) if expected { cmd.AssertOutContains(testutil.NginxAlpineIndexHTMLSnippet) } else { cmd.AssertFail() } } // Tests begin testWget("c0-in-n0", "c1-in-n0", true) testWget("c0-in-n0", "c1-in-n0.n0", true) testWget("c0-in-n0", "c1-in-n0-foobar", true) testWget("c0-in-n0", "c1-in-n0-foobar.n0", true) testWget("c0-in-n0", "c2-in-n1", false) testWget("c0-in-n0", "c2-in-n1.n1", false) testWget("c0-in-n0", "c3-in-bridge", false) testWget("c1-in-n0", "c0-in-n0", true) testWget("c1-in-n0", "c0-in-n0.n0", true) testWget("c1-in-n0", "c0-in-n0-foobar", true) testWget("c1-in-n0", "c0-in-n0-foobar.n0", true) } func TestRunPortWithNoHostPort(t *testing.T) { if rootlessutil.IsRootless() { t.Skip("Auto port assign is not supported rootless mode yet") } type testCase struct { containerPort string runShouldSuccess bool } testCases := []testCase{ { containerPort: "80", runShouldSuccess: true, }, { containerPort: "80-81", runShouldSuccess: true, }, { containerPort: "80-81/tcp", runShouldSuccess: true, }, } tID := testutil.Identifier(t) for i, tc := range testCases { i := i tc := tc tcName := fmt.Sprintf("%+v", tc) t.Run(tcName, func(t *testing.T) { testContainerName := fmt.Sprintf("%s-%d", tID, i) base := testutil.NewBase(t) defer base.Cmd("rm", "-f", testContainerName).Run() pFlag := tc.containerPort cmd := base.Cmd("run", "-d", "--name", testContainerName, "-p", pFlag, testutil.NginxAlpineImage) var result *icmd.Result stdoutContent := "" if tc.runShouldSuccess { cmd.AssertOK() } else { cmd.AssertFail() return } portCmd := base.Cmd("port", testContainerName) portCmd.Base.T.Helper() result = portCmd.Run() stdoutContent = result.Stdout() + result.Stderr() assert.Assert(cmd.Base.T, result.ExitCode == 0, stdoutContent) regexExpression := regexp.MustCompile(`80\/tcp.*?->.*?0.0.0.0:(?P\d{1,5}).*?`) match := regexExpression.FindStringSubmatch(stdoutContent) paramsMap := make(map[string]string) for i, name := range regexExpression.SubexpNames() { if i > 0 && i <= len(match) { paramsMap[name] = match[i] } } if _, ok := paramsMap["portNumber"]; !ok { t.Fail() return } connectURL := fmt.Sprintf("http://%s:%s", "127.0.0.1", paramsMap["portNumber"]) resp, err := nettestutil.HTTPGet(connectURL, 5, false) assert.NilError(t, err) respBody, err := io.ReadAll(resp.Body) assert.NilError(t, err) assert.Assert(t, strings.Contains(string(respBody), testutil.NginxAlpineIndexHTMLSnippet)) }) } } func TestUniqueHostPortAssignement(t *testing.T) { if rootlessutil.IsRootless() { t.Skip("Auto port assign is not supported rootless mode yet") } type testCase struct { containerPort string runShouldSuccess bool } testCases := []testCase{ { containerPort: "80", runShouldSuccess: true, }, { containerPort: "80-81", runShouldSuccess: true, }, { containerPort: "80-81/tcp", runShouldSuccess: true, }, } tID := testutil.Identifier(t) for i, tc := range testCases { i := i tc := tc tcName := fmt.Sprintf("%+v", tc) t.Run(tcName, func(t *testing.T) { testContainerName1 := fmt.Sprintf("%s-%d-1", tID, i) testContainerName2 := fmt.Sprintf("%s-%d-2", tID, i) base := testutil.NewBase(t) defer base.Cmd("rm", "-f", testContainerName1, testContainerName2).Run() pFlag := tc.containerPort cmd1 := base.Cmd("run", "-d", "--name", testContainerName1, "-p", pFlag, testutil.NginxAlpineImage) cmd2 := base.Cmd("run", "-d", "--name", testContainerName2, "-p", pFlag, testutil.NginxAlpineImage) var result *icmd.Result stdoutContent := "" if tc.runShouldSuccess { cmd1.AssertOK() cmd2.AssertOK() } else { cmd1.AssertFail() cmd2.AssertFail() return } portCmd1 := base.Cmd("port", testContainerName1) portCmd2 := base.Cmd("port", testContainerName2) portCmd1.Base.T.Helper() portCmd2.Base.T.Helper() result = portCmd1.Run() stdoutContent = result.Stdout() + result.Stderr() assert.Assert(t, result.ExitCode == 0, stdoutContent) port1, err := extractHostPort(stdoutContent, "80") assert.NilError(t, err) result = portCmd2.Run() stdoutContent = result.Stdout() + result.Stderr() assert.Assert(t, result.ExitCode == 0, stdoutContent) port2, err := extractHostPort(stdoutContent, "80") assert.NilError(t, err) assert.Assert(t, port1 != port2, "Host ports are not unique") // Make HTTP GET request to container 1 connectURL1 := fmt.Sprintf("http://%s:%s", "127.0.0.1", port1) resp1, err := nettestutil.HTTPGet(connectURL1, 5, false) assert.NilError(t, err) respBody1, err := io.ReadAll(resp1.Body) assert.NilError(t, err) assert.Assert(t, strings.Contains(string(respBody1), testutil.NginxAlpineIndexHTMLSnippet)) // Make HTTP GET request to container 2 connectURL2 := fmt.Sprintf("http://%s:%s", "127.0.0.1", port2) resp2, err := nettestutil.HTTPGet(connectURL2, 5, false) assert.NilError(t, err) respBody2, err := io.ReadAll(resp2.Body) assert.NilError(t, err) assert.Assert(t, strings.Contains(string(respBody2), testutil.NginxAlpineIndexHTMLSnippet)) }) } } func TestHostPortAlreadyInUse(t *testing.T) { testCases := []struct { hostPort string containerPort string }{ { hostPort: "5000", containerPort: "80/tcp", }, { hostPort: "5000", containerPort: "80/tcp", }, { hostPort: "5000", containerPort: "80/udp", }, { hostPort: "5000", containerPort: "80/sctp", }, } tID := testutil.Identifier(t) for i, tc := range testCases { tc := tc tcName := fmt.Sprintf("%+v", tc) t.Run(tcName, func(t *testing.T) { if strings.Contains(tc.containerPort, "sctp") && rootlessutil.IsRootless() { t.Skip("sctp is not supported in rootless mode") } testContainerName1 := fmt.Sprintf("%s-%d-1", tID, i) testContainerName2 := fmt.Sprintf("%s-%d-2", tID, i) base := testutil.NewBase(t) t.Cleanup(func() { base.Cmd("rm", "-f", testContainerName1, testContainerName2).AssertOK() }) pFlag := fmt.Sprintf("%s:%s", tc.hostPort, tc.containerPort) cmd1 := base.Cmd("run", "-d", "--name", testContainerName1, "-p", pFlag, testutil.NginxAlpineImage) cmd2 := base.Cmd("run", "-d", "--name", testContainerName2, "-p", pFlag, testutil.NginxAlpineImage) cmd1.AssertOK() cmd2.AssertFail() }) } } func TestRunPort(t *testing.T) { baseTestRunPort(t, testutil.NginxAlpineImage, testutil.NginxAlpineIndexHTMLSnippet, true) } func TestRunWithManyPortsThenCleanUp(t *testing.T) { testCase := nerdtest.Setup() // docker does not set label restriction to 4096 bytes testCase.Require = require.Not(nerdtest.Docker) testCase.SubTests = []*test.Case{ { Description: "Run a container with many ports, and then clean up.", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--data-root", data.Temp().Path(), "--rm", "-p", "22200-22299:22200-22299", testutil.CommonImage) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Errors: []error{}, Output: func(stdout string, t tig.T) { getAddrHash := func(addr string) string { const addrHashLen = 8 d := digest.SHA256.FromString(addr) h := d.Encoded()[0:addrHashLen] return h } dataRoot := data.Temp().Path() h := getAddrHash(defaults.DefaultAddress) dataStore := filepath.Join(dataRoot, h) namespace := string(helpers.Read(nerdtest.Namespace)) etchostsPath := filepath.Join(dataStore, "etchosts", namespace) etchostsDirs, err := os.ReadDir(etchostsPath) assert.NilError(t, err) assert.Equal(t, len(etchostsDirs), 0) }, } }, }, } testCase.Run(t) } func TestRunContainerWithStaticIP(t *testing.T) { if rootlessutil.IsRootless() { t.Skip("Static IP assignment is not supported rootless mode yet.") } networkName := "test-network" networkSubnet := "172.0.0.0/16" base := testutil.NewBase(t) cmd := base.Cmd("network", "create", networkName, "--subnet", networkSubnet) cmd.AssertOK() defer base.Cmd("network", "rm", networkName).Run() testCases := []struct { ip string shouldSuccess bool useNetwork bool checkTheIPAddress bool }{ { ip: "172.0.0.2", shouldSuccess: true, useNetwork: true, checkTheIPAddress: true, }, { ip: "192.0.0.2", shouldSuccess: false, useNetwork: true, checkTheIPAddress: false, }, // XXX see https://github.com/containerd/nerdctl/issues/3101 // docker 24 silently ignored the ip - now, docker 26 is erroring out - furthermore, this ip only makes sense // in the context of nerdctl bridge network, so, this test needs rewritting either way /* { ip: "10.4.0.2", shouldSuccess: true, useNetwork: false, checkTheIPAddress: false, }, */ } tID := testutil.Identifier(t) for i, tc := range testCases { i := i tc := tc tcName := fmt.Sprintf("%+v", tc) t.Run(tcName, func(t *testing.T) { testContainerName := fmt.Sprintf("%s-%d", tID, i) base := testutil.NewBase(t) defer base.Cmd("rm", "-f", testContainerName).Run() args := []string{ "run", "-d", "--name", testContainerName, } if tc.useNetwork { args = append(args, []string{"--network", networkName}...) } args = append(args, []string{"--ip", tc.ip, testutil.NginxAlpineImage}...) cmd := base.Cmd(args...) if !tc.shouldSuccess { cmd.AssertFail() return } cmd.AssertOK() if tc.checkTheIPAddress { inspectCmd := base.Cmd("inspect", testContainerName, "--format", "\"{{range .NetworkSettings.Networks}} {{.IPAddress}}{{end}}\"") result := inspectCmd.Run() stdoutContent := result.Stdout() + result.Stderr() assert.Assert(inspectCmd.Base.T, result.ExitCode == 0, stdoutContent) if !strings.Contains(stdoutContent, tc.ip) { t.Fail() return } } }) } } func TestRunDNS(t *testing.T) { base := testutil.NewBase(t) base.Cmd("run", "--rm", "--dns", "8.8.8.8", testutil.CommonImage, "cat", "/etc/resolv.conf").AssertOutContains("nameserver 8.8.8.8\n") base.Cmd("run", "--rm", "--dns-search", "test", testutil.CommonImage, "cat", "/etc/resolv.conf").AssertOutContains("search test\n") base.Cmd("run", "--rm", "--dns-search", "test", "--dns-search", "test1", testutil.CommonImage, "cat", "/etc/resolv.conf").AssertOutContains("search test test1\n") base.Cmd("run", "--rm", "--dns-opt", "no-tld-query", "--dns-option", "attempts:10", testutil.CommonImage, "cat", "/etc/resolv.conf").AssertOutContains("options no-tld-query attempts:10\n") cmd := base.Cmd("run", "--rm", "--dns", "8.8.8.8", "--dns-search", "test", "--dns-option", "attempts:10", testutil.CommonImage, "cat", "/etc/resolv.conf") cmd.AssertOutContains("nameserver 8.8.8.8\n") cmd.AssertOutContains("search test\n") cmd.AssertOutContains("options attempts:10\n") } func TestRunNetworkHostHostname(t *testing.T) { base := testutil.NewBase(t) hostname, err := os.Hostname() assert.NilError(t, err) hostname = hostname + "\n" base.Cmd("run", "--rm", "--network", "host", testutil.CommonImage, "hostname").AssertOutExactly(hostname) base.Cmd("run", "--rm", "--network", "host", testutil.CommonImage, "sh", "-euxc", "echo $HOSTNAME").AssertOutExactly(hostname) base.Cmd("run", "--rm", "--network", "host", "--hostname", "override", testutil.CommonImage, "hostname").AssertOutExactly("override\n") base.Cmd("run", "--rm", "--network", "host", "--hostname", "override", testutil.CommonImage, "sh", "-euxc", "echo $HOSTNAME").AssertOutExactly("override\n") } func TestRunNetworkHost2613(t *testing.T) { base := testutil.NewBase(t) base.Cmd("run", "--rm", "--add-host", "foo:1.2.3.4", testutil.CommonImage, "getent", "hosts", "foo").AssertOutExactly("1.2.3.4 foo foo\n") } func TestSharedNetworkSetup(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.Not(require.Windows), Setup: func(data test.Data, helpers test.Helpers) { data.Labels().Set("container1", data.Identifier("container1")) helpers.Ensure("run", "-d", "--name", data.Identifier("container1"), testutil.CommonImage, "sleep", "inf") nerdtest.EnsureContainerStarted(helpers, data.Identifier("container1")) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("container1")) }, SubTests: []*test.Case{ { Description: "Test network is shared", NoParallel: true, // The validation involves starting of the main container: container1 Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("container2")) }, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure( "run", "-d", "--name", data.Identifier("container2"), "--network=container:"+data.Labels().Get("container1"), testutil.NginxAlpineImage) data.Labels().Set("container2", data.Identifier("container2")) nerdtest.EnsureContainerStarted(helpers, data.Identifier("container2")) }, SubTests: []*test.Case{ { NoParallel: true, Description: "Test network is shared", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Labels().Get("container2"), "wget", "-qO-", "http://127.0.0.1:80") }, Expected: test.Expects(0, nil, expect.Contains(testutil.NginxAlpineIndexHTMLSnippet)), }, { NoParallel: true, Description: "Test network is shared after restart", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("restart", data.Labels().Get("container1")) helpers.Ensure("stop", "--time=1", data.Labels().Get("container2")) helpers.Ensure("start", data.Labels().Get("container2")) nerdtest.EnsureContainerStarted(helpers, data.Labels().Get("container2")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Labels().Get("container2"), "wget", "-qO-", "http://127.0.0.1:80") }, Expected: test.Expects(0, nil, expect.Contains(testutil.NginxAlpineIndexHTMLSnippet)), }, }, }, { Description: "Test uts is supported in shared network", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--uts", "host", "--network=container:"+data.Labels().Get("container1"), testutil.CommonImage) }, Expected: test.Expects(0, nil, nil), }, { Description: "Test dns is not supported", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--dns", "0.1.2.3", "--network=container:"+data.Labels().Get("container1"), testutil.CommonImage) }, // 1 for nerdctl, 125 for docker Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, { Description: "Test dns options is not supported", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--dns-option", "attempts:5", "--network=container:"+data.Labels().Get("container1"), testutil.CommonImage, "cat", "/etc/resolv.conf") }, // The Option doesn't throw an error but is never inserted to the resolv.conf Expected: test.Expects(0, nil, expect.DoesNotContain("attempts:5")), }, { Description: "Test publish is not supported", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--publish", "80:8080", "--network=container:"+data.Labels().Get("container1"), testutil.AlpineImage) }, // 1 for nerdctl, 125 for docker Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, { Description: "Test hostname is not supported", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--hostname", "test", "--network=container:"+data.Labels().Get("container1"), testutil.AlpineImage) }, // 1 for nerdctl, 125 for docker Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, }, } testCase.Run(t) } func TestSharedNetworkWithNone(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.Not(require.Windows), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier("container1"), "--network", "none", testutil.CommonImage, "sleep", "inf") nerdtest.EnsureContainerStarted(helpers, data.Identifier("container1")) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("container1")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--network=container:"+data.Identifier("container1"), testutil.CommonImage) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), } testCase.Run(t) } func TestRunContainerInExistingNetNS(t *testing.T) { if rootlessutil.IsRootless() { t.Skip("Can't create new netns in rootless mode") } testutil.DockerIncompatible(t) base := testutil.NewBase(t) netNS, err := netns.NewNetNS(t.TempDir() + "/netns") assert.NilError(t, err) err = netNS.Do(func(netns ns.NetNS) error { loopback, err := netlink.LinkByName("lo") assert.NilError(t, err) err = netlink.LinkSetUp(loopback) assert.NilError(t, err) return nil }) assert.NilError(t, err) defer netNS.Remove() containerName := testutil.Identifier(t) defer base.Cmd("rm", "-f", containerName).AssertOK() base.Cmd("run", "-d", "--name", containerName, "--network=ns:"+netNS.GetPath(), testutil.NginxAlpineImage).AssertOK() base.EnsureContainerStarted(containerName) time.Sleep(3 * time.Second) err = netNS.Do(func(netns ns.NetNS) error { stdout, err := exec.Command("curl", "-s", "http://127.0.0.1:80").Output() assert.NilError(t, err) assert.Assert(t, strings.Contains(string(stdout), testutil.NginxAlpineIndexHTMLSnippet)) return nil }) assert.NilError(t, err) } func TestRunContainerWithMACAddress(t *testing.T) { base := testutil.NewBase(t) tID := testutil.Identifier(t) networkBridge := "testNetworkBridge" + tID networkMACvlan := "testNetworkMACvlan" + tID networkIPvlan := "testNetworkIPvlan" + tID tearDown := func() { base.Cmd("network", "rm", networkBridge).Run() base.Cmd("network", "rm", networkMACvlan).Run() base.Cmd("network", "rm", networkIPvlan).Run() } tearDown() t.Cleanup(tearDown) base.Cmd("network", "create", networkBridge, "--driver", "bridge").AssertOK() base.Cmd("network", "create", networkMACvlan, "--driver", "macvlan").AssertOK() base.Cmd("network", "create", networkIPvlan, "--driver", "ipvlan").AssertOK() defaultMac := base.Cmd("run", "--rm", "-i", "--network", "host", testutil.CommonImage). CmdOption(testutil.WithStdin(strings.NewReader("ip addr show eth0 | grep ether | awk '{printf $2}'"))). Run().Stdout() passedMac := "we expect the generated mac on the output" tests := []struct { Network string WantErr bool Expect string }{ {"host", false, defaultMac}, // anything but the actual address being passed {"none", false, ""}, // nothing {"container:whatever" + tID, true, "container"}, // "No such container" vs. "could not find container" {"bridge", false, passedMac}, {networkBridge, false, passedMac}, {networkMACvlan, false, passedMac}, {networkIPvlan, true, "not support"}, } for i, test := range tests { containerName := fmt.Sprintf("%s_%d", tID, i) testName := fmt.Sprintf("%s_container:%s_network:%s_expect:%s", tID, containerName, test.Network, test.Expect) expect := test.Expect network := test.Network wantErr := test.WantErr t.Run(testName, func(tt *testing.T) { tt.Parallel() macAddress, err := nettestutil.GenerateMACAddress() if err != nil { t.Errorf("failed to generate MAC address: %s", err) } if expect == passedMac { expect = macAddress } res := base.Cmd("run", "--rm", "-i", "--network", network, "--mac-address", macAddress, testutil.CommonImage). CmdOption(testutil.WithStdin(strings.NewReader("ip addr show eth0 | grep ether | awk '{printf $2}'"))).Run() if wantErr { assert.Assert(t, res.ExitCode != 0, "Command should have failed", res) assert.Assert(t, strings.Contains(res.Combined(), expect), fmt.Sprintf("expected output to contain %q: %q", expect, res.Combined())) } else { assert.Assert(t, res.ExitCode == 0, "Command should have succeeded", res) assert.Assert(t, strings.Contains(res.Stdout(), expect), fmt.Sprintf("expected output to contain %q: %q", expect, res.Stdout())) } }) } } func TestHostsFileMounts(t *testing.T) { if rootlessutil.IsRootless() { if detachedNetNS, _ := rootlessutil.DetachedNetNS(); detachedNetNS != "" { t.Skip("/etc/hosts is not writable") } } base := testutil.NewBase(t) base.Cmd("run", "--rm", testutil.CommonImage, "sh", "-euxc", "echo >> /etc/hosts").AssertOK() base.Cmd("run", "--rm", "--network", "host", testutil.CommonImage, "sh", "-euxc", "echo >> /etc/hosts").AssertOK() base.Cmd("run", "--rm", "-v", "/etc/hosts:/etc/hosts:ro", "--network", "host", testutil.CommonImage, "sh", "-euxc", "echo >> /etc/hosts").AssertFail() // add a line into /etc/hosts and remove it. base.Cmd("run", "--rm", "-v", "/etc/hosts:/etc/hosts", "--network", "host", testutil.CommonImage, "sh", "-euxc", "echo >> /etc/hosts").AssertOK() base.Cmd("run", "--rm", "-v", "/etc/hosts:/etc/hosts", "--network", "host", testutil.CommonImage, "sh", "-euxc", "head -n -1 /etc/hosts > temp && cat temp > /etc/hosts").AssertOK() base.Cmd("run", "--rm", "--network", "none", testutil.CommonImage, "sh", "-euxc", "echo >> /etc/hosts").AssertOK() base.Cmd("run", "--rm", testutil.CommonImage, "sh", "-euxc", "echo >> /etc/resolv.conf").AssertOK() base.Cmd("run", "--rm", "--network", "host", testutil.CommonImage, "sh", "-euxc", "echo >> /etc/resolv.conf").AssertOK() base.Cmd("run", "--rm", "-v", "/etc/resolv.conf:/etc/resolv.conf:ro", "--network", "host", testutil.CommonImage, "sh", "-euxc", "echo >> /etc/resolv.conf").AssertFail() // add a line into /etc/resolv.conf and remove it. base.Cmd("run", "--rm", "-v", "/etc/resolv.conf:/etc/resolv.conf", "--network", "host", testutil.CommonImage, "sh", "-euxc", "echo >> /etc/resolv.conf").AssertOK() base.Cmd("run", "--rm", "-v", "/etc/resolv.conf:/etc/resolv.conf", "--network", "host", testutil.CommonImage, "sh", "-euxc", "head -n -1 /etc/resolv.conf > temp && cat temp > /etc/resolv.conf").AssertOK() base.Cmd("run", "--rm", "--network", "host", testutil.CommonImage, "sh", "-euxc", "echo >> /etc/resolv.conf").AssertOK() } func TestRunContainerWithStaticIP6(t *testing.T) { if rootlessutil.IsRootless() { t.Skip("Static IP6 assignment is not supported rootless mode yet.") } networkName := "test-network" networkSubnet := "2001:db8:5::/64" _, subnet, err := net.ParseCIDR(networkSubnet) assert.Assert(t, err == nil) base := testutil.NewBaseWithIPv6Compatible(t) base.Cmd("network", "create", networkName, "--subnet", networkSubnet, "--ipv6").AssertOK() t.Cleanup(func() { base.Cmd("network", "rm", networkName).Run() }) testCases := []struct { ip string shouldSuccess bool checkTheIPAddress bool }{ { ip: "", shouldSuccess: true, checkTheIPAddress: false, }, { ip: "2001:db8:5::6", shouldSuccess: true, checkTheIPAddress: true, }, { ip: "2001:db8:4::6", shouldSuccess: false, checkTheIPAddress: false, }, } tID := testutil.Identifier(t) for i, tc := range testCases { i := i tc := tc tcName := fmt.Sprintf("%+v", tc) t.Run(tcName, func(t *testing.T) { testContainerName := fmt.Sprintf("%s-%d", tID, i) base := testutil.NewBaseWithIPv6Compatible(t) args := []string{ "run", "--rm", "--name", testContainerName, "--network", networkName, } if tc.ip != "" { args = append(args, "--ip6", tc.ip) } args = append(args, []string{testutil.NginxAlpineImage, "ip", "addr", "show", "dev", "eth0"}...) cmd := base.Cmd(args...) if !tc.shouldSuccess { cmd.AssertFail() return } cmd.AssertOutWithFunc(func(stdout string) error { ip := nerdtest.FindIPv6(stdout) if !subnet.Contains(ip) { return fmt.Errorf("expected subnet %s include ip %s", subnet, ip) } if tc.checkTheIPAddress { if ip.String() != tc.ip { return fmt.Errorf("expected ip %s, got %s", tc.ip, ip) } } return nil }) }) } } func TestNoneNetworkHostName(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.Not(require.Windows), Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Setup: func(data test.Data, helpers test.Helpers) { output := helpers.Capture("run", "-d", "--name", data.Identifier(), "--network", "none", testutil.CommonImage, "sleep", "inf") assert.Assert(helpers.T(), len(output) > 12, output) data.Labels().Set("hostname", output[:12]) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Identifier(), "cat", "/etc/hostname") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.Equals(data.Labels().Get("hostname") + "\n"), } }, } testCase.Run(t) } func TestHostNetworkHostName(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.Not(require.Windows), Setup: func(data test.Data, helpers test.Helpers) { helpers.Custom("cat", "/etc/hostname").Run(&test.Expected{ Output: func(stdout string, t tig.T) { data.Labels().Set("hostHostname", stdout) }, }) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--network", "host", testutil.AlpineImage, "cat", "/etc/hostname") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.Equals(data.Labels().Get("hostHostname")), } }, } testCase.Run(t) } func TestHostNetworkDnsPreserved(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.Not(require.Windows), Setup: func(data test.Data, helpers test.Helpers) { // In some rootless CI job, slirp provides 10.0.2.3 as DNS server. // We cannot simply parse host /etc/resolv.conf here. helpers.Command("run", "--rm", "-v", "/etc/resolv.conf:/mnt/resolv.conf:ro", testutil.AlpineImage, "grep", "-E", "^nameserver\\s+", "/mnt/resolv.conf").Run(&test.Expected{ Output: func(stdout string, t tig.T) { data.Labels().Set("nameservers", stdout) }, }) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--network", "host", testutil.AlpineImage, "grep", "-E", "^nameserver\\s+", "/etc/resolv.conf") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { // container with --network=host should have same nameserver as host nameservers := data.Labels().Get("nameservers") return &test.Expected{ Output: expect.Equals(nameservers), } }, } testCase.Run(t) } func TestDefaultNetworkDnsNoLocalhost(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.Not(require.Windows), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", testutil.AlpineImage, "grep", "-E", "^nameserver\\s+(127\\.|::1)", "/etc/resolv.conf") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, // no match } }, } testCase.Run(t) } func TestNoneNetworkDnsConfigs(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.Not(require.Windows), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--network", "none", "--dns", "0.1.2.3", "--dns-search", "example.com", "--dns-option", "timeout:3", "--dns-option", "attempts:5", testutil.CommonImage, "cat", "/etc/resolv.conf") }, Expected: test.Expects(0, nil, expect.Contains( "0.1.2.3", "example.com", "attempts:5", "timeout:3", )), } testCase.Run(t) } func TestHostNetworkDnsConfigs(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.Not(require.Windows), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--network", "host", "--dns", "0.1.2.3", "--dns-search", "example.com", "--dns-option", "timeout:3", "--dns-option", "attempts:5", testutil.CommonImage, "cat", "/etc/resolv.conf") }, Expected: test.Expects(0, nil, expect.Contains( "0.1.2.3", "example.com", "attempts:5", "timeout:3", )), } testCase.Run(t) } func TestDNSWithGlobalConfig(t *testing.T) { var configContent test.ConfigValue = `debug = false debug_full = false dns = ["10.10.10.10", "20.20.20.20"] dns_opts = ["ndots:2", "timeout:5"] dns_search = ["example.com", "test.local"]` nerdtest.Setup() testCase := &test.Case{ Config: test.WithConfig(nerdtest.NerdctlToml, configContent), // NERDCTL_TOML not supported in Docker Require: require.Not(nerdtest.Docker), SubTests: []*test.Case{ { Description: "Global DNS settings are used when command line options are not provided", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { nerdctlTomlContent := string(helpers.Read(nerdtest.NerdctlToml)) helpers.T().Log("NERDCTL_TOML file content:\n%s", nerdctlTomlContent) cmd := helpers.Command("run", "--rm", testutil.CommonImage, "cat", "/etc/resolv.conf") return cmd }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( expect.Contains("nameserver 10.10.10.10"), expect.Contains("nameserver 20.20.20.20"), expect.Contains("search example.com test.local"), expect.Contains("options ndots:2 timeout:5"), )), }, { Description: "Command line DNS options override global config", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { nerdctlTomlContent := string(helpers.Read(nerdtest.NerdctlToml)) helpers.T().Log("NERDCTL_TOML file content:\n%s", nerdctlTomlContent) cmd := helpers.Command("run", "--rm", "--dns", "9.9.9.9", "--dns-search", "override.com", "--dns-opt", "ndots:3", testutil.CommonImage, "cat", "/etc/resolv.conf") return cmd }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( expect.Contains("nameserver 9.9.9.9"), expect.Contains("search override.com"), expect.Contains("options ndots:3"), )), }, { Description: "Global DNS settings should also apply when using host network", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { nerdctlTomlContent := string(helpers.Read(nerdtest.NerdctlToml)) helpers.T().Log("NERDCTL_TOML file content:\n%s", nerdctlTomlContent) cmd := helpers.Command("run", "--rm", "--network", "host", testutil.CommonImage, "cat", "/etc/resolv.conf") return cmd }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( expect.Contains("nameserver 10.10.10.10"), expect.Contains("nameserver 20.20.20.20"), expect.Contains("search example.com test.local"), expect.Contains("options ndots:2 timeout:5"), )), }, { Description: "Global DNS settings should also apply when using none network", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { nerdctlTomlContent := string(helpers.Read(nerdtest.NerdctlToml)) helpers.T().Log("NERDCTL_TOML file content:\n%s", nerdctlTomlContent) cmd := helpers.Command("run", "--rm", "--network", "none", testutil.CommonImage, "cat", "/etc/resolv.conf") return cmd }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( expect.Contains("nameserver 10.10.10.10"), expect.Contains("nameserver 20.20.20.20"), expect.Contains("search example.com test.local"), expect.Contains("options ndots:2 timeout:5"), )), }, }, } testCase.Run(t) } // TestReservePorts tests that a published port appears // as a listening port on the host. // See https://github.com/containerd/nerdctl/pull/4526 func TestReservePorts(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.All( require.Not(require.Windows), require.Not(nerdtest.RootlessWithoutDetachNetNS), // RootlessKit v1 ), NoParallel: true, SubTests: []*test.Case{ { Description: "TCP", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier("nginx"), "-p", "60080:80", testutil.NginxAlpineImage) nerdtest.EnsureContainerStarted(helpers, data.Identifier("nginx")) time.Sleep(3 * time.Second) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("nginx")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--network=host", testutil.CommonImage, "netstat", "-lnt") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( expect.Contains(":60080"), )), }, { Description: "UDP", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier("coredns"), "-p", "60053:53/udp", testutil.CoreDNSImage) nerdtest.EnsureContainerStarted(helpers, data.Identifier("coredns")) time.Sleep(3 * time.Second) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("coredns")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--network=host", testutil.CommonImage, "netstat", "-lnu") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( expect.Contains(":60053"), )), }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_run_network_windows_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "fmt" "regexp" "strings" "testing" "github.com/Microsoft/hcsshim" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/v2/pkg/defaults" "github.com/containerd/nerdctl/v2/pkg/netutil" "github.com/containerd/nerdctl/v2/pkg/testutil" ) // TestRunInternetConnectivity tests Internet connectivity by pinging github.com. func TestRunInternetConnectivity(t *testing.T) { base := testutil.NewBase(t) type testCase struct { args []string } testCases := []testCase{ { args: []string{"--net", "nat"}, }, } for _, tc := range testCases { tc := tc // IMPORTANT name := "default" if len(tc.args) > 0 { name = strings.Join(tc.args, "_") } t.Run(name, func(t *testing.T) { args := []string{"run", "--rm"} args = append(args, tc.args...) // TODO(aznashwan): smarter way to ensure internet connectivity is working. // ping doesn't seem to work on GitHub Actions ("Request timed out.") args = append(args, testutil.CommonImage, "curl.exe -sSL https://github.com") cmd := base.Cmd(args...) cmd.AssertOutContains("") }) } } func TestRunPort(t *testing.T) { // NOTE: currently no isolation between the loopback and host namespaces on Windows. baseTestRunPort(t, testutil.NginxAlpineImage, testutil.NginxAlpineIndexHTMLSnippet, false) } // Checks whether an HNS endpoint with a name matching exists. func listHnsEndpointsRegex(hnsEndpointNameRegex string) ([]hcsshim.HNSEndpoint, error) { r, err := regexp.Compile(hnsEndpointNameRegex) if err != nil { return nil, err } hnsEndpoints, err := hcsshim.HNSListEndpointRequest() if err != nil { return nil, fmt.Errorf("failed to list HNS endpoints for request: %w", err) } res := []hcsshim.HNSEndpoint{} for _, endp := range hnsEndpoints { if r.Match([]byte(endp.Name)) { res = append(res, endp) } } return res, nil } // Asserts whether the container with the provided has any HNS endpoints with the expected // naming format (`${container_id}_${network_name}`) for all of the provided network names. // The container ID can be a regex. func assertHnsEndpointsExistence(t *testing.T, shouldExist bool, containerIDRegex string, networkNames ...string) { for _, netName := range networkNames { endpointName := fmt.Sprintf("%s_%s", containerIDRegex, netName) testName := fmt.Sprintf("hns_endpoint_%s_shouldExist_%t", endpointName, shouldExist) t.Run(testName, func(t *testing.T) { matchingEndpoints, err := listHnsEndpointsRegex(endpointName) assert.NilError(t, err) if shouldExist { assert.Equal(t, len(matchingEndpoints), 1) assert.Equal(t, matchingEndpoints[0].Name, endpointName) } else { assert.Equal(t, len(matchingEndpoints), 0) } }) } } // Tests whether HNS endpoints are properly created and managed throughout the lifecycle of a container. func TestHnsEndpointsExistDuringContainerLifecycle(t *testing.T) { base := testutil.NewBase(t) testNet, err := getTestingNetwork() assert.NilError(t, err) tID := testutil.Identifier(t) defer base.Cmd("rm", "-f", tID).Run() cmd := base.Cmd( "create", "--name", tID, "--net", testNet.Name, testutil.CommonImage, "bash", "-c", // NOTE: the BusyBox image used in Windows testing's `sleep` binary // does not support the `infinity` argument. "tail", "-f", ) t.Logf("Creating HNS lifecycle test container with command: %q", strings.Join(cmd.Command, " ")) containerID := strings.TrimSpace(cmd.Run().Stdout()) t.Logf("HNS endpoint lifecycle test container ID: %q", containerID) // HNS endpoints should be allocated on container creation. assertHnsEndpointsExistence(t, true, containerID, testNet.Name) // Starting and stopping the container should NOT affect/change the endpoints. base.Cmd("start", containerID).AssertOK() assertHnsEndpointsExistence(t, true, containerID, testNet.Name) base.Cmd("stop", containerID).AssertOK() assertHnsEndpointsExistence(t, true, containerID, testNet.Name) // Removing the container should remove the HNS endpoints. base.Cmd("rm", containerID).AssertOK() assertHnsEndpointsExistence(t, false, containerID, testNet.Name) } // Returns a network to be used for testing. // Note: currently hardcoded to return the default network, as `network create` // does not work on Windows. func getTestingNetwork() (*netutil.NetworkConfig, error) { // NOTE: cannot currently `nerdctl network create` on Windows so we use a pre-existing network: cniEnv, err := netutil.NewCNIEnv(defaults.CNIPath(), defaults.CNINetConfPath()) if err != nil { return nil, err } return cniEnv.GetDefaultNetworkConfig() } // Tests whether HNS endpoints are properly removed when running `run --rm`. func TestHnsEndpointsRemovedAfterAttachedRun(t *testing.T) { base := testutil.NewBase(t) testNet, err := getTestingNetwork() assert.NilError(t, err) // NOTE: because we cannot set/obtain the ID of the container to check for the exact HNS // endpoint name, we record the number of HNS endpoints on the testing network and // ensure it remains constant until after the test. existingEndpoints, err := listHnsEndpointsRegex(fmt.Sprintf(".*_%s", testNet.Name)) assert.NilError(t, err) originalEndpointsCount := len(existingEndpoints) tID := testutil.Identifier(t) base.Cmd( "run", "--name", tID, "--rm", "--net", testNet.Name, testutil.CommonImage, "ipconfig", "/all", ).AssertOK() existingEndpoints, err = listHnsEndpointsRegex(fmt.Sprintf(".*_%s", testNet.Name)) assert.NilError(t, err) assert.Equal(t, originalEndpointsCount, len(existingEndpoints), "the number of HNS endpoints should equal pre-test amount") } ================================================ FILE: cmd/nerdctl/container/container_run_nolinux.go ================================================ //go:build !linux /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "github.com/spf13/cobra" ) func capShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { candidates := []string{} return candidates, cobra.ShellCompDirectiveNoFileComp } ================================================ FILE: cmd/nerdctl/container/container_run_restart_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "fmt" "io" "os/exec" "strings" "testing" "time" "gotest.tools/v3/assert" "gotest.tools/v3/poll" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" ) func TestRunRestart(t *testing.T) { const ( hostPort = 8080 ) testContainerName := testutil.Identifier(t) if testing.Short() { t.Skipf("test is long") } base := testutil.NewBase(t) if !base.DaemonIsKillable { t.Skip("daemon is not killable (hint: set \"-test.allow-kill-daemon\")") } t.Log("NOTE: this test may take a while") defer base.Cmd("rm", "-f", testContainerName).Run() base.Cmd("run", "-d", "--restart=always", "--name", testContainerName, "-p", fmt.Sprintf("127.0.0.1:%d:80", hostPort), testutil.NginxAlpineImage).AssertOK() check := func(httpGetRetry int) error { resp, err := nettestutil.HTTPGet(fmt.Sprintf("http://127.0.0.1:%d", hostPort), httpGetRetry, false) if err != nil { return err } respBody, err := io.ReadAll(resp.Body) if err != nil { return err } if !strings.Contains(string(respBody), testutil.NginxAlpineIndexHTMLSnippet) { return fmt.Errorf("expected contain %q, got %q", testutil.NginxAlpineIndexHTMLSnippet, string(respBody)) } return nil } assert.NilError(t, check(5)) base.KillDaemon() base.EnsureDaemonActive() const ( maxRetry = 30 sleep = 3 * time.Second ) for i := 0; i < maxRetry; i++ { t.Logf("(retry %d) ps -a: %q", i, base.Cmd("ps", "-a").Run().Combined()) err := check(1) if err == nil { t.Logf("test is passing, after %d retries", i) return } time.Sleep(sleep) } base.DumpDaemonLogs(10) t.Fatalf("the container does not seem to be restarted") } func TestRunRestartWithOnFailure(t *testing.T) { base := testutil.NewBase(t) if !nerdtest.IsDocker() { testutil.RequireContainerdPlugin(base, "io.containerd.internal.v1", "restart", []string{"on-failure"}) } tID := testutil.Identifier(t) defer base.Cmd("rm", "-f", tID).Run() base.Cmd("run", "-d", "--restart=on-failure:2", "--name", tID, testutil.AlpineImage, "sh", "-c", "exit 1").AssertOK() check := func(log poll.LogT) poll.Result { inspect := base.InspectContainer(tID) if inspect.State != nil && inspect.State.Status == "exited" { return poll.Success() } return poll.Continue("container is not yet exited") } poll.WaitOn(t, check, poll.WithDelay(100*time.Microsecond), poll.WithTimeout(60*time.Second)) inspect := base.InspectContainer(tID) assert.Equal(t, inspect.RestartCount, 2) } func TestRunRestartWithUnlessStopped(t *testing.T) { base := testutil.NewBase(t) if !nerdtest.IsDocker() { testutil.RequireContainerdPlugin(base, "io.containerd.internal.v1", "restart", []string{"unless-stopped"}) } tID := testutil.Identifier(t) defer base.Cmd("rm", "-f", tID).Run() base.Cmd("run", "-d", "--restart=unless-stopped", "--name", tID, testutil.AlpineImage, "sh", "-c", "exit 1").AssertOK() check := func(log poll.LogT) poll.Result { inspect := base.InspectContainer(tID) if inspect.State != nil && inspect.State.Status == "exited" { return poll.Success() } if inspect.RestartCount == 2 { base.Cmd("stop", tID).AssertOK() } return poll.Continue("container is not yet exited") } poll.WaitOn(t, check, poll.WithDelay(100*time.Microsecond), poll.WithTimeout(60*time.Second)) inspect := base.InspectContainer(tID) assert.Equal(t, inspect.RestartCount, 2) } func TestUpdateRestartPolicy(t *testing.T) { base := testutil.NewBase(t) if !nerdtest.IsDocker() { testutil.RequireContainerdPlugin(base, "io.containerd.internal.v1", "restart", []string{"on-failure"}) } tID := testutil.Identifier(t) defer base.Cmd("rm", "-f", tID).Run() base.Cmd("run", "-d", "--restart=on-failure:1", "--name", tID, testutil.AlpineImage, "sh", "-c", "exit 1").AssertOK() base.Cmd("update", "--restart=on-failure:2", tID).AssertOK() check := func(log poll.LogT) poll.Result { inspect := base.InspectContainer(tID) if inspect.State != nil && inspect.State.Status == "exited" { return poll.Success() } return poll.Continue("container is not yet exited") } poll.WaitOn(t, check, poll.WithDelay(100*time.Microsecond), poll.WithTimeout(60*time.Second)) inspect := base.InspectContainer(tID) assert.Equal(t, inspect.RestartCount, 2) } // The test is to add a restart policy to a container which has not restart policy before, // and check it can work correctly. func TestAddRestartPolicy(t *testing.T) { base := testutil.NewBase(t) if !nerdtest.IsDocker() { testutil.RequireContainerdPlugin(base, "io.containerd.internal.v1", "restart", []string{"on-failure"}) } tID := testutil.Identifier(t) defer base.Cmd("rm", "-f", tID).Run() base.Cmd("run", "-d", "--name", tID, testutil.NginxAlpineImage).AssertOK() base.Cmd("update", "--restart=on-failure", tID).AssertOK() inspect := base.InspectContainer(tID) orgialPid := inspect.State.Pid exec.Command("kill", "-9", fmt.Sprintf("%v", orgialPid)).Run() check := func(log poll.LogT) poll.Result { inspect := base.InspectContainer(tID) if inspect.State != nil && inspect.State.Status == "running" && inspect.State.Pid != orgialPid { return poll.Success() } return poll.Continue("container is not yet running") } poll.WaitOn(t, check, poll.WithDelay(100*time.Microsecond), poll.WithTimeout(60*time.Second)) inspect = base.InspectContainer(tID) assert.Equal(t, inspect.RestartCount, 1) } ================================================ FILE: cmd/nerdctl/container/container_run_runtime_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRunSysctl(t *testing.T) { testCase := nerdtest.Setup() testCase.Command = test.Command("run", "--rm", "--sysctl", "net.ipv4.ip_forward=1", testutil.AlpineImage, "cat", "/proc/sys/net/ipv4/ip_forward") testCase.Expected = test.Expects(0, nil, expect.Equals("1\n")) testCase.Run(t) } func TestRunSysctl_DefaultUnprivilegedPortStart(t *testing.T) { testCase := nerdtest.Setup() // No --sysctl flags, default network mode (non-host). // We expect net.ipv4.ip_unprivileged_port_start=0 inside the container, // because withDefaultUnprivilegedPortSysctl should apply the default. testCase.Command = test.Command("run", "--rm", testutil.AlpineImage, "cat", "/proc/sys/net/ipv4/ip_unprivileged_port_start") testCase.Expected = test.Expects(0, nil, expect.Equals("0\n")) testCase.Run(t) } func TestRunSysctl_UnprivilegedPortStartOverride(t *testing.T) { testCase := nerdtest.Setup() // User explicitly sets net.ipv4.ip_unprivileged_port_start=1000. // We must NOT override this; the container should see "1000". testCase.Command = test.Command("run", "--rm", "--sysctl", "net.ipv4.ip_unprivileged_port_start=1000", testutil.AlpineImage, "cat", "/proc/sys/net/ipv4/ip_unprivileged_port_start") testCase.Expected = test.Expects(0, nil, expect.Equals("1000\n")) testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_run_security_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "fmt" "os" "os/exec" "strconv" "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/v2/pkg/apparmorutil" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" ) func getCapEff(base *testutil.Base, args ...string) uint64 { fullArgs := []string{"run", "--rm"} fullArgs = append(fullArgs, args...) fullArgs = append(fullArgs, testutil.AlpineImage, "sh", "-euc", "grep -w ^CapEff: /proc/self/status | sed -e \"s/^CapEff:[[:space:]]*//g\"", ) cmd := base.Cmd(fullArgs...) res := cmd.Run() assert.NilError(base.T, res.Error) s := strings.TrimSpace(res.Stdout()) ui64, err := strconv.ParseUint(s, 16, 64) assert.NilError(base.T, err) return ui64 } const ( CapNetRaw = 13 CapIPCLock = 14 ) func TestRunCap(t *testing.T) { t.Parallel() base := testutil.NewBase(t) // allCaps varies depending on the target version and the kernel version. allCaps := getCapEff(base, "--privileged") // https://github.com/containerd/containerd/blob/9a9bd097564b0973bfdb0b39bf8262aa1b7da6aa/oci/spec.go#L93 defaultCaps := uint64(0xa80425fb) t.Logf("allCaps=%016x", allCaps) type testCase struct { args []string capEff uint64 } testCases := []testCase{ { capEff: allCaps & defaultCaps, }, { args: []string{"--cap-add=all"}, capEff: allCaps, }, { args: []string{"--cap-add=ipc_lock"}, capEff: (allCaps & defaultCaps) | (1 << CapIPCLock), }, { args: []string{"--cap-add=all", "--cap-drop=net_raw"}, capEff: allCaps ^ (1 << CapNetRaw), }, { args: []string{"--cap-drop=all", "--cap-add=net_raw"}, capEff: 1 << CapNetRaw, }, { args: []string{"--cap-drop=all", "--cap-add=NET_RAW"}, capEff: 1 << CapNetRaw, }, { args: []string{"--cap-drop=all", "--cap-add=cap_net_raw"}, capEff: 1 << CapNetRaw, }, { args: []string{"--cap-drop=all", "--cap-add=CAP_NET_RAW"}, capEff: 1 << CapNetRaw, }, } for _, tc := range testCases { tc := tc // IMPORTANT name := "default" if len(tc.args) > 0 { name = strings.Join(tc.args, "_") } t.Run(name, func(t *testing.T) { t.Parallel() got := getCapEff(base, tc.args...) assert.Equal(t, tc.capEff, got) }) } } func TestRunSecurityOptSeccomp(t *testing.T) { t.Parallel() base := testutil.NewBase(t) type testCase struct { args []string seccomp int } testCases := []testCase{ { seccomp: 2, }, { args: []string{"--security-opt", "seccomp=unconfined"}, seccomp: 0, }, { args: []string{"--privileged"}, seccomp: 0, }, } for _, tc := range testCases { tc := tc // IMPORTANT name := "default" if len(tc.args) > 0 { name = strings.Join(tc.args, "_") } t.Run(name, func(t *testing.T) { t.Parallel() args := []string{"run", "--rm"} args = append(args, tc.args...) // NOTE: busybox grep does not support -oP \K args = append(args, testutil.AlpineImage, "grep", "-Eo", `^Seccomp:\s*([0-9]+)`, "/proc/1/status") cmd := base.Cmd(args...) f := func(expectedSeccomp int) func(string) error { return func(stdout string) error { s := strings.TrimPrefix(stdout, "Seccomp:") s = strings.TrimSpace(s) i, err := strconv.Atoi(s) if err != nil { return fmt.Errorf("failed to parse line %q: %w", stdout, err) } if i != expectedSeccomp { return fmt.Errorf("expected Seccomp to be %d, got %d", expectedSeccomp, i) } return nil } } cmd.AssertOutWithFunc(f(tc.seccomp)) }) } } func TestRunApparmor(t *testing.T) { base := testutil.NewBase(t) defaultProfile := fmt.Sprintf("%s-default", base.Target) if !apparmorutil.CanLoadNewProfile() && !apparmorutil.CanApplySpecificExistingProfile(defaultProfile) { t.Skipf("needs to be able to apply %q profile", defaultProfile) } attrCurrentPath := "/proc/self/attr/apparmor/current" if _, err := os.Stat(attrCurrentPath); err != nil { attrCurrentPath = "/proc/self/attr/current" } attrCurrentEnforceExpected := fmt.Sprintf("%s (enforce)\n", defaultProfile) base.Cmd("run", "--rm", testutil.AlpineImage, "cat", attrCurrentPath).AssertOutExactly(attrCurrentEnforceExpected) base.Cmd("run", "--rm", "--security-opt", "apparmor="+defaultProfile, testutil.AlpineImage, "cat", attrCurrentPath).AssertOutExactly(attrCurrentEnforceExpected) base.Cmd("run", "--rm", "--security-opt", "apparmor=unconfined", testutil.AlpineImage, "cat", attrCurrentPath).AssertOutContains("unconfined") base.Cmd("run", "--rm", "--privileged", testutil.AlpineImage, "cat", attrCurrentPath).AssertOutContains("unconfined") } // TestRunSeccompCapSysPtrace tests https://github.com/containerd/nerdctl/issues/976 func TestRunSeccompCapSysPtrace(t *testing.T) { base := testutil.NewBase(t) base.Cmd("run", "--rm", "--cap-add", "sys_ptrace", testutil.AlpineImage, "sh", "-euxc", "apk add -q strace && strace true").AssertOK() // Docker/Moby 's seccomp profile allows ptrace(2) by default, but containerd does not (yet): https://github.com/containerd/containerd/issues/6802 } func TestRunSystemPathsUnconfined(t *testing.T) { base := testutil.NewBase(t) const findmnt = "`apk add -q findmnt && findmnt -R /proc && findmnt -R /sys`" result := base.Cmd("run", "--rm", testutil.AlpineImage, "sh", "-euxc", findmnt).Run() defaultContainerOutput := result.Combined() var confined []string for _, path := range []string{ "/proc/kcore", "/proc/keys", "/proc/latency_stats", "/proc/sched_debug", "/proc/scsi", "/proc/timer_list", "/proc/timer_stats", "/sys/firmware", "/sys/fs/selinux", } { // Not each distribution will support every masked path here. if strings.Contains(defaultContainerOutput, path) { confined = append(confined, path) } } assert.Check(t, len(confined) != 0, "Default container has no confined paths to validate") result = base.Cmd("run", "--rm", "--security-opt", "systempaths=unconfined", testutil.AlpineImage, "sh", "-euxc", findmnt).Run() unconfinedContainerOutput := result.Combined() for _, path := range confined { assert.Assert(t, !strings.Contains(unconfinedContainerOutput, path), fmt.Sprintf("%s should not be masked when unconfined", path)) } for _, path := range []string{ "/proc/acpi", "/proc/bus", "/proc/fs", "/proc/irq", "/proc/sysrq-trigger", "/proc/sys", } { findmntPath := fmt.Sprintf("`apk add -q findmnt && findmnt %s`", path) result := base.Cmd("run", "--rm", testutil.AlpineImage, "sh", "-euxc", findmntPath).Run() // Not each distribution will support every read-only path here. if strings.Contains(result.Combined(), path) { result = base.Cmd("run", "--rm", "--security-opt", "systempaths=unconfined", testutil.AlpineImage, "sh", "-euxc", findmntPath).Run() assert.Assert(t, !strings.Contains(result.Combined(), "ro,"), fmt.Sprintf("%s should not be read-only when unconfined", path)) } } } func TestRunPrivileged(t *testing.T) { // docker does not support --privileged-without-host-devices testutil.DockerIncompatible(t) if rootlessutil.IsRootless() { t.Skip("test skipped for rootless privileged containers") } base := testutil.NewBase(t) devPath := "/dev/dummy-zero" // a dummy zero device: mknod /dev/dummy-zero c 1 5 helperCmd := exec.Command("mknod", []string{devPath, "c", "1", "5"}...) if out, err := helperCmd.CombinedOutput(); err != nil { err = fmt.Errorf("cannot create %q: %q: %w", devPath, string(out), err) t.Fatal(err) } // ensure the file will be removed in case of failed in the test defer func() { exec.Command("rm", devPath).Run() }() // get device with host devices base.Cmd("run", "--rm", "--privileged", testutil.AlpineImage, "ls", devPath).AssertOutExactly(devPath + "\n") // get device without host devices res := base.Cmd("run", "--rm", "--privileged", "--security-opt", "privileged-without-host-devices", testutil.AlpineImage, "ls", devPath).Run() // normally for not a exists file, the `ls` will return `1``. assert.Check(t, res.ExitCode != 0, res) // something like `ls: /dev/dummy-zero: No such file or directory` assert.Check(t, strings.Contains(res.Combined(), "No such file or directory")) } ================================================ FILE: cmd/nerdctl/container/container_run_soci_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "strconv" "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRunSoci(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.All( require.Not(nerdtest.Docker), require.Amd64, nerdtest.Soci, ) // Tests relying on the output of "mount" cannot be run in parallel obviously testCase.NoParallel = true testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Custom("mount").Run(&test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { data.Labels().Set("beforeCount", strconv.Itoa(strings.Count(stdout, "fuse.rawBridge"))) }, }) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", testutil.FfmpegSociImage) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("--snapshotter=soci", "run", "--rm", testutil.FfmpegSociImage) } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var afterCount int beforeCount, _ := strconv.Atoi(data.Labels().Get("beforeCount")) helpers.Custom("mount").Run(&test.Expected{ Output: func(stdout string, t tig.T) { afterCount = strings.Count(stdout, "fuse.rawBridge") }, }) assert.Equal(t, 11, afterCount-beforeCount, "expected the number of fuse.rawBridge") }, } } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_run_stargz_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "testing" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRunStargz(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.All( nerdtest.Stargz, require.Amd64, require.Not(nerdtest.Docker), ) testCase.Command = test.Command("--snapshotter=stargz", "run", "--quiet", "--rm", testutil.FedoraESGZImage, "ls", "/.stargz-snapshotter") testCase.Expected = test.Expects(0, nil, nil) testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_run_systemd_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "errors" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRunWithSystemdAlways(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.Not(nerdtest.Docker) testCase.Setup = func(data test.Data, helpers test.Helpers) { data.Labels().Set("containerName", testutil.Identifier(t)) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { containerName := data.Labels().Get("containerName") helpers.Anyhow("container", "rm", "-f", containerName) } testCase.SubTests = []*test.Case{ { Description: "should mount cgroup filesystem as rw", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { containerName := data.Labels().Get("containerName") return helpers.Command("run", "--name", containerName, "--systemd=always", "--entrypoint=/bin/bash", testutil.UbuntuImage, "-c", "mount | grep cgroup") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("(rw,")), }, { Description: "should expose SIGTERM+3 stop signal label", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { containerName := data.Labels().Get("containerName") return helpers.Command("inspect", "--format", "{{json .Config.Labels}}", containerName) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("SIGRTMIN+3")), }, } testCase.Run(t) } func TestRunWithSystemdTrueEnabled(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.All( require.Amd64, require.Not(nerdtest.Docker), ) testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--systemd=true", "--entrypoint=/sbin/init", testutil.SystemdImage) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("container", "rm", "-f", data.Identifier()) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { // should expose SIGTERM+3 stop signal labels helpers.Command("inspect", "--format", "{{json .Config.Labels}}", data.Identifier()). Run(&test.Expected{ ExitCode: expect.ExitCodeSuccess, Output: expect.Contains("SIGRTMIN+3"), }) // waits for systemd to become ready and lists systemd jobs return helpers.Command("exec", data.Identifier(), "sh", "-c", "--", `tries=0 until systemctl is-system-running >/dev/null 2>&1; do >&2 printf "Waiting for systemd to come up...\n" sleep 1s tries=$(( tries + 1)) [ $tries -lt 10 ] || { >&2 printf "systemd failed to come up in a reasonable amount of time\n" exit 1 } done systemctl list-jobs`) } testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("jobs")) testCase.Run(t) } func TestRunWithSystemdTrueDisabled(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.All( require.Amd64, require.Not(nerdtest.Docker), ) testCase.Setup = func(data test.Data, helpers test.Helpers) { data.Labels().Set("containerName", testutil.Identifier(t)) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { containerName := data.Labels().Get("containerName") helpers.Anyhow("container", "rm", "-f", containerName) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { containerName := data.Labels().Get("containerName") return helpers.Command("run", "--name", containerName, "--systemd=true", "--entrypoint=/bin/bash", testutil.SystemdImage, "-c", "systemctl list-jobs") } testCase.Expected = test.Expects(1, []error{errors.New("System has not been booted with systemd as init system")}, nil) testCase.Run(t) } func TestRunWithSystemdFalse(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.Not(nerdtest.Docker) testCase.Setup = func(data test.Data, helpers test.Helpers) { data.Labels().Set("containerName", testutil.Identifier(t)) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { containerName := data.Labels().Get("containerName") helpers.Anyhow("container", "rm", "-f", containerName) } testCase.SubTests = []*test.Case{ { Description: "should mount cgroup filesystem as ro", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { containerName := data.Labels().Get("containerName") return helpers.Command("run", "--name", containerName, "--systemd=false", "--entrypoint=/bin/bash", testutil.UbuntuImage, "-c", "mount | grep cgroup") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("(ro,")), }, { Description: "should expose SIGTERM stop signal label", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { containerName := data.Labels().Get("containerName") return helpers.Command("inspect", "--format", "{{json .Config.Labels}}", containerName) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("SIGTERM")), }, } testCase.Run(t) } func TestRunWithNoSystemd(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.Not(nerdtest.Docker) testCase.Setup = func(data test.Data, helpers test.Helpers) { data.Labels().Set("containerName", testutil.Identifier(t)) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { containerName := data.Labels().Get("containerName") helpers.Anyhow("container", "rm", "-f", containerName) } testCase.SubTests = []*test.Case{ { Description: "should mount cgroup filesystem as ro", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { containerName := data.Labels().Get("containerName") return helpers.Command("run", "--name", containerName, "--entrypoint=/bin/bash", testutil.UbuntuImage, "-c", "mount | grep cgroup") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("(ro,")), }, { Description: "should expose SIGTERM stop signal label", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { containerName := data.Labels().Get("containerName") return helpers.Command("inspect", "--format", "{{json .Config.Labels}}", containerName) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("SIGTERM")), }, } testCase.Run(t) } func TestRunWithSystemdPrivilegedError(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.All( require.Amd64, require.Not(nerdtest.Docker), ) testCase.Command = test.Command("run", "--privileged", "--rm", "--systemd=always", "--entrypoint=/sbin/init", testutil.SystemdImage) testCase.Expected = test.Expects(1, []error{errors.New("if --privileged is used with systemd `--security-opt privileged-without-host-devices` must also be used")}, nil) testCase.Run(t) } func TestRunWithSystemdPrivilegedSuccess(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.All( require.Amd64, require.Not(nerdtest.Docker), ) testCase.Setup = func(data test.Data, helpers test.Helpers) { containerName := testutil.Identifier(t) data.Labels().Set("containerName", containerName) helpers.Ensure("run", "-d", "--name", containerName, "--privileged", "--security-opt", "privileged-without-host-devices", "--systemd=true", "--entrypoint=/sbin/init", testutil.SystemdImage) nerdtest.EnsureContainerStarted(helpers, containerName) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { containerName := data.Labels().Get("containerName") helpers.Anyhow("container", "rm", "-f", containerName) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { containerName := data.Labels().Get("containerName") return helpers.Command("inspect", "--format", "{{json .Config.Labels}}", containerName) } testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("SIGRTMIN+3")) testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_run_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "bufio" "bytes" "errors" "fmt" "os" "os/exec" "path/filepath" "regexp" "runtime" "strings" "testing" "time" "gotest.tools/v3/assert" "gotest.tools/v3/icmd" "gotest.tools/v3/poll" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRunEntrypointWithBuild(t *testing.T) { nerdtest.Setup() dockerfile := fmt.Sprintf(`FROM %s ENTRYPOINT ["echo", "foo"] CMD ["echo", "bar"] `, testutil.CommonImage) testCase := &test.Case{ Require: nerdtest.Build, Setup: func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerfile, "Dockerfile") data.Labels().Set("image", data.Identifier()) helpers.Ensure("build", "-t", data.Labels().Get("image"), data.Temp().Path()) }, SubTests: []*test.Case{ { Description: "Run image", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Labels().Get("image")) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("foo echo bar\n")), }, { Description: "Run image empty entrypoint", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--entrypoint", "", data.Labels().Get("image")) }, Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, { Description: "Run image time entrypoint", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--entrypoint", "time", data.Labels().Get("image")) }, Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, { Description: "Run image empty entrypoint custom command", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--entrypoint", "", data.Labels().Get("image"), "echo", "blah") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( expect.Contains("blah"), expect.DoesNotContain("foo", "bar"), )), }, { Description: "Run image time entrypoint custom command", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--entrypoint", "time", data.Labels().Get("image"), "echo", "blah") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( expect.Contains("blah"), expect.DoesNotContain("foo", "bar"), )), }, }, } testCase.Run(t) } func TestRunWorkdir(t *testing.T) { testCase := nerdtest.Setup() dir := "/foo" if runtime.GOOS == "windows" { dir = "c:" + dir } testCase.Command = test.Command("run", "--rm", "--workdir="+dir, testutil.CommonImage, "pwd") testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(dir)) testCase.Run(t) } func TestRunWithDoubleDash(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.Not(nerdtest.Docker) testCase.Command = test.Command("run", "--rm", testutil.CommonImage, "--", "sh", "-euxc", "exit 0") testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, nil) testCase.Run(t) } func TestRunExitCode(t *testing.T) { testCase := nerdtest.Setup() testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("exit0")) helpers.Anyhow("rm", "-f", data.Identifier("exit123")) } testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "--name", data.Identifier("exit0"), testutil.CommonImage, "sh", "-euxc", "exit 0") helpers.Command("run", "--name", data.Identifier("exit123"), testutil.CommonImage, "sh", "-euxc", "exit 123"). Run(&test.Expected{ExitCode: 123}) } testCase.Command = test.Command("ps", "-a") testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, Errors: nil, Output: expect.All( expect.Match(regexp.MustCompile("Exited [(]123[)][A-Za-z0-9 ]+"+data.Identifier("exit123"))), expect.Match(regexp.MustCompile("Exited [(]0[)][A-Za-z0-9 ]+"+data.Identifier("exit0"))), func(stdout string, t tig.T) { assert.Equal(t, nerdtest.InspectContainer(helpers, data.Identifier("exit0")).State.Status, "exited") assert.Equal(t, nerdtest.InspectContainer(helpers, data.Identifier("exit123")).State.Status, "exited") }, ), } } testCase.Run(t) } func TestRunCIDFile(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "--rm", "--cidfile", data.Temp().Path("cid-file"), testutil.CommonImage) data.Temp().Exists("cid-file") } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--cidfile", data.Temp().Path("cid-file"), testutil.CommonImage) } // Docker will return 125 while nerdctl returns 1, so, generic fail instead of specific exit code testCase.Expected = test.Expects(expect.ExitCodeGenericFail, []error{errors.New("container ID file found")}, nil) testCase.Run(t) } func TestRunEnvFile(t *testing.T) { testCase := nerdtest.Setup() testCase.Env = map[string]string{ "HOST_ENV": "ENV-IN-HOST", } testCase.Setup = func(data test.Data, helpers test.Helpers) { data.Temp().Save("# this is a comment line\nTESTKEY1=TESTVAL1", "env1-file") data.Temp().Save("# this is a comment line\nTESTKEY2=TESTVAL2\nHOST_ENV", "env2-file") } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command( "run", "--rm", "--env-file", data.Temp().Path("env1-file"), "--env-file", data.Temp().Path("env2-file"), testutil.CommonImage, "env") } testCase.Expected = test.Expects( expect.ExitCodeSuccess, nil, expect.Contains("TESTKEY1=TESTVAL1", "TESTKEY2=TESTVAL2", "HOST_ENV=ENV-IN-HOST"), ) testCase.Run(t) } func TestRunEnv(t *testing.T) { testCase := nerdtest.Setup() testCase.Env = map[string]string{ "CORGE": "corge-value-in-host", "GARPLY": "garply-value-in-host", } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--env", "FOO=foo1,foo2", "--env", "BAR=bar1 bar2", "--env", "BAZ=", "--env", "QUX", // not exported in OS "--env", "QUUX=quux1", "--env", "QUUX=quux2", "--env", "CORGE", // OS exported "--env", "GRAULT=grault_key=grault_value", // value contains `=` char "--env", "GARPLY=", // OS exported "--env", "WALDO=", // not exported in OS testutil.CommonImage, "env") } validate := []test.Comparator{ expect.Contains( "\nFOO=foo1,foo2\n", "\nBAR=bar1 bar2\n", "\nQUUX=quux2\n", "\nCORGE=corge-value-in-host\n", "\nGRAULT=grault_key=grault_value\n", ), expect.DoesNotContain("QUX"), } if runtime.GOOS != "windows" { validate = append( validate, expect.Contains( "\nBAZ=\n", "\nGARPLY=\n", "\nWALDO=\n", ), ) } testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.All(validate...)) testCase.Run(t) } func TestRunHostnameEnv(t *testing.T) { testCase := nerdtest.Setup() testCase.SubTests = []*test.Case{ { Description: "default hostname", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("run", "--rm", "--quiet", testutil.CommonImage) // Note: on Windows, just straight passing the command will not work (some cmd escaping weirdness?) cmd.Feed(strings.NewReader(`[[ "HOSTNAME=$(hostname)" == "$(env | grep HOSTNAME)" ]]`)) return cmd }, Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, { Description: "with --hostname", // Windows does not support --hostname Require: require.Not(require.Windows), Command: test.Command("run", "--rm", "--quiet", "--hostname", "foobar", testutil.CommonImage, "env"), Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("HOSTNAME=foobar")), }, } testCase.Run(t) } func TestRunStdin(t *testing.T) { testCase := nerdtest.Setup() const testStr = "test-run-stdin" testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("run", "--rm", "-i", testutil.CommonImage, "cat") cmd.Feed(strings.NewReader(testStr)) return cmd } testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Equals(testStr)) testCase.Run(t) } func TestRunWithJsonFileLogDriver(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("json-file log driver is not yet implemented on Windows") } base := testutil.NewBase(t) containerName := testutil.Identifier(t) defer base.Cmd("rm", "-f", containerName).AssertOK() base.Cmd("run", "-d", "--log-driver", "json-file", "--log-opt", "max-size=5K", "--log-opt", "max-file=2", "--name", containerName, testutil.CommonImage, "sh", "-euxc", "hexdump -C /dev/urandom | head -n1000").AssertOK() time.Sleep(3 * time.Second) inspectedContainer := base.InspectContainer(containerName) logJSONPath := filepath.Dir(inspectedContainer.LogPath) // matches = current log file + old log files to retain matches, err := filepath.Glob(filepath.Join(logJSONPath, inspectedContainer.ID+"*")) assert.NilError(t, err) if len(matches) != 2 { t.Fatalf("the number of log files is not equal to 2 files, got: %s", matches) } for _, file := range matches { fInfo, err := os.Stat(file) assert.NilError(t, err) // The log file size is compared to 5200 bytes (instead 5k) to keep docker compatibility. // Docker log rotation lacks precision because the size check is done at the log entry level // and not at the byte level (io.Writer), so docker log files can exceed 5k if fInfo.Size() > 5200 { t.Fatal("file size exceeded 5k") } } } func TestRunWithJsonFileLogDriverAndLogPathOpt(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("json-file log driver is not yet implemented on Windows") } testutil.DockerIncompatible(t) base := testutil.NewBase(t) containerName := testutil.Identifier(t) defer base.Cmd("rm", "-f", containerName).AssertOK() customLogJSONPath := filepath.Join(t.TempDir(), containerName, containerName+"-json.log") base.Cmd("run", "-d", "--log-driver", "json-file", "--log-opt", fmt.Sprintf("log-path=%s", customLogJSONPath), "--log-opt", "max-size=5K", "--log-opt", "max-file=2", "--name", containerName, testutil.CommonImage, "sh", "-euxc", "hexdump -C /dev/urandom | head -n1000").AssertOK() time.Sleep(3 * time.Second) rawBytes, err := os.ReadFile(customLogJSONPath) assert.NilError(t, err) if len(rawBytes) == 0 { t.Fatalf("logs are not written correctly to log-path: %s", customLogJSONPath) } // matches = current log file + old log files to retain matches, err := filepath.Glob(filepath.Join(filepath.Dir(customLogJSONPath), containerName+"*")) assert.NilError(t, err) if len(matches) != 2 { t.Fatalf("the number of log files is not equal to 2 files, got: %s", matches) } for _, file := range matches { fInfo, err := os.Stat(file) assert.NilError(t, err) if fInfo.Size() > 5200 { t.Fatal("file size exceeded 5k") } } } func TestRunWithJournaldLogDriver(t *testing.T) { testutil.RequireExecutable(t, "journalctl") journalctl, _ := exec.LookPath("journalctl") res := icmd.RunCmd(icmd.Command(journalctl, "-xe")) if res.ExitCode != 0 { t.Skipf("current user is not allowed to access journal logs: %s", res.Combined()) } if runtime.GOOS == "windows" { t.Skip("journald log driver is not yet implemented on Windows") } base := testutil.NewBase(t) containerName := testutil.Identifier(t) defer base.Cmd("rm", "-f", containerName).AssertOK() base.Cmd("run", "-d", "--log-driver", "journald", "--name", containerName, testutil.CommonImage, "sh", "-euxc", "echo foo; echo bar").AssertOK() time.Sleep(3 * time.Second) inspectedContainer := base.InspectContainer(containerName) type testCase struct { name string filter string } testCases := []testCase{ { name: "filter journald logs using SYSLOG_IDENTIFIER field", filter: fmt.Sprintf("SYSLOG_IDENTIFIER=%s", inspectedContainer.ID[:12]), }, { name: "filter journald logs using CONTAINER_NAME field", filter: fmt.Sprintf("CONTAINER_NAME=%s", containerName), }, { name: "filter journald logs using IMAGE_NAME field", filter: fmt.Sprintf("IMAGE_NAME=%s", testutil.CommonImage), }, } for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { found := 0 check := func(log poll.LogT) poll.Result { res := icmd.RunCmd(icmd.Command(journalctl, "--no-pager", "--since", "2 minutes ago", tc.filter)) assert.Equal(t, 0, res.ExitCode, res) if strings.Contains(res.Stdout(), "bar") && strings.Contains(res.Stdout(), "foo") { found = 1 return poll.Success() } return poll.Continue("reading from journald is not yet finished") } poll.WaitOn(t, check, poll.WithDelay(100*time.Microsecond), poll.WithTimeout(20*time.Second)) assert.Equal(t, 1, found) }) } } func TestRunWithJournaldLogDriverAndLogOpt(t *testing.T) { testutil.RequireExecutable(t, "journalctl") journalctl, _ := exec.LookPath("journalctl") res := icmd.RunCmd(icmd.Command(journalctl, "-xe")) if res.ExitCode != 0 { t.Skipf("current user is not allowed to access journal logs: %s", res.Combined()) } if runtime.GOOS == "windows" { t.Skip("journald log driver is not yet implemented on Windows") } base := testutil.NewBase(t) containerName := testutil.Identifier(t) defer base.Cmd("rm", "-f", containerName).AssertOK() base.Cmd("run", "-d", "--log-driver", "journald", "--log-opt", "tag={{.FullID}}", "--name", containerName, testutil.CommonImage, "sh", "-euxc", "echo foo; echo bar").AssertOK() time.Sleep(3 * time.Second) inspectedContainer := base.InspectContainer(containerName) found := 0 check := func(log poll.LogT) poll.Result { res := icmd.RunCmd(icmd.Command(journalctl, "--no-pager", "--since", "2 minutes ago", fmt.Sprintf("SYSLOG_IDENTIFIER=%s", inspectedContainer.ID))) assert.Equal(t, 0, res.ExitCode, res) if strings.Contains(res.Stdout(), "bar") && strings.Contains(res.Stdout(), "foo") { found = 1 return poll.Success() } return poll.Continue("reading from journald is not yet finished") } poll.WaitOn(t, check, poll.WithDelay(100*time.Microsecond), poll.WithTimeout(20*time.Second)) assert.Equal(t, 1, found) } func TestRunWithLogBinary(t *testing.T) { testutil.RequiresBuild(t) if runtime.GOOS == "windows" { t.Skip("buildkit is not enabled on windows, this feature may work on windows.") } testutil.DockerIncompatible(t) t.Parallel() base := testutil.NewBase(t) imageName := testutil.Identifier(t) + "-image" containerName := testutil.Identifier(t) var dockerfile = ` FROM ` + testutil.GolangImage + ` as builder WORKDIR /go/src/ RUN mkdir -p logger WORKDIR /go/src/logger RUN echo '\ package main \n\ \n\ import ( \n\ "bufio" \n\ "context" \n\ "fmt" \n\ "io" \n\ "os" \n\ "path/filepath" \n\ "sync" \n\ \n\ "github.com/containerd/containerd/v2/core/runtime/v2/logging"\n\ )\n\ func main() {\n\ logging.Run(log)\n\ }\n\ func log(ctx context.Context, config *logging.Config, ready func() error) error {\n\ var wg sync.WaitGroup \n\ wg.Add(2) \n\ // forward both stdout and stderr to temp files \n\ go copy(&wg, config.Stdout, config.ID, "stdout") \n\ go copy(&wg, config.Stderr, config.ID, "stderr") \n\ // signal that we are ready and setup for the container to be started \n\ if err := ready(); err != nil { \n\ return err \n\ } \n\ wg.Wait() \n\ return nil \n\ }\n\ \n\ func copy(wg *sync.WaitGroup, r io.Reader, id string, kind string) { \n\ f, _ := os.Create(filepath.Join(os.TempDir(), fmt.Sprintf("%s_%s.log", id, kind))) \n\ defer f.Close() \n\ defer wg.Done() \n\ s := bufio.NewScanner(r) \n\ for s.Scan() { \n\ f.WriteString(s.Text()) \n\ } \n\ }\n' >> main.go RUN go mod init # Workaround for "package slices is not in GOROOT" https://github.com/containerd/nerdctl/issues/4214 RUN go get github.com/containerd/containerd/v2@v2.0.5 RUN go mod tidy RUN go build . FROM scratch COPY --from=builder /go/src/logger/logger / ` buildCtx := helpers.CreateBuildContext(t, dockerfile) tmpDir := t.TempDir() base.Cmd("build", buildCtx, "--output", fmt.Sprintf("type=local,src=/go/src/logger/logger,dest=%s", tmpDir)).AssertOK() defer base.Cmd("image", "rm", "-f", imageName).AssertOK() base.Cmd("container", "rm", "-f", containerName).AssertOK() base.Cmd("run", "-d", "--log-driver", fmt.Sprintf("binary://%s/logger", tmpDir), "--name", containerName, testutil.CommonImage, "sh", "-euxc", "echo foo; echo bar").AssertOK() defer base.Cmd("container", "rm", "-f", containerName).AssertOK() inspectedContainer := base.InspectContainer(containerName) bytes, err := os.ReadFile(filepath.Join(os.TempDir(), fmt.Sprintf("%s_%s.log", inspectedContainer.ID, "stdout"))) assert.NilError(t, err) log := string(bytes) assert.Check(t, strings.Contains(log, "foo")) assert.Check(t, strings.Contains(log, "bar")) } // history: There was a bug that the --add-host items disappear when the another container created. // This test ensures that it doesn't happen. // (https://github.com/containerd/nerdctl/issues/2560) func TestRunAddHostRemainsWhenAnotherContainerCreated(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("ocihook is not yet supported on Windows") } base := testutil.NewBase(t) containerName := testutil.Identifier(t) hostMapping := "test-add-host:10.0.0.1" base.Cmd("run", "-d", "--add-host", hostMapping, "--name", containerName, testutil.CommonImage, "sleep", nerdtest.Infinity).AssertOK() defer base.Cmd("container", "rm", "-f", containerName).Run() checkEtcHosts := func(stdout string) error { matcher, err := regexp.Compile(`^10.0.0.1\s+test-add-host$`) if err != nil { return err } var found bool sc := bufio.NewScanner(bytes.NewBufferString(stdout)) for sc.Scan() { if matcher.Match(sc.Bytes()) { found = true } } if !found { return fmt.Errorf("host not found") } return nil } base.Cmd("exec", containerName, "cat", "/etc/hosts").AssertOutWithFunc(checkEtcHosts) // run another container base.Cmd("run", "--rm", testutil.CommonImage).AssertOK() base.Cmd("exec", containerName, "cat", "/etc/hosts").AssertOutWithFunc(checkEtcHosts) } // https://github.com/containerd/nerdctl/issues/2726 func TestRunRmTime(t *testing.T) { base := testutil.NewBase(t) base.Cmd("pull", "--quiet", testutil.CommonImage) t0 := time.Now() base.Cmd("run", "--rm", testutil.CommonImage, "true").AssertOK() t1 := time.Now() took := t1.Sub(t0) var deadline = 3 * time.Second // FIXME: Investigate? it appears that since the move to containerd 2 on Windows, this is taking longer. if runtime.GOOS == "windows" { deadline = 10 * time.Second } if took > deadline { t.Fatalf("expected to have completed in %v, took %v", deadline, took) } } func runAttachStdin(t *testing.T, testStr string, args []string) string { if runtime.GOOS == "windows" { t.Skip("run attach test is not yet implemented on Windows") } t.Parallel() base := testutil.NewBase(t) containerName := testutil.Identifier(t) opts := []func(*testutil.Cmd){ testutil.WithStdin(strings.NewReader("echo " + testStr + "\nexit\n")), } fullArgs := []string{"run", "--rm", "-i"} fullArgs = append(fullArgs, args...) fullArgs = append(fullArgs, "--name", containerName, testutil.CommonImage, ) defer base.Cmd("rm", "-f", containerName).AssertOK() result := base.Cmd(fullArgs...).CmdOption(opts...).Run() return result.Combined() } func runAttach(t *testing.T, testStr string, args []string) string { if runtime.GOOS == "windows" { t.Skip("run attach test is not yet implemented on Windows") } t.Parallel() base := testutil.NewBase(t) containerName := testutil.Identifier(t) fullArgs := []string{"run"} fullArgs = append(fullArgs, args...) fullArgs = append(fullArgs, "--name", containerName, testutil.CommonImage, "sh", "-euxc", "echo "+testStr, ) defer base.Cmd("rm", "-f", containerName).AssertOK() result := base.Cmd(fullArgs...).Run() return result.Combined() } func TestRunAttachFlag(t *testing.T) { type testCase struct { name string args []string testFunc func(t *testing.T, testStr string, args []string) string testStr string expectedOut string dockerOut string } testCases := []testCase{ { name: "AttachFlagStdin", args: []string{"-a", "STDIN", "-a", "STDOUT"}, testFunc: runAttachStdin, testStr: "test-run-stdio", expectedOut: "test-run-stdio", dockerOut: "test-run-stdio", }, { name: "AttachFlagStdOut", args: []string{"-a", "STDOUT"}, testFunc: runAttach, testStr: "foo", expectedOut: "foo", dockerOut: "foo", }, { name: "AttachFlagMixedValue", args: []string{"-a", "STDIN", "-a", "invalid-value"}, testFunc: runAttach, testStr: "foo", expectedOut: "invalid stream specified with -a flag. Valid streams are STDIN, STDOUT, and STDERR", dockerOut: "valid streams are STDIN, STDOUT and STDERR", }, { name: "AttachFlagInvalidValue", args: []string{"-a", "invalid-stream"}, testFunc: runAttach, testStr: "foo", expectedOut: "invalid stream specified with -a flag. Valid streams are STDIN, STDOUT, and STDERR", dockerOut: "valid streams are STDIN, STDOUT and STDERR", }, { name: "AttachFlagCaseInsensitive", args: []string{"-a", "stdin", "-a", "stdout"}, testFunc: runAttachStdin, testStr: "test-run-stdio", expectedOut: "test-run-stdio", dockerOut: "test-run-stdio", }, } for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { actualOut := tc.testFunc(t, tc.testStr, tc.args) errorMsg := fmt.Sprintf("%s failed;\nExpected: '%s'\nActual: '%s'", tc.name, tc.expectedOut, actualOut) if nerdtest.IsDocker() { assert.Equal(t, true, strings.Contains(actualOut, tc.dockerOut), errorMsg) } else { assert.Equal(t, true, strings.Contains(actualOut, tc.expectedOut), errorMsg) } }) } } func TestRunQuiet(t *testing.T) { base := testutil.NewBase(t) teardown := func() { base.Cmd("rmi", "-f", testutil.CommonImage).Run() } defer teardown() teardown() sentinel := "test run quiet" result := base.Cmd("run", "--rm", "--quiet", testutil.CommonImage, fmt.Sprintf(`echo "%s"`, sentinel)).Run() assert.Assert(t, strings.Contains(result.Combined(), sentinel)) wasQuiet := func(output, sentinel string) bool { return !strings.Contains(output, sentinel) } // Docker and nerdctl image pulls are not 1:1. if nerdtest.IsDocker() { sentinel = "Pull complete" } else { sentinel = "resolved" } assert.Assert(t, wasQuiet(result.Combined(), sentinel), "Found %s in container run output", sentinel) } func TestRunFromOCIArchive(t *testing.T) { testutil.RequiresBuild(t) testutil.RegisterBuildCacheCleanup(t) // Docker does not support running container images from OCI archive. testutil.DockerIncompatible(t) base := testutil.NewBase(t) imageName := testutil.Identifier(t) teardown := func() { base.Cmd("rmi", "-f", imageName).Run() } defer teardown() teardown() const sentinel = "test-nerdctl-run-from-oci-archive" dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "%s"]`, testutil.CommonImage, sentinel) buildCtx := helpers.CreateBuildContext(t, dockerfile) tag := fmt.Sprintf("%s:latest", imageName) tarPath := fmt.Sprintf("%s/%s.tar", buildCtx, imageName) base.Cmd("build", "--tag", tag, fmt.Sprintf("--output=type=oci,dest=%s", tarPath), buildCtx).AssertOK() base.Cmd("run", "--rm", fmt.Sprintf("oci-archive://%s", tarPath)).AssertOutContainsAll(tag, sentinel) } func TestRunDomainname(t *testing.T) { t.Parallel() if runtime.GOOS == "windows" { t.Skip("run --hostname not implemented on Windows yet") } testCases := []struct { name string hostname string domainname string Cmd string CmdFlag string expectedOut string }{ { name: "Check domain name", hostname: "foobar", domainname: "example.com", Cmd: "hostname", CmdFlag: "-d", expectedOut: "example.com", }, { name: "check fqdn", hostname: "foobar", domainname: "example.com", Cmd: "hostname", CmdFlag: "-f", expectedOut: "foobar.example.com", }, } for _, tc := range testCases { tc := tc // capture range variable t.Run(tc.name, func(t *testing.T) { t.Parallel() base := testutil.NewBase(t) base.Cmd("run", "--rm", "--hostname", tc.hostname, "--domainname", tc.domainname, testutil.CommonImage, tc.Cmd, tc.CmdFlag, ).AssertOutContains(tc.expectedOut) }) } } func TestRunHealthcheckFlags(t *testing.T) { if rootlessutil.IsRootless() { t.Skip("healthcheck tests are skipped in rootless environment") } testCase := nerdtest.Setup() testCases := []struct { name string args []string shouldFail bool expectTest []string expectRetries int expectInterval time.Duration expectTimeout time.Duration expectStartPeriod time.Duration }{ { name: "Valid_full_config", args: []string{ "--health-cmd", "curl -f http://localhost || exit 1", "--health-interval", "30s", "--health-timeout", "5s", "--health-retries", "3", "--health-start-period", "2s", }, expectTest: []string{"CMD-SHELL", "curl -f http://localhost || exit 1"}, expectInterval: 30 * time.Second, expectTimeout: 5 * time.Second, expectRetries: 3, expectStartPeriod: 2 * time.Second, }, { name: "No_healthcheck", args: []string{ "--no-healthcheck", }, expectTest: []string{"NONE"}, }, { name: "No_healthcheck_flag", args: []string{}, expectTest: nil, }, { name: "Conflicting_flags", args: []string{ "--no-healthcheck", "--health-cmd", "true", }, shouldFail: true, }, { name: "Negative_retries", args: []string{ "--health-cmd", "true", "--health-retries", "-2", }, shouldFail: true, }, { name: "Negative_timeout", args: []string{ "--health-cmd", "true", "--health-timeout", "-5s", }, shouldFail: true, }, { name: "Invalid_timeout_format", args: []string{ "--health-cmd", "true", "--health-timeout", "5blah", }, shouldFail: true, }, { name: "Health_cmd_cmd_shell", args: []string{ "--health-cmd", "curl -f http://localhost || exit 1", }, expectTest: []string{"CMD-SHELL", "curl -f http://localhost || exit 1"}, }, { name: "Health_cmd_array_like", args: []string{ "--health-cmd", "echo hello", }, expectTest: []string{"CMD-SHELL", "echo hello"}, }, { name: "Health_cmd_empty", args: []string{ "--health-cmd", "", "--health-retries", "2", }, expectTest: nil, expectRetries: 2, }, } for _, tc := range testCases { tc := tc testCase.SubTests = append(testCase.SubTests, &test.Case{ Description: tc.name, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { args := append([]string{"run", "-d", "--name", tc.name}, tc.args...) args = append(args, testutil.CommonImage, "sleep", "infinity") return helpers.Command(args...) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { if tc.shouldFail { return &test.Expected{ ExitCode: expect.ExitCodeGenericFail, } } return &test.Expected{ ExitCode: expect.ExitCodeSuccess, Output: expect.All( func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, tc.name) hc := inspect.Config.Healthcheck if tc.expectTest == nil { assert.Assert(t, hc == nil || len(hc.Test) == 0) } else { assert.Assert(t, hc != nil) assert.DeepEqual(t, hc.Test, tc.expectTest) } if tc.expectRetries > 0 { assert.Equal(t, hc.Retries, tc.expectRetries) } if tc.expectTimeout > 0 { assert.Equal(t, hc.Timeout, tc.expectTimeout) } if tc.expectInterval > 0 { assert.Equal(t, hc.Interval, tc.expectInterval) } if tc.expectStartPeriod > 0 { assert.Equal(t, hc.StartPeriod, tc.expectStartPeriod) } }, ), } }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", tc.name) }, }) } testCase.Run(t) } func TestRunHealthcheckFromImage(t *testing.T) { if rootlessutil.IsRootless() { t.Skip("healthcheck tests are skipped in rootless environment") } nerdtest.Setup() dockerfile := fmt.Sprintf(`FROM %s HEALTHCHECK --interval=30s --timeout=10s CMD wget -q --spider http://localhost:8080 || exit 1 `, testutil.CommonImage) testCase := &test.Case{ Require: nerdtest.Build, Setup: func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerfile, "Dockerfile") data.Labels().Set("image", data.Identifier()) helpers.Ensure("build", "-t", data.Labels().Get("image"), data.Temp().Path()) }, SubTests: []*test.Case{ { Description: "merge_with_image", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "-d", "--name", data.Identifier(), "--health-retries=5", "--health-interval=45s", data.Labels().Get("image")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) hc := inspect.Config.Healthcheck assert.Assert(t, hc != nil, "expected healthcheck config to be present") assert.DeepEqual(t, hc.Test, []string{"CMD-SHELL", "wget -q --spider http://localhost:8080 || exit 1"}) assert.Equal(t, 5, hc.Retries) // From CLI flags assert.Equal(t, 45*time.Second, hc.Interval) // From CLI flags assert.Equal(t, 10*time.Second, hc.Timeout) // From Dockerfile }), } }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, }, { Description: "Disable image health checks via runtime flag", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command( "run", "-d", "--name", data.Identifier(), "--no-healthcheck", data.Labels().Get("image"), ) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) hc := inspect.Config.Healthcheck assert.Assert(t, hc != nil, "expected healthcheck config to be present") assert.DeepEqual(t, hc.Test, []string{"NONE"}) }), } }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, }, }, } testCase.Run(t) } func countFIFOFiles(root string) (int, error) { count := 0 err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.Mode()&os.ModeNamedPipe != 0 { count++ } return nil }) return count, err } func TestCleanupFIFOs(t *testing.T) { if rootlessutil.IsRootless() { t.Skip("/run/containerd/fifo/ doesn't exist on rootless") } if runtime.GOOS == "windows" { t.Skip("test is not compatible with windows") } testutil.DockerIncompatible(t) testCase := nerdtest.Setup() testCase.NoParallel = true testCase.Setup = func(data test.Data, helpers test.Helpers) { cmd := helpers.Command("run", "-it", "--rm", testutil.CommonImage, "date") cmd.WithPseudoTTY() cmd.Run(&test.Expected{ ExitCode: 0, }) oldNumFifos, err := countFIFOFiles("/run/containerd/fifo/") assert.NilError(t, err) cmd = helpers.Command("run", "-it", "--rm", testutil.CommonImage, "date") cmd.WithPseudoTTY() cmd.Run(&test.Expected{ ExitCode: 0, }) newNumFifos, err := countFIFOFiles("/run/containerd/fifo/") assert.NilError(t, err) assert.Equal(t, oldNumFifos, newNumFifos) } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_run_user_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "fmt" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRunUserGID(t *testing.T) { testCase := nerdtest.Setup() testCase.SubTests = []*test.Case{ { Description: "Test container run as default user (root) and verify root belongs to standard system groups", Command: test.Command("run", "--rm", testutil.AlpineImage, "id", "-nG"), Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("root bin daemon sys adm disk wheel floppy dialout tape video")), }, { Description: "Test container run with numeric UID (1000) and verify it resolves to root group inside the container", Command: test.Command("run", "--rm", "--user", "1000", testutil.AlpineImage, "id", "-nG"), Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("root")), }, { Description: "Test container run as user (guest) and verify group membership is resolved correctly", Command: test.Command("run", "--rm", "--user", "guest", testutil.AlpineImage, "id", "-nG"), Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("users")), }, { Description: "Test container run with well-known user 'nobody' and verify it belongs to the 'nobody' group", Command: test.Command("run", "--rm", "--user", "nobody", testutil.AlpineImage, "id", "-nG"), Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("nobody")), }, } testCase.Run(t) } func TestRunUmask(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.Not(nerdtest.Docker) testCase.Command = test.Command("run", "--rm", "--umask", "0200", testutil.AlpineImage, "sh", "-c", "umask") testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("0200")) testCase.Run(t) } func TestRunAddGroup(t *testing.T) { testCase := nerdtest.Setup() testCase.SubTests = []*test.Case{ { Description: "Test container run as default root user and its inherited system groups", Command: test.Command("run", "--rm", testutil.AlpineImage, "id", "-nG"), Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("root bin daemon sys adm disk wheel floppy dialout tape video\n")), }, { Description: "Test container run as numeric UID only and its fallback to root group", Command: test.Command("run", "--rm", "--user", "1000", testutil.AlpineImage, "id", "-nG"), Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("root\n")), }, { Description: "Test container run as numeric UID with extra group addition", Command: test.Command("run", "--rm", "--user", "1000", "--group-add", "nogroup", testutil.AlpineImage, "id", "-nG"), Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("root nogroup\n")), }, { Description: "Test container run as UID:GID pair with extra group addition", Command: test.Command("run", "--rm", "--user", "1000:wheel", "--group-add", "nogroup", testutil.AlpineImage, "id", "-nG"), Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("wheel nogroup\n")), }, { Description: "Test container run as root with extra group addition and system group persistence", Command: test.Command("run", "--rm", "--user", "root", "--group-add", "nogroup", testutil.AlpineImage, "id", "-nG"), Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("root bin daemon sys adm disk wheel floppy dialout tape video nogroup\n")), }, { Description: "Test container run as root:group override and its effect on supplementary groups", Command: test.Command("run", "--rm", "--user", "root:nogroup", "--group-add", "nogroup", testutil.AlpineImage, "id", "-nG"), Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("nogroup\n")), }, { Description: "Test container run as named non-root user with multiple group additions", Command: test.Command("run", "--rm", "--user", "guest", "--group-add", "root", "--group-add", "nogroup", testutil.AlpineImage, "id", "-nG"), Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("users root nogroup\n")), }, { Description: "Test container run as named user:group with numeric GID resolution", Command: test.Command("run", "--rm", "--user", "guest:nogroup", "--group-add", "0", testutil.AlpineImage, "id", "-nG"), Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("nogroup root\n")), }, } testCase.Run(t) } // TestRunAddGroup_CVE_2023_25173 tests https://github.com/advisories/GHSA-hmfx-3pcx-653p // // Equates to https://github.com/containerd/containerd/commit/286a01f350a2298b4fdd7e2a0b31c04db3937ea8 func TestRunAddGroup_CVE_2023_25173(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.BusyboxImage) } testCase.SubTests = []*test.Case{ { Description: "Test container run as default root user", Command: test.Command("run", "--rm", testutil.BusyboxImage, "id"), Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("groups=0(root),10(wheel)\n")), }, { Description: "Test container run as root with additional groups", Command: test.Command("run", "--rm", "--group-add", "1", "--group-add", "1234", testutil.BusyboxImage, "id"), Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("groups=0(root),1(daemon),10(wheel),1234\n")), }, { Description: "Test container run as custom UID with inherited root group", Command: test.Command("run", "--rm", "--user", "1234", testutil.BusyboxImage, "id"), Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("groups=0(root)\n")), }, { Description: "Test container run as custom UID and GID pair", Command: test.Command("run", "--rm", "--user", "1234:1234", testutil.BusyboxImage, "id"), Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("groups=1234\n")), }, { Description: "Test container run as custom UID with explicit group add", Command: test.Command("run", "--rm", "--user", "1234", "--group-add", "1234", testutil.BusyboxImage, "id"), Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("groups=0(root),1234\n")), }, { Description: "Test container run as named non-root user (daemon)", Command: test.Command("run", "--rm", "--user", "daemon", testutil.BusyboxImage, "id"), Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("groups=1(daemon)\n")), }, { Description: "Test container run as named user with extra groups", Command: test.Command("run", "--rm", "--user", "daemon", "--group-add", "1234", testutil.BusyboxImage, "id"), Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("groups=1(daemon),1234\n")), }, } testCase.Run(t) } func TestUsernsMappingRunCmd(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.All( nerdtest.AllowModifyUserns, nerdtest.RemapIDs, require.Not(nerdtest.Docker), ), Setup: func(data test.Data, helpers test.Helpers) { data.Labels().Set("validUserns", "nerdctltestuser") data.Labels().Set("expectedHostUID", "123456789") data.Labels().Set("validUid", "123456789") data.Labels().Set("net-container", "net-container") data.Labels().Set("invalidUserns", "invaliduser") }, SubTests: []*test.Case{ { Description: "Test container run with valid Userns format userns username", NoParallel: true, // Changes system config so running in non parallel mode Setup: func(data test.Data, helpers test.Helpers) { err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers) assert.NilError(t, err, "Failed to append Userns config") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--tty", "-d", "--userns-remap", data.Labels().Get("validUserns"), "--name", data.Identifier(), testutil.CommonImage, "sleep", "inf") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) if err != nil { t.Log(fmt.Sprintf("Failed to get container host UID: %v", err)) t.FailNow() } assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID")) }, } }, }, { Description: "Test container run with valid Userns --userns uid", NoParallel: true, // Changes system config so running in non parallel mode Setup: func(data test.Data, helpers test.Helpers) { err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers) assert.NilError(t, err, "Failed to append Userns config") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--tty", "-d", "--userns-remap", data.Labels().Get("validUid"), "--name", data.Identifier(), testutil.CommonImage, "sleep", "inf") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) if err != nil { t.Log(fmt.Sprintf("Failed to get container host UID: %v", err)) t.FailNow() } assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID")) }, } }, }, { Description: "Test container run failure with valid Userns and privileged flag", NoParallel: true, // Changes system config so running in non parallel mode Setup: func(data test.Data, helpers test.Helpers) { err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers) assert.NilError(t, err, "Failed to append Userns config") }, Cleanup: func(data test.Data, helpers test.Helpers) { removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--tty", "--privileged", "--userns-remap", data.Labels().Get("validUserns"), "--name", data.Identifier(), testutil.CommonImage, "sleep", "inf") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, } }, }, { Description: "Test container run with valid Userns format --userns :", NoParallel: true, // Changes system config so running in non parallel mode Setup: func(data test.Data, helpers test.Helpers) { err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers) assert.NilError(t, err, "Failed to append Userns config") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--tty", "-d", "--userns-remap", fmt.Sprintf("%s:%s", data.Labels().Get("validUserns"), data.Labels().Get("validUserns")), "--name", data.Identifier(), testutil.CommonImage, "sleep", "inf") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) if err != nil { t.Log(fmt.Sprintf("Failed to get container host UID: %v", err)) t.FailNow() } assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID")) }, } }, }, { Description: "Test container run with valid Userns --userns uid:gid", NoParallel: true, // Changes system config so running in non parallel mode Setup: func(data test.Data, helpers test.Helpers) { err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers) assert.NilError(t, err, "Failed to append Userns config") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--tty", "-d", "--userns-remap", fmt.Sprintf("%s:%s", data.Labels().Get("validUid"), data.Labels().Get("validUid")), "--name", data.Identifier(), testutil.CommonImage, "sleep", "inf") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) if err != nil { t.Log(fmt.Sprintf("Failed to get container host UID: %v", err)) t.FailNow() } assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID")) }, } }, }, { Description: "Test container network share with valid Userns", NoParallel: true, // Changes system config so running in non parallel mode Setup: func(data test.Data, helpers test.Helpers) { err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers) assert.NilError(t, err, "Failed to append Userns config") helpers.Ensure("run", "--tty", "-d", "--userns-remap", data.Labels().Get("validUserns"), "--name", data.Labels().Get("net-container"), testutil.CommonImage, "sleep", "inf") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("rm", "-f", data.Labels().Get("net-container")) removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--tty", "-d", "--userns-remap", data.Labels().Get("validUserns"), "--net", fmt.Sprintf("container:%s", data.Labels().Get("net-container")), "--name", data.Identifier(), testutil.CommonImage, "sleep", "inf") }, Expected: test.Expects(0, nil, nil), }, { Description: "Test container run with valid Userns with override --userns=host", NoParallel: true, // Changes system config so running in non parallel mode Setup: func(data test.Data, helpers test.Helpers) { err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers) assert.NilError(t, err, "Failed to append Userns config") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--tty", "-d", "--userns-remap", data.Labels().Get("validUserns"), "--userns", "host", "--name", data.Identifier(), testutil.CommonImage, "sleep", "inf") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) if err != nil { t.Log(fmt.Sprintf("Failed to get container host UID: %v", err)) t.FailNow() } assert.Assert(t, actualHostUID == "0") }, } }, }, { Description: "Test container run with valid Userns with invalid overrid --userns=hostinvalid", NoParallel: true, // Changes system config so running in non parallel mode Setup: func(data test.Data, helpers test.Helpers) { err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers) assert.NilError(t, err, "Failed to append Userns config") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--tty", "-d", "--userns-remap", data.Labels().Get("validUserns"), "--userns", "hostinvalid", "--name", data.Identifier(), testutil.CommonImage, "sleep", "inf") }, Expected: test.Expects(1, nil, nil), }, { Description: "Test container run with invalid Userns", Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--tty", "-d", "--userns-remap", data.Labels().Get("invalidUserns"), "--name", data.Identifier(), testutil.CommonImage, "sleep", "inf") }, Expected: test.Expects(1, nil, nil), }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_run_user_windows_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRunUserName(t *testing.T) { testCase := nerdtest.Setup() testCase.SubTests = []*test.Case{ { Description: "should run Windows container as ContainerAdministrator by default", Command: test.Command("run", "--rm", testutil.WindowsNano, "whoami"), Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("ContainerAdministrator")), }, { Description: "should run Windows container as ContainerAdministrator when user is set to ContainerAdministrator", Command: test.Command("run", "--rm", "--user", "ContainerAdministrator", testutil.WindowsNano, "whoami"), Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("ContainerAdministrator")), }, { Description: "should run Windows container as ContainerUser when user is set to ContainerUser", Command: test.Command("run", "--rm", "--user", "ContainerUser", testutil.WindowsNano, "whoami"), Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("ContainerUser")), }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_run_verify_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "fmt" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" ) func TestRunVerifyCosign(t *testing.T) { dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-test-string"] `, testutil.CommonImage) testCase := nerdtest.Setup() var reg *registry.Server testCase.Require = require.All( require.Binary("cosign"), require.Not(nerdtest.Docker), nerdtest.Build, nerdtest.Registry, ) testCase.Env["COSIGN_PASSWORD"] = "1" testCase.Setup = func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerfile, "Dockerfile") pri, pub := nerdtest.GenerateCosignKeyPair(data, helpers, "1") reg = nerdtest.RegistryWithNoAuth(data, helpers, 0, false) reg.Setup(data, helpers) testImageRef := fmt.Sprintf("127.0.0.1:%d/%s", reg.Port, data.Identifier("push-cosign-image")) helpers.Ensure("build", "-t", testImageRef, data.Temp().Path()) helpers.Ensure("push", testImageRef, "--sign=cosign", "--cosign-key="+pri) data.Labels().Set("public_key", pub) data.Labels().Set("image_ref", testImageRef) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if reg != nil { reg.Cleanup(data, helpers) } } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { helpers.Fail( "run", "--rm", "--verify=cosign", "--cosign-key=dummy", data.Labels().Get("image_ref")) return helpers.Command( "run", "--rm", "--verify=cosign", "--cosign-key="+data.Labels().Get("public_key"), data.Labels().Get("image_ref")) } testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, nil) testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_run_windows_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "bytes" "os/exec" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRunHostProcessContainer(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.Not(nerdtest.Docker) testCase.Setup = func(data test.Data, helpers test.Helpers) { hostname, err := exec.Command("hostname").Output() if err != nil { t.Fatalf("unable to get hostname: %s", err) } data.Labels().Set("hostname", string(bytes.TrimSpace(hostname))) whoami := helpers.Capture("run", "--rm", "--isolation=host", testutil.WindowsNano, "whoami") t.Logf("whoami %s", whoami) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--isolation=host", testutil.WindowsNano, "hostname") } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(data.Labels().Get("hostname")))(data, helpers) } testCase.Run(t) } func TestRunHostProcessContainerAsUser(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.Not(nerdtest.Docker) testCase.Command = test.Command("run", "--rm", "--isolation=host", "-u", "NT AUTHORITY\\SYSTEM", testutil.WindowsNano, "whoami") testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("nt authority\\system")) testCase.Run(t) } func TestRunHostProcessContainerAsLocalService(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.Not(nerdtest.Docker) testCase.Command = test.Command("run", "--rm", "--isolation=host", "-u", "NT AUTHORITY\\Local Service", testutil.WindowsNano, "whoami") testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("nt authority\\local service")) testCase.Run(t) } func TestRunHostProcessContainerAsNetworkService(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.Not(nerdtest.Docker) testCase.Command = test.Command("run", "--rm", "--isolation=host", "-u", "NT AUTHORITY\\Network Service", testutil.WindowsNano, "whoami") testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("nt authority\\network service")) testCase.Run(t) } func TestRunProcessIsolated(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.Not(nerdtest.Docker) testCase.Setup = func(data test.Data, helpers test.Helpers) { data.Labels().Set("containeruser", "ContainerUser") } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--isolation=process", "-u", data.Labels().Get("containeruser"), testutil.WindowsNano, "whoami") } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(data.Labels().Get("containeruser")))(data, helpers) } testCase.Run(t) } func TestRunHyperVContainer(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.All( require.Not(nerdtest.Docker), nerdtest.HyperV, ) testCase.Setup = func(data test.Data, helpers test.Helpers) { // hyperv must not be in the name for this test, the output is parsed for it containerName := "nerdctl-testwcowcontainer" data.Labels().Set("containerName", containerName) helpers.Ensure("run", "--isolation", "hyperv", "--name", containerName, testutil.WindowsNano) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Labels().Get("containerName")) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("container", "inspect", "--mode", "native", data.Labels().Get("containerName")) } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("hyperv"))(data, helpers) } testCase.Run(t) } func TestRunProcessContainer(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.Not(nerdtest.Docker) testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "--isolation", "process", "--name", data.Identifier(), testutil.WindowsNano) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("container", "inspect", "--mode", "native", data.Identifier()) } testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.DoesNotContain("hyperv")) testCase.Run(t) } // Note that the current implementation of this test is not ideal, since it relies on internal HCS details that // Microsoft could decide to change in the future (breaking both this unit test and the one in containerd itself): // https://github.com/containerd/containerd/pull/6618#discussion_r823302852 func TestRunProcessContainerWithDevice(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.Not(nerdtest.Docker) testCase.Command = test.Command( "run", "--rm", "--isolation=process", "--device", "class://5B45201D-F2F2-4F3B-85BB-30FF1F953599", testutil.WindowsNano, "cmd", "/S", "/C", "dir C:\\Windows\\System32\\HostDriverStore", ) testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("FileRepository")) testCase.Run(t) } func TestRunWithTtyAndDetached(t *testing.T) { testCase := nerdtest.Setup() // This test is currently disabled, as it is failing most of the time. testCase.Require = nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/3437") testCase.Setup = func(data test.Data, helpers test.Helpers) { // with -t, success, the container should run with tty support. helpers.Ensure("run", "-d", "-t", "--name", data.Identifier("with-terminal"), testutil.CommonImage, "cmd", "/c", "echo", "Hello, World with TTY!") } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("container", "rm", "-f", data.Identifier("with-terminal")) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { withTtyContainer := nerdtest.InspectContainer(helpers, data.Identifier("with-terminal")) assert.Equal(helpers.T(), 0, withTtyContainer.State.ExitCode) return helpers.Command("logs", data.Identifier("with-terminal")) } testCase.Expected = test.Expects(0, nil, expect.Contains("Hello, World with TTY!")) testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_start.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "github.com/spf13/cobra" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/container" "github.com/containerd/nerdctl/v2/pkg/consoleutil" ) func StartCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "start [flags] CONTAINER [CONTAINER, ...]", Args: cobra.MinimumNArgs(1), Short: "Start one or more running containers", RunE: startAction, ValidArgsFunction: startShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().SetInterspersed(false) cmd.Flags().BoolP("attach", "a", false, "Attach STDOUT/STDERR and forward signals") cmd.Flags().String("detach-keys", consoleutil.DefaultDetachKeys, "Override the default detach keys") cmd.Flags().BoolP("interactive", "i", false, "Attach container's STDIN") cmd.Flags().String("checkpoint", "", "checkpoint name") cmd.Flags().String("checkpoint-dir", "", "checkpoint directory") return cmd } func startOptions(cmd *cobra.Command) (types.ContainerStartOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ContainerStartOptions{}, err } attach, err := cmd.Flags().GetBool("attach") if err != nil { return types.ContainerStartOptions{}, err } detachKeys, err := cmd.Flags().GetString("detach-keys") if err != nil { return types.ContainerStartOptions{}, err } interactive, err := cmd.Flags().GetBool("interactive") if err != nil { return types.ContainerStartOptions{}, err } checkpoint, err := cmd.Flags().GetString("checkpoint") if err != nil { return types.ContainerStartOptions{}, err } checkpointDir, err := cmd.Flags().GetString("checkpoint-dir") if err != nil { return types.ContainerStartOptions{}, err } return types.ContainerStartOptions{ Stdout: cmd.OutOrStdout(), GOptions: globalOptions, Attach: attach, DetachKeys: detachKeys, Interactive: interactive, Checkpoint: checkpoint, CheckpointDir: checkpointDir, }, nil } func startAction(cmd *cobra.Command, args []string) error { options, err := startOptions(cmd) if err != nil { return err } options.NerdctlCmd, options.NerdctlArgs = helpers.GlobalFlags(cmd) client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return container.Start(ctx, client, args, options) } func startShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show non-running container names statusFilterFn := func(st containerd.ProcessStatus) bool { return st != containerd.Running && st != containerd.Unknown } return completion.ContainerNames(cmd, statusFilterFn) } ================================================ FILE: cmd/nerdctl/container/container_start_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "bytes" "errors" "io" "strconv" "strings" "testing" "time" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestStartDetachKeys(t *testing.T) { testCase := nerdtest.Setup() testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Setup = func(data test.Data, helpers test.Helpers) { cmd := helpers.Command("run", "-it", "--name", data.Identifier(), testutil.CommonImage) cmd.WithPseudoTTY() cmd.Feed(strings.NewReader("exit\n")) cmd.Run(&test.Expected{ ExitCode: 0, }) assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":false"), ) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("start", "-ai", "--detach-keys=ctrl-a,ctrl-b", data.Identifier()) cmd.WithPseudoTTY() cmd.WithFeeder(func() io.Reader { // ctrl+a and ctrl+b (see https://en.wikipedia.org/wiki/C0_and_C1_control_codes) return bytes.NewReader([]byte{1, 2}) }) return cmd } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Errors: []error{errors.New("detach keys")}, Output: expect.All( func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true")) }, ), } } testCase.Run(t) } func TestStartWithCheckpoint(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.All( require.Not(nerdtest.Rootless), // Docker version 28.x has a known regression that breaks Checkpoint/Restore functionality. // The issue is tracked in the moby/moby project as https://github.com/moby/moby/issues/50750. require.Not(nerdtest.Docker), ) testCase.Setup = func(data test.Data, helpers test.Helpers) { // Use an in-memory tmpfs to model in-memory state without introducing extra processes // Single PID 1 shell: continuously increment a counter and write to /state/counter (tmpfs) helpers.Ensure("run", "-d", "--name", data.Identifier(), "--tmpfs", "/state", testutil.CommonImage, "sh", "-c", `i=0; while true; do i=$((i+1)); printf "%d\n" "$i" >/state/counter; sleep 0.2; done`) // Give some time for the counter to increase before checkpoint to validate continuity after restore time.Sleep(1 * time.Second) helpers.Ensure("checkpoint", "create", data.Identifier(), data.Identifier()+"-checkpoint") } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("start", "--checkpoint", data.Identifier()+"-checkpoint", data.Identifier()) } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.All( func(_ string, t tig.T) { // Validate in-memory state continuity via tmpfs: counter should not reset and must keep increasing // Short delay to allow the container to resume; if the counter had reset to 0, it could not reach >5 this fast time.Sleep(200 * time.Millisecond) c1Str := strings.TrimSpace(helpers.Capture("exec", data.Identifier(), "cat", "/state/counter")) var parseErrs []error c1, err1 := strconv.Atoi(c1Str) if err1 != nil { parseErrs = append(parseErrs, err1) } assert.Assert(t, len(parseErrs) == 0, "failed to parse counter values: %v", parseErrs) assert.Assert(t, c1 > 5, "tmpfs in-memory counter seems reset or too small: %d", c1) }, ), } } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_start_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestStart(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("start", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return test.Expects(0, nil, expect.Contains(data.Identifier()))(data, helpers) }, } testCase.Run(t) } func TestStartAttach(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.Not(require.Windows), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "--name", data.Identifier(), testutil.CommonImage, "sh", "-euxc", "echo foo") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("start", "-a", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return test.Expects(0, nil, expect.Contains("foo"))(data, helpers) }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_stats.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "github.com/spf13/cobra" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/container" ) func StatsCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "stats", Short: "Display a live stream of container(s) resource usage statistics.", RunE: statsAction, ValidArgsFunction: statsShellComplete, SilenceUsage: true, SilenceErrors: true, } addStatsFlags(cmd) return cmd } func addStatsFlags(cmd *cobra.Command) { cmd.Flags().BoolP("all", "a", false, "Show all containers (default shows just running)") cmd.Flags().String("format", "", "Pretty-print images using a Go template, e.g, '{{json .}}'") cmd.Flags().Bool("no-stream", false, "Disable streaming stats and only pull the first result") cmd.Flags().Bool("no-trunc", false, "Do not truncate output") } func processStatsCommandFlags(cmd *cobra.Command) (types.ContainerStatsOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ContainerStatsOptions{}, err } all, err := cmd.Flags().GetBool("all") if err != nil { return types.ContainerStatsOptions{}, err } noStream, err := cmd.Flags().GetBool("no-stream") if err != nil { return types.ContainerStatsOptions{}, err } format, err := cmd.Flags().GetString("format") if err != nil { return types.ContainerStatsOptions{}, err } noTrunc, err := cmd.Flags().GetBool("no-trunc") if err != nil { return types.ContainerStatsOptions{}, err } return types.ContainerStatsOptions{ Stdout: cmd.OutOrStdout(), Stderr: cmd.ErrOrStderr(), GOptions: globalOptions, All: all, Format: format, NoStream: noStream, NoTrunc: noTrunc, }, nil } func statsAction(cmd *cobra.Command, args []string) error { options, err := processStatsCommandFlags(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return container.Stats(ctx, client, args, options) } func statsShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show running container names statusFilterFn := func(st containerd.ProcessStatus) bool { return st == containerd.Running } return completion.ContainerNames(cmd, statusFilterFn) } ================================================ FILE: cmd/nerdctl/container/container_stats_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "runtime" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestStats(t *testing.T) { testCase := nerdtest.Setup() // FIXME: does not seem to work on windows testCase.Require = require.Not(require.Windows) if runtime.GOOS == "linux" { // this comment is for `nerdctl ps` but it also valid for `nerdctl stats` : // https://github.com/containerd/nerdctl/pull/223#issuecomment-851395178 testCase.Require = require.All( testCase.Require, nerdtest.CgroupsAccessible, ) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("container")) helpers.Anyhow("rm", "-f", data.Identifier("memlimited")) helpers.Anyhow("rm", "-f", data.Identifier("exited")) } testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier("container"), testutil.CommonImage, "sleep", nerdtest.Infinity) helpers.Ensure("run", "-d", "--name", data.Identifier("memlimited"), "--memory", "1g", testutil.CommonImage, "sleep", nerdtest.Infinity) helpers.Ensure("run", "--name", data.Identifier("exited"), testutil.CommonImage, "echo", "'exited'") data.Labels().Set("id", data.Identifier("container")) } testCase.SubTests = []*test.Case{ { Description: "stats", Command: test.Command("stats", "--no-stream", "--no-trunc"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.Contains(data.Labels().Get("id")), } }, }, { Description: "container stats", Command: test.Command("container", "stats", "--no-stream", "--no-trunc"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.Contains(data.Labels().Get("id")), } }, }, { Description: "stats ID", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("stats", "--no-stream", data.Labels().Get("id")) }, Expected: test.Expects(0, nil, nil), }, { Description: "container stats ID", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("container", "stats", "--no-stream", data.Labels().Get("id")) }, Expected: test.Expects(0, nil, nil), }, { Description: "no mem limit set", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("stats", "--no-stream") }, // https://github.com/containerd/nerdctl/issues/1240 // nerdctl used to print UINT64_MAX as the memory limit, so, ensure it does no more Expected: test.Expects(0, nil, expect.DoesNotContain("16EiB")), }, { Description: "mem limit set", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("stats", "--no-stream") }, Expected: test.Expects(0, nil, expect.Contains("1GiB")), }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_stop.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "time" "github.com/spf13/cobra" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/container" ) func StopCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "stop [flags] CONTAINER [CONTAINER, ...]", Args: cobra.MinimumNArgs(1), Short: "Stop one or more running containers", RunE: stopAction, ValidArgsFunction: stopShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().IntP("time", "t", 10, "Seconds to wait before sending a SIGKILL") cmd.Flags().StringP("signal", "s", "SIGTERM", "Signal to send to the container") return cmd } func stopOptions(cmd *cobra.Command) (types.ContainerStopOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ContainerStopOptions{}, err } var timeout *time.Duration if cmd.Flags().Changed("time") { timeValue, err := cmd.Flags().GetInt("time") if err != nil { return types.ContainerStopOptions{}, err } t := time.Duration(timeValue) * time.Second timeout = &t } var signal string if cmd.Flags().Changed("signal") { signalValue, err := cmd.Flags().GetString("signal") if err != nil { return types.ContainerStopOptions{}, err } signal = signalValue } return types.ContainerStopOptions{ Stdout: cmd.OutOrStdout(), Stderr: cmd.ErrOrStderr(), GOptions: globalOptions, Timeout: timeout, Signal: signal, }, nil } func stopAction(cmd *cobra.Command, args []string) error { options, err := stopOptions(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return container.Stop(ctx, client, args, options) } func stopShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show non-stopped container names statusFilterFn := func(st containerd.ProcessStatus) bool { return st != containerd.Stopped && st != containerd.Created && st != containerd.Unknown } return completion.ContainerNames(cmd, statusFilterFn) } ================================================ FILE: cmd/nerdctl/container/container_stop_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "fmt" "io" "strings" "testing" "time" "github.com/coreos/go-iptables/iptables" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" iptablesutil "github.com/containerd/nerdctl/v2/pkg/testutil/iptables" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" ) func TestStopStart(t *testing.T) { const ( hostPort = 8080 ) testContainerName := testutil.Identifier(t) base := testutil.NewBase(t) defer base.Cmd("rm", "-f", testContainerName).Run() base.Cmd("run", "-d", "--restart=no", "--name", testContainerName, "-p", fmt.Sprintf("127.0.0.1:%d:80", hostPort), testutil.NginxAlpineImage).AssertOK() check := func(httpGetRetry int) error { resp, err := nettestutil.HTTPGet(fmt.Sprintf("http://127.0.0.1:%d", hostPort), httpGetRetry, false) if err != nil { return err } respBody, err := io.ReadAll(resp.Body) if err != nil { return err } if !strings.Contains(string(respBody), testutil.NginxAlpineIndexHTMLSnippet) { return fmt.Errorf("expected contain %q, got %q", testutil.NginxAlpineIndexHTMLSnippet, string(respBody)) } return nil } assert.NilError(t, check(5)) base.Cmd("stop", testContainerName).AssertOK() base.Cmd("exec", testContainerName, "ps").AssertFail() if check(1) == nil { t.Fatal("expected to get an error") } base.Cmd("start", testContainerName).AssertOK() assert.NilError(t, check(5)) } func TestStopWithStopSignal(t *testing.T) { testCase := nerdtest.Setup() testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := nerdtest.RunSigProxyContainer(nerdtest.SigQuit, false, []string{"--stop-signal", nerdtest.SigQuit.String()}, data, helpers) helpers.Ensure("stop", data.Identifier()) return cmd } // Verify that SIGQUIT was sent to the container AND that the container did forcefully exit testCase.Expected = test.Expects(137, nil, expect.Contains(nerdtest.SignalCaught)) testCase.Run(t) } func TestStopCleanupForwards(t *testing.T) { const ( hostPort = 9999 testContainerName = "ngx" ) base := testutil.NewBase(t) defer func() { base.Cmd("rm", "-f", testContainerName).Run() }() // skip if rootless if rootlessutil.IsRootless() { t.Skip("pkg/testutil/iptables does not support rootless") } ipt, err := iptables.New() assert.NilError(t, err) containerID := base.Cmd("run", "-d", "--restart=no", "--name", testContainerName, "-p", fmt.Sprintf("127.0.0.1:%d:80", hostPort), testutil.NginxAlpineImage).Run().Stdout() containerID = strings.TrimSuffix(containerID, "\n") containerIP := base.Cmd("inspect", "-f", "'{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}'", testContainerName).Run().Stdout() containerIP = strings.ReplaceAll(containerIP, "'", "") containerIP = strings.TrimSuffix(containerIP, "\n") // define iptables chain name depending on the target (docker/nerdctl) var chain string if nerdtest.IsDocker() { chain = "DOCKER" } else { redirectChain := "CNI-HOSTPORT-DNAT" chain = iptablesutil.GetRedirectedChain(t, ipt, redirectChain, testutil.Namespace, containerID) } assert.Equal(t, iptablesutil.ForwardExists(t, ipt, chain, containerIP, hostPort), true) base.Cmd("stop", testContainerName).AssertOK() assert.Equal(t, iptablesutil.ForwardExists(t, ipt, chain, containerIP, hostPort), false) } // Regression test for https://github.com/containerd/nerdctl/issues/3353 func TestStopCreated(t *testing.T) { t.Parallel() base := testutil.NewBase(t) tID := testutil.Identifier(t) tearDown := func() { base.Cmd("rm", "-f", tID).Run() } setup := func() { base.Cmd("create", "--name", tID, testutil.CommonImage).AssertOK() } t.Cleanup(tearDown) tearDown() setup() base.Cmd("stop", tID).AssertOK() } func TestStopWithLongTimeoutAndSIGKILL(t *testing.T) { t.Parallel() base := testutil.NewBase(t) testContainerName := testutil.Identifier(t) defer base.Cmd("rm", "-f", testContainerName).Run() // Start a container that sleeps forever base.Cmd("run", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", "Inf").AssertOK() // Stop the container with a 5-second timeout and SIGKILL start := time.Now() base.Cmd("stop", "--time=5", "--signal", "SIGKILL", testContainerName).AssertOK() elapsed := time.Since(start) // The container should be stopped almost immediately, well before the 5-second timeout assert.Assert(t, elapsed < 5*time.Second, "Container wasn't stopped immediately with SIGKILL") } func TestStopWithTimeout(t *testing.T) { t.Parallel() base := testutil.NewBase(t) testContainerName := testutil.Identifier(t) defer base.Cmd("rm", "-f", testContainerName).Run() // Start a container that sleeps forever base.Cmd("run", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", "Inf").AssertOK() // Stop the container with a 3-second timeout start := time.Now() base.Cmd("stop", "--time=3", testContainerName).AssertOK() elapsed := time.Since(start) // The container should get the SIGKILL before the 10s default timeout assert.Assert(t, elapsed < 10*time.Second, "Container did not respect --timeout flag") } func TestStopCleanupFIFOs(t *testing.T) { if rootlessutil.IsRootless() { t.Skip("/run/containerd/fifo/ doesn't exist on rootless") } testutil.DockerIncompatible(t) base := testutil.NewBase(t) testContainerName := testutil.Identifier(t) oldNumFifos, err := countFIFOFiles("/run/containerd/fifo/") assert.NilError(t, err) // Stop the container after 2 seconds go func() { time.Sleep(2 * time.Second) base.Cmd("stop", testContainerName).AssertOK() newNumFifos, err := countFIFOFiles("/run/containerd/fifo/") assert.NilError(t, err) assert.Equal(t, oldNumFifos, newNumFifos) }() // Start a container that is automatically removed after it exits base.Cmd("run", "--rm", "--name", testContainerName, testutil.NginxAlpineImage).AssertOK() } ================================================ FILE: cmd/nerdctl/container/container_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "testing" "github.com/containerd/nerdctl/v2/pkg/testutil" ) func TestMain(m *testing.M) { testutil.M(m) } ================================================ FILE: cmd/nerdctl/container/container_top.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "errors" "fmt" "strings" "github.com/spf13/cobra" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/container" "github.com/containerd/nerdctl/v2/pkg/infoutil" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" ) func TopCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "top CONTAINER [ps OPTIONS]", Args: cobra.MinimumNArgs(1), Short: "Display the running processes of a container", RunE: topAction, ValidArgsFunction: topShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().SetInterspersed(false) return cmd } func topAction(cmd *cobra.Command, args []string) error { // NOTE: rootless container does not rely on cgroupv1. // more details about possible ways to resolve this concern: #223 globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } if rootlessutil.IsRootless() && infoutil.CgroupsVersion() == "1" { return fmt.Errorf("top requires cgroup v2 for rootless containers, see https://rootlesscontaine.rs/getting-started/common/cgroup2/") } if globalOptions.CgroupManager == "none" { return errors.New("cgroup manager must not be \"none\"") } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() containerID := args[0] var psArgs string if len(args) > 1 { // Join all remaining arguments as ps args psArgs = strings.Join(args[1:], " ") } return container.Top(ctx, client, []string{containerID}, types.ContainerTopOptions{ Stdout: cmd.OutOrStdout(), GOptions: globalOptions, PsArgs: psArgs, }) } func topShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show running container names statusFilterFn := func(st containerd.ProcessStatus) bool { return st == containerd.Running } return completion.ContainerNames(cmd, statusFilterFn) } ================================================ FILE: cmd/nerdctl/container/container_top_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "runtime" "testing" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestTop(t *testing.T) { testCase := nerdtest.Setup() //more details https://github.com/containerd/nerdctl/pull/223#issuecomment-851395178 if runtime.GOOS == "linux" { testCase.Require = nerdtest.CgroupsAccessible } testCase.Setup = func(data test.Data, helpers test.Helpers) { // FIXME: busybox 1.36 on windows still appears to not support sleep inf. Unclear why. helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) data.Labels().Set("cID", data.Identifier()) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.SubTests = []*test.Case{ { Description: "with o pid,user,cmd", // Docker does not support top -o Require: require.Not(nerdtest.Docker), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("top", data.Labels().Get("cID"), "-o", "pid,user,cmd") }, Expected: test.Expects(0, nil, nil), }, { Description: "simple", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("top", data.Labels().Get("cID")) }, Expected: test.Expects(0, nil, nil), }, } testCase.Run(t) } func TestTopHyperVContainer(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = nerdtest.HyperV testCase.Setup = func(data test.Data, helpers test.Helpers) { // FIXME: busybox 1.36 on windows still appears to not support sleep inf. Unclear why. helpers.Ensure("run", "--isolation", "hyperv", "-d", "--name", data.Identifier("container"), testutil.CommonImage, "sleep", nerdtest.Infinity) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("container")) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("top", data.Identifier("container")) } testCase.Expected = test.Expects(0, nil, nil) testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_unpause.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "github.com/spf13/cobra" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/container" ) func UnpauseCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "unpause [flags] CONTAINER [CONTAINER, ...]", Args: cobra.MinimumNArgs(1), Short: "Unpause all processes within one or more containers", RunE: unpauseAction, ValidArgsFunction: unpauseShellComplete, SilenceUsage: true, SilenceErrors: true, } return cmd } func unpauseOptions(cmd *cobra.Command) (types.ContainerUnpauseOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ContainerUnpauseOptions{}, err } nerdctlCmd, nerdctlArgs := helpers.GlobalFlags(cmd) return types.ContainerUnpauseOptions{ GOptions: globalOptions, Stdout: cmd.OutOrStdout(), NerdctlCmd: nerdctlCmd, NerdctlArgs: nerdctlArgs, }, nil } func unpauseAction(cmd *cobra.Command, args []string) error { options, err := unpauseOptions(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return container.Unpause(ctx, client, args, options) } func unpauseShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show paused container names statusFilterFn := func(st containerd.ProcessStatus) bool { return st == containerd.Paused } return completion.ContainerNames(cmd, statusFilterFn) } ================================================ FILE: cmd/nerdctl/container/container_update.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "context" "encoding/json" "errors" "fmt" "runtime" "time" "github.com/docker/go-units" runtimespec "github.com/opencontainers/runtime-spec/specs-go" "github.com/spf13/cobra" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/core/containers" "github.com/containerd/errdefs" "github.com/containerd/log" "github.com/containerd/typeurl/v2" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" nerdctlcontainer "github.com/containerd/nerdctl/v2/pkg/cmd/container" "github.com/containerd/nerdctl/v2/pkg/formatter" "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" "github.com/containerd/nerdctl/v2/pkg/infoutil" ) type updateResourceOptions struct { CPUPeriod uint64 CPUQuota int64 CPUShares uint64 MemoryLimitInBytes int64 MemoryReservation int64 MemorySwapInBytes int64 CpusetCpus string CpusetMems string PidsLimit int64 BlkioWeight uint16 } func UpdateCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "update [flags] CONTAINER [CONTAINER, ...]", Args: cobra.MinimumNArgs(1), Short: "Update one or more running containers", RunE: updateAction, ValidArgsFunction: updateShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().SetInterspersed(false) setUpdateFlags(cmd) return cmd } func setUpdateFlags(cmd *cobra.Command) { cmd.Flags().Float64("cpus", 0.0, "Number of CPUs") cmd.Flags().Uint64("cpu-period", 0, "Limit CPU CFS (Completely Fair Scheduler) period") cmd.Flags().Int64("cpu-quota", -1, "Limit CPU CFS (Completely Fair Scheduler) quota") cmd.Flags().Uint64("cpu-shares", 0, "CPU shares (relative weight)") cmd.Flags().StringP("memory", "m", "", "Memory limit") cmd.Flags().String("memory-reservation", "", "Memory soft limit") cmd.Flags().String("memory-swap", "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") cmd.Flags().String("kernel-memory", "", "Kernel memory limit (deprecated)") cmd.Flags().String("cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)") cmd.Flags().String("cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)") cmd.Flags().Int64("pids-limit", -1, "Tune container pids limit (set -1 for unlimited)") cmd.Flags().Uint16("blkio-weight", 0, "Block IO (relative weight), between 10 and 1000, or 0 to disable (default 0)") cmd.Flags().String("restart", "no", `Restart policy to apply when a container exits (implemented values: "no"|"always|on-failure:n|unless-stopped")`) cmd.RegisterFlagCompletionFunc("restart", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"no", "always", "on-failure", "unless-stopped"}, cobra.ShellCompDirectiveNoFileComp }) } func updateAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() options, err := getUpdateOption(cmd, globalOptions) if err != nil { return err } walker := &containerwalker.ContainerWalker{ Client: client, OnFound: func(ctx context.Context, found containerwalker.Found) error { if found.MatchCount > 1 { return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) } err = updateContainer(ctx, client, found.Container.ID(), options, cmd) return err }, } return walker.WalkAll(ctx, args, true) } func getUpdateOption(cmd *cobra.Command, globalOptions types.GlobalCommandOptions) (updateResourceOptions, error) { var options updateResourceOptions cpus, err := cmd.Flags().GetFloat64("cpus") if err != nil { return options, err } cpuPeriod, err := cmd.Flags().GetUint64("cpu-period") if err != nil { return options, err } cpuQuota, err := cmd.Flags().GetInt64("cpu-quota") if err != nil { return options, err } if cpuQuota != -1 || cpuPeriod != 0 { if cpus > 0.0 { return options, errors.New("cpus and quota/period should be used separately") } } if cpus > 0.0 { cpuPeriod = uint64(100000) cpuQuota = int64(cpus * 100000.0) } shares, err := cmd.Flags().GetUint64("cpu-shares") if err != nil { return options, err } memStr, err := cmd.Flags().GetString("memory") if err != nil { return options, err } memSwap, err := cmd.Flags().GetString("memory-swap") if err != nil { return options, err } var mem64 int64 if memStr != "" { mem64, err = units.RAMInBytes(memStr) if err != nil { return options, fmt.Errorf("failed to parse memory bytes %q: %w", memStr, err) } } var memSwap64 int64 if memSwap != "" { if memSwap == "-1" { memSwap64 = -1 } else { memSwap64, err = units.RAMInBytes(memSwap) if err != nil { return options, fmt.Errorf("failed to parse memory-swap bytes %q: %w", memSwap, err) } if mem64 > 0 && memSwap64 > 0 && memSwap64 < mem64 { return options, fmt.Errorf("minimum memoryswap limit should be larger than memory limit, see usage") } } } else { memSwap64 = mem64 * 2 } if memSwap64 == 0 { memSwap64 = mem64 * 2 } memReserve, err := cmd.Flags().GetString("memory-reservation") if err != nil { return options, err } var memReserve64 int64 if memReserve != "" { memReserve64, err = units.RAMInBytes(memReserve) if err != nil { return options, fmt.Errorf("failed to parse memory bytes %q: %w", memReserve, err) } } if mem64 > 0 && memReserve64 > 0 && mem64 < memReserve64 { return options, fmt.Errorf("minimum memory limit can not be less than memory reservation limit, see usage") } kernelMemStr, err := cmd.Flags().GetString("kernel-memory") if err != nil { return options, err } if kernelMemStr != "" && cmd.Flag("kernel-memory").Changed { log.L.Warnf("The --kernel-memory flag is no longer supported. This flag is a noop.") } cpuset, err := cmd.Flags().GetString("cpuset-cpus") if err != nil { return options, err } cpusetMems, err := cmd.Flags().GetString("cpuset-mems") if err != nil { return options, err } pidsLimit, err := cmd.Flags().GetInt64("pids-limit") if err != nil { return options, err } blkioWeight, err := cmd.Flags().GetUint16("blkio-weight") if err != nil { return options, err } if blkioWeight != 0 && !infoutil.BlockIOWeight(globalOptions.CgroupManager) { return options, fmt.Errorf("kernel support for cgroup blkio weight missing, weight discarded") } if blkioWeight > 0 && blkioWeight < 10 || blkioWeight > 1000 { return options, errors.New("range of blkio weight is from 10 to 1000") } if runtime.GOOS == "linux" { options = updateResourceOptions{ CPUPeriod: cpuPeriod, CPUQuota: cpuQuota, CPUShares: shares, CpusetCpus: cpuset, CpusetMems: cpusetMems, MemoryLimitInBytes: mem64, MemoryReservation: memReserve64, MemorySwapInBytes: memSwap64, PidsLimit: pidsLimit, BlkioWeight: blkioWeight, } } return options, nil } func updateContainer(ctx context.Context, client *containerd.Client, id string, opts updateResourceOptions, cmd *cobra.Command) (retErr error) { container, err := client.LoadContainer(ctx, id) if err != nil { return err } cStatus := formatter.ContainerStatus(ctx, container) if cStatus == "pausing" { return fmt.Errorf("container %q is in pausing state", id) } spec, err := container.Spec(ctx) if err != nil { return err } oldSpec, err := copySpec(spec) if err != nil { return err } if runtime.GOOS == "linux" { if spec.Linux == nil { spec.Linux = &runtimespec.Linux{} } if spec.Linux.Resources == nil { spec.Linux.Resources = &runtimespec.LinuxResources{} } if cmd.Flags().Changed("blkio-weight") { if spec.Linux.Resources.BlockIO == nil { spec.Linux.Resources.BlockIO = &runtimespec.LinuxBlockIO{} } if spec.Linux.Resources.BlockIO.Weight != &opts.BlkioWeight { spec.Linux.Resources.BlockIO.Weight = &opts.BlkioWeight } } if cmd.Flags().Changed("cpu-shares") || cmd.Flags().Changed("cpu-quota") || cmd.Flags().Changed("cpu-period") || cmd.Flags().Changed("cpus") || cmd.Flags().Changed("cpuset-mems") || cmd.Flags().Changed("cpuset-cpus") { if spec.Linux.Resources.CPU == nil { spec.Linux.Resources.CPU = &runtimespec.LinuxCPU{} } } if cmd.Flags().Changed("cpu-shares") { if spec.Linux.Resources.CPU.Shares != &opts.CPUShares { spec.Linux.Resources.CPU.Shares = &opts.CPUShares } } if cmd.Flags().Changed("cpu-quota") { if spec.Linux.Resources.CPU.Quota != &opts.CPUQuota { spec.Linux.Resources.CPU.Quota = &opts.CPUQuota } } if cmd.Flags().Changed("cpu-period") { if spec.Linux.Resources.CPU.Period != &opts.CPUPeriod { spec.Linux.Resources.CPU.Period = &opts.CPUPeriod } } if cmd.Flags().Changed("cpus") { if spec.Linux.Resources.CPU.Cpus != opts.CpusetCpus { spec.Linux.Resources.CPU.Cpus = opts.CpusetCpus } } if cmd.Flags().Changed("cpuset-mems") { if spec.Linux.Resources.CPU.Mems != opts.CpusetMems { spec.Linux.Resources.CPU.Mems = opts.CpusetMems } } if cmd.Flags().Changed("cpuset-cpus") { if spec.Linux.Resources.CPU.Cpus != opts.CpusetCpus { spec.Linux.Resources.CPU.Cpus = opts.CpusetCpus } } if cmd.Flags().Changed("memory") || cmd.Flags().Changed("memory-reservation") { if spec.Linux.Resources.Memory == nil { spec.Linux.Resources.Memory = &runtimespec.LinuxMemory{} } } if cmd.Flags().Changed("memory") { if spec.Linux.Resources.Memory.Limit != &opts.MemoryLimitInBytes { spec.Linux.Resources.Memory.Limit = &opts.MemoryLimitInBytes } if spec.Linux.Resources.Memory.Swap != &opts.MemorySwapInBytes { spec.Linux.Resources.Memory.Swap = &opts.MemorySwapInBytes } } if cmd.Flags().Changed("memory-reservation") { if spec.Linux.Resources.Memory.Reservation != &opts.MemoryReservation { spec.Linux.Resources.Memory.Reservation = &opts.MemoryReservation } } if cmd.Flags().Changed("pids-limit") { if spec.Linux.Resources.Pids == nil { spec.Linux.Resources.Pids = &runtimespec.LinuxPids{} } if spec.Linux.Resources.Pids.Limit == nil || (spec.Linux.Resources.Pids.Limit != nil && *spec.Linux.Resources.Pids.Limit != opts.PidsLimit) { spec.Linux.Resources.Pids.Limit = &opts.PidsLimit } } } if err := updateContainerSpec(ctx, container, spec); err != nil { return fmt.Errorf("failed to update spec %+v for container %q", spec, id) } defer func() { if retErr != nil { deferCtx, deferCancel := context.WithTimeout(ctx, 1*time.Minute) defer deferCancel() // Reset spec on error. if err := updateContainerSpec(deferCtx, container, oldSpec); err != nil { log.G(ctx).WithError(err).Errorf("Failed to update spec %+v for container %q", oldSpec, id) } } }() restart, err := cmd.Flags().GetString("restart") if err != nil { return err } if cmd.Flags().Changed("restart") && restart != "" { if err := nerdctlcontainer.UpdateContainerRestartPolicyLabel(ctx, client, container, restart); err != nil { return err } } // If container is not running, only update spec is enough, new resource // limit will be applied when container start. if cStatus != "Up" { return nil } task, err := container.Task(ctx, nil) if err != nil { if errdefs.IsNotFound(err) { // Task exited already. return nil } return fmt.Errorf("failed to get task:%w", err) } return task.Update(ctx, containerd.WithResources(spec.Linux.Resources)) } func updateContainerSpec(ctx context.Context, container containerd.Container, spec *runtimespec.Spec) error { if err := container.Update(ctx, func(ctx context.Context, client *containerd.Client, c *containers.Container) error { a, err := typeurl.MarshalAny(spec) if err != nil { return fmt.Errorf("failed to marshal spec %+v:%w", spec, err) } c.Spec = a return nil }); err != nil { return fmt.Errorf("failed to update container spec:%w", err) } return nil } func copySpec(spec *runtimespec.Spec) (*runtimespec.Spec, error) { var copySpec runtimespec.Spec if spec == nil { return nil, errors.New("spec cannot be nil") } bytes, err := json.Marshal(spec) if err != nil { return nil, fmt.Errorf("unable to marshal spec: %w", err) } err = json.Unmarshal(bytes, ©Spec) if err != nil { return nil, fmt.Errorf("unable to unmarshal into spec copy: %w", err) } return ©Spec, nil } func updateShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.ContainerNames(cmd, nil) } ================================================ FILE: cmd/nerdctl/container/container_update_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "errors" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestUpdateContainer(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.Not(nerdtest.Docker) testCase.Setup = func(data test.Data, helpers test.Helpers) { containerName := testutil.Identifier(t) data.Labels().Set("containerName", containerName) helpers.Ensure("run", "-d", "--name", containerName, testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, containerName) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { containerName := data.Labels().Get("containerName") helpers.Anyhow("rm", "-f", containerName) } testCase.SubTests = []*test.Case{ { Description: "should fail on unsupported restart policy value", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { containerName := data.Labels().Get("containerName") return helpers.Command("update", "--memory", "999999999", "--restart", "123", containerName) }, Expected: test.Expects(1, []error{errors.New("unsupported restart policy")}, nil), }, { Description: "should not update memory in inspect", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { containerName := data.Labels().Get("containerName") return helpers.Command("inspect", "--mode=native", containerName) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.DoesNotContain(`"limit": 999999999,`)), }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/container_wait.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "github.com/spf13/cobra" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/container" ) func WaitCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "wait [flags] CONTAINER [CONTAINER, ...]", Args: cobra.MinimumNArgs(1), Short: "Block until one or more containers stop, then print their exit codes.", RunE: waitAction, ValidArgsFunction: waitShellComplete, SilenceUsage: true, SilenceErrors: true, } return cmd } func waitOptions(cmd *cobra.Command) (types.ContainerWaitOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ContainerWaitOptions{}, err } return types.ContainerWaitOptions{ Stdout: cmd.OutOrStdout(), GOptions: globalOptions, }, nil } func waitAction(cmd *cobra.Command, args []string) error { options, err := waitOptions(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return container.Wait(ctx, client, args, options) } func waitShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show running container names statusFilterFn := func(st containerd.ProcessStatus) bool { return st == containerd.Running } return completion.ContainerNames(cmd, statusFilterFn) } ================================================ FILE: cmd/nerdctl/container/container_wait_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestWait(t *testing.T) { testCase := nerdtest.Setup() testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("1"), data.Identifier("2"), data.Identifier("3")) } testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier("1"), testutil.CommonImage) helpers.Ensure("run", "-d", "--name", data.Identifier("2"), testutil.CommonImage, "sleep", "1") helpers.Ensure("run", "-d", "--name", data.Identifier("3"), testutil.CommonImage, "sh", "-euxc", "sleep 5; exit 123") } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("wait", data.Identifier("1"), data.Identifier("2"), data.Identifier("3")) } testCase.Expected = test.Expects(0, nil, expect.Equals(`0 0 123 `)) testCase.Run(t) } ================================================ FILE: cmd/nerdctl/container/multi_platform_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "fmt" "io" "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" ) func testMultiPlatformRun(base *testutil.Base, alpineImage string) { t := base.T testutil.RequireExecPlatform(t, "linux/amd64", "linux/arm64", "linux/arm/v7") testCases := map[string]string{ "amd64": "x86_64", "arm64": "aarch64", "arm": "armv7l", "linux/arm": "armv7l", "linux/arm/v7": "armv7l", } for plat, expectedUnameM := range testCases { t.Logf("Testing %q (%q)", plat, expectedUnameM) cmd := base.Cmd("run", "--rm", "--platform="+plat, alpineImage, "uname", "-m") cmd.AssertOutExactly(expectedUnameM + "\n") } } func TestMultiPlatformRun(t *testing.T) { base := testutil.NewBase(t) testMultiPlatformRun(base, testutil.AlpineImage) } func TestMultiPlatformBuildPush(t *testing.T) { testutil.DockerIncompatible(t) // non-buildx version of `docker build` lacks multi-platform. Also, `docker push` lacks --platform. testutil.RequiresBuild(t) testutil.RegisterBuildCacheCleanup(t) testutil.RequireExecPlatform(t, "linux/amd64", "linux/arm64", "linux/arm/v7") base := testutil.NewBase(t) tID := testutil.Identifier(t) reg := testregistry.NewWithNoAuth(base, 0, false) defer reg.Cleanup(nil) imageName := fmt.Sprintf("localhost:%d/%s:latest", reg.Port, tID) defer base.Cmd("rmi", imageName).Run() dockerfile := fmt.Sprintf(`FROM %s RUN echo dummy `, testutil.AlpineImage) buildCtx := helpers.CreateBuildContext(t, dockerfile) base.Cmd("build", "-t", imageName, "--platform=amd64,arm64,linux/arm/v7", buildCtx).AssertOK() testMultiPlatformRun(base, imageName) base.Cmd("push", "--platform=amd64,arm64,linux/arm/v7", imageName).AssertOK() } // TestMultiPlatformBuildPushNoRun tests if the push succeeds in a situation where nerdctl builds // a Dockerfile without RUN, COPY, etc commands. In such situation, BuildKit doesn't download the base image // so nerdctl needs to ensure these blobs to be locally available. func TestMultiPlatformBuildPushNoRun(t *testing.T) { testutil.DockerIncompatible(t) // non-buildx version of `docker build` lacks multi-platform. Also, `docker push` lacks --platform. testutil.RequiresBuild(t) testutil.RegisterBuildCacheCleanup(t) testutil.RequireExecPlatform(t, "linux/amd64", "linux/arm64", "linux/arm/v7") base := testutil.NewBase(t) tID := testutil.Identifier(t) reg := testregistry.NewWithNoAuth(base, 0, false) defer reg.Cleanup(nil) imageName := fmt.Sprintf("localhost:%d/%s:latest", reg.Port, tID) defer base.Cmd("rmi", imageName).Run() dockerfile := fmt.Sprintf(`FROM %s CMD echo dummy `, testutil.AlpineImage) buildCtx := helpers.CreateBuildContext(t, dockerfile) base.Cmd("build", "-t", imageName, "--platform=amd64,arm64,linux/arm/v7", buildCtx).AssertOK() testMultiPlatformRun(base, imageName) base.Cmd("push", "--platform=amd64,arm64,linux/arm/v7", imageName).AssertOK() } func TestMultiPlatformPullPushAllPlatforms(t *testing.T) { testutil.DockerIncompatible(t) base := testutil.NewBase(t) tID := testutil.Identifier(t) reg := testregistry.NewWithNoAuth(base, 0, false) defer reg.Cleanup(nil) pushImageName := fmt.Sprintf("localhost:%d/%s:latest", reg.Port, tID) defer base.Cmd("rmi", pushImageName).Run() base.Cmd("pull", "--quiet", "--all-platforms", testutil.AlpineImage).AssertOK() base.Cmd("tag", testutil.AlpineImage, pushImageName).AssertOK() base.Cmd("push", "--all-platforms", pushImageName).AssertOK() testMultiPlatformRun(base, pushImageName) } func TestMultiPlatformComposeUpBuild(t *testing.T) { testutil.DockerIncompatible(t) testutil.RequiresBuild(t) testutil.RegisterBuildCacheCleanup(t) testutil.RequireExecPlatform(t, "linux/amd64", "linux/arm64", "linux/arm/v7") base := testutil.NewBase(t) const dockerComposeYAML = ` services: svc0: build: . platform: amd64 ports: - 8080:80 svc1: build: . platform: arm64 ports: - 8081:80 svc2: build: . platform: linux/arm/v7 ports: - 8082:80 ` dockerfile := fmt.Sprintf(`FROM %s RUN uname -m > /usr/share/nginx/html/index.html `, testutil.NginxAlpineImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() comp.WriteFile("Dockerfile", dockerfile) base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d", "--build").AssertOK() defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() testCases := map[string]string{ "http://127.0.0.1:8080": "x86_64", "http://127.0.0.1:8081": "aarch64", "http://127.0.0.1:8082": "armv7l", } for testURL, expectedIndexHTML := range testCases { resp, err := nettestutil.HTTPGet(testURL, 5, false) assert.NilError(t, err) respBody, err := io.ReadAll(resp.Body) assert.NilError(t, err) t.Logf("respBody=%q", respBody) assert.Assert(t, strings.Contains(string(respBody), expectedIndexHTML)) } } ================================================ FILE: cmd/nerdctl/helpers/cobra.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package helpers import ( "errors" "fmt" "os" "strconv" "time" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/containerd/log" ) // UnknownSubcommandAction is needed to let `nerdctl system non-existent-command` fail // https://github.com/containerd/nerdctl/issues/487 // // Ideally this should be implemented in Cobra itself. func UnknownSubcommandAction(cmd *cobra.Command, args []string) error { if len(args) == 0 { return cmd.Help() } // The output mimics https://github.com/spf13/cobra/blob/v1.2.1/command.go#L647-L662 msg := fmt.Sprintf("unknown subcommand %q for %q", args[0], cmd.Name()) if suggestions := cmd.SuggestionsFor(args[0]); len(suggestions) > 0 { msg += "\n\nDid you mean this?\n" for _, s := range suggestions { msg += fmt.Sprintf("\t%v\n", s) } } return errors.New(msg) } // IsExactArgs returns an error if there is not the exact number of args func IsExactArgs(number int) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { if len(args) == number { return nil } return fmt.Errorf( "%q requires exactly %d %s.\nSee '%s --help'.\n\nUsage: %s\n\n%s", cmd.CommandPath(), number, "argument(s)", cmd.CommandPath(), cmd.UseLine(), cmd.Short, ) } } // AddStringFlag is similar to cmd.Flags().String but supports aliases and env var func AddStringFlag(cmd *cobra.Command, name string, aliases []string, value string, env, usage string) { if env != "" { usage = fmt.Sprintf("%s [$%s]", usage, env) } if envV, ok := os.LookupEnv(env); ok { value = envV } aliasesUsage := fmt.Sprintf("Alias of --%s", name) p := new(string) flags := cmd.Flags() flags.StringVar(p, name, value, usage) for _, a := range aliases { if len(a) == 1 { // pflag doesn't support short-only flags, so we have to register long one as well here flags.StringVarP(p, a, a, value, aliasesUsage) } else { flags.StringVar(p, a, value, aliasesUsage) } } } // AddIntFlag is similar to cmd.Flags().Int but supports aliases and env var func AddIntFlag(cmd *cobra.Command, name string, aliases []string, value int, env, usage string) { if env != "" { usage = fmt.Sprintf("%s [$%s]", usage, env) } if envV, ok := os.LookupEnv(env); ok { v, err := strconv.ParseInt(envV, 10, 64) if err != nil { log.L.WithError(err).Warnf("Invalid int value for `%s`", env) } value = int(v) } aliasesUsage := fmt.Sprintf("Alias of --%s", name) p := new(int) flags := cmd.Flags() flags.IntVar(p, name, value, usage) for _, a := range aliases { if len(a) == 1 { // pflag doesn't support short-only flags, so we have to register long one as well here flags.IntVarP(p, a, a, value, aliasesUsage) } else { flags.IntVar(p, a, value, aliasesUsage) } } } // AddDurationFlag is similar to cmd.Flags().Duration but supports aliases and env var func AddDurationFlag(cmd *cobra.Command, name string, aliases []string, value time.Duration, env, usage string) { if env != "" { usage = fmt.Sprintf("%s [$%s]", usage, env) } if envV, ok := os.LookupEnv(env); ok { var err error value, err = time.ParseDuration(envV) if err != nil { log.L.WithError(err).Warnf("Invalid duration value for `%s`", env) } } aliasesUsage := fmt.Sprintf("Alias of --%s", name) p := new(time.Duration) flags := cmd.Flags() flags.DurationVar(p, name, value, usage) for _, a := range aliases { if len(a) == 1 { // pflag doesn't support short-only flags, so we have to register long one as well here flags.DurationVarP(p, a, a, value, aliasesUsage) } else { flags.DurationVar(p, a, value, aliasesUsage) } } } func GlobalFlags(cmd *cobra.Command) (string, []string) { args0, err := os.Executable() if err != nil { log.L.WithError(err).Warnf("cannot call os.Executable(), assuming the executable to be %q", os.Args[0]) args0 = os.Args[0] } if len(os.Args) < 2 { return args0, nil } rootCmd := cmd.Root() flagSet := rootCmd.Flags() args := []string{} flagSet.VisitAll(func(f *pflag.Flag) { key := f.Name val := f.Value.String() // Include flag if: // 1. It was explicitly changed via CLI (highest priority), OR // 2. It has a non-default value (from TOML config) // This ensures both CLI flags and TOML config values are propagated if f.Changed || (val != f.DefValue && val != "") { args = append(args, "--"+key+"="+val) } }) return args0, args } // AddPersistentStringArrayFlag is similar to cmd.Flags().StringArray but supports aliases and env var and persistent. // See https://github.com/spf13/cobra/blob/main/user_guide.md#persistent-flags to learn what is "persistent". func AddPersistentStringArrayFlag(cmd *cobra.Command, name string, aliases, nonPersistentAliases []string, value []string, env string, usage string) { if env != "" { usage = fmt.Sprintf("%s [$%s]", usage, env) } if envV, ok := os.LookupEnv(env); ok { value = []string{envV} } aliasesUsage := fmt.Sprintf("Alias of --%s", name) p := new([]string) flags := cmd.Flags() for _, a := range nonPersistentAliases { if len(a) == 1 { // pflag doesn't support short-only flags, so we have to register long one as well here flags.StringArrayVarP(p, a, a, value, aliasesUsage) } else { flags.StringArrayVar(p, a, value, aliasesUsage) } } persistentFlags := cmd.PersistentFlags() persistentFlags.StringArrayVar(p, name, value, usage) for _, a := range aliases { if len(a) == 1 { // pflag doesn't support short-only flags, so we have to register long one as well here persistentFlags.StringArrayVarP(p, a, a, value, aliasesUsage) } else { persistentFlags.StringArrayVar(p, a, value, aliasesUsage) } } } // AddPersistentStringFlag is similar to AddStringFlag but persistent. // See https://github.com/spf13/cobra/blob/main/user_guide.md#persistent-flags to learn what is "persistent". func AddPersistentStringFlag(cmd *cobra.Command, name string, aliases, localAliases, persistentAliases []string, aliasToBeInherited *pflag.FlagSet, value string, env, usage string) { if env != "" { usage = fmt.Sprintf("%s [$%s]", usage, env) } if envV, ok := os.LookupEnv(env); ok { value = envV } aliasesUsage := fmt.Sprintf("Alias of --%s", name) p := new(string) // flags is full set of flag(s) // flags can redefine alias already used in subcommands flags := cmd.Flags() for _, a := range aliases { if len(a) == 1 { // pflag doesn't support short-only flags, so we have to register long one as well here flags.StringVarP(p, a, a, value, aliasesUsage) } else { flags.StringVar(p, a, value, aliasesUsage) } // non-persistent flags are not added to the InheritedFlags, so we should add them manually f := flags.Lookup(a) aliasToBeInherited.AddFlag(f) } // localFlags are local to the rootCmd localFlags := cmd.LocalFlags() for _, a := range localAliases { if len(a) == 1 { // pflag doesn't support short-only flags, so we have to register long one as well here localFlags.StringVarP(p, a, a, value, aliasesUsage) } else { localFlags.StringVar(p, a, value, aliasesUsage) } } // persistentFlags cannot redefine alias already used in subcommands persistentFlags := cmd.PersistentFlags() persistentFlags.StringVar(p, name, value, usage) for _, a := range persistentAliases { if len(a) == 1 { // pflag doesn't support short-only flags, so we have to register long one as well here persistentFlags.StringVarP(p, a, a, value, aliasesUsage) } else { persistentFlags.StringVar(p, a, value, aliasesUsage) } } } // AddPersistentBoolFlag is similar to AddBoolFlag but persistent. // See https://github.com/spf13/cobra/blob/main/user_guide.md#persistent-flags to learn what is "persistent". func AddPersistentBoolFlag(cmd *cobra.Command, name string, aliases, nonPersistentAliases []string, value bool, env, usage string) { if env != "" { usage = fmt.Sprintf("%s [$%s]", usage, env) } if envV, ok := os.LookupEnv(env); ok { var err error value, err = strconv.ParseBool(envV) if err != nil { log.L.WithError(err).Warnf("Invalid boolean value for `%s`", env) } } aliasesUsage := fmt.Sprintf("Alias of --%s", name) p := new(bool) flags := cmd.Flags() for _, a := range nonPersistentAliases { if len(a) == 1 { // pflag doesn't support short-only flags, so we have to register long one as well here flags.BoolVarP(p, a, a, value, aliasesUsage) } else { flags.BoolVar(p, a, value, aliasesUsage) } } persistentFlags := cmd.PersistentFlags() persistentFlags.BoolVar(p, name, value, usage) for _, a := range aliases { if len(a) == 1 { // pflag doesn't support short-only flags, so we have to register long one as well here persistentFlags.BoolVarP(p, a, a, value, aliasesUsage) } else { persistentFlags.BoolVar(p, a, value, aliasesUsage) } } } // HiddenPersistentStringArrayFlag creates a persistent string slice flag and hides it. // Used mainly to pass global config values to individual commands. func HiddenPersistentStringArrayFlag(cmd *cobra.Command, name string, value []string, usage string) { cmd.PersistentFlags().StringSlice(name, value, usage) cmd.PersistentFlags().MarkHidden(name) } ================================================ FILE: cmd/nerdctl/helpers/consts.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package helpers const ( Category = "category" Management = "management" ) ================================================ FILE: cmd/nerdctl/helpers/flagutil.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package helpers import ( "fmt" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/fs" ) func VerifyOptions(cmd *cobra.Command) (opt types.ImageVerifyOptions, err error) { if opt.Provider, err = cmd.Flags().GetString("verify"); err != nil { return } if opt.CosignKey, err = cmd.Flags().GetString("cosign-key"); err != nil { return } if opt.CosignCertificateIdentity, err = cmd.Flags().GetString("cosign-certificate-identity"); err != nil { return } if opt.CosignCertificateIdentityRegexp, err = cmd.Flags().GetString("cosign-certificate-identity-regexp"); err != nil { return } if opt.CosignCertificateOidcIssuer, err = cmd.Flags().GetString("cosign-certificate-oidc-issuer"); err != nil { return } if opt.CosignCertificateOidcIssuerRegexp, err = cmd.Flags().GetString("cosign-certificate-oidc-issuer-regexp"); err != nil { return } return } func ValidateHealthcheckFlags(options types.ContainerCreateOptions) error { healthFlagsSet := options.HealthInterval != 0 || options.HealthTimeout != 0 || options.HealthRetries != 0 || options.HealthStartPeriod != 0 if options.NoHealthcheck { if options.HealthCmd != "" || healthFlagsSet { return fmt.Errorf("--no-healthcheck conflicts with --health-* options") } } // Note: HealthCmd can be empty with other healthcheck flags set cause healthCmd could be coming from image. if options.HealthInterval < 0 { return fmt.Errorf("--health-interval cannot be negative") } if options.HealthTimeout < 0 { return fmt.Errorf("--health-timeout cannot be negative") } if options.HealthRetries < 0 { return fmt.Errorf("--health-retries cannot be negative") } if options.HealthStartPeriod < 0 { return fmt.Errorf("--health-start-period cannot be negative") } return nil } func ProcessRootCmdFlags(cmd *cobra.Command) (types.GlobalCommandOptions, error) { debug, err := cmd.Flags().GetBool("debug") if err != nil { return types.GlobalCommandOptions{}, err } debugFull, err := cmd.Flags().GetBool("debug-full") if err != nil { return types.GlobalCommandOptions{}, err } address, err := cmd.Flags().GetString("address") if err != nil { return types.GlobalCommandOptions{}, err } namespace, err := cmd.Flags().GetString("namespace") if err != nil { return types.GlobalCommandOptions{}, err } snapshotter, err := cmd.Flags().GetString("snapshotter") if err != nil { return types.GlobalCommandOptions{}, err } cniPath, err := cmd.Flags().GetString("cni-path") if err != nil { return types.GlobalCommandOptions{}, err } cniConfigPath, err := cmd.Flags().GetString("cni-netconfpath") if err != nil { return types.GlobalCommandOptions{}, err } dataRoot, err := cmd.Flags().GetString("data-root") if err != nil { return types.GlobalCommandOptions{}, err } cgroupManager, err := cmd.Flags().GetString("cgroup-manager") if err != nil { return types.GlobalCommandOptions{}, err } insecureRegistry, err := cmd.Flags().GetBool("insecure-registry") if err != nil { return types.GlobalCommandOptions{}, err } hostsDir, err := cmd.Flags().GetStringSlice("hosts-dir") if err != nil { return types.GlobalCommandOptions{}, err } experimental, err := cmd.Flags().GetBool("experimental") if err != nil { return types.GlobalCommandOptions{}, err } hostGatewayIP, err := cmd.Flags().GetString("host-gateway-ip") if err != nil { return types.GlobalCommandOptions{}, err } bridgeIP, err := cmd.Flags().GetString("bridge-ip") if err != nil { return types.GlobalCommandOptions{}, err } kubeHideDupe, err := cmd.Flags().GetBool("kube-hide-dupe") if err != nil { return types.GlobalCommandOptions{}, err } cdiSpecDirs, err := cmd.Flags().GetStringSlice("cdi-spec-dirs") if err != nil { return types.GlobalCommandOptions{}, err } dns, err := cmd.Flags().GetStringSlice("global-dns") if err != nil { return types.GlobalCommandOptions{}, err } dnsOpts, err := cmd.Flags().GetStringSlice("global-dns-opts") if err != nil { return types.GlobalCommandOptions{}, err } dnsSearch, err := cmd.Flags().GetStringSlice("global-dns-search") if err != nil { return types.GlobalCommandOptions{}, err } // Point to dataRoot for filesystem-helpers implementing rollback / backups. err = fs.InitFS(dataRoot) if err != nil { return types.GlobalCommandOptions{}, err } return types.GlobalCommandOptions{ Debug: debug, DebugFull: debugFull, Address: address, Namespace: namespace, Snapshotter: snapshotter, CNIPath: cniPath, CNINetConfPath: cniConfigPath, DataRoot: dataRoot, CgroupManager: cgroupManager, InsecureRegistry: insecureRegistry, HostsDir: hostsDir, Experimental: experimental, HostGatewayIP: hostGatewayIP, BridgeIP: bridgeIP, KubeHideDupe: kubeHideDupe, CDISpecDirs: cdiSpecDirs, DNS: dns, DNSOpts: dnsOpts, DNSSearch: dnsSearch, }, nil } func CheckExperimental(feature string) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { globalOptions, err := ProcessRootCmdFlags(cmd) if err != nil { return err } if !globalOptions.Experimental { return fmt.Errorf("%s is experimental feature, you should enable experimental config", feature) } return nil } } ================================================ FILE: cmd/nerdctl/helpers/prompt.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package helpers import ( "fmt" "strings" "github.com/spf13/cobra" ) func Confirm(cmd *cobra.Command, message string) (bool, error) { message += "\nAre you sure you want to continue? [y/N] " _, err := fmt.Fprint(cmd.OutOrStdout(), message) if err != nil { return false, err } var confirm string _, err = fmt.Fscanf(cmd.InOrStdin(), "%s", &confirm) if err != nil { return false, err } return strings.ToLower(confirm) == "y", err } ================================================ FILE: cmd/nerdctl/helpers/testing.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package helpers import ( "encoding/json" "errors" "fmt" "os" "os/exec" "path/filepath" "testing" "gotest.tools/v3/assert" ) func CreateBuildContext(t *testing.T, dockerfile string) string { tmpDir := t.TempDir() err := os.WriteFile(filepath.Join(tmpDir, "Dockerfile"), []byte(dockerfile), 0644) assert.NilError(t, err) return tmpDir } func ExtractDockerArchive(archiveTarPath, rootfsPath string) error { if err := os.MkdirAll(rootfsPath, 0755); err != nil { return err } workDir, err := os.MkdirTemp("", "extract-docker-archive") if err != nil { return err } defer os.RemoveAll(workDir) if err := ExtractTarFile(workDir, archiveTarPath); err != nil { return err } manifestJSONPath := filepath.Join(workDir, "manifest.json") manifestJSONBytes, err := os.ReadFile(manifestJSONPath) if err != nil { return err } var mani DockerArchiveManifestJSON if err := json.Unmarshal(manifestJSONBytes, &mani); err != nil { return err } if len(mani) > 1 { return fmt.Errorf("multi-image archive cannot be extracted: contains %d images", len(mani)) } if len(mani) < 1 { return errors.New("invalid archive") } ent := mani[0] for _, l := range ent.Layers { layerTarPath := filepath.Join(workDir, l) if err := ExtractTarFile(rootfsPath, layerTarPath); err != nil { return err } } return nil } type DockerArchiveManifestJSON []DockerArchiveManifestJSONEntry type DockerArchiveManifestJSONEntry struct { Config string RepoTags []string Layers []string } func ExtractTarFile(dirPath, tarFilePath string) error { cmd := exec.Command("tar", "Cxf", dirPath, tarFilePath) if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err) } return nil } ================================================ FILE: cmd/nerdctl/helpers/testing_linux.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package helpers import ( "fmt" "io" "os" "os/exec" "path/filepath" "strings" "testing" "time" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" ) type CosignKeyPair struct { PublicKey string PrivateKey string Cleanup func() } func NewCosignKeyPair(t testing.TB, path string, password string) *CosignKeyPair { td, err := os.MkdirTemp(t.TempDir(), path) assert.NilError(t, err) cmd := exec.Command("cosign", "generate-key-pair") cmd.Dir = td cmd.Env = append(cmd.Env, fmt.Sprintf("COSIGN_PASSWORD=%s", password)) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("failed to run %v: %v (%q)", cmd.Args, err, string(out)) } publicKey := filepath.Join(td, "cosign.pub") privateKey := filepath.Join(td, "cosign.key") return &CosignKeyPair{ PublicKey: publicKey, PrivateKey: privateKey, Cleanup: func() { _ = os.RemoveAll(td) }, } } func ComposeUp(t *testing.T, base *testutil.Base, dockerComposeYAML string, opts ...string) { comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() projectName := comp.ProjectName() t.Logf("projectName=%q", projectName) base.ComposeCmd(append(append([]string{"-f", comp.YAMLFullPath()}, opts...), "up", "-d")...).AssertOK() defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() base.Cmd("volume", "inspect", fmt.Sprintf("%s_db", projectName)).AssertOK() base.Cmd("network", "inspect", fmt.Sprintf("%s_default", projectName)).AssertOK() checkWordpress := func() error { resp, err := nettestutil.HTTPGet("http://127.0.0.1:8080", 5, false) if err != nil { return err } respBody, err := io.ReadAll(resp.Body) if err != nil { return err } if !strings.Contains(string(respBody), testutil.WordpressIndexHTMLSnippet) { t.Logf("respBody=%q", respBody) return fmt.Errorf("respBody does not contain %q", testutil.WordpressIndexHTMLSnippet) } return nil } var wordpressWorking bool for i := 0; i < 30; i++ { t.Logf("(retry %d)", i) err := checkWordpress() if err == nil { wordpressWorking = true break } // NOTE: "

Error establishing a database connection

" is expected for the first few iterations t.Log(err) time.Sleep(3 * time.Second) } if !wordpressWorking { t.Fatal("wordpress is not working") } t.Log("wordpress seems functional") base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() base.Cmd("volume", "inspect", fmt.Sprintf("%s_db", projectName)).AssertFail() base.Cmd("network", "inspect", fmt.Sprintf("%s_default", projectName)).AssertFail() } ================================================ FILE: cmd/nerdctl/image/image.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/builder" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" ) func Command() *cobra.Command { cmd := &cobra.Command{ Annotations: map[string]string{helpers.Category: helpers.Management}, Use: "image", Short: "Manage images", RunE: helpers.UnknownSubcommandAction, SilenceUsage: true, SilenceErrors: true, } cmd.AddCommand( builder.BuildCommand(), // commitCommand is in "container", not in "image" imageLsCommand(), HistoryCommand(), PullCommand(), PushCommand(), LoadCommand(), SaveCommand(), ImportCommand(), TagCommand(), imageRemoveCommand(), convertCommand(), inspectCommand(), encryptCommand(), decryptCommand(), pruneCommand(), ) return cmd } func imageLsCommand() *cobra.Command { x := ImagesCommand() x.Use = "ls" x.Aliases = []string{"list"} return x } func imageRemoveCommand() *cobra.Command { x := RmiCommand() x.Use = "rm" x.Aliases = []string{"remove"} return x } ================================================ FILE: cmd/nerdctl/image/image_convert.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "compress/gzip" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/image" ) const imageConvertHelp = `Convert an image format. e.g., 'nerdctl image convert --estargz --oci example.com/foo:orig example.com/foo:esgz' Use '--platform' to define the output platform. When '--all-platforms' is given all images in a manifest list must be available. For encryption and decryption, use 'nerdctl image (encrypt|decrypt)' command. ` // imageConvertCommand is from https://github.com/containerd/stargz-snapshotter/blob/d58f43a8235e46da73fb94a1a35280cb4d607b2c/cmd/ctr-remote/commands/convert.go func convertCommand() *cobra.Command { cmd := &cobra.Command{ Use: "convert [flags] ...", Short: "convert an image", Long: imageConvertHelp, Args: cobra.MinimumNArgs(2), RunE: imageConvertAction, ValidArgsFunction: imageConvertShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().String("format", "", "Format the output using the given Go template, e.g, 'json'") // #region estargz flags cmd.Flags().Bool("estargz", false, "Convert legacy tar(.gz) layers to eStargz for lazy pulling. Should be used in conjunction with '--oci'") cmd.Flags().String("estargz-record-in", "", "Read 'ctr-remote optimize --record-out=' record file (EXPERIMENTAL)") cmd.Flags().Int("estargz-compression-level", gzip.BestCompression, "eStargz compression level") cmd.Flags().Int("estargz-chunk-size", 0, "eStargz chunk size") cmd.Flags().Int("estargz-min-chunk-size", 0, "The minimal number of bytes of data must be written in one gzip stream. (requires stargz-snapshotter >= v0.13.0)") cmd.Flags().Bool("estargz-external-toc", false, "Separate TOC JSON into another image (called \"TOC image\"). The name of TOC image is the original + \"-esgztoc\" suffix. Both eStargz and the TOC image should be pushed to the same registry. (requires stargz-snapshotter >= v0.13.0) (EXPERIMENTAL)") cmd.Flags().Bool("estargz-keep-diff-id", false, "Convert to esgz without changing diffID (cannot be used in conjunction with '--estargz-record-in'. must be specified with '--estargz-external-toc')") cmd.Flags().String("estargz-gzip-helper", "", "Helper command for decompressing layers compressed with gzip. Options: pigz, igzip, or gzip.") // #endregion // #region zstd flags cmd.Flags().Bool("zstd", false, "Convert legacy tar(.gz) layers to zstd. Should be used in conjunction with '--oci'") cmd.Flags().Int("zstd-compression-level", 3, "zstd compression level") // #endregion // #region zstd:chunked flags cmd.Flags().Bool("zstdchunked", false, "Convert legacy tar(.gz) layers to zstd:chunked for lazy pulling. Should be used in conjunction with '--oci'") cmd.Flags().String("zstdchunked-record-in", "", "Read 'ctr-remote optimize --record-out=' record file (EXPERIMENTAL)") cmd.Flags().Int("zstdchunked-compression-level", 3, "zstd:chunked compression level") // SpeedDefault; see also https://pkg.go.dev/github.com/klauspost/compress/zstd#EncoderLevel cmd.Flags().Int("zstdchunked-chunk-size", 0, "zstd:chunked chunk size") // #endregion // #region nydus flags cmd.Flags().Bool("nydus", false, "Convert an OCI image to Nydus image. Should be used in conjunction with '--oci'") cmd.Flags().String("nydus-builder-path", "nydus-image", "The nydus-image binary path, if unset, search in PATH environment") cmd.Flags().String("nydus-work-dir", "", "Work directory path for image conversion, default is the nerdctl data root directory") cmd.Flags().String("nydus-prefetch-patterns", "", "The file path pattern list want to prefetch") cmd.Flags().String("nydus-compressor", "lz4_block", "Nydus blob compression algorithm, possible values: `none`, `lz4_block`, `zstd`, default is `lz4_block`") // #endregion // #region overlaybd flags cmd.Flags().Bool("overlaybd", false, "Convert tar.gz layers to overlaybd layers") cmd.Flags().String("overlaybd-fs-type", "ext4", "Filesystem type for overlaybd") cmd.Flags().String("overlaybd-dbstr", "", "Database config string for overlaybd") // #endregion // #region soci flags cmd.Flags().Bool("soci", false, "Convert image to SOCI Index V2 format.") cmd.Flags().Int64("soci-min-layer-size", -1, "The minimum size of layers that will be converted to SOCI Index V2 format") cmd.Flags().Int64("soci-span-size", -1, "The size of SOCI spans") // #endregion // #region generic flags cmd.Flags().Bool("uncompress", false, "Convert tar.gz layers to uncompressed tar layers") cmd.Flags().Bool("oci", false, "Convert Docker media types to OCI media types") // #endregion // #region platform flags // platform is defined as StringSlice, not StringArray, to allow specifying "--platform=amd64,arm64" cmd.Flags().StringSlice("platform", []string{}, "Convert content for a specific platform") cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) cmd.Flags().Bool("all-platforms", false, "Convert content for all platforms") // #endregion return cmd } func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ImageConvertOptions{}, err } progressOutput := cmd.ErrOrStderr() format, err := cmd.Flags().GetString("format") if err != nil { return types.ImageConvertOptions{}, err } // #region estargz flags estargz, err := cmd.Flags().GetBool("estargz") if err != nil { return types.ImageConvertOptions{}, err } estargzRecordIn, err := cmd.Flags().GetString("estargz-record-in") if err != nil { return types.ImageConvertOptions{}, err } estargzCompressionLevel, err := cmd.Flags().GetInt("estargz-compression-level") if err != nil { return types.ImageConvertOptions{}, err } estargzChunkSize, err := cmd.Flags().GetInt("estargz-chunk-size") if err != nil { return types.ImageConvertOptions{}, err } estargzMinChunkSize, err := cmd.Flags().GetInt("estargz-min-chunk-size") if err != nil { return types.ImageConvertOptions{}, err } estargzExternalTOC, err := cmd.Flags().GetBool("estargz-external-toc") if err != nil { return types.ImageConvertOptions{}, err } estargzKeepDiffID, err := cmd.Flags().GetBool("estargz-keep-diff-id") if err != nil { return types.ImageConvertOptions{}, err } estargzGzipHelper, err := cmd.Flags().GetString("estargz-gzip-helper") if err != nil { return types.ImageConvertOptions{}, err } // #endregion // #region zstd flags zstd, err := cmd.Flags().GetBool("zstd") if err != nil { return types.ImageConvertOptions{}, err } zstdCompressionLevel, err := cmd.Flags().GetInt("zstd-compression-level") if err != nil { return types.ImageConvertOptions{}, err } // #endregion // #region zstd:chunked flags zstdchunked, err := cmd.Flags().GetBool("zstdchunked") if err != nil { return types.ImageConvertOptions{}, err } zstdChunkedCompressionLevel, err := cmd.Flags().GetInt("zstdchunked-compression-level") if err != nil { return types.ImageConvertOptions{}, err } zstdChunkedChunkSize, err := cmd.Flags().GetInt("zstdchunked-chunk-size") if err != nil { return types.ImageConvertOptions{}, err } zstdChunkedRecordIn, err := cmd.Flags().GetString("zstdchunked-record-in") if err != nil { return types.ImageConvertOptions{}, err } // #endregion // #region nydus flags nydus, err := cmd.Flags().GetBool("nydus") if err != nil { return types.ImageConvertOptions{}, err } nydusBuilderPath, err := cmd.Flags().GetString("nydus-builder-path") if err != nil { return types.ImageConvertOptions{}, err } nydusWorkDir, err := cmd.Flags().GetString("nydus-work-dir") if err != nil { return types.ImageConvertOptions{}, err } nydusPrefetchPatterns, err := cmd.Flags().GetString("nydus-prefetch-patterns") if err != nil { return types.ImageConvertOptions{}, err } nydusCompressor, err := cmd.Flags().GetString("nydus-compressor") if err != nil { return types.ImageConvertOptions{}, err } // #endregion // #region overlaybd flags overlaybd, err := cmd.Flags().GetBool("overlaybd") if err != nil { return types.ImageConvertOptions{}, err } overlaybdFsType, err := cmd.Flags().GetString("overlaybd-fs-type") if err != nil { return types.ImageConvertOptions{}, err } overlaybdDbstr, err := cmd.Flags().GetString("overlaybd-dbstr") if err != nil { return types.ImageConvertOptions{}, err } // #endregion // #region soci flags soci, err := cmd.Flags().GetBool("soci") if err != nil { return types.ImageConvertOptions{}, err } sociMinLayerSize, err := cmd.Flags().GetInt64("soci-min-layer-size") if err != nil { return types.ImageConvertOptions{}, err } sociSpanSize, err := cmd.Flags().GetInt64("soci-span-size") if err != nil { return types.ImageConvertOptions{}, err } // #endregion // #region generic flags uncompress, err := cmd.Flags().GetBool("uncompress") if err != nil { return types.ImageConvertOptions{}, err } oci, err := cmd.Flags().GetBool("oci") if err != nil { return types.ImageConvertOptions{}, err } // #endregion // #region platform flags platforms, err := cmd.Flags().GetStringSlice("platform") if err != nil { return types.ImageConvertOptions{}, err } allPlatforms, err := cmd.Flags().GetBool("all-platforms") if err != nil { return types.ImageConvertOptions{}, err } // #endregion return types.ImageConvertOptions{ GOptions: globalOptions, Format: format, // #region generic flags Uncompress: uncompress, Oci: oci, // #endregion // #region platform flags Platforms: platforms, AllPlatforms: allPlatforms, // #endregion // Embed image format options EstargzOptions: types.EstargzOptions{ Estargz: estargz, EstargzRecordIn: estargzRecordIn, EstargzCompressionLevel: estargzCompressionLevel, EstargzChunkSize: estargzChunkSize, EstargzMinChunkSize: estargzMinChunkSize, EstargzExternalToc: estargzExternalTOC, EstargzKeepDiffID: estargzKeepDiffID, EstargzGzipHelper: estargzGzipHelper, }, ZstdOptions: types.ZstdOptions{ Zstd: zstd, ZstdCompressionLevel: zstdCompressionLevel, }, ZstdChunkedOptions: types.ZstdChunkedOptions{ ZstdChunked: zstdchunked, ZstdChunkedCompressionLevel: zstdChunkedCompressionLevel, ZstdChunkedChunkSize: zstdChunkedChunkSize, ZstdChunkedRecordIn: zstdChunkedRecordIn, }, NydusOptions: types.NydusOptions{ Nydus: nydus, NydusBuilderPath: nydusBuilderPath, NydusWorkDir: nydusWorkDir, NydusPrefetchPatterns: nydusPrefetchPatterns, NydusCompressor: nydusCompressor, }, OverlaybdOptions: types.OverlaybdOptions{ Overlaybd: overlaybd, OverlayFsType: overlaybdFsType, OverlaydbDBStr: overlaybdDbstr, }, SociConvertOptions: types.SociConvertOptions{ Soci: soci, SociOptions: types.SociOptions{ SpanSize: sociSpanSize, MinLayerSize: sociMinLayerSize, Platforms: platforms, AllPlatforms: allPlatforms, }, }, ProgressOutput: progressOutput, Stdout: cmd.OutOrStdout(), }, nil } func imageConvertAction(cmd *cobra.Command, args []string) error { options, err := convertOptions(cmd) if err != nil { return err } srcRawRef := args[0] destRawRef := args[1] client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return image.Convert(ctx, client, srcRawRef, destRawRef, options) } func imageConvertShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show image names return completion.ImageNames(cmd) } ================================================ FILE: cmd/nerdctl/image/image_convert_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "fmt" "testing" "time" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" ) func TestImageConvert(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.All( // FIXME: windows does not support stargz require.Not(require.Windows), require.Not(nerdtest.Docker), ), NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", "--all-platforms", testutil.CommonImage) }, SubTests: []*test.Case{ { Description: "esgz", Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier("converted-image")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("image", "convert", "--oci", "--estargz", testutil.CommonImage, data.Identifier("converted-image")) }, Expected: test.Expects(0, nil, nil), }, { Description: "nydus", Require: require.All( require.Binary("nydus-image"), ), Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier("converted-image")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("image", "convert", "--oci", "--nydus", testutil.CommonImage, data.Identifier("converted-image")) }, Expected: test.Expects(0, nil, nil), }, { Description: "zstd", Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier("converted-image")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("image", "convert", "--oci", "--zstd", "--zstd-compression-level", "3", testutil.CommonImage, data.Identifier("converted-image")) }, Expected: test.Expects(0, nil, nil), }, { Description: "zstdchunked", Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier("converted-image")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("image", "convert", "--oci", "--zstdchunked", "--zstdchunked-compression-level", "3", testutil.CommonImage, data.Identifier("converted-image")) }, Expected: test.Expects(0, nil, nil), }, { Description: "soci", NoParallel: true, Require: require.All( require.Not(nerdtest.Docker), nerdtest.Soci, nerdtest.SociVersion("0.10.0"), ), Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier("converted-image")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("image", "convert", "--soci", "--soci-span-size", "2097152", "--soci-min-layer-size", "0", testutil.CommonImage, data.Identifier("converted-image")) }, Expected: test.Expects(0, nil, nil), }, { Description: "soci with all-platforms", NoParallel: true, Require: require.All( require.Not(nerdtest.Docker), nerdtest.Soci, nerdtest.SociVersion("0.10.0"), ), Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier("converted-image")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("image", "convert", "--soci", "--all-platforms", "--soci-span-size", "2097152", "--soci-min-layer-size", "0", testutil.CommonImage, data.Identifier("converted-image")) }, Expected: test.Expects(0, nil, nil), }, }, } testCase.Run(t) } func TestImageConvertNydusVerify(t *testing.T) { nerdtest.Setup() const remoteImageKey = "remoteImageKey" var reg *registry.Server // It is unclear what is problematic here, but we use the kernel version to discriminate against EL // See: https://github.com/containerd/nerdctl/issues/4332 testutil.RequireKernelVersion(t, ">= 6.0.0-0") testCase := &test.Case{ Require: require.All( require.Linux, require.Binary("nydus-image"), require.Binary("nydusify"), require.Binary("nydusd"), require.Not(nerdtest.Docker), nerdtest.Rootful, nerdtest.Registry, ), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) reg = nerdtest.RegistryWithNoAuth(data, helpers, 0, false) reg.Setup(data, helpers) data.Labels().Set(remoteImageKey, fmt.Sprintf("%s:%d/nydusd-image:test", "localhost", reg.Port)) helpers.Ensure("image", "convert", "--nydus", "--oci", testutil.CommonImage, data.Identifier("converted-image")) helpers.Ensure("tag", data.Identifier("converted-image"), data.Labels().Get(remoteImageKey)) helpers.Ensure("push", data.Labels().Get(remoteImageKey)) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier("converted-image")) if reg != nil { reg.Cleanup(data, helpers) helpers.Anyhow("rmi", "-f", data.Labels().Get(remoteImageKey)) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Custom("nydusify", "check", "--work-dir", data.Temp().Dir("nydusify-temp"), "--source", testutil.CommonImage, "--target", data.Labels().Get(remoteImageKey), "--source-insecure", "--target-insecure", ) cmd.WithTimeout(30 * time.Second) return cmd }, Expected: test.Expects(0, nil, nil), } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/image/image_cryptutil.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/image" ) // registerImgcryptFlags register flags that correspond to parseImgcryptFlags(). // Platform flags are registered too. // // From: // - https://github.com/containerd/imgcrypt/blob/v1.1.2/cmd/ctr/commands/flags/flags.go#L23-L44 (except skip-decrypt-auth) // - https://github.com/containerd/imgcrypt/blob/v1.1.2/cmd/ctr/commands/images/encrypt.go#L52-L55 func registerImgcryptFlags(cmd *cobra.Command, encrypt bool) { flags := cmd.Flags() // #region platform flags // platform is defined as StringSlice, not StringArray, to allow specifying "--platform=amd64,arm64" flags.StringSlice("platform", []string{}, "Convert content for a specific platform") cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) flags.Bool("all-platforms", false, "Convert content for all platforms") // #endregion flags.String("gpg-homedir", "", "The GPG homedir to use; by default gpg uses ~/.gnupg") flags.String("gpg-version", "", "The GPG version (\"v1\" or \"v2\"), default will make an educated guess") flags.StringSlice("key", []string{}, "A secret key's filename and an optional password separated by colon; this option may be provided multiple times") // While --recipient can be specified only for `nerdctl image encrypt`, // --dec-recipient can be specified for both `nerdctl image encrypt` and `nerdctl image decrypt`. flags.StringSlice("dec-recipient", []string{}, "Recipient of the image; used only for PKCS7 and must be an x509 certificate") if encrypt { // recipient is defined as StringSlice, not StringArray, to allow specifying "--recipient=jwe:FILE1,jwe:FILE2" flags.StringSlice("recipient", []string{}, "Recipient of the image is the person who can decrypt it in the form specified above (i.e. jwe:/path/to/pubkey)") } } func cryptOptions(cmd *cobra.Command, args []string, encrypt bool) (types.ImageCryptOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ImageCryptOptions{}, err } platforms, err := cmd.Flags().GetStringSlice("platform") if err != nil { return types.ImageCryptOptions{}, err } allPlatforms, err := cmd.Flags().GetBool("all-platforms") if err != nil { return types.ImageCryptOptions{}, err } gpgHomeDir, err := cmd.Flags().GetString("gpg-homedir") if err != nil { return types.ImageCryptOptions{}, err } gpgVersion, err := cmd.Flags().GetString("gpg-version") if err != nil { return types.ImageCryptOptions{}, err } keys, err := cmd.Flags().GetStringSlice("key") if err != nil { return types.ImageCryptOptions{}, err } decRecipients, err := cmd.Flags().GetStringSlice("dec-recipient") if err != nil { return types.ImageCryptOptions{}, err } var recipients []string if encrypt { recipients, err = cmd.Flags().GetStringSlice("recipient") if err != nil { return types.ImageCryptOptions{}, err } } return types.ImageCryptOptions{ GOptions: globalOptions, Platforms: platforms, AllPlatforms: allPlatforms, GpgHomeDir: gpgHomeDir, GpgVersion: gpgVersion, Keys: keys, DecRecipients: decRecipients, Recipients: recipients, Stdout: cmd.OutOrStdout(), }, nil } func getImgcryptAction(encrypt bool) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { options, err := cryptOptions(cmd, args, encrypt) if err != nil { return err } srcRawRef := args[0] targetRawRef := args[1] client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return image.Crypt(ctx, client, srcRawRef, targetRawRef, encrypt, options) } } func imgcryptShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show image names return completion.ImageNames(cmd) } ================================================ FILE: cmd/nerdctl/image/image_decrypt.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "github.com/spf13/cobra" ) const imageDecryptHelp = `Decrypt an image locally. Use '--key' to specify the private keys. Private keys in PEM format may be encrypted and the password may be passed along in any of the following formats: - : - :pass= - :fd= (not available for rootless mode) - :filename= Use '--platform' to define the platforms to decrypt. Defaults to the host platform. When '--all-platforms' is given all images in a manifest list must be available. Unspecified platforms are omitted from the output image. Example (encrypt): openssl genrsa -out mykey.pem openssl rsa -in mykey.pem -pubout -out mypubkey.pem nerdctl image encrypt --recipient=jwe:mypubkey.pem --platform=linux/amd64,linux/arm64 foo example.com/foo:encrypted nerdctl push example.com/foo:encrypted Example (decrypt): nerdctl pull --unpack=false example.com/foo:encrypted nerdctl image decrypt --key=mykey.pem example.com/foo:encrypted foo:decrypted ` func decryptCommand() *cobra.Command { cmd := &cobra.Command{ Use: "decrypt [flags] ...", Short: "decrypt an image", Long: imageDecryptHelp, Args: cobra.MinimumNArgs(2), RunE: getImgcryptAction(false), ValidArgsFunction: imgcryptShellComplete, SilenceUsage: true, SilenceErrors: true, } registerImgcryptFlags(cmd, false) return cmd } ================================================ FILE: cmd/nerdctl/image/image_encrypt.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "github.com/spf13/cobra" ) const imageEncryptHelp = `Encrypt image layers. Use '--recipient' to specify the recipients. The following protocol prefixes are supported: - pgp: - jwe: - pkcs7: Use '--platform' to define the platforms to encrypt. Defaults to the host platform. When '--all-platforms' is given all images in a manifest list must be available. Unspecified platforms are omitted from the output image. Example: openssl genrsa -out mykey.pem openssl rsa -in mykey.pem -pubout -out mypubkey.pem nerdctl image encrypt --recipient=jwe:mypubkey.pem --platform=linux/amd64,linux/arm64 foo example.com/foo:encrypted nerdctl push example.com/foo:encrypted To run the encrypted image, put the private key file (mykey.pem) to /etc/containerd/ocicrypt/keys (rootful) or ~/.config/containerd/ocicrypt/keys (rootless). containerd before v1.4 requires extra configuration steps, see https://github.com/containerd/nerdctl/blob/main/docs/ocicrypt.md CAUTION: This command only encrypts image layers, but does NOT encrypt container configuration such as 'Env' and 'Cmd'. To see non-encrypted information, run 'nerdctl image inspect --mode=native --platform=PLATFORM example.com/foo:encrypted' . ` func encryptCommand() *cobra.Command { cmd := &cobra.Command{ Use: "encrypt [flags] ...", Short: "encrypt image layers", Long: imageEncryptHelp, Args: cobra.MinimumNArgs(2), RunE: getImgcryptAction(true), ValidArgsFunction: imgcryptShellComplete, SilenceUsage: true, SilenceErrors: true, } registerImgcryptFlags(cmd, true) return cmd } ================================================ FILE: cmd/nerdctl/image/image_encrypt_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "fmt" "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" ) func TestImageEncryptJWE(t *testing.T) { nerdtest.Setup() var reg *registry.Server const remoteImageKey = "remoteImageKey" testCase := &test.Case{ Require: require.All( require.Linux, require.Not(nerdtest.Docker), // This test needs to rmi the common image nerdtest.Private, nerdtest.Registry, ), Cleanup: func(data test.Data, helpers test.Helpers) { if reg != nil { reg.Cleanup(data, helpers) helpers.Anyhow("rmi", "-f", data.Labels().Get(remoteImageKey)) } helpers.Anyhow("rmi", "-f", data.Identifier("decrypted")) }, Setup: func(data test.Data, helpers test.Helpers) { pri, pub := nerdtest.GenerateJWEKeyPair(data, helpers) data.Labels().Set("private", pri) data.Labels().Set("public", pub) reg = nerdtest.RegistryWithNoAuth(data, helpers, 0, false) reg.Setup(data, helpers) helpers.Ensure("pull", "--quiet", testutil.CommonImage) encryptImageRef := fmt.Sprintf("127.0.0.1:%d/%s:encrypted", reg.Port, data.Identifier()) helpers.Ensure("image", "encrypt", "--recipient=jwe:"+pub, testutil.CommonImage, encryptImageRef) inspector := helpers.Capture("image", "inspect", "--mode=native", "--format={{len .Index.Manifests}}", encryptImageRef) assert.Equal(t, inspector, "1\n") inspector = helpers.Capture("image", "inspect", "--mode=native", "--format={{json .Manifest.Layers}}", encryptImageRef) assert.Assert(t, strings.Contains(inspector, "org.opencontainers.image.enc.keys.jwe")) helpers.Ensure("push", encryptImageRef) helpers.Anyhow("rmi", "-f", encryptImageRef) helpers.Anyhow("rmi", "-f", testutil.CommonImage) data.Labels().Set(remoteImageKey, encryptImageRef) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { helpers.Fail("pull", data.Labels().Get(remoteImageKey)) helpers.Ensure("pull", "--quiet", "--unpack=false", data.Labels().Get(remoteImageKey)) helpers.Fail("image", "decrypt", "--key="+data.Labels().Get("public"), data.Labels().Get(remoteImageKey), data.Identifier("decrypted")) // decryption needs prv key, not pub key return helpers.Command("image", "decrypt", "--key="+data.Labels().Get("private"), data.Labels().Get(remoteImageKey), data.Identifier("decrypted")) }, Expected: test.Expects(0, nil, nil), } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/image/image_history.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "bytes" "context" "errors" "fmt" "io" "os" "strconv" "text/tabwriter" "text/template" "time" "github.com/docker/go-units" "github.com/opencontainers/image-spec/identity" "github.com/spf13/cobra" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/formatter" "github.com/containerd/nerdctl/v2/pkg/idutil/imagewalker" "github.com/containerd/nerdctl/v2/pkg/imgutil" ) func HistoryCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "history [flags] IMAGE", Short: "Show the history of an image", Args: helpers.IsExactArgs(1), RunE: historyAction, ValidArgsFunction: historyShellComplete, SilenceUsage: true, SilenceErrors: true, } addHistoryFlags(cmd) return cmd } func addHistoryFlags(cmd *cobra.Command) { cmd.Flags().StringP("format", "f", "", "Format the output using the given Go template, e.g, '{{json .}}'") cmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"json"}, cobra.ShellCompDirectiveNoFileComp }) cmd.Flags().BoolP("quiet", "q", false, "Only show numeric IDs") cmd.Flags().BoolP("human", "H", true, "Print sizes and dates in human readable format (default true)") cmd.Flags().Bool("no-trunc", false, "Don't truncate output") } type historyPrintable struct { creationTime *time.Time size int64 Snapshot string CreatedAt string CreatedSince string CreatedBy string Size string Comment string } func historyAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err } defer cancel() walker := &imagewalker.ImageWalker{ Client: client, OnFound: func(ctx context.Context, found imagewalker.Found) error { if found.MatchCount > 1 { return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) } ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() img := containerd.NewImage(client, found.Image) imageConfig, _, err := imgutil.ReadImageConfig(ctx, img) if err != nil { return fmt.Errorf("failed to ReadImageConfig: %w", err) } configHistories := imageConfig.History layerCounter := 0 diffIDs, err := img.RootFS(ctx) if err != nil { return fmt.Errorf("failed to get diffIDS: %w", err) } var historys []historyPrintable for _, h := range configHistories { var size int64 var snapshotName string if !h.EmptyLayer { if len(diffIDs) <= layerCounter { return fmt.Errorf("too many non-empty layers in History section") } diffIDs := diffIDs[0 : layerCounter+1] chainID := identity.ChainID(diffIDs).String() s := client.SnapshotService(globalOptions.Snapshotter) stat, err := s.Stat(ctx, chainID) if err != nil { return fmt.Errorf("failed to get stat: %w", err) } use, err := s.Usage(ctx, chainID) if err != nil { return fmt.Errorf("failed to get usage: %w", err) } size = use.Size snapshotName = stat.Name layerCounter++ } else { size = 0 snapshotName = "" } history := historyPrintable{ creationTime: h.Created, size: size, Snapshot: snapshotName, CreatedBy: h.CreatedBy, Comment: h.Comment, } historys = append(historys, history) } err = printHistory(cmd, historys) if err != nil { return fmt.Errorf("failed printHistory: %w", err) } return nil }, } return walker.WalkAll(ctx, args, true) } type historyPrinter struct { w io.Writer quiet, noTrunc, human bool tmpl *template.Template } func printHistory(cmd *cobra.Command, historys []historyPrintable) error { quiet, err := cmd.Flags().GetBool("quiet") if err != nil { return err } noTrunc, err := cmd.Flags().GetBool("no-trunc") if err != nil { return err } human, err := cmd.Flags().GetBool("human") if err != nil { return err } var w io.Writer w = os.Stdout format, err := cmd.Flags().GetString("format") if err != nil { return err } var tmpl *template.Template switch format { case "", "table": w = tabwriter.NewWriter(w, 4, 8, 4, ' ', 0) if !quiet { fmt.Fprintln(w, "SNAPSHOT\tCREATED\tCREATED BY\tSIZE\tCOMMENT") } case "raw": return errors.New("unsupported format: \"raw\"") default: quiet = false var err error tmpl, err = formatter.ParseTemplate(format) if err != nil { return err } } printer := &historyPrinter{ w: w, quiet: quiet, noTrunc: noTrunc, human: human, tmpl: tmpl, } for index := len(historys) - 1; index >= 0; index-- { if err := printer.printHistory(historys[index]); err != nil { log.L.Warn(err) } } if f, ok := w.(formatter.Flusher); ok { return f.Flush() } return nil } func (x *historyPrinter) printHistory(printable historyPrintable) error { // Truncate long values unless --no-trunc is passed if !x.noTrunc { if len(printable.CreatedBy) > 45 { printable.CreatedBy = printable.CreatedBy[0:44] + "…" } // Do not truncate snapshot id if quiet is being passed if !x.quiet && len(printable.Snapshot) > 45 { printable.Snapshot = printable.Snapshot[0:44] + "…" } } // Format date and size for display based on --human preference printable.CreatedAt = printable.creationTime.Local().Format(time.RFC3339) if x.human { printable.CreatedSince = formatter.TimeSinceInHuman(*printable.creationTime) printable.Size = units.HumanSize(float64(printable.size)) } else { printable.CreatedSince = printable.CreatedAt printable.Size = strconv.FormatInt(printable.size, 10) } if x.tmpl != nil { var b bytes.Buffer if err := x.tmpl.Execute(&b, printable); err != nil { return err } if _, err := fmt.Fprintln(x.w, b.String()); err != nil { return err } } else if x.quiet { if _, err := fmt.Fprintln(x.w, printable.Snapshot); err != nil { return err } } else { if _, err := fmt.Fprintf(x.w, "%s\t%s\t%s\t%s\t%s\n", printable.Snapshot, printable.CreatedSince, printable.CreatedBy, printable.Size, printable.Comment, ); err != nil { return err } } return nil } func historyShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show image names return completion.ImageNames(cmd) } ================================================ FILE: cmd/nerdctl/image/image_history_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "encoding/json" "errors" "io" "strings" "testing" "time" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/formatter" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) type historyObj struct { Snapshot string CreatedAt string CreatedSince string CreatedBy string Size string Comment string } const createdAt1 = "2021-03-31T10:21:21-07:00" const createdAt2 = "2021-03-31T10:21:23-07:00" // Expected content of the common image on arm64 var ( createdAtTime, _ = time.Parse(time.RFC3339, createdAt2) expectedHistory = []historyObj{ { CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", Size: "0B", CreatedAt: createdAt2, Snapshot: "", Comment: "", CreatedSince: formatter.TimeSinceInHuman(createdAtTime), }, { CreatedBy: "/bin/sh -c #(nop) ADD file:3b16ffee2b26d8af5…", Size: "5.947MB", CreatedAt: createdAt1, Snapshot: "sha256:56bf55b8eed1f0b4794a30386e4d1d3da949c…", Comment: "", CreatedSince: formatter.TimeSinceInHuman(createdAtTime), }, } expectedHistoryNoTrunc = []historyObj{ { Snapshot: "", Size: "0", }, { Snapshot: "sha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a", CreatedBy: "/bin/sh -c #(nop) ADD file:3b16ffee2b26d8af5db152fcc582aaccd9e1ec9e3343874e9969a205550fe07d in / ", Size: "5947392", }, } ) func decode(stdout string) ([]historyObj, error) { dec := json.NewDecoder(strings.NewReader(stdout)) object := []historyObj{} for { var v historyObj if err := dec.Decode(&v); err == io.EOF { break } else if err != nil { return nil, errors.New("failed to decode history object") } object = append(object, v) } return object, nil } func TestImageHistory(t *testing.T) { // Here are the current issues with regard to docker true compatibility: // - we have a different definition of what a layer id is (snapshot vs. id) // this will require indepth convergence when moby will handle multi-platform images // - our definition of size is different // this requires some investigation to figure out why it differs // possibly one is unpacked on the filessystem while the other is the tar file size? // - we do not truncate ids when --quiet has been provided // this is a conscious decision here - truncating with --quiet does not make much sense nerdtest.Setup() testCase := &test.Case{ Require: require.All( require.Not(nerdtest.Docker), // XXX the results here are obviously platform dependent - and it seems like windows cannot pull a linux image? require.Not(require.Windows), // XXX Currently, history does not work on non-native platform, so, we cannot test reliably on other platforms require.Arm64, ), Setup: func(data test.Data, helpers test.Helpers) { // XXX: despite efforts to isolate this test, it keeps on having side effects linked to // https://github.com/containerd/nerdctl/issues/3512 // Isolating it into a completely different root is the last ditched attempt at avoiding the issue helpers.Write(nerdtest.DataRoot, test.ConfigValue(data.Temp().Path())) helpers.Ensure("pull", "--quiet", "--platform", "linux/arm64", testutil.CommonImage) }, SubTests: []*test.Case{ { Description: "trunc, no quiet, human", Command: test.Command("image", "history", "--human=true", "--format=json", testutil.CommonImage), Expected: test.Expects(0, nil, func(stdout string, t tig.T) { history, err := decode(stdout) assert.NilError(t, err, "decode should not fail") assert.Equal(t, len(history), 2, "history should be 2 in length") h0Time, _ := time.Parse(time.RFC3339, history[0].CreatedAt) h1Time, _ := time.Parse(time.RFC3339, history[1].CreatedAt) comp0Time, _ := time.Parse(time.RFC3339, expectedHistory[0].CreatedAt) comp1Time, _ := time.Parse(time.RFC3339, expectedHistory[1].CreatedAt) assert.Equal(t, h0Time.UTC().String(), comp0Time.UTC().String()) assert.Equal(t, history[0].CreatedBy, expectedHistory[0].CreatedBy) assert.Equal(t, history[0].Size, expectedHistory[0].Size) assert.Equal(t, history[0].CreatedSince, expectedHistory[0].CreatedSince) assert.Equal(t, history[0].Snapshot, expectedHistory[0].Snapshot) assert.Equal(t, history[0].Comment, expectedHistory[0].Comment) assert.Equal(t, h1Time.UTC().String(), comp1Time.UTC().String()) assert.Equal(t, history[1].CreatedBy, expectedHistory[1].CreatedBy) assert.Equal(t, history[1].Size, expectedHistory[1].Size) assert.Equal(t, history[1].CreatedSince, expectedHistory[1].CreatedSince) assert.Equal(t, history[1].Snapshot, expectedHistory[1].Snapshot) assert.Equal(t, history[1].Comment, expectedHistory[1].Comment) }), }, { Description: "no human - dates and sizes are not prettyfied", Command: test.Command("image", "history", "--human=false", "--format=json", testutil.CommonImage), Expected: test.Expects(0, nil, func(stdout string, t tig.T) { history, err := decode(stdout) assert.NilError(t, err, "decode should not fail") assert.Equal(t, history[0].Size, expectedHistoryNoTrunc[0].Size) assert.Equal(t, history[0].CreatedSince, history[0].CreatedAt) assert.Equal(t, history[1].Size, expectedHistoryNoTrunc[1].Size) assert.Equal(t, history[1].CreatedSince, history[1].CreatedAt) }), }, { Description: "no trunc - do not truncate sha or cmd", Command: test.Command("image", "history", "--human=false", "--no-trunc", "--format=json", testutil.CommonImage), Expected: test.Expects(0, nil, func(stdout string, t tig.T) { history, err := decode(stdout) assert.NilError(t, err, "decode should not fail") assert.Equal(t, history[1].Snapshot, expectedHistoryNoTrunc[1].Snapshot) assert.Equal(t, history[1].CreatedBy, expectedHistoryNoTrunc[1].CreatedBy) }), }, { Description: "Quiet has no effect with format, so, go no-json, no-trunc", Command: test.Command("image", "history", "--human=false", "--no-trunc", "--quiet", testutil.CommonImage), Expected: test.Expects(0, nil, func(stdout string, t tig.T) { assert.Equal(t, stdout, expectedHistoryNoTrunc[0].Snapshot+"\n"+expectedHistoryNoTrunc[1].Snapshot+"\n") }), }, { Description: "With quiet, trunc has no effect", Command: test.Command("image", "history", "--human=false", "--no-trunc", "--quiet", testutil.CommonImage), Expected: test.Expects(0, nil, func(stdout string, t tig.T) { assert.Equal(t, stdout, expectedHistoryNoTrunc[0].Snapshot+"\n"+expectedHistoryNoTrunc[1].Snapshot+"\n") }), }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/image/image_import.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "fmt" "io" "net/http" "os" "strings" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/image" ) func ImportCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "import [OPTIONS] file|URL|- [REPOSITORY[:TAG]]", Short: "Import the contents from a tarball to create a filesystem image", Args: cobra.MinimumNArgs(1), RunE: importAction, ValidArgsFunction: imageImportShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().StringP("message", "m", "", "Set commit message for imported image") cmd.Flags().String("platform", "", "Set platform for imported image (e.g., linux/amd64)") return cmd } func importOptions(cmd *cobra.Command, args []string) (types.ImageImportOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ImageImportOptions{}, err } message, err := cmd.Flags().GetString("message") if err != nil { return types.ImageImportOptions{}, err } platform, err := cmd.Flags().GetString("platform") if err != nil { return types.ImageImportOptions{}, err } var reference string if len(args) > 1 { reference = args[1] } var in io.ReadCloser src := args[0] switch { case src == "-": in = io.NopCloser(cmd.InOrStdin()) case hasHTTPPrefix(src): resp, err := http.Get(src) if err != nil { return types.ImageImportOptions{}, err } if resp.StatusCode < 200 || resp.StatusCode >= 300 { defer resp.Body.Close() return types.ImageImportOptions{}, fmt.Errorf("failed to download %s: %s", src, resp.Status) } in = resp.Body default: f, err := os.Open(src) if err != nil { return types.ImageImportOptions{}, err } in = f } return types.ImageImportOptions{ Stdout: cmd.OutOrStdout(), Stdin: in, GOptions: globalOptions, Source: args[0], Reference: reference, Message: message, Platform: platform, }, nil } func importAction(cmd *cobra.Command, args []string) error { opt, err := importOptions(cmd, args) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), opt.GOptions.Namespace, opt.GOptions.Address) if err != nil { return err } defer cancel() defer func() { if rc, ok := opt.Stdin.(io.ReadCloser); ok { _ = rc.Close() } }() name, err := image.Import(ctx, client, opt) if err != nil { return err } _, err = cmd.OutOrStdout().Write([]byte(name + "\n")) return err } func imageImportShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.ImageNames(cmd) } func hasHTTPPrefix(s string) bool { return strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://") } ================================================ FILE: cmd/nerdctl/image/image_import_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "archive/tar" "bytes" "errors" "net/http" "os" "path/filepath" "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) // minimalRootfsTar returns a valid tar archive with no files. func minimalRootfsTar(t *testing.T) *bytes.Buffer { t.Helper() buf := new(bytes.Buffer) tw := tar.NewWriter(buf) assert.NilError(t, tw.Close()) return buf } func TestImageImportErrors(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Description: "TestImageImportErrors", Require: require.Linux, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("import", "", "image:tag") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New(data.Labels().Get("error"))}, } }, Data: test.WithLabels(map[string]string{ "error": "no such file or directory", }), } testCase.Run(t) } func TestImageImport(t *testing.T) { testCase := nerdtest.Setup() var stopServer func() testCase.SubTests = []*test.Case{ { Description: "image import from stdin", Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("import", "-", data.Identifier()) cmd.Feed(bytes.NewReader(minimalRootfsTar(t).Bytes())) return cmd }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { identifier := data.Identifier() return &test.Expected{ Output: expect.All( func(stdout string, t tig.T) { imgs := helpers.Capture("images") assert.Assert(t, strings.Contains(imgs, identifier)) }, ), } }, }, { Description: "image import from file", Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Setup: func(data test.Data, helpers test.Helpers) { p := filepath.Join(data.Temp().Path(), "rootfs.tar") assert.NilError(t, os.WriteFile(p, minimalRootfsTar(t).Bytes(), 0644)) data.Labels().Set("tar", p) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("import", data.Labels().Get("tar"), data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { identifier := data.Identifier() return &test.Expected{ Output: expect.All( func(stdout string, t tig.T) { imgs := helpers.Capture("images") assert.Assert(t, strings.Contains(imgs, identifier)) }, ), } }, }, { Description: "image import with message", Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("import", "-m", "A message", "-", data.Identifier()) cmd.Feed(bytes.NewReader(minimalRootfsTar(t).Bytes())) return cmd }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { identifier := data.Identifier() + ":latest" return &test.Expected{ Output: expect.All( func(stdout string, t tig.T) { img := nerdtest.InspectImage(helpers, identifier) assert.Equal(t, img.Comment, "A message") }, ), } }, }, { Description: "image import with platform", Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("import", "--platform", "linux/amd64", "-", data.Identifier()) cmd.Feed(bytes.NewReader(minimalRootfsTar(t).Bytes())) return cmd }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { identifier := data.Identifier() + ":latest" return &test.Expected{ Output: expect.All( func(stdout string, t tig.T) { img := nerdtest.InspectImage(helpers, identifier) assert.Equal(t, img.Architecture, "amd64") assert.Equal(t, img.Os, "linux") }, ), } }, }, { Description: "image import from URL", Cleanup: func(data test.Data, helpers test.Helpers) { if stopServer != nil { stopServer() stopServer = nil } helpers.Anyhow("rmi", "-f", data.Identifier()) }, Setup: func(data test.Data, helpers test.Helpers) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/x-tar") _, _ = w.Write(minimalRootfsTar(t).Bytes()) }) url, stop, err := nerdtest.StartHTTPServer(handler) assert.NilError(t, err) stopServer = stop data.Labels().Set("url", url) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("import", data.Labels().Get("url"), data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { identifier := data.Identifier() return &test.Expected{ Output: expect.All( func(stdout string, t tig.T) { imgs := helpers.Capture("images") assert.Assert(t, strings.Contains(imgs, identifier)) }, ), } }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/image/image_inspect.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "fmt" "github.com/spf13/cobra" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/image" "github.com/containerd/nerdctl/v2/pkg/formatter" ) func inspectCommand() *cobra.Command { cmd := &cobra.Command{ Use: "inspect [flags] IMAGE [IMAGE...]", Args: cobra.MinimumNArgs(1), Short: "Display detailed information on one or more images.", Long: "Hint: set `--mode=native` for showing the full output", RunE: imageInspectAction, ValidArgsFunction: imageInspectShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().String("mode", "dockercompat", `Inspect mode, "dockercompat" for Docker-compatible output, "native" for containerd-native output`) cmd.RegisterFlagCompletionFunc("mode", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"dockercompat", "native"}, cobra.ShellCompDirectiveNoFileComp }) cmd.Flags().StringP("format", "f", "", "Format the output using the given Go template, e.g, '{{json .}}'") cmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"json"}, cobra.ShellCompDirectiveNoFileComp }) // #region platform flags cmd.Flags().String("platform", "", "Inspect a specific platform") // not a slice, and there is no --all-platforms cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) // #endregion return cmd } func InspectOptions(cmd *cobra.Command, platform *string) (types.ImageInspectOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ImageInspectOptions{}, err } mode, err := cmd.Flags().GetString("mode") if err != nil { return types.ImageInspectOptions{}, err } format, err := cmd.Flags().GetString("format") if err != nil { return types.ImageInspectOptions{}, err } if platform == nil { tempPlatform, err := cmd.Flags().GetString("platform") if err != nil { return types.ImageInspectOptions{}, err } platform = &tempPlatform } return types.ImageInspectOptions{ GOptions: globalOptions, Mode: mode, Format: format, Platform: *platform, Stdout: cmd.OutOrStdout(), }, nil } func imageInspectAction(cmd *cobra.Command, args []string) error { options, err := InspectOptions(cmd, nil) if err != nil { return err } // Verify we have a valid mode if options.Mode != "native" && options.Mode != "dockercompat" { return fmt.Errorf("unknown mode %q", options.Mode) } client, ctx, cancel, err := clientutil.NewClientWithPlatform(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address, options.Platform) if err != nil { return err } defer cancel() entries, err := image.Inspect(ctx, client, args, options) if err != nil { return err } // Display if len(entries) > 0 { if formatErr := formatter.FormatSlice(options.Format, options.Stdout, entries); formatErr != nil { log.G(ctx).Error(formatErr) } } return err } func imageInspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show image names return completion.ImageNames(cmd) } ================================================ FILE: cmd/nerdctl/image/image_inspect_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "encoding/json" "errors" "fmt" "runtime" "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestImageInspectSimpleCases(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) }, SubTests: []*test.Case{ { Description: "Contains some stuff", Command: test.Command("image", "inspect", testutil.CommonImage), Expected: test.Expects(0, nil, func(stdout string, t tig.T) { var dc []dockercompat.Image err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") assert.Assert(t, len(dc[0].RootFS.Layers) > 0, "there should be at least one rootfs layer\n") assert.Assert(t, dc[0].Architecture != "", "architecture should be set\n") assert.Assert(t, dc[0].Size > 0, "size should be > 0 \n") }), }, { Description: "RawFormat support (.Id)", Command: test.Command("image", "inspect", testutil.CommonImage, "--format", "{{.Id}}"), Expected: test.Expects(0, nil, nil), }, { Description: "typedFormat support (.ID)", Command: test.Command("image", "inspect", testutil.CommonImage, "--format", "{{.ID}}"), Expected: test.Expects(0, nil, nil), }, { Description: "Config.Image field is set", Command: test.Command("image", "inspect", testutil.CommonImage), Expected: test.Expects(0, nil, func(stdout string, t tig.T) { var dc []dockercompat.Image err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") assert.Assert(t, dc[0].Config != nil, "image Config should not be nil") assert.Assert(t, dc[0].Config.Image != "", "Config.Image should not be empty") }), }, { Description: "Error for image not found", Command: test.Command("image", "inspect", "dne:latest", "dne2:latest"), Expected: test.Expects(1, []error{ errors.New("no such image: dne:latest"), errors.New("no such image: dne2:latest"), }, nil), }, }, } if runtime.GOOS == "windows" { testCase.Require = nerdtest.IsFlaky("https://github.com/containerd/nerdctl/issues/3524") } testCase.Run(t) } func TestImageInspectDifferentValidReferencesForTheSameImage(t *testing.T) { nerdtest.Setup() tags := []string{ "", ":latest", } names := []string{ "busybox", "docker.io/library/busybox", "registry-1.docker.io/library/busybox", } testCase := &test.Case{ Require: require.All( require.Not(nerdtest.Docker), // FIXME: this test depends on hub images that do not have windows versions require.Not(require.Windows), // We need a clean slate nerdtest.Private, ), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", "alpine") helpers.Ensure("pull", "--quiet", "busybox") helpers.Ensure("pull", "--quiet", "registry-1.docker.io/library/busybox") }, SubTests: []*test.Case{ { Description: "name and tags +/- sha combinations", Command: test.Command("image", "inspect", "busybox"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var dc []dockercompat.Image err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") reference := dc[0].ID sha := strings.TrimPrefix(dc[0].RepoDigests[0], "busybox@sha256:") for _, name := range names { for _, tag := range tags { it := nerdtest.InspectImage(helpers, name+tag) assert.Equal(t, it.ID, reference) it = nerdtest.InspectImage(helpers, name+tag+"@sha256:"+sha) assert.Equal(t, it.ID, reference) } } }, } }, }, { Description: "by digest, short or long, with or without prefix", Command: test.Command("image", "inspect", "busybox"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var dc []dockercompat.Image err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") reference := dc[0].ID sha := strings.TrimPrefix(dc[0].RepoDigests[0], "busybox@sha256:") for _, id := range []string{"sha256:" + sha, sha, sha[0:8], "sha256:" + sha[0:8]} { it := nerdtest.InspectImage(helpers, id) assert.Equal(t, it.ID, reference) } // Now, tag alpine with a short id // Build reference values for comparison alpine := nerdtest.InspectImage(helpers, "alpine") // Demonstrate image name precedence over digest lookup // Using the shortened sha should no longer get busybox, but rather the newly tagged Alpine // FIXME: this is triggering https://github.com/containerd/nerdctl/issues/3016 // We cannot get rid of that image now, which does break local testing helpers.Ensure("tag", "alpine", sha[0:8]) it := nerdtest.InspectImage(helpers, sha[0:8]) assert.Equal(t, it.ID, alpine.ID) }, } }, }, { Description: "prove that wrong references with correct digest do not get resolved", Command: test.Command("image", "inspect", "busybox"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var dc []dockercompat.Image err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") sha := strings.TrimPrefix(dc[0].RepoDigests[0], "busybox@sha256:") for _, id := range []string{"doesnotexist", "doesnotexist:either", "busybox:bogustag"} { cmd := helpers.Command("image", "inspect", id+"@sha256:"+sha) cmd.Run(&test.Expected{ ExitCode: 1, Errors: []error{fmt.Errorf("no such image: %s@sha256:%s", id, sha)}, }) } }, } }, }, { Description: "prove that invalid reference return no result without crashing", Command: test.Command("image", "inspect", "busybox"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var dc []dockercompat.Image err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") for _, id := range []string{"∞∞∞∞∞∞∞∞∞∞", "busybox:∞∞∞∞∞∞∞∞∞∞"} { cmd := helpers.Command("image", "inspect", id) cmd.Run(&test.Expected{ ExitCode: 1, Errors: []error{fmt.Errorf("invalid reference format: %s", id)}, }) } }, } }, }, { Description: "retrieving multiple entries at once", Command: test.Command("image", "inspect", "busybox", "busybox"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var dc []dockercompat.Image err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") assert.Equal(t, 2, len(dc), "Unexpectedly did not get 2 results\n") reference := nerdtest.InspectImage(helpers, "busybox") assert.Equal(t, dc[0].ID, reference.ID) assert.Equal(t, dc[1].ID, reference.ID) }, } }, }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/image/image_list.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "fmt" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/image" "github.com/containerd/nerdctl/v2/pkg/referenceutil" ) func ImagesCommand() *cobra.Command { shortHelp := "List images" longHelp := shortHelp + ` Properties: - REPOSITORY: Repository - TAG: Tag - NAME: Name of the image, --names for skip parsing as repository and tag. - IMAGE ID: OCI Digest. Usually different from Docker image ID. Shared for multi-platform images. - CREATED: Created time - PLATFORM: Platform - SIZE: Size of the unpacked snapshots - BLOB SIZE: Size of the blobs (such as layer tarballs) in the content store ` var cmd = &cobra.Command{ Use: "images [flags] [REPOSITORY[:TAG]]", Short: shortHelp, Long: longHelp, Args: cobra.MaximumNArgs(1), RunE: imagesAction, ValidArgsFunction: imagesShellComplete, SilenceUsage: true, SilenceErrors: true, DisableFlagsInUseLine: true, } cmd.Flags().BoolP("quiet", "q", false, "Only show numeric IDs") cmd.Flags().Bool("no-trunc", false, "Don't truncate output") // Alias "-f" is reserved for "--filter" cmd.Flags().String("format", "", "Format the output using the given Go template, e.g, '{{json .}}', 'wide'") cmd.Flags().StringSliceP("filter", "f", []string{}, "Filter output based on conditions provided") cmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"json", "table", "wide"}, cobra.ShellCompDirectiveNoFileComp }) cmd.Flags().Bool("digests", false, "Show digests (compatible with Docker, unlike ID)") cmd.Flags().Bool("names", false, "Show image names") cmd.Flags().BoolP("all", "a", true, "(unimplemented yet, always true)") return cmd } func listOptions(cmd *cobra.Command, args []string) (*types.ImageListOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return nil, err } var filters []string if len(args) > 0 { parsedReference, err := referenceutil.Parse(args[0]) if err != nil { return nil, err } filters = []string{fmt.Sprintf("name==%s", parsedReference)} } quiet, err := cmd.Flags().GetBool("quiet") if err != nil { return nil, err } noTrunc, err := cmd.Flags().GetBool("no-trunc") if err != nil { return nil, err } format, err := cmd.Flags().GetString("format") if err != nil { return nil, err } var inputFilters []string if cmd.Flags().Changed("filter") { inputFilters, err = cmd.Flags().GetStringSlice("filter") if err != nil { return nil, err } } digests, err := cmd.Flags().GetBool("digests") if err != nil { return nil, err } names, err := cmd.Flags().GetBool("names") if err != nil { return nil, err } return &types.ImageListOptions{ GOptions: globalOptions, Quiet: quiet, NoTrunc: noTrunc, Format: format, Filters: inputFilters, NameAndRefFilter: filters, Digests: digests, Names: names, All: true, Stdout: cmd.OutOrStdout(), }, nil } func imagesAction(cmd *cobra.Command, args []string) error { options, err := listOptions(cmd, args) if err != nil { return err } if !options.All { options.All = true } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return image.ListCommandHandler(ctx, client, options) } func imagesShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) == 0 { // show image names return completion.ImageNames(cmd) } return nil, cobra.ShellCompDirectiveNoFileComp } ================================================ FILE: cmd/nerdctl/image/image_list_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "errors" "fmt" "regexp" "runtime" "slices" "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/referenceutil" "github.com/containerd/nerdctl/v2/pkg/tabutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestImages(t *testing.T) { nerdtest.Setup() commonImage, _ := referenceutil.Parse(testutil.CommonImage) testCase := &test.Case{ Require: require.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", commonImage.String()) helpers.Ensure("pull", "--quiet", testutil.NginxAlpineImage) }, SubTests: []*test.Case{ { Description: "No params", Command: test.Command("images"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 2, "there should be at least two lines\n") header := "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tPLATFORM\tSIZE\tBLOB SIZE" if nerdtest.IsDocker() { header = "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tSIZE" } tab := tabutil.NewReader(header) err := tab.ParseHeader(lines[0]) assert.NilError(t, err, "ParseHeader should not fail\n") found := false for _, line := range lines[1:] { repo, _ := tab.ReadRow(line, "REPOSITORY") tag, _ := tab.ReadRow(line, "TAG") if repo+":"+tag == commonImage.FamiliarName()+":"+commonImage.Tag { found = true break } } assert.Assert(t, found, "we should have found an image\n") }, } }, }, { Description: "With names", Command: test.Command("images", "--names", commonImage.String()), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( expect.Contains(commonImage.String()), func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 2, "there should be at least two lines\n") tab := tabutil.NewReader("NAME\tIMAGE ID\tCREATED\tPLATFORM\tSIZE\tBLOB SIZE") err := tab.ParseHeader(lines[0]) assert.NilError(t, err, "ParseHeader should not fail\n") found := false for _, line := range lines[1:] { name, _ := tab.ReadRow(line, "NAME") if name == commonImage.String() { found = true break } } assert.Assert(t, found, "we should have found an image\n") }, ), } }, }, { Description: "CheckCreatedTime", Command: test.Command("images", "--format", "'{{json .CreatedAt}}'"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 2, "there should be at least two lines\n") createdTimes := lines slices.Reverse(createdTimes) assert.Assert(t, slices.IsSorted(createdTimes), "created times should be sorted\n") }, } }, }, }, } if runtime.GOOS == "windows" { testCase.Require = require.All( testCase.Require, nerdtest.IsFlaky("https://github.com/containerd/nerdctl/issues/3524"), ) } testCase.Run(t) } func TestImagesFilter(t *testing.T) { nerdtest.Setup() commonImage, _ := referenceutil.Parse(testutil.CommonImage) testCase := &test.Case{ Require: nerdtest.Build, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", commonImage.String()) helpers.Ensure("tag", commonImage.String(), "taggedimage:one-fragment-one") helpers.Ensure("tag", commonImage.String(), "taggedimage:two-fragment-two") dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-test-string"] \n LABEL foo=bar LABEL version=0.1 RUN echo "actually creating a layer so that docker sets the createdAt time" `, commonImage.String()) buildCtx := data.Temp().Path() data.Temp().Save(dockerfile, "Dockerfile") data.Labels().Set("buildCtx", buildCtx) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", "taggedimage:one-fragment-one") helpers.Anyhow("rmi", "-f", "taggedimage:two-fragment-two") helpers.Anyhow("rmi", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { data.Labels().Set("builtImageID", data.Identifier()) return helpers.Command("build", "-t", data.Identifier(), data.Labels().Get("buildCtx")) }, Expected: test.Expects(0, nil, nil), SubTests: []*test.Case{ { Description: "label=foo=bar", Command: test.Command("images", "--filter", "label=foo=bar"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.Contains(data.Labels().Get("builtImageID")), } }, }, { Description: "label=foo=bar1", Command: test.Command("images", "--filter", "label=foo=bar1"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.DoesNotContain(data.Labels().Get("builtImageID")), } }, }, { Description: "label=foo=bar label=version=0.1", Command: test.Command("images", "--filter", "label=foo=bar", "--filter", "label=version=0.1"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.Contains(data.Labels().Get("builtImageID")), } }, }, { Description: "label=foo=bar label=version=0.2", Command: test.Command("images", "--filter", "label=foo=bar", "--filter", "label=version=0.2"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.DoesNotContain(data.Labels().Get("builtImageID")), } }, }, { Description: "label=version", Command: test.Command("images", "--filter", "label=version"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.Contains(data.Labels().Get("builtImageID")), } }, }, { Description: "reference=ID*", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("images", "--filter", fmt.Sprintf("reference=%s*", data.Labels().Get("builtImageID"))) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.Contains(data.Labels().Get("builtImageID")), } }, }, { Description: "reference=tagged*:*fragment*", Command: test.Command("images", "--filter", "reference=tagged*:*fragment*"), Expected: test.Expects( 0, nil, expect.Contains("one-", "two-"), ), }, { Description: "before=ID:latest", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("images", "--filter", fmt.Sprintf("before=%s:latest", data.Labels().Get("builtImageID"))) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( expect.Contains(commonImage.FamiliarName(), commonImage.Tag), expect.DoesNotContain(data.Labels().Get("builtImageID")), ), } }, }, { Description: "since=" + commonImage.String(), Command: test.Command("images", "--filter", fmt.Sprintf("since=%s", commonImage.String())), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( expect.Contains(data.Labels().Get("builtImageID")), expect.DoesNotMatch(regexp.MustCompile(commonImage.FamiliarName()+"[\\s]+"+commonImage.Tag)), ), } }, }, { Description: "since=" + commonImage.String() + " " + commonImage.String(), Command: test.Command("images", "--filter", fmt.Sprintf("since=%s", commonImage.String()), commonImage.String()), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( expect.DoesNotContain(data.Labels().Get("builtImageID")), expect.DoesNotMatch(regexp.MustCompile(commonImage.FamiliarName()+"[\\s]+"+commonImage.Tag)), ), } }, }, { Description: "since=non-exists-image", Command: test.Command("images", "--filter", "since=non-exists-image"), Expected: test.Expects(expect.ExitCodeGenericFail, []error{errors.New("no such image: ")}, nil), }, { Description: "before=non-exists-image", Command: test.Command("images", "--filter", "before=non-exists-image"), Expected: test.Expects(expect.ExitCodeGenericFail, []error{errors.New("no such image: ")}, nil), }, }, } testCase.Run(t) } func TestImagesFilterDangling(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Description: "TestImagesFilterDangling", // This test relies on a clean slate and the ability to GC everything NoParallel: true, Require: nerdtest.Build, Setup: func(data test.Data, helpers test.Helpers) { dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-notag-string"] `, testutil.CommonImage) buildCtx := data.Temp().Path() data.Temp().Save(dockerfile, "Dockerfile") data.Labels().Set("buildCtx", buildCtx) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("container", "prune", "-f") helpers.Anyhow("image", "prune", "--all", "-f") }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("build", data.Labels().Get("buildCtx")) }, Expected: test.Expects(0, nil, nil), SubTests: []*test.Case{ { Description: "dangling", Command: test.Command("images", "--filter", "dangling=true"), Expected: test.Expects(0, nil, expect.Contains("")), }, { Description: "not dangling", Command: test.Command("images", "--filter", "dangling=false"), Expected: test.Expects(0, nil, expect.DoesNotContain("")), }, }, } testCase.Run(t) } func TestImagesKubeWithKubeHideDupe(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.All( nerdtest.OnlyKubernetes, ), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.BusyboxImage) }, SubTests: []*test.Case{ { Description: "The same imageID will not print no-repo:tag in k8s.io with kube-hide-dupe", Command: test.Command("--kube-hide-dupe", "images"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var imageID string var skipLine int lines := strings.Split(strings.TrimSpace(stdout), "\n") header := "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tPLATFORM\tSIZE\tBLOB SIZE" if nerdtest.IsDocker() { header = "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tSIZE" } tab := tabutil.NewReader(header) err := tab.ParseHeader(lines[0]) assert.NilError(t, err, "ParseHeader should not fail\n") found := true for i, line := range lines[1:] { repo, _ := tab.ReadRow(line, "REPOSITORY") tag, _ := tab.ReadRow(line, "TAG") if repo+":"+tag == testutil.BusyboxImage { skipLine = i imageID, _ = tab.ReadRow(line, "IMAGE ID") break } } for i, line := range lines[1:] { if i == skipLine { continue } id, _ := tab.ReadRow(line, "IMAGE ID") if id == imageID { found = false break } } assert.Assert(t, found, "We should have found the image\n") }, } }, }, { Description: "the same imageId will print no-repo:tag in k8s.io without kube-hide-dupe", Command: test.Command("images"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.Contains(""), } }, }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/image/image_load.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/imgutil/load" ) func LoadCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "load", Args: cobra.NoArgs, Short: "Load an image from a tar archive or STDIN", Long: "Supports both Docker Image Spec v1.2 and OCI Image Spec v1.0.", RunE: loadAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().StringP("input", "i", "", "Read from tar archive file, instead of STDIN") cmd.Flags().BoolP("quiet", "q", false, "Suppress the load output") // #region platform flags // platform is defined as StringSlice, not StringArray, to allow specifying "--platform=amd64,arm64" cmd.Flags().StringSlice("platform", []string{}, "Import content for a specific platform") cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) cmd.Flags().Bool("all-platforms", false, "Import content for all platforms") // #endregion return cmd } func processLoadCommandFlags(cmd *cobra.Command) (types.ImageLoadOptions, error) { input, err := cmd.Flags().GetString("input") if err != nil { return types.ImageLoadOptions{}, err } globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ImageLoadOptions{}, err } allPlatforms, err := cmd.Flags().GetBool("all-platforms") if err != nil { return types.ImageLoadOptions{}, err } platform, err := cmd.Flags().GetStringSlice("platform") if err != nil { return types.ImageLoadOptions{}, err } quiet, err := cmd.Flags().GetBool("quiet") if err != nil { return types.ImageLoadOptions{}, err } return types.ImageLoadOptions{ GOptions: globalOptions, Input: input, Platform: platform, AllPlatforms: allPlatforms, Stdout: cmd.OutOrStdout(), Stdin: cmd.InOrStdin(), Quiet: quiet, }, nil } func loadAction(cmd *cobra.Command, _ []string) error { options, err := processLoadCommandFlags(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() _, err = load.FromArchive(ctx, client, options) return err } ================================================ FILE: cmd/nerdctl/image/image_load_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "os" "path/filepath" "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestLoadStdinFromPipe(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Description: "TestLoadStdinFromPipe", Require: require.Linux, Setup: func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() helpers.Ensure("pull", "--quiet", testutil.CommonImage) helpers.Ensure("tag", testutil.CommonImage, identifier) helpers.Ensure("save", identifier, "-o", filepath.Join(data.Temp().Path(), "common.tar")) helpers.Ensure("rmi", "-f", identifier) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("load") reader, err := os.Open(filepath.Join(data.Temp().Path(), "common.tar")) assert.NilError(t, err, "failed to open common.tar") cmd.Feed(reader) return cmd }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { identifier := data.Identifier() return &test.Expected{ Output: expect.All( expect.Contains(identifier), func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(helpers.Capture("images"), identifier)) }, ), } }, } testCase.Run(t) } func TestLoadStdinEmpty(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Description: "TestLoadStdinEmpty", Require: require.Linux, Command: test.Command("load"), Expected: test.Expects(1, nil, nil), } testCase.Run(t) } func TestLoadQuiet(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Description: "TestLoadQuiet", Setup: func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() helpers.Ensure("pull", "--quiet", testutil.CommonImage) helpers.Ensure("tag", testutil.CommonImage, identifier) helpers.Ensure("save", identifier, "-o", filepath.Join(data.Temp().Path(), "common.tar")) helpers.Ensure("rmi", "-f", identifier) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("load", "--quiet", "--input", filepath.Join(data.Temp().Path(), "common.tar")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( expect.Contains(data.Identifier()), expect.DoesNotContain("Loading layer"), ), } }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/image/image_prune.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "fmt" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/image" ) func pruneCommand() *cobra.Command { cmd := &cobra.Command{ Use: "prune [flags]", Short: "Remove unused images", Args: cobra.NoArgs, RunE: imagePruneAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().BoolP("all", "a", false, "Remove all unused images, not just dangling ones") cmd.Flags().StringSlice("filter", []string{}, "Filter output based on conditions provided") cmd.Flags().BoolP("force", "f", false, "Do not prompt for confirmation") return cmd } func pruneOptions(cmd *cobra.Command) (types.ImagePruneOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ImagePruneOptions{}, err } all, err := cmd.Flags().GetBool("all") if err != nil { return types.ImagePruneOptions{}, err } var filters []string if cmd.Flags().Changed("filter") { filters, err = cmd.Flags().GetStringSlice("filter") if err != nil { return types.ImagePruneOptions{}, err } } force, err := cmd.Flags().GetBool("force") if err != nil { return types.ImagePruneOptions{}, err } return types.ImagePruneOptions{ Stdout: cmd.OutOrStdout(), GOptions: globalOptions, All: all, Filters: filters, Force: force, }, err } func imagePruneAction(cmd *cobra.Command, _ []string) error { options, err := pruneOptions(cmd) if err != nil { return err } if !options.Force { var msg string if !options.All { msg = "This will remove all dangling images." } else { msg = "This will remove all images without at least one container associated to them." } if confirmed, err := helpers.Confirm(cmd, fmt.Sprintf("WARNING! %s.", msg)); err != nil || !confirmed { return err } } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return image.Prune(ctx, client, options) } ================================================ FILE: cmd/nerdctl/image/image_prune_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "fmt" "strings" "testing" "time" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestImagePrune(t *testing.T) { testCase := nerdtest.Setup() // Cannot use a custom namespace with buildkitd right now, so, no parallel it is testCase.NoParallel = true testCase.Cleanup = func(data test.Data, helpers test.Helpers) { // Stop and remove all running containers. This is to ensure we can remove all contList := strings.TrimSpace(helpers.Capture("ps", "-aq")) if contList != "" { helpers.Ensure(append([]string{"rm", "-f"}, strings.Split(contList, "\n")...)...) } // We need to delete everything here for prune to make any sense imgList := strings.TrimSpace(helpers.Capture("images", "--no-trunc", "-aq")) if imgList != "" { helpers.Ensure(append([]string{"rmi", "-f"}, strings.Split(imgList, "\n")...)...) } } testCase.SubTests = []*test.Case{ { Description: "without all", NoParallel: true, Require: require.All( // This never worked with Docker - the only reason we ever got was side effects from other tests // See inline comments. require.Not(nerdtest.Docker), nerdtest.Build, ), Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Setup: func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-test-image-prune"] `, testutil.CommonImage) buildCtx := data.Temp().Path() data.Temp().Save(dockerfile, "Dockerfile") helpers.Ensure("build", buildCtx) // After we rebuild with tag, docker will no longer show the version from above // Swapping order does not change anything. helpers.Ensure("build", "-t", identifier, buildCtx) imgList := helpers.Capture("images") assert.Assert(t, strings.Contains(imgList, ""), "Missing ") assert.Assert(t, strings.Contains(imgList, identifier), "Missing "+identifier) }, Command: test.Command("image", "prune", "--force"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { identifier := data.Identifier() return &test.Expected{ Output: expect.All( func(stdout string, t tig.T) { assert.Assert(t, !strings.Contains(stdout, identifier)) }, func(stdout string, t tig.T) { imgList := helpers.Capture("images") assert.Assert(t, !strings.Contains(imgList, ""), imgList) assert.Assert(t, strings.Contains(imgList, identifier)) }, ), } }, }, { Description: "with all", Require: require.All( // Same as above require.Not(nerdtest.Docker), nerdtest.Build, ), // Cannot use a custom namespace with buildkitd right now, so, no parallel it is NoParallel: true, Cleanup: func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() helpers.Anyhow("rmi", "-f", identifier) helpers.Anyhow("rm", "-f", identifier) }, Setup: func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-test-image-prune"] `, testutil.CommonImage) buildCtx := data.Temp().Path() data.Temp().Save(dockerfile, "Dockerfile") helpers.Ensure("build", buildCtx) helpers.Ensure("build", "-t", identifier, buildCtx) imgList := helpers.Capture("images") assert.Assert(t, strings.Contains(imgList, ""), "Missing ") assert.Assert(t, strings.Contains(imgList, identifier), "Missing "+identifier) helpers.Ensure("run", "--name", identifier, identifier) }, Command: test.Command("image", "prune", "--force", "--all"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( func(stdout string, t tig.T) { assert.Assert(t, !strings.Contains(stdout, data.Identifier())) }, func(stdout string, t tig.T) { imgList := helpers.Capture("images") assert.Assert(t, strings.Contains(imgList, data.Identifier())) assert.Assert(t, !strings.Contains(imgList, ""), imgList) helpers.Ensure("rm", "-f", data.Identifier()) removed := helpers.Capture("image", "prune", "--force", "--all") assert.Assert(t, strings.Contains(removed, data.Identifier())) imgList = helpers.Capture("images") assert.Assert(t, !strings.Contains(imgList, data.Identifier())) }, ), } }, }, { Description: "with filter label", Require: nerdtest.Build, // Cannot use a custom namespace with buildkitd right now, so, no parallel it is NoParallel: true, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Setup: func(data test.Data, helpers test.Helpers) { dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-test-image-prune-filter-label"] LABEL foo=bar LABEL version=0.1`, testutil.CommonImage) buildCtx := data.Temp().Path() data.Temp().Save(dockerfile, "Dockerfile") helpers.Ensure("build", "-t", data.Identifier(), buildCtx) imgList := helpers.Capture("images") assert.Assert(t, strings.Contains(imgList, data.Identifier()), "Missing "+data.Identifier()) }, Command: test.Command("image", "prune", "--force", "--all", "--filter", "label=foo=baz"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( func(stdout string, t tig.T) { assert.Assert(t, !strings.Contains(stdout, data.Identifier())) }, func(stdout string, t tig.T) { imgList := helpers.Capture("images") assert.Assert(t, strings.Contains(imgList, data.Identifier())) }, func(stdout string, t tig.T) { prune := helpers.Capture("image", "prune", "--force", "--all", "--filter", "label=foo=bar") assert.Assert(t, strings.Contains(prune, data.Identifier())) imgList := helpers.Capture("images") assert.Assert(t, !strings.Contains(imgList, data.Identifier())) }, ), } }, }, { Description: "with until", Require: nerdtest.Build, // Cannot use a custom namespace with buildkitd right now, so, no parallel it is NoParallel: true, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, Setup: func(data test.Data, helpers test.Helpers) { dockerfile := fmt.Sprintf(`FROM %s RUN echo "Anything, so that we create actual content for docker to set the current time for CreatedAt" CMD ["echo", "nerdctl-test-image-prune-until"]`, testutil.CommonImage) buildCtx := data.Temp().Path() data.Temp().Save(dockerfile, "Dockerfile") helpers.Ensure("build", "-t", data.Identifier(), buildCtx) imgList := helpers.Capture("images") assert.Assert(t, strings.Contains(imgList, data.Identifier()), "Missing "+data.Identifier()) data.Labels().Set("imageID", data.Identifier()) }, Command: test.Command("image", "prune", "--force", "--all", "--filter", "until=12h"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( expect.DoesNotContain(data.Labels().Get("imageID")), func(stdout string, t tig.T) { imgList := helpers.Capture("images") assert.Assert(t, strings.Contains(imgList, data.Labels().Get("imageID"))) }, ), } }, SubTests: []*test.Case{ { Description: "Wait and remove until=10ms", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { time.Sleep(1 * time.Second) }, Command: test.Command("image", "prune", "--force", "--all", "--filter", "until=10ms"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( expect.Contains(data.Labels().Get("imageID")), func(stdout string, t tig.T) { imgList := helpers.Capture("images") assert.Assert(t, !strings.Contains(imgList, data.Labels().Get("imageID")), imgList) }, ), } }, }, }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/image/image_pull.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/image" "github.com/containerd/nerdctl/v2/pkg/platformutil" "github.com/containerd/nerdctl/v2/pkg/strutil" ) func PullCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "pull [flags] NAME[:TAG]", Short: "Pull an image from a registry. Optionally specify \"ipfs://\" or \"ipns://\" scheme to pull image from IPFS.", Args: helpers.IsExactArgs(1), RunE: pullAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().String("unpack", "auto", "Unpack the image for the current single platform (auto/true/false)") cmd.RegisterFlagCompletionFunc("unpack", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"auto", "true", "false"}, cobra.ShellCompDirectiveNoFileComp }) // #region platform flags // platform is defined as StringSlice, not StringArray, to allow specifying "--platform=amd64,arm64" cmd.Flags().StringSlice("platform", nil, "Pull content for a specific platform") cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) cmd.Flags().Bool("all-platforms", false, "Pull content for all platforms") // #endregion // #region verify flags cmd.Flags().String("verify", "none", "Verify the image (none|cosign|notation)") cmd.RegisterFlagCompletionFunc("verify", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"none", "cosign", "notation"}, cobra.ShellCompDirectiveNoFileComp }) cmd.Flags().String("cosign-key", "", "Path to the public key file, KMS, URI or Kubernetes Secret for --verify=cosign") cmd.Flags().String("cosign-certificate-identity", "", "The identity expected in a valid Fulcio certificate for --verify=cosign. Valid values include email address, DNS names, IP addresses, and URIs. Either --cosign-certificate-identity or --cosign-certificate-identity-regexp must be set for keyless flows") cmd.Flags().String("cosign-certificate-identity-regexp", "", "A regular expression alternative to --cosign-certificate-identity for --verify=cosign. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either --cosign-certificate-identity or --cosign-certificate-identity-regexp must be set for keyless flows") cmd.Flags().String("cosign-certificate-oidc-issuer", "", "The OIDC issuer expected in a valid Fulcio certificate for --verify=cosign,, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth. Either --cosign-certificate-oidc-issuer or --cosign-certificate-oidc-issuer-regexp must be set for keyless flows") cmd.Flags().String("cosign-certificate-oidc-issuer-regexp", "", "A regular expression alternative to --certificate-oidc-issuer for --verify=cosign,. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either --cosign-certificate-oidc-issuer or --cosign-certificate-oidc-issuer-regexp must be set for keyless flows") // #endregion // #region socipull flags cmd.Flags().String("soci-index-digest", "", "Specify a particular index digest for SOCI. If left empty, SOCI will automatically use the index determined by the selection policy.") // #endregion cmd.Flags().BoolP("quiet", "q", false, "Suppress verbose output") cmd.Flags().String("ipfs-address", "", "multiaddr of IPFS API (default uses $IPFS_PATH env variable if defined or local directory ~/.ipfs)") return cmd } func processPullCommandFlags(cmd *cobra.Command) (types.ImagePullOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ImagePullOptions{}, err } allPlatforms, err := cmd.Flags().GetBool("all-platforms") if err != nil { return types.ImagePullOptions{}, err } platform, err := cmd.Flags().GetStringSlice("platform") if err != nil { return types.ImagePullOptions{}, err } ociSpecPlatform, err := platformutil.NewOCISpecPlatformSlice(allPlatforms, platform) if err != nil { return types.ImagePullOptions{}, err } unpackStr, err := cmd.Flags().GetString("unpack") if err != nil { return types.ImagePullOptions{}, err } unpack, err := strutil.ParseBoolOrAuto(unpackStr) if err != nil { return types.ImagePullOptions{}, err } quiet, err := cmd.Flags().GetBool("quiet") if err != nil { return types.ImagePullOptions{}, err } ipfsAddressStr, err := cmd.Flags().GetString("ipfs-address") if err != nil { return types.ImagePullOptions{}, err } sociIndexDigest, err := cmd.Flags().GetString("soci-index-digest") if err != nil { return types.ImagePullOptions{}, err } verifyOptions, err := helpers.VerifyOptions(cmd) if err != nil { return types.ImagePullOptions{}, err } return types.ImagePullOptions{ GOptions: globalOptions, VerifyOptions: verifyOptions, OCISpecPlatform: ociSpecPlatform, Unpack: unpack, Mode: "always", Quiet: quiet, IPFSAddress: ipfsAddressStr, RFlags: types.RemoteSnapshotterFlags{ SociIndexDigest: sociIndexDigest, }, Stdout: cmd.OutOrStdout(), Stderr: cmd.OutOrStderr(), ProgressOutputToStdout: true, }, nil } func pullAction(cmd *cobra.Command, args []string) error { options, err := processPullCommandFlags(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return image.Pull(ctx, client, args[0], options) } ================================================ FILE: cmd/nerdctl/image/image_pull_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "fmt" "strconv" "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" ) func TestImagePullWithCosign(t *testing.T) { dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-test-string"] `, testutil.CommonImage) nerdtest.Setup() var reg *registry.Server testCase := &test.Case{ Require: require.All( require.Linux, nerdtest.Build, require.Binary("cosign"), require.Not(nerdtest.Docker), nerdtest.Registry, ), Env: map[string]string{ "COSIGN_PASSWORD": "1", }, Setup: func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerfile, "Dockerfile") pri, pub := nerdtest.GenerateCosignKeyPair(data, helpers, "1") reg = nerdtest.RegistryWithNoAuth(data, helpers, 0, false) reg.Setup(data, helpers) testImageRef := fmt.Sprintf("%s:%d/%s", "127.0.0.1", reg.Port, data.Identifier()) buildCtx := data.Temp().Path() helpers.Ensure("build", "-t", testImageRef+":one", buildCtx) helpers.Ensure("build", "-t", testImageRef+":two", buildCtx) helpers.Ensure("push", "--sign=cosign", "--cosign-key="+pri, testImageRef+":one") helpers.Ensure("push", "--sign=cosign", "--cosign-key="+pri, testImageRef+":two") data.Labels().Set("public_key", pub) data.Labels().Set("image_ref", testImageRef) }, Cleanup: func(data test.Data, helpers test.Helpers) { if reg != nil { reg.Cleanup(data, helpers) testImageRef := data.Labels().Get("image_ref") helpers.Anyhow("rmi", "-f", testImageRef+":one") helpers.Anyhow("rmi", "-f", testImageRef+":two") } }, SubTests: []*test.Case{ { Description: "Pull with the correct key", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command( "pull", "--quiet", "--verify=cosign", "--cosign-key="+data.Labels().Get("public_key"), data.Labels().Get("image_ref")+":one") }, Expected: test.Expects(0, nil, nil), }, { Description: "Pull with unrelated key", Env: map[string]string{ "COSIGN_PASSWORD": "2", }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { _, pub := nerdtest.GenerateCosignKeyPair(data, helpers, "2") return helpers.Command("pull", "--quiet", "--verify=cosign", "--cosign-key="+pub, data.Labels().Get("image_ref")+":two") }, Expected: test.Expects(12, nil, nil), }, }, } testCase.Run(t) } func TestImagePullPlainHttpWithDefaultPort(t *testing.T) { nerdtest.Setup() var reg *registry.Server dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-test-string"] `, testutil.CommonImage) testCase := &test.Case{ Require: require.All( require.Linux, require.Not(nerdtest.Docker), nerdtest.Build, nerdtest.Registry, ), Setup: func(data test.Data, helpers test.Helpers) { data.Temp().Save(dockerfile, "Dockerfile") reg = nerdtest.RegistryWithNoAuth(data, helpers, 80, false) reg.Setup(data, helpers) testImageRef := fmt.Sprintf("%s/%s", reg.IP.String(), data.Identifier()) buildCtx := data.Temp().Path() helpers.Ensure("build", "-t", testImageRef, buildCtx) helpers.Ensure("--insecure-registry", "push", testImageRef) helpers.Ensure("rmi", "-f", testImageRef) data.Labels().Set("image_ref", testImageRef) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("--insecure-registry", "pull", data.Labels().Get("image_ref")) }, Expected: test.Expects(0, nil, nil), Cleanup: func(data test.Data, helpers test.Helpers) { if reg != nil { reg.Cleanup(data, helpers) helpers.Anyhow("rmi", "-f", data.Labels().Get("image_ref")) } }, } testCase.Run(t) } func TestImagePullSoci(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.All( require.Linux, require.Not(nerdtest.Docker), nerdtest.Soci, ), // NOTE: these tests cannot be run in parallel, as they depend on the output of host `mount` // They also feel prone to raciness... NoParallel: true, SubTests: []*test.Case{ { Description: "Run without specifying SOCI index", NoParallel: true, Data: test.WithLabels(map[string]string{ "remoteSnapshotsExpectedCount": "11", "sociIndexDigest": "", }), Setup: func(data test.Data, helpers test.Helpers) { cmd := helpers.Custom("mount") cmd.Run(&test.Expected{ Output: func(stdout string, t tig.T) { data.Labels().Set("remoteSnapshotsInitialCount", strconv.Itoa(strings.Count(stdout, "fuse.rawBridge"))) }, }) helpers.Ensure("--snapshotter=soci", "pull", testutil.FfmpegSociImage) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", testutil.FfmpegSociImage) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Custom("mount") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { remoteSnapshotsInitialCount, _ := strconv.Atoi(data.Labels().Get("remoteSnapshotsInitialCount")) remoteSnapshotsActualCount := strings.Count(stdout, "fuse.rawBridge") assert.Equal(t, data.Labels().Get("remoteSnapshotsExpectedCount"), strconv.Itoa(remoteSnapshotsActualCount-remoteSnapshotsInitialCount), "expected remote snapshot count to match", ) }, } }, }, { Description: "Run with bad SOCI index", NoParallel: true, Data: test.WithLabels(map[string]string{ "remoteSnapshotsExpectedCount": "11", "sociIndexDigest": "sha256:thisisabadindex0000000000000000000000000000000000000000000000000", }), Setup: func(data test.Data, helpers test.Helpers) { cmd := helpers.Custom("mount") cmd.Run(&test.Expected{ Output: func(stdout string, t tig.T) { data.Labels().Set("remoteSnapshotsInitialCount", strconv.Itoa(strings.Count(stdout, "fuse.rawBridge"))) }, }) helpers.Ensure("--snapshotter=soci", "pull", testutil.FfmpegSociImage) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", testutil.FfmpegSociImage) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Custom("mount") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { remoteSnapshotsInitialCount, _ := strconv.Atoi(data.Labels().Get("remoteSnapshotsInitialCount")) remoteSnapshotsActualCount := strings.Count(stdout, "fuse.rawBridge") assert.Equal(t, data.Labels().Get("remoteSnapshotsExpectedCount"), strconv.Itoa(remoteSnapshotsActualCount-remoteSnapshotsInitialCount), "expected remote snapshot count to match") }, } }, }, }, } testCase.Run(t) } func TestImagePullProcessOutput(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ SubTests: []*test.Case{ { Description: "Pull Image - output should be in stdout", NoParallel: true, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", testutil.BusyboxImage) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("pull", testutil.BusyboxImage) }, Expected: test.Expects(0, nil, expect.Contains(testutil.BusyboxImage)), }, { Description: "Run Container with image pull - output should be in stderr", NoParallel: true, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", testutil.BusyboxImage) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", testutil.BusyboxImage) }, Expected: test.Expects(0, nil, expect.DoesNotContain(testutil.BusyboxImage)), }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/image/image_push.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/image" ) const ( allowNonDistFlag = "allow-nondistributable-artifacts" ) func PushCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "push [flags] NAME[:TAG]", Short: "Push an image or a repository to a registry. Optionally specify \"ipfs://\" or \"ipns://\" scheme to push image to IPFS.", Args: helpers.IsExactArgs(1), RunE: pushAction, ValidArgsFunction: pushShellComplete, SilenceUsage: true, SilenceErrors: true, } // #region platform flags // platform is defined as StringSlice, not StringArray, to allow specifying "--platform=amd64,arm64" cmd.Flags().StringSlice("platform", []string{}, "Push content for a specific platform") cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) cmd.Flags().Bool("all-platforms", false, "Push content for all platforms") // #endregion cmd.Flags().Bool("estargz", false, "Convert the image into eStargz") cmd.Flags().Bool("ipfs-ensure-image", true, "Ensure the entire contents of the image is locally available before push") cmd.Flags().String("ipfs-address", "", "multiaddr of IPFS API (default uses $IPFS_PATH env variable if defined or local directory ~/.ipfs)") // #region sign flags cmd.Flags().String("sign", "none", "Sign the image (none|cosign|notation") cmd.RegisterFlagCompletionFunc("sign", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"none", "cosign", "notation"}, cobra.ShellCompDirectiveNoFileComp }) cmd.Flags().String("cosign-key", "", "Path to the private key file, KMS URI or Kubernetes Secret for --sign=cosign") cmd.Flags().String("notation-key-name", "", "Signing key name for a key previously added to notation's key list for --sign=notation") // #endregion // #region soci flags cmd.Flags().Int64("soci-span-size", -1, "Span size that soci index uses to segment layer data. Default is 4 MiB.") cmd.Flags().Int64("soci-min-layer-size", -1, "Minimum layer size to build zTOC for. Smaller layers won't have zTOC and not lazy pulled. Default is 10 MiB.") // #endregion cmd.Flags().BoolP("quiet", "q", false, "Suppress verbose output") cmd.Flags().Bool(allowNonDistFlag, false, "Allow pushing images with non-distributable blobs") return cmd } func pushOptions(cmd *cobra.Command) (types.ImagePushOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ImagePushOptions{}, err } platform, err := cmd.Flags().GetStringSlice("platform") if err != nil { return types.ImagePushOptions{}, err } allPlatforms, err := cmd.Flags().GetBool("all-platforms") if err != nil { return types.ImagePushOptions{}, err } estargz, err := cmd.Flags().GetBool("estargz") if err != nil { return types.ImagePushOptions{}, err } ipfsEnsureImage, err := cmd.Flags().GetBool("ipfs-ensure-image") if err != nil { return types.ImagePushOptions{}, err } ipfsAddress, err := cmd.Flags().GetString("ipfs-address") if err != nil { return types.ImagePushOptions{}, err } quiet, err := cmd.Flags().GetBool("quiet") if err != nil { return types.ImagePushOptions{}, err } allowNonDist, err := cmd.Flags().GetBool(allowNonDistFlag) if err != nil { return types.ImagePushOptions{}, err } signOptions, err := signOptions(cmd) if err != nil { return types.ImagePushOptions{}, err } sociOptions, err := sociOptions(cmd) if err != nil { return types.ImagePushOptions{}, err } return types.ImagePushOptions{ GOptions: globalOptions, SignOptions: signOptions, SociOptions: sociOptions, Platforms: platform, AllPlatforms: allPlatforms, Estargz: estargz, IpfsEnsureImage: ipfsEnsureImage, IpfsAddress: ipfsAddress, Quiet: quiet, AllowNondistributableArtifacts: allowNonDist, Stdout: cmd.OutOrStdout(), }, nil } func pushAction(cmd *cobra.Command, args []string) error { options, err := pushOptions(cmd) if err != nil { return err } rawRef := args[0] client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return image.Push(ctx, client, rawRef, options) } func pushShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show image names return completion.ImageNames(cmd) } func signOptions(cmd *cobra.Command) (opt types.ImageSignOptions, err error) { if opt.Provider, err = cmd.Flags().GetString("sign"); err != nil { return } if opt.CosignKey, err = cmd.Flags().GetString("cosign-key"); err != nil { return } if opt.NotationKeyName, err = cmd.Flags().GetString("notation-key-name"); err != nil { return } return } func sociOptions(cmd *cobra.Command) (opt types.SociOptions, err error) { if opt.SpanSize, err = cmd.Flags().GetInt64("soci-span-size"); err != nil { return } if opt.MinLayerSize, err = cmd.Flags().GetInt64("soci-min-layer-size"); err != nil { return } return } ================================================ FILE: cmd/nerdctl/image/image_push_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "errors" "fmt" "net/http" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" ) func TestPush(t *testing.T) { nerdtest.Setup() var registryNoAuthHTTPRandom, registryNoAuthHTTPDefault, registryTokenAuthHTTPSRandom *registry.Server var tokenServer *registry.TokenAuthServer testCase := &test.Case{ Require: require.All( require.Linux, nerdtest.Registry, nerdtest.IsFlaky("https://github.com/containerd/nerdctl/issues/4470"), ), Setup: func(data test.Data, helpers test.Helpers) { registryNoAuthHTTPRandom = nerdtest.RegistryWithNoAuth(data, helpers, 0, false) registryNoAuthHTTPRandom.Setup(data, helpers) registryNoAuthHTTPDefault = nerdtest.RegistryWithNoAuth(data, helpers, 80, false) registryNoAuthHTTPDefault.Setup(data, helpers) registryTokenAuthHTTPSRandom, tokenServer = nerdtest.RegistryWithTokenAuth(data, helpers, "admin", "badmin", 0, true) tokenServer.Setup(data, helpers) registryTokenAuthHTTPSRandom.Setup(data, helpers) }, Cleanup: func(data test.Data, helpers test.Helpers) { if registryNoAuthHTTPRandom != nil { registryNoAuthHTTPRandom.Cleanup(data, helpers) } if registryNoAuthHTTPDefault != nil { registryNoAuthHTTPDefault.Cleanup(data, helpers) } if registryTokenAuthHTTPSRandom != nil { registryTokenAuthHTTPSRandom.Cleanup(data, helpers) } if tokenServer != nil { tokenServer.Cleanup(data, helpers) } }, SubTests: []*test.Case{ { Description: "plain http", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) testImageRef := fmt.Sprintf("%s:%d/%s", registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier()) data.Labels().Set("testImageRef", testImageRef) helpers.Ensure("tag", testutil.CommonImage, testImageRef) }, Cleanup: func(data test.Data, helpers test.Helpers) { if data.Labels().Get("testImageRef") != "" { helpers.Anyhow("rmi", "-f", data.Labels().Get("testImageRef")) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("push", data.Labels().Get("testImageRef")) }, Expected: test.Expects(1, []error{errors.New("server gave HTTP response to HTTPS client")}, nil), }, { Description: "plain http with insecure", Require: require.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) testImageRef := fmt.Sprintf("%s:%d/%s", registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier()) data.Labels().Set("testImageRef", testImageRef) helpers.Ensure("tag", testutil.CommonImage, testImageRef) }, Cleanup: func(data test.Data, helpers test.Helpers) { if data.Labels().Get("testImageRef") != "" { helpers.Anyhow("rmi", "-f", data.Labels().Get("testImageRef")) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("push", "--insecure-registry", data.Labels().Get("testImageRef")) }, Expected: test.Expects(0, nil, nil), }, { Description: "plain http with localhost", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) testImageRef := fmt.Sprintf("%s:%d/%s", "127.0.0.1", registryNoAuthHTTPRandom.Port, data.Identifier()) data.Labels().Set("testImageRef", testImageRef) helpers.Ensure("tag", testutil.CommonImage, testImageRef) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("push", data.Labels().Get("testImageRef")) }, Expected: test.Expects(0, nil, nil), }, { Description: "plain http with insecure, default port", Require: require.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) testImageRef := fmt.Sprintf("%s/%s", registryNoAuthHTTPDefault.IP.String(), data.Identifier()) data.Labels().Set("testImageRef", testImageRef) helpers.Ensure("tag", testutil.CommonImage, testImageRef) }, Cleanup: func(data test.Data, helpers test.Helpers) { if data.Labels().Get("testImageRef") != "" { helpers.Anyhow("rmi", "-f", data.Labels().Get("testImageRef")) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("push", "--insecure-registry", data.Labels().Get("testImageRef")) }, Expected: test.Expects(0, nil, nil), }, { Description: "with insecure, with login", Require: require.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) testImageRef := fmt.Sprintf("%s:%d/%s", registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port, data.Identifier()) data.Labels().Set("testImageRef", testImageRef) helpers.Ensure("tag", testutil.CommonImage, testImageRef) helpers.Ensure("--insecure-registry", "login", "-u", "admin", "-p", "badmin", fmt.Sprintf("%s:%d", registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port)) }, Cleanup: func(data test.Data, helpers test.Helpers) { if data.Labels().Get("testImageRef") != "" { helpers.Anyhow("rmi", "-f", data.Labels().Get("testImageRef")) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("push", "--insecure-registry", data.Labels().Get("testImageRef")) }, Expected: test.Expects(0, nil, nil), }, { Description: "with hosts dir, with login", Require: require.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) testImageRef := fmt.Sprintf("%s:%d/%s", registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port, data.Identifier()) data.Labels().Set("testImageRef", testImageRef) helpers.Ensure("tag", testutil.CommonImage, testImageRef) helpers.Ensure("--hosts-dir", registryTokenAuthHTTPSRandom.HostsDir, "login", "-u", "admin", "-p", "badmin", fmt.Sprintf("%s:%d", registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port)) }, Cleanup: func(data test.Data, helpers test.Helpers) { if data.Labels().Get("testImageRef") != "" { helpers.Anyhow("rmi", "-f", data.Labels().Get("testImageRef")) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("push", "--hosts-dir", registryTokenAuthHTTPSRandom.HostsDir, data.Labels().Get("testImageRef")) }, Expected: test.Expects(0, nil, nil), }, { Description: "non distributable artifacts", Require: require.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.NonDistBlobImage) testImageRef := fmt.Sprintf("%s:%d/%s", registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier()) data.Labels().Set("testImageRef", testImageRef) helpers.Ensure("tag", testutil.NonDistBlobImage, testImageRef) }, Cleanup: func(data test.Data, helpers test.Helpers) { if data.Labels().Get("testImageRef") != "" { helpers.Anyhow("rmi", "-f", data.Labels().Get("testImageRef")) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("push", "--insecure-registry", data.Labels().Get("testImageRef")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { blobURL := fmt.Sprintf("http://%s:%d/v2/%s/blobs/%s", registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), testutil.NonDistBlobDigest) resp, err := http.Get(blobURL) assert.Assert(t, err, "error making http request") if resp.Body != nil { _ = resp.Body.Close() } assert.Equal(t, resp.StatusCode, http.StatusNotFound, "non-distributable blob should not be available") }, } }, }, { Description: "non distributable artifacts (with)", Require: require.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.NonDistBlobImage) testImageRef := fmt.Sprintf("%s:%d/%s", registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier()) data.Labels().Set("testImageRef", testImageRef) helpers.Ensure("tag", testutil.NonDistBlobImage, testImageRef) }, Cleanup: func(data test.Data, helpers test.Helpers) { if data.Labels().Get("testImageRef") != "" { helpers.Anyhow("rmi", "-f", data.Labels().Get("testImageRef")) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("push", "--insecure-registry", "--allow-nondistributable-artifacts", data.Labels().Get("testImageRef")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { blobURL := fmt.Sprintf("http://%s:%d/v2/%s/blobs/%s", registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), testutil.NonDistBlobDigest) resp, err := http.Get(blobURL) assert.Assert(t, err, "error making http request") if resp.Body != nil { _ = resp.Body.Close() } assert.Equal(t, resp.StatusCode, http.StatusOK, "non-distributable blob should be available") }, } }, }, { Description: "soci", Require: require.All( nerdtest.Soci, require.Not(nerdtest.Docker), ), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.UbuntuImage) testImageRef := fmt.Sprintf("%s:%d/%s", registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier()) data.Labels().Set("testImageRef", testImageRef) helpers.Ensure("tag", testutil.UbuntuImage, testImageRef) }, Cleanup: func(data test.Data, helpers test.Helpers) { if data.Labels().Get("testImageRef") != "" { helpers.Anyhow("rmi", "-f", data.Labels().Get("testImageRef")) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("push", "--snapshotter=soci", "--insecure-registry", "--soci-span-size=2097152", "--soci-min-layer-size=20971520", data.Labels().Get("testImageRef")) }, Expected: test.Expects(0, nil, nil), }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/image/image_remove.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/image" ) func RmiCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "rmi [flags] IMAGE [IMAGE, ...]", Short: "Remove one or more images", Args: cobra.MinimumNArgs(1), RunE: rmiAction, ValidArgsFunction: rmiShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().BoolP("force", "f", false, "Force removal of the image") // Alias `-a` is reserved for `--all`. Should be compatible with `podman rmi --all`. cmd.Flags().Bool("async", false, "Asynchronous mode") return cmd } func removeOptions(cmd *cobra.Command) (types.ImageRemoveOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ImageRemoveOptions{}, err } force, err := cmd.Flags().GetBool("force") if err != nil { return types.ImageRemoveOptions{}, err } async, err := cmd.Flags().GetBool("async") if err != nil { return types.ImageRemoveOptions{}, err } return types.ImageRemoveOptions{ Stdout: cmd.OutOrStdout(), GOptions: globalOptions, Force: force, Async: async, }, nil } func rmiAction(cmd *cobra.Command, args []string) error { options, err := removeOptions(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return image.Remove(ctx, client, args, options) } func rmiShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show image names return completion.ImageNames(cmd) } ================================================ FILE: cmd/nerdctl/image/image_remove_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "errors" "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/imgutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRemove(t *testing.T) { testCase := nerdtest.Setup() const ( imgShortIDKey = "imgShortID" ) repoName, _ := imgutil.ParseRepoTag(testutil.CommonImage) nginxRepoName, _ := imgutil.ParseRepoTag(testutil.NginxAlpineImage) // NOTES: // - since all of these are rmi-ing the common image, we need private mode testCase.Require = nerdtest.Private testCase.SubTests = []*test.Case{ { Description: "Remove image with stopped container - without -f", NoParallel: true, Require: require.All( require.Not(nerdtest.Docker), ), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "--quiet", "--pull", "always", "--name", data.Identifier(), testutil.CommonImage) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: test.Command("rmi", testutil.CommonImage), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New("image is being used")}, Output: func(stdout string, t tig.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.Contains(repoName), }) }, } }, }, { Description: "Remove image with stopped container - with -f", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "--quiet", "--pull", "always", "--name", data.Identifier(), testutil.CommonImage) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: test.Command("rmi", "-f", testutil.CommonImage), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.DoesNotContain(repoName), }) }, } }, }, { Description: "Remove image with running container - without -f", NoParallel: true, Require: require.All( require.Not(nerdtest.Docker), ), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "--quiet", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: test.Command("rmi", testutil.CommonImage), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New("image is being used")}, Output: func(stdout string, t tig.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.Contains(repoName), }) }, } }, }, { Description: "Remove image with running container - with -f", NoParallel: true, Require: require.All( require.Not(nerdtest.Docker), ), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "--quiet", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) img := nerdtest.InspectImage(helpers, testutil.CommonImage) repoName, _ := imgutil.ParseRepoTag(testutil.CommonImage) imgShortID := strings.TrimPrefix(img.RepoDigests[0], repoName+"@sha256:")[0:8] data.Labels().Set(imgShortIDKey, imgShortID) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("rmi", "-f", data.Labels().Get(imgShortIDKey)) }, Command: test.Command("rmi", "-f", testutil.CommonImage), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Errors: []error{}, Output: func(stdout string, t tig.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.Contains(""), }) }, } }, }, { Description: "Remove image with created container - without -f", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("create", "--quiet", "--pull", "always", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: test.Command("rmi", testutil.CommonImage), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New("image is being used")}, Output: func(stdout string, t tig.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.Contains(repoName), }) }, } }, }, { Description: "Remove image with created container - with -f", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.NginxAlpineImage) helpers.Ensure("create", "--quiet", "--pull", "always", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) helpers.Ensure("rmi", testutil.NginxAlpineImage) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: test.Command("rmi", "-f", testutil.CommonImage), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { helpers.Command("images").Run(&test.Expected{ // a created container with removed image doesn't impact other `rmi` command Output: expect.DoesNotContain(repoName, nginxRepoName), }) }, } }, }, { Description: "Remove image with paused container - without -f", NoParallel: true, Require: require.All( require.Not(nerdtest.Docker), nerdtest.CGroup, ), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "--quiet", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) helpers.Ensure("pause", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: test.Command("rmi", testutil.CommonImage), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New("image is being used")}, Output: func(stdout string, t tig.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.Contains(repoName), }) }, } }, }, { Description: "Remove image with paused container - with -f", NoParallel: true, Require: require.All( nerdtest.CGroup, require.Not(nerdtest.Docker), ), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "--quiet", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) helpers.Ensure("pause", data.Identifier()) img := nerdtest.InspectImage(helpers, testutil.CommonImage) repoName, _ := imgutil.ParseRepoTag(testutil.CommonImage) imgShortID := strings.TrimPrefix(img.RepoDigests[0], repoName+"@sha256:")[0:8] data.Labels().Set(imgShortIDKey, imgShortID) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("rmi", "-f", data.Labels().Get(imgShortIDKey)) }, Command: test.Command("rmi", "-f", testutil.CommonImage), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Errors: []error{}, Output: func(stdout string, t tig.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.Contains(""), }) }, } }, }, { Description: "Remove image with killed container - without -f", NoParallel: true, Require: require.All( require.Not(nerdtest.Docker), ), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "--quiet", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) helpers.Ensure("kill", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: test.Command("rmi", testutil.CommonImage), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New("image is being used")}, Output: func(stdout string, t tig.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.Contains(repoName), }) }, } }, }, { Description: "Remove image with killed container - with -f", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "--quiet", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) helpers.Ensure("kill", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: test.Command("rmi", "-f", testutil.CommonImage), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.DoesNotContain(repoName), }) }, } }, }, } testCase.Run(t) } // TestIssue3016 tests https://github.com/containerd/nerdctl/issues/3016 func TestIssue3016(t *testing.T) { testCase := nerdtest.Setup() const ( tagIDKey = "tagID" ) testCase.SubTests = []*test.Case{ { Description: "Issue #3016 - Tags created using the short digest ids of container images cannot be deleted using the nerdctl rmi command.", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) helpers.Ensure("pull", "--quiet", testutil.NginxAlpineImage) img := nerdtest.InspectImage(helpers, testutil.NginxAlpineImage) repoName, _ := imgutil.ParseRepoTag(testutil.NginxAlpineImage) tagID := strings.TrimPrefix(img.RepoDigests[0], repoName+"@sha256:")[0:8] helpers.Ensure("tag", testutil.CommonImage, tagID) data.Labels().Set(tagIDKey, tagID) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("rmi", data.Labels().Get(tagIDKey)) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Errors: []error{}, Output: func(stdout string, t tig.T) { helpers.Command("images", data.Labels().Get(tagIDKey)).Run(&test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { assert.Equal(t, len(strings.Split(stdout, "\n")), 2) }, }) }, } }, }, } testCase.Run(t) } func TestRemoveKubeWithKubeHideDupe(t *testing.T) { var numTags, numNoTags int testCase := nerdtest.Setup() testCase.NoParallel = true testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("--kube-hide-dupe", "rmi", "-f", testutil.BusyboxImage) } testCase.Setup = func(data test.Data, helpers test.Helpers) { numTags = len(strings.Split(strings.TrimSpace(helpers.Capture("--kube-hide-dupe", "images")), "\n")) numNoTags = len(strings.Split(strings.TrimSpace(helpers.Capture("images")), "\n")) } testCase.Require = require.All( nerdtest.OnlyKubernetes, ) testCase.SubTests = []*test.Case{ { Description: "After removing the tag without kube-hide-dupe, repodigest is shown as ", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.BusyboxImage) }, Command: test.Command("rmi", "-f", testutil.BusyboxImage), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Errors: []error{}, Output: func(stdout string, t tig.T) { helpers.Command("--kube-hide-dupe", "images").Run(&test.Expected{ Output: func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) == numTags+1) }, }) helpers.Command("images").Run(&test.Expected{ Output: func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) == numNoTags+1) }, }) }, } }, }, { Description: "If there are other tags, the Repodigest will not be deleted", NoParallel: true, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("--kube-hide-dupe", "rmi", data.Identifier()) }, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.BusyboxImage) helpers.Ensure("tag", testutil.BusyboxImage, data.Identifier()) }, Command: test.Command("--kube-hide-dupe", "rmi", testutil.BusyboxImage), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Errors: []error{}, Output: func(stdout string, t tig.T) { helpers.Command("--kube-hide-dupe", "images").Run(&test.Expected{ Output: func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) == numTags+1) }, }) helpers.Command("images").Run(&test.Expected{ Output: func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) == numNoTags+2) }, }) }, } }, }, { Description: "After deleting all repo:tag entries, all repodigests will be cleaned up", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.BusyboxImage) helpers.Ensure("tag", testutil.BusyboxImage, data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { helpers.Ensure("--kube-hide-dupe", "rmi", "-f", testutil.BusyboxImage) return helpers.Command("--kube-hide-dupe", "rmi", "-f", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { helpers.Command("--kube-hide-dupe", "images").Run(&test.Expected{ Output: func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) == numTags) }, }) helpers.Command("images").Run(&test.Expected{ Output: func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) == numNoTags) }, }) }, } }, }, { Description: "Test multiple IDs found with provided prefix and force with shortID", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.BusyboxImage) helpers.Ensure("tag", testutil.BusyboxImage, data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("--kube-hide-dupe", "images", testutil.BusyboxImage, "-q") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { helpers.Command("--kube-hide-dupe", "rmi", stdout[0:12]).Run(&test.Expected{ ExitCode: 1, Errors: []error{errors.New("multiple IDs found with provided prefix: ")}, }) helpers.Command("--kube-hide-dupe", "rmi", "--force", stdout[0:12]).Run(&test.Expected{ ExitCode: 0, }) helpers.Command("images").Run(&test.Expected{ Output: func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) == numNoTags) }, }) }, } }, }, { Description: "Test remove image with digestID", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.BusyboxImage) helpers.Ensure("tag", testutil.BusyboxImage, data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("--kube-hide-dupe", "images", testutil.BusyboxImage, "-q", "--no-trunc") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { imgID := strings.Split(stdout, "\n") helpers.Command("--kube-hide-dupe", "rmi", imgID[0]).Run(&test.Expected{ ExitCode: 1, Errors: []error{errors.New("multiple IDs found with provided prefix: ")}, }) helpers.Command("--kube-hide-dupe", "rmi", "--force", imgID[0]).Run(&test.Expected{ ExitCode: 0, }) helpers.Command("images").Run(&test.Expected{ Output: func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) == numNoTags) }, }) }, } }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/image/image_save.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "fmt" "os" "github.com/mattn/go-isatty" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/image" ) func SaveCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "save [flags] IMAGE [IMAGE...]", Args: cobra.MinimumNArgs(1), Short: "Save one or more images to a tar archive (streamed to STDOUT by default)", Long: "The archive implements both Docker Image Spec v1.2 and OCI Image Spec v1.0.", RunE: saveAction, ValidArgsFunction: saveShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().StringP("output", "o", "", "Write to a file, instead of STDOUT") // #region platform flags // platform is defined as StringSlice, not StringArray, to allow specifying "--platform=amd64,arm64" cmd.Flags().StringSlice("platform", []string{}, "Export content for a specific platform") cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) cmd.Flags().Bool("all-platforms", false, "Export content for all platforms") // #endregion return cmd } func saveOptions(cmd *cobra.Command) (types.ImageSaveOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ImageSaveOptions{}, err } allPlatforms, err := cmd.Flags().GetBool("all-platforms") if err != nil { return types.ImageSaveOptions{}, err } platform, err := cmd.Flags().GetStringSlice("platform") if err != nil { return types.ImageSaveOptions{}, err } return types.ImageSaveOptions{ GOptions: globalOptions, AllPlatforms: allPlatforms, Platform: platform, }, err } func saveAction(cmd *cobra.Command, args []string) error { options, err := saveOptions(cmd) if err != nil { return err } output := cmd.OutOrStdout() outputPath, err := cmd.Flags().GetString("output") if err != nil { return err } else if outputPath != "" { f, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return err } output = f defer f.Close() } else if out, ok := output.(*os.File); ok && isatty.IsTerminal(out.Fd()) { return fmt.Errorf("cowardly refusing to save to a terminal. Use the -o flag or redirect") } options.Stdout = output client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() if err = image.Save(ctx, client, args, options); err != nil && outputPath != "" { os.Remove(outputPath) } return err } func saveShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show image names return completion.ImageNames(cmd) } ================================================ FILE: cmd/nerdctl/image/image_save_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "os" "path/filepath" "runtime" "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestSaveContent(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ // FIXME: move to busybox for windows? Require: require.Not(require.Windows), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("save", "-o", filepath.Join(data.Temp().Path(), "out.tar"), testutil.CommonImage) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { rootfsPath := filepath.Join(data.Temp().Path(), "rootfs") err := testhelpers.ExtractDockerArchive(filepath.Join(data.Temp().Path(), "out.tar"), rootfsPath) assert.NilError(t, err) etcOSReleasePath := filepath.Join(rootfsPath, "/etc/os-release") etcOSReleaseBytes, err := os.ReadFile(etcOSReleasePath) assert.NilError(t, err) etcOSRelease := string(etcOSReleaseBytes) assert.Assert(t, strings.Contains(etcOSRelease, "Alpine")) }, } }, } testCase.Run(t) } func TestSave(t *testing.T) { testCase := nerdtest.Setup() // This test relies on the fact that we can remove the common image, which definitely conflicts with others, // hence the private mode. // Further note though, that this will hide the fact this the save command could fail if some layers are missing. // See https://github.com/containerd/nerdctl/issues/3425 and others for details. testCase.Require = nerdtest.Private if runtime.GOOS == "windows" { testCase.Require = nerdtest.IsFlaky("https://github.com/containerd/nerdctl/issues/3524") } testCase.SubTests = []*test.Case{ { Description: "Single image, by id", NoParallel: true, Cleanup: func(data test.Data, helpers test.Helpers) { if data.Labels().Get("id") != "" { helpers.Anyhow("rmi", "-f", data.Labels().Get("id")) } }, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) img := nerdtest.InspectImage(helpers, testutil.CommonImage) var id string // Docker and Nerdctl do not agree on what is the definition of an image ID if nerdtest.IsDocker() { id = img.ID } else { id = strings.Split(img.RepoDigests[0], ":")[1] } tarPath := filepath.Join(data.Temp().Path(), "out.tar") helpers.Ensure("save", "-o", tarPath, id) helpers.Ensure("rmi", "-f", testutil.CommonImage) helpers.Ensure("load", "-i", tarPath) data.Labels().Set("id", id) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Labels().Get("id"), "sh", "-euxc", "echo foo") }, Expected: test.Expects(0, nil, expect.Equals("foo\n")), }, { Description: "Image with different names, by id", NoParallel: true, Cleanup: func(data test.Data, helpers test.Helpers) { if data.Labels().Get("id") != "" { helpers.Anyhow("rmi", "-f", data.Labels().Get("id")) } }, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) img := nerdtest.InspectImage(helpers, testutil.CommonImage) var id string if nerdtest.IsDocker() { id = img.ID } else { id = strings.Split(img.RepoDigests[0], ":")[1] } helpers.Ensure("tag", testutil.CommonImage, data.Identifier()) tarPath := filepath.Join(data.Temp().Path(), "out.tar") helpers.Ensure("save", "-o", tarPath, id) helpers.Ensure("rmi", "-f", testutil.CommonImage) helpers.Ensure("load", "-i", tarPath) data.Labels().Set("id", id) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Labels().Get("id"), "sh", "-euxc", "echo foo") }, Expected: test.Expects(0, nil, expect.Equals("foo\n")), }, } testCase.Run(t) } // TestSaveMultipleImagesWithSameIDAndLoad tests https://github.com/containerd/nerdctl/issues/3806 func TestSaveMultipleImagesWithSameIDAndLoad(t *testing.T) { testCase := nerdtest.Setup() // This test relies on the fact that we can remove the common image, which definitely conflicts with others, // hence the private mode. // Further note though, that this will hide the fact this the save command could fail if some layers are missing. // See https://github.com/containerd/nerdctl/issues/3425 and others for details. testCase.Require = nerdtest.Private if runtime.GOOS == "windows" { testCase.Require = nerdtest.IsFlaky("https://github.com/containerd/nerdctl/issues/3524") } testCase.SubTests = []*test.Case{ { Description: "Issue #3568 - Save multiple container images with the same image ID but different image names", NoParallel: true, Cleanup: func(data test.Data, helpers test.Helpers) { if data.Labels().Get("id") != "" { helpers.Anyhow("rmi", "-f", data.Labels().Get("id")) } }, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) img := nerdtest.InspectImage(helpers, testutil.CommonImage) var id string if nerdtest.IsDocker() { id = img.ID } else { id = strings.Split(img.RepoDigests[0], ":")[1] } helpers.Ensure("tag", testutil.CommonImage, data.Identifier()) tarPath := filepath.Join(data.Temp().Path(), "out.tar") helpers.Ensure("save", "-o", tarPath, testutil.CommonImage, data.Identifier()) helpers.Ensure("rmi", "-f", id) helpers.Ensure("load", "-i", tarPath) data.Labels().Set("id", id) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("images", "--no-trunc") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Errors: []error{}, Output: func(stdout string, t tig.T) { assert.Equal(t, strings.Count(stdout, data.Labels().Get("id")), 2) }, } }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/image/image_tag.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/image" ) func TagCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "tag [flags] SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]", Short: "Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE", Args: helpers.IsExactArgs(2), RunE: tagAction, ValidArgsFunction: tagShellComplete, SilenceUsage: true, SilenceErrors: true, } return cmd } func tagAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } options := types.ImageTagOptions{ GOptions: globalOptions, Source: args[0], Target: args[1], } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return image.Tag(ctx, client, options) } func tagShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) < 2 { // show image names return completion.ImageNames(cmd) } return nil, cobra.ShellCompDirectiveNoFileComp } ================================================ FILE: cmd/nerdctl/image/image_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package image import ( "testing" "github.com/containerd/nerdctl/v2/pkg/testutil" ) func TestMain(m *testing.M) { testutil.M(m) } ================================================ FILE: cmd/nerdctl/inspect/inspect.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package inspect import ( "context" "fmt" "github.com/spf13/cobra" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" containercmd "github.com/containerd/nerdctl/v2/cmd/nerdctl/container" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" imagecmd "github.com/containerd/nerdctl/v2/cmd/nerdctl/image" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/container" "github.com/containerd/nerdctl/v2/pkg/cmd/image" "github.com/containerd/nerdctl/v2/pkg/formatter" "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" "github.com/containerd/nerdctl/v2/pkg/idutil/imagewalker" ) func Command() *cobra.Command { cmd := &cobra.Command{ Use: "inspect", Short: "Return low-level information on objects.", Args: cobra.MinimumNArgs(1), RunE: inspectAction, ValidArgsFunction: inspectShellComplete, SilenceUsage: true, SilenceErrors: true, } addInspectFlags(cmd) return cmd } var validInspectType = map[string]bool{ "container": true, "image": true, } func addInspectFlags(cmd *cobra.Command) { cmd.Flags().BoolP("size", "s", false, "Display total file sizes (for containers)") cmd.Flags().StringP("format", "f", "", "Format the output using the given Go template, e.g, '{{json .}}'") cmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"json"}, cobra.ShellCompDirectiveNoFileComp }) cmd.Flags().String("type", "", "Return JSON for specified type") cmd.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"image", "container", ""}, cobra.ShellCompDirectiveNoFileComp }) cmd.Flags().String("mode", "dockercompat", `Inspect mode, "dockercompat" for Docker-compatible output, "native" for containerd-native output`) cmd.RegisterFlagCompletionFunc("mode", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"dockercompat", "native"}, cobra.ShellCompDirectiveNoFileComp }) } func inspectAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } namespace := globalOptions.Namespace address := globalOptions.Address format, err := cmd.Flags().GetString("format") if err != nil { return err } inspectType, err := cmd.Flags().GetString("type") if err != nil { return err } if len(inspectType) > 0 && !validInspectType[inspectType] { return fmt.Errorf("%q is not a valid value for --type", inspectType) } // container and image inspect can share the same client, since no `platform` // flag will be passed for image inspect. client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), namespace, address) if err != nil { return err } defer cancel() imagewalker := &imagewalker.ImageWalker{ Client: client, OnFound: func(ctx context.Context, found imagewalker.Found) error { return nil }, } containerwalker := &containerwalker.ContainerWalker{ Client: client, OnFound: func(ctx context.Context, found containerwalker.Found) error { return nil }, } inspectImage := len(inspectType) == 0 || inspectType == "image" inspectContainer := len(inspectType) == 0 || inspectType == "container" var imageInspectOptions types.ImageInspectOptions var containerInspectOptions types.ContainerInspectOptions if inspectImage { platform := "" imageInspectOptions, err = imagecmd.InspectOptions(cmd, &platform) if err != nil { return err } } if inspectContainer { containerInspectOptions, err = containercmd.InspectOptions(cmd) if err != nil { return err } } var errs []error var entries []interface{} for _, req := range args { var ni int var nc int if inspectImage { ni, err = imagewalker.Walk(ctx, req) if err != nil { return err } } if inspectContainer { nc, err = containerwalker.Walk(ctx, req) if err != nil { return err } } if ni == 0 && nc == 0 { errs = append(errs, fmt.Errorf("no such object %s", req)) } else if ni > 0 { if imageEntries, err := image.Inspect(ctx, client, []string{req}, imageInspectOptions); err != nil { errs = append(errs, err) } else { entries = append(entries, imageEntries...) } } else if nc > 0 { if containerEntries, err := container.Inspect(ctx, client, []string{req}, containerInspectOptions); err != nil { errs = append(errs, err) } else { entries = append(entries, containerEntries...) } } } if len(errs) > 0 { return fmt.Errorf("%d errors: %v", len(errs), errs) } if formatErr := formatter.FormatSlice(format, cmd.OutOrStdout(), entries); formatErr != nil { log.G(ctx).Error(formatErr) } return nil } func inspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show container names containers, _ := completion.ContainerNames(cmd, nil) // show image names images, _ := completion.ImageNames(cmd) return append(containers, images...), cobra.ShellCompDirectiveNoFileComp } ================================================ FILE: cmd/nerdctl/inspect/inspect_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package inspect import ( "encoding/json" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestMain(m *testing.M) { testutil.M(m) } func TestInspectSimpleCase(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Description: "inspect container and image return one single json array", Setup: func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() helpers.Ensure("run", "-d", "--quiet", "--name", identifier, testutil.CommonImage, "sleep", nerdtest.Infinity) }, Cleanup: func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() helpers.Anyhow("rm", "-f", identifier) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("inspect", testutil.CommonImage, data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var inspectResult []json.RawMessage err := json.Unmarshal([]byte(stdout), &inspectResult) assert.NilError(t, err, "Unable to unmarshal output\n") assert.Equal(t, len(inspectResult), 2, "Unexpectedly got multiple results\n") var dci dockercompat.Image err = json.Unmarshal(inspectResult[0], &dci) assert.NilError(t, err, "Unable to unmarshal output\n") inspecti := nerdtest.InspectImage(helpers, testutil.CommonImage) assert.Equal(t, dci.ID, inspecti.ID, "id should match\n") var dcc dockercompat.Container err = json.Unmarshal(inspectResult[1], &dcc) assert.NilError(t, err, "Unable to unmarshal output\n") inspectc := nerdtest.InspectContainer(helpers, data.Identifier()) assert.Equal(t, dcc.ID, inspectc.ID, "id should match\n") }, } }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/internal/internal.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package internal import ( "github.com/spf13/cobra" ) func Command() *cobra.Command { var cmd = &cobra.Command{ Use: "internal", Short: "DO NOT EXECUTE MANUALLY", Hidden: true, SilenceUsage: true, SilenceErrors: true, } cmd.AddCommand( newInternalOCIHookCommandCommand(), ) return cmd } ================================================ FILE: cmd/nerdctl/internal/internal_oci_hook.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package internal import ( "errors" "os" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/ocihook" ) func newInternalOCIHookCommandCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "oci-hook", Short: "OCI hook", RunE: internalOCIHookAction, SilenceUsage: true, SilenceErrors: true, } return cmd } func internalOCIHookAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } event := "" if len(args) > 0 { event = args[0] } if event == "" { return errors.New("event type needs to be passed") } dataStore, err := clientutil.DataStore(globalOptions.DataRoot, globalOptions.Address) if err != nil { return err } cniPath := globalOptions.CNIPath cniNetconfpath := globalOptions.CNINetConfPath bridgeIP := globalOptions.BridgeIP return ocihook.Run(os.Stdin, os.Stderr, event, dataStore, cniPath, cniNetconfpath, bridgeIP, ) } ================================================ FILE: cmd/nerdctl/ipfs/ipfs.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ipfs import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" ) func NewIPFSCommand() *cobra.Command { cmd := &cobra.Command{ Annotations: map[string]string{helpers.Category: helpers.Management}, Use: "ipfs", Short: "Distributing images on IPFS", RunE: helpers.UnknownSubcommandAction, SilenceUsage: true, SilenceErrors: true, } cmd.AddCommand( newIPFSRegistryCommand(), ) return cmd } ================================================ FILE: cmd/nerdctl/ipfs/ipfs_compose_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ipfs import ( "fmt" "io" "os" "strconv" "strings" "testing" "time" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" "github.com/containerd/nerdctl/v2/pkg/testutil/portlock" ) func TestIPFSCompNoBuild(t *testing.T) { testCase := nerdtest.Setup() const ipfsAddrKey = "ipfsAddrKey" var ipfsRegistry *registry.Server testCase.Require = require.All( require.Linux, require.Not(nerdtest.Docker), nerdtest.Registry, nerdtest.IPFS, nerdtest.IsFlaky("https://github.com/containerd/nerdctl/issues/3510"), // See note below // nerdtest.Private, ) testCase.Setup = func(data test.Data, helpers test.Helpers) { // Start Kubo ipfsRegistry = registry.NewKuboRegistry(data, helpers, t, nil, 0, nil) ipfsRegistry.Setup(data, helpers) data.Labels().Set(ipfsAddrKey, fmt.Sprintf("/ip4/%s/tcp/%d", ipfsRegistry.IP, ipfsRegistry.Port)) // Ensure we have the images helpers.Ensure("pull", "--quiet", testutil.WordpressImage) helpers.Ensure("pull", "--quiet", testutil.MariaDBImage) } testCase.SubTests = []*test.Case{ subtestTestIPFSCompNoB(t, false, false), subtestTestIPFSCompNoB(t, true, false), subtestTestIPFSCompNoB(t, false, true), } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if ipfsRegistry != nil { ipfsRegistry.Cleanup(data, helpers) } helpers.Anyhow("rmi", "-f", testutil.WordpressImage) helpers.Anyhow("rmi", "-f", testutil.MariaDBImage) } testCase.Run(t) } func subtestTestIPFSCompNoB(t *testing.T, stargz bool, byAddr bool) *test.Case { t.Helper() const ipfsAddrKey = "ipfsAddrKey" const mariaImageCIDKey = "mariaImageCIDKey" const wordpressImageCIDKey = "wordpressImageCIDKey" const composeExtraKey = "composeExtraKey" testCase := &test.Case{} testCase.Description += "with" if !stargz { testCase.Description += "-no" } testCase.Description += "-stargz" if !byAddr { testCase.Description += "-no" } testCase.Description += "-byAddr" if stargz { testCase.Require = nerdtest.Stargz } testCase.Setup = func(data test.Data, helpers test.Helpers) { var ipfsCIDWP, ipfsCIDMD string if stargz { ipfsCIDWP = pushToIPFS(helpers, testutil.WordpressImage, "--estargz") ipfsCIDMD = pushToIPFS(helpers, testutil.MariaDBImage, "--estargz") } else if byAddr { ipfsCIDWP = pushToIPFS(helpers, testutil.WordpressImage, "--ipfs-address="+data.Labels().Get(ipfsAddrKey)) ipfsCIDMD = pushToIPFS(helpers, testutil.MariaDBImage, "--ipfs-address="+data.Labels().Get(ipfsAddrKey)) data.Labels().Set(composeExtraKey, "--ipfs-address="+data.Labels().Get(ipfsAddrKey)) } else { ipfsCIDWP = pushToIPFS(helpers, testutil.WordpressImage) ipfsCIDMD = pushToIPFS(helpers, testutil.MariaDBImage) } data.Labels().Set(wordpressImageCIDKey, ipfsCIDWP) data.Labels().Set(mariaImageCIDKey, ipfsCIDMD) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { // NOTE: // Removing these images locally forces tests to be sequentials (as IPFS being content addressable, // they have the same cid - except for the estargz version obviously) // Deliberately electing to not remove them here so that we can parallelize and cut down the running time /* if data.Labels().Get(mariaImageCIDKey) != "" { helpers.Anyhow("rmi", "-f", data.Labels().Get(mariaImageCIDKey)) helpers.Anyhow("rmi", "-f", data.Labels().Get(wordpressImageCIDKey)) } */ } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { safePort, err := portlock.Acquire(0) assert.NilError(helpers.T(), err) data.Labels().Set("wordpressPort", strconv.Itoa(safePort)) composeUP(data, helpers, fmt.Sprintf(` version: '3.1' services: wordpress: image: ipfs://%s restart: always ports: - %d:80 environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser WORDPRESS_DB_PASSWORD: examplepass WORDPRESS_DB_NAME: exampledb # FIXME: this is flaky and will make the container fail on occasions volumes: - wordpress:/var/www/html db: image: ipfs://%s restart: always environment: MYSQL_DATABASE: exampledb MYSQL_USER: exampleuser MYSQL_PASSWORD: examplepass MYSQL_RANDOM_ROOT_PASSWORD: '1' volumes: - db:/var/lib/mysql volumes: wordpress: db: `, data.Labels().Get(wordpressImageCIDKey), safePort, data.Labels().Get(mariaImageCIDKey)), data.Labels().Get(composeExtraKey)) // FIXME: need to break down composeUP into testable commands instead // Right now, this is just a dummy placeholder return helpers.Command("info") } testCase.Expected = test.Expects(0, nil, nil) return testCase } func TestIPFSCompBuild(t *testing.T) { testCase := nerdtest.Setup() var ipfsServer test.TestableCommand var comp *testutil.ComposeDir const mainImageCIDKey = "mainImageCIDKey" safePort, err := portlock.Acquire(0) assert.NilError(t, err) var listenAddr = "localhost:" + strconv.Itoa(safePort) testCase.Require = require.All( // Linux only require.Linux, // Obviously not docker supported require.Not(nerdtest.Docker), nerdtest.Build, nerdtest.IPFS, ) testCase.Setup = func(data test.Data, helpers test.Helpers) { // Get alpine helpers.Ensure("pull", "--quiet", testutil.NginxAlpineImage) // Start a local ipfs backed registry // FIXME: this is bad and likely to collide with other tests ipfsServer = helpers.Command("ipfs", "registry", "serve", "--listen-registry", listenAddr) // This should not take longer than that ipfsServer.WithTimeout(30 * time.Second) ipfsServer.Background() // Apparently necessary to let it start... time.Sleep(time.Second) // Save nginx to ipfs data.Labels().Set(mainImageCIDKey, pushToIPFS(helpers, testutil.NginxAlpineImage)) const dockerComposeYAML = ` services: web: build: . ports: - 8081:80 ` dockerfile := fmt.Sprintf(`FROM %s/ipfs/%s COPY index.html /usr/share/nginx/html/index.html `, listenAddr, data.Labels().Get(mainImageCIDKey)) comp = testutil.NewComposeDir(t, dockerComposeYAML) comp.WriteFile("Dockerfile", dockerfile) comp.WriteFile("index.html", data.Identifier("indexhtml")) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if ipfsServer != nil { helpers.Anyhow("rmi", "-f", data.Labels().Get(mainImageCIDKey)) ipfsServer.Signal(os.Kill) } if comp != nil { helpers.Anyhow("compose", "-f", comp.YAMLFullPath(), "down", "-v") comp.CleanUp() } } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", comp.YAMLFullPath(), "up", "-d", "--build") } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { resp, err := nettestutil.HTTPGet("http://127.0.0.1:8081", 5, false) assert.NilError(t, err) respBody, err := io.ReadAll(resp.Body) assert.NilError(t, err) t.Log(fmt.Sprintf("respBody=%q", respBody)) assert.Assert(t, strings.Contains(string(respBody), data.Identifier("indexhtml"))) }, } } testCase.Run(t) } func composeUP(data test.Data, helpers test.Helpers, dockerComposeYAML string, opts string) { comp := testutil.NewComposeDir(helpers.T(), dockerComposeYAML) // defer comp.CleanUp() // Because it might or might not happen, and helpers.Anyhow("compose", "-f", comp.YAMLFullPath(), "down", "-v") defer helpers.Anyhow("compose", "-f", comp.YAMLFullPath(), "down", "-v") projectName := comp.ProjectName() args := []string{"compose", "-f", comp.YAMLFullPath()} if opts != "" { args = append(args, opts) } helpers.Ensure(append(args, "up", "--quiet-pull", "-d")...) helpers.Ensure("volume", "inspect", fmt.Sprintf("%s_db", projectName)) helpers.Ensure("network", "inspect", fmt.Sprintf("%s_default", projectName)) checkWordpress := func() error { // FIXME: see other notes on using the same port repeatedly resp, err := nettestutil.HTTPGet("http://127.0.0.1:"+data.Labels().Get("wordpressPort"), 5, false) if err != nil { return err } respBody, err := io.ReadAll(resp.Body) if err != nil { return err } if !strings.Contains(string(respBody), testutil.WordpressIndexHTMLSnippet) { return fmt.Errorf("respBody does not contain %q (%s)", testutil.WordpressIndexHTMLSnippet, string(respBody)) } return nil } var wordpressWorking bool var err error // 15 seconds is long enough for i := 0; i < 5; i++ { err = checkWordpress() if err == nil { wordpressWorking = true break } time.Sleep(3 * time.Second) } if !wordpressWorking { ccc := helpers.Capture("ps", "-a") helpers.T().Log(ccc) helpers.T().Log(helpers.Err("logs", projectName+"-wordpress-1")) helpers.T().Log(fmt.Sprintf("wordpress is not working %v", err)) helpers.T().FailNow() } helpers.Ensure("compose", "-f", comp.YAMLFullPath(), "down", "-v") helpers.Fail("volume", "inspect", fmt.Sprintf("%s_db", projectName)) helpers.Fail("network", "inspect", fmt.Sprintf("%s_default", projectName)) } ================================================ FILE: cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ipfs import ( "fmt" "regexp" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" ) func TestIPFSAddrWithKubo(t *testing.T) { testCase := nerdtest.Setup() const mainImageCIDKey = "mainImagemainImageCIDKey" const ipfsAddrKey = "ipfsAddrKey" var ipfsRegistry *registry.Server testCase.Require = require.All( require.Linux, require.Not(nerdtest.Docker), nerdtest.Registry, nerdtest.Private, ) testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) ipfsRegistry = registry.NewKuboRegistry(data, helpers, t, nil, 0, nil) ipfsRegistry.Setup(data, helpers) ipfsAddr := fmt.Sprintf("/ip4/%s/tcp/%d", ipfsRegistry.IP, ipfsRegistry.Port) data.Labels().Set(ipfsAddrKey, ipfsAddr) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if ipfsRegistry != nil { ipfsRegistry.Cleanup(data, helpers) } } testCase.SubTests = []*test.Case{ { Description: "with default snapshotter", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { ipfsCID := pushToIPFS(helpers, testutil.CommonImage, fmt.Sprintf("--ipfs-address=%s", data.Labels().Get(ipfsAddrKey))) helpers.Ensure("pull", "--quiet", "--ipfs-address", data.Labels().Get(ipfsAddrKey), "ipfs://"+ipfsCID) data.Labels().Set(mainImageCIDKey, ipfsCID) }, Cleanup: func(data test.Data, helpers test.Helpers) { if data.Labels().Get(mainImageCIDKey) != "" { helpers.Anyhow("rmi", "-f", data.Labels().Get(mainImageCIDKey)) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Labels().Get(mainImageCIDKey), "echo", "hello") }, Expected: test.Expects(0, nil, expect.Equals("hello\n")), }, { Description: "with stargz snapshotter", NoParallel: true, Require: require.All( nerdtest.Stargz, nerdtest.Private, nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/3475"), ), Setup: func(data test.Data, helpers test.Helpers) { ipfsCID := pushToIPFS(helpers, testutil.CommonImage, fmt.Sprintf("--ipfs-address=%s", data.Labels().Get(ipfsAddrKey)), "--estargz") helpers.Ensure("pull", "--quiet", "--ipfs-address", data.Labels().Get(ipfsAddrKey), "ipfs://"+ipfsCID) data.Labels().Set(mainImageCIDKey, ipfsCID) }, Cleanup: func(data test.Data, helpers test.Helpers) { if data.Labels().Get(mainImageCIDKey) != "" { helpers.Anyhow("rmi", "-f", data.Labels().Get(mainImageCIDKey)) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Labels().Get(mainImageCIDKey), "ls", "/.stargz-snapshotter") }, Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile("sha256:.*[.]json[\n]"))), }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/ipfs/ipfs_registry.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ipfs import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" ) func newIPFSRegistryCommand() *cobra.Command { cmd := &cobra.Command{ Annotations: map[string]string{helpers.Category: helpers.Management}, Use: "registry", Short: "Manage read-only registry backed by IPFS", PreRunE: helpers.CheckExperimental("ipfs"), RunE: helpers.UnknownSubcommandAction, SilenceUsage: true, SilenceErrors: true, } cmd.AddCommand( newIPFSRegistryServeCommand(), ) return cmd } ================================================ FILE: cmd/nerdctl/ipfs/ipfs_registry_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ipfs import ( "fmt" "os" "regexp" "strings" "testing" "time" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func pushToIPFS(helpers test.Helpers, name string, opts ...string) string { var ipfsCID string cmd := helpers.Command("push", "ipfs://"+name) cmd.WithArgs(opts...) cmd.Run(&test.Expected{ Output: func(stdout string, t tig.T) { lines := strings.Split(stdout, "\n") assert.Equal(t, len(lines) >= 2, true) ipfsCID = lines[len(lines)-2] }, }) return ipfsCID } func TestIPFSNerdctlRegistry(t *testing.T) { testCase := nerdtest.Setup() // FIXME: this is bad and likely to collide with other tests const listenAddr = "localhost:5555" const ipfsImageURLKey = "ipfsImageURLKey" var ipfsServer test.TestableCommand testCase.Require = require.All( require.Linux, require.Not(nerdtest.Docker), nerdtest.IPFS, ) testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) // Start a local ipfs backed registry ipfsServer = helpers.Command("ipfs", "registry", "serve", "--listen-registry", listenAddr) // This should not take longer than that ipfsServer.WithTimeout(30 * time.Second) ipfsServer.Background() // Apparently necessary to let it start... time.Sleep(time.Second) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if ipfsServer != nil { // Close the server once done ipfsServer.Signal(os.Kill) } } testCase.SubTests = []*test.Case{ { Description: "with default snapshotter", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { data.Labels().Set(ipfsImageURLKey, listenAddr+"/ipfs/"+pushToIPFS(helpers, testutil.CommonImage)) helpers.Ensure("pull", "--quiet", data.Labels().Get(ipfsImageURLKey)) }, Cleanup: func(data test.Data, helpers test.Helpers) { if data.Labels().Get(ipfsImageURLKey) != "" { helpers.Anyhow("rmi", "-f", data.Labels().Get(ipfsImageURLKey)) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Labels().Get(ipfsImageURLKey), "echo", "hello") }, Expected: test.Expects(0, nil, expect.Equals("hello\n")), }, { Description: "with stargz snapshotterr", NoParallel: true, Require: nerdtest.Stargz, Setup: func(data test.Data, helpers test.Helpers) { data.Labels().Set(ipfsImageURLKey, listenAddr+"/ipfs/"+pushToIPFS(helpers, testutil.CommonImage, "--estargz")) helpers.Ensure("pull", "--quiet", data.Labels().Get(ipfsImageURLKey)) }, Cleanup: func(data test.Data, helpers test.Helpers) { if data.Labels().Get(ipfsImageURLKey) != "" { helpers.Anyhow("rmi", "-f", data.Labels().Get(ipfsImageURLKey)) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Labels().Get(ipfsImageURLKey), "ls", "/.stargz-snapshotter") }, Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile("sha256:.*[.]json[\n]"))), }, { Description: "with build", NoParallel: true, Require: nerdtest.Build, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier("built-image")) if data.Labels().Get(ipfsImageURLKey) != "" { helpers.Anyhow("rmi", "-f", data.Labels().Get(ipfsImageURLKey)) } }, Setup: func(data test.Data, helpers test.Helpers) { data.Labels().Set(ipfsImageURLKey, listenAddr+"/ipfs/"+pushToIPFS(helpers, testutil.CommonImage)) dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-test-string"] `, data.Labels().Get(ipfsImageURLKey)) buildCtx := data.Temp().Path() data.Temp().Save(dockerfile, "Dockerfile") helpers.Ensure("build", "-t", data.Identifier("built-image"), buildCtx) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier("built-image")) }, Expected: test.Expects(0, nil, expect.Equals("nerdctl-build-test-string\n")), }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/ipfs/ipfs_registry_serve.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ipfs import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/cmd/ipfs" ) const ( defaultIPFSRegistry = "localhost:5050" defaultIPFSReadRetryNum = 0 defaultIPFSReadTimeoutDuration = 0 ) func newIPFSRegistryServeCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "serve", Short: "serve read-only registry backed by IPFS on localhost.", RunE: ipfsRegistryServeAction, SilenceUsage: true, SilenceErrors: true, } helpers.AddStringFlag(cmd, "listen-registry", nil, defaultIPFSRegistry, "IPFS_REGISTRY_SERVE_LISTEN_REGISTRY", "address to listen") helpers.AddStringFlag(cmd, "ipfs-address", nil, "", "IPFS_REGISTRY_SERVE_IPFS_ADDRESS", "multiaddr of IPFS API (default is pulled from $IPFS_PATH/api file. If $IPFS_PATH env var is not present, it defaults to ~/.ipfs)") helpers.AddIntFlag(cmd, "read-retry-num", nil, defaultIPFSReadRetryNum, "IPFS_REGISTRY_SERVE_READ_RETRY_NUM", "times to retry query on IPFS. Zero or lower means no retry.") helpers.AddDurationFlag(cmd, "read-timeout", nil, defaultIPFSReadTimeoutDuration, "IPFS_REGISTRY_SERVE_READ_TIMEOUT", "timeout duration of a read request to IPFS. Zero means no timeout.") return cmd } func processIPFSRegistryServeOptions(cmd *cobra.Command) (opts types.IPFSRegistryServeOptions, err error) { ipfsAddressStr, err := cmd.Flags().GetString("ipfs-address") if err != nil { return types.IPFSRegistryServeOptions{}, err } listenAddress, err := cmd.Flags().GetString("listen-registry") if err != nil { return types.IPFSRegistryServeOptions{}, err } readTimeout, err := cmd.Flags().GetDuration("read-timeout") if err != nil { return types.IPFSRegistryServeOptions{}, err } readRetryNum, err := cmd.Flags().GetInt("read-retry-num") if err != nil { return types.IPFSRegistryServeOptions{}, err } return types.IPFSRegistryServeOptions{ ListenRegistry: listenAddress, IPFSAddress: ipfsAddressStr, ReadTimeout: readTimeout, ReadRetryNum: readRetryNum, }, nil } func ipfsRegistryServeAction(cmd *cobra.Command, args []string) error { options, err := processIPFSRegistryServeOptions(cmd) if err != nil { return err } return ipfs.RegistryServe(options) } ================================================ FILE: cmd/nerdctl/ipfs/ipfs_simple_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ipfs import ( "regexp" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestIPFSSimple(t *testing.T) { testCase := nerdtest.Setup() const mainImageCIDKey = "mainImageCIDKey" const transformedImageCIDKey = "transformedImageCIDKey" testCase.Require = require.All( require.Linux, require.Not(nerdtest.Docker), nerdtest.IPFS, // We constantly rmi the image by its CID which is shared across tests, so, we make this group private // and every subtest NoParallel nerdtest.Private, ) testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) } testCase.SubTests = []*test.Case{ { Description: "with default snapshotter", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { data.Labels().Set(mainImageCIDKey, pushToIPFS(helpers, testutil.CommonImage)) helpers.Ensure("pull", "--quiet", "ipfs://"+data.Labels().Get(mainImageCIDKey)) }, Cleanup: func(data test.Data, helpers test.Helpers) { if data.Labels().Get(mainImageCIDKey) != "" { helpers.Anyhow("rmi", "-f", data.Labels().Get(mainImageCIDKey)) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Labels().Get(mainImageCIDKey), "echo", "hello") }, Expected: test.Expects(0, nil, expect.Equals("hello\n")), }, { Description: "with stargz snapshotter", NoParallel: true, Require: require.All( nerdtest.Stargz, nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/3475"), ), Setup: func(data test.Data, helpers test.Helpers) { data.Labels().Set(mainImageCIDKey, pushToIPFS(helpers, testutil.CommonImage, "--estargz")) helpers.Ensure("pull", "--quiet", "ipfs://"+data.Labels().Get(mainImageCIDKey)) }, Cleanup: func(data test.Data, helpers test.Helpers) { if data.Labels().Get(mainImageCIDKey) != "" { helpers.Anyhow("rmi", "-f", data.Labels().Get(mainImageCIDKey)) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Labels().Get(mainImageCIDKey), "ls", "/.stargz-snapshotter") }, Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile("sha256:.*[.]json[\n]"))), }, { Description: "with commit and push", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { data.Labels().Set(mainImageCIDKey, pushToIPFS(helpers, testutil.CommonImage)) helpers.Ensure("pull", "--quiet", "ipfs://"+data.Labels().Get(mainImageCIDKey)) // Run a container that does modify something, then commit and push it helpers.Ensure("run", "--name", data.Identifier("commit-container"), data.Labels().Get(mainImageCIDKey), "sh", "-c", "--", "echo hello > /hello") helpers.Ensure("commit", data.Identifier("commit-container"), data.Identifier("commit-image")) data.Labels().Set(transformedImageCIDKey, pushToIPFS(helpers, data.Identifier("commit-image"))) // Clean-up helpers.Ensure("rm", data.Identifier("commit-container")) helpers.Ensure("rmi", data.Identifier("commit-image")) // Pull back the committed image helpers.Ensure("pull", "--quiet", "ipfs://"+data.Labels().Get(transformedImageCIDKey)) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("commit-container")) helpers.Anyhow("rmi", "-f", data.Identifier("commit-image")) if data.Labels().Get(mainImageCIDKey) != "" { helpers.Anyhow("rmi", "-f", data.Labels().Get(mainImageCIDKey)) helpers.Anyhow("rmi", "-f", data.Labels().Get(transformedImageCIDKey)) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Labels().Get(transformedImageCIDKey), "cat", "/hello") }, Expected: test.Expects(0, nil, expect.Equals("hello\n")), }, { Description: "with commit and push, stargz lazy pulling", NoParallel: true, Require: require.All( nerdtest.Stargz, nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/3475"), ), Setup: func(data test.Data, helpers test.Helpers) { data.Labels().Set(mainImageCIDKey, pushToIPFS(helpers, testutil.CommonImage, "--estargz")) helpers.Ensure("pull", "--quiet", "ipfs://"+data.Labels().Get(mainImageCIDKey)) // Run a container that does modify something, then commit and push it helpers.Ensure("run", "--name", data.Identifier("commit-container"), data.Labels().Get(mainImageCIDKey), "sh", "-c", "--", "echo hello > /hello") helpers.Ensure("commit", data.Identifier("commit-container"), data.Identifier("commit-image")) data.Labels().Set(transformedImageCIDKey, pushToIPFS(helpers, data.Identifier("commit-image"))) // Clean-up helpers.Ensure("rm", data.Identifier("commit-container")) helpers.Ensure("rmi", data.Identifier("commit-image")) // Pull back the image helpers.Ensure("pull", "--quiet", "ipfs://"+data.Labels().Get(transformedImageCIDKey)) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("commit-container")) helpers.Anyhow("rmi", "-f", data.Identifier("commit-image")) if data.Labels().Get(mainImageCIDKey) != "" { helpers.Anyhow("rmi", "-f", data.Labels().Get(mainImageCIDKey)) helpers.Anyhow("rmi", "-f", data.Labels().Get(transformedImageCIDKey)) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Labels().Get(transformedImageCIDKey), "sh", "-c", "--", "cat /hello && ls /.stargz-snapshotter") }, Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile("hello[\n]sha256:.*[.]json[\n]"))), }, { Description: "with encryption", NoParallel: true, Require: require.Binary("openssl"), Setup: func(data test.Data, helpers test.Helpers) { data.Labels().Set(mainImageCIDKey, pushToIPFS(helpers, testutil.CommonImage)) helpers.Ensure("pull", "--quiet", "ipfs://"+data.Labels().Get(mainImageCIDKey)) // Prep a key pair pri, pub := nerdtest.GenerateJWEKeyPair(data, helpers) data.Labels().Set("prv", pri) data.Labels().Set("pub", pub) // Encrypt the image, and verify it is encrypted helpers.Ensure("image", "encrypt", "--recipient=jwe:"+pub, data.Labels().Get(mainImageCIDKey), data.Identifier("encrypted")) cmd := helpers.Command("image", "inspect", "--mode=native", "--format={{len .Index.Manifests}}", data.Identifier("encrypted")) cmd.Run(&test.Expected{ Output: expect.Equals("1\n"), }) cmd = helpers.Command("image", "inspect", "--mode=native", "--format={{json (index .Manifest.Layers 0) }}", data.Identifier("encrypted")) cmd.Run(&test.Expected{ Output: expect.Contains("org.opencontainers.image.enc.keys.jwe"), }) // Push the encrypted image and save the CID data.Labels().Set(transformedImageCIDKey, pushToIPFS(helpers, data.Identifier("encrypted"))) // Remove both images locally helpers.Ensure("rmi", "-f", data.Labels().Get(mainImageCIDKey)) helpers.Ensure("rmi", "-f", data.Labels().Get(transformedImageCIDKey)) // Pull back without unpacking helpers.Ensure("pull", "--quiet", "--unpack=false", "ipfs://"+data.Labels().Get(transformedImageCIDKey)) }, Cleanup: func(data test.Data, helpers test.Helpers) { if data.Labels().Get(mainImageCIDKey) != "" { helpers.Anyhow("rmi", "-f", data.Labels().Get(mainImageCIDKey)) helpers.Anyhow("rmi", "-f", data.Labels().Get(transformedImageCIDKey)) } }, SubTests: []*test.Case{ { Description: "decrypt with pub key does not work", Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("decrypted")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("image", "decrypt", "--key="+data.Labels().Get("pub"), data.Labels().Get(transformedImageCIDKey), data.Identifier("decrypted")) }, Expected: test.Expects(1, nil, nil), }, { Description: "decrypt with priv key does work", Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("decrypted")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("image", "decrypt", "--key="+data.Labels().Get("prv"), data.Labels().Get(transformedImageCIDKey), data.Identifier("decrypted")) }, Expected: test.Expects(0, nil, nil), }, }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/ipfs/ipfs_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ipfs import ( "testing" "github.com/containerd/nerdctl/v2/pkg/testutil" ) func TestMain(m *testing.M) { testutil.M(m) } ================================================ FILE: cmd/nerdctl/issues/issues_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package issues is meant to document testing for complex scenarios type of issues that cannot simply be ascribed // to a specific package. package issues import ( "fmt" "testing" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" ) func TestIssue3425(t *testing.T) { nerdtest.Setup() var reg *registry.Server testCase := &test.Case{ Require: nerdtest.Registry, Setup: func(data test.Data, helpers test.Helpers) { reg = nerdtest.RegistryWithNoAuth(data, helpers, 0, false) reg.Setup(data, helpers) }, Cleanup: func(data test.Data, helpers test.Helpers) { if reg != nil { reg.Cleanup(data, helpers) } }, SubTests: []*test.Case{ { Description: "with tag", Require: nerdtest.Private, Setup: func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() helpers.Ensure("image", "pull", "--quiet", testutil.CommonImage) helpers.Ensure("run", "-d", "--name", identifier, testutil.CommonImage) helpers.Ensure("image", "rm", "-f", testutil.CommonImage) helpers.Ensure("image", "pull", "--quiet", testutil.CommonImage) helpers.Ensure("tag", testutil.CommonImage, fmt.Sprintf("localhost:%d/%s", reg.Port, identifier)) }, Cleanup: func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() helpers.Anyhow("rm", "-f", identifier) helpers.Anyhow("rmi", "-f", fmt.Sprintf("localhost:%d/%s", reg.Port, identifier)) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("push", fmt.Sprintf("localhost:%d/%s", reg.Port, data.Identifier())) }, Expected: test.Expects(0, nil, nil), }, { Description: "with commit", Require: nerdtest.Private, Setup: func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() helpers.Ensure("image", "pull", "--quiet", testutil.CommonImage) helpers.Ensure("run", "-d", "--name", identifier, testutil.CommonImage, "touch", "/something") helpers.Ensure("image", "rm", "-f", testutil.CommonImage) helpers.Ensure("image", "pull", "--quiet", testutil.CommonImage) helpers.Ensure("commit", identifier, fmt.Sprintf("localhost:%d/%s", reg.Port, identifier)) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("rmi", "-f", fmt.Sprintf("localhost:%d/%s", reg.Port, data.Identifier())) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("push", fmt.Sprintf("localhost:%d/%s", reg.Port, data.Identifier())) }, Expected: test.Expects(0, nil, nil), }, { Description: "with save", Require: nerdtest.Private, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("image", "pull", "--quiet", testutil.CommonImage) helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage) helpers.Ensure("image", "rm", "-f", testutil.CommonImage) helpers.Ensure("image", "pull", "--quiet", testutil.CommonImage) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("save", testutil.CommonImage) }, Expected: test.Expects(0, nil, nil), }, { Description: "with convert", Require: require.All( nerdtest.Private, require.Not(require.Windows), require.Not(nerdtest.Docker), ), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("image", "pull", "--quiet", testutil.CommonImage) helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage) helpers.Ensure("image", "rm", "-f", testutil.CommonImage) helpers.Ensure("image", "pull", "--quiet", testutil.CommonImage) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("rmi", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("image", "convert", "--oci", "--estargz", testutil.CommonImage, data.Identifier()) }, Expected: test.Expects(0, nil, nil), }, { Description: "with ipfs", Require: require.All( nerdtest.Private, nerdtest.IPFS, require.Not(require.Windows), require.Not(nerdtest.Docker), ), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("image", "pull", "--quiet", testutil.CommonImage) helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage) helpers.Ensure("image", "rm", "-f", testutil.CommonImage) helpers.Ensure("image", "pull", "--quiet", testutil.CommonImage) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("rmi", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("image", "push", "ipfs://"+testutil.CommonImage) }, Expected: test.Expects(0, nil, nil), }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/issues/main_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package issues import ( "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestMain(m *testing.M) { testutil.M(m) } // TestIssue108 tests https://github.com/containerd/nerdctl/issues/108 // ("`nerdctl run --net=host -it` fails while `nerdctl run -it --net=host` works") func TestIssue108(t *testing.T) { testCase := nerdtest.Setup() testCase.SubTests = []*test.Case{ { Description: "-it --net=host", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("run", "--quiet", "-it", "--rm", "--net=host", testutil.CommonImage, "echo", "this was always working") cmd.WithPseudoTTY() return cmd }, Expected: test.Expects(0, nil, expect.Equals("this was always working\r\n")), }, { Description: "--net=host -it", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("run", "--quiet", "--rm", "--net=host", "-it", testutil.CommonImage, "echo", "this was not working due to issue #108") cmd.WithPseudoTTY() return cmd }, Expected: test.Expects(0, nil, expect.Equals("this was not working due to issue #108\r\n")), }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/login/login.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package login import ( "errors" "io" "strings" "github.com/spf13/cobra" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/cmd/login" ) func Command() *cobra.Command { var cmd = &cobra.Command{ Use: "login [flags] [SERVER]", Args: cobra.MaximumNArgs(1), Short: "Log in to a container registry", RunE: loginAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().StringP("username", "u", "", "Username") cmd.Flags().StringP("password", "p", "", "Password") cmd.Flags().Bool("password-stdin", false, "Take the password from stdin") return cmd } func loginOptions(cmd *cobra.Command) (types.LoginCommandOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.LoginCommandOptions{}, err } username, err := cmd.Flags().GetString("username") if err != nil { return types.LoginCommandOptions{}, err } password, err := cmd.Flags().GetString("password") if err != nil { return types.LoginCommandOptions{}, err } passwordStdin, err := cmd.Flags().GetBool("password-stdin") if err != nil { return types.LoginCommandOptions{}, err } if strings.Contains(username, ":") { return types.LoginCommandOptions{}, errors.New("username cannot contain colons") } if password != "" { log.L.Warn("WARNING! Using --password via the CLI is insecure. Use --password-stdin.") if passwordStdin { return types.LoginCommandOptions{}, errors.New("--password and --password-stdin are mutually exclusive") } } if passwordStdin { if username == "" { return types.LoginCommandOptions{}, errors.New("must provide --username with --password-stdin") } contents, err := io.ReadAll(cmd.InOrStdin()) if err != nil { return types.LoginCommandOptions{}, err } password = strings.TrimSuffix(string(contents), "\n") password = strings.TrimSuffix(password, "\r") } return types.LoginCommandOptions{ GOptions: globalOptions, Username: username, Password: password, }, nil } func loginAction(cmd *cobra.Command, args []string) error { options, err := loginOptions(cmd) if err != nil { return err } if len(args) == 1 { options.ServerAddress = args[0] } return login.Login(cmd.Context(), options, cmd.OutOrStdout()) } ================================================ FILE: cmd/nerdctl/login/login_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // https://docs.docker.com/reference/cli/dockerd/#insecure-registries // Local registries, whose IP address falls in the 127.0.0.0/8 range, are automatically marked as insecure as of Docker 1.3.2. // It isn't recommended to rely on this, as it may change in the future. // "--insecure" means that either the certificates are untrusted, or that the protocol is plain http package login import ( "fmt" "net" "os" "strconv" "testing" "gotest.tools/v3/icmd" "github.com/containerd/nerdctl/mod/tigron/utils" "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/testca" "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" ) type Client struct { args []string configPath string } func (ag *Client) WithInsecure(value bool) *Client { ag.args = append(ag.args, "--insecure-registry="+strconv.FormatBool(value)) return ag } func (ag *Client) WithHostsDir(hostDirs string) *Client { ag.args = append(ag.args, "--hosts-dir", hostDirs) return ag } func (ag *Client) WithCredentials(username, password string) *Client { if username != "" { ag.args = append(ag.args, "--username", username) } if password != "" { ag.args = append(ag.args, "--password", password) } return ag } func (ag *Client) WithConfigPath(value string) *Client { ag.configPath = value return ag } func (ag *Client) GetConfigPath() string { return ag.configPath } func (ag *Client) Run(base *testutil.Base, host string) *testutil.Cmd { if ag.configPath == "" { ag.configPath, _ = os.MkdirTemp(base.T.TempDir(), "docker-config") } args := []string{"login"} if !nerdtest.IsDocker() { args = append(args, "--debug-full") } args = append(args, ag.args...) icmdCmd := icmd.Command(base.Binary, append(base.Args, append(args, host)...)...) icmdCmd.Env = append(base.Env, "HOME="+os.Getenv("HOME"), "DOCKER_CONFIG="+ag.configPath) return &testutil.Cmd{ Cmd: icmdCmd, Base: base, } } func TestLoginPersistence(t *testing.T) { base := testutil.NewBase(t) t.Parallel() // Retrieve from the store testCases := []struct { auth string }{ { "basic", }, { "token", }, } for _, tc := range testCases { tc := tc t.Run(fmt.Sprintf("Server %s", tc.auth), func(t *testing.T) { t.Parallel() username := utils.RandomStringBase64(30) + "∞" password := utils.RandomStringBase64(30) + ":∞" // Add the requested authentication var auth testregistry.Auth var dependentCleanup func(error) auth = &testregistry.NoAuth{} if tc.auth == "basic" { auth = &testregistry.BasicAuth{ Username: username, Password: password, } } else if tc.auth == "token" { authCa := testca.New(base.T) as := testregistry.NewAuthServer(base, authCa, 0, username, password, false) auth = &testregistry.TokenAuth{ Address: as.Scheme + "://" + net.JoinHostPort(as.IP.String(), strconv.Itoa(as.Port)), CertPath: as.CertPath, } dependentCleanup = as.Cleanup } // Start the registry with the requested options reg := testregistry.NewRegistry(base, nil, 0, auth, dependentCleanup) // Register registry cleanup t.Cleanup(func() { reg.Cleanup(nil) }) // First, login successfully c := (&Client{}). WithCredentials(username, password) c.Run(base, fmt.Sprintf("localhost:%d", reg.Port)). AssertOK() // Now, log in successfully without passing any explicit credentials nc := (&Client{}). WithConfigPath(c.GetConfigPath()) nc.Run(base, fmt.Sprintf("localhost:%d", reg.Port)). AssertOK() // Now fail while using invalid credentials nc.WithCredentials("invalid", "invalid"). Run(base, fmt.Sprintf("localhost:%d", reg.Port)). AssertFail() // And login again without, reverting to the last saved good state nc = (&Client{}). WithConfigPath(c.GetConfigPath()) nc.Run(base, fmt.Sprintf("localhost:%d", reg.Port)). AssertOK() }) } } /* func TestAgainstNoAuth(t *testing.T) { base := testutil.NewBase(t) t.Parallel() // Start the registry with the requested options reg := testregistry.NewRegistry(base, nil, 0, &testregistry.NoAuth{}, nil) // Register registry cleanup t.Cleanup(func() { reg.Cleanup(nil) }) c := (&Client{}). WithCredentials("invalid", "invalid") c.Run(base, fmt.Sprintf("localhost:%d", reg.Port)). AssertOK() content, _ := os.ReadFile(filepath.Join(c.configPath, "config.json")) fmt.Println(string(content)) c.Run(base, fmt.Sprintf("localhost:%d", reg.Port)). AssertFail() } */ func TestLoginAgainstVariants(t *testing.T) { // Skip docker, because Docker doesn't have `--hosts-dir` nor `insecure-registry` option // This will test access to a wide variety of servers, with or without TLS, with basic or token authentication testutil.DockerIncompatible(t) base := testutil.NewBase(t) t.Parallel() testCases := []struct { port int tls bool auth string }{ // Basic auth, no TLS { 80, false, "basic", }, { 443, false, "basic", }, { 0, false, "basic", }, // Token auth, no TLS { 80, false, "token", }, { 443, false, "token", }, { 0, false, "token", }, // Basic auth, with TLS /* // This is not working currently, unless we would force a server https:// in hosts // To be fixed with login rewrite { 80, true, "basic", }, */ { 443, true, "basic", }, { 0, true, "basic", }, // Token auth, with TLS /* // This is not working currently, unless we would force a server https:// in hosts // To be fixed with login rewrite { 80, true, "token", }, */ { 443, true, "token", }, { 0, true, "token", }, } // Iterate through all cases, that will present a variety of port (80, 443, random), TLS (yes or no), and authentication (basic, token) type combinations for _, tc := range testCases { port := tc.port tls := tc.tls auth := tc.auth t.Run(fmt.Sprintf("Login against `tls: %t port: %d auth: %s`", tls, port, auth), func(t *testing.T) { // Tests with fixed ports should not be parallelized (although the port locking mechanism will prevent conflicts) // as their children tests are parallelized, and this might deadlock given the way `Parallel` works if port == 0 { t.Parallel() } // Generate credentials that are specific to each registry, so that we never cross hit another one username := utils.RandomStringBase64(30) + "∞" password := utils.RandomStringBase64(30) + ":∞" // Get a CA if we want TLS var ca *testca.CA if tls { ca = testca.New(base.T) } // Add the requested authenticator var authenticator testregistry.Auth var dependentCleanup func(error) authenticator = &testregistry.NoAuth{} if auth == "basic" { authenticator = &testregistry.BasicAuth{ Username: username, Password: password, } } else if auth == "token" { authCa := ca // We could be on !tls, meaning no ca - but we still need a CA to sign jwt tokens if authCa == nil { authCa = testca.New(base.T) } as := testregistry.NewAuthServer(base, authCa, 0, username, password, tls) authenticator = &testregistry.TokenAuth{ Address: as.Scheme + "://" + net.JoinHostPort(as.IP.String(), strconv.Itoa(as.Port)), CertPath: as.CertPath, } dependentCleanup = as.Cleanup } // Start the registry with the requested options reg := testregistry.NewRegistry(base, ca, port, authenticator, dependentCleanup) // Register registry cleanup t.Cleanup(func() { reg.Cleanup(nil) }) // Any registry is reachable through its ip+port, and localhost variants regHosts := []string{ net.JoinHostPort(reg.IP.String(), strconv.Itoa(reg.Port)), net.JoinHostPort("localhost", strconv.Itoa(reg.Port)), net.JoinHostPort("127.0.0.1", strconv.Itoa(reg.Port)), // TODO: ipv6 // net.JoinHostPort("::1", strconv.Itoa(reg.Port)), } // Registries that use port 443 also allow access without specifying a port if reg.Port == 443 { regHosts = append(regHosts, reg.IP.String()) regHosts = append(regHosts, "localhost") regHosts = append(regHosts, "127.0.0.1") // TODO: ipv6 // regHosts = append(regHosts, "::1") } // Iterate through these hosts access points, and create a test per-variant for _, value := range regHosts { regHost := value t.Run(regHost, func(t *testing.T) { t.Parallel() // 1. test with valid credentials but no access to the CA t.Run("1. valid credentials (no CA) ", func(t *testing.T) { t.Parallel() c := (&Client{}). WithCredentials(username, password) rl, _ := dockerconfigresolver.Parse(regHost) // a. Insecure flag not being set // TODO: remove specialization when we fix the localhost mess if rl.IsLocalhost() && !tls { c.Run(base, regHost). AssertOK() } else { c.Run(base, regHost). AssertFail() } // b. Insecure flag set to false // TODO: remove specialization when we fix the localhost mess if !rl.IsLocalhost() { (&Client{}). WithCredentials(username, password). WithInsecure(false). Run(base, regHost). AssertFail() } // c. Insecure flag set to true // TODO: remove specialization when we fix the localhost mess if !rl.IsLocalhost() || !tls { (&Client{}). WithCredentials(username, password). WithInsecure(true). Run(base, regHost). AssertOK() } }) // 2. test with valid credentials AND access to the CA t.Run("2. valid credentials (with access to server CA)", func(t *testing.T) { t.Parallel() rl, _ := dockerconfigresolver.Parse(regHost) // a. Insecure flag not being set c := (&Client{}). WithCredentials(username, password). WithHostsDir(reg.HostsDir) if tls || rl.IsLocalhost() { c.Run(base, regHost). AssertOK() } else { c.Run(base, regHost). AssertFail() } // b. Insecure flag set to false if tls { c.WithInsecure(false). Run(base, regHost). AssertOK() } else { // TODO: remove specialization when we fix the localhost mess if !rl.IsLocalhost() { c.WithInsecure(false). Run(base, regHost). AssertFail() } } // c. Insecure flag set to true c.WithInsecure(true). Run(base, regHost). AssertOK() }) t.Run("3. valid credentials, any url variant, should always succeed", func(t *testing.T) { t.Parallel() c := (&Client{}). WithCredentials(username, password). WithHostsDir(reg.HostsDir). // Just use insecure here for all servers - it does not matter for what we are testing here // in this case, which is whether we can successfully log in against any of these variants WithInsecure(true) // TODO: remove specialization when we fix the localhost mess rl, _ := dockerconfigresolver.Parse(regHost) if !rl.IsLocalhost() || !tls { c.Run(base, "http://"+regHost).AssertOK() c.Run(base, "https://"+regHost).AssertOK() c.Run(base, "http://"+regHost+"/whatever?foo=bar;foo:bar#foo=bar").AssertOK() c.Run(base, "https://"+regHost+"/whatever?foo=bar&bar=foo;foo=foo+bar:bar#foo=bar").AssertOK() } }) t.Run("4. wrong password should always fail", func(t *testing.T) { t.Parallel() (&Client{}). WithCredentials(username, "invalid"). WithHostsDir(reg.HostsDir). Run(base, regHost). AssertFail() (&Client{}). WithCredentials(username, "invalid"). WithHostsDir(reg.HostsDir). WithInsecure(false). Run(base, regHost). AssertFail() (&Client{}). WithCredentials(username, "invalid"). WithHostsDir(reg.HostsDir). WithInsecure(true). Run(base, regHost). AssertFail() (&Client{}). WithCredentials(username, "invalid"). Run(base, regHost). AssertFail() (&Client{}). WithCredentials(username, "invalid"). WithInsecure(false). Run(base, regHost). AssertFail() (&Client{}). WithCredentials(username, "invalid"). WithInsecure(true). Run(base, regHost). AssertFail() }) t.Run("5. wrong username should always fail", func(t *testing.T) { t.Parallel() (&Client{}). WithCredentials("invalid", password). WithHostsDir(reg.HostsDir). Run(base, regHost). AssertFail() (&Client{}). WithCredentials("invalid", password). WithHostsDir(reg.HostsDir). WithInsecure(false). Run(base, regHost). AssertFail() (&Client{}). WithCredentials("invalid", password). WithHostsDir(reg.HostsDir). WithInsecure(true). Run(base, regHost). AssertFail() (&Client{}). WithCredentials("invalid", password). Run(base, regHost). AssertFail() (&Client{}). WithCredentials("invalid", password). WithInsecure(false). Run(base, regHost). AssertFail() (&Client{}). WithCredentials("invalid", password). WithInsecure(true). Run(base, regHost). AssertFail() }) }) } }) } } ================================================ FILE: cmd/nerdctl/login/login_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package login import ( "testing" "github.com/containerd/nerdctl/v2/pkg/testutil" ) func TestMain(m *testing.M) { testutil.M(m) } ================================================ FILE: cmd/nerdctl/login/logout.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package login import ( "github.com/spf13/cobra" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/cmd/logout" ) func LogoutCommand() *cobra.Command { return &cobra.Command{ Use: "logout [flags] [SERVER]", Args: cobra.MaximumNArgs(1), Short: "Log out from a container registry", RunE: logoutAction, ValidArgsFunction: logoutShellComplete, SilenceUsage: true, SilenceErrors: true, } } func logoutAction(cmd *cobra.Command, args []string) error { logoutServer := "" if len(args) > 0 { logoutServer = args[0] } errGroup, err := logout.Logout(cmd.Context(), logoutServer) if err != nil { log.L.WithError(err).Errorf("Failed to erase credentials for: %s", logoutServer) } if errGroup != nil { log.L.Error("None of the following entries could be found") for _, v := range errGroup { log.L.Errorf("%s", v) } } return err } func logoutShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { candidates, err := logout.ShellCompletion() if err != nil { return nil, cobra.ShellCompDirectiveError } return candidates, cobra.ShellCompDirectiveNoFileComp } ================================================ FILE: cmd/nerdctl/main.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "errors" "fmt" "os" "runtime" "strings" "github.com/fatih/color" "github.com/pelletier/go-toml/v2" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/cmd/nerdctl/builder" "github.com/containerd/nerdctl/v2/cmd/nerdctl/checkpoint" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/compose" "github.com/containerd/nerdctl/v2/cmd/nerdctl/container" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/cmd/nerdctl/image" "github.com/containerd/nerdctl/v2/cmd/nerdctl/inspect" "github.com/containerd/nerdctl/v2/cmd/nerdctl/internal" "github.com/containerd/nerdctl/v2/cmd/nerdctl/ipfs" "github.com/containerd/nerdctl/v2/cmd/nerdctl/login" "github.com/containerd/nerdctl/v2/cmd/nerdctl/manifest" "github.com/containerd/nerdctl/v2/cmd/nerdctl/namespace" "github.com/containerd/nerdctl/v2/cmd/nerdctl/network" "github.com/containerd/nerdctl/v2/cmd/nerdctl/search" "github.com/containerd/nerdctl/v2/cmd/nerdctl/system" "github.com/containerd/nerdctl/v2/cmd/nerdctl/volume" "github.com/containerd/nerdctl/v2/pkg/config" ncdefaults "github.com/containerd/nerdctl/v2/pkg/defaults" "github.com/containerd/nerdctl/v2/pkg/errutil" "github.com/containerd/nerdctl/v2/pkg/logging" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/store" "github.com/containerd/nerdctl/v2/pkg/version" ) var ( // To print Bold Text Bold = color.New(color.Bold).SprintfFunc() ) // usage was derived from https://github.com/spf13/cobra/blob/v1.2.1/command.go#L491-L514 func usage(c *cobra.Command) error { s := "Usage: " if c.HasSubCommands() { s += c.CommandPath() + " [command]\n" } else if c.Runnable() { s += c.UseLine() + "\n" } else { s += c.CommandPath() + " [command]\n" } s += "\n" if len(c.Aliases) > 0 { s += "Aliases: " + c.NameAndAliases() + "\n" } if c.HasExample() { s += "Example:\n" s += c.Example + "\n" } var managementCommands, nonManagementCommands []*cobra.Command for _, f := range c.Commands() { f := f if f.Hidden { continue } if f.Annotations[helpers.Category] == helpers.Management { managementCommands = append(managementCommands, f) } else { nonManagementCommands = append(nonManagementCommands, f) } } printCommands := func(title string, commands []*cobra.Command) string { if len(commands) == 0 { return "" } var longest int for _, f := range commands { if l := len(f.Name()); l > longest { longest = l } } title = Bold(title) t := title + ":\n" for _, f := range commands { t += " " t += f.Name() t += strings.Repeat(" ", longest-len(f.Name())) t += " " + f.Short + "\n" } t += "\n" return t } s += printCommands("Management commands", managementCommands) s += printCommands("Commands", nonManagementCommands) s += Bold("Flags") + ":\n" s += c.LocalFlags().FlagUsages() + "\n" if c == c.Root() { s += "Run '" + c.CommandPath() + " COMMAND --help' for more information on a command.\n" } else { s += "See also '" + c.Root().CommandPath() + " --help' for the global flags such as '--namespace', '--snapshotter', and '--cgroup-manager'." } fmt.Fprintln(c.OutOrStdout(), s) return nil } func main() { if err := xmain(); err != nil { errutil.HandleExitCoder(err) log.L.Fatal(err) } } func xmain() error { if len(os.Args) == 3 && os.Args[1] == logging.MagicArgv1 { // containerd runtime v2 logging plugin mode. // "binary://BIN?KEY=VALUE" URI is parsed into Args {BIN, KEY, VALUE}. return logging.Main(os.Args[2]) } // nerdctl CLI mode app, err := newApp() if err != nil { return err } return app.Execute() } func initRootCmdFlags(rootCmd *cobra.Command, tomlPath string) (*pflag.FlagSet, error) { cfg := config.New() if r, err := os.Open(tomlPath); err == nil { log.L.Debugf("Loading config from %q", tomlPath) defer r.Close() dec := toml.NewDecoder(r).DisallowUnknownFields() // set Strict to detect typo if err := dec.Decode(cfg); err != nil { return nil, fmt.Errorf("failed to load nerdctl config (not daemon config) from %q (Hint: don't mix up daemon's `config.toml` with `nerdctl.toml`): %w", tomlPath, err) } log.L.Debugf("Loaded config %+v", cfg) } else { log.L.WithError(err).Debugf("Not loading config from %q", tomlPath) if !errors.Is(err, os.ErrNotExist) { return nil, err } } aliasToBeInherited := pflag.NewFlagSet(rootCmd.Name(), pflag.ExitOnError) rootCmd.PersistentFlags().Bool("debug", cfg.Debug, "debug mode") rootCmd.PersistentFlags().Bool("debug-full", cfg.DebugFull, "debug mode (with full output)") // -a is aliases (conflicts with nerdctl images -a) helpers.AddPersistentStringFlag(rootCmd, "address", []string{"a", "H"}, nil, []string{"host"}, aliasToBeInherited, cfg.Address, "CONTAINERD_ADDRESS", `containerd address, optionally with "unix://" prefix`) // -n is aliases (conflicts with nerdctl logs -n) helpers.AddPersistentStringFlag(rootCmd, "namespace", []string{"n"}, nil, nil, aliasToBeInherited, cfg.Namespace, "CONTAINERD_NAMESPACE", `containerd namespace, such as "moby" for Docker, "k8s.io" for Kubernetes`) rootCmd.RegisterFlagCompletionFunc("namespace", completion.NamespaceNames) helpers.AddPersistentStringFlag(rootCmd, "snapshotter", nil, nil, []string{"storage-driver"}, aliasToBeInherited, cfg.Snapshotter, "CONTAINERD_SNAPSHOTTER", "containerd snapshotter") rootCmd.RegisterFlagCompletionFunc("snapshotter", completion.SnapshotterNames) rootCmd.RegisterFlagCompletionFunc("storage-driver", completion.SnapshotterNames) helpers.AddPersistentStringFlag(rootCmd, "cni-path", nil, nil, nil, aliasToBeInherited, cfg.CNIPath, "CNI_PATH", "cni plugins binary directory") helpers.AddPersistentStringFlag(rootCmd, "cni-netconfpath", nil, nil, nil, aliasToBeInherited, cfg.CNINetConfPath, "NETCONFPATH", "cni config directory") rootCmd.PersistentFlags().String("data-root", cfg.DataRoot, "Root directory of persistent nerdctl state (managed by nerdctl, not by containerd)") rootCmd.PersistentFlags().String("cgroup-manager", cfg.CgroupManager, `Cgroup manager to use ("cgroupfs"|"systemd")`) rootCmd.RegisterFlagCompletionFunc("cgroup-manager", completion.CgroupManagerNames) rootCmd.PersistentFlags().Bool("insecure-registry", cfg.InsecureRegistry, "skips verifying HTTPS certs, and allows falling back to plain HTTP") // hosts-dir is defined as StringSlice, not StringArray, to allow specifying "--hosts-dir=/etc/containerd/certs.d,/etc/docker/certs.d" rootCmd.PersistentFlags().StringSlice("hosts-dir", cfg.HostsDir, "A directory that contains /hosts.toml (containerd style) or /{ca.cert, cert.pem, key.pem} (docker style)") // Experimental enable experimental feature, see in https://github.com/containerd/nerdctl/blob/main/docs/experimental.md helpers.AddPersistentBoolFlag(rootCmd, "experimental", nil, nil, cfg.Experimental, "NERDCTL_EXPERIMENTAL", "Control experimental: https://github.com/containerd/nerdctl/blob/main/docs/experimental.md") helpers.AddPersistentStringFlag(rootCmd, "host-gateway-ip", nil, nil, nil, aliasToBeInherited, cfg.HostGatewayIP, "NERDCTL_HOST_GATEWAY_IP", "IP address that the special 'host-gateway' string in --add-host resolves to. Defaults to the IP address of the host. It has no effect without setting --add-host") helpers.AddPersistentStringFlag(rootCmd, "bridge-ip", nil, nil, nil, aliasToBeInherited, cfg.BridgeIP, "NERDCTL_BRIDGE_IP", "IP address for the default nerdctl bridge network") rootCmd.PersistentFlags().Bool("kube-hide-dupe", cfg.KubeHideDupe, "Deduplicate images for Kubernetes with namespace k8s.io") rootCmd.PersistentFlags().StringSlice("cdi-spec-dirs", cfg.CDISpecDirs, "The directories to search for CDI spec files. Defaults to /etc/cdi,/var/run/cdi") rootCmd.PersistentFlags().String("userns-remap", cfg.UsernsRemap, "Support idmapping for creating and running containers. This options is only supported on linux. If `host` is passed, no idmapping is done. if a user name is passed, it does idmapping based on the uidmap and gidmap ranges specified in /etc/subuid and /etc/subgid respectively") helpers.HiddenPersistentStringArrayFlag(rootCmd, "global-dns", cfg.DNS, "Global DNS servers for containers") helpers.HiddenPersistentStringArrayFlag(rootCmd, "global-dns-opts", cfg.DNSOpts, "Global DNS options for containers") helpers.HiddenPersistentStringArrayFlag(rootCmd, "global-dns-search", cfg.DNSSearch, "Global DNS search domains for containers") return aliasToBeInherited, nil } func newApp() (*cobra.Command, error) { tomlPath := ncdefaults.NerdctlTOML() if v, ok := os.LookupEnv("NERDCTL_TOML"); ok { tomlPath = v } short := "nerdctl is a command line interface for containerd" long := fmt.Sprintf(`%s Config file ($NERDCTL_TOML): %s `, short, tomlPath) var rootCmd = &cobra.Command{ Use: "nerdctl", Short: short, Long: long, Version: strings.TrimPrefix(version.GetVersion(), "v"), SilenceUsage: true, SilenceErrors: true, TraverseChildren: true, // required for global short hands like -a, -H, -n } rootCmd.SetUsageFunc(usage) aliasToBeInherited, err := initRootCmdFlags(rootCmd, tomlPath) if err != nil { return nil, err } if err := resetSavedSETUID(); err != nil { return nil, err } rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } debug := globalOptions.DebugFull if !debug { debug = globalOptions.Debug } if debug { log.SetLevel(log.DebugLevel.String()) } address := globalOptions.Address if strings.Contains(address, "://") && !strings.HasPrefix(address, "unix://") { return fmt.Errorf("invalid address %q", address) } cgroupManager := globalOptions.CgroupManager if runtime.GOOS == "linux" { switch cgroupManager { case "systemd", "cgroupfs", "none": default: return fmt.Errorf("invalid cgroup-manager %q (supported values: \"systemd\", \"cgroupfs\", \"none\")", cgroupManager) } } // Since we store containers' stateful information on the filesystem per namespace, we need namespaces to be // valid, safe path segments. // Note that the container runtime will further enforce additional restrictions on namespace names // (containerd treats namespaces as valid identifiers - eg: alphanumericals + dash, starting with a letter) // See https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#path-segment-names for // considerations about path segments identifiers. if err = store.IsFilesystemSafe(globalOptions.Namespace); err != nil { return err } if appNeedsRootlessParentMain(cmd, args) { // reexec /proc/self/exe with `nsenter` into RootlessKit namespaces return rootlessutil.ParentMain(globalOptions.HostGatewayIP) } return nil } rootCmd.RunE = helpers.UnknownSubcommandAction rootCmd.AddCommand( container.CreateCommand(), // #region Run & Exec container.RunCommand(), container.UpdateCommand(), container.ExecCommand(), // #endregion // #region Container management container.PsCommand(), container.LogsCommand(), container.PortCommand(), container.StopCommand(), container.StartCommand(), container.DiffCommand(), container.RestartCommand(), container.KillCommand(), container.RemoveCommand(), container.PauseCommand(), container.UnpauseCommand(), container.CommitCommand(), container.ExportCommand(), container.WaitCommand(), container.RenameCommand(), container.AttachCommand(), container.HealthCheckCommand(), // #endregion // Build builder.BuildCommand(), // #region Image management image.ImagesCommand(), image.PullCommand(), image.PushCommand(), image.LoadCommand(), image.SaveCommand(), image.ImportCommand(), image.TagCommand(), image.RmiCommand(), image.HistoryCommand(), search.Command(), // #endregion // #region System system.EventsCommand(), system.InfoCommand(), versionCommand(), // #endregion // Inspect inspect.Command(), // stats container.TopCommand(), container.StatsCommand(), // #region helpers.Management container.Command(), image.Command(), network.Command(), volume.Command(), system.Command(), namespace.Command(), builder.Command(), // #endregion // Internal internal.Command(), // login login.Command(), // Logout login.LogoutCommand(), // Compose compose.Command(), // IPFS ipfs.NewIPFSCommand(), // Manifest manifest.Command(), // Checkpoint checkpoint.Command(), ) addApparmorCommand(rootCmd) container.AddCpCommand(rootCmd) // add aliasToBeInherited to subCommand(s) InheritedFlags for _, subCmd := range rootCmd.Commands() { subCmd.InheritedFlags().AddFlagSet(aliasToBeInherited) } return rootCmd, nil } ================================================ FILE: cmd/nerdctl/main_linux.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" "golang.org/x/sys/unix" "github.com/containerd/nerdctl/v2/cmd/nerdctl/apparmor" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/strutil" ) func appNeedsRootlessParentMain(cmd *cobra.Command, args []string) bool { commands := []string{} for tcmd := cmd; tcmd != nil; tcmd = tcmd.Parent() { commands = append(commands, tcmd.Name()) } commands = strutil.ReverseStrSlice(commands) if !rootlessutil.IsRootlessParent() { return false } if len(commands) < 2 { return true } switch commands[1] { // completion, login, logout, version: false, because it shouldn't require the daemon to be running // apparmor: false, because it requires the initial mount namespace to access /sys/kernel/security // cp, compose cp: false, because it requires the initial mount namespace to inspect file owners case "", "completion", "login", "logout", "apparmor", "cp", "version": return false case "container": if len(commands) < 3 { return true } switch commands[2] { case "cp": return false } case "compose": if len(commands) < 3 { return true } switch commands[2] { case "cp": return false } } return true } func addApparmorCommand(rootCmd *cobra.Command) { rootCmd.AddCommand(apparmor.Command()) } // resetSavedSETUID drops the saved UID of a setuid-root process to the original real UID. // This ensures the process cannot regain root privileges later. // It only performs the operation if the process is currently running with effective UID 0 (root) // and was started by a non-root user (i.e., real UID != effective UID). // For more info see issue https://github.com/containerd/nerdctl/issues/4098 func resetSavedSETUID() error { var err error uid := unix.Getuid() euid := unix.Geteuid() if uid != euid && euid == 0 { err = unix.Setresuid(0, 0, uid) } return err } ================================================ FILE: cmd/nerdctl/main_nolinux.go ================================================ //go:build !linux /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" ) func appNeedsRootlessParentMain(cmd *cobra.Command, args []string) bool { return false } func addApparmorCommand(rootCmd *cobra.Command) { // NOP } func resetSavedSETUID() error { // NOP return nil } ================================================ FILE: cmd/nerdctl/main_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "errors" "testing" "github.com/containerd/containerd/v2/defaults" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestMain(m *testing.M) { testutil.M(m) } // TestUnknownCommand tests https://github.com/containerd/nerdctl/issues/487 func TestUnknownCommand(t *testing.T) { testCase := nerdtest.Setup() var cmd = errors.New("unknown subcommand") testCase.SubTests = []*test.Case{ { Description: "non-existent-command", Command: test.Command("non-existent-command"), Expected: test.Expects(1, []error{cmd}, nil), }, { Description: "non-existent-command info", Command: test.Command("non-existent-command", "info"), Expected: test.Expects(1, []error{cmd}, nil), }, { Description: "system non-existent-command", Command: test.Command("system", "non-existent-command"), Expected: test.Expects(1, []error{cmd}, nil), }, { Description: "system non-existent-command info", Command: test.Command("system", "non-existent-command", "info"), Expected: test.Expects(1, []error{cmd}, nil), }, { Description: "system", Command: test.Command("system"), Expected: test.Expects(0, nil, nil), }, { Description: "system info", Command: test.Command("system", "info"), Expected: test.Expects(0, nil, expect.Contains("Kernel Version:")), }, { Description: "info", Command: test.Command("info"), Expected: test.Expects(0, nil, expect.Contains("Kernel Version:")), }, } testCase.Run(t) } // TestNerdctlConfig validates the configuration precedence [CLI, Env, TOML, Default] and broken config rejection func TestNerdctlConfig(t *testing.T) { testCase := nerdtest.Setup() // Docker does not support nerdctl.toml obviously testCase.Require = require.Not(nerdtest.Docker) testCase.SubTests = []*test.Case{ { Description: "Default", Command: test.Command("info", "-f", "{{.Driver}}"), Expected: test.Expects(0, nil, expect.Equals(defaults.DefaultSnapshotter+"\n")), }, { Description: "TOML > Default", Command: test.Command("info", "-f", "{{.Driver}}"), Expected: test.Expects(0, nil, expect.Equals("dummy-snapshotter-via-toml\n")), Config: test.WithConfig(nerdtest.NerdctlToml, `snapshotter = "dummy-snapshotter-via-toml"`), }, { Description: "Cli > TOML > Default", Command: test.Command("info", "-f", "{{.Driver}}", "--snapshotter=dummy-snapshotter-via-cli"), Expected: test.Expects(0, nil, expect.Equals("dummy-snapshotter-via-cli\n")), Config: test.WithConfig(nerdtest.NerdctlToml, `snapshotter = "dummy-snapshotter-via-toml"`), }, { Description: "Env > TOML > Default", Command: test.Command("info", "-f", "{{.Driver}}"), Env: map[string]string{"CONTAINERD_SNAPSHOTTER": "dummy-snapshotter-via-env"}, Expected: test.Expects(0, nil, expect.Equals("dummy-snapshotter-via-env\n")), Config: test.WithConfig(nerdtest.NerdctlToml, `snapshotter = "dummy-snapshotter-via-toml"`), }, { Description: "Cli > Env > TOML > Default", Command: test.Command("info", "-f", "{{.Driver}}", "--snapshotter=dummy-snapshotter-via-cli"), Env: map[string]string{"CONTAINERD_SNAPSHOTTER": "dummy-snapshotter-via-env"}, Expected: test.Expects(0, nil, expect.Equals("dummy-snapshotter-via-cli\n")), Config: test.WithConfig(nerdtest.NerdctlToml, `snapshotter = "dummy-snapshotter-via-toml"`), }, { Description: "Broken config", Command: test.Command("info"), Expected: test.Expects(1, []error{errors.New("failed to load nerdctl config")}, nil), Config: test.WithConfig(nerdtest.NerdctlToml, `# containerd config, not nerdctl config version = 2`), }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/main_test_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "errors" "log" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) // TestTest is testing the test tooling itself func TestTest(t *testing.T) { testCase := nerdtest.Setup() testCase.SubTests = []*test.Case{ { Description: "failure", Command: test.Command("undefinedcommand"), Expected: test.Expects(1, nil, nil), }, { Description: "success", Command: test.Command("info"), Expected: test.Expects(0, nil, nil), }, { Description: "failure with single error testing", Command: test.Command("undefinedcommand"), Expected: test.Expects(1, []error{errors.New("unknown subcommand")}, nil), }, { Description: "success with contains output testing", Command: test.Command("info"), Expected: test.Expects(0, nil, expect.Contains("Kernel")), }, { Description: "success with negative output testing", Command: test.Command("info"), Expected: test.Expects(0, nil, expect.DoesNotContain("foobar")), }, // Note that docker annoyingly returns 125 in a few conditions like this { Description: "failure with multiple error testing", Command: test.Command("-fail"), Expected: test.Expects(expect.ExitCodeGenericFail, []error{errors.New("unknown"), errors.New("shorthand")}, nil), }, { Description: "success with exact output testing", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Custom("echo", "foobar") }, Expected: test.Expects(0, nil, expect.Equals("foobar\n")), }, { Description: "data propagation", Data: test.WithLabels(map[string]string{"status": "uninitialized"}), Setup: func(data test.Data, helpers test.Helpers) { data.Labels().Set("status", data.Labels().Get("status")+"-setup") }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Custom("printf", data.Labels().Get("status")) data.Labels().Set("status", data.Labels().Get("status")+"-command") return cmd }, Cleanup: func(data test.Data, helpers test.Helpers) { if data.Labels().Get("status") == "uninitialized" { return } if data.Labels().Get("status") != "uninitialized-setup-command" { log.Fatalf("unexpected status label %q", data.Labels().Get("status")) } data.Labels().Set("status", data.Labels().Get("status")+"-cleanup") }, SubTests: []*test.Case{ { Description: "Subtest data propagation", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Custom("printf", data.Labels().Get("status")) }, Expected: test.Expects(0, nil, expect.Equals("uninitialized-setup-command")), }, }, Expected: test.Expects(0, nil, expect.Equals("uninitialized-setup")), }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/manifest/manifest.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package manifest import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" ) func Command() *cobra.Command { cmd := &cobra.Command{ Annotations: map[string]string{helpers.Category: helpers.Management}, Use: "manifest", Short: "Manage image manifests.", RunE: helpers.UnknownSubcommandAction, SilenceUsage: true, SilenceErrors: true, } cmd.AddCommand( inspectCommand(), createCommand(), annotateCommand(), removeCommand(), pushCommand(), ) return cmd } ================================================ FILE: cmd/nerdctl/manifest/manifest_annotate.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package manifest import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/cmd/manifest" ) func annotateCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "annotate INDEX/MANIFESTLIST MANIFEST", Short: "Add additional information to a local image manifest", Args: cobra.ExactArgs(2), RunE: annotateAction, ValidArgsFunction: annotateShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().String("os", "", "Set operating system") cmd.Flags().String("arch", "", "Set architecture") cmd.Flags().String("os-version", "", "Set operating system version") cmd.Flags().String("variant", "", "Set operating system feature") cmd.Flags().StringArray("os-features", []string{}, "Set architecture variant") return cmd } func processAnnotateFlags(cmd *cobra.Command) (types.ManifestAnnotateOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ManifestAnnotateOptions{}, err } os, err := cmd.Flags().GetString("os") if err != nil { return types.ManifestAnnotateOptions{}, err } arch, err := cmd.Flags().GetString("arch") if err != nil { return types.ManifestAnnotateOptions{}, err } osVersion, err := cmd.Flags().GetString("os-version") if err != nil { return types.ManifestAnnotateOptions{}, err } variant, err := cmd.Flags().GetString("variant") if err != nil { return types.ManifestAnnotateOptions{}, err } osFeatures, err := cmd.Flags().GetStringArray("os-features") if err != nil { return types.ManifestAnnotateOptions{}, err } return types.ManifestAnnotateOptions{ Stdout: cmd.OutOrStdout(), GOptions: globalOptions, Os: os, Arch: arch, OsVersion: osVersion, Variant: variant, OsFeatures: osFeatures, }, nil } func annotateAction(cmd *cobra.Command, args []string) error { annotateOptions, err := processAnnotateFlags(cmd) if err != nil { return err } listRef := args[0] manifestRef := args[1] return manifest.Annotate(cmd.Context(), listRef, manifestRef, annotateOptions) } func annotateShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.ImageNames(cmd) } ================================================ FILE: cmd/nerdctl/manifest/manifest_annotate_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package manifest import ( "errors" "testing" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestManifestAnnotateErrors(t *testing.T) { testCase := nerdtest.Setup() manifestListName := "test-list:v1" manifestName := "example.com/alpine:latest" invalidName := "invalid/name/with/special@chars" testCase.SubTests = []*test.Case{ { Description: "too-few-arguments", Command: test.Command("manifest", "annotate", manifestListName), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, } }, }, { Description: "invalid-list-name", Command: test.Command("manifest", "annotate", invalidName, manifestName), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New(data.Labels().Get("error"))}, } }, Data: test.WithLabels(map[string]string{ "error": "invalid reference format", }), }, { Description: "invalid-manifest-reference", Command: test.Command("manifest", "annotate", manifestListName, invalidName), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New(data.Labels().Get("error"))}, } }, Data: test.WithLabels(map[string]string{ "error": "invalid reference format", }), }, } testCase.Run(t) } func TestManifestAnnotate(t *testing.T) { testCase := nerdtest.Setup() manifestListName := "example.com/test-list-annotate:v1" manifestRef := testutil.GetTestImageWithoutTag("alpine") + "@" + testutil.GetTestImageManifestDigest("alpine", "linux/amd64") testCase.SubTests = []*test.Case{ { Description: "annotate-non-existent-manifest", Setup: func(data test.Data, helpers test.Helpers) { cmd := helpers.Command("manifest", "create", manifestListName, manifestRef) cmd.Run(&test.Expected{ExitCode: 0}) }, Command: test.Command("manifest", "annotate", manifestListName, "example.com/fake:0.0"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New(data.Labels().Get("error"))}, } }, Data: test.WithLabels(map[string]string{ "error": "manifest for image example.com/fake:0.0 does not exist", }), }, { Description: "annotate-success", Setup: func(data test.Data, helpers test.Helpers) { cmd := helpers.Command("manifest", "create", manifestListName+"-success", manifestRef) cmd.Run(&test.Expected{ExitCode: 0}) }, Command: test.Command("manifest", "annotate", manifestListName+"-success", manifestRef, "--os", "freebsd", "--arch", "arm", "--os-version", "1", "--os-features", "feature1", "--variant", "v7"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, } }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/manifest/manifest_create.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package manifest import ( "fmt" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/cmd/manifest" ) func createCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "create INDEX/MANIFESTLIST MANIFEST [MANIFEST...]", Short: "Create a local index/manifest list for annotating and pushing to a registry", Args: cobra.MinimumNArgs(2), RunE: createAction, ValidArgsFunction: createShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().Bool("amend", false, "Amend the existing index/manifest list") cmd.Flags().Bool("insecure", false, "Allow communication with an insecure registry") return cmd } func processCreateFlags(cmd *cobra.Command) (types.ManifestCreateOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ManifestCreateOptions{}, err } amend, err := cmd.Flags().GetBool("amend") if err != nil { return types.ManifestCreateOptions{}, err } insecure, err := cmd.Flags().GetBool("insecure") if err != nil { return types.ManifestCreateOptions{}, err } return types.ManifestCreateOptions{ Stdout: cmd.OutOrStdout(), GOptions: globalOptions, Amend: amend, Insecure: insecure, }, nil } func createAction(cmd *cobra.Command, args []string) error { createOptions, err := processCreateFlags(cmd) if err != nil { return err } listRef := args[0] manifestRefs := args[1:] listRef, err = manifest.Create(cmd.Context(), listRef, manifestRefs, createOptions) if err != nil { return err } fmt.Fprintln(createOptions.Stdout, "Created manifest list", listRef) return nil } func createShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.ImageNames(cmd) } ================================================ FILE: cmd/nerdctl/manifest/manifest_create_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package manifest import ( "errors" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestManifestCreateErrors(t *testing.T) { testCase := nerdtest.Setup() manifestListName := "test-list:v1" manifestName := "example.com/alpine:latest" invalidName := "invalid/name/with/special@chars" testCase.SubTests = []*test.Case{ { Description: "too-few-arguments", Command: test.Command("manifest", "create", manifestListName), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New(data.Labels().Get("error"))}, } }, Data: test.WithLabels(map[string]string{ "error": "requires at least 2 arg", }), }, { Description: "invalid-list-name", Command: test.Command("manifest", "create", invalidName, manifestName), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New(data.Labels().Get("error"))}, } }, Data: test.WithLabels(map[string]string{ "error": "invalid reference format", }), }, { Description: "invalid-manifest-reference", Command: test.Command("manifest", "create", manifestListName, invalidName), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New(data.Labels().Get("error"))}, } }, Data: test.WithLabels(map[string]string{ "error": "invalid reference format", }), }, } testCase.Run(t) } func TestManifestCreate(t *testing.T) { testCase := nerdtest.Setup() manifestListName := "test-list-create:v1" manifestRef := testutil.GetTestImageWithoutTag("alpine") + "@" + testutil.GetTestImageManifestDigest("alpine", "linux/amd64") testCase.SubTests = []*test.Case{ { Description: "create-manifest-list", Command: test.Command("manifest", "create", manifestListName, manifestRef), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.Contains(data.Labels().Get("output")), } }, Data: test.WithLabels(map[string]string{ "output": "Created manifest list docker.io/library/" + manifestListName, }), }, { Description: "create-existed-manifest-list-without-amend-flag", Setup: func(data test.Data, helpers test.Helpers) { cmd := helpers.Command("manifest", "create", manifestListName+"-without-amend-flag", manifestRef) cmd.Run(&test.Expected{ExitCode: 0}) }, Command: test.Command("manifest", "create", manifestListName+"-without-amend-flag", manifestRef), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New(data.Labels().Get("error"))}, } }, Data: test.WithLabels(map[string]string{ "error": "refusing to amend an existing manifest list with no --amend flag", }), }, { Description: "create-manifest-list-with-amend-flag", Setup: func(data test.Data, helpers test.Helpers) { cmd := helpers.Command("manifest", "create", manifestListName+"-with-amend-flag", manifestRef) cmd.Run(&test.Expected{ExitCode: 0}) }, Command: test.Command("manifest", "create", "--amend", manifestListName+"-with-amend-flag", manifestRef), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.Contains(data.Labels().Get("output")), } }, Data: test.WithLabels(map[string]string{ "output": "Created manifest list docker.io/library/" + manifestListName + "-with-amend-flag", }), }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/manifest/manifest_inspect.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package manifest import ( "fmt" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/cmd/manifest" "github.com/containerd/nerdctl/v2/pkg/formatter" ) func inspectCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "inspect MANIFEST", Short: "Display the contents of a manifest or image index/manifest list", Args: cobra.MinimumNArgs(1), RunE: inspectAction, ValidArgsFunction: inspectShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().Bool("verbose", false, "Verbose output additional info including layers and platform") cmd.Flags().Bool("insecure", false, "Allow communication with an insecure registry") return cmd } func processInspectFlags(cmd *cobra.Command) (types.ManifestInspectOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ManifestInspectOptions{}, err } verbose, err := cmd.Flags().GetBool("verbose") if err != nil { return types.ManifestInspectOptions{}, err } insecure, err := cmd.Flags().GetBool("insecure") if err != nil { return types.ManifestInspectOptions{}, err } return types.ManifestInspectOptions{ Stdout: cmd.OutOrStdout(), GOptions: globalOptions, Verbose: verbose, Insecure: insecure, }, nil } func inspectAction(cmd *cobra.Command, args []string) error { inspectOptions, err := processInspectFlags(cmd) if err != nil { return err } rawRef := args[0] res, err := manifest.Inspect(cmd.Context(), rawRef, inspectOptions) if err != nil { return err } // Output format: single object for single result, array for multiple results if len(res) == 1 { jsonStr, err := formatter.ToJSON(res[0], "", " ") if err != nil { return err } fmt.Fprint(inspectOptions.Stdout, jsonStr) } else { if formatErr := formatter.FormatSlice("", inspectOptions.Stdout, res); formatErr != nil { return formatErr } } return nil } func inspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.ImageNames(cmd) } ================================================ FILE: cmd/nerdctl/manifest/manifest_inspect_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package manifest import ( "encoding/json" "testing" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/manifesttypes" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) const ( testImageName = "alpine" testPlatform = "linux/amd64" ) type testData struct { imageName string platform string imageRef string manifestDigest string configDigest string rawData string } func newTestData(imageName, platform string) *testData { return &testData{ imageName: imageName, platform: platform, imageRef: testutil.GetTestImage(imageName), manifestDigest: testutil.GetTestImageManifestDigest(imageName, platform), configDigest: testutil.GetTestImageConfigDigest(imageName, platform), rawData: testutil.GetTestImageRaw(imageName, platform), } } func (td *testData) imageWithDigest() string { return testutil.GetTestImageWithoutTag(td.imageName) + "@" + td.manifestDigest } func (td *testData) isAmd64Platform(platform *ocispec.Platform) bool { return platform != nil && platform.Architecture == "amd64" && platform.OS == "linux" } func TestManifestInspect(t *testing.T) { testCase := nerdtest.Setup() td := newTestData(testImageName, testPlatform) testCase.SubTests = []*test.Case{ { Description: "tag-non-verbose", Command: test.Command("manifest", "inspect", td.imageRef), Expected: test.Expects(0, nil, func(stdout string, t tig.T) { var manifest manifesttypes.DockerManifestListStruct assert.NilError(t, json.Unmarshal([]byte(stdout), &manifest)) assert.Equal(t, manifest.SchemaVersion, testutil.GetTestImageSchemaVersion(td.imageName)) assert.Equal(t, manifest.MediaType, testutil.GetTestImageMediaType(td.imageName)) assert.Assert(t, len(manifest.Manifests) > 0) var foundManifest *ocispec.Descriptor for _, m := range manifest.Manifests { if td.isAmd64Platform(m.Platform) { foundManifest = &m break } } assert.Assert(t, foundManifest != nil, "should find amd64 platform manifest") assert.Equal(t, foundManifest.Digest.String(), td.manifestDigest) assert.Equal(t, foundManifest.MediaType, testutil.GetTestImagePlatformMediaType(td.imageName, td.platform)) }), }, { Description: "tag-verbose", Command: test.Command("manifest", "inspect", td.imageRef, "--verbose"), Expected: test.Expects(0, nil, func(stdout string, t tig.T) { var entries []manifesttypes.DockerManifestEntry assert.NilError(t, json.Unmarshal([]byte(stdout), &entries)) assert.Assert(t, len(entries) > 0) var foundEntry *manifesttypes.DockerManifestEntry for _, e := range entries { if td.isAmd64Platform(e.Descriptor.Platform) { foundEntry = &e break } } assert.Assert(t, foundEntry != nil, "should find amd64 platform entry") expectedRef := td.imageRef + "@" + td.manifestDigest assert.Equal(t, foundEntry.Ref, expectedRef) assert.Equal(t, foundEntry.Descriptor.Digest.String(), td.manifestDigest) assert.Equal(t, foundEntry.Descriptor.MediaType, testutil.GetTestImagePlatformMediaType(td.imageName, td.platform)) assert.Equal(t, foundEntry.Raw, td.rawData) }), }, { Description: "digest-non-verbose", Command: test.Command("manifest", "inspect", td.imageWithDigest()), Expected: test.Expects(0, nil, func(stdout string, t tig.T) { var manifest manifesttypes.DockerManifestStruct assert.NilError(t, json.Unmarshal([]byte(stdout), &manifest)) assert.Equal(t, manifest.SchemaVersion, testutil.GetTestImageSchemaVersion(td.imageName)) assert.Equal(t, manifest.MediaType, testutil.GetTestImagePlatformMediaType(td.imageName, td.platform)) assert.Equal(t, manifest.Config.Digest.String(), td.configDigest) }), }, { Description: "digest-verbose", Command: test.Command("manifest", "inspect", td.imageWithDigest(), "--verbose"), Expected: test.Expects(0, nil, func(stdout string, t tig.T) { var entry manifesttypes.DockerManifestEntry assert.NilError(t, json.Unmarshal([]byte(stdout), &entry)) assert.Equal(t, entry.Ref, td.imageWithDigest()) assert.Equal(t, entry.Descriptor.Digest.String(), td.manifestDigest) assert.Equal(t, entry.Descriptor.MediaType, testutil.GetTestImagePlatformMediaType(td.imageName, td.platform)) assert.Equal(t, entry.Raw, td.rawData) }), }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/manifest/manifest_push.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package manifest import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/cmd/manifest" ) func pushCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "push [OPTIONS] INDEX/MANIFESTLIST", Short: "Push a manifest list to a registry", Args: cobra.ExactArgs(1), RunE: pushAction, ValidArgsFunction: pushShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().Bool("insecure", false, "Allow communication with an insecure registry") cmd.Flags().Bool("purge", false, "Remove the manifest list after pushing") return cmd } func processPushFlags(cmd *cobra.Command) (types.ManifestPushOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.ManifestPushOptions{}, err } insecure, err := cmd.Flags().GetBool("insecure") if err != nil { return types.ManifestPushOptions{}, err } purge, err := cmd.Flags().GetBool("purge") if err != nil { return types.ManifestPushOptions{}, err } return types.ManifestPushOptions{ Stdout: cmd.OutOrStdout(), GOptions: globalOptions, Insecure: insecure, Purge: purge, }, nil } func pushAction(cmd *cobra.Command, args []string) error { pushOptions, err := processPushFlags(cmd) if err != nil { return err } err = manifest.Push(cmd.Context(), args[0], pushOptions) if err != nil { return err } return nil } func pushShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.ImageNames(cmd) } ================================================ FILE: cmd/nerdctl/manifest/manifest_push_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package manifest import ( "errors" "fmt" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" ) func TestManifestPushErrors(t *testing.T) { testCase := nerdtest.Setup() invalidName := "invalid/name/with/special@chars" testCase.SubTests = []*test.Case{ { Description: "require-one-argument", Command: test.Command("manifest", "push", "arg1", "arg2"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, } }, }, { Description: "invalid-list-name", Command: test.Command("manifest", "push", invalidName), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New(data.Labels().Get("error"))}, } }, Data: test.WithLabels(map[string]string{ "error": "invalid reference format", }), }, } testCase.Run(t) } func TestManifestPush(t *testing.T) { nerdtest.Setup() var registryTokenAuthHTTPSRandom *registry.Server var tokenServer *registry.TokenAuthServer manifestRef := testutil.GetTestImageWithoutTag("alpine") + "@" + testutil.GetTestImageManifestDigest("alpine", "linux/amd64") expectedDigest := "sha256:5317ce2da263afa23570c692d62c1b01381285b2198b3ea9739ce64bec22aff2" testCase := &test.Case{ Require: require.All( require.Linux, nerdtest.Registry, ), Setup: func(data test.Data, helpers test.Helpers) { registryTokenAuthHTTPSRandom, tokenServer = nerdtest.RegistryWithTokenAuth(data, helpers, "admin", "badmin", 0, true) tokenServer.Setup(data, helpers) registryTokenAuthHTTPSRandom.Setup(data, helpers) }, Cleanup: func(data test.Data, helpers test.Helpers) { if registryTokenAuthHTTPSRandom != nil { registryTokenAuthHTTPSRandom.Cleanup(data, helpers) } if tokenServer != nil { tokenServer.Cleanup(data, helpers) } }, SubTests: []*test.Case{ { Description: "push-to-registry", Require: require.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { targetRef := fmt.Sprintf("%s:%d/%s", registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port, "test-list-push:v1") helpers.Ensure("pull", manifestRef) helpers.Ensure("tag", manifestRef, targetRef) helpers.Ensure("--hosts-dir", registryTokenAuthHTTPSRandom.HostsDir, "login", "-u", "admin", "-p", "badmin", fmt.Sprintf("%s:%d", registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port)) helpers.Ensure("push", "--hosts-dir", registryTokenAuthHTTPSRandom.HostsDir, targetRef) helpers.Ensure("rmi", targetRef) helpers.Ensure("manifest", "create", "--insecure", targetRef+"-success", targetRef) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { targetRef := fmt.Sprintf("%s:%d/%s", registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port, "test-list-push:v1") return helpers.Command("manifest", "push", "--insecure", targetRef+"-success") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: expect.Contains(data.Labels().Get("output")), } }, Data: test.WithLabels(map[string]string{ "output": expectedDigest, }), }, { Description: "reject-cross-registry-sources", Require: require.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { targetRef := fmt.Sprintf("%s:%d/%s", registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port, "test-list-push:v1") helpers.Ensure("manifest", "create", "--insecure", targetRef+"-cross", manifestRef) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { targetRef := fmt.Sprintf("%s:%d/%s", registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port, "test-list-push:v1") return helpers.Command("manifest", "push", "--insecure", targetRef+"-cross") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New(data.Labels().Get("error"))}, } }, Data: test.WithLabels(map[string]string{ "error": "cannot use source images from a different registry than the target image:", }), }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/manifest/manifest_remove.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package manifest import ( "errors" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/cmd/manifest" ) func removeCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "rm INDEX/MANIFESTLIST [INDEX/MANIFESTLIST...]", Short: "Remove one or more index/manifest lists", Args: cobra.MinimumNArgs(1), RunE: removeAction, ValidArgsFunction: removeShellComplete, SilenceUsage: true, SilenceErrors: true, } return cmd } func removeAction(cmd *cobra.Command, refs []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } var errs []error for _, ref := range refs { err := manifest.Remove(cmd.Context(), ref, globalOptions) if err != nil { errs = append(errs, err) } } return errors.Join(errs...) } func removeShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.ImageNames(cmd) } ================================================ FILE: cmd/nerdctl/manifest/manifest_remove_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package manifest import ( "errors" "testing" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestManifestsRemove(t *testing.T) { testCase := nerdtest.Setup() manifestListName1 := "example.com/test-list-remove:v1" manifestListName2 := "example.com/test-list-remove:v2" manifestRef1 := testutil.GetTestImageWithoutTag("alpine") + "@" + testutil.GetTestImageManifestDigest("alpine", "linux/amd64") manifestRef2 := testutil.GetTestImageWithoutTag("alpine") + "@" + testutil.GetTestImageManifestDigest("alpine", "linux/arm64") testCase.SubTests = []*test.Case{ { Description: "remove-several-manifestlists", Setup: func(data test.Data, helpers test.Helpers) { cmd := helpers.Command("manifest", "create", manifestListName1, manifestRef1) cmd.Run(&test.Expected{ExitCode: 0}) cmd = helpers.Command("manifest", "create", manifestListName2, manifestRef2) cmd.Run(&test.Expected{ExitCode: 0}) }, Command: test.Command("manifest", "rm", manifestListName1, manifestListName2), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, } }, }, { Description: "remove-non-existent-manifestlist", Command: test.Command("manifest", "rm", "example.com/non-existent:latest"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New(data.Labels().Get("error"))}, } }, Data: test.WithLabels(map[string]string{ "error": "No such manifest: example.com/non-existent:latest", }), }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/manifest/manifest_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package manifest import ( "testing" "github.com/containerd/nerdctl/v2/pkg/testutil" ) func TestMain(m *testing.M) { testutil.M(m) } ================================================ FILE: cmd/nerdctl/namespace/namespace.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package namespace import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" ) func Command() *cobra.Command { cmd := &cobra.Command{ Annotations: map[string]string{helpers.Category: helpers.Management}, Use: "namespace", Aliases: []string{"ns"}, Short: "Manage containerd namespaces", Long: "Unrelated to Linux namespaces and Kubernetes namespaces", RunE: helpers.UnknownSubcommandAction, SilenceUsage: true, SilenceErrors: true, } cmd.AddCommand(listCommand()) cmd.AddCommand(removeCommand()) cmd.AddCommand(createCommand()) cmd.AddCommand(updateCommand()) cmd.AddCommand(inspectCommand()) return cmd } ================================================ FILE: cmd/nerdctl/namespace/namespace_create.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package namespace import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/namespace" ) func createCommand() *cobra.Command { cmd := &cobra.Command{ Use: "create NAMESPACE", Short: "Create a new namespace", Args: cobra.MinimumNArgs(1), RunE: createAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().StringArrayP("label", "l", nil, "Set labels for a namespace") return cmd } func processNamespaceCreateCommandOption(cmd *cobra.Command) (types.NamespaceCreateOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.NamespaceCreateOptions{}, err } labels, err := cmd.Flags().GetStringArray("label") if err != nil { return types.NamespaceCreateOptions{}, err } return types.NamespaceCreateOptions{ GOptions: globalOptions, Labels: labels, }, nil } func createAction(cmd *cobra.Command, args []string) error { options, err := processNamespaceCreateCommandOption(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return namespace.Create(ctx, client, args[0], options) } ================================================ FILE: cmd/nerdctl/namespace/namespace_inspect.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package namespace import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/namespace" ) func inspectCommand() *cobra.Command { cmd := &cobra.Command{ Use: "inspect NAMESPACE", Short: "Display detailed information on one or more namespaces.", RunE: inspectAction, ValidArgsFunction: namespaceInspectShellComplete, Args: cobra.MinimumNArgs(1), SilenceUsage: true, SilenceErrors: true, } cmd.Flags().StringP("format", "f", "", "Format the output using the given Go template, e.g, '{{json .}}'") cmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"json"}, cobra.ShellCompDirectiveNoFileComp }) return cmd } func inspectOptions(cmd *cobra.Command) (types.NamespaceInspectOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.NamespaceInspectOptions{}, err } format, err := cmd.Flags().GetString("format") if err != nil { return types.NamespaceInspectOptions{}, err } return types.NamespaceInspectOptions{ GOptions: globalOptions, Format: format, Stdout: cmd.OutOrStdout(), }, nil } func inspectAction(cmd *cobra.Command, args []string) error { options, err := inspectOptions(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return namespace.Inspect(ctx, client, args, options) } func namespaceInspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.NamespaceNames(cmd, args, toComplete) } ================================================ FILE: cmd/nerdctl/namespace/namespace_list.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package namespace import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/namespace" ) func listCommand() *cobra.Command { cmd := &cobra.Command{ Use: "ls", Aliases: []string{"list"}, Short: "List containerd namespaces", RunE: listAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().BoolP("quiet", "q", false, "Only display names") cmd.Flags().StringP("format", "f", "", "Format the output using the given Go template, e.g, '{{json .}}'") return cmd } func listOptions(cmd *cobra.Command) (types.NamespaceListOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.NamespaceListOptions{}, err } format, err := cmd.Flags().GetString("format") if err != nil { return types.NamespaceListOptions{}, err } quiet, err := cmd.Flags().GetBool("quiet") if err != nil { return types.NamespaceListOptions{}, err } return types.NamespaceListOptions{ GOptions: globalOptions, Format: format, Quiet: quiet, Stdout: cmd.OutOrStdout(), }, nil } func listAction(cmd *cobra.Command, args []string) error { options, err := listOptions(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return namespace.List(ctx, client, options) } ================================================ FILE: cmd/nerdctl/namespace/namespace_remove.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package namespace import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/namespace" ) func removeCommand() *cobra.Command { cmd := &cobra.Command{ Use: "remove [flags] NAMESPACE [NAMESPACE...]", Aliases: []string{"rm"}, Args: cobra.MinimumNArgs(1), Short: "Remove one or more namespaces", RunE: removeAction, ValidArgsFunction: namespaceRemoveShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().BoolP("cgroup", "c", false, "delete the namespace's cgroup") return cmd } func removeOptions(cmd *cobra.Command) (types.NamespaceRemoveOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.NamespaceRemoveOptions{}, err } cgroup, err := cmd.Flags().GetBool("cgroup") if err != nil { return types.NamespaceRemoveOptions{}, err } return types.NamespaceRemoveOptions{ GOptions: globalOptions, CGroup: cgroup, Stdout: cmd.OutOrStdout(), }, nil } func removeAction(cmd *cobra.Command, args []string) error { options, err := removeOptions(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return namespace.Remove(ctx, client, args, options) } func namespaceRemoveShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.NamespaceNames(cmd, args, toComplete) } ================================================ FILE: cmd/nerdctl/namespace/namespace_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package namespace import ( "testing" "github.com/containerd/nerdctl/v2/pkg/testutil" ) func TestMain(m *testing.M) { testutil.M(m) } ================================================ FILE: cmd/nerdctl/namespace/namespace_update.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package namespace import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/namespace" ) func updateCommand() *cobra.Command { cmd := &cobra.Command{ Use: "update [flags] NAMESPACE", Short: "Update labels for a namespace", RunE: updateAction, ValidArgsFunction: namespaceUpdateShellComplete, Args: cobra.MinimumNArgs(1), SilenceUsage: true, SilenceErrors: true, } cmd.Flags().StringArrayP("label", "l", nil, "Set labels for a namespace (required)") cmd.MarkFlagRequired("label") return cmd } func processNamespaceUpdateCommandOption(cmd *cobra.Command) (types.NamespaceUpdateOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.NamespaceUpdateOptions{}, err } labels, err := cmd.Flags().GetStringArray("label") if err != nil { return types.NamespaceUpdateOptions{}, err } return types.NamespaceUpdateOptions{ GOptions: globalOptions, Labels: labels, }, nil } func updateAction(cmd *cobra.Command, args []string) error { options, err := processNamespaceUpdateCommandOption(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return namespace.Update(ctx, client, args[0], options) } func namespaceUpdateShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.NamespaceNames(cmd, args, toComplete) } ================================================ FILE: cmd/nerdctl/network/network.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package network import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" ) func Command() *cobra.Command { cmd := &cobra.Command{ Annotations: map[string]string{helpers.Category: helpers.Management}, Use: "network", Short: "Manage networks", RunE: helpers.UnknownSubcommandAction, SilenceUsage: true, SilenceErrors: true, } cmd.AddCommand( listCommand(), inspectCommand(), createCommand(), removeCommand(), pruneCommand(), ) return cmd } ================================================ FILE: cmd/nerdctl/network/network_create.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package network import ( "fmt" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/cmd/network" "github.com/containerd/nerdctl/v2/pkg/identifiers" "github.com/containerd/nerdctl/v2/pkg/strutil" ) func createCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "create [flags] NETWORK", Short: "Create a network", Long: `NOTE: To isolate CNI bridge, CNI plugin "firewall" (>= v1.1.0) is needed.`, Args: helpers.IsExactArgs(1), RunE: createAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().StringP("driver", "d", DefaultNetworkDriver, "Driver to manage the Network") cmd.RegisterFlagCompletionFunc("driver", completion.NetworkDrivers) cmd.Flags().StringArrayP("opt", "o", nil, "Set driver specific options") cmd.RegisterFlagCompletionFunc("opt", completion.NetworkOptions) cmd.Flags().String("ipam-driver", "default", "IP Address helpers.Management Driver") cmd.RegisterFlagCompletionFunc("ipam-driver", completion.IPAMDrivers) cmd.Flags().StringArray("ipam-opt", nil, "Set IPAM driver specific options") cmd.Flags().StringArray("subnet", nil, `Subnet in CIDR format that represents a network segment, e.g. "10.5.0.0/16"`) cmd.Flags().String("gateway", "", `Gateway for the master subnet`) cmd.Flags().String("ip-range", "", `Allocate container ip from a sub-range`) cmd.Flags().StringArray("label", nil, "Set metadata for a network") cmd.Flags().Bool("ipv6", false, "Enable IPv6 networking") cmd.Flags().Bool("internal", false, "Restrict external access to the network") return cmd } func createAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } name := args[0] if err := identifiers.ValidateDockerCompat(name); err != nil { return fmt.Errorf("invalid network name: %w", err) } driver, err := cmd.Flags().GetString("driver") if err != nil { return err } opts, err := cmd.Flags().GetStringArray("opt") if err != nil { return err } ipamDriver, err := cmd.Flags().GetString("ipam-driver") if err != nil { return err } ipamOpts, err := cmd.Flags().GetStringArray("ipam-opt") if err != nil { return err } subnets, err := cmd.Flags().GetStringArray("subnet") if err != nil { return err } gatewayStr, err := cmd.Flags().GetString("gateway") if err != nil { return err } ipRangeStr, err := cmd.Flags().GetString("ip-range") if err != nil { return err } labels, err := cmd.Flags().GetStringArray("label") if err != nil { return err } labels = strutil.DedupeStrSlice(labels) ipv6, err := cmd.Flags().GetBool("ipv6") if err != nil { return err } internal, err := cmd.Flags().GetBool("internal") if err != nil { return err } return network.Create(types.NetworkCreateOptions{ GOptions: globalOptions, Name: name, Driver: driver, Options: strutil.ConvertKVStringsToMap(opts), IPAMDriver: ipamDriver, IPAMOptions: strutil.ConvertKVStringsToMap(ipamOpts), Subnets: subnets, Gateway: gatewayStr, IPRange: ipRangeStr, Labels: labels, IPv6: ipv6, Internal: internal, }, cmd.OutOrStdout()) } ================================================ FILE: cmd/nerdctl/network/network_create_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package network import ( "encoding/json" "fmt" "net" "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestNetworkCreate(t *testing.T) { testCase := nerdtest.Setup() testCase.SubTests = []*test.Case{ { Description: "vanilla", Setup: func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() helpers.Ensure("network", "create", identifier) netw := nerdtest.InspectNetwork(helpers, identifier) assert.Equal(t, len(netw.IPAM.Config), 1) data.Labels().Set("subnet", netw.IPAM.Config[0].Subnet) helpers.Ensure("network", "create", data.Identifier("1")) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("network", "rm", data.Identifier()) helpers.Anyhow("network", "rm", data.Identifier("1")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { data.Labels().Set("container2", helpers.Capture("run", "--rm", "--net", data.Identifier("1"), testutil.CommonImage, "ip", "route")) return helpers.Command("run", "--rm", "--net", data.Identifier(), testutil.CommonImage, "ip", "route") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Errors: nil, Output: func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(stdout, data.Labels().Get("subnet"))) assert.Assert(t, !strings.Contains(data.Labels().Get("container2"), data.Labels().Get("subnet"))) }, } }, }, { Description: "with MTU", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("network", "create", data.Identifier(), "--driver", "bridge", "--opt", "com.docker.network.driver.mtu=9216") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("network", "rm", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--net", data.Identifier(), testutil.CommonImage, "ifconfig", "eth0") }, Expected: test.Expects(0, nil, expect.Contains("MTU:9216")), }, { Description: "with ipv6", Require: nerdtest.OnlyIPv6, Setup: func(data test.Data, helpers test.Helpers) { subnetStr := "2001:db8:8::/64" data.Labels().Set("subnetStr", subnetStr) _, _, err := net.ParseCIDR(subnetStr) assert.Assert(t, err == nil) helpers.Ensure("network", "create", data.Identifier(), "--ipv6", "--subnet", subnetStr) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("network", "rm", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--net", data.Identifier(), testutil.CommonImage, "ip", "addr", "show", "dev", "eth0") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { _, subnet, _ := net.ParseCIDR(data.Labels().Get("subnetStr")) ip := nerdtest.FindIPv6(stdout) assert.Assert(t, subnet.Contains(ip), fmt.Sprintf("subnet %s contains ip %s", subnet, ip)) }, } }, }, { Description: "internal enabled", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("network", "create", "--internal", data.Identifier()) netw := nerdtest.InspectNetwork(helpers, data.Identifier()) assert.Equal(t, len(netw.IPAM.Config), 1) data.Labels().Set("subnet", netw.IPAM.Config[0].Subnet) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("network", "rm", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--net", data.Identifier(), testutil.CommonImage, "ip", "route") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(stdout, data.Labels().Get("subnet"))) assert.Assert(t, !strings.Contains(stdout, "default ")) if nerdtest.IsDocker() { return } nativeNet := nerdtest.InspectNetworkNative(helpers, data.Identifier()) var cni struct { Plugins []struct { Type string `json:"type"` IsGW bool `json:"isGateway"` IPMasq bool `json:"ipMasq"` } `json:"plugins"` } _ = json.Unmarshal(nativeNet.CNI, &cni) // bridge plugin assertions and no portmap foundBridge := false for _, p := range cni.Plugins { assert.Assert(t, p.Type != "portmap") if p.Type == "bridge" { foundBridge = true assert.Assert(t, !p.IsGW) assert.Assert(t, !p.IPMasq) } } assert.Assert(t, foundBridge) }, } }, }, } testCase.Run(t) } func TestNetworkCreateICC(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.All( require.Linux, ) testCase.SubTests = []*test.Case{ { Description: "with enable_icc=false", Require: nerdtest.CNIFirewallVersion("1.7.1"), NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { // Create a network with ICC disabled helpers.Ensure("network", "create", data.Identifier(), "--driver", "bridge", "--opt", "com.docker.network.bridge.enable_icc=false") // Run a container in that network data.Labels().Set("container1", helpers.Capture("run", "-d", "--net", data.Identifier(), "--name", data.Identifier("c1"), testutil.CommonImage, "sleep", "infinity")) // Wait for container to be running nerdtest.EnsureContainerStarted(helpers, data.Identifier("c1")) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("container", "rm", "-f", data.Identifier("c1")) helpers.Anyhow("network", "rm", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { // DEBUG: Check br_netfilter module status helpers.Custom("sh", "-ec", "lsmod | grep br_netfilter || echo 'br_netfilter not loaded'").Run(&test.Expected{}) helpers.Custom("sh", "-ec", "cat /proc/sys/net/bridge/bridge-nf-call-iptables 2>/dev/null || echo 'bridge-nf-call-iptables not available'").Run(&test.Expected{}) helpers.Custom("sh", "-ec", "ls /proc/sys/net/bridge/ 2>/dev/null || echo 'bridge sysctl not available'").Run(&test.Expected{}) // Try to ping the other container in the same network // This should fail when ICC is disabled return helpers.Command("run", "--rm", "--net", data.Identifier(), testutil.CommonImage, "ping", "-c", "1", "-W", "1", data.Identifier("c1")) }, Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), // Expect ping to fail with exit code 1 }, { Description: "with enable_icc=true", Require: nerdtest.CNIFirewallVersion("1.7.1"), NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { // Create a network with ICC enabled (default) helpers.Ensure("network", "create", data.Identifier(), "--driver", "bridge", "--opt", "com.docker.network.bridge.enable_icc=true") // Run a container in that network data.Labels().Set("container1", helpers.Capture("run", "-d", "--net", data.Identifier(), "--name", data.Identifier("c1"), testutil.CommonImage, "sleep", "infinity")) // Wait for container to be running nerdtest.EnsureContainerStarted(helpers, data.Identifier("c1")) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("container", "rm", "-f", data.Identifier("c1")) helpers.Anyhow("network", "rm", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { // Try to ping the other container in the same network // This should succeed when ICC is enabled return helpers.Command("run", "--rm", "--net", data.Identifier(), testutil.CommonImage, "ping", "-c", "1", "-W", "1", data.Identifier("c1")) }, Expected: test.Expects(0, nil, nil), // Expect ping to succeed with exit code 0 }, { Description: "with no enable_icc option set", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { // Create a network with ICC enabled (default) helpers.Ensure("network", "create", data.Identifier(), "--driver", "bridge") // Run a container in that network data.Labels().Set("container1", helpers.Capture("run", "-d", "--net", data.Identifier(), "--name", data.Identifier("c1"), testutil.CommonImage, "sleep", "infinity")) // Wait for container to be running nerdtest.EnsureContainerStarted(helpers, data.Identifier("c1")) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("container", "rm", "-f", data.Identifier("c1")) helpers.Anyhow("network", "rm", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { // Try to ping the other container in the same network // This should succeed when no ICC is set return helpers.Command("run", "--rm", "--net", data.Identifier(), testutil.CommonImage, "ping", "-c", "1", "-W", "1", data.Identifier("c1")) }, Expected: test.Expects(0, nil, nil), // Expect ping to succeed with exit code 0 }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/network/network_create_unix.go ================================================ //go:build unix /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package network const DefaultNetworkDriver = "bridge" ================================================ FILE: cmd/nerdctl/network/network_create_windows.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package network const DefaultNetworkDriver = "nat" ================================================ FILE: cmd/nerdctl/network/network_inspect.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package network import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/network" ) func inspectCommand() *cobra.Command { cmd := &cobra.Command{ Use: "inspect [flags] NETWORK [NETWORK, ...]", Short: "Display detailed information on one or more networks", Args: cobra.MinimumNArgs(1), RunE: inspectAction, ValidArgsFunction: networkInspectShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().String("mode", "dockercompat", `Inspect mode, "dockercompat" for Docker-compatible output, "native" for containerd-native output`) cmd.RegisterFlagCompletionFunc("mode", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"dockercompat", "native"}, cobra.ShellCompDirectiveNoFileComp }) cmd.Flags().StringP("format", "f", "", "Format the output using the given Go template, e.g, '{{json .}}'") cmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"json"}, cobra.ShellCompDirectiveNoFileComp }) return cmd } func inspectAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } mode, err := cmd.Flags().GetString("mode") if err != nil { return err } format, err := cmd.Flags().GetString("format") if err != nil { return err } options := types.NetworkInspectOptions{ GOptions: globalOptions, Mode: mode, Format: format, Networks: args, Stdout: cmd.OutOrStdout(), } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return network.Inspect(ctx, client, options) } func networkInspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show network names, including "bridge" exclude := []string{"host", "none"} return completion.NetworkNames(cmd, exclude) } ================================================ FILE: cmd/nerdctl/network/network_inspect_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package network import ( "encoding/json" "errors" "os/exec" "runtime" "strings" "testing" "time" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestNetworkInspect(t *testing.T) { testCase := nerdtest.Setup() const ( testSubnet = "10.24.24.0/24" testGateway = "10.24.24.1" testIPRange = "10.24.24.0/25" ) testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("network", "create", data.Identifier("basenet")) data.Labels().Set("basenet", data.Identifier("basenet")) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("network", "rm", data.Identifier("basenet")) } testCase.SubTests = []*test.Case{ { Description: "non existent network", Command: test.Command("network", "inspect", "nonexistent"), // FIXME: where is this error even comin from? Expected: test.Expects(1, []error{errors.New("no network found matching")}, nil), }, { Description: "invalid name network", Command: test.Command("network", "inspect", "∞"), // FIXME: this is not even a valid identifier Expected: test.Expects(1, []error{errors.New("no network found matching")}, nil), }, { Description: "none", Require: nerdtest.NerdctlNeedsFixing("no issue opened"), Command: test.Command("network", "inspect", "none"), Expected: test.Expects(0, nil, func(stdout string, t tig.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") assert.Equal(t, dc[0].Name, "none") }), }, { Description: "host", Require: nerdtest.NerdctlNeedsFixing("no issue opened"), Command: test.Command("network", "inspect", "host"), Expected: test.Expects(0, nil, func(stdout string, t tig.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") assert.Equal(t, dc[0].Name, "host") }), }, { Description: "bridge", Require: require.Not(require.Windows), Command: test.Command("network", "inspect", "bridge"), Expected: test.Expects(0, nil, func(stdout string, t tig.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") assert.Equal(t, dc[0].Name, "bridge") }), }, { Description: "nat", Require: require.Windows, Command: test.Command("network", "inspect", "nat"), Expected: test.Expects(0, nil, func(stdout string, t tig.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") assert.Equal(t, dc[0].Name, "nat") }), }, { Description: "custom", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("network", "create", "custom") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("network", "remove", "custom") }, Command: test.Command("network", "inspect", "custom"), Expected: test.Expects(0, nil, func(stdout string, t tig.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") assert.Equal(t, dc[0].Name, "custom") }), }, { Description: "match exact id", // See notes below Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { id := strings.TrimSpace(helpers.Capture("network", "inspect", data.Labels().Get("basenet"), "--format", "{{ .Id }}")) return helpers.Command("network", "inspect", id) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") assert.Equal(t, dc[0].Name, data.Labels().Get("basenet")) }, } }, }, { Description: "match part of id", // FIXME: for windows, network inspect testnetworkinspect-basenet-468cf999 --format {{ .Id }} MAY fail here // This is bizarre, as it is working in the match exact id test - and there does not seem to be a particular reason for that Require: require.Not(require.Windows), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { id := strings.TrimSpace(helpers.Capture("network", "inspect", data.Labels().Get("basenet"), "--format", "{{ .Id }}")) return helpers.Command("network", "inspect", id[0:25]) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") assert.Equal(t, dc[0].Name, data.Labels().Get("basenet")) }, } }, }, { Description: "using another net short id", // FIXME: for windows, network inspect testnetworkinspect-basenet-468cf999 --format {{ .Id }} MAY fail here // This is bizarre, as it is working in the match exact id test - and there does not seem to be a particular reason for that Require: require.Not(require.Windows), Setup: func(data test.Data, helpers test.Helpers) { id := strings.TrimSpace(helpers.Capture("network", "inspect", data.Labels().Get("basenet"), "--format", "{{ .Id }}")) helpers.Ensure("network", "create", id[0:12]) data.Labels().Set("netname", id[0:12]) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("network", "remove", data.Labels().Get("netname")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("network", "inspect", data.Labels().Get("netname")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") assert.Equal(t, dc[0].Name, data.Labels().Get("netname")) }, } }, }, { Description: "basic", // FIXME: IPAMConfig is not implemented on Windows yet Require: require.Not(require.Windows), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("network", "create", "--label", "tag=testNetwork", "--subnet", testSubnet, "--gateway", testGateway, "--ip-range", testIPRange, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("network", "rm", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("network", "inspect", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") got := dc[0] assert.Equal(t, got.Name, data.Identifier()) assert.Equal(t, got.Labels["tag"], "testNetwork") assert.Equal(t, len(got.IPAM.Config), 1) assert.Equal(t, got.IPAM.Config[0].Subnet, testSubnet) assert.Equal(t, got.IPAM.Config[0].Gateway, testGateway) assert.Equal(t, got.IPAM.Config[0].IPRange, testIPRange) }, } }, }, { Description: "with namespace", Require: require.Not(nerdtest.Docker), Cleanup: func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() helpers.Anyhow("network", "rm", identifier) helpers.Anyhow("namespace", "remove", identifier) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("network", "create", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { // Note: some functions need to be tested without the automatic --namespace nerdctl-test argument, so we need // to retrieve the binary name. // Note that we know this works already, so no need to assert err. bin, _ := exec.LookPath(testutil.GetTarget()) cmd := helpers.Custom(bin, "--namespace", data.Identifier()) com := cmd.Clone() com.WithArgs("network", "inspect", data.Identifier()) com.Run(&test.Expected{ ExitCode: 1, Errors: []error{errors.New("no network found")}, }) com = cmd.Clone() com.WithArgs("network", "remove", data.Identifier()) com.Run(&test.Expected{ ExitCode: 1, Errors: []error{errors.New("no network found")}, }) com = cmd.Clone() com.WithArgs("network", "ls") com.Run(&test.Expected{ Output: expect.DoesNotContain(data.Identifier()), }) com = cmd.Clone() com.WithArgs("network", "prune", "-f") com.Run(&test.Expected{ Output: expect.DoesNotContain(data.Identifier()), }) }, } }, }, { Description: "Verify that only active containers appear in the network inspect output", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("network", "create", data.Identifier("nginx-network-1")) helpers.Ensure("network", "create", data.Identifier("nginx-network-2")) // See https://github.com/containerd/nerdctl/issues/4322 // Maybe network create on windows is asynchronous? if runtime.GOOS == "windows" { time.Sleep(time.Second) } helpers.Ensure("create", "--name", data.Identifier("nginx-container-1"), "--network", data.Identifier("nginx-network-1"), testutil.NginxAlpineImage) helpers.Ensure("create", "--name", data.Identifier("nginx-container-2"), "--network", data.Identifier("nginx-network-1"), testutil.NginxAlpineImage) helpers.Ensure("create", "--name", data.Identifier("nginx-container-on-diff-network"), "--network", data.Identifier("nginx-network-2"), testutil.NginxAlpineImage) helpers.Ensure("start", data.Identifier("nginx-container-1"), data.Identifier("nginx-container-on-diff-network")) data.Labels().Set("nginx-container-1-id", strings.Trim(helpers.Capture("inspect", data.Identifier("nginx-container-1"), "--format", "{{.Id}}"), "\n")) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("nginx-container-1")) helpers.Anyhow("rm", "-f", data.Identifier("nginx-container-2")) helpers.Anyhow("rm", "-f", data.Identifier("nginx-container-on-diff-network")) helpers.Anyhow("network", "remove", data.Identifier("nginx-network-1")) helpers.Anyhow("network", "remove", data.Identifier("nginx-network-2")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("network", "inspect", data.Identifier("nginx-network-1")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") assert.Equal(t, dc[0].Name, data.Identifier("nginx-network-1")) // Assert only the "running" containers on the same network are returned. assert.Equal(t, 1, len(dc[0].Containers), "Expected a single container as per configuration, but got multiple.") assert.Equal(t, data.Identifier("nginx-container-1"), dc[0].Containers[data.Labels().Get("nginx-container-1-id")].Name) }, } }, }, { Description: "Display containers belonging to multiple networks in the output of nerdctl network inspect", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("network", "create", data.Identifier("network-1")) helpers.Ensure("network", "create", data.Identifier("network-2")) // See https://github.com/containerd/nerdctl/issues/4322 // Maybe network create on windows is asynchronous? if runtime.GOOS == "windows" { time.Sleep(time.Second) } containerID := helpers.Capture("run", "-d", "--name", data.Identifier(), "--network", data.Identifier("network-1"), "--network", data.Identifier("network-2"), testutil.CommonImage, "sleep", nerdtest.Infinity) data.Labels().Set("containerID", strings.Trim(containerID, "\n")) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("network", "remove", data.Identifier("network-1")) helpers.Anyhow("network", "remove", data.Identifier("network-2")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("network", "inspect", data.Identifier("network-1")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.JSON([]dockercompat.Network{}, func(dc []dockercompat.Network, t tig.T) { assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") assert.Equal(t, dc[0].Name, data.Identifier("network-1")) assert.Equal(t, 1, len(dc[0].Containers), "Expected a single container as per configuration, but got multiple.") assert.Equal(t, data.Identifier(), dc[0].Containers[data.Labels().Get("containerID")].Name) }), } }, }, { Description: "Display only containers attached to the specific network", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("network", "create", data.Identifier("some-network")) helpers.Ensure("network", "create", data.Identifier("some-network-as-well")) // See https://github.com/containerd/nerdctl/issues/4322 // Maybe network create on windows is asynchronous? if runtime.GOOS == "windows" { time.Sleep(time.Second) } helpers.Ensure("run", "-d", "--name", data.Identifier(), "--network", data.Identifier("some-network-as-well"), testutil.CommonImage, "sleep", nerdtest.Infinity) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("network", "remove", data.Identifier("some-network")) helpers.Anyhow("network", "remove", data.Identifier("some-network-as-well")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("network", "inspect", data.Identifier("some-network")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.JSON([]dockercompat.Network{}, func(dc []dockercompat.Network, t tig.T) { assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") assert.Equal(t, dc[0].Name, data.Identifier("some-network")) assert.Equal(t, 0, len(dc[0].Containers), "Expected no containers as per configuration, but got multiple.") }), } }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/network/network_list.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package network import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/cmd/network" ) func listCommand() *cobra.Command { cmd := &cobra.Command{ Use: "ls", Aliases: []string{"list"}, Short: "List networks", Args: cobra.NoArgs, RunE: listAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().BoolP("quiet", "q", false, "Only display network IDs") cmd.Flags().StringSliceP("filter", "f", []string{}, "Provide filter values (e.g. \"name=default\")") cmd.Flags().String("format", "", "Format the output using the given Go template, e.g, '{{json .}}'") cmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"json", "table", "wide"}, cobra.ShellCompDirectiveNoFileComp }) return cmd } func listAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } quiet, err := cmd.Flags().GetBool("quiet") if err != nil { return err } format, err := cmd.Flags().GetString("format") if err != nil { return err } filters, err := cmd.Flags().GetStringSlice("filter") if err != nil { return err } return network.List(cmd.Context(), types.NetworkListOptions{ GOptions: globalOptions, Quiet: quiet, Format: format, Filters: filters, Stdout: cmd.OutOrStdout(), }) } ================================================ FILE: cmd/nerdctl/network/network_list_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package network import ( "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestNetworkLsFilter(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { data.Labels().Set("identifier", data.Identifier()) data.Labels().Set("label", "mylabel=label-1") data.Labels().Set("net1", data.Identifier("1")) data.Labels().Set("net2", data.Identifier("2")) data.Labels().Set("netID1", helpers.Capture("network", "create", "--label="+data.Labels().Get("label"), data.Labels().Get("net1"))) data.Labels().Set("netID2", helpers.Capture("network", "create", data.Labels().Get("net2"))) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("network", "rm", data.Identifier("1")) helpers.Anyhow("network", "rm", data.Identifier("2")) } testCase.SubTests = []*test.Case{ { Description: "filter label", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("network", "ls", "--quiet", "--filter", "label="+data.Labels().Get("label")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 1, "expected at least one line\n") netNames := map[string]struct{}{ data.Labels().Get("netID1")[:12]: {}, } for _, name := range lines { _, ok := netNames[name] assert.Assert(t, ok, "expected to find name\n") } }, } }, }, { Description: "filter name", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("network", "ls", "--quiet", "--filter", "name="+data.Labels().Get("net2")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 1, "expected at least one line\n") netNames := map[string]struct{}{ data.Labels().Get("netID2")[:12]: {}, } for _, name := range lines { _, ok := netNames[name] assert.Assert(t, ok, "expected to find name\n") } }, } }, }, { Description: "filter name regexp", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("network", "ls", "--quiet", "--filter", "name=.*"+data.Labels().Get("net2")+".*") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 1) netNames := map[string]struct{}{ data.Labels().Get("netID2")[:12]: {}, } for _, name := range lines { _, ok := netNames[name] assert.Assert(t, ok) } }, } }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/network/network_prune.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package network import ( "fmt" "strings" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/network" ) var NetworkDriversToKeep = []string{"host", "none", DefaultNetworkDriver} func pruneCommand() *cobra.Command { cmd := &cobra.Command{ Use: "prune [flags]", Short: "Remove all unused networks", Args: cobra.NoArgs, RunE: pruneAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().BoolP("force", "f", false, "Do not prompt for confirmation") return cmd } func pruneAction(cmd *cobra.Command, _ []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } force, err := cmd.Flags().GetBool("force") if err != nil { return err } if !force { var confirm string msg := "This will remove all custom networks not used by at least one container." msg += "\nAre you sure you want to continue? [y/N] " fmt.Fprintf(cmd.OutOrStdout(), "WARNING! %s", msg) fmt.Fscanf(cmd.InOrStdin(), "%s", &confirm) if strings.ToLower(confirm) != "y" { return nil } } options := types.NetworkPruneOptions{ GOptions: globalOptions, NetworkDriversToKeep: NetworkDriversToKeep, Stdout: cmd.OutOrStdout(), } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return network.Prune(ctx, client, options) } ================================================ FILE: cmd/nerdctl/network/network_prune_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package network import ( "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestNetworkPrune(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = nerdtest.Private testCase.SubTests = []*test.Case{ { Description: "Prune does not collect started container network", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() helpers.Ensure("network", "create", identifier) helpers.Ensure("run", "-d", "--net", identifier, "--name", identifier, testutil.CommonImage, "sleep", nerdtest.Infinity) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("network", "rm", data.Identifier()) }, Command: test.Command("network", "prune", "-f"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.DoesNotContain(data.Identifier()), } }, }, { Description: "Prune does collect stopped container network", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("network", "create", data.Identifier()) helpers.Ensure("run", "-d", "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) helpers.Ensure("stop", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("network", "rm", data.Identifier()) }, Command: test.Command("network", "prune", "-f"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.Contains(data.Identifier()), } }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/network/network_remove.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package network import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/network" "github.com/containerd/nerdctl/v2/pkg/netutil" ) func removeCommand() *cobra.Command { cmd := &cobra.Command{ Use: "rm [flags] NETWORK [NETWORK, ...]", Aliases: []string{"remove"}, Short: "Remove one or more networks", Long: "NOTE: network in use is deleted without caution", Args: cobra.MinimumNArgs(1), RunE: removeAction, ValidArgsFunction: networkRmShellComplete, SilenceUsage: true, SilenceErrors: true, } return cmd } func removeAction(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } options := types.NetworkRemoveOptions{ GOptions: globalOptions, Networks: args, Stdout: cmd.OutOrStdout(), } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return network.Remove(ctx, client, options) } func networkRmShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show network names, including "bridge" exclude := []string{netutil.DefaultNetworkName, "host", "none"} return completion.NetworkNames(cmd, exclude) } ================================================ FILE: cmd/nerdctl/network/network_remove_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package network import ( "errors" "testing" "github.com/vishvananda/netlink" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestNetworkRemove(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = nerdtest.Rootful testCase.SubTests = []*test.Case{ { Description: "Simple network remove", Setup: func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() helpers.Ensure("network", "create", identifier) data.Labels().Set("netID", nerdtest.InspectNetwork(helpers, identifier).ID) helpers.Ensure("run", "--rm", "--net", identifier, "--name", identifier, testutil.CommonImage) // Verity the network is here _, err := netlink.LinkByName("br-" + data.Labels().Get("netID")[:12]) assert.NilError(t, err, "failed to find network br-"+data.Labels().Get("netID")[:12], "%v") }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("network", "rm", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("network", "rm", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { _, err := netlink.LinkByName("br-" + data.Labels().Get("netID")[:12]) assert.Error(t, err, "Link not found") }, } }, }, { Description: "Network remove when linked to container", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("network", "create", data.Identifier()) helpers.Ensure("run", "-d", "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("network", "rm", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("network", "rm", data.Identifier()) }, Expected: test.Expects(1, []error{errors.New("is in use")}, nil), }, { Description: "Network remove by id", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("network", "create", data.Identifier()) data.Labels().Set("netID", nerdtest.InspectNetwork(helpers, data.Identifier()).ID) helpers.Ensure("run", "--rm", "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage) // Verity the network is here _, err := netlink.LinkByName("br-" + data.Labels().Get("netID")[:12]) assert.NilError(t, err, "failed to find network br-"+data.Labels().Get("netID")[:12], "%v") }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("network", "rm", data.Labels().Get("netID")) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("network", "rm", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { _, err := netlink.LinkByName("br-" + data.Labels().Get("netID")[:12]) assert.Error(t, err, "Link not found") }, } }, }, { Description: "Network remove by short id", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("network", "create", data.Identifier()) data.Labels().Set("netID", nerdtest.InspectNetwork(helpers, data.Identifier()).ID) helpers.Ensure("run", "--rm", "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage) // Verity the network is here _, err := netlink.LinkByName("br-" + data.Labels().Get("netID")[:12]) assert.NilError(t, err, "failed to find network br-"+data.Labels().Get("netID")[:12], "%v") }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("network", "rm", data.Labels().Get("netID")[:12]) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("network", "rm", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { _, err := netlink.LinkByName("br-" + data.Labels().Get("netID")[:12]) assert.Error(t, err, "Link not found") }, } }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/network/network_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package network import ( "testing" "github.com/containerd/nerdctl/v2/pkg/testutil" ) func TestMain(m *testing.M) { testutil.M(m) } ================================================ FILE: cmd/nerdctl/search/search.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package search import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/cmd/search" ) func Command() *cobra.Command { cmd := &cobra.Command{ Use: "search [OPTIONS] TERM", Short: "Search registry for images", Args: cobra.ExactArgs(1), RunE: runSearch, DisableFlagsInUseLine: true, } flags := cmd.Flags() flags.Bool("no-trunc", false, "Don't truncate output") flags.StringSliceP("filter", "f", nil, "Filter output based on conditions provided") flags.Int("limit", 0, "Max number of search results") flags.String("format", "", "Pretty-print search using a Go template") return cmd } func processSearchFlags(cmd *cobra.Command) (types.SearchOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.SearchOptions{}, err } noTrunc, err := cmd.Flags().GetBool("no-trunc") if err != nil { return types.SearchOptions{}, err } limit, err := cmd.Flags().GetInt("limit") if err != nil { return types.SearchOptions{}, err } format, err := cmd.Flags().GetString("format") if err != nil { return types.SearchOptions{}, err } filter, err := cmd.Flags().GetStringSlice("filter") if err != nil { return types.SearchOptions{}, err } return types.SearchOptions{ Stdout: cmd.OutOrStdout(), GOptions: globalOptions, NoTrunc: noTrunc, Limit: limit, Filters: filter, Format: format, }, nil } func runSearch(cmd *cobra.Command, args []string) error { options, err := processSearchFlags(cmd) if err != nil { return err } return search.Search(cmd.Context(), args[0], options) } ================================================ FILE: cmd/nerdctl/search/search_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package search import ( "errors" "regexp" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) // All tests in this file are based on the output of `nerdctl search alpine`. // // Expected output format (default behavior with --limit 10): // // NAME DESCRIPTION STARS OFFICIAL // alpine A minimal Docker image based on Alpine Linux… 11437 [OK] // alpine/git A simple git container running in alpine li… 249 // alpine/socat Run socat command in alpine container 115 // alpine/helm Auto-trigger docker build for kubernetes hel… 69 // alpine/curl 11 // alpine/k8s Kubernetes toolbox for EKS (kubectl, helm, i… 64 // alpine/bombardier Auto-trigger docker build for bombardier whe… 28 // alpine/httpie Auto-trigger docker build for `httpie` when … 21 // alpine/terragrunt Auto-trigger docker build for terragrunt whe… 18 // alpine/openssl openssl 7 func TestSearch(t *testing.T) { testCase := nerdtest.Setup() testCase.SubTests = []*test.Case{ { Description: "basic-search", Command: test.Command("search", "alpine", "--limit", "5"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, Output: expect.All( expect.Contains("NAME"), expect.Contains("DESCRIPTION"), expect.Contains("STARS"), expect.Contains("OFFICIAL"), expect.Match(regexp.MustCompile(`NAME\s+DESCRIPTION\s+STARS\s+OFFICIAL`)), expect.Contains("alpine"), expect.Match(regexp.MustCompile(`alpine\s+A minimal Docker image based on Alpine Linux`)), expect.Match(regexp.MustCompile(`alpine\s+.*\s+\d+\s+\[OK\]`)), expect.Contains("[OK]"), expect.Match(regexp.MustCompile(`alpine/\w+`)), ), } }, }, { Description: "search-library-image", Command: test.Command("search", "library/alpine", "--limit", "5"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, Output: expect.All( expect.Contains("NAME"), expect.Contains("DESCRIPTION"), expect.Contains("STARS"), expect.Contains("OFFICIAL"), expect.Contains("alpine"), expect.Match(regexp.MustCompile(`alpine\s+.*\s+\d+\s+\[OK\]`)), ), } }, }, { Description: "search-with-no-trunc", Command: test.Command("search", "alpine", "--limit", "3", "--no-trunc"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, Output: expect.All( expect.Contains("NAME"), expect.Contains("DESCRIPTION"), expect.Contains("alpine"), // With --no-trunc, the full description should be visible (not truncated with …) expect.Match(regexp.MustCompile(`alpine\s+A minimal Docker image based on Alpine Linux with a complete package index and only 5 MB in size!`)), ), } }, }, { Description: "search-with-format", Command: test.Command("search", "alpine", "--limit", "2", "--format", "{{.Name}}: {{.StarCount}}"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, Output: expect.All( expect.Match(regexp.MustCompile(`alpine:\s*\d+`)), expect.DoesNotContain("NAME"), expect.DoesNotContain("DESCRIPTION"), expect.DoesNotContain("OFFICIAL"), ), } }, }, { Description: "search-output-format", Command: test.Command("search", "alpine", "--limit", "5"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, Output: expect.All( expect.Match(regexp.MustCompile(`NAME\s+DESCRIPTION\s+STARS\s+OFFICIAL`)), expect.Match(regexp.MustCompile(`(?m)^alpine\s+.*\s+\d+\s+\[OK\]\s*$`)), expect.Match(regexp.MustCompile(`(?m)^alpine/\w+\s+.*\s+\d+\s*$`)), expect.DoesNotMatch(regexp.MustCompile(`(?m)^\s+\d+\s*$`)), ), } }, }, { Description: "search-description-formatting", Command: test.Command("search", "alpine", "--limit", "10"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, Output: expect.All( expect.Match(regexp.MustCompile(`Alpine Linux…`)), expect.DoesNotMatch(regexp.MustCompile(`(?m)^\s+\d+\s+`)), expect.Match(regexp.MustCompile(`(?m)^[a-z0-9/_-]+\s+.*\s+\d+`)), ), } }, }, } testCase.Run(t) } func TestSearchWithFilter(t *testing.T) { testCase := nerdtest.Setup() testCase.SubTests = []*test.Case{ { Description: "filter-is-official-true", Command: test.Command("search", "alpine", "--filter", "is-official=true", "--limit", "5"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, Output: expect.All( expect.Contains("NAME"), expect.Contains("OFFICIAL"), expect.Contains("alpine"), expect.Contains("[OK]"), expect.Match(regexp.MustCompile(`alpine\s+.*\s+\d+\s+\[OK\]`)), ), } }, }, { Description: "filter-stars", Command: test.Command("search", "alpine", "--filter", "stars=10000"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, Output: expect.All( expect.Contains("NAME"), expect.Contains("STARS"), expect.Contains("alpine"), // The official alpine image has > 10000 stars expect.Match(regexp.MustCompile(`alpine\s+.*\s+\d{4,}\s+\[OK\]`)), ), } }, }, } testCase.Run(t) } func TestSearchFilterErrors(t *testing.T) { testCase := nerdtest.Setup() testCase.SubTests = []*test.Case{ { Description: "invalid-filter-format", Command: test.Command("search", "alpine", "--filter", "foo"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeGenericFail, Errors: []error{errors.New("bad format of filter (expected name=value)")}, } }, }, { Description: "invalid-filter-key", Command: test.Command("search", "alpine", "--filter", "foo=bar"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeGenericFail, Errors: []error{errors.New("invalid filter 'foo'")}, } }, }, { Description: "invalid-stars-value", Command: test.Command("search", "alpine", "--filter", "stars=abc"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeGenericFail, Errors: []error{errors.New("invalid filter 'stars=abc'")}, } }, }, { Description: "invalid-is-official-value", Command: test.Command("search", "alpine", "--filter", "is-official=abc"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeGenericFail, Errors: []error{errors.New("invalid filter 'is-official=abc'")}, } }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/search/search_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package search import ( "testing" "github.com/containerd/nerdctl/v2/pkg/testutil" ) func TestMain(m *testing.M) { testutil.M(m) } ================================================ FILE: cmd/nerdctl/system/system.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package system import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" ) func Command() *cobra.Command { var cmd = &cobra.Command{ Annotations: map[string]string{helpers.Category: helpers.Management}, Use: "system", Short: "Manage containerd", RunE: helpers.UnknownSubcommandAction, SilenceUsage: true, SilenceErrors: true, } // versionCommand is not here cmd.AddCommand( EventsCommand(), InfoCommand(), pruneCommand(), ) return cmd } ================================================ FILE: cmd/nerdctl/system/system_events.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package system import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/system" ) func EventsCommand() *cobra.Command { shortHelp := `Get real time events from the server` longHelp := shortHelp + "\nNOTE: The output format is not compatible with Docker." var cmd = &cobra.Command{ Use: "events", Args: cobra.NoArgs, Short: shortHelp, Long: longHelp, RunE: eventsAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().String("format", "", "Format the output using the given Go template, e.g, '{{json .}}'") cmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"json"}, cobra.ShellCompDirectiveNoFileComp }) cmd.Flags().StringSliceP("filter", "f", []string{}, "Filter matches containers based on given conditions") return cmd } func eventsOptions(cmd *cobra.Command) (types.SystemEventsOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.SystemEventsOptions{}, err } format, err := cmd.Flags().GetString("format") if err != nil { return types.SystemEventsOptions{}, err } filters, err := cmd.Flags().GetStringSlice("filter") if err != nil { return types.SystemEventsOptions{}, err } return types.SystemEventsOptions{ Stdout: cmd.OutOrStdout(), GOptions: globalOptions, Format: format, Filters: filters, }, nil } func eventsAction(cmd *cobra.Command, args []string) error { options, err := eventsOptions(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return system.Events(ctx, client, options) } ================================================ FILE: cmd/nerdctl/system/system_events_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package system import ( "testing" "time" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func testEventFilterExecutor(data test.Data, helpers test.Helpers) test.TestableCommand { helpers.Ensure("pull", testutil.CommonImage) cmd := helpers.Command("events", "--filter", data.Labels().Get("filter"), "--format", "json") // 3 seconds is too short on slow rig (EL8) cmd.WithTimeout(10 * time.Second) cmd.Background() helpers.Ensure("run", "--rm", testutil.CommonImage) return cmd } func TestEventFilters(t *testing.T) { testCase := nerdtest.Setup() testCase.SubTests = []*test.Case{ { Description: "CapitalizedFilter", Require: require.Not(nerdtest.Docker), Command: testEventFilterExecutor, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeTimeout, Output: expect.Contains(data.Labels().Get("output")), } }, Data: test.WithLabels(map[string]string{ "filter": "event=START", "output": "\"Status\":\"start\"", }), }, { Description: "StartEventFilter", Command: testEventFilterExecutor, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeTimeout, Output: expect.Contains(data.Labels().Get("output")), } }, Data: test.WithLabels(map[string]string{ "filter": "event=start", "output": "tatus\":\"start\"", }), }, { Description: "UnsupportedEventFilter", Require: require.Not(nerdtest.Docker), Command: testEventFilterExecutor, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeTimeout, Output: expect.Contains(data.Labels().Get("output")), } }, Data: test.WithLabels(map[string]string{ "filter": "event=unknown", "output": "\"Status\":\"unknown\"", }), }, { Description: "StatusFilter", Command: testEventFilterExecutor, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeTimeout, Output: expect.Contains(data.Labels().Get("output")), } }, Data: test.WithLabels(map[string]string{ "filter": "status=start", "output": "tatus\":\"start\"", }), }, { Description: "UnsupportedStatusFilter", Require: require.Not(nerdtest.Docker), Command: testEventFilterExecutor, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeTimeout, Output: expect.Contains(data.Labels().Get("output")), } }, Data: test.WithLabels(map[string]string{ "filter": "status=unknown", "output": "\"Status\":\"unknown\"", }), }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/system/system_info.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package system import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/system" ) func InfoCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "info", Args: cobra.NoArgs, Short: "Display system-wide information", RunE: infoAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().String("mode", "dockercompat", `Information mode, "dockercompat" for Docker-compatible output, "native" for containerd-native output`) cmd.RegisterFlagCompletionFunc("mode", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"dockercompat", "native"}, cobra.ShellCompDirectiveNoFileComp }) cmd.Flags().StringP("format", "f", "", "Format the output using the given Go template, e.g, '{{json .}}'") cmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"json"}, cobra.ShellCompDirectiveNoFileComp }) return cmd } func infoOptions(cmd *cobra.Command) (types.SystemInfoOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.SystemInfoOptions{}, err } mode, err := cmd.Flags().GetString("mode") if err != nil { return types.SystemInfoOptions{}, err } format, err := cmd.Flags().GetString("format") if err != nil { return types.SystemInfoOptions{}, err } return types.SystemInfoOptions{ GOptions: globalOptions, Mode: mode, Format: format, Stdout: cmd.OutOrStdout(), Stderr: cmd.OutOrStderr(), }, nil } func infoAction(cmd *cobra.Command, args []string) error { options, err := infoOptions(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return system.Info(ctx, client, options) } ================================================ FILE: cmd/nerdctl/system/system_info_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package system import ( "encoding/json" "fmt" "os/exec" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/infoutil" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func testInfoComparator(stdout string, t tig.T) { var dinf dockercompat.Info err := json.Unmarshal([]byte(stdout), &dinf) assert.NilError(t, err, "failed to unmarshal stdout") unameM := infoutil.UnameM() assert.Assert(t, dinf.Architecture == unameM, fmt.Sprintf("expected info.Architecture to be %q, got %q", unameM, dinf.Architecture)) } func TestInfo(t *testing.T) { testCase := nerdtest.Setup() // Note: some functions need to be tested without the automatic --namespace nerdctl-test argument, so we need // to retrieve the binary name. // Note that we know this works already, so no need to assert err. bin, _ := exec.LookPath(testutil.GetTarget()) testCase.SubTests = []*test.Case{ { Description: "info", Command: test.Command("info", "--format", "{{json .}}"), Expected: test.Expects(0, nil, testInfoComparator), }, { Description: "info convenience form", Command: test.Command("info", "--format", "json"), Expected: test.Expects(0, nil, testInfoComparator), }, { Description: "info with namespace", Require: require.Not(nerdtest.Docker), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Custom(bin, "info") }, Expected: test.Expects(0, nil, expect.Contains("Namespace: default")), }, { Description: "info with namespace env var", Env: map[string]string{ "CONTAINERD_NAMESPACE": "test", }, Require: require.Not(nerdtest.Docker), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Custom(bin, "info") }, Expected: test.Expects(0, nil, expect.Contains("Namespace: test")), }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/system/system_prune.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package system import ( "fmt" "strings" "github.com/spf13/cobra" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/cmd/nerdctl/builder" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/cmd/nerdctl/network" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/system" ) func pruneCommand() *cobra.Command { cmd := &cobra.Command{ Use: "prune [flags]", Short: "Remove unused data", Args: cobra.NoArgs, RunE: pruneAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().BoolP("all", "a", false, "Remove all unused images, not just dangling ones") cmd.Flags().BoolP("force", "f", false, "Do not prompt for confirmation") cmd.Flags().Bool("volumes", false, "Prune volumes") return cmd } func pruneOptions(cmd *cobra.Command) (types.SystemPruneOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.SystemPruneOptions{}, err } all, err := cmd.Flags().GetBool("all") if err != nil { return types.SystemPruneOptions{}, err } vFlag, err := cmd.Flags().GetBool("volumes") if err != nil { return types.SystemPruneOptions{}, err } buildkitHost, err := builder.GetBuildkitHost(cmd, globalOptions.Namespace) if err != nil { log.L.WithError(err).Warn("BuildKit is not running. Build caches will not be pruned.") buildkitHost = "" } return types.SystemPruneOptions{ Stdout: cmd.OutOrStdout(), Stderr: cmd.ErrOrStderr(), GOptions: globalOptions, All: all, Volumes: vFlag, BuildKitHost: buildkitHost, NetworkDriversToKeep: network.NetworkDriversToKeep, }, nil } func grantSystemPrunePermission(cmd *cobra.Command, options types.SystemPruneOptions) (bool, error) { force, err := cmd.Flags().GetBool("force") if err != nil { return false, err } if !force { var confirm string msg := `This will remove: - all stopped containers - all networks not used by at least one container` if options.Volumes { msg += ` - all volumes not used by at least one container` } if options.All { msg += ` - all images without at least one container associated to them - all build cache` } else { msg += ` - all dangling images - all dangling build cache` } msg += "\nAre you sure you want to continue? [y/N] " fmt.Fprintf(options.Stdout, "WARNING! %s", msg) fmt.Fscanf(cmd.InOrStdin(), "%s", &confirm) if strings.ToLower(confirm) != "y" { return false, nil } } return true, nil } func pruneAction(cmd *cobra.Command, _ []string) error { options, err := pruneOptions(cmd) if err != nil { return err } if ok, err := grantSystemPrunePermission(cmd, options); err != nil { return err } else if !ok { return nil } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return system.Prune(ctx, client, options) } ================================================ FILE: cmd/nerdctl/system/system_prune_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package system import ( "fmt" "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestSystemPrune(t *testing.T) { testCase := nerdtest.Setup() testCase.NoParallel = true testCase.SubTests = []*test.Case{ { Description: "volume prune all success", // Private because of prune evidently Require: nerdtest.Private, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("network", "create", data.Identifier()) helpers.Ensure("volume", "create", data.Identifier()) anonIdentifier := helpers.Capture("volume", "create") helpers.Ensure("run", "-v", fmt.Sprintf("%s:/volume", data.Identifier()), "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage) data.Labels().Set("anonIdentifier", anonIdentifier) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("network", "rm", data.Identifier()) helpers.Anyhow("volume", "rm", data.Identifier()) helpers.Anyhow("volume", "rm", data.Labels().Get("anonIdentifier")) helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: test.Command("system", "prune", "-f", "--volumes", "--all"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, Output: func(stdout string, t tig.T) { volumes := helpers.Capture("volume", "ls") networks := helpers.Capture("network", "ls") images := helpers.Capture("images") containers := helpers.Capture("ps", "-a") assert.Assert(t, strings.Contains(volumes, data.Identifier()), volumes) assert.Assert(t, !strings.Contains(volumes, data.Labels().Get("anonIdentifier")), volumes) assert.Assert(t, !strings.Contains(containers, data.Identifier()), containers) assert.Assert(t, !strings.Contains(networks, data.Identifier()), networks) assert.Assert(t, !strings.Contains(images, testutil.CommonImage), images) }, } }, }, { Description: "buildkit", // FIXME: using a dedicated namespace does not work with rootful (because of buildkitd) NoParallel: true, // buildkitd is not available with docker Require: require.All(nerdtest.Build, require.Not(nerdtest.Docker)), // FIXME: this test will happily say "green" even if the command actually fails to do its duty // if there is nothing in the build cache. // Ensure with setup here that we DO build something first Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("system", "prune", "-f", "--volumes", "--all") }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return nerdtest.BuildCtlCommand(helpers, "du") }, Expected: test.Expects(0, nil, expect.Contains("Total:\t\t0B")), }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/system/system_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package system import ( "testing" "github.com/containerd/nerdctl/v2/pkg/testutil" ) func TestMain(m *testing.M) { testutil.M(m) } ================================================ FILE: cmd/nerdctl/version.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "bytes" "fmt" "io" "os" "text/template" "github.com/spf13/cobra" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/formatter" "github.com/containerd/nerdctl/v2/pkg/infoutil" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" ) func versionCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "version", Args: cobra.NoArgs, Short: "Show the nerdctl version information", RunE: versionAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().StringP("format", "f", "", "Format the output using the given Go template, e.g, '{{json .}}'") cmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"json"}, cobra.ShellCompDirectiveNoFileComp }) return cmd } func versionAction(cmd *cobra.Command, args []string) error { var w io.Writer = os.Stdout var tmpl *template.Template globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err } format, err := cmd.Flags().GetString("format") if err != nil { return err } if format != "" { var err error tmpl, err = formatter.ParseTemplate(format) if err != nil { return err } } address := globalOptions.Address // rootless `nerdctl version` runs in the host namespaces, so the address is different if rootlessutil.IsRootless() { address, err = rootlessutil.RootlessContainredSockAddress() if err != nil { log.L.WithError(err).Warning("failed to inspect the rootless containerd socket address") address = "" } } v, vErr := versionInfo(cmd, globalOptions.Namespace, address) if tmpl != nil { var b bytes.Buffer if err := tmpl.Execute(&b, v); err != nil { return err } if _, err := fmt.Fprintln(w, b.String()); err != nil { return err } } else { fmt.Fprintln(w, "Client:") fmt.Fprintf(w, " Version:\t%s\n", v.Client.Version) fmt.Fprintf(w, " OS/Arch:\t%s/%s\n", v.Client.Os, v.Client.Arch) fmt.Fprintf(w, " Git commit:\t%s\n", v.Client.GitCommit) for _, compo := range v.Client.Components { fmt.Fprintf(w, " %s:\n", compo.Name) fmt.Fprintf(w, " Version:\t%s\n", compo.Version) for detailK, detailV := range compo.Details { fmt.Fprintf(w, " %s:\t%s\n", detailK, detailV) } } if v.Server != nil { fmt.Fprintln(w) fmt.Fprintln(w, "Server:") for _, compo := range v.Server.Components { fmt.Fprintf(w, " %s:\n", compo.Name) fmt.Fprintf(w, " Version:\t%s\n", compo.Version) for detailK, detailV := range compo.Details { fmt.Fprintf(w, " %s:\t%s\n", detailK, detailV) } } } } return vErr } // versionInfo may return partial VersionInfo on error. // Address can be empty to skip inspecting the server. func versionInfo(cmd *cobra.Command, ns, address string) (dockercompat.VersionInfo, error) { v := dockercompat.VersionInfo{ Client: infoutil.ClientVersion(), } if address == "" { return v, nil } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), ns, address) if err != nil { return v, err } defer cancel() v.Server, err = infoutil.ServerVersion(ctx, client) return v, err } ================================================ FILE: cmd/nerdctl/volume/volume.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package volume import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" ) func Command() *cobra.Command { cmd := &cobra.Command{ Annotations: map[string]string{helpers.Category: helpers.Management}, Use: "volume", Short: "Manage volumes", RunE: helpers.UnknownSubcommandAction, SilenceUsage: true, SilenceErrors: true, } cmd.AddCommand( listCommand(), inspectCommand(), createCommand(), removeCommand(), pruneCommand(), ) return cmd } ================================================ FILE: cmd/nerdctl/volume/volume_create.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package volume import ( "fmt" "github.com/spf13/cobra" "github.com/containerd/errdefs" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/cmd/volume" ) func createCommand() *cobra.Command { cmd := &cobra.Command{ Use: "create [flags] [VOLUME]", Short: "Create a volume", Args: cobra.MaximumNArgs(1), RunE: createAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().StringArray("label", nil, "Set a label on the volume") return cmd } func createOptions(cmd *cobra.Command) (types.VolumeCreateOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.VolumeCreateOptions{}, err } labels, err := cmd.Flags().GetStringArray("label") if err != nil { return types.VolumeCreateOptions{}, err } for _, label := range labels { if label == "" { return types.VolumeCreateOptions{}, fmt.Errorf("labels cannot be empty (%w)", errdefs.ErrInvalidArgument) } } return types.VolumeCreateOptions{ GOptions: globalOptions, Labels: labels, Stdout: cmd.OutOrStdout(), }, nil } func createAction(cmd *cobra.Command, args []string) error { options, err := createOptions(cmd) if err != nil { return err } volumeName := "" if len(args) > 0 { volumeName = args[0] } _, err = volume.Create(volumeName, options) return err } ================================================ FILE: cmd/nerdctl/volume/volume_create_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package volume import ( "errors" "regexp" "testing" "github.com/containerd/errdefs" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestVolumeCreate(t *testing.T) { testCase := nerdtest.Setup() testCase.SubTests = []*test.Case{ { Description: "arg missing should create anonymous volume", Command: test.Command("volume", "create"), Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile("^[a-f0-9]{64}\n$"))), }, { Description: "invalid identifier should fail", Command: test.Command("volume", "create", "∞"), Expected: test.Expects(1, []error{errdefs.ErrInvalidArgument}, nil), }, { Description: "too many args should fail", Command: test.Command("volume", "create", "too", "many"), Expected: test.Expects(1, []error{errors.New("at most 1 arg")}, nil), }, { Description: "success", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "create", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("volume", "rm", "-f", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.Equals(data.Identifier() + "\n"), } }, }, { Description: "success with labels", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "create", "--label", "foo1=baz1", "--label", "foo2=baz2", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("volume", "rm", "-f", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.Equals(data.Identifier() + "\n"), } }, }, { Description: "invalid labels should fail", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { // See https://github.com/containerd/nerdctl/issues/3126 return helpers.Command("volume", "create", "--label", "a", "--label", "", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("volume", "rm", "-f", data.Identifier()) }, // NOTE: docker returns 125 on this Expected: test.Expects(expect.ExitCodeGenericFail, []error{errdefs.ErrInvalidArgument}, nil), }, { Description: "creating already existing volume should succeed", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("volume", "create", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "create", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("volume", "rm", "-f", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.Equals(data.Identifier() + "\n"), } }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/volume/volume_inspect.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package volume import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/cmd/volume" ) func inspectCommand() *cobra.Command { cmd := &cobra.Command{ Use: "inspect [flags] VOLUME [VOLUME...]", Short: "Display detailed information on one or more volumes", Args: cobra.MinimumNArgs(1), RunE: inspectAction, ValidArgsFunction: volumeInspectShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().StringP("format", "f", "", "Format the output using the given Go template, e.g, '{{json .}}'") cmd.Flags().BoolP("size", "s", false, "Display the disk usage of the volume") cmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"json"}, cobra.ShellCompDirectiveNoFileComp }) return cmd } func inspectOptions(cmd *cobra.Command) (types.VolumeInspectOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.VolumeInspectOptions{}, err } volumeSize, err := cmd.Flags().GetBool("size") if err != nil { return types.VolumeInspectOptions{}, err } format, err := cmd.Flags().GetString("format") if err != nil { return types.VolumeInspectOptions{}, err } return types.VolumeInspectOptions{ GOptions: globalOptions, Format: format, Size: volumeSize, Stdout: cmd.OutOrStdout(), }, nil } func inspectAction(cmd *cobra.Command, args []string) error { options, err := inspectOptions(cmd) if err != nil { return err } return volume.Inspect(cmd.Context(), args, options) } func volumeInspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show volume names return completion.VolumeNames(cmd) } ================================================ FILE: cmd/nerdctl/volume/volume_inspect_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package volume import ( "crypto/rand" "errors" "fmt" "os" "path/filepath" "testing" "gotest.tools/v3/assert" "github.com/containerd/errdefs" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func createFileWithSize(mountPoint string, size int64) error { token := make([]byte, size) _, _ = rand.Read(token) err := os.WriteFile(filepath.Join(mountPoint, "test-file"), token, 0644) return err } func TestVolumeInspect(t *testing.T) { var size int64 = 1028 testCase := nerdtest.Setup() testCase.Require = nerdtest.BrokenTest("This test assumes that the host-side of a volume can be written into, "+ "which is not always true. To be replaced by cp into the container.", &test.Requirement{ Check: func(data test.Data, helpers test.Helpers) (bool, string) { isDocker, _ := nerdtest.Docker.Check(data, helpers) return !isDocker || os.Geteuid() == 0, "docker cli needs to be run as root" }, }) testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("volume", "create", data.Identifier("first")) helpers.Ensure("volume", "create", "--label", "foo=fooval", "--label", "bar=barval", data.Identifier("second")) // Obviously note here that if inspect code gets totally hosed, this entire suite will // probably fail right here on the Setup instead of actually testing something vol := nerdtest.InspectVolume(helpers, data.Identifier("first")) err := createFileWithSize(vol.Mountpoint, size) assert.NilError(t, err, "File creation failed") data.Labels().Set("vol1", data.Identifier("first")) data.Labels().Set("vol2", data.Identifier("second")) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("volume", "rm", "-f", data.Identifier("first")) helpers.Anyhow("volume", "rm", "-f", data.Identifier("second")) } testCase.SubTests = []*test.Case{ { Description: "arg missing should fail", Command: test.Command("volume", "inspect"), Expected: test.Expects(1, []error{errors.New("requires at least 1 arg")}, nil), }, { Description: "invalid identifier should fail", Command: test.Command("volume", "inspect", "∞"), Expected: test.Expects(1, []error{errdefs.ErrInvalidArgument}, nil), }, { Description: "non existent volume should fail", Command: test.Command("volume", "inspect", "doesnotexist"), Expected: test.Expects(1, []error{errdefs.ErrNotFound}, nil), }, { Description: "success", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "inspect", data.Labels().Get("vol1")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( expect.Contains(data.Labels().Get("vol1")), expect.JSON([]native.Volume{}, func(dc []native.Volume, t tig.T) { assert.Assert(t, len(dc) == 1, fmt.Sprintf("one result, not %d", len(dc))) assert.Assert(t, dc[0].Name == data.Labels().Get("vol1"), fmt.Sprintf("expected name to be %q (was %q)", data.Labels().Get("vol1"), dc[0].Name)) assert.Assert(t, dc[0].Labels == nil, fmt.Sprintf("expected labels to be nil and were %v", dc[0].Labels)) }), ), } }, }, { Description: "inspect labels", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "inspect", data.Labels().Get("vol2")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( expect.Contains(data.Labels().Get("vol2")), expect.JSON([]native.Volume{}, func(dc []native.Volume, t tig.T) { labels := *dc[0].Labels assert.Assert(t, len(labels) == 2, fmt.Sprintf("two results, not %d", len(labels))) assert.Assert(t, labels["foo"] == "fooval", fmt.Sprintf("label foo should be fooval, not %s", labels["foo"])) assert.Assert(t, labels["bar"] == "barval", fmt.Sprintf("label bar should be barval, not %s", labels["bar"])) }), ), } }, }, { Description: "inspect size", Require: require.Not(nerdtest.Docker), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "inspect", "--size", data.Labels().Get("vol1")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( expect.Contains(data.Labels().Get("vol1")), expect.JSON([]native.Volume{}, func(dc []native.Volume, t tig.T) { assert.Assert(t, dc[0].Size == size, fmt.Sprintf("expected size to be %d (was %d)", size, dc[0].Size)) }), ), } }, }, { Description: "multi success", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "inspect", data.Labels().Get("vol1"), data.Labels().Get("vol2")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( expect.Contains(data.Labels().Get("vol1"), data.Labels().Get("vol2")), expect.JSON([]native.Volume{}, func(dc []native.Volume, t tig.T) { assert.Assert(t, len(dc) == 2, fmt.Sprintf("two results, not %d", len(dc))) assert.Assert(t, dc[0].Name == data.Labels().Get("vol1"), fmt.Sprintf("expected name to be %q (was %q)", data.Labels().Get("vol1"), dc[0].Name)) assert.Assert(t, dc[1].Name == data.Labels().Get("vol2"), fmt.Sprintf("expected name to be %q (was %q)", data.Labels().Get("vol2"), dc[1].Name)) }), ), } }, }, { Description: "part success multi", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "inspect", "invalid∞", "nonexistent", data.Labels().Get("vol1")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errdefs.ErrNotFound, errdefs.ErrInvalidArgument}, Output: expect.All( expect.Contains(data.Labels().Get("vol1")), expect.JSON([]native.Volume{}, func(dc []native.Volume, t tig.T) { assert.Assert(t, len(dc) == 1, fmt.Sprintf("one result, not %d", len(dc))) assert.Assert(t, dc[0].Name == data.Labels().Get("vol1"), fmt.Sprintf("expected name to be %q (was %q)", data.Labels().Get("vol1"), dc[0].Name)) }), ), } }, }, { Description: "multi failure", Command: test.Command("volume", "inspect", "invalid∞", "nonexistent"), Expected: test.Expects(1, []error{errdefs.ErrNotFound, errdefs.ErrInvalidArgument}, nil), }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/volume/volume_list.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package volume import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/cmd/volume" ) func listCommand() *cobra.Command { cmd := &cobra.Command{ Use: "ls", Aliases: []string{"list"}, Short: "List volumes", RunE: listAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().BoolP("quiet", "q", false, "Only display volume names") // Alias "-f" is reserved for "--filter" cmd.Flags().String("format", "", "Format the output using the given go template") cmd.Flags().BoolP("size", "s", false, "Display the disk usage of volumes. Can be slow with volumes having loads of directories.") cmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"json", "table", "wide"}, cobra.ShellCompDirectiveNoFileComp }) cmd.Flags().StringSliceP("filter", "f", []string{}, "Filter matches volumes based on given conditions") return cmd } func listOptions(cmd *cobra.Command) (types.VolumeListOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.VolumeListOptions{}, err } quiet, err := cmd.Flags().GetBool("quiet") if err != nil { return types.VolumeListOptions{}, err } format, err := cmd.Flags().GetString("format") if err != nil { return types.VolumeListOptions{}, err } size, err := cmd.Flags().GetBool("size") if err != nil { return types.VolumeListOptions{}, err } filters, err := cmd.Flags().GetStringSlice("filter") if err != nil { return types.VolumeListOptions{}, err } return types.VolumeListOptions{ GOptions: globalOptions, Quiet: quiet, Format: format, Size: size, Filters: filters, Stdout: cmd.OutOrStdout(), }, nil } func listAction(cmd *cobra.Command, args []string) error { options, err := listOptions(cmd) if err != nil { return err } return volume.List(options) } ================================================ FILE: cmd/nerdctl/volume/volume_list_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package volume import ( "fmt" "os" "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/tabutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestVolumeLsSize(t *testing.T) { nerdtest.Setup() tc := &test.Case{ Require: require.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("volume", "create", data.Identifier("1")) helpers.Ensure("volume", "create", data.Identifier("2")) helpers.Ensure("volume", "create", data.Identifier("empty")) vol1 := nerdtest.InspectVolume(helpers, data.Identifier("1")) vol2 := nerdtest.InspectVolume(helpers, data.Identifier("2")) err := createFileWithSize(vol1.Mountpoint, 102400) assert.NilError(t, err, "File creation failed") err = createFileWithSize(vol2.Mountpoint, 204800) assert.NilError(t, err, "File creation failed") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("volume", "rm", "-f", data.Identifier("1")) helpers.Anyhow("volume", "rm", "-f", data.Identifier("2")) helpers.Anyhow("volume", "rm", "-f", data.Identifier("empty")) }, Command: test.Command("volume", "ls", "--size"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 4, "expected at least 4 lines") volSizes := map[string]string{ data.Identifier("1"): "100.0 KiB", data.Identifier("2"): "200.0 KiB", data.Identifier("empty"): "0.0 B", } var numMatches = 0 var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE") var err = tab.ParseHeader(lines[0]) assert.NilError(t, err, "ParseHeader should not fail\n") for _, line := range lines { name, _ := tab.ReadRow(line, "VOLUME NAME") size, _ := tab.ReadRow(line, "SIZE") expectSize, ok := volSizes[name] if !ok { continue } assert.Assert(t, size == expectSize, fmt.Sprintf("expected size %s for volume %s, got %s", expectSize, name, size)) numMatches++ } assert.Assert(t, numMatches == len(volSizes), fmt.Sprintf("expected %d volumes, got: %d", len(volSizes), numMatches)) }, } }, } tc.Run(t) } func TestVolumeLsFilter(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = nerdtest.BrokenTest("This test assumes that the host-side of a volume can be written into, "+ "which is not always true. To be replaced by cp into the container.", &test.Requirement{ Check: func(data test.Data, helpers test.Helpers) (bool, string) { isDocker, _ := nerdtest.Docker.Check(data, helpers) return !isDocker || os.Geteuid() == 0, "docker cli needs to be run as root" }, }) testCase.Setup = func(data test.Data, helpers test.Helpers) { var vol1, vol2, vol3, vol4 = data.Identifier("1"), data.Identifier("2"), data.Identifier("3"), data.Identifier("4") var label1, label2, label3, label4 = "mylabel=label-1", "mylabel=label-2", "mylabel=label-3", "mylabel-group=label-4" helpers.Ensure("volume", "create", "--label="+label1, "--label="+label4, vol1) helpers.Ensure("volume", "create", "--label="+label2, "--label="+label4, vol2) helpers.Ensure("volume", "create", "--label="+label3, vol3) helpers.Ensure("volume", "create", vol4) // FIXME // This will not work with Docker rootful and Docker cli run as a user // We should replace it with cp inside the container err := createFileWithSize(nerdtest.InspectVolume(helpers, vol1).Mountpoint, 409600) assert.NilError(t, err, "File creation failed") err = createFileWithSize(nerdtest.InspectVolume(helpers, vol2).Mountpoint, 1024000) assert.NilError(t, err, "File creation failed") err = createFileWithSize(nerdtest.InspectVolume(helpers, vol3).Mountpoint, 409600) assert.NilError(t, err, "File creation failed") err = createFileWithSize(nerdtest.InspectVolume(helpers, vol4).Mountpoint, 1024000) assert.NilError(t, err, "File creation failed") data.Labels().Set("vol1", vol1) data.Labels().Set("vol2", vol2) data.Labels().Set("vol3", vol3) data.Labels().Set("vol4", vol4) data.Labels().Set("mainlabel", "mylabel") data.Labels().Set("label1", label1) data.Labels().Set("label2", label2) data.Labels().Set("label3", label3) data.Labels().Set("label4", label4) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("volume", "rm", "-f", data.Labels().Get("vol1")) helpers.Anyhow("volume", "rm", "-f", data.Labels().Get("vol2")) helpers.Anyhow("volume", "rm", "-f", data.Labels().Get("vol3")) helpers.Anyhow("volume", "rm", "-f", data.Labels().Get("vol4")) } testCase.SubTests = []*test.Case{ { Description: "No filter", Command: test.Command("volume", "ls", "--quiet"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 4, "expected at least 4 lines") volNames := map[string]struct{}{ data.Labels().Get("vol1"): {}, data.Labels().Get("vol2"): {}, data.Labels().Get("vol3"): {}, data.Labels().Get("vol4"): {}, } var numMatches = 0 for _, name := range lines { _, ok := volNames[name] if !ok { continue } numMatches++ } assert.Assert(t, len(volNames) == numMatches, fmt.Sprintf("expected %d volumes, got: %d", len(volNames), numMatches)) }, } }, }, { Description: "Retrieving label=mainlabel", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Labels().Get("mainlabel")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 3, "expected at least 3 lines") volNames := map[string]struct{}{ data.Labels().Get("vol1"): {}, data.Labels().Get("vol2"): {}, data.Labels().Get("vol3"): {}, } for _, name := range lines { _, ok := volNames[name] assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)) } }, } }, }, { Description: "Retrieving label=mainlabel=label2", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Labels().Get("label2")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 1, "expected at least 1 lines") volNames := map[string]struct{}{ data.Labels().Get("vol2"): {}, } for _, name := range lines { _, ok := volNames[name] assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)) } }, } }, }, { Description: "Retrieving label=mainlabel=", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Labels().Get("mainlabel")+"=") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { assert.Assert(t, strings.TrimSpace(stdout) == "", "expected no result") }, } }, }, { Description: "Retrieving label=mainlabel=label1 and label=mainlabel=label2", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Labels().Get("label1"), "--filter", "label="+data.Labels().Get("label2")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { assert.Assert(t, strings.TrimSpace(stdout) == "", "expected no result") }, } }, }, { Description: "Retrieving label=mainlabel and label=grouplabel=label4", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Labels().Get("mainlabel"), "--filter", "label="+data.Labels().Get("label4")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 2, "expected at least 2 lines") volNames := map[string]struct{}{ data.Labels().Get("vol1"): {}, data.Labels().Get("vol2"): {}, } for _, name := range lines { _, ok := volNames[name] assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)) } }, } }, }, { Description: "Retrieving name=volume1", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "ls", "--quiet", "--filter", "name="+data.Labels().Get("vol1")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 1, "expected at least 1 line") volNames := map[string]struct{}{ data.Labels().Get("vol1"): {}, } for _, name := range lines { _, ok := volNames[name] assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)) } }, } }, }, { Description: "Retrieving name=.*volume1.*", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "ls", "--quiet", "--filter", "name=.*"+data.Labels().Get("vol1")+".*") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 1, "expected at least 1 line") volNames := map[string]struct{}{ data.Labels().Get("vol1"): {}, } for _, name := range lines { _, ok := volNames[name] assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)) } }, } }, }, { Description: "Retrieving name=volume1 and name=volume2", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "ls", "--quiet", "--filter", "name="+data.Labels().Get("vol1"), "--filter", "name="+data.Labels().Get("vol2")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 2, "expected at least 2 lines") volNames := map[string]struct{}{ data.Labels().Get("vol1"): {}, data.Labels().Get("vol2"): {}, } for _, name := range lines { _, ok := volNames[name] assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)) } }, } }, }, { Description: "Retrieving size=1024000", Require: require.Not(nerdtest.Docker), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "ls", "--size", "--filter", "size=1024000") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 3, "expected at least 3 lines") volNames := map[string]struct{}{ data.Labels().Get("vol2"): {}, data.Labels().Get("vol4"): {}, } var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE") var err = tab.ParseHeader(lines[0]) assert.NilError(t, err, "Tab reader failed") for _, line := range lines { name, _ := tab.ReadRow(line, "VOLUME NAME") if name == "VOLUME NAME" { continue } _, ok := volNames[name] assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)) } }, } }, }, { Description: "Retrieving size>=1024000 size<=2048000", Require: require.Not(nerdtest.Docker), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "ls", "--size", "--filter", "size>=1024000", "--filter", "size<=2048000") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 3, "expected at least 3 lines") volNames := map[string]struct{}{ data.Labels().Get("vol2"): {}, data.Labels().Get("vol4"): {}, } var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE") var err = tab.ParseHeader(lines[0]) assert.NilError(t, err, "Tab reader failed") for _, line := range lines { name, _ := tab.ReadRow(line, "VOLUME NAME") if name == "VOLUME NAME" { continue } _, ok := volNames[name] assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)) } }, } }, }, { Description: "Retrieving size>204800 size<1024000", Require: require.Not(nerdtest.Docker), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "ls", "--size", "--filter", "size>204800", "--filter", "size<1024000") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 3, "expected at least 3 lines") volNames := map[string]struct{}{ data.Labels().Get("vol1"): {}, data.Labels().Get("vol3"): {}, } var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE") var err = tab.ParseHeader(lines[0]) assert.NilError(t, err, "Tab reader failed") for _, line := range lines { name, _ := tab.ReadRow(line, "VOLUME NAME") if name == "VOLUME NAME" { continue } _, ok := volNames[name] assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)) } }, } }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/volume/volume_namespace_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package volume import ( "testing" "github.com/containerd/errdefs" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestVolumeNamespace(t *testing.T) { testCase := nerdtest.Setup() // Docker does not support namespaces testCase.Require = require.Not(nerdtest.Docker) // Create a volume in a different namespace testCase.Setup = func(data test.Data, helpers test.Helpers) { data.Labels().Set("root_namespace", data.Identifier()) data.Labels().Set("root_volume", data.Identifier()) helpers.Ensure("--namespace", data.Identifier(), "volume", "create", data.Identifier()) } // Cleanup once done testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if data.Labels().Get("root_namespace") != "" { helpers.Anyhow("--namespace", data.Identifier(), "volume", "remove", data.Identifier()) helpers.Anyhow("namespace", "remove", data.Identifier()) } } testCase.SubTests = []*test.Case{ { Description: "inspect another namespace volume should fail", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "inspect", data.Labels().Get("root_volume")) }, Expected: test.Expects(1, []error{ errdefs.ErrNotFound, }, nil), }, { Description: "removing another namespace volume should fail", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "remove", data.Labels().Get("root_volume")) }, Expected: test.Expects(1, []error{ errdefs.ErrNotFound, }, nil), }, { Description: "prune should leave another namespace volume untouched", // Make it private so that we do not interact with other tests in the main namespace Require: nerdtest.Private, Command: test.Command("volume", "prune", "-a", "-f"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( expect.DoesNotContain(data.Labels().Get("root_volume")), func(stdout string, t tig.T) { helpers.Ensure("--namespace", data.Labels().Get("root_namespace"), "volume", "inspect", data.Labels().Get("root_volume")) }, ), } }, }, { Description: "create with the same name should work, then delete it", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "create", data.Labels().Get("root_volume")) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("volume", "rm", data.Labels().Get("root_volume")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { helpers.Ensure("volume", "inspect", data.Labels().Get("root_volume")) helpers.Ensure("volume", "rm", data.Labels().Get("root_volume")) helpers.Ensure("--namespace", data.Labels().Get("root_namespace"), "volume", "inspect", data.Labels().Get("root_volume")) }, } }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/volume/volume_prune.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package volume import ( "fmt" "strings" "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/volume" ) func pruneCommand() *cobra.Command { cmd := &cobra.Command{ Use: "prune [flags]", Short: "Remove all unused local volumes", Args: cobra.NoArgs, RunE: pruneAction, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().BoolP("all", "a", false, "Remove all unused volumes, not just anonymous ones") cmd.Flags().BoolP("force", "f", false, "Do not prompt for confirmation") return cmd } func pruneOptions(cmd *cobra.Command) (types.VolumePruneOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.VolumePruneOptions{}, err } all, err := cmd.Flags().GetBool("all") if err != nil { return types.VolumePruneOptions{}, err } force, err := cmd.Flags().GetBool("force") if err != nil { return types.VolumePruneOptions{}, err } options := types.VolumePruneOptions{ GOptions: globalOptions, All: all, Force: force, Stdout: cmd.OutOrStdout(), } return options, nil } func pruneAction(cmd *cobra.Command, _ []string) error { options, err := pruneOptions(cmd) if err != nil { return err } if !options.Force { var confirm string msg := "This will remove all local volumes not used by at least one container." msg += "\nAre you sure you want to continue? [y/N] " fmt.Fprintf(options.Stdout, "WARNING! %s", msg) fmt.Fscanf(cmd.InOrStdin(), "%s", &confirm) if strings.ToLower(confirm) != "y" { return nil } } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return volume.Prune(ctx, client, options) } ================================================ FILE: cmd/nerdctl/volume/volume_prune_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package volume import ( "strings" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestVolumePrune(t *testing.T) { var setup = func(data test.Data, helpers test.Helpers) { anonIDBusy := strings.TrimSpace(helpers.Capture("volume", "create")) anonIDDangling := strings.TrimSpace(helpers.Capture("volume", "create")) namedBusy := data.Identifier("busy") namedDangling := data.Identifier("free") helpers.Ensure("volume", "create", namedBusy) helpers.Ensure("volume", "create", namedDangling) helpers.Ensure("run", "--name", data.Identifier(), "-v", namedBusy+":/namedbusyvolume", "-v", anonIDBusy+":/anonbusyvolume", testutil.CommonImage) data.Labels().Set("anonIDBusy", anonIDBusy) data.Labels().Set("anonIDDangling", anonIDDangling) data.Labels().Set("namedBusy", namedBusy) data.Labels().Set("namedDangling", namedDangling) } var cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("volume", "rm", "-f", data.Labels().Get("anonIDBusy")) helpers.Anyhow("volume", "rm", "-f", data.Labels().Get("anonIDDangling")) helpers.Anyhow("volume", "rm", "-f", data.Labels().Get("namedBusy")) helpers.Anyhow("volume", "rm", "-f", data.Labels().Get("namedDangling")) } testCase := nerdtest.Setup() // This set must be marked as private, since we cannot prune without interacting with other tests. testCase.Require = nerdtest.Private // Furthermore, these two subtests cannot be run in parallel testCase.SubTests = []*test.Case{ { Description: "prune anonymous only", NoParallel: true, Setup: setup, Cleanup: cleanup, Command: test.Command("volume", "prune", "-f"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( expect.Contains(data.Labels().Get("anonIDDangling")), expect.DoesNotContain( data.Labels().Get("anonIDBusy"), data.Labels().Get("namedBusy"), data.Labels().Get("namedDangling"), ), func(stdout string, t tig.T) { helpers.Ensure("volume", "inspect", data.Labels().Get("anonIDBusy")) helpers.Fail("volume", "inspect", data.Labels().Get("anonIDDangling")) helpers.Ensure("volume", "inspect", data.Labels().Get("namedBusy")) helpers.Ensure("volume", "inspect", data.Labels().Get("namedDangling")) }, ), } }, }, { Description: "prune all", NoParallel: true, Setup: setup, Cleanup: cleanup, Command: test.Command("volume", "prune", "-f", "--all"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( expect.DoesNotContain(data.Labels().Get("anonIDBusy"), data.Labels().Get("namedBusy")), expect.Contains(data.Labels().Get("anonIDDangling"), data.Labels().Get("namedDangling")), func(stdout string, t tig.T) { helpers.Ensure("volume", "inspect", data.Labels().Get("anonIDBusy")) helpers.Fail("volume", "inspect", data.Labels().Get("anonIDDangling")) helpers.Ensure("volume", "inspect", data.Labels().Get("namedBusy")) helpers.Fail("volume", "inspect", data.Labels().Get("namedDangling")) }, ), } }, }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/volume/volume_remove.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package volume import ( "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/volume" ) func removeCommand() *cobra.Command { cmd := &cobra.Command{ Use: "rm [flags] VOLUME [VOLUME...]", Aliases: []string{"remove"}, Short: "Remove one or more volumes", Long: "NOTE: You cannot remove a volume that is in use by a container.", Args: cobra.MinimumNArgs(1), RunE: removeAction, ValidArgsFunction: removeShellComplete, SilenceUsage: true, SilenceErrors: true, } cmd.Flags().BoolP("force", "f", false, "(unimplemented yet)") return cmd } func removeOptions(cmd *cobra.Command) (types.VolumeRemoveOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return types.VolumeRemoveOptions{}, err } force, err := cmd.Flags().GetBool("force") if err != nil { return types.VolumeRemoveOptions{}, err } return types.VolumeRemoveOptions{ GOptions: globalOptions, Force: force, Stdout: cmd.OutOrStdout(), }, nil } func removeAction(cmd *cobra.Command, args []string) error { options, err := removeOptions(cmd) if err != nil { return err } client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err } defer cancel() return volume.Remove(ctx, client, args, options) } func removeShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show volume names return completion.VolumeNames(cmd) } ================================================ FILE: cmd/nerdctl/volume/volume_remove_linux_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package volume import ( "errors" "fmt" "testing" "gotest.tools/v3/assert" "github.com/containerd/errdefs" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) // TestVolumeRemove does test a large variety of volume remove situations, albeit none of them being // hard filesystem errors. // Behavior in such cases is largely unspecified, as there is no easy way to compare with Docker. // Anyhow, borked filesystem conditions is not something we should be expected to deal with in a smart way. func TestVolumeRemove(t *testing.T) { testCase := nerdtest.Setup() testCase.SubTests = []*test.Case{ { Description: "arg missing should fail", Command: test.Command("volume", "rm"), Expected: test.Expects(1, []error{errors.New("requires at least 1 arg")}, nil), }, { Description: "invalid identifier should fail", Command: test.Command("volume", "rm", "∞"), Expected: test.Expects(1, []error{errdefs.ErrInvalidArgument}, nil), }, { Description: "non existent volume should fail", Command: test.Command("volume", "rm", "doesnotexist"), Expected: test.Expects(1, []error{errdefs.ErrNotFound}, nil), }, { Description: "busy volume should fail", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("volume", "create", data.Identifier()) helpers.Ensure("run", "-v", fmt.Sprintf("%s:/volume", data.Identifier()), "--name", data.Identifier(), testutil.CommonImage) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("volume", "rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "rm", data.Identifier()) }, Expected: test.Expects(1, []error{errdefs.ErrFailedPrecondition}, nil), }, { Description: "busy anonymous volume should fail", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-v", "/volume", "--name", data.Identifier(), testutil.CommonImage) // Inspect the container and find the anonymous volume id inspect := nerdtest.InspectContainer(helpers, data.Identifier()) var anonName string for _, v := range inspect.Mounts { if v.Destination == "/volume" { anonName = v.Name break } } assert.Assert(t, anonName != "", "Failed to find anonymous volume id", inspect) data.Labels().Set("anonName", anonName) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("volume", "rm", "-f", data.Labels().Get("anonName")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { // Try to remove that anon volume return helpers.Command("volume", "rm", data.Labels().Get("anonName")) }, Expected: test.Expects(1, []error{errdefs.ErrFailedPrecondition}, nil), }, { Description: "freed volume should succeed", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("volume", "create", data.Identifier()) helpers.Ensure("run", "-v", fmt.Sprintf("%s:/volume", data.Identifier()), "--name", data.Identifier(), testutil.CommonImage) helpers.Ensure("rm", "-f", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("volume", "rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "rm", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.Equals(data.Identifier() + "\n"), } }, }, { Description: "dangling volume should succeed", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("volume", "create", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("volume", "rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "rm", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.Equals(data.Identifier() + "\n"), } }, }, { Description: "part success multi-remove", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("volume", "create", data.Identifier()) helpers.Ensure("volume", "create", data.Identifier("busy")) helpers.Ensure("run", "-v", fmt.Sprintf("%s:/volume", data.Identifier("busy")), "--name", data.Identifier(), testutil.CommonImage) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("volume", "rm", "-f", data.Identifier()) helpers.Anyhow("volume", "rm", "-f", data.Identifier("busy")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "rm", "invalid∞", "nonexistent", data.Identifier("busy"), data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{ errdefs.ErrNotFound, errdefs.ErrFailedPrecondition, errdefs.ErrInvalidArgument, }, Output: expect.Equals(data.Identifier() + "\n"), } }, }, { Description: "success multi-remove", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("volume", "create", data.Identifier("1")) helpers.Ensure("volume", "create", data.Identifier("2")) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("volume", "rm", "-f", data.Identifier("1"), data.Identifier("2")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "rm", data.Identifier("1"), data.Identifier("2")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.Equals(data.Identifier("1") + "\n" + data.Identifier("2") + "\n"), } }, }, { Description: "failing multi-remove", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("volume", "create", data.Identifier("busy")) helpers.Ensure("run", "-v", fmt.Sprintf("%s:/volume", data.Identifier("busy")), "--name", data.Identifier(), testutil.CommonImage) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("volume", "rm", "-f", data.Identifier("busy")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "rm", "invalid∞", "nonexistent", data.Identifier("busy")) }, Expected: test.Expects(1, []error{ errdefs.ErrNotFound, errdefs.ErrFailedPrecondition, errdefs.ErrInvalidArgument, }, nil), }, } testCase.Run(t) } ================================================ FILE: cmd/nerdctl/volume/volume_test.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package volume import ( "testing" "github.com/containerd/nerdctl/v2/pkg/testutil" ) func TestMain(m *testing.M) { testutil.M(m) } ================================================ FILE: docs/build.md ================================================ # Setting up `nerdctl build` with BuildKit `nerdctl build` (and `nerdctl compose build`) relies on [BuildKit](https://github.com/moby/buildkit). To use it, you need to set up BuildKit. BuildKit has 2 types of backends. - **containerd worker**: BuildKit relies on containerd to manage containers and images, etc. containerd needs to be up-and-running on the host. - **OCI worker**: BuildKit manages containers and images, etc. containerd isn't needed. This worker relies on runc for container execution. You need to set up BuildKit with either of the above workers. Note that OCI worker cannot access base images (`FROM` images in Dockerfiles) managed by containerd. Thus you cannot let `nerdctl build` use containerd-managed images as the base image. They include images previously built using `nerdctl build`. For example, the following build `bar` fails with OCI worker because it tries to use the previously built and containerd-managed image `foo`. ```console $ mkdir -p /tmp/ctx && cat < /tmp/ctx/Dockerfile FROM ghcr.io/stargz-containers/ubuntu:20.04-org RUN echo hello EOF $ nerdctl build -t foo /tmp/ctx $ cat < /tmp/ctx/Dockerfile FROM foo RUN echo bar EOF $ nerdctl build -t bar /tmp/ctx ``` This limitation can be avoided using containerd worker as mentioned later. ## Setting up BuildKit with containerd worker ### Rootless | :zap: Requirement | nerdctl >= 0.18, BuildKit >= 0.10 | |-------------------|-----------------------------------| ``` $ CONTAINERD_NAMESPACE=default containerd-rootless-setuptool.sh install-buildkit-containerd ``` `containerd-rootless-setuptool.sh` is aware of `CONTAINERD_NAMESPACE` and `CONTAINERD_SNAPSHOTTER` envvars. It installs buildkitd to the specified containerd namespace. This allows BuildKit using containerd-managed images in that namespace as the base image. Note that BuildKit can't use images in other namespaces as of now. If `CONTAINERD_NAMESPACE` envvar is not specified, this script configures buildkitd to use "buildkit" namespace (not "default" namespace). You can install an additional buildkitd process in a different namespace by executing this script with specifying the namespace with `CONTAINERD_NAMESPACE`. BuildKit will expose the socket at `$XDG_RUNTIME_DIR/buildkit-$CONTAINERD_NAMESPACE/buildkitd.sock` if `CONTAINERD_NAMESPACE` is specified. If `CONTAINERD_NAMESPACE` is not specified, that location will be `$XDG_RUNTIME_DIR/buildkit/buildkitd.sock`. ### Rootful ``` $ sudo systemctl enable --now buildkit ``` Then add the following configuration to `/etc/buildkit/buildkitd.toml` to enable containerd worker. ```toml [worker.oci] enabled = false [worker.containerd] enabled = true # namespace should be "k8s.io" for Kubernetes (including Rancher Desktop) namespace = "default" ``` ## Setting up BuildKit with OCI worker ### Rootless ``` $ containerd-rootless-setuptool.sh install-buildkit ``` As mentioned in the above, BuildKit with this configuration cannot use images managed by containerd. They include images previously built with `nerdctl build`. BuildKit will expose the socket at `$XDG_RUNTIME_DIR/buildkit/buildkitd.sock`. ### rootful ``` $ sudo systemctl enable --now buildkit ``` ## Which BuildKit socket will nerdctl use? You can specify BuildKit address for `nerdctl build` using `--buildkit-host` flag or `BUILDKIT_HOST` envvar. When BuildKit address isn't specified, nerdctl tries some default BuildKit addresses the following order and uses the first available one. - `/buildkit-/buildkitd.sock` - `/buildkit-default/buildkitd.sock` - `/buildkit/buildkitd.sock` For example, if you run rootless nerdctl with `test` containerd namespace, it tries to use `$XDG_RUNTIME_DIR/buildkit-test/buildkitd.sock` by default then try to fall back to `$XDG_RUNTIME_DIR/buildkit-default/buildkitd.sock` and `$XDG_RUNTIME_DIR/buildkit/buildkitd.sock` ================================================ FILE: docs/builder-debug.md ================================================ # Interactive debugging of Dockerfile (Experimental) nerdctl supports interactive debugging of Dockerfile as `nerdctl builder debug`. ``` $ nerdctl builder debug /path/to/context ``` This feature leverages [buildg](https://github.com/ktock/buildg), interactive debugger of Dockerfile. For command reference, please refer to the [Command reference doc in buildg repo](https://github.com/ktock/buildg#command-reference). :warning: This command currently doesn't use the host's `buildkitd` daemon but uses the patched version of BuildKit provided by buildg. This should be fixed to use the host's `buildkitd` in the future. ## Example Example Dockerfile: ```Dockerfile FROM busybox AS build1 RUN echo a > /a RUN echo b > /b RUN echo c > /c ``` Example debugging: ```console $ nerdctl builder debug --image=ubuntu:22.04 /tmp/ctx/ WARN[2022-05-17T10:15:48Z] using host network as the default#1 [internal] load .dockerignore #1 transferring context: 2B done #1 DONE 0.1s #2 [internal] load build definition from Dockerfile #2 transferring dockerfile: 108B done #2 DONE 0.1s #3 [internal] load metadata for docker.io/library/busybox:latest INFO[2022-05-17T10:15:51Z] debug session started. type "help" for command reference. Filename: "Dockerfile" => 1| FROM busybox AS build1 2| RUN echo a > /a 3| RUN echo b > /b 4| RUN echo c > /c (buildg) break 3 (buildg) breakpoints [0]: line: Dockerfile:3 [on-fail]: breaks on fail (buildg) continue #3 DONE 3.1s #4 [1/4] FROM docker.io/library/busybox@sha256:d2b53584f580310186df7a2055ce3ff83cc0df6caacf1e3489bff8cf5d0af5d8 #4 resolve docker.io/library/busybox@sha256:d2b53584f580310186df7a2055ce3ff83cc0df6caacf1e3489bff8cf5d0af5d8 0.0s done #4 sha256:50e8d59317eb665383b2ef4d9434aeaa394dcd6f54b96bb7810fdde583e9c2d1 0B / 772.81kB 0.2s #4 sha256:50e8d59317eb665383b2ef4d9434aeaa394dcd6f54b96bb7810fdde583e9c2d1 0B / 772.81kB 5.3s #4 sha256:50e8d59317eb665383b2ef4d9434aeaa394dcd6f54b96bb7810fdde583e9c2d1 0B / 772.81kB 10.4s #4 sha256:50e8d59317eb665383b2ef4d9434aeaa394dcd6f54b96bb7810fdde583e9c2d1 772.81kB / 772.81kB 11.4s done #4 extracting sha256:50e8d59317eb665383b2ef4d9434aeaa394dcd6f54b96bb7810fdde583e9c2d1 0.1s done #4 DONE 20.2s #5 [2/4] RUN echo a > /a #5 DONE 0.1s Breakpoint[0]: reached line: Dockerfile:3 Filename: "Dockerfile" 1| FROM busybox AS build1 2| RUN echo a > /a *=> 3| RUN echo b > /b 4| RUN echo c > /c (buildg) exec --image sh # ls /debugroot/ a b bin dev etc home proc root tmp usr var # cat /debugroot/a /debugroot/b a b # (buildg) quit ``` ================================================ FILE: docs/cni.md ================================================ # Using CNI with nerdctl nerdctl uses CNI plugins for its container network, you can set network by either `--network` or `--net` option. ## Basic networks nerdctl support some basic types of CNI plugins without any configuration needed(you should have CNI plugin be installed), for Linux systems the basic CNI plugin types are `bridge`, `portmap`, `firewall`, `tuning`, for Windows system, the supported CNI plugin types are `nat` only. The default network `bridge` for Linux and `nat` for Windows if you don't set any network options. Configuration of the default network `bridge` of Linux: ```json { "cniVersion": "1.0.0", "name": "bridge", "plugins": [ { "type": "bridge", "bridge": "nerdctl0", "isGateway": true, "ipMasq": true, "hairpinMode": true, "ipam": { "type": "host-local", "routes": [{ "dst": "0.0.0.0/0" }], "ranges": [ [ { "subnet": "10.4.0.0/24", "gateway": "10.4.0.1" } ] ] } }, { "type": "portmap", "capabilities": { "portMappings": true } }, { "type": "firewall", "ingressPolicy": "same-bridge" }, { "type": "tuning" } ] } ``` ## Bridge isolation nerdctl >= 0.18 sets the `ingressPolicy` to `same-bridge` when `firewall` plugin >= 1.1.0 is installed. This `ingressPolicy` replaces the CNI `isolation` plugin used in nerdctl <= 0.17. When `firewall` plugin >= 1.1.0 is not found, nerdctl does not enable the bridge isolation. This means a container in `--net=foo` can connect to a container in `--net=bar`. ## macvlan/IPvlan networks nerdctl also support macvlan and IPvlan network driver. To create a `macvlan` network which bridges with a given physical network interface, use `--driver macvlan` with `nerdctl network create` command. ``` # nerdctl network create mac0 --driver macvlan \ --subnet=192.168.5.0/24 --gateway=192.168.5.2 -o parent=eth0 ``` You can specify the `parent`, which is the interface the traffic will physically go through on the host, defaults to default route interface. And the `subnet` should be under the same network as the network interface, an easier way is to use DHCP to assign the IP: ``` # nerdctl network create mac0 --driver macvlan --ipam-driver=dhcp ``` Using `--driver ipvlan` can create `ipvlan` network, the default mode for IPvlan is `l2`. ## DHCP host-name and other DHCP options Nerdctl automatically sets the DHCP host-name option to the hostname value of the container. Furthermore, on network creation, nerdctl supports the ability to set other DHCP options through `--ipam-options`. Currently, the following options are supported by the DHCP plugin: ``` dhcp-client-identifier subnet-mask routers user-class vendor-class-identifier ``` For example: ``` # nerdctl network create --driver macvlan \ --ipam-driver dhcp \ --ipam-opt 'vendor-class-identifier={"type": "provide", "value": "Hey! Its me!"}' \ my-dhcp-net ``` ## Custom networks You can also customize your CNI network by providing configuration files. When rootful, the expected root location is `/etc/cni/net.d`. For rootless, the expected root location is `~/.config/cni/net.d/` Configuration files (like `10-mynet.conf`) can be placed either in the root location, or under a subfolder. If in the root location, this network will be available to all nerdctl namespaces. If placed in a subfolder, it will be available only to the identically named namespace. For example, you have one configuration file(`/etc/cni/net.d/10-mynet.conf`) for `bridge` network: ```json { "cniVersion": "1.0.0", "name": "mynet", "type": "bridge", "bridge": "cni0", "isGateway": true, "ipMasq": true, "ipam": { "type": "host-local", "subnet": "172.19.0.0/24", "routes": [ { "dst": "0.0.0.0/0" } ] } } ``` This will configure a new CNI network with the name `mynet`, and you can use this network to create a container in any namespace: ```console # nerdctl run -it --net mynet --rm alpine ip addr show 1: lo: mtu 65536 qdisc noqueue state UNKNOWN qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 3: eth0@if6120: mtu 1500 qdisc noqueue state UP link/ether 5e:5b:3f:0c:36:56 brd ff:ff:ff:ff:ff:ff inet 172.19.0.51/24 brd 172.19.0.255 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::5c5b:3fff:fe0c:3656/64 scope link tentative valid_lft forever preferred_lft forever ``` ================================================ FILE: docs/command-reference.md ================================================ # Command reference :whale: = Docker compatible :nerd_face: = nerdctl specific > [!NOTE] > - Unlisted `docker` CLI flags are unimplemented yet in `nerdctl` CLI. > It does not necessarily mean that the corresponding features are missing in containerd. > - Some commands and flags are only available on Linux. - [Container management](#container-management) - [:whale: nerdctl run](#whale-nerdctl-run) - [:whale: nerdctl exec](#whale-nerdctl-exec) - [:whale: nerdctl create](#whale-nerdctl-create) - [:whale: nerdctl cp](#whale-nerdctl-cp) - [:whale: nerdctl ps](#whale-nerdctl-ps) - [:whale: nerdctl inspect](#whale-nerdctl-inspect) - [:whale: nerdctl logs](#whale-nerdctl-logs) - [:whale: nerdctl port](#whale-nerdctl-port) - [:whale: nerdctl rm](#whale-nerdctl-rm) - [:whale: nerdctl stop](#whale-nerdctl-stop) - [:whale: nerdctl start](#whale-nerdctl-start) - [:whale: nerdctl restart](#whale-nerdctl-restart) - [:whale: nerdctl update](#whale-nerdctl-update) - [:whale: nerdctl wait](#whale-nerdctl-wait) - [:whale: nerdctl kill](#whale-nerdctl-kill) - [:whale: nerdctl pause](#whale-nerdctl-pause) - [:whale: nerdctl unpause](#whale-nerdctl-unpause) - [:whale: nerdctl rename](#whale-nerdctl-rename) - [:whale: nerdctl attach](#whale-nerdctl-attach) - [:whale: nerdctl container prune](#whale-nerdctl-container-prune) - [:whale: nerdctl diff](#whale-nerdctl-diff) - [:whale: nerdctl export](#whale-nerdctl-export) - [Build](#build) - [:whale: nerdctl build](#whale-nerdctl-build) - [:whale: nerdctl commit](#whale-nerdctl-commit) - [Image management](#image-management) - [:whale: nerdctl images](#whale-nerdctl-images) - [:whale: nerdctl pull](#whale-nerdctl-pull) - [:whale: nerdctl push](#whale-nerdctl-push) - [:whale: nerdctl load](#whale-nerdctl-load) - [:whale: nerdctl save](#whale-nerdctl-save) - [:whale: nerdctl import](#whale-nerdctl-import) - [:whale: nerdctl tag](#whale-nerdctl-tag) - [:whale: nerdctl rmi](#whale-nerdctl-rmi) - [:whale: nerdctl image inspect](#whale-nerdctl-image-inspect) - [:whale: nerdctl image history](#whale-nerdctl-image-history) - [:whale: nerdctl image prune](#whale-nerdctl-image-prune) - [:nerd_face: nerdctl image convert](#nerd_face-nerdctl-image-convert) - [:nerd_face: nerdctl image encrypt](#nerd_face-nerdctl-image-encrypt) - [:nerd_face: nerdctl image decrypt](#nerd_face-nerdctl-image-decrypt) - [Checkpoint management](#checkpoint-management) - [:whale: nerdctl checkpoint create](#whale-nerdctl-checkpoint-create) - [:whale: nerdctl checkpoint list](#whale-nerdctl-checkpoint-list) - [:whale: nerdctl checkpoint remove](#whale-nerdctl-checkpoint-remove) - [Manifest management](#manifest-management) - [:whale: nerdctl manifest annotate](#whale-nerdctl-manifest-annotate) - [:whale: nerdctl manifest create](#whale-nerdctl-manifest-create) - [:whale: nerdctl manifest inspect](#whale-nerdctl-manifest-inspect) - [:whale: nerdctl manifest push](#whale-nerdctl-manifest-push) - [:whale: nerdctl manifest rm](#whale-nerdctl-manifest-rm) - [Registry](#registry) - [:whale: nerdctl login](#whale-nerdctl-login) - [:whale: nerdctl logout](#whale-nerdctl-logout) - [:whale: nerdctl search](#whale-nerdctl-search) - [Network management](#network-management) - [:whale: nerdctl network create](#whale-nerdctl-network-create) - [:whale: nerdctl network ls](#whale-nerdctl-network-ls) - [:whale: nerdctl network inspect](#whale-nerdctl-network-inspect) - [:whale: nerdctl network rm](#whale-nerdctl-network-rm) - [:whale: nerdctl network prune](#whale-nerdctl-network-prune) - [Volume management](#volume-management) - [:whale: nerdctl volume create](#whale-nerdctl-volume-create) - [:whale: nerdctl volume ls](#whale-nerdctl-volume-ls) - [:whale: nerdctl volume inspect](#whale-nerdctl-volume-inspect) - [:whale: nerdctl volume rm](#whale-nerdctl-volume-rm) - [:whale: nerdctl volume prune](#whale-nerdctl-volume-prune) - [Namespace management](#namespace-management) - [:nerd_face: nerdctl namespace create](#nerd_face-nerdctl-namespace-create) - [:nerd_face: nerdctl namespace inspect](#nerd_face-nerdctl-namespace-inspect) - [:nerd_face: nerdctl namespace ls](#nerd_face-nerdctl-namespace-ls) - [:nerd_face: nerdctl namespace remove](#nerd_face-nerdctl-namespace-remove) - [:nerd_face: nerdctl namespace update](#nerd_face-nerdctl-namespace-update) - [AppArmor profile management](#apparmor-profile-management) - [:nerd_face: nerdctl apparmor inspect](#nerd_face-nerdctl-apparmor-inspect) - [:nerd_face: nerdctl apparmor load](#nerd_face-nerdctl-apparmor-load) - [:nerd_face: nerdctl apparmor ls](#nerd_face-nerdctl-apparmor-ls) - [:nerd_face: nerdctl apparmor unload](#nerd_face-nerdctl-apparmor-unload) - [Builder management](#builder-management) - [:whale: nerdctl builder prune](#whale-nerdctl-builder-prune) - [:nerd_face: nerdctl builder debug](#nerd_face-nerdctl-builder-debug) - [System](#system) - [:whale: nerdctl events](#whale-nerdctl-events) - [:whale: nerdctl info](#whale-nerdctl-info) - [:whale: nerdctl version](#whale-nerdctl-version) - [:whale: nerdctl system prune](#whale-nerdctl-system-prune) - [Stats](#stats) - [:whale: nerdctl stats](#whale-nerdctl-stats) - [:whale: nerdctl top](#whale-nerdctl-top) - [Shell completion](#shell-completion) - [:nerd_face: nerdctl completion bash](#nerd_face-nerdctl-completion-bash) - [:nerd_face: nerdctl completion zsh](#nerd_face-nerdctl-completion-zsh) - [:nerd_face: nerdctl completion fish](#nerd_face-nerdctl-completion-fish) - [:nerd_face: nerdctl completion powershell](#nerd_face-nerdctl-completion-powershell) - [Compose](#compose) - [:whale: nerdctl compose](#whale-nerdctl-compose) - [:whale: nerdctl compose up](#whale-nerdctl-compose-up) - [:whale: nerdctl compose logs](#whale-nerdctl-compose-logs) - [:whale: nerdctl compose build](#whale-nerdctl-compose-build) - [:whale: nerdctl compose create](#whale-nerdctl-compose-create) - [:whale: nerdctl compose exec](#whale-nerdctl-compose-exec) - [:whale: nerdctl compose down](#whale-nerdctl-compose-down) - [:whale: nerdctl compose images](#whale-nerdctl-compose-images) - [:whale: nerdctl compose start](#whale-nerdctl-compose-start) - [:whale: nerdctl compose stop](#whale-nerdctl-compose-stop) - [:whale: nerdctl compose port](#whale-nerdctl-compose-port) - [:whale: nerdctl compose ps](#whale-nerdctl-compose-ps) - [:whale: nerdctl compose pull](#whale-nerdctl-compose-pull) - [:whale: nerdctl compose push](#whale-nerdctl-compose-push) - [:whale: nerdctl compose pause](#whale-nerdctl-compose-pause) - [:whale: nerdctl compose unpause](#whale-nerdctl-compose-unpause) - [:whale: nerdctl compose config](#whale-nerdctl-compose-config) - [:whale: nerdctl compose cp](#whale-nerdctl-compose-cp) - [:whale: nerdctl compose kill](#whale-nerdctl-compose-kill) - [:whale: nerdctl compose restart](#whale-nerdctl-compose-restart) - [:whale: nerdctl compose rm](#whale-nerdctl-compose-rm) - [:whale: nerdctl compose run](#whale-nerdctl-compose-run) - [:whale: nerdctl compose top](#whale-nerdctl-compose-top) - [:whale: nerdctl compose version](#whale-nerdctl-compose-version) - [IPFS management](#ipfs-management) - [:nerd_face: nerdctl ipfs registry serve](#nerd_face-nerdctl-ipfs-registry-serve) - [Global flags](#global-flags) - [Unimplemented Docker commands](#unimplemented-docker-commands) ## Container management ### :whale: nerdctl run Run a command in a new container. Usage: `nerdctl run [OPTIONS] IMAGE [COMMAND] [ARG...]` :nerd_face: `ipfs://` prefix can be used for `IMAGE` to pull it from IPFS. See [`ipfs.md`](./ipfs.md) for details. :nerd_face: `oci-archive://` prefix can be used for `IMAGE` to specify a local file system path to an OCI formatted tarball. Basic flags: - :whale: `-a, --attach`: Attach STDIN, STDOUT, or STDERR - :whale: `-i, --interactive`: Keep STDIN open even if not attached" - :whale: `-t, --tty`: Allocate a pseudo-TTY - :warning: WIP: currently `-t` conflicts with `-d` - :whale: `-sig-proxy`: Proxy received signals to the process (default true) - :whale: `-d, --detach`: Run container in background and print container ID - :whale: `--restart=(no|always|on-failure|unless-stopped)`: Restart policy to apply when a container exits - Default: "no" - always: Always restart the container if it stops. - on-failure[:max-retries]: Restart only if the container exits with a non-zero exit status. Optionally, limit the number of times attempts to restart the container using the :max-retries option. - unless-stopped: Always restart the container unless it is stopped. - :whale: `--rm`: Automatically remove the container when it exits - :whale: `--pull=(always|missing|never)`: Pull image before running - Default: "missing" - :whale: `-q, --quiet`: Suppress the pull output - :whale: `--pid=(host|container:)`: PID namespace to use - :whale: `--uts=(host)` : UTS namespace to use - :whale: `--stop-signal`: Signal to stop a container (default "SIGTERM") - :whale: `--stop-timeout`: Timeout (in seconds) to stop a container - :whale: `--detach-keys`: Override the default detach keys Platform flags: - :whale: `--platform=(amd64|arm64|...)`: Set platform Init process flags: - :whale: `--init`: Run an init inside the container that forwards signals and reaps processes. - :nerd_face: `--init-binary=`: The custom init binary to use. We suggest you use the [tini](https://github.com/krallin/tini) binary which is used in Docker project to get the same behavior. Please make sure the binary exists in your `PATH`. - Default: `tini` Isolation flags: - :whale: :nerd_face: `--isolation=(default|process|host|hyperv)`: Used on Windows to change process isolation level. `default` will use the runtime options configured in `default_runtime` in the [containerd configuration](https://github.com/containerd/containerd/blob/master/docs/cri/config.md#cri-plugin-config-guide) which is `process` in containerd by default. `process` runs process isolated containers. `host` runs [Host Process containers](https://kubernetes.io/docs/tasks/configure-pod-container/create-hostprocess-pod/). Host process containers inherit permissions from containerd process unless `--user` is specified then will start with user specified and the user specified must be present on the host. `host` requires Containerd 1.7+. `hyperv` runs Hyper-V hypervisor partition-based isolated containers. Not implemented for Linux. Network flags: - :whale: `--net, --network=(bridge|host|none|container:|ns:|)`: Connect a container to a network. - Default: "bridge" - `container:`: reuse another container's network stack, container has to be precreated. - :nerd_face: `ns:`: run inside an existing network namespace - :nerd_face: Unlike Docker, this flag can be specified multiple times (`--net foo --net bar`) - :whale: `-p, --publish`: Publish a container's port(s) to the host - :whale: `--dns`: Set custom DNS servers - :whale: `--dns-search`: Set custom DNS search domains - :whale: `--dns-opt, --dns-option`: Set DNS options - :whale: `-h, --hostname`: Container host name - :whale: `--domainname`: Container domain name - :whale: `--add-host`: Add a custom host-to-IP mapping (host:ip). `ip` could be a special string `host-gateway`, - which will be resolved to the `host-gateway-ip` in nerdctl.toml or global flag. - :whale: `--ip`: Specific static IP address(es) to use. Note that unlike docker, nerdctl allows specifying it with the default bridge network. - :whale: `--ip6`: Specific static IP6 address(es) to use. Should be used with user networks - :whale: `--mac-address`: Specific MAC address to use. Be aware that it does not check if manually specified MAC addresses are unique. Supports network type `bridge` and `macvlan` Resource flags: - :whale: `--cpus`: Number of CPUs - :whale: `--cpu-quota`: Limit the CPU CFS (Completely Fair Scheduler) quota - :whale: `--cpu-period`: Limit the CPU CFS (Completely Fair Scheduler) period - :whale: `--cpu-shares`: CPU shares (relative weight) - :whale: `--cpuset-cpus`: CPUs in which to allow execution (0-3, 0,1) - :whale: `--cpuset-mems`: Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems - :whale: `--cpu-rt-period`: Limit CPU real-time period in microseconds. Only supported with cgroup v1. - :whale: `--cpu-rt-runtime`: Limit CPU real-time runtime in microseconds. Only supported with cgroup v1. - :whale: `--memory`: Memory limit - :whale: `--memory-reservation`: Memory soft limit - :whale: `--memory-swap`: Swap limit equal to memory plus swap: '-1' to enable unlimited swap - :whale: `--memory-swappiness`: Tune container memory swappiness (0 to 100) (default -1) - :whale: `--kernel-memory`: Kernel memory limit (deprecated) - :whale: `--oom-kill-disable`: Disable OOM Killer - :whale: `--oom-score-adj`: Tune container’s OOM preferences (-1000 to 1000, rootless: 100 to 1000) - :whale: `--pids-limit`: Tune container pids limit - :nerd_face: `--cgroup-conf`: Configure cgroup v2 (key=value) - :whale: `--blkio-weight`: Block IO (relative weight), between 10 and 1000, or 0 to disable (default 0) - :whale: `--blkio-weight-device`: Block IO weight (relative device weight) - :whale: `--device-read-bps`: Limit read rate (bytes per second) from a device - :whale: `--device-read-iops`: Limit read rate (IO per second) from a device - :whale: `--device-write-bps`: Limit write rate (bytes per second) to a device - :whale: `--device-write-iops`: Limit write rate (IO per second) to a device - :whale: `--cgroupns=(host|private)`: Cgroup namespace to use - Default: "private" on cgroup v2 hosts, "host" on cgroup v1 hosts - :whale: `--cgroup-parent`: Optional parent cgroup for the container - :whale: `--device`: Add a host device to the container Intel RDT flags: - :nerd_face: `--rdt-class=CLASS`: Name of the RDT class (or CLOS) to associate the container wit User flags: - :whale: `-u, --user`: Username or UID (format: [:]) - :nerd_face: `--umask`: Set the umask inside the container. Defaults to 0022. Corresponds to Podman CLI. - :whale: `--group-add`: Add additional groups to join - :whale: `--userns`: Set it to `host` to disable user namespacing set in nerdctl.toml or in cli. Security flags: - :whale: `--security-opt seccomp=`: specify custom seccomp profile - :whale: `--security-opt apparmor=`: specify custom AppArmor profile - :whale: `--security-opt no-new-privileges`: disallow privilege escalation, e.g., setuid and file capabilities - :whale: `--security-opt systempaths=unconfined`: Turn off confinement for system paths (masked paths, read-only paths) for the container - :whale: `--security-opt writable-cgroups`: making the cgroups writeable - :nerd_face: `--security-opt privileged-without-host-devices`: Don't pass host devices to privileged containers - :whale: `--cap-add=`: Add Linux capabilities - :whale: `--cap-drop=`: Drop Linux capabilities - :whale: `--privileged`: Give extended privileges to this container - :nerd_face: `--systemd=(true|false|always)`: Enable systemd compatibility (default: false). - Default: "false" - true: Enable systemd compatibility is enabled if the entrypoint executable matches one of the following paths: - `/sbin/init` - `/usr/sbin/init` - `/usr/local/sbin/init` - always: Always enable systemd compatibility Corresponds to Podman CLI. Runtime flags: - :whale: `--runtime`: Runtime to use for this container, e.g. \"crun\", or \"io.containerd.runsc.v1\". - :whale: `--sysctl`: Sysctl options, e.g \"net.ipv4.ip_forward=1\" Volume flags: - :whale: `-v, --volume :[:]`: Bind mount a volume, e.g., `-v /mnt:/mnt:rro,rprivate` - :whale: option `rw` : Read/Write (when writable) - :whale: option `ro` : Non-recursive read-only - :nerd_face: option `rro`: Recursive read-only. Should be used in conjunction with `rprivate`. e.g., `-v /mnt:/mnt:rro,rprivate` makes children such as `/mnt/usb` to be read-only, too. Requires kernel >= 5.12, and crun >= 1.4 or runc >= 1.1 (PR [#3272](https://github.com/opencontainers/runc/pull/3272)). With older runc, `rro` just works as `ro`. - :whale: option `shared`, `slave`, `private`: Non-recursive "shared" / "slave" / "private" propagation - :whale: option `rshared`, `rslave`, `rprivate`: Recursive "shared" / "slave" / "private" propagation - :nerd_face: option `bind`: Not-recursively bind-mounted - :nerd_face: option `rbind`: Recursively bind-mounted - unimplemented options: `:z` and `:Z` (SELinux relabeling) - :whale: `--tmpfs`: Mount a tmpfs directory, e.g. `--tmpfs /tmp:size=64m,exec`. - :whale: `--mount`: Attach a filesystem mount to the container. Consists of multiple key-value pairs, separated by commas and each consisting of a `=` tuple. e.g., `-- mount type=bind,source=/src,target=/app,bind-propagation=shared`. - :whale: `type`: Current supported mount types are `bind`, `volume`, `tmpfs`. The default type will be set to `volume` if not specified. i.e., `--mount src=vol-1,dst=/app,readonly` equals `--mount type=volume,src=vol-1,dst=/app,readonly` - unimplemented type: `image` - Common Options: - :whale: `src`, `source`: Mount source spec for bind and volume. Mandatory for bind. - :whale: `dst`, `destination`, `target`: Mount destination spec. - :whale: `readonly`, `ro`, `rw`, `rro`: Filesystem permissions. - Options specific to `bind`: - :whale: `bind-propagation`: `shared`, `slave`, `private`, `rshared`, `rslave`, or `rprivate`(default). - :whale: `bind-nonrecursive`: `true` or `false`(default). If set to true, submounts are not recursively bind-mounted. This option is useful for readonly bind mount. - unimplemented options: `consistency` - Options specific to `tmpfs`: - :whale: `tmpfs-size`: Size of the tmpfs mount in bytes. Unlimited by default. - :whale: `tmpfs-mode`: File mode of the tmpfs in **octal**. Defaults to `1777` or world-writable. - Options specific to `volume`: - unimplemented options: `volume-nocopy`, `volume-label`, `volume-driver`, `volume-opt` - :whale: `--volumes-from`: Mount volumes from the specified container(s), e.g. "--volumes-from my-container". Rootfs flags: - :whale: `--read-only`: Mount the container's root filesystem as read only - :nerd_face: `--rootfs`: The first argument is not an image but the rootfs to the exploded container. Corresponds to Podman CLI. Env flags: - :whale: `--entrypoint`: Overwrite the default ENTRYPOINT of the image - :whale: `-w, --workdir`: Working directory inside the container - :whale: `-e, --env`: Set environment variables - :whale: `--env-file`: Set environment variables from file Metadata flags: - :whale: `--name`: Assign a name to the container - :whale: `-l, --label`: Set meta data on a container (Not passed through the OCI runtime since nerdctl v2.0, with an exception for `nerdctl/bypass4netns`) - :whale: `--label-file`: Read in a line delimited file of labels - :whale: `--annotation`: Add an annotation to the container (passed through to the OCI runtime) - :whale: `--cidfile`: Write the container ID to the file - :nerd_face: `--pidfile`: file path to write the task's pid. The CLI syntax conforms to Podman convention. Health check flags: - :whale: `--health-cmd`: Command to run to check container health - :whale: `--health-interval`: Time between running the check (e.g., 30s, 1m) - :whale: `--health-timeout`: Time to wait before considering the check failed (e.g., 5s) - :whale: `--health-retries`: Number of failures before container is considered unhealthy - :whale: `--health-start-period`: Start period for the container to initialize before starting health-retries countdown - :whale: `--health-start-interval`: Interval between checks during the start period - :whale: `--no-healthcheck`: Disable any health checks defined by image or CLI Logging flags: - :whale: `--log-driver=(json-file|journald|fluentd|syslog|none)`: Logging driver for the container (default `json-file`). - :whale: `--log-driver=json-file`: The logs are formatted as JSON. The default logging driver for nerdctl. - The `json-file` logging driver supports the following logging options: - :whale: `--log-opt=max-size=`: The maximum size of the log before it is rolled. A positive integer plus a modifier representing the unit of measure (k, m, or g). Defaults to unlimited. - :whale: `--log-opt=max-file=`: The maximum number of log files that can be present. If rolling the logs creates excess files, the oldest file is removed. Only effective when `max-size` is also set. A positive integer. Defaults to 1. - :nerd_face: `--log-opt=log-path=`: The log path where the logs are written. The path will be created if it does not exist. If the log file exists, the old file will be renamed to `.1`. - Default: `////-json.log` - Example: `/var/lib/nerdctl/1935db59/containers/default//-json.log` - :whale: `--log-opt labels=production_status,geo`: A comma-separated list of logging-related labels this daemon accepts. - :whale: `--log-opt env=os,customer`: A comma-separated list of logging-related environment variables this daemon accepts. - :whale: `--log-driver=journald`: Writes log messages to `journald`. The `journald` daemon must be running on the host machine. - :whale: `--log-opt=tag=