Repository: containers/buildah Branch: main Commit: 85f2cdc00ae4 Files: 4859 Total size: 44.3 MB Directory structure: gitextract_9yro7slu/ ├── .cirrus.yml ├── .codespellrc ├── .fmf/ │ └── version ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── buildah_bug_report.yaml │ │ ├── config.yaml │ │ └── podman_build_bug_report.yaml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── release.yml │ ├── renovate.json5 │ └── workflows/ │ ├── check_cirrus_cron.yml │ ├── issue_pr_lock.yml │ ├── pr.yml │ └── stale.yml ├── .gitignore ├── .golangci.yml ├── .packit.yaml ├── CHANGELOG.md ├── CODE-OF-CONDUCT.md ├── CONTRIBUTING.md ├── GOVERNANCE.md ├── LICENSE ├── MAINTAINERS.md ├── Makefile ├── OWNERS ├── README.md ├── ROADMAP.md ├── SECURITY.md ├── add.go ├── bind/ │ ├── mount.go │ ├── mount_unsupported.go │ └── util.go ├── btrfs_installed_tag.sh ├── buildah.go ├── buildah_test.go ├── changelog.txt ├── chroot/ │ ├── run_common.go │ ├── run_freebsd.go │ ├── run_linux.go │ ├── run_linux_test.go │ ├── run_test.go │ ├── seccomp.go │ ├── seccomp_freebsd.go │ ├── seccomp_test.go │ ├── seccomp_unsupported.go │ ├── seccomp_unsupported_test.go │ ├── selinux.go │ ├── selinux_unsupported.go │ └── unsupported.go ├── cmd/ │ └── buildah/ │ ├── addcopy.go │ ├── build.go │ ├── commit.go │ ├── common.go │ ├── common_test.go │ ├── config.go │ ├── containers.go │ ├── containers_test.go │ ├── dumpbolt.go │ ├── from.go │ ├── images.go │ ├── images_test.go │ ├── info.go │ ├── inspect.go │ ├── login.go │ ├── logout.go │ ├── main.go │ ├── manifest.go │ ├── mkcw.go │ ├── mount.go │ ├── prune.go │ ├── pull.go │ ├── push.go │ ├── rename.go │ ├── rm.go │ ├── rmi.go │ ├── rpc.go │ ├── run.go │ ├── source.go │ ├── tag.go │ ├── umount.go │ ├── unshare.go │ ├── unshare_unsupported.go │ └── version.go ├── commit.go ├── commit_test.go ├── common.go ├── common_test.go ├── config.go ├── contrib/ │ ├── buildahimage/ │ │ └── README.md │ ├── cirrus/ │ │ ├── build.sh │ │ ├── lib.sh │ │ ├── logcollector.sh │ │ ├── setup.sh │ │ ├── test.sh │ │ └── timestamp.awk │ └── completions/ │ └── bash/ │ └── buildah ├── convertcw.go ├── convertcw_test.go ├── copier/ │ ├── copier.go │ ├── copier_linux_test.go │ ├── copier_test.go │ ├── copier_unix_test.go │ ├── copier_windows_test.go │ ├── hardlink_not_uint64.go │ ├── hardlink_uint64.go │ ├── hardlink_unix.go │ ├── hardlink_windows.go │ ├── mknod_int.go │ ├── mknod_uint64.go │ ├── syscall_unix.go │ ├── syscall_windows.go │ ├── xattrs.go │ ├── xattrs_test.go │ └── xattrs_unsupported.go ├── define/ │ ├── build.go │ ├── build_test.go │ ├── isolation.go │ ├── mount_freebsd.go │ ├── mount_linux.go │ ├── mount_unsupported.go │ ├── namespace.go │ ├── pull.go │ ├── pull_test.go │ ├── types.go │ ├── types_test.go │ ├── types_unix.go │ └── types_unsupported.go ├── delete.go ├── demos/ │ ├── README.md │ ├── buildah-bud-demo.sh │ ├── buildah-scratch-demo.sh │ ├── buildah_multi_stage.sh │ └── docker-compatibility-demo.sh ├── developmentplan.md ├── digester.go ├── digester_test.go ├── docker/ │ ├── AUTHORS │ └── types.go ├── docs/ │ ├── Makefile │ ├── buildah-add.1.md │ ├── buildah-build.1.md │ ├── buildah-commit.1.md │ ├── buildah-config.1.md │ ├── buildah-containers.1.md │ ├── buildah-copy.1.md │ ├── buildah-from.1.md │ ├── buildah-images.1.md │ ├── buildah-info.1.md │ ├── buildah-inspect.1.md │ ├── buildah-login.1.md │ ├── buildah-logout.1.md │ ├── buildah-manifest-add.1.md │ ├── buildah-manifest-annotate.1.md │ ├── buildah-manifest-create.1.md │ ├── buildah-manifest-exists.1.md │ ├── buildah-manifest-inspect.1.md │ ├── buildah-manifest-push.1.md │ ├── buildah-manifest-remove.1.md │ ├── buildah-manifest-rm.1.md │ ├── buildah-manifest.1.md │ ├── buildah-mkcw.1.md │ ├── buildah-mount.1.md │ ├── buildah-prune.1.md │ ├── buildah-pull.1.md │ ├── buildah-push.1.md │ ├── buildah-rename.1.md │ ├── buildah-rm.1.md │ ├── buildah-rmi.1.md │ ├── buildah-run.1.md │ ├── buildah-source-add.1.md │ ├── buildah-source-create.1.md │ ├── buildah-source-pull.1.md │ ├── buildah-source-push.1.md │ ├── buildah-source.1.md │ ├── buildah-tag.1.md │ ├── buildah-umount.1.md │ ├── buildah-unshare.1.md │ ├── buildah-version.1.md │ ├── buildah.1.md │ ├── containertools/ │ │ └── README.md │ ├── links/ │ │ └── buildah-bud.1 │ ├── release-announcements/ │ │ ├── README.md │ │ ├── v0.12.md │ │ ├── v0.16.md │ │ ├── v1.1.md │ │ ├── v1.2.md │ │ ├── v1.3.md │ │ ├── v1.4.md │ │ └── v1.5.md │ ├── samples/ │ │ └── registries.conf │ └── tutorials/ │ ├── 01-intro.md │ ├── 02-registries-repositories.md │ ├── 03-on-build.md │ ├── 04-include-in-your-build-tool.md │ ├── 05-openshift-rootless-build.md │ └── README.md ├── examples/ │ ├── all-the-things.sh │ ├── copy.sh │ └── lighttpd.sh ├── go.mod ├── go.sum ├── hack/ │ ├── apparmor_tag.sh │ ├── build_speed.sh │ ├── check_vendor_toolchain.sh │ ├── get_ci_vm.sh │ ├── libsubid_tag.sh │ ├── sqlite_tag.sh │ ├── systemd_tag.sh │ ├── tree_status.sh │ └── xref-helpmsgs-manpages ├── image.go ├── imagebuildah/ │ ├── build.go │ ├── build_linux.go │ ├── build_linux_test.go │ ├── build_notlinux.go │ ├── executor.go │ ├── stage_executor.go │ ├── stage_executor_test.go │ ├── util.go │ └── util_test.go ├── import.go ├── info.go ├── install.md ├── internal/ │ ├── config/ │ │ ├── convert.go │ │ ├── convert_test.go │ │ ├── executor.go │ │ ├── executor_test.go │ │ └── override.go │ ├── metadata/ │ │ └── metadata.go │ ├── mkcw/ │ │ ├── archive.go │ │ ├── archive_test.go │ │ ├── attest.go │ │ ├── embed/ │ │ │ ├── asm/ │ │ │ │ ├── doc.md │ │ │ │ └── entrypoint_amd64.s │ │ │ ├── check.sh │ │ │ ├── doc.go │ │ │ ├── entrypoint.go │ │ │ ├── entrypoint_amd64.s │ │ │ ├── entrypoint_arm64.s │ │ │ ├── entrypoint_ppc64le.s │ │ │ └── entrypoint_s390x.s │ │ ├── entrypoint.go │ │ ├── luks.go │ │ ├── luks_test.go │ │ ├── makefs.go │ │ ├── types/ │ │ │ ├── attest.go │ │ │ └── workload.go │ │ ├── workload.go │ │ └── workload_test.go │ ├── open/ │ │ ├── open.go │ │ ├── open_linux.go │ │ ├── open_linux_test.go │ │ ├── open_test.go │ │ ├── open_types.go │ │ ├── open_unix.go │ │ └── open_unsupported.go │ ├── output/ │ │ ├── build_output.go │ │ └── build_output_test.go │ ├── parsevolume/ │ │ └── parse.go │ ├── pty/ │ │ ├── pty_posix.go │ │ ├── pty_ptmx.go │ │ └── pty_unsupported.go │ ├── rpc/ │ │ ├── listen/ │ │ │ └── listen.go │ │ └── noop/ │ │ ├── noop.go │ │ └── pb/ │ │ ├── build.sh │ │ ├── noop.pb.go │ │ ├── noop.proto │ │ └── noop_grpc.pb.go │ ├── sanitize/ │ │ ├── sanitize.go │ │ └── sanitize_test.go │ ├── sbom/ │ │ ├── merge.go │ │ ├── merge_test.go │ │ ├── presets.go │ │ └── presets_test.go │ ├── source/ │ │ ├── add.go │ │ ├── create.go │ │ ├── pull.go │ │ ├── push.go │ │ └── source.go │ ├── tmpdir/ │ │ ├── tmpdir.go │ │ └── tmpdir_test.go │ ├── types.go │ ├── util/ │ │ ├── util.go │ │ └── util_test.go │ └── volumes/ │ ├── bind_linux.go │ ├── bind_linux_test.go │ ├── bind_notlinux.go │ ├── bind_test.go │ ├── volumes.go │ └── volumes_test.go ├── manifests/ │ └── compat.go ├── mount.go ├── new.go ├── new_test.go ├── pkg/ │ ├── binfmt/ │ │ ├── binfmt.go │ │ └── binfmt_unsupported.go │ ├── blobcache/ │ │ └── blobcache.go │ ├── chrootuser/ │ │ ├── user.go │ │ ├── user_basic.go │ │ ├── user_test.go │ │ └── user_unix.go │ ├── cli/ │ │ ├── build.go │ │ ├── common.go │ │ ├── common_test.go │ │ └── exec_codes.go │ ├── completion/ │ │ └── completion.go │ ├── dummy/ │ │ └── dummy_test.go │ ├── formats/ │ │ ├── doc.go │ │ ├── formats.go │ │ └── templates.go │ ├── jail/ │ │ ├── jail.go │ │ ├── jail_int32.go │ │ ├── jail_int64.go │ │ └── jail_test.go │ ├── manifests/ │ │ └── compat.go │ ├── overlay/ │ │ ├── overlay.go │ │ ├── overlay_freebsd.go │ │ ├── overlay_linux.go │ │ └── overlay_unsupported.go │ ├── parse/ │ │ ├── parse.go │ │ ├── parse_test.go │ │ ├── parse_unix.go │ │ └── parse_unsupported.go │ ├── rusage/ │ │ ├── rusage.go │ │ ├── rusage_test.go │ │ ├── rusage_unix.go │ │ └── rusage_unsupported.go │ ├── sourcepolicy/ │ │ ├── policy.go │ │ └── policy_test.go │ ├── sshagent/ │ │ ├── sshagent.go │ │ └── sshagent_test.go │ ├── supplemented/ │ │ └── compat.go │ ├── umask/ │ │ └── umask.go │ ├── util/ │ │ ├── resource_unix.go │ │ ├── resource_unix_test.go │ │ ├── resource_windows.go │ │ ├── test/ │ │ │ └── test1/ │ │ │ └── Containerfile │ │ ├── uptime_darwin.go │ │ ├── uptime_freebsd.go │ │ ├── uptime_linux.go │ │ ├── uptime_netbsd.go │ │ ├── uptime_windows.go │ │ ├── util.go │ │ ├── util_test.go │ │ ├── version_unix.go │ │ └── version_windows.go │ └── volumes/ │ └── volumes.go ├── plans/ │ └── main.fmf ├── pull.go ├── push.go ├── release.sh ├── rpm/ │ ├── Makefile │ ├── buildah.spec │ ├── gating.yaml │ └── update-spec-version.sh ├── run.go ├── run_common.go ├── run_common_test.go ├── run_freebsd.go ├── run_linux.go ├── run_test.go ├── run_unix.go ├── run_unsupported.go ├── scan.go ├── seccomp.go ├── seccomp_unsupported.go ├── selinux.go ├── selinux_unsupported.go ├── tests/ │ ├── NEW-IMAGES │ ├── add.bats │ ├── authenticate.bats │ ├── basic.bats │ ├── blobcache.bats │ ├── bud/ │ │ ├── add-checksum/ │ │ │ ├── Containerfile │ │ │ ├── Containerfile.bad │ │ │ └── Containerfile.bad-checksum │ │ ├── add-chmod/ │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile.bad │ │ │ ├── Dockerfile.combined │ │ │ └── addchmod.txt │ │ ├── add-chown/ │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile.bad │ │ │ └── addchown.txt │ │ ├── add-create-absolute-path/ │ │ │ ├── Dockerfile │ │ │ └── distutils.cfg │ │ ├── add-create-relative-path/ │ │ │ ├── Dockerfile │ │ │ └── distutils.cfg │ │ ├── add-file/ │ │ │ ├── Dockerfile │ │ │ ├── file │ │ │ └── file2 │ │ ├── add-run-dir/ │ │ │ └── Dockerfile │ │ ├── addtl-tags/ │ │ │ └── Dockerfile │ │ ├── all-platform/ │ │ │ └── Containerfile.default-arg │ │ ├── arg-scope/ │ │ │ ├── Containerfile.arg-mixed-defaults │ │ │ ├── Containerfile.copy-from-arg │ │ │ ├── Containerfile.from-arg │ │ │ ├── Containerfile.multiple-args-one-step │ │ │ ├── Containerfile.multiple-args-stage-only │ │ │ ├── Containerfile.multiple-args-stage-overrides-one │ │ │ ├── Containerfile.stage-overrides-header │ │ │ └── Containerfile.stage-overrides-header-copy-from │ │ ├── base-with-arg/ │ │ │ ├── Containerfile │ │ │ ├── Containerfile2 │ │ │ ├── Containerfilebad │ │ │ ├── first.args │ │ │ └── second.args │ │ ├── base-with-labels/ │ │ │ ├── Containerfile │ │ │ ├── Containerfile.layer │ │ │ ├── Containerfile.multi-stage │ │ │ └── Containerfile2 │ │ ├── bud.limits/ │ │ │ └── Containerfile │ │ ├── build-arg/ │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile2 │ │ │ ├── Dockerfile3 │ │ │ └── Dockerfile4 │ │ ├── build-with-from/ │ │ │ └── Containerfile │ │ ├── buildkit-mount/ │ │ │ ├── Containerfile5 │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile2 │ │ │ ├── Dockerfile3 │ │ │ ├── Dockerfile4 │ │ │ ├── Dockerfile6 │ │ │ ├── Dockerfilecacheread │ │ │ ├── Dockerfilecachereadwithoutz │ │ │ ├── Dockerfilecachewrite │ │ │ ├── Dockerfilecachewritesharing │ │ │ ├── Dockerfilecachewritewithoutz │ │ │ ├── Dockerfilemultiplemounts │ │ │ ├── Dockerfiletmpfs │ │ │ ├── Dockerfiletmpfscopyup │ │ │ ├── input_file │ │ │ └── subdir/ │ │ │ └── input_file │ │ ├── buildkit-mount-from/ │ │ │ ├── Dockerfilebindfrom │ │ │ ├── Dockerfilebindfromrelative │ │ │ ├── Dockerfilebindfromwithemptyfrom │ │ │ ├── Dockerfilebindfromwithoutsource │ │ │ ├── Dockerfilebindfromwriteable │ │ │ ├── Dockerfilebuildkitbase │ │ │ ├── Dockerfilebuildkitbaserelative │ │ │ ├── Dockerfilecachefrom │ │ │ ├── Dockerfilecachefromimage │ │ │ ├── Dockerfilecachemultiplefrom │ │ │ ├── Dockerfilemultistagefrom │ │ │ ├── Dockerfilemultistagefromcache │ │ │ ├── Dockermultistagefrom │ │ │ ├── hello │ │ │ ├── hello1 │ │ │ └── hello2 │ │ ├── cache-chown/ │ │ │ ├── Dockerfile.add1 │ │ │ ├── Dockerfile.add2 │ │ │ ├── Dockerfile.copy1 │ │ │ ├── Dockerfile.copy2 │ │ │ ├── Dockerfile.prev1 │ │ │ ├── Dockerfile.prev2 │ │ │ ├── Dockerfile.tar1 │ │ │ ├── Dockerfile.tar2 │ │ │ ├── Dockerfile.url1 │ │ │ ├── Dockerfile.url2 │ │ │ └── testfile │ │ ├── cache-format/ │ │ │ └── Dockerfile │ │ ├── cache-from/ │ │ │ └── Containerfile │ │ ├── cache-from-stage/ │ │ │ ├── Containerfile │ │ │ └── testfile │ │ ├── cache-mount-locked/ │ │ │ ├── Containerfile │ │ │ └── file │ │ ├── cache-scratch/ │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile.config │ │ │ ├── Dockerfile.different1 │ │ │ └── Dockerfile.different2 │ │ ├── cache-stages/ │ │ │ ├── Dockerfile.1 │ │ │ ├── Dockerfile.2 │ │ │ └── Dockerfile.3 │ │ ├── capabilities/ │ │ │ └── Dockerfile │ │ ├── cdi/ │ │ │ ├── Dockerfile │ │ │ └── containers-cdi.yaml │ │ ├── check-race/ │ │ │ └── Containerfile │ │ ├── container-ignoresymlink/ │ │ │ ├── Dockerfile │ │ │ ├── hello │ │ │ └── world │ │ ├── containeranddockerfile/ │ │ │ ├── Containerfile │ │ │ └── Dockerfile │ │ ├── containerfile/ │ │ │ ├── Containerfile │ │ │ └── Containerfile.in │ │ ├── containerignore/ │ │ │ ├── .containerignore │ │ │ ├── .dockerignore │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile.succeed │ │ │ ├── subdir/ │ │ │ │ ├── sub1.txt │ │ │ │ └── sub2.txt │ │ │ ├── test1.txt │ │ │ └── test2.txt │ │ ├── context-escape-dir/ │ │ │ ├── testdir/ │ │ │ │ └── Containerfile │ │ │ └── upperfile.txt │ │ ├── context-from-stdin/ │ │ │ └── Dockerfile │ │ ├── copy-archive/ │ │ │ └── Containerfile │ │ ├── copy-chmod/ │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile.bad │ │ │ ├── Dockerfile.combined │ │ │ └── copychmod.txt │ │ ├── copy-chown/ │ │ │ ├── Containerfile.chown_user │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile.bad │ │ │ ├── Dockerfile.bad2 │ │ │ └── copychown.txt │ │ ├── copy-create-absolute-path/ │ │ │ ├── Dockerfile │ │ │ └── distutils.cfg │ │ ├── copy-create-relative-path/ │ │ │ ├── Dockerfile │ │ │ └── distutils.cfg │ │ ├── copy-envvar/ │ │ │ ├── Containerfile │ │ │ └── file-0.0.1.txt │ │ ├── copy-exclude/ │ │ │ ├── Containerfile │ │ │ ├── Containerfile.missing │ │ │ ├── test1.txt │ │ │ └── test2.txt │ │ ├── copy-from/ │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile.bad │ │ │ ├── Dockerfile2 │ │ │ ├── Dockerfile2.bad │ │ │ ├── Dockerfile3 │ │ │ └── Dockerfile4 │ │ ├── copy-from-stage-scoped-arg/ │ │ │ ├── Containerfile │ │ │ └── Containerfile.multi-copy-arg-override │ │ ├── copy-globs/ │ │ │ ├── Containerfile │ │ │ ├── Containerfile.bad │ │ │ ├── Containerfile.missing │ │ │ ├── Dockerfile │ │ │ ├── test1.txt │ │ │ └── test2.txt │ │ ├── copy-multiple-files/ │ │ │ ├── Dockerfile │ │ │ ├── file │ │ │ └── file2 │ │ ├── copy-multistage-paths/ │ │ │ ├── Dockerfile.absolute │ │ │ ├── Dockerfile.invalid_from │ │ │ └── Dockerfile.relative │ │ ├── copy-parents/ │ │ │ ├── Containerfile │ │ │ ├── Containerfile-hardlinks │ │ │ ├── x/ │ │ │ │ ├── a.txt │ │ │ │ ├── y/ │ │ │ │ │ ├── a.txt │ │ │ │ │ └── b.txt │ │ │ │ └── z/ │ │ │ │ ├── a.txt │ │ │ │ └── b.txt │ │ │ └── y/ │ │ │ ├── a.txt │ │ │ └── b.txt │ │ ├── copy-root/ │ │ │ ├── Dockerfile │ │ │ └── distutils.cfg │ │ ├── copy-workdir/ │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile.2 │ │ │ ├── file1.txt │ │ │ └── file2.txt │ │ ├── dest-final-slash/ │ │ │ └── Dockerfile │ │ ├── dest-symlink/ │ │ │ └── Dockerfile │ │ ├── dest-symlink-dangling/ │ │ │ └── Dockerfile │ │ ├── device/ │ │ │ └── Dockerfile │ │ ├── dns/ │ │ │ └── Dockerfile │ │ ├── dockerfile/ │ │ │ └── Dockerfile │ │ ├── dockerignore/ │ │ │ ├── .dockerignore │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile.succeed │ │ │ ├── subdir/ │ │ │ │ ├── sub1.txt │ │ │ │ └── sub2.txt │ │ │ ├── test1.txt │ │ │ └── test2.txt │ │ ├── dockerignore2/ │ │ │ ├── .dockerignore │ │ │ ├── Dockerfile │ │ │ └── subdir/ │ │ │ ├── sub1.txt │ │ │ └── subsubdir/ │ │ │ └── subsub1.txt │ │ ├── dockerignore3/ │ │ │ ├── .dockerignore │ │ │ ├── BUILD.md │ │ │ ├── COPYRIGHT │ │ │ ├── Dockerfile │ │ │ ├── LICENSE │ │ │ ├── README-secret.md │ │ │ ├── README.md │ │ │ ├── manifest │ │ │ ├── src/ │ │ │ │ ├── Makefile │ │ │ │ ├── cmd/ │ │ │ │ │ ├── Makefile │ │ │ │ │ └── main.in │ │ │ │ ├── etc/ │ │ │ │ │ ├── foo.conf │ │ │ │ │ └── foo.conf.d/ │ │ │ │ │ └── dropin.conf │ │ │ │ └── lib/ │ │ │ │ ├── Makefile │ │ │ │ └── framework.in │ │ │ ├── test1.txt │ │ │ ├── test2.txt │ │ │ └── test3.txt │ │ ├── dockerignore4/ │ │ │ ├── BUILD.md │ │ │ ├── COPYRIGHT │ │ │ ├── Dockerfile.test │ │ │ ├── Dockerfile.test.dockerignore │ │ │ ├── LICENSE │ │ │ ├── README-secret.md │ │ │ ├── README.md │ │ │ ├── manifest │ │ │ ├── src/ │ │ │ │ ├── Makefile │ │ │ │ ├── cmd/ │ │ │ │ │ ├── Makefile │ │ │ │ │ └── main.in │ │ │ │ ├── etc/ │ │ │ │ │ ├── foo.conf │ │ │ │ │ └── foo.conf.d/ │ │ │ │ │ └── dropin.conf │ │ │ │ └── lib/ │ │ │ │ ├── Makefile │ │ │ │ └── framework.in │ │ │ ├── test1.txt │ │ │ ├── test2.txt │ │ │ └── test3.txt │ │ ├── dockerignore6/ │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile.dockerignore │ │ │ ├── Dockerfile.succeed │ │ │ ├── Dockerfile.succeed.dockerignore │ │ │ ├── subdir/ │ │ │ │ ├── sub1.txt │ │ │ │ └── sub2.txt │ │ │ ├── test1.txt │ │ │ └── test2.txt │ │ ├── dupe-arg-env-name/ │ │ │ └── Containerfile │ │ ├── env/ │ │ │ ├── Dockerfile.check-env │ │ │ ├── Dockerfile.env-from-image │ │ │ ├── Dockerfile.env-precedence │ │ │ ├── Dockerfile.env-same-file │ │ │ └── Dockerfile.special-chars │ │ ├── exit42/ │ │ │ └── Containerfile │ │ ├── from-as/ │ │ │ ├── Dockerfile │ │ │ └── Dockerfile.skip │ │ ├── from-base/ │ │ │ └── Containerfile │ │ ├── from-invalid-registry/ │ │ │ └── Containerfile │ │ ├── from-multiple-files/ │ │ │ ├── Dockerfile1.alpine │ │ │ ├── Dockerfile1.scratch │ │ │ ├── Dockerfile2.glob │ │ │ ├── Dockerfile2.nofrom │ │ │ └── Dockerfile2.withfrom │ │ ├── from-scratch/ │ │ │ ├── Containerfile │ │ │ ├── Containerfile2 │ │ │ └── Dockerfile │ │ ├── from-with-arg/ │ │ │ └── Containerfile │ │ ├── group/ │ │ │ └── Containerfile │ │ ├── hardlink/ │ │ │ └── Dockerfile │ │ ├── healthcheck/ │ │ │ └── Dockerfile │ │ ├── heredoc/ │ │ │ ├── Containerfile │ │ │ ├── Containerfile.bash_file │ │ │ ├── Containerfile.she_bang │ │ │ └── Containerfile.verify_mount_leak │ │ ├── heredoc-ignore/ │ │ │ ├── .containerignore │ │ │ └── Containerfile │ │ ├── inline-network/ │ │ │ ├── Dockerfile1 │ │ │ ├── Dockerfile2 │ │ │ ├── Dockerfile3 │ │ │ └── Dockerfile4 │ │ ├── layers-squash/ │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile.hardlinks │ │ │ ├── Dockerfile.multi-stage │ │ │ └── artifact │ │ ├── leading-args/ │ │ │ └── Dockerfile │ │ ├── long-sleep/ │ │ │ └── Dockerfile │ │ ├── maintainer/ │ │ │ └── Dockerfile │ │ ├── masks/ │ │ │ ├── Containerfile │ │ │ └── test.sh │ │ ├── metadata-only/ │ │ │ └── Containerfile │ │ ├── mount/ │ │ │ └── Dockerfile │ │ ├── multi-stage-builds/ │ │ │ ├── Dockerfile.arg │ │ │ ├── Dockerfile.arg_in_copy │ │ │ ├── Dockerfile.arg_in_stage │ │ │ ├── Dockerfile.extended │ │ │ ├── Dockerfile.index │ │ │ ├── Dockerfile.mixed │ │ │ ├── Dockerfile.name │ │ │ ├── Dockerfile.rebase │ │ │ ├── Dockerfile.reused │ │ │ └── Dockerfile.reused2 │ │ ├── multi-stage-builds-small-as/ │ │ │ ├── Dockerfile.index │ │ │ ├── Dockerfile.mixed │ │ │ └── Dockerfile.name │ │ ├── multi-stage-only-base/ │ │ │ ├── Containerfile1 │ │ │ ├── Containerfile2 │ │ │ └── Containerfile3 │ │ ├── multiarch/ │ │ │ ├── Containerfile.reset-platform │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile.built-in-args │ │ │ ├── Dockerfile.fail │ │ │ ├── Dockerfile.fail-multistage │ │ │ └── Dockerfile.no-run │ │ ├── namespaces/ │ │ │ └── Containerfile │ │ ├── network/ │ │ │ └── Containerfile │ │ ├── no-change/ │ │ │ └── Dockerfile │ │ ├── no-history/ │ │ │ └── Dockerfile │ │ ├── no-hostname/ │ │ │ ├── Containerfile │ │ │ └── Containerfile.noetc │ │ ├── non-directory-in-path/ │ │ │ └── non-directory │ │ ├── onbuild/ │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile1 │ │ │ └── Dockerfile2 │ │ ├── only-base/ │ │ │ └── Containerfile │ │ ├── platform-sets-args/ │ │ │ └── Containerfile │ │ ├── preprocess/ │ │ │ ├── Decomposed.in │ │ │ ├── Error.in │ │ │ ├── base │ │ │ ├── common │ │ │ └── install-base │ │ ├── preserve-volumes/ │ │ │ └── Dockerfile │ │ ├── pull/ │ │ │ └── Containerfile │ │ ├── recurse/ │ │ │ └── Dockerfile │ │ ├── run-mounts/ │ │ │ ├── Dockerfile.secret │ │ │ ├── Dockerfile.secret-access │ │ │ ├── Dockerfile.secret-mode │ │ │ ├── Dockerfile.secret-not-required │ │ │ ├── Dockerfile.secret-options │ │ │ ├── Dockerfile.secret-required │ │ │ ├── Dockerfile.secret-required-false │ │ │ ├── Dockerfile.secret-required-wo-value │ │ │ ├── Dockerfile.ssh │ │ │ ├── Dockerfile.ssh_access │ │ │ └── Dockerfile.ssh_options │ │ ├── run-privd/ │ │ │ └── Dockerfile │ │ ├── run-scenarios/ │ │ │ ├── Dockerfile.args │ │ │ ├── Dockerfile.cmd-empty-run │ │ │ ├── Dockerfile.cmd-run │ │ │ ├── Dockerfile.entrypoint-cmd-empty-run │ │ │ ├── Dockerfile.entrypoint-cmd-run │ │ │ ├── Dockerfile.entrypoint-empty-run │ │ │ ├── Dockerfile.entrypoint-run │ │ │ ├── Dockerfile.multi-args │ │ │ └── Dockerfile.noop-flags │ │ ├── save-stages/ │ │ │ ├── Dockerfile.arg-build-stages-and-chained-build-stages │ │ │ ├── Dockerfile.chained-three-build-stages │ │ │ ├── Dockerfile.chained-two-build-stages │ │ │ ├── Dockerfile.chained-two-build-stages-no-aliases │ │ │ ├── Dockerfile.empty-intermediate-build-stage │ │ │ ├── Dockerfile.final-uses-build-stage │ │ │ ├── Dockerfile.simple │ │ │ ├── Dockerfile.single-build-stage │ │ │ ├── Dockerfile.single-build-stage-modifiable │ │ │ ├── Dockerfile.single-build-stage-modifiable-renamed │ │ │ ├── Dockerfile.three-build-stages-parent-child-independent │ │ │ ├── Dockerfile.two-build-stages │ │ │ └── Dockerfile.two-build-stages-one-unused │ │ ├── secret-env/ │ │ │ └── Dockerfile │ │ ├── secret-relative/ │ │ │ ├── Dockerfile │ │ │ ├── secret1.txt │ │ │ └── secret2.txt │ │ ├── shell/ │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile.build-shell-custom │ │ │ └── Dockerfile.build-shell-default │ │ ├── simple-multi-step/ │ │ │ └── Containerfile │ │ ├── stdio/ │ │ │ └── Dockerfile │ │ ├── supplemental-groups/ │ │ │ └── Dockerfile │ │ ├── symlink/ │ │ │ ├── Containerfile.add-tar-gz-with-link │ │ │ ├── Containerfile.add-tar-with-link │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile.absolute-dir-symlink │ │ │ ├── Dockerfile.absolute-symlink │ │ │ ├── Dockerfile.multiple-symlinks │ │ │ ├── Dockerfile.relative-symlink │ │ │ ├── Dockerfile.replace-symlink │ │ │ └── Dockerfile.symlink-points-to-itself │ │ ├── target/ │ │ │ └── Dockerfile │ │ ├── targetarch/ │ │ │ └── Dockerfile │ │ ├── terminal/ │ │ │ └── Dockerfile │ │ ├── unrecognized/ │ │ │ └── Dockerfile │ │ ├── use-args/ │ │ │ ├── Containerfile │ │ │ ├── Containerfile.dest_nobrace │ │ │ └── Containerfile.destination │ │ ├── use-layers/ │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile.2 │ │ │ ├── Dockerfile.3 │ │ │ ├── Dockerfile.4 │ │ │ ├── Dockerfile.5 │ │ │ ├── Dockerfile.6 │ │ │ ├── Dockerfile.7 │ │ │ ├── Dockerfile.build-args │ │ │ ├── Dockerfile.dangling-symlink │ │ │ ├── Dockerfile.fail-case │ │ │ ├── Dockerfile.multistage-copy │ │ │ └── Dockerfile.non-existent-registry │ │ ├── verify-cleanup/ │ │ │ ├── Dockerfile │ │ │ ├── hey │ │ │ └── secret1.txt │ │ ├── volume-ownership/ │ │ │ └── Dockerfile │ │ ├── volume-perms/ │ │ │ └── Dockerfile │ │ ├── volume-symlink/ │ │ │ ├── Dockerfile │ │ │ └── Dockerfile.no-symlink │ │ ├── with-arg/ │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile2 │ │ │ └── Dockerfilefromarg │ │ ├── workdir-symlink/ │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile-2 │ │ │ └── Dockerfile-3 │ │ └── workdir-user/ │ │ └── Dockerfile │ ├── bud.bats │ ├── bud_overlay_leaks.bats │ ├── byid.bats │ ├── cdi.bats │ ├── chroot.bats │ ├── commit.bats │ ├── config.bats │ ├── conformance/ │ │ ├── README.md │ │ ├── conformance_test.go │ │ ├── selinux_linux_test.go │ │ ├── selinux_unsupported_test.go │ │ └── testdata/ │ │ ├── Dockerfile.add │ │ ├── Dockerfile.copyfrom_1 │ │ ├── Dockerfile.copyfrom_10 │ │ ├── Dockerfile.copyfrom_11 │ │ ├── Dockerfile.copyfrom_12 │ │ ├── Dockerfile.copyfrom_13 │ │ ├── Dockerfile.copyfrom_2 │ │ ├── Dockerfile.copyfrom_3 │ │ ├── Dockerfile.copyfrom_3_1 │ │ ├── Dockerfile.copyfrom_4 │ │ ├── Dockerfile.copyfrom_5 │ │ ├── Dockerfile.copyfrom_6 │ │ ├── Dockerfile.copyfrom_7 │ │ ├── Dockerfile.copyfrom_8 │ │ ├── Dockerfile.copyfrom_9 │ │ ├── Dockerfile.edgecases │ │ ├── Dockerfile.env │ │ ├── Dockerfile.exposedefault │ │ ├── Dockerfile.heredoc-quoting │ │ ├── Dockerfile.margs │ │ ├── Dockerfile.quoted-arg │ │ ├── Dockerfile.reusebase │ │ ├── Dockerfile.run.args │ │ ├── Dockerfile.shell │ │ ├── add/ │ │ │ ├── archive/ │ │ │ │ ├── Dockerfile.1 │ │ │ │ ├── Dockerfile.2 │ │ │ │ ├── Dockerfile.3 │ │ │ │ ├── Dockerfile.4 │ │ │ │ ├── Dockerfile.5 │ │ │ │ ├── Dockerfile.6 │ │ │ │ └── Dockerfile.7 │ │ │ ├── dir-not-dir/ │ │ │ │ └── Dockerfile │ │ │ ├── not-dir-dir/ │ │ │ │ └── Dockerfile │ │ │ ├── parent-clean/ │ │ │ │ └── Dockerfile │ │ │ ├── parent-dangling/ │ │ │ │ └── Dockerfile │ │ │ ├── parent-symlink/ │ │ │ │ └── Dockerfile │ │ │ └── populated-dir-not-dir/ │ │ │ └── Dockerfile │ │ ├── builtins/ │ │ │ ├── Dockerfile │ │ │ ├── otherfile.txt │ │ │ └── sourcefile.txt │ │ ├── chown-volume/ │ │ │ └── Dockerfile │ │ ├── copy/ │ │ │ ├── Dockerfile │ │ │ └── script │ │ ├── copy-escape-glob/ │ │ │ ├── Dockerfile │ │ │ └── app/ │ │ │ ├── [xyz]/ │ │ │ │ ├── [abc]/ │ │ │ │ │ └── file.txt │ │ │ │ └── file.txt │ │ │ ├── nope/ │ │ │ │ └── file.txt │ │ │ └── strauv/ │ │ │ └── file.txt │ │ ├── copy-parents/ │ │ │ ├── x/ │ │ │ │ ├── a.txt │ │ │ │ ├── y/ │ │ │ │ │ ├── a.txt │ │ │ │ │ └── b.txt │ │ │ │ └── z/ │ │ │ │ ├── a.txt │ │ │ │ └── b.txt │ │ │ └── y/ │ │ │ ├── a.txt │ │ │ └── b.txt │ │ ├── copyblahblub/ │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile2 │ │ │ ├── Dockerfile3 │ │ │ └── firstdir/ │ │ │ └── seconddir/ │ │ │ ├── dir-a/ │ │ │ │ └── file-a │ │ │ └── dir-b/ │ │ │ └── file-b │ │ ├── copychown/ │ │ │ ├── Dockerfile │ │ │ ├── script │ │ │ └── script2 │ │ ├── copydir/ │ │ │ ├── Dockerfile │ │ │ └── dir/ │ │ │ └── file │ │ ├── copyempty/ │ │ │ ├── .script │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile2 │ │ │ ├── script1 │ │ │ └── script2 │ │ ├── copyglob/ │ │ │ ├── Beach.txt │ │ │ ├── Dockerfile │ │ │ ├── a/ │ │ │ │ └── sub/ │ │ │ │ └── subsub/ │ │ │ │ ├── protopatriarchal.txt │ │ │ │ └── undestructible.txt │ │ │ ├── b/ │ │ │ │ ├── .sub/ │ │ │ │ │ ├── gade.txt │ │ │ │ │ └── parcae.txt │ │ │ │ ├── heliacally.txt │ │ │ │ ├── overgoing.txt │ │ │ │ └── sub/ │ │ │ │ ├── ileosigmoidostomy.txt │ │ │ │ └── overdilation.txt │ │ │ ├── c/ │ │ │ │ └── sub/ │ │ │ │ ├── disadvise.txt │ │ │ │ ├── subsub/ │ │ │ │ │ └── subsubsub/ │ │ │ │ │ ├── fiddlecome.txt │ │ │ │ │ └── unweariableness.txt │ │ │ │ └── travel-sick.txt │ │ │ ├── d/ │ │ │ │ ├── .sub/ │ │ │ │ │ ├── restocks.txt │ │ │ │ │ └── unblazoned.txt │ │ │ │ └── sub/ │ │ │ │ ├── alkalinity.txt │ │ │ │ └── glandules.txt │ │ │ ├── e/ │ │ │ │ ├── .sub/ │ │ │ │ │ ├── Tytonidae.txt │ │ │ │ │ └── vice-guilty.txt │ │ │ │ ├── Towroy.txt │ │ │ │ ├── sub/ │ │ │ │ │ └── subsub/ │ │ │ │ │ ├── near-blindness.txt │ │ │ │ │ └── paymaster-generalship.txt │ │ │ │ └── subjectivities.txt │ │ │ ├── f/ │ │ │ │ ├── .sub/ │ │ │ │ │ ├── bilobate.txt │ │ │ │ │ └── fine-headed.txt │ │ │ │ ├── Etnean.txt │ │ │ │ ├── Sheya.txt │ │ │ │ └── sub/ │ │ │ │ ├── Vernaccia.txt │ │ │ │ ├── inaccordance.txt │ │ │ │ └── subsub/ │ │ │ │ └── subsubsub/ │ │ │ │ ├── ankylosing.txt │ │ │ │ └── ocean-born.txt │ │ │ ├── g/ │ │ │ │ ├── sub/ │ │ │ │ │ ├── huerta.txt │ │ │ │ │ └── smopple.txt │ │ │ │ ├── triple-throw.txt │ │ │ │ └── unexciting.txt │ │ │ ├── h/ │ │ │ │ ├── .sub/ │ │ │ │ │ ├── Ellston.txt │ │ │ │ │ └── mine-run.txt │ │ │ │ └── sub/ │ │ │ │ ├── chromaticness.txt │ │ │ │ └── noninhabitancy.txt │ │ │ ├── i/ │ │ │ │ ├── .sub/ │ │ │ │ │ ├── Auer.txt │ │ │ │ │ └── hexachlorocyclohexane.txt │ │ │ │ └── sub/ │ │ │ │ └── subsub/ │ │ │ │ ├── Minnnie.txt │ │ │ │ ├── papsquidder.txt │ │ │ │ └── subsubsub/ │ │ │ │ ├── Croghan.txt │ │ │ │ └── stacc..txt │ │ │ ├── j/ │ │ │ │ └── sub/ │ │ │ │ ├── Hamon.txt │ │ │ │ ├── subsub/ │ │ │ │ │ └── subsubsub/ │ │ │ │ │ ├── anapaestic.txt │ │ │ │ │ └── castlelike.txt │ │ │ │ └── topsy-turvydom.txt │ │ │ ├── k/ │ │ │ │ ├── coembody.txt │ │ │ │ └── unvoweled.txt │ │ │ ├── l/ │ │ │ │ ├── .sub/ │ │ │ │ │ ├── galliots.txt │ │ │ │ │ └── minning.txt │ │ │ │ ├── sub/ │ │ │ │ │ └── subsub/ │ │ │ │ │ ├── misidentification.txt │ │ │ │ │ └── palling.txt │ │ │ │ ├── torpifying.txt │ │ │ │ └── unmarring.txt │ │ │ ├── m/ │ │ │ │ └── sub/ │ │ │ │ ├── cache.txt │ │ │ │ └── ribbon-marked.txt │ │ │ └── pasteups.txt │ │ ├── copyrename/ │ │ │ ├── Dockerfile │ │ │ └── file1 │ │ ├── copysymlink/ │ │ │ ├── Dockerfile │ │ │ └── Dockerfile2 │ │ ├── dir/ │ │ │ ├── Dockerfile │ │ │ ├── file │ │ │ └── subdir/ │ │ │ └── file2 │ │ ├── dockerignore/ │ │ │ ├── allowlist/ │ │ │ │ ├── alternating/ │ │ │ │ │ ├── .dockerignore │ │ │ │ │ ├── Dockerfile │ │ │ │ │ └── subdir/ │ │ │ │ │ └── subdir1/ │ │ │ │ │ └── subdir2/ │ │ │ │ │ └── subdir3/ │ │ │ │ │ └── subdir4/ │ │ │ │ │ └── subdir5/ │ │ │ │ │ └── file │ │ │ │ ├── alternating-nothing/ │ │ │ │ │ ├── .dockerignore │ │ │ │ │ ├── Dockerfile │ │ │ │ │ └── subdir/ │ │ │ │ │ └── subdir1/ │ │ │ │ │ └── subdir2/ │ │ │ │ │ └── subdir3/ │ │ │ │ │ └── subdir4/ │ │ │ │ │ └── subdir5/ │ │ │ │ │ └── file │ │ │ │ ├── alternating-other/ │ │ │ │ │ ├── .dockerignore │ │ │ │ │ ├── Dockerfile │ │ │ │ │ └── subdir/ │ │ │ │ │ └── subdir1/ │ │ │ │ │ └── subdir2/ │ │ │ │ │ └── subdir3/ │ │ │ │ │ └── subdir4/ │ │ │ │ │ └── subdir5/ │ │ │ │ │ └── file2 │ │ │ │ ├── nothing-dot/ │ │ │ │ │ ├── .dockerignore │ │ │ │ │ └── Dockerfile │ │ │ │ ├── nothing-slash/ │ │ │ │ │ ├── .dockerignore │ │ │ │ │ └── Dockerfile │ │ │ │ ├── subdir-file/ │ │ │ │ │ ├── .dockerignore │ │ │ │ │ ├── Dockerfile │ │ │ │ │ └── folder1/ │ │ │ │ │ ├── file1 │ │ │ │ │ └── file2 │ │ │ │ ├── subdir-nofile/ │ │ │ │ │ ├── .dockerignore │ │ │ │ │ ├── Dockerfile │ │ │ │ │ └── folder1/ │ │ │ │ │ ├── file1 │ │ │ │ │ └── file2 │ │ │ │ ├── subsubdir-file/ │ │ │ │ │ ├── .dockerignore │ │ │ │ │ ├── Dockerfile │ │ │ │ │ └── folder/ │ │ │ │ │ └── subfolder/ │ │ │ │ │ └── file │ │ │ │ ├── subsubdir-nofile/ │ │ │ │ │ ├── .dockerignore │ │ │ │ │ ├── Dockerfile │ │ │ │ │ └── folder/ │ │ │ │ │ └── file │ │ │ │ └── subsubdir-nosubdir/ │ │ │ │ ├── .dockerignore │ │ │ │ ├── Dockerfile │ │ │ │ └── folder/ │ │ │ │ └── file │ │ │ ├── empty/ │ │ │ │ ├── .dockerignore │ │ │ │ ├── Dockerfile │ │ │ │ └── test1.txt │ │ │ ├── exceptions-skip/ │ │ │ │ ├── .dockerignore │ │ │ │ ├── Dockerfile │ │ │ │ └── volume/ │ │ │ │ └── data/ │ │ │ │ └── oneline.txt │ │ │ ├── exceptions-weirdness-1/ │ │ │ │ ├── .dockerignore │ │ │ │ ├── Dockerfile │ │ │ │ └── subdir/ │ │ │ │ ├── sub1.txt │ │ │ │ ├── sub2.txt │ │ │ │ └── sub3.txt │ │ │ ├── exceptions-weirdness-2/ │ │ │ │ ├── .dockerignore │ │ │ │ ├── Dockerfile │ │ │ │ └── subdir/ │ │ │ │ ├── sub1.txt │ │ │ │ ├── sub2.txt │ │ │ │ └── sub3.txt │ │ │ ├── integration1/ │ │ │ │ ├── .dockerignore │ │ │ │ ├── Dockerfile │ │ │ │ ├── subdir/ │ │ │ │ │ ├── sub1.txt │ │ │ │ │ └── sub2.txt │ │ │ │ ├── test1.txt │ │ │ │ └── test2.txt │ │ │ ├── integration2/ │ │ │ │ ├── .dockerignore │ │ │ │ ├── Dockerfile │ │ │ │ └── subdir/ │ │ │ │ ├── sub1.txt │ │ │ │ └── subsubdir/ │ │ │ │ └── subsub1.txt │ │ │ ├── integration3/ │ │ │ │ ├── .dockerignore │ │ │ │ ├── BUILD.md │ │ │ │ ├── COPYRIGHT │ │ │ │ ├── Dockerfile │ │ │ │ ├── LICENSE │ │ │ │ ├── README-secret.md │ │ │ │ ├── README.md │ │ │ │ ├── manifest │ │ │ │ ├── src/ │ │ │ │ │ ├── Makefile │ │ │ │ │ ├── cmd/ │ │ │ │ │ │ ├── Makefile │ │ │ │ │ │ └── main.in │ │ │ │ │ ├── etc/ │ │ │ │ │ │ ├── foo.conf │ │ │ │ │ │ └── foo.conf.d/ │ │ │ │ │ │ └── dropin.conf │ │ │ │ │ └── lib/ │ │ │ │ │ ├── Makefile │ │ │ │ │ └── framework.in │ │ │ │ ├── test1.txt │ │ │ │ ├── test2.txt │ │ │ │ └── test3.txt │ │ │ ├── minimal_test/ │ │ │ │ ├── .dockerignore │ │ │ │ ├── Dockerfile │ │ │ │ └── stuff/ │ │ │ │ └── huge/ │ │ │ │ └── usr/ │ │ │ │ └── bin/ │ │ │ │ ├── file1 │ │ │ │ └── file2 │ │ │ └── populated/ │ │ │ ├── .dotfile-a.txt │ │ │ ├── file-a.txt │ │ │ ├── file-b.txt │ │ │ ├── file-c.txt │ │ │ ├── subdir-b/ │ │ │ │ └── .dotfile-b.txt │ │ │ └── subdir-e/ │ │ │ ├── file-n.txt │ │ │ └── subdir-f/ │ │ │ └── file-o.txt │ │ ├── env/ │ │ │ └── precedence/ │ │ │ └── Dockerfile │ │ ├── header-builtin/ │ │ │ └── Dockerfile │ │ ├── heredoc/ │ │ │ ├── Dockerfile.heredoc_copy │ │ │ └── file │ │ ├── mount/ │ │ │ ├── Dockerfile │ │ │ ├── file │ │ │ └── file2 │ │ ├── mount-targets/ │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile.mount │ │ │ └── true.go │ │ ├── multistage/ │ │ │ └── copyback/ │ │ │ └── Dockerfile │ │ ├── overlapdirwithoutslash/ │ │ │ ├── Dockerfile │ │ │ └── existing/ │ │ │ └── etc/ │ │ │ └── file-in-existing-dir │ │ ├── overlapdirwithslash/ │ │ │ ├── Dockerfile │ │ │ └── existing/ │ │ │ └── etc/ │ │ │ └── file-in-existing-dir │ │ ├── replace/ │ │ │ └── symlink-with-directory/ │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile.2 │ │ │ ├── Dockerfile.3 │ │ │ ├── Dockerfile.4 │ │ │ ├── tree1/ │ │ │ │ └── directory/ │ │ │ │ └── file-in-directory │ │ │ └── tree2/ │ │ │ └── maybe-directory/ │ │ │ └── file-in-maybe-directory │ │ ├── subdir/ │ │ │ └── subdir/ │ │ │ └── Dockerfile │ │ ├── tar-g/ │ │ │ ├── Dockerfile │ │ │ ├── content.sh │ │ │ └── content.txt │ │ ├── transientmount/ │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile.env │ │ │ ├── file │ │ │ └── subdir/ │ │ │ └── file2 │ │ ├── volume/ │ │ │ ├── Dockerfile │ │ │ ├── file │ │ │ └── file2 │ │ ├── volumerun/ │ │ │ ├── Dockerfile │ │ │ ├── file │ │ │ ├── file2 │ │ │ └── file4 │ │ └── wildcard/ │ │ ├── Dockerfile │ │ └── dir2/ │ │ ├── file.a │ │ ├── file.b │ │ ├── file.c │ │ └── file2.b │ ├── containers.bats │ ├── containers.conf │ ├── containers_conf.bats │ ├── copy/ │ │ └── copy.go │ ├── copy.bats │ ├── crash/ │ │ ├── crash_notunix.go │ │ └── crash_unix.go │ ├── deny.json │ ├── digest/ │ │ ├── README.md │ │ └── make-v2sN │ ├── digest.bats │ ├── docker.json │ ├── dumpspec/ │ │ ├── dumpspec.go │ │ ├── dumpspec_linux.go │ │ ├── dumpspec_notlinux.go │ │ ├── dumpspec_notunix.go │ │ ├── dumpspec_unix.go │ │ └── notes.md │ ├── formats.bats │ ├── from.bats │ ├── help.bats │ ├── helpers.bash │ ├── helpers.bash.t │ ├── history.bats │ ├── images.bats │ ├── imgtype/ │ │ └── imgtype.go │ ├── inet/ │ │ └── inet.go │ ├── info.bats │ ├── inspect.bats │ ├── lists.bats │ ├── loglevel.bats │ ├── mkcw.bats │ ├── mount.bats │ ├── namespaces.bats │ ├── overlay.bats │ ├── passwd/ │ │ ├── README.md │ │ └── passwd.go │ ├── platforms.bats │ ├── policy.json │ ├── pull.bats │ ├── push.bats │ ├── registries-cached.conf │ ├── registries.bats │ ├── registries.conf │ ├── registries.conf.block │ ├── registries.conf.hub │ ├── rename.bats │ ├── rm.bats │ ├── rmi.bats │ ├── rpc/ │ │ └── noop/ │ │ └── noop.go │ ├── rpc.bats │ ├── run.bats │ ├── sbom.bats │ ├── selinux.bats │ ├── serve/ │ │ └── serve.go │ ├── sign.bats │ ├── source-policy.bats │ ├── source.bats │ ├── squash.bats │ ├── ssh.bats │ ├── subscriptions.bats │ ├── tag.bats │ ├── test_buildah_authentication.sh │ ├── test_buildah_baseline.sh │ ├── test_buildah_build_rpm.sh │ ├── test_buildah_rpm.sh │ ├── test_runner.sh │ ├── testreport/ │ │ ├── testreport.go │ │ └── types/ │ │ └── types.go │ ├── tmt/ │ │ ├── system.fmf │ │ └── system.sh │ ├── tools/ │ │ ├── Makefile │ │ ├── go.mod │ │ ├── go.sum │ │ ├── tools.go │ │ └── vendor/ │ │ ├── github.com/ │ │ │ └── russross/ │ │ │ └── blackfriday/ │ │ │ └── v2/ │ │ │ ├── .gitignore │ │ │ ├── .travis.yml │ │ │ ├── LICENSE.txt │ │ │ ├── README.md │ │ │ ├── block.go │ │ │ ├── doc.go │ │ │ ├── entities.go │ │ │ ├── esc.go │ │ │ ├── html.go │ │ │ ├── inline.go │ │ │ ├── markdown.go │ │ │ ├── node.go │ │ │ └── smartypants.go │ │ └── modules.txt │ ├── tutorial/ │ │ └── tutorial.go │ ├── tutorial.bats │ ├── umount.bats │ ├── validate/ │ │ ├── buildahimages-are-sane │ │ ├── pr-should-include-tests │ │ ├── pr-should-include-tests.t │ │ └── whitespace.sh │ └── wait/ │ ├── wait_notunix.go │ └── wait_unix.go ├── troubleshooting.md ├── unmount.go ├── util/ │ ├── types.go │ ├── util.go │ ├── util_test.go │ ├── util_unix.go │ ├── util_unsupported.go │ └── util_windows.go ├── util.go └── vendor/ ├── cyphar.com/ │ └── go-pathrs/ │ ├── .golangci.yml │ ├── COPYING │ ├── doc.go │ ├── handle_linux.go │ ├── internal/ │ │ ├── fdutils/ │ │ │ └── fd_linux.go │ │ └── libpathrs/ │ │ ├── error_unix.go │ │ └── libpathrs_linux.go │ ├── procfs/ │ │ └── procfs_linux.go │ ├── root_linux.go │ └── utils_linux.go ├── dario.cat/ │ └── mergo/ │ ├── .deepsource.toml │ ├── .gitignore │ ├── .travis.yml │ ├── CODE_OF_CONDUCT.md │ ├── CONTRIBUTING.md │ ├── FUNDING.json │ ├── LICENSE │ ├── README.md │ ├── SECURITY.md │ ├── doc.go │ ├── map.go │ ├── merge.go │ └── mergo.go ├── github.com/ │ ├── Azure/ │ │ └── go-ansiterm/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── SECURITY.md │ │ ├── constants.go │ │ ├── context.go │ │ ├── csi_entry_state.go │ │ ├── csi_param_state.go │ │ ├── escape_intermediate_state.go │ │ ├── escape_state.go │ │ ├── event_handler.go │ │ ├── ground_state.go │ │ ├── osc_string_state.go │ │ ├── parser.go │ │ ├── parser_action_helpers.go │ │ ├── parser_actions.go │ │ ├── states.go │ │ ├── utilities.go │ │ └── winterm/ │ │ ├── ansi.go │ │ ├── api.go │ │ ├── attr_translation.go │ │ ├── cursor_helpers.go │ │ ├── erase_helpers.go │ │ ├── scroll_helper.go │ │ ├── utilities.go │ │ └── win_event_handler.go │ ├── BurntSushi/ │ │ └── toml/ │ │ ├── .gitignore │ │ ├── COPYING │ │ ├── README.md │ │ ├── decode.go │ │ ├── deprecated.go │ │ ├── doc.go │ │ ├── encode.go │ │ ├── error.go │ │ ├── internal/ │ │ │ └── tz.go │ │ ├── lex.go │ │ ├── meta.go │ │ ├── parse.go │ │ ├── type_fields.go │ │ └── type_toml.go │ ├── Microsoft/ │ │ └── go-winio/ │ │ ├── .gitattributes │ │ ├── .gitignore │ │ ├── .golangci.yml │ │ ├── CODEOWNERS │ │ ├── LICENSE │ │ ├── README.md │ │ ├── SECURITY.md │ │ ├── backup.go │ │ ├── doc.go │ │ ├── ea.go │ │ ├── file.go │ │ ├── fileinfo.go │ │ ├── hvsock.go │ │ ├── internal/ │ │ │ ├── fs/ │ │ │ │ ├── doc.go │ │ │ │ ├── fs.go │ │ │ │ ├── security.go │ │ │ │ └── zsyscall_windows.go │ │ │ ├── socket/ │ │ │ │ ├── rawaddr.go │ │ │ │ ├── socket.go │ │ │ │ └── zsyscall_windows.go │ │ │ └── stringbuffer/ │ │ │ └── wstring.go │ │ ├── pipe.go │ │ ├── pkg/ │ │ │ └── guid/ │ │ │ ├── guid.go │ │ │ ├── guid_nonwindows.go │ │ │ ├── guid_windows.go │ │ │ └── variant_string.go │ │ ├── privilege.go │ │ ├── reparse.go │ │ ├── sd.go │ │ ├── syscall.go │ │ └── zsyscall_windows.go │ ├── VividCortex/ │ │ └── ewma/ │ │ ├── .gitignore │ │ ├── .whitesource │ │ ├── LICENSE │ │ ├── README.md │ │ ├── codecov.yml │ │ └── ewma.go │ ├── acarl005/ │ │ └── stripansi/ │ │ ├── LICENSE │ │ ├── README.md │ │ └── stripansi.go │ ├── aead/ │ │ └── serpent/ │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── README.md │ │ ├── sbox_ref.go │ │ ├── serpent.go │ │ └── serpent_ref.go │ ├── cespare/ │ │ └── xxhash/ │ │ └── v2/ │ │ ├── LICENSE.txt │ │ ├── README.md │ │ ├── testall.sh │ │ ├── xxhash.go │ │ ├── xxhash_amd64.s │ │ ├── xxhash_arm64.s │ │ ├── xxhash_asm.go │ │ ├── xxhash_other.go │ │ ├── xxhash_safe.go │ │ └── xxhash_unsafe.go │ ├── chzyer/ │ │ └── readline/ │ │ ├── .gitignore │ │ ├── .travis.yml │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── ansi_windows.go │ │ ├── complete.go │ │ ├── complete_helper.go │ │ ├── complete_segment.go │ │ ├── history.go │ │ ├── operation.go │ │ ├── password.go │ │ ├── rawreader_windows.go │ │ ├── readline.go │ │ ├── remote.go │ │ ├── runebuf.go │ │ ├── runes.go │ │ ├── search.go │ │ ├── std.go │ │ ├── std_windows.go │ │ ├── term.go │ │ ├── term_bsd.go │ │ ├── term_linux.go │ │ ├── term_nosyscall6.go │ │ ├── term_unix.go │ │ ├── term_windows.go │ │ ├── terminal.go │ │ ├── utils.go │ │ ├── utils_unix.go │ │ ├── utils_windows.go │ │ ├── vim.go │ │ └── windows_api.go │ ├── clipperhouse/ │ │ └── uax29/ │ │ └── v2/ │ │ ├── LICENSE │ │ └── graphemes/ │ │ ├── README.md │ │ ├── ansi.go │ │ ├── ansi8.go │ │ ├── iterator.go │ │ ├── reader.go │ │ ├── splitfunc.go │ │ └── trie.go │ ├── containerd/ │ │ ├── errdefs/ │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── errors.go │ │ │ ├── pkg/ │ │ │ │ ├── LICENSE │ │ │ │ ├── errhttp/ │ │ │ │ │ └── http.go │ │ │ │ └── internal/ │ │ │ │ └── cause/ │ │ │ │ └── cause.go │ │ │ └── resolve.go │ │ ├── log/ │ │ │ ├── .golangci.yml │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ └── context.go │ │ ├── platforms/ │ │ │ ├── .gitattributes │ │ │ ├── .golangci.yml │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── compare.go │ │ │ ├── cpuinfo.go │ │ │ ├── cpuinfo_linux.go │ │ │ ├── cpuinfo_other.go │ │ │ ├── database.go │ │ │ ├── defaults.go │ │ │ ├── defaults_darwin.go │ │ │ ├── defaults_freebsd.go │ │ │ ├── defaults_unix.go │ │ │ ├── defaults_windows.go │ │ │ ├── errors.go │ │ │ ├── platform_windows_compat.go │ │ │ └── platforms.go │ │ ├── stargz-snapshotter/ │ │ │ └── estargz/ │ │ │ ├── LICENSE │ │ │ ├── build.go │ │ │ ├── errorutil/ │ │ │ │ └── errors.go │ │ │ ├── estargz.go │ │ │ ├── gzip.go │ │ │ ├── testutil.go │ │ │ └── types.go │ │ └── typeurl/ │ │ └── v2/ │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── README.md │ │ ├── doc.go │ │ ├── types.go │ │ └── types_gogo.go │ ├── containers/ │ │ ├── libtrust/ │ │ │ ├── CODE-OF-CONDUCT.md │ │ │ ├── CONTRIBUTING.md │ │ │ ├── LICENSE │ │ │ ├── MAINTAINERS │ │ │ ├── README.md │ │ │ ├── SECURITY.md │ │ │ ├── certificates.go │ │ │ ├── doc.go │ │ │ ├── ec_key.go │ │ │ ├── ec_key_no_openssl.go │ │ │ ├── ec_key_openssl.go │ │ │ ├── filter.go │ │ │ ├── hash.go │ │ │ ├── jsonsign.go │ │ │ ├── key.go │ │ │ ├── key_files.go │ │ │ ├── key_manager.go │ │ │ ├── rsa_key.go │ │ │ └── util.go │ │ ├── luksy/ │ │ │ ├── .cirrus.yml │ │ │ ├── .dockerignore │ │ │ ├── .gitignore │ │ │ ├── LICENSE │ │ │ ├── Makefile │ │ │ ├── OWNERS │ │ │ ├── README.md │ │ │ ├── decrypt.go │ │ │ ├── encrypt.go │ │ │ ├── encryption.go │ │ │ ├── luks.go │ │ │ ├── tune.go │ │ │ ├── v1header.go │ │ │ ├── v2header.go │ │ │ └── v2json.go │ │ └── ocicrypt/ │ │ ├── .gitignore │ │ ├── .golangci.yml │ │ ├── ADOPTERS.md │ │ ├── CODE-OF-CONDUCT.md │ │ ├── LICENSE │ │ ├── MAINTAINERS │ │ ├── Makefile │ │ ├── README.md │ │ ├── SECURITY.md │ │ ├── blockcipher/ │ │ │ ├── blockcipher.go │ │ │ └── blockcipher_aes_ctr.go │ │ ├── config/ │ │ │ ├── config.go │ │ │ ├── constructors.go │ │ │ ├── keyprovider-config/ │ │ │ │ └── config.go │ │ │ └── pkcs11config/ │ │ │ └── config.go │ │ ├── crypto/ │ │ │ └── pkcs11/ │ │ │ ├── common.go │ │ │ ├── pkcs11helpers.go │ │ │ ├── pkcs11helpers_nocgo.go │ │ │ └── utils.go │ │ ├── encryption.go │ │ ├── gpg.go │ │ ├── gpgvault.go │ │ ├── helpers/ │ │ │ └── parse_helpers.go │ │ ├── keywrap/ │ │ │ ├── jwe/ │ │ │ │ └── keywrapper_jwe.go │ │ │ ├── keyprovider/ │ │ │ │ └── keyprovider.go │ │ │ ├── keywrap.go │ │ │ ├── pgp/ │ │ │ │ └── keywrapper_gpg.go │ │ │ ├── pkcs11/ │ │ │ │ └── keywrapper_pkcs11.go │ │ │ └── pkcs7/ │ │ │ └── keywrapper_pkcs7.go │ │ ├── reader.go │ │ ├── spec/ │ │ │ └── spec.go │ │ └── utils/ │ │ ├── delayedreader.go │ │ ├── ioutils.go │ │ ├── keyprovider/ │ │ │ ├── keyprovider.pb.go │ │ │ └── keyprovider.proto │ │ ├── testing.go │ │ └── utils.go │ ├── coreos/ │ │ └── go-systemd/ │ │ └── v22/ │ │ ├── LICENSE │ │ ├── NOTICE │ │ └── dbus/ │ │ ├── dbus.go │ │ ├── methods.go │ │ ├── properties.go │ │ ├── set.go │ │ ├── subscription.go │ │ └── subscription_set.go │ ├── cyberphone/ │ │ └── json-canonicalization/ │ │ ├── LICENSE │ │ └── go/ │ │ └── src/ │ │ └── webpki.org/ │ │ └── jsoncanonicalizer/ │ │ ├── es6numfmt.go │ │ └── jsoncanonicalizer.go │ ├── cyphar/ │ │ └── filepath-securejoin/ │ │ ├── .golangci.yml │ │ ├── CHANGELOG.md │ │ ├── COPYING.md │ │ ├── LICENSE.BSD │ │ ├── LICENSE.MPL-2.0 │ │ ├── README.md │ │ ├── VERSION │ │ ├── codecov.yml │ │ ├── doc.go │ │ ├── internal/ │ │ │ └── consts/ │ │ │ └── consts.go │ │ ├── join.go │ │ ├── pathrs-lite/ │ │ │ ├── README.md │ │ │ ├── doc.go │ │ │ ├── internal/ │ │ │ │ ├── assert/ │ │ │ │ │ └── assert.go │ │ │ │ ├── errors_linux.go │ │ │ │ ├── fd/ │ │ │ │ │ ├── at_linux.go │ │ │ │ │ ├── fd.go │ │ │ │ │ ├── fd_linux.go │ │ │ │ │ ├── mount_linux.go │ │ │ │ │ └── openat2_linux.go │ │ │ │ ├── gocompat/ │ │ │ │ │ ├── README.md │ │ │ │ │ ├── doc.go │ │ │ │ │ ├── gocompat_atomic_go119.go │ │ │ │ │ ├── gocompat_atomic_unsupported.go │ │ │ │ │ ├── gocompat_errors_go120.go │ │ │ │ │ ├── gocompat_errors_unsupported.go │ │ │ │ │ ├── gocompat_generics_go121.go │ │ │ │ │ └── gocompat_generics_unsupported.go │ │ │ │ ├── gopathrs/ │ │ │ │ │ ├── doc.go │ │ │ │ │ ├── lookup_linux.go │ │ │ │ │ ├── mkdir_linux.go │ │ │ │ │ ├── open_linux.go │ │ │ │ │ └── openat2_linux.go │ │ │ │ ├── kernelversion/ │ │ │ │ │ └── kernel_linux.go │ │ │ │ ├── linux/ │ │ │ │ │ ├── doc.go │ │ │ │ │ ├── mount_linux.go │ │ │ │ │ └── openat2_linux.go │ │ │ │ └── procfs/ │ │ │ │ ├── procfs_linux.go │ │ │ │ └── procfs_lookup_linux.go │ │ │ ├── mkdir.go │ │ │ ├── mkdir_libpathrs.go │ │ │ ├── mkdir_purego.go │ │ │ ├── open.go │ │ │ ├── open_libpathrs.go │ │ │ ├── open_purego.go │ │ │ └── procfs/ │ │ │ ├── procfs_libpathrs.go │ │ │ └── procfs_purego.go │ │ └── vfs.go │ ├── davecgh/ │ │ └── go-spew/ │ │ ├── LICENSE │ │ └── spew/ │ │ ├── bypass.go │ │ ├── bypasssafe.go │ │ ├── common.go │ │ ├── config.go │ │ ├── doc.go │ │ ├── dump.go │ │ ├── format.go │ │ └── spew.go │ ├── disiqueira/ │ │ └── gotree/ │ │ └── v3/ │ │ ├── .gitignore │ │ ├── .travis.yml │ │ ├── LICENSE │ │ ├── README.md │ │ ├── _config.yml │ │ └── gotree.go │ ├── distribution/ │ │ └── reference/ │ │ ├── .gitattributes │ │ ├── .gitignore │ │ ├── .golangci.yml │ │ ├── CODE-OF-CONDUCT.md │ │ ├── CONTRIBUTING.md │ │ ├── GOVERNANCE.md │ │ ├── LICENSE │ │ ├── MAINTAINERS │ │ ├── Makefile │ │ ├── README.md │ │ ├── SECURITY.md │ │ ├── helpers.go │ │ ├── normalize.go │ │ ├── reference.go │ │ ├── regexp.go │ │ └── sort.go │ ├── docker/ │ │ ├── distribution/ │ │ │ ├── LICENSE │ │ │ └── registry/ │ │ │ └── api/ │ │ │ ├── errcode/ │ │ │ │ ├── errors.go │ │ │ │ ├── handler.go │ │ │ │ └── register.go │ │ │ └── v2/ │ │ │ ├── descriptors.go │ │ │ ├── doc.go │ │ │ ├── errors.go │ │ │ ├── headerparser.go │ │ │ ├── routes.go │ │ │ └── urls.go │ │ ├── docker/ │ │ │ ├── AUTHORS │ │ │ ├── LICENSE │ │ │ ├── NOTICE │ │ │ └── api/ │ │ │ └── types/ │ │ │ ├── filters/ │ │ │ │ ├── errors.go │ │ │ │ ├── filters_deprecated.go │ │ │ │ └── parse.go │ │ │ ├── registry/ │ │ │ │ ├── authconfig.go │ │ │ │ ├── authenticate.go │ │ │ │ ├── registry.go │ │ │ │ └── search.go │ │ │ └── versions/ │ │ │ └── compare.go │ │ ├── docker-credential-helpers/ │ │ │ ├── LICENSE │ │ │ ├── client/ │ │ │ │ ├── client.go │ │ │ │ └── command.go │ │ │ └── credentials/ │ │ │ ├── credentials.go │ │ │ ├── error.go │ │ │ ├── helper.go │ │ │ └── version.go │ │ ├── go-connections/ │ │ │ ├── LICENSE │ │ │ ├── sockets/ │ │ │ │ ├── inmem_socket.go │ │ │ │ ├── proxy.go │ │ │ │ ├── sockets.go │ │ │ │ ├── sockets_unix.go │ │ │ │ ├── sockets_windows.go │ │ │ │ ├── tcp_socket.go │ │ │ │ ├── unix_socket.go │ │ │ │ ├── unix_socket_unix.go │ │ │ │ └── unix_socket_windows.go │ │ │ └── tlsconfig/ │ │ │ ├── certpool.go │ │ │ └── config.go │ │ └── go-units/ │ │ ├── CONTRIBUTING.md │ │ ├── LICENSE │ │ ├── MAINTAINERS │ │ ├── README.md │ │ ├── circle.yml │ │ ├── duration.go │ │ ├── size.go │ │ └── ulimit.go │ ├── felixge/ │ │ └── httpsnoop/ │ │ ├── .gitignore │ │ ├── LICENSE.txt │ │ ├── Makefile │ │ ├── README.md │ │ ├── capture_metrics.go │ │ ├── docs.go │ │ ├── wrap_generated_gteq_1.8.go │ │ └── wrap_generated_lt_1.8.go │ ├── fsnotify/ │ │ └── fsnotify/ │ │ ├── .cirrus.yml │ │ ├── .gitignore │ │ ├── .mailmap │ │ ├── CHANGELOG.md │ │ ├── CONTRIBUTING.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── backend_fen.go │ │ ├── backend_inotify.go │ │ ├── backend_kqueue.go │ │ ├── backend_other.go │ │ ├── backend_windows.go │ │ ├── fsnotify.go │ │ ├── internal/ │ │ │ ├── darwin.go │ │ │ ├── debug_darwin.go │ │ │ ├── debug_dragonfly.go │ │ │ ├── debug_freebsd.go │ │ │ ├── debug_kqueue.go │ │ │ ├── debug_linux.go │ │ │ ├── debug_netbsd.go │ │ │ ├── debug_openbsd.go │ │ │ ├── debug_solaris.go │ │ │ ├── debug_windows.go │ │ │ ├── freebsd.go │ │ │ ├── internal.go │ │ │ ├── unix.go │ │ │ ├── unix2.go │ │ │ └── windows.go │ │ ├── shared.go │ │ ├── staticcheck.conf │ │ ├── system_bsd.go │ │ └── system_darwin.go │ ├── fsouza/ │ │ └── go-dockerclient/ │ │ ├── .gitattributes │ │ ├── .gitignore │ │ ├── .golangci.yaml │ │ ├── DOCKER-LICENSE │ │ ├── LICENSE │ │ ├── Makefile │ │ ├── README.md │ │ ├── auth.go │ │ ├── change.go │ │ ├── client.go │ │ ├── client_unix.go │ │ ├── client_windows.go │ │ ├── container.go │ │ ├── container_archive.go │ │ ├── container_attach.go │ │ ├── container_changes.go │ │ ├── container_commit.go │ │ ├── container_copy.go │ │ ├── container_create.go │ │ ├── container_export.go │ │ ├── container_inspect.go │ │ ├── container_kill.go │ │ ├── container_list.go │ │ ├── container_logs.go │ │ ├── container_pause.go │ │ ├── container_prune.go │ │ ├── container_remove.go │ │ ├── container_rename.go │ │ ├── container_resize.go │ │ ├── container_restart.go │ │ ├── container_start.go │ │ ├── container_stats.go │ │ ├── container_stop.go │ │ ├── container_top.go │ │ ├── container_unpause.go │ │ ├── container_update.go │ │ ├── container_wait.go │ │ ├── distribution.go │ │ ├── env.go │ │ ├── event.go │ │ ├── exec.go │ │ ├── image.go │ │ ├── misc.go │ │ ├── network.go │ │ ├── plugin.go │ │ ├── registry_auth.go │ │ ├── signal.go │ │ ├── system.go │ │ ├── tar.go │ │ ├── tls.go │ │ └── volume.go │ ├── go-jose/ │ │ └── go-jose/ │ │ └── v4/ │ │ ├── .gitignore │ │ ├── .golangci.yml │ │ ├── .travis.yml │ │ ├── CONTRIBUTING.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── SECURITY.md │ │ ├── asymmetric.go │ │ ├── cipher/ │ │ │ ├── cbc_hmac.go │ │ │ ├── concat_kdf.go │ │ │ ├── ecdh_es.go │ │ │ └── key_wrap.go │ │ ├── crypter.go │ │ ├── doc.go │ │ ├── encoding.go │ │ ├── json/ │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── decode.go │ │ │ ├── encode.go │ │ │ ├── indent.go │ │ │ ├── scanner.go │ │ │ ├── stream.go │ │ │ └── tags.go │ │ ├── jwe.go │ │ ├── jwk.go │ │ ├── jws.go │ │ ├── opaque.go │ │ ├── shared.go │ │ ├── signing.go │ │ └── symmetric.go │ ├── go-logr/ │ │ ├── logr/ │ │ │ ├── .golangci.yaml │ │ │ ├── CHANGELOG.md │ │ │ ├── CONTRIBUTING.md │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── SECURITY.md │ │ │ ├── context.go │ │ │ ├── context_noslog.go │ │ │ ├── context_slog.go │ │ │ ├── discard.go │ │ │ ├── funcr/ │ │ │ │ ├── funcr.go │ │ │ │ └── slogsink.go │ │ │ ├── logr.go │ │ │ ├── sloghandler.go │ │ │ ├── slogr.go │ │ │ └── slogsink.go │ │ └── stdr/ │ │ ├── LICENSE │ │ ├── README.md │ │ └── stdr.go │ ├── godbus/ │ │ └── dbus/ │ │ └── v5/ │ │ ├── .cirrus.yml │ │ ├── .golangci.yml │ │ ├── CONTRIBUTING.md │ │ ├── LICENSE │ │ ├── MAINTAINERS │ │ ├── README.md │ │ ├── SECURITY.md │ │ ├── auth.go │ │ ├── auth_anonymous.go │ │ ├── auth_default_other.go │ │ ├── auth_default_windows.go │ │ ├── auth_external.go │ │ ├── auth_sha1_windows.go │ │ ├── call.go │ │ ├── conn.go │ │ ├── conn_darwin.go │ │ ├── conn_other.go │ │ ├── conn_unix.go │ │ ├── conn_windows.go │ │ ├── dbus.go │ │ ├── decoder.go │ │ ├── default_handler.go │ │ ├── doc.go │ │ ├── encoder.go │ │ ├── escape.go │ │ ├── export.go │ │ ├── match.go │ │ ├── message.go │ │ ├── object.go │ │ ├── sequence.go │ │ ├── sequential_handler.go │ │ ├── server_interfaces.go │ │ ├── sig.go │ │ ├── transport_darwin.go │ │ ├── transport_generic.go │ │ ├── transport_nonce_tcp.go │ │ ├── transport_tcp.go │ │ ├── transport_unix.go │ │ ├── transport_unixcred_dragonfly.go │ │ ├── transport_unixcred_freebsd.go │ │ ├── transport_unixcred_linux.go │ │ ├── transport_unixcred_netbsd.go │ │ ├── transport_unixcred_openbsd.go │ │ ├── transport_zos.go │ │ ├── variant.go │ │ ├── variant_lexer.go │ │ └── variant_parser.go │ ├── gogo/ │ │ └── protobuf/ │ │ ├── AUTHORS │ │ ├── CONTRIBUTORS │ │ ├── LICENSE │ │ └── proto/ │ │ ├── Makefile │ │ ├── clone.go │ │ ├── custom_gogo.go │ │ ├── decode.go │ │ ├── deprecated.go │ │ ├── discard.go │ │ ├── duration.go │ │ ├── duration_gogo.go │ │ ├── encode.go │ │ ├── encode_gogo.go │ │ ├── equal.go │ │ ├── extensions.go │ │ ├── extensions_gogo.go │ │ ├── lib.go │ │ ├── lib_gogo.go │ │ ├── message_set.go │ │ ├── pointer_reflect.go │ │ ├── pointer_reflect_gogo.go │ │ ├── pointer_unsafe.go │ │ ├── pointer_unsafe_gogo.go │ │ ├── properties.go │ │ ├── properties_gogo.go │ │ ├── skip_gogo.go │ │ ├── table_marshal.go │ │ ├── table_marshal_gogo.go │ │ ├── table_merge.go │ │ ├── table_unmarshal.go │ │ ├── table_unmarshal_gogo.go │ │ ├── text.go │ │ ├── text_gogo.go │ │ ├── text_parser.go │ │ ├── timestamp.go │ │ ├── timestamp_gogo.go │ │ ├── wrappers.go │ │ └── wrappers_gogo.go │ ├── golang/ │ │ └── protobuf/ │ │ ├── AUTHORS │ │ ├── CONTRIBUTORS │ │ ├── LICENSE │ │ └── proto/ │ │ ├── buffer.go │ │ ├── defaults.go │ │ ├── deprecated.go │ │ ├── discard.go │ │ ├── extensions.go │ │ ├── properties.go │ │ ├── proto.go │ │ ├── registry.go │ │ ├── text_decode.go │ │ ├── text_encode.go │ │ ├── wire.go │ │ └── wrappers.go │ ├── google/ │ │ ├── go-containerregistry/ │ │ │ ├── LICENSE │ │ │ └── pkg/ │ │ │ ├── name/ │ │ │ │ ├── README.md │ │ │ │ ├── check.go │ │ │ │ ├── digest.go │ │ │ │ ├── doc.go │ │ │ │ ├── errors.go │ │ │ │ ├── options.go │ │ │ │ ├── ref.go │ │ │ │ ├── registry.go │ │ │ │ ├── repository.go │ │ │ │ └── tag.go │ │ │ └── v1/ │ │ │ ├── config.go │ │ │ ├── doc.go │ │ │ ├── hash.go │ │ │ ├── image.go │ │ │ ├── index.go │ │ │ ├── layer.go │ │ │ ├── manifest.go │ │ │ ├── platform.go │ │ │ ├── progress.go │ │ │ ├── types/ │ │ │ │ └── types.go │ │ │ └── zz_deepcopy_generated.go │ │ ├── go-intervals/ │ │ │ ├── LICENSE │ │ │ └── intervalset/ │ │ │ ├── intervalset.go │ │ │ └── intervalset_immutable.go │ │ └── uuid/ │ │ ├── CHANGELOG.md │ │ ├── CONTRIBUTING.md │ │ ├── CONTRIBUTORS │ │ ├── LICENSE │ │ ├── README.md │ │ ├── dce.go │ │ ├── doc.go │ │ ├── hash.go │ │ ├── marshal.go │ │ ├── node.go │ │ ├── node_js.go │ │ ├── node_net.go │ │ ├── null.go │ │ ├── sql.go │ │ ├── time.go │ │ ├── util.go │ │ ├── uuid.go │ │ ├── version1.go │ │ ├── version4.go │ │ ├── version6.go │ │ └── version7.go │ ├── gorilla/ │ │ └── mux/ │ │ ├── .editorconfig │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── Makefile │ │ ├── README.md │ │ ├── doc.go │ │ ├── middleware.go │ │ ├── mux.go │ │ ├── regexp.go │ │ ├── route.go │ │ └── test_helpers.go │ ├── hashicorp/ │ │ ├── errwrap/ │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ └── errwrap.go │ │ └── go-multierror/ │ │ ├── LICENSE │ │ ├── Makefile │ │ ├── README.md │ │ ├── append.go │ │ ├── flatten.go │ │ ├── format.go │ │ ├── group.go │ │ ├── multierror.go │ │ ├── prefix.go │ │ └── sort.go │ ├── inconshreveable/ │ │ └── mousetrap/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── trap_others.go │ │ └── trap_windows.go │ ├── jinzhu/ │ │ └── copier/ │ │ ├── .gitignore │ │ ├── License │ │ ├── README.md │ │ ├── copier.go │ │ └── errors.go │ ├── klauspost/ │ │ ├── compress/ │ │ │ ├── .gitattributes │ │ │ ├── .gitignore │ │ │ ├── .goreleaser.yml │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── SECURITY.md │ │ │ ├── compressible.go │ │ │ ├── flate/ │ │ │ │ ├── deflate.go │ │ │ │ ├── dict_decoder.go │ │ │ │ ├── fast_encoder.go │ │ │ │ ├── huffman_bit_writer.go │ │ │ │ ├── huffman_code.go │ │ │ │ ├── huffman_sortByFreq.go │ │ │ │ ├── huffman_sortByLiteral.go │ │ │ │ ├── inflate.go │ │ │ │ ├── inflate_gen.go │ │ │ │ ├── level1.go │ │ │ │ ├── level2.go │ │ │ │ ├── level3.go │ │ │ │ ├── level4.go │ │ │ │ ├── level5.go │ │ │ │ ├── level6.go │ │ │ │ ├── matchlen_generic.go │ │ │ │ ├── regmask_amd64.go │ │ │ │ ├── regmask_other.go │ │ │ │ ├── stateless.go │ │ │ │ └── token.go │ │ │ ├── fse/ │ │ │ │ ├── README.md │ │ │ │ ├── bitreader.go │ │ │ │ ├── bitwriter.go │ │ │ │ ├── bytereader.go │ │ │ │ ├── compress.go │ │ │ │ ├── decompress.go │ │ │ │ └── fse.go │ │ │ ├── gen.sh │ │ │ ├── huff0/ │ │ │ │ ├── .gitignore │ │ │ │ ├── README.md │ │ │ │ ├── bitreader.go │ │ │ │ ├── bitwriter.go │ │ │ │ ├── compress.go │ │ │ │ ├── decompress.go │ │ │ │ ├── decompress_amd64.go │ │ │ │ ├── decompress_amd64.s │ │ │ │ ├── decompress_generic.go │ │ │ │ └── huff0.go │ │ │ ├── internal/ │ │ │ │ ├── cpuinfo/ │ │ │ │ │ ├── cpuinfo.go │ │ │ │ │ ├── cpuinfo_amd64.go │ │ │ │ │ └── cpuinfo_amd64.s │ │ │ │ ├── le/ │ │ │ │ │ ├── le.go │ │ │ │ │ ├── unsafe_disabled.go │ │ │ │ │ └── unsafe_enabled.go │ │ │ │ └── snapref/ │ │ │ │ ├── LICENSE │ │ │ │ ├── decode.go │ │ │ │ ├── decode_other.go │ │ │ │ ├── encode.go │ │ │ │ ├── encode_other.go │ │ │ │ └── snappy.go │ │ │ ├── s2sx.mod │ │ │ ├── s2sx.sum │ │ │ └── zstd/ │ │ │ ├── README.md │ │ │ ├── bitreader.go │ │ │ ├── bitwriter.go │ │ │ ├── blockdec.go │ │ │ ├── blockenc.go │ │ │ ├── blocktype_string.go │ │ │ ├── bytebuf.go │ │ │ ├── bytereader.go │ │ │ ├── decodeheader.go │ │ │ ├── decoder.go │ │ │ ├── decoder_options.go │ │ │ ├── dict.go │ │ │ ├── enc_base.go │ │ │ ├── enc_best.go │ │ │ ├── enc_better.go │ │ │ ├── enc_dfast.go │ │ │ ├── enc_fast.go │ │ │ ├── encoder.go │ │ │ ├── encoder_options.go │ │ │ ├── framedec.go │ │ │ ├── frameenc.go │ │ │ ├── fse_decoder.go │ │ │ ├── fse_decoder_amd64.go │ │ │ ├── fse_decoder_amd64.s │ │ │ ├── fse_decoder_generic.go │ │ │ ├── fse_encoder.go │ │ │ ├── fse_predefined.go │ │ │ ├── hash.go │ │ │ ├── history.go │ │ │ ├── internal/ │ │ │ │ └── xxhash/ │ │ │ │ ├── LICENSE.txt │ │ │ │ ├── README.md │ │ │ │ ├── xxhash.go │ │ │ │ ├── xxhash_amd64.s │ │ │ │ ├── xxhash_arm64.s │ │ │ │ ├── xxhash_asm.go │ │ │ │ ├── xxhash_other.go │ │ │ │ └── xxhash_safe.go │ │ │ ├── matchlen_amd64.go │ │ │ ├── matchlen_amd64.s │ │ │ ├── matchlen_generic.go │ │ │ ├── seqdec.go │ │ │ ├── seqdec_amd64.go │ │ │ ├── seqdec_amd64.s │ │ │ ├── seqdec_generic.go │ │ │ ├── seqenc.go │ │ │ ├── simple_go124.go │ │ │ ├── snappy.go │ │ │ ├── zip.go │ │ │ └── zstd.go │ │ └── pgzip/ │ │ ├── .gitignore │ │ ├── .travis.yml │ │ ├── GO_LICENSE │ │ ├── LICENSE │ │ ├── README.md │ │ ├── gunzip.go │ │ └── gzip.go │ ├── mattn/ │ │ ├── go-runewidth/ │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── benchstat.txt │ │ │ ├── new.txt │ │ │ ├── old.txt │ │ │ ├── runewidth.go │ │ │ ├── runewidth_appengine.go │ │ │ ├── runewidth_js.go │ │ │ ├── runewidth_posix.go │ │ │ ├── runewidth_table.go │ │ │ └── runewidth_windows.go │ │ ├── go-shellwords/ │ │ │ ├── .travis.yml │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── go.test.sh │ │ │ ├── shellwords.go │ │ │ ├── util_posix.go │ │ │ └── util_windows.go │ │ └── go-sqlite3/ │ │ ├── .codecov.yml │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── README.md │ │ ├── backup.go │ │ ├── callback.go │ │ ├── convert.go │ │ ├── doc.go │ │ ├── error.go │ │ ├── sqlite3-binding.c │ │ ├── sqlite3-binding.h │ │ ├── sqlite3.go │ │ ├── sqlite3_context.go │ │ ├── sqlite3_func_crypt.go │ │ ├── sqlite3_go18.go │ │ ├── sqlite3_libsqlite3.go │ │ ├── sqlite3_load_extension.go │ │ ├── sqlite3_load_extension_omit.go │ │ ├── sqlite3_opt_allow_uri_authority.go │ │ ├── sqlite3_opt_app_armor.go │ │ ├── sqlite3_opt_column_metadata.go │ │ ├── sqlite3_opt_foreign_keys.go │ │ ├── sqlite3_opt_fts5.go │ │ ├── sqlite3_opt_icu.go │ │ ├── sqlite3_opt_introspect.go │ │ ├── sqlite3_opt_math_functions.go │ │ ├── sqlite3_opt_os_trace.go │ │ ├── sqlite3_opt_percentile.go │ │ ├── sqlite3_opt_preupdate.go │ │ ├── sqlite3_opt_preupdate_hook.go │ │ ├── sqlite3_opt_preupdate_omit.go │ │ ├── sqlite3_opt_secure_delete.go │ │ ├── sqlite3_opt_secure_delete_fast.go │ │ ├── sqlite3_opt_serialize.go │ │ ├── sqlite3_opt_serialize_omit.go │ │ ├── sqlite3_opt_stat4.go │ │ ├── sqlite3_opt_unlock_notify.c │ │ ├── sqlite3_opt_unlock_notify.go │ │ ├── sqlite3_opt_userauth.go │ │ ├── sqlite3_opt_userauth_omit.go │ │ ├── sqlite3_opt_vacuum_full.go │ │ ├── sqlite3_opt_vacuum_incr.go │ │ ├── sqlite3_opt_vtable.go │ │ ├── sqlite3_other.go │ │ ├── sqlite3_solaris.go │ │ ├── sqlite3_trace.go │ │ ├── sqlite3_type.go │ │ ├── sqlite3_usleep_windows.go │ │ ├── sqlite3_windows.go │ │ ├── sqlite3ext.h │ │ └── static_mock.go │ ├── miekg/ │ │ └── pkcs11/ │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── Makefile.release │ │ ├── README.md │ │ ├── error.go │ │ ├── params.go │ │ ├── pkcs11.go │ │ ├── pkcs11.h │ │ ├── pkcs11f.h │ │ ├── pkcs11go.h │ │ ├── pkcs11t.h │ │ ├── release.go │ │ ├── softhsm.conf │ │ ├── softhsm2.conf │ │ ├── types.go │ │ ├── vendor.go │ │ └── zconst.go │ ├── mistifyio/ │ │ └── go-zfs/ │ │ └── v4/ │ │ ├── .envrc │ │ ├── .gitignore │ │ ├── .golangci.yml │ │ ├── .yamllint │ │ ├── CHANGELOG.md │ │ ├── CONTRIBUTING.md │ │ ├── LICENSE │ │ ├── Makefile │ │ ├── README.md │ │ ├── Vagrantfile │ │ ├── error.go │ │ ├── lint.mk │ │ ├── rules.mk │ │ ├── shell.nix │ │ ├── utils.go │ │ ├── utils_notsolaris.go │ │ ├── utils_solaris.go │ │ ├── zfs.go │ │ └── zpool.go │ ├── moby/ │ │ ├── buildkit/ │ │ │ ├── AUTHORS │ │ │ ├── LICENSE │ │ │ ├── frontend/ │ │ │ │ └── dockerfile/ │ │ │ │ ├── command/ │ │ │ │ │ └── command.go │ │ │ │ ├── parser/ │ │ │ │ │ ├── directives.go │ │ │ │ │ ├── errors.go │ │ │ │ │ ├── line_parsers.go │ │ │ │ │ ├── parser.go │ │ │ │ │ └── split_command.go │ │ │ │ └── shell/ │ │ │ │ ├── envVarTest │ │ │ │ ├── equal_env_unix.go │ │ │ │ ├── equal_env_windows.go │ │ │ │ ├── lex.go │ │ │ │ └── wordsTest │ │ │ └── util/ │ │ │ └── stack/ │ │ │ ├── compress.go │ │ │ ├── stack.go │ │ │ ├── stack.pb.go │ │ │ ├── stack.proto │ │ │ └── stack_vtproto.pb.go │ │ ├── docker-image-spec/ │ │ │ ├── LICENSE │ │ │ └── specs-go/ │ │ │ └── v1/ │ │ │ └── image.go │ │ ├── go-archive/ │ │ │ ├── .gitattributes │ │ │ ├── .gitignore │ │ │ ├── .golangci.yml │ │ │ ├── LICENSE │ │ │ ├── archive.go │ │ │ ├── archive_linux.go │ │ │ ├── archive_other.go │ │ │ ├── archive_unix.go │ │ │ ├── archive_windows.go │ │ │ ├── changes.go │ │ │ ├── changes_linux.go │ │ │ ├── changes_other.go │ │ │ ├── changes_unix.go │ │ │ ├── changes_windows.go │ │ │ ├── compression/ │ │ │ │ ├── compression.go │ │ │ │ └── compression_detect.go │ │ │ ├── copy.go │ │ │ ├── copy_unix.go │ │ │ ├── copy_windows.go │ │ │ ├── dev_freebsd.go │ │ │ ├── dev_unix.go │ │ │ ├── diff.go │ │ │ ├── diff_unix.go │ │ │ ├── diff_windows.go │ │ │ ├── path.go │ │ │ ├── path_unix.go │ │ │ ├── path_windows.go │ │ │ ├── tarheader/ │ │ │ │ ├── tarheader.go │ │ │ │ ├── tarheader_unix.go │ │ │ │ └── tarheader_windows.go │ │ │ ├── time.go │ │ │ ├── time_nonwindows.go │ │ │ ├── time_windows.go │ │ │ ├── whiteouts.go │ │ │ ├── wrap.go │ │ │ ├── xattr_supported.go │ │ │ ├── xattr_supported_linux.go │ │ │ ├── xattr_supported_unix.go │ │ │ └── xattr_unsupported.go │ │ ├── moby/ │ │ │ ├── api/ │ │ │ │ ├── LICENSE │ │ │ │ ├── pkg/ │ │ │ │ │ └── stdcopy/ │ │ │ │ │ └── stdcopy.go │ │ │ │ └── types/ │ │ │ │ ├── blkiodev/ │ │ │ │ │ └── blkio.go │ │ │ │ ├── build/ │ │ │ │ │ ├── build.go │ │ │ │ │ ├── cache.go │ │ │ │ │ └── disk_usage.go │ │ │ │ ├── checkpoint/ │ │ │ │ │ ├── create_request.go │ │ │ │ │ └── list.go │ │ │ │ ├── common/ │ │ │ │ │ ├── error_response.go │ │ │ │ │ ├── error_response_ext.go │ │ │ │ │ └── id_response.go │ │ │ │ ├── container/ │ │ │ │ │ ├── change_type.go │ │ │ │ │ ├── change_types.go │ │ │ │ │ ├── commit.go │ │ │ │ │ ├── config.go │ │ │ │ │ ├── container.go │ │ │ │ │ ├── create_request.go │ │ │ │ │ ├── create_response.go │ │ │ │ │ ├── disk_usage.go │ │ │ │ │ ├── errors.go │ │ │ │ │ ├── exec.go │ │ │ │ │ ├── exec_create_request.go │ │ │ │ │ ├── exec_start_request.go │ │ │ │ │ ├── filesystem_change.go │ │ │ │ │ ├── health.go │ │ │ │ │ ├── hostconfig.go │ │ │ │ │ ├── hostconfig_unix.go │ │ │ │ │ ├── hostconfig_windows.go │ │ │ │ │ ├── network_settings.go │ │ │ │ │ ├── port_summary.go │ │ │ │ │ ├── state.go │ │ │ │ │ ├── stats.go │ │ │ │ │ ├── top_response.go │ │ │ │ │ ├── update_response.go │ │ │ │ │ ├── wait_exit_error.go │ │ │ │ │ ├── wait_response.go │ │ │ │ │ └── waitcondition.go │ │ │ │ ├── events/ │ │ │ │ │ └── events.go │ │ │ │ ├── image/ │ │ │ │ │ ├── build_identity.go │ │ │ │ │ ├── delete_response.go │ │ │ │ │ ├── disk_usage.go │ │ │ │ │ ├── history_response_item.go │ │ │ │ │ ├── identity.go │ │ │ │ │ ├── image.go │ │ │ │ │ ├── image_inspect.go │ │ │ │ │ ├── manifest.go │ │ │ │ │ ├── pull_identity.go │ │ │ │ │ ├── signature_identity.go │ │ │ │ │ ├── signature_timestamp.go │ │ │ │ │ ├── signer_identity.go │ │ │ │ │ └── summary.go │ │ │ │ ├── jsonstream/ │ │ │ │ │ ├── json_error.go │ │ │ │ │ ├── message.go │ │ │ │ │ └── progress.go │ │ │ │ ├── mount/ │ │ │ │ │ └── mount.go │ │ │ │ ├── network/ │ │ │ │ │ ├── config_reference.go │ │ │ │ │ ├── connect_request.go │ │ │ │ │ ├── create_response.go │ │ │ │ │ ├── disconnect_request.go │ │ │ │ │ ├── endpoint.go │ │ │ │ │ ├── endpoint_resource.go │ │ │ │ │ ├── hwaddr.go │ │ │ │ │ ├── inspect.go │ │ │ │ │ ├── ipam.go │ │ │ │ │ ├── ipam_status.go │ │ │ │ │ ├── network.go │ │ │ │ │ ├── network_types.go │ │ │ │ │ ├── peer_info.go │ │ │ │ │ ├── port.go │ │ │ │ │ ├── service_info.go │ │ │ │ │ ├── status.go │ │ │ │ │ ├── subnet_status.go │ │ │ │ │ ├── summary.go │ │ │ │ │ └── task.go │ │ │ │ ├── plugin/ │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── capability.go │ │ │ │ │ ├── device.go │ │ │ │ │ ├── env.go │ │ │ │ │ ├── mount.go │ │ │ │ │ ├── plugin.go │ │ │ │ │ └── plugin_responses.go │ │ │ │ ├── registry/ │ │ │ │ │ ├── auth_response.go │ │ │ │ │ ├── authconfig.go │ │ │ │ │ ├── registry.go │ │ │ │ │ └── search.go │ │ │ │ ├── storage/ │ │ │ │ │ ├── driver_data.go │ │ │ │ │ ├── root_f_s_storage.go │ │ │ │ │ ├── root_f_s_storage_snapshot.go │ │ │ │ │ └── storage.go │ │ │ │ ├── swarm/ │ │ │ │ │ ├── common.go │ │ │ │ │ ├── config.go │ │ │ │ │ ├── container.go │ │ │ │ │ ├── network.go │ │ │ │ │ ├── node.go │ │ │ │ │ ├── runtime.go │ │ │ │ │ ├── secret.go │ │ │ │ │ ├── service.go │ │ │ │ │ ├── service_create_response.go │ │ │ │ │ ├── service_update_response.go │ │ │ │ │ ├── swarm.go │ │ │ │ │ └── task.go │ │ │ │ ├── system/ │ │ │ │ │ ├── disk_usage.go │ │ │ │ │ ├── info.go │ │ │ │ │ ├── runtime.go │ │ │ │ │ └── version_response.go │ │ │ │ ├── types.go │ │ │ │ └── volume/ │ │ │ │ ├── cluster_volume.go │ │ │ │ ├── create_request.go │ │ │ │ ├── disk_usage.go │ │ │ │ ├── list_response.go │ │ │ │ ├── prune_report.go │ │ │ │ └── volume.go │ │ │ ├── client/ │ │ │ │ ├── LICENSE │ │ │ │ ├── README.md │ │ │ │ ├── auth.go │ │ │ │ ├── build_cancel.go │ │ │ │ ├── build_prune.go │ │ │ │ ├── checkpoint_create.go │ │ │ │ ├── checkpoint_list.go │ │ │ │ ├── checkpoint_remove.go │ │ │ │ ├── client.go │ │ │ │ ├── client_interfaces.go │ │ │ │ ├── client_options.go │ │ │ │ ├── client_responsehook.go │ │ │ │ ├── client_unix.go │ │ │ │ ├── client_windows.go │ │ │ │ ├── config_create.go │ │ │ │ ├── config_inspect.go │ │ │ │ ├── config_list.go │ │ │ │ ├── config_remove.go │ │ │ │ ├── config_update.go │ │ │ │ ├── container_attach.go │ │ │ │ ├── container_commit.go │ │ │ │ ├── container_copy.go │ │ │ │ ├── container_create.go │ │ │ │ ├── container_create_opts.go │ │ │ │ ├── container_diff.go │ │ │ │ ├── container_diff_opts.go │ │ │ │ ├── container_exec.go │ │ │ │ ├── container_export.go │ │ │ │ ├── container_inspect.go │ │ │ │ ├── container_kill.go │ │ │ │ ├── container_list.go │ │ │ │ ├── container_logs.go │ │ │ │ ├── container_pause.go │ │ │ │ ├── container_prune.go │ │ │ │ ├── container_remove.go │ │ │ │ ├── container_rename.go │ │ │ │ ├── container_resize.go │ │ │ │ ├── container_restart.go │ │ │ │ ├── container_start.go │ │ │ │ ├── container_stats.go │ │ │ │ ├── container_stop.go │ │ │ │ ├── container_top.go │ │ │ │ ├── container_unpause.go │ │ │ │ ├── container_update.go │ │ │ │ ├── container_wait.go │ │ │ │ ├── distribution_inspect.go │ │ │ │ ├── envvars.go │ │ │ │ ├── errors.go │ │ │ │ ├── filters.go │ │ │ │ ├── hijack.go │ │ │ │ ├── image_build.go │ │ │ │ ├── image_build_opts.go │ │ │ │ ├── image_history.go │ │ │ │ ├── image_history_opts.go │ │ │ │ ├── image_import.go │ │ │ │ ├── image_import_opts.go │ │ │ │ ├── image_inspect.go │ │ │ │ ├── image_inspect_opts.go │ │ │ │ ├── image_list.go │ │ │ │ ├── image_list_opts.go │ │ │ │ ├── image_load.go │ │ │ │ ├── image_load_opts.go │ │ │ │ ├── image_prune.go │ │ │ │ ├── image_pull.go │ │ │ │ ├── image_pull_opts.go │ │ │ │ ├── image_push.go │ │ │ │ ├── image_push_opts.go │ │ │ │ ├── image_remove.go │ │ │ │ ├── image_remove_opts.go │ │ │ │ ├── image_save.go │ │ │ │ ├── image_save_opts.go │ │ │ │ ├── image_search.go │ │ │ │ ├── image_search_opts.go │ │ │ │ ├── image_tag.go │ │ │ │ ├── internal/ │ │ │ │ │ ├── json-stream.go │ │ │ │ │ ├── jsonmessages.go │ │ │ │ │ └── timestamp/ │ │ │ │ │ └── timestamp.go │ │ │ │ ├── login.go │ │ │ │ ├── network_connect.go │ │ │ │ ├── network_create.go │ │ │ │ ├── network_disconnect.go │ │ │ │ ├── network_inspect.go │ │ │ │ ├── network_inspect_opts.go │ │ │ │ ├── network_list.go │ │ │ │ ├── network_list_opts.go │ │ │ │ ├── network_prune.go │ │ │ │ ├── network_remove.go │ │ │ │ ├── node_inspect.go │ │ │ │ ├── node_list.go │ │ │ │ ├── node_remove.go │ │ │ │ ├── node_update.go │ │ │ │ ├── ping.go │ │ │ │ ├── pkg/ │ │ │ │ │ ├── jsonmessage/ │ │ │ │ │ │ └── jsonmessage.go │ │ │ │ │ └── versions/ │ │ │ │ │ └── compare.go │ │ │ │ ├── plugin_create.go │ │ │ │ ├── plugin_disable.go │ │ │ │ ├── plugin_enable.go │ │ │ │ ├── plugin_inspect.go │ │ │ │ ├── plugin_install.go │ │ │ │ ├── plugin_list.go │ │ │ │ ├── plugin_push.go │ │ │ │ ├── plugin_remove.go │ │ │ │ ├── plugin_set.go │ │ │ │ ├── plugin_upgrade.go │ │ │ │ ├── request.go │ │ │ │ ├── secret_create.go │ │ │ │ ├── secret_inspect.go │ │ │ │ ├── secret_list.go │ │ │ │ ├── secret_remove.go │ │ │ │ ├── secret_update.go │ │ │ │ ├── service_create.go │ │ │ │ ├── service_inspect.go │ │ │ │ ├── service_list.go │ │ │ │ ├── service_logs.go │ │ │ │ ├── service_remove.go │ │ │ │ ├── service_update.go │ │ │ │ ├── swarm_get_unlock_key.go │ │ │ │ ├── swarm_init.go │ │ │ │ ├── swarm_inspect.go │ │ │ │ ├── swarm_join.go │ │ │ │ ├── swarm_leave.go │ │ │ │ ├── swarm_unlock.go │ │ │ │ ├── swarm_update.go │ │ │ │ ├── system_disk_usage.go │ │ │ │ ├── system_events.go │ │ │ │ ├── system_info.go │ │ │ │ ├── task_inspect.go │ │ │ │ ├── task_list.go │ │ │ │ ├── task_logs.go │ │ │ │ ├── utils.go │ │ │ │ ├── version.go │ │ │ │ ├── volume_create.go │ │ │ │ ├── volume_inspect.go │ │ │ │ ├── volume_list.go │ │ │ │ ├── volume_prune.go │ │ │ │ ├── volume_remove.go │ │ │ │ └── volume_update.go │ │ │ └── v2/ │ │ │ ├── AUTHORS │ │ │ ├── LICENSE │ │ │ ├── NOTICE │ │ │ └── pkg/ │ │ │ └── homedir/ │ │ │ ├── homedir.go │ │ │ ├── homedir_linux.go │ │ │ └── homedir_others.go │ │ ├── patternmatcher/ │ │ │ ├── LICENSE │ │ │ ├── NOTICE │ │ │ └── patternmatcher.go │ │ └── sys/ │ │ ├── capability/ │ │ │ ├── CHANGELOG.md │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── capability.go │ │ │ ├── capability_linux.go │ │ │ ├── capability_noop.go │ │ │ ├── enum.go │ │ │ ├── enum_gen.go │ │ │ └── syscall_linux.go │ │ ├── mountinfo/ │ │ │ ├── LICENSE │ │ │ ├── doc.go │ │ │ ├── mounted_linux.go │ │ │ ├── mounted_unix.go │ │ │ ├── mountinfo.go │ │ │ ├── mountinfo_bsd.go │ │ │ ├── mountinfo_filters.go │ │ │ ├── mountinfo_freebsdlike.go │ │ │ ├── mountinfo_linux.go │ │ │ ├── mountinfo_openbsd.go │ │ │ ├── mountinfo_unsupported.go │ │ │ └── mountinfo_windows.go │ │ ├── sequential/ │ │ │ ├── LICENSE │ │ │ ├── doc.go │ │ │ ├── sequential_unix.go │ │ │ └── sequential_windows.go │ │ ├── user/ │ │ │ ├── LICENSE │ │ │ ├── idtools.go │ │ │ ├── idtools_unix.go │ │ │ ├── idtools_windows.go │ │ │ ├── lookup_unix.go │ │ │ ├── user.go │ │ │ └── user_fuzzer.go │ │ └── userns/ │ │ ├── LICENSE │ │ ├── userns.go │ │ ├── userns_linux.go │ │ ├── userns_linux_fuzzer.go │ │ └── userns_unsupported.go │ ├── modern-go/ │ │ └── concurrent/ │ │ ├── .gitignore │ │ ├── .travis.yml │ │ ├── LICENSE │ │ ├── README.md │ │ ├── executor.go │ │ ├── go_above_19.go │ │ ├── go_below_19.go │ │ ├── log.go │ │ ├── test.sh │ │ └── unbounded_executor.go │ ├── opencontainers/ │ │ ├── cgroups/ │ │ │ ├── .golangci-extra.yml │ │ │ ├── .golangci.yml │ │ │ ├── CODEOWNERS │ │ │ ├── CONTRIBUTING.md │ │ │ ├── GOVERNANCE.md │ │ │ ├── LICENSE │ │ │ ├── MAINTAINERS │ │ │ ├── MAINTAINERS_GUIDE.md │ │ │ ├── README.md │ │ │ ├── RELEASES.md │ │ │ ├── cgroups.go │ │ │ ├── config_blkio_device.go │ │ │ ├── config_hugepages.go │ │ │ ├── config_ifprio_map.go │ │ │ ├── config_linux.go │ │ │ ├── config_rdma.go │ │ │ ├── config_unsupported.go │ │ │ ├── devices/ │ │ │ │ └── config/ │ │ │ │ ├── device.go │ │ │ │ └── mknod_unix.go │ │ │ ├── file.go │ │ │ ├── fs2/ │ │ │ │ ├── cpu.go │ │ │ │ ├── cpuset.go │ │ │ │ ├── create.go │ │ │ │ ├── defaultpath.go │ │ │ │ ├── freezer.go │ │ │ │ ├── fs2.go │ │ │ │ ├── hugetlb.go │ │ │ │ ├── io.go │ │ │ │ ├── memory.go │ │ │ │ ├── misc.go │ │ │ │ ├── pids.go │ │ │ │ └── psi.go │ │ │ ├── fscommon/ │ │ │ │ ├── rdma.go │ │ │ │ └── utils.go │ │ │ ├── getallpids.go │ │ │ ├── internal/ │ │ │ │ └── path/ │ │ │ │ └── path.go │ │ │ ├── stats.go │ │ │ ├── utils.go │ │ │ └── v1_utils.go │ │ ├── go-digest/ │ │ │ ├── .mailmap │ │ │ ├── .pullapprove.yml │ │ │ ├── .travis.yml │ │ │ ├── CONTRIBUTING.md │ │ │ ├── LICENSE │ │ │ ├── LICENSE.docs │ │ │ ├── MAINTAINERS │ │ │ ├── README.md │ │ │ ├── algorithm.go │ │ │ ├── digest.go │ │ │ ├── digester.go │ │ │ ├── doc.go │ │ │ └── verifiers.go │ │ ├── image-spec/ │ │ │ ├── LICENSE │ │ │ └── specs-go/ │ │ │ ├── v1/ │ │ │ │ ├── annotations.go │ │ │ │ ├── config.go │ │ │ │ ├── descriptor.go │ │ │ │ ├── index.go │ │ │ │ ├── layout.go │ │ │ │ ├── manifest.go │ │ │ │ └── mediatype.go │ │ │ ├── version.go │ │ │ └── versioned.go │ │ ├── runc/ │ │ │ ├── LICENSE │ │ │ ├── NOTICE │ │ │ ├── internal/ │ │ │ │ ├── linux/ │ │ │ │ │ ├── doc.go │ │ │ │ │ ├── eintr.go │ │ │ │ │ └── linux.go │ │ │ │ └── pathrs/ │ │ │ │ ├── doc.go │ │ │ │ ├── mkdirall.go │ │ │ │ ├── mkdirall_pathrslite.go │ │ │ │ ├── path.go │ │ │ │ ├── procfs_pathrslite.go │ │ │ │ ├── retry.go │ │ │ │ └── root_pathrslite.go │ │ │ └── libcontainer/ │ │ │ ├── apparmor/ │ │ │ │ ├── apparmor.go │ │ │ │ ├── apparmor_linux.go │ │ │ │ └── apparmor_unsupported.go │ │ │ └── devices/ │ │ │ ├── device_deprecated.go │ │ │ └── device_unix.go │ │ ├── runtime-spec/ │ │ │ ├── LICENSE │ │ │ └── specs-go/ │ │ │ ├── config.go │ │ │ ├── state.go │ │ │ └── version.go │ │ ├── runtime-tools/ │ │ │ ├── LICENSE │ │ │ ├── generate/ │ │ │ │ ├── config.go │ │ │ │ ├── generate.go │ │ │ │ └── seccomp/ │ │ │ │ ├── consts.go │ │ │ │ ├── parse_action.go │ │ │ │ ├── parse_architecture.go │ │ │ │ ├── parse_arguments.go │ │ │ │ ├── parse_remove.go │ │ │ │ ├── seccomp_default.go │ │ │ │ ├── seccomp_default_linux.go │ │ │ │ ├── seccomp_default_unsupported.go │ │ │ │ └── syscall_compare.go │ │ │ └── validate/ │ │ │ └── capabilities/ │ │ │ ├── lastcap.go │ │ │ └── validate.go │ │ └── selinux/ │ │ ├── LICENSE │ │ ├── go-selinux/ │ │ │ ├── doc.go │ │ │ ├── label/ │ │ │ │ ├── label.go │ │ │ │ ├── label_linux.go │ │ │ │ └── label_stub.go │ │ │ ├── selinux.go │ │ │ ├── selinux_linux.go │ │ │ ├── selinux_stub.go │ │ │ └── xattrs_linux.go │ │ └── pkg/ │ │ └── pwalkdir/ │ │ ├── README.md │ │ └── pwalkdir.go │ ├── openshift/ │ │ └── imagebuilder/ │ │ ├── .gitignore │ │ ├── .travis.yml │ │ ├── LICENSE │ │ ├── Makefile │ │ ├── OWNERS │ │ ├── README.md │ │ ├── builder.go │ │ ├── constants.go │ │ ├── dispatchers.go │ │ ├── doc.go │ │ ├── dockerclient/ │ │ │ ├── archive.go │ │ │ ├── client.go │ │ │ ├── copyinfo.go │ │ │ └── directory.go │ │ ├── dockerfile/ │ │ │ ├── NOTICE │ │ │ ├── command/ │ │ │ │ └── command.go │ │ │ └── parser/ │ │ │ ├── line_parsers.go │ │ │ ├── parser.go │ │ │ └── split_command.go │ │ ├── evaluator.go │ │ ├── imagebuilder.spec │ │ ├── imageprogress/ │ │ │ ├── progress.go │ │ │ ├── pull.go │ │ │ └── push.go │ │ ├── internal/ │ │ │ └── env.go │ │ ├── internals.go │ │ ├── shell_parser.go │ │ ├── signal/ │ │ │ ├── README.md │ │ │ ├── signal.go │ │ │ └── signals.go │ │ └── strslice/ │ │ └── strslice.go │ ├── pkg/ │ │ └── errors/ │ │ ├── .gitignore │ │ ├── .travis.yml │ │ ├── LICENSE │ │ ├── Makefile │ │ ├── README.md │ │ ├── appveyor.yml │ │ ├── errors.go │ │ ├── go113.go │ │ └── stack.go │ ├── planetscale/ │ │ └── vtprotobuf/ │ │ ├── LICENSE │ │ └── protohelpers/ │ │ └── protohelpers.go │ ├── pmezard/ │ │ └── go-difflib/ │ │ ├── LICENSE │ │ └── difflib/ │ │ └── difflib.go │ ├── proglottis/ │ │ └── gpgme/ │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── README.md │ │ ├── data.go │ │ ├── go_gpgme.c │ │ ├── go_gpgme.h │ │ ├── gpgme.go │ │ ├── unset_agent_info.go │ │ └── unset_agent_info_windows.go │ ├── seccomp/ │ │ └── libseccomp-golang/ │ │ ├── .gitignore │ │ ├── .golangci.yml │ │ ├── CHANGELOG │ │ ├── CONTRIBUTING.md │ │ ├── LICENSE │ │ ├── Makefile │ │ ├── README.md │ │ ├── SECURITY.md │ │ ├── seccomp.go │ │ └── seccomp_internal.go │ ├── secure-systems-lab/ │ │ └── go-securesystemslib/ │ │ ├── LICENSE │ │ └── encrypted/ │ │ └── encrypted.go │ ├── sigstore/ │ │ ├── fulcio/ │ │ │ ├── COPYRIGHT.txt │ │ │ ├── LICENSE │ │ │ └── pkg/ │ │ │ └── certificate/ │ │ │ ├── doc.go │ │ │ └── extensions.go │ │ ├── protobuf-specs/ │ │ │ ├── COPYRIGHT.txt │ │ │ ├── LICENSE │ │ │ └── gen/ │ │ │ └── pb-go/ │ │ │ └── common/ │ │ │ └── v1/ │ │ │ └── sigstore_common.pb.go │ │ └── sigstore/ │ │ ├── COPYRIGHT.txt │ │ ├── LICENSE │ │ └── pkg/ │ │ ├── cryptoutils/ │ │ │ ├── certificate.go │ │ │ ├── doc.go │ │ │ ├── generic.go │ │ │ ├── password.go │ │ │ ├── privatekey.go │ │ │ ├── publickey.go │ │ │ ├── safestring.go │ │ │ └── sans.go │ │ └── signature/ │ │ ├── algorithm_registry.go │ │ ├── doc.go │ │ ├── ecdsa.go │ │ ├── ed25519.go │ │ ├── ed25519ph.go │ │ ├── message.go │ │ ├── options/ │ │ │ ├── context.go │ │ │ ├── digest.go │ │ │ ├── doc.go │ │ │ ├── keyversion.go │ │ │ ├── loadoptions.go │ │ │ ├── noop.go │ │ │ ├── rand.go │ │ │ ├── remoteverification.go │ │ │ ├── rpcauth.go │ │ │ └── signeropts.go │ │ ├── options.go │ │ ├── payload/ │ │ │ ├── doc.go │ │ │ └── payload.go │ │ ├── publickey.go │ │ ├── rsapkcs1v15.go │ │ ├── rsapss.go │ │ ├── signer.go │ │ ├── signerverifier.go │ │ ├── util.go │ │ └── verifier.go │ ├── smallstep/ │ │ └── pkcs7/ │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── Makefile │ │ ├── README.md │ │ ├── ber.go │ │ ├── decrypt.go │ │ ├── encrypt.go │ │ ├── internal/ │ │ │ └── legacy/ │ │ │ └── x509/ │ │ │ ├── debug.go │ │ │ ├── doc.go │ │ │ ├── oid.go │ │ │ ├── parser.go │ │ │ ├── pkcs1.go │ │ │ ├── verify.go │ │ │ └── x509.go │ │ ├── pkcs7.go │ │ ├── sign.go │ │ └── verify.go │ ├── spf13/ │ │ ├── cobra/ │ │ │ ├── .gitignore │ │ │ ├── .golangci.yml │ │ │ ├── .mailmap │ │ │ ├── CONDUCT.md │ │ │ ├── CONTRIBUTING.md │ │ │ ├── LICENSE.txt │ │ │ ├── MAINTAINERS │ │ │ ├── Makefile │ │ │ ├── README.md │ │ │ ├── SECURITY.md │ │ │ ├── active_help.go │ │ │ ├── args.go │ │ │ ├── bash_completions.go │ │ │ ├── bash_completionsV2.go │ │ │ ├── cobra.go │ │ │ ├── command.go │ │ │ ├── command_notwin.go │ │ │ ├── command_win.go │ │ │ ├── completions.go │ │ │ ├── fish_completions.go │ │ │ ├── flag_groups.go │ │ │ ├── powershell_completions.go │ │ │ ├── shell_completions.go │ │ │ └── zsh_completions.go │ │ └── pflag/ │ │ ├── .editorconfig │ │ ├── .gitignore │ │ ├── .golangci.yaml │ │ ├── .travis.yml │ │ ├── LICENSE │ │ ├── README.md │ │ ├── bool.go │ │ ├── bool_func.go │ │ ├── bool_slice.go │ │ ├── bytes.go │ │ ├── count.go │ │ ├── duration.go │ │ ├── duration_slice.go │ │ ├── errors.go │ │ ├── flag.go │ │ ├── float32.go │ │ ├── float32_slice.go │ │ ├── float64.go │ │ ├── float64_slice.go │ │ ├── func.go │ │ ├── golangflag.go │ │ ├── int.go │ │ ├── int16.go │ │ ├── int32.go │ │ ├── int32_slice.go │ │ ├── int64.go │ │ ├── int64_slice.go │ │ ├── int8.go │ │ ├── int_slice.go │ │ ├── ip.go │ │ ├── ip_slice.go │ │ ├── ipmask.go │ │ ├── ipnet.go │ │ ├── ipnet_slice.go │ │ ├── string.go │ │ ├── string_array.go │ │ ├── string_slice.go │ │ ├── string_to_int.go │ │ ├── string_to_int64.go │ │ ├── string_to_string.go │ │ ├── text.go │ │ ├── time.go │ │ ├── uint.go │ │ ├── uint16.go │ │ ├── uint32.go │ │ ├── uint64.go │ │ ├── uint8.go │ │ └── uint_slice.go │ ├── stefanberger/ │ │ └── go-pkcs11uri/ │ │ ├── .gitignore │ │ ├── .travis.yml │ │ ├── LICENSE │ │ ├── Makefile │ │ ├── README.md │ │ └── pkcs11uri.go │ ├── stretchr/ │ │ └── testify/ │ │ ├── LICENSE │ │ ├── assert/ │ │ │ ├── assertion_compare.go │ │ │ ├── assertion_format.go │ │ │ ├── assertion_format.go.tmpl │ │ │ ├── assertion_forward.go │ │ │ ├── assertion_forward.go.tmpl │ │ │ ├── assertion_order.go │ │ │ ├── assertions.go │ │ │ ├── doc.go │ │ │ ├── errors.go │ │ │ ├── forward_assertions.go │ │ │ ├── http_assertions.go │ │ │ └── yaml/ │ │ │ ├── yaml_custom.go │ │ │ ├── yaml_default.go │ │ │ └── yaml_fail.go │ │ └── require/ │ │ ├── doc.go │ │ ├── forward_requirements.go │ │ ├── require.go │ │ ├── require.go.tmpl │ │ ├── require_forward.go │ │ ├── require_forward.go.tmpl │ │ └── requirements.go │ ├── sylabs/ │ │ └── sif/ │ │ └── v2/ │ │ ├── LICENSE.md │ │ └── pkg/ │ │ └── sif/ │ │ ├── add.go │ │ ├── arch.go │ │ ├── buffer.go │ │ ├── create.go │ │ ├── delete.go │ │ ├── descriptor.go │ │ ├── descriptor_input.go │ │ ├── load.go │ │ ├── select.go │ │ ├── set.go │ │ └── sif.go │ ├── tchap/ │ │ └── go-patricia/ │ │ └── v2/ │ │ ├── AUTHORS │ │ ├── LICENSE │ │ └── patricia/ │ │ ├── children.go │ │ └── patricia.go │ ├── ulikunitz/ │ │ └── xz/ │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── README.md │ │ ├── SECURITY.md │ │ ├── TODO.md │ │ ├── bits.go │ │ ├── crc.go │ │ ├── format.go │ │ ├── fox-check-none.xz │ │ ├── fox.xz │ │ ├── internal/ │ │ │ ├── hash/ │ │ │ │ ├── cyclic_poly.go │ │ │ │ ├── doc.go │ │ │ │ ├── rabin_karp.go │ │ │ │ └── roller.go │ │ │ └── xlog/ │ │ │ └── xlog.go │ │ ├── lzma/ │ │ │ ├── bintree.go │ │ │ ├── bitops.go │ │ │ ├── breader.go │ │ │ ├── buffer.go │ │ │ ├── bytewriter.go │ │ │ ├── decoder.go │ │ │ ├── decoderdict.go │ │ │ ├── directcodec.go │ │ │ ├── distcodec.go │ │ │ ├── encoder.go │ │ │ ├── encoderdict.go │ │ │ ├── fox.lzma │ │ │ ├── hashtable.go │ │ │ ├── header.go │ │ │ ├── header2.go │ │ │ ├── lengthcodec.go │ │ │ ├── literalcodec.go │ │ │ ├── matchalgorithm.go │ │ │ ├── operation.go │ │ │ ├── prob.go │ │ │ ├── properties.go │ │ │ ├── rangecodec.go │ │ │ ├── reader.go │ │ │ ├── reader2.go │ │ │ ├── state.go │ │ │ ├── treecodecs.go │ │ │ ├── writer.go │ │ │ └── writer2.go │ │ ├── lzmafilter.go │ │ ├── make-docs │ │ ├── none-check.go │ │ ├── reader.go │ │ └── writer.go │ ├── vbatts/ │ │ └── tar-split/ │ │ ├── LICENSE │ │ ├── archive/ │ │ │ └── tar/ │ │ │ ├── common.go │ │ │ ├── format.go │ │ │ ├── reader.go │ │ │ ├── stat_actime1.go │ │ │ ├── stat_actime2.go │ │ │ ├── stat_unix.go │ │ │ ├── strconv.go │ │ │ └── writer.go │ │ └── tar/ │ │ ├── asm/ │ │ │ ├── README.md │ │ │ ├── assemble.go │ │ │ ├── disassemble.go │ │ │ ├── doc.go │ │ │ └── iterate.go │ │ └── storage/ │ │ ├── doc.go │ │ ├── entry.go │ │ ├── getter.go │ │ └── packer.go │ └── vbauerster/ │ └── mpb/ │ └── v8/ │ ├── .gitignore │ ├── CONTRIBUTING │ ├── README.md │ ├── UNLICENSE │ ├── bar.go │ ├── bar_filler.go │ ├── bar_filler_bar.go │ ├── bar_filler_nop.go │ ├── bar_filler_spinner.go │ ├── bar_heap.go │ ├── bar_option.go │ ├── container_option.go │ ├── cwriter/ │ │ ├── doc.go │ │ ├── util_bsd.go │ │ ├── util_linux.go │ │ ├── util_solaris.go │ │ ├── util_zos.go │ │ ├── writer.go │ │ ├── writer_posix.go │ │ └── writer_windows.go │ ├── decor/ │ │ ├── any.go │ │ ├── counters.go │ │ ├── decorator.go │ │ ├── doc.go │ │ ├── elapsed.go │ │ ├── eta.go │ │ ├── meta.go │ │ ├── moving_average.go │ │ ├── name.go │ │ ├── on_abort.go │ │ ├── on_abort_or_on_complete.go │ │ ├── on_complete.go │ │ ├── on_complete_or_on_abort.go │ │ ├── on_condition.go │ │ ├── percentage.go │ │ ├── size_type.go │ │ ├── sizeb1000_string.go │ │ ├── sizeb1024_string.go │ │ ├── speed.go │ │ └── spinner.go │ ├── doc.go │ ├── heap_manager.go │ ├── internal/ │ │ ├── percentage.go │ │ └── width.go │ ├── progress.go │ ├── proxyreader.go │ └── proxywriter.go ├── go.etcd.io/ │ └── bbolt/ │ ├── .gitignore │ ├── .go-version │ ├── LICENSE │ ├── Makefile │ ├── OWNERS │ ├── README.md │ ├── bolt_aix.go │ ├── bolt_android.go │ ├── bolt_linux.go │ ├── bolt_openbsd.go │ ├── bolt_solaris.go │ ├── bolt_unix.go │ ├── bolt_windows.go │ ├── boltsync_unix.go │ ├── bucket.go │ ├── compact.go │ ├── cursor.go │ ├── db.go │ ├── doc.go │ ├── errors/ │ │ └── errors.go │ ├── errors.go │ ├── internal/ │ │ ├── common/ │ │ │ ├── bolt_386.go │ │ │ ├── bolt_amd64.go │ │ │ ├── bolt_arm.go │ │ │ ├── bolt_arm64.go │ │ │ ├── bolt_loong64.go │ │ │ ├── bolt_mips64x.go │ │ │ ├── bolt_mipsx.go │ │ │ ├── bolt_ppc.go │ │ │ ├── bolt_ppc64.go │ │ │ ├── bolt_ppc64le.go │ │ │ ├── bolt_riscv64.go │ │ │ ├── bolt_s390x.go │ │ │ ├── bucket.go │ │ │ ├── inode.go │ │ │ ├── meta.go │ │ │ ├── page.go │ │ │ ├── types.go │ │ │ ├── unsafe.go │ │ │ ├── utils.go │ │ │ └── verify.go │ │ └── freelist/ │ │ ├── array.go │ │ ├── freelist.go │ │ ├── hashmap.go │ │ └── shared.go │ ├── logger.go │ ├── mlock_unix.go │ ├── mlock_windows.go │ ├── node.go │ ├── tx.go │ └── tx_check.go ├── go.opentelemetry.io/ │ ├── auto/ │ │ └── sdk/ │ │ ├── CONTRIBUTING.md │ │ ├── LICENSE │ │ ├── VERSIONING.md │ │ ├── doc.go │ │ ├── internal/ │ │ │ └── telemetry/ │ │ │ ├── attr.go │ │ │ ├── doc.go │ │ │ ├── id.go │ │ │ ├── number.go │ │ │ ├── resource.go │ │ │ ├── scope.go │ │ │ ├── span.go │ │ │ ├── status.go │ │ │ ├── traces.go │ │ │ └── value.go │ │ ├── limit.go │ │ ├── span.go │ │ ├── tracer.go │ │ └── tracer_provider.go │ ├── contrib/ │ │ └── instrumentation/ │ │ └── net/ │ │ └── http/ │ │ └── otelhttp/ │ │ ├── LICENSE │ │ ├── common.go │ │ ├── config.go │ │ ├── doc.go │ │ ├── handler.go │ │ ├── internal/ │ │ │ ├── request/ │ │ │ │ ├── body_wrapper.go │ │ │ │ ├── gen.go │ │ │ │ └── resp_writer_wrapper.go │ │ │ └── semconv/ │ │ │ ├── client.go │ │ │ ├── gen.go │ │ │ ├── server.go │ │ │ └── util.go │ │ ├── labeler.go │ │ ├── start_time_context.go │ │ ├── transport.go │ │ └── version.go │ └── otel/ │ ├── .clomonitor.yml │ ├── .codespellignore │ ├── .codespellrc │ ├── .gitattributes │ ├── .gitignore │ ├── .golangci.yml │ ├── .lycheeignore │ ├── .markdownlint.yaml │ ├── CHANGELOG.md │ ├── CODEOWNERS │ ├── CONTRIBUTING.md │ ├── LICENSE │ ├── Makefile │ ├── README.md │ ├── RELEASING.md │ ├── SECURITY-INSIGHTS.yml │ ├── VERSIONING.md │ ├── attribute/ │ │ ├── README.md │ │ ├── doc.go │ │ ├── encoder.go │ │ ├── filter.go │ │ ├── hash.go │ │ ├── internal/ │ │ │ ├── attribute.go │ │ │ └── xxhash/ │ │ │ └── xxhash.go │ │ ├── iterator.go │ │ ├── key.go │ │ ├── kv.go │ │ ├── rawhelpers.go │ │ ├── set.go │ │ ├── type_string.go │ │ └── value.go │ ├── baggage/ │ │ ├── README.md │ │ ├── baggage.go │ │ ├── context.go │ │ └── doc.go │ ├── codes/ │ │ ├── README.md │ │ ├── codes.go │ │ └── doc.go │ ├── dependencies.Dockerfile │ ├── doc.go │ ├── error_handler.go │ ├── handler.go │ ├── internal/ │ │ ├── baggage/ │ │ │ ├── baggage.go │ │ │ └── context.go │ │ └── global/ │ │ ├── handler.go │ │ ├── instruments.go │ │ ├── internal_logging.go │ │ ├── meter.go │ │ ├── propagator.go │ │ ├── state.go │ │ └── trace.go │ ├── internal_logging.go │ ├── metric/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── asyncfloat64.go │ │ ├── asyncint64.go │ │ ├── config.go │ │ ├── doc.go │ │ ├── embedded/ │ │ │ ├── README.md │ │ │ └── embedded.go │ │ ├── instrument.go │ │ ├── meter.go │ │ ├── noop/ │ │ │ ├── README.md │ │ │ └── noop.go │ │ ├── syncfloat64.go │ │ └── syncint64.go │ ├── metric.go │ ├── propagation/ │ │ ├── README.md │ │ ├── baggage.go │ │ ├── doc.go │ │ ├── propagation.go │ │ └── trace_context.go │ ├── propagation.go │ ├── renovate.json │ ├── requirements.txt │ ├── semconv/ │ │ ├── v1.37.0/ │ │ │ ├── MIGRATION.md │ │ │ ├── README.md │ │ │ ├── attribute_group.go │ │ │ ├── doc.go │ │ │ ├── error_type.go │ │ │ ├── exception.go │ │ │ └── schema.go │ │ └── v1.39.0/ │ │ ├── MIGRATION.md │ │ ├── README.md │ │ ├── attribute_group.go │ │ ├── doc.go │ │ ├── error_type.go │ │ ├── exception.go │ │ ├── httpconv/ │ │ │ └── metric.go │ │ └── schema.go │ ├── trace/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── auto.go │ │ ├── config.go │ │ ├── context.go │ │ ├── doc.go │ │ ├── embedded/ │ │ │ ├── README.md │ │ │ └── embedded.go │ │ ├── hex.go │ │ ├── internal/ │ │ │ └── telemetry/ │ │ │ ├── attr.go │ │ │ ├── doc.go │ │ │ ├── id.go │ │ │ ├── number.go │ │ │ ├── resource.go │ │ │ ├── scope.go │ │ │ ├── span.go │ │ │ ├── status.go │ │ │ ├── traces.go │ │ │ └── value.go │ │ ├── nonrecording.go │ │ ├── noop/ │ │ │ ├── README.md │ │ │ └── noop.go │ │ ├── noop.go │ │ ├── provider.go │ │ ├── span.go │ │ ├── trace.go │ │ ├── tracer.go │ │ └── tracestate.go │ ├── trace.go │ ├── verify_released_changelog.sh │ ├── version.go │ └── versions.yaml ├── go.podman.io/ │ ├── common/ │ │ ├── LICENSE │ │ ├── internal/ │ │ │ ├── attributedstring/ │ │ │ │ └── slice.go │ │ │ └── deepcopy.go │ │ ├── libimage/ │ │ │ ├── copier.go │ │ │ ├── define/ │ │ │ │ ├── manifests.go │ │ │ │ ├── platform.go │ │ │ │ └── search.go │ │ │ ├── disk_usage.go │ │ │ ├── events.go │ │ │ ├── filter/ │ │ │ │ └── filter.go │ │ │ ├── filters.go │ │ │ ├── history.go │ │ │ ├── image.go │ │ │ ├── image_config.go │ │ │ ├── image_tree.go │ │ │ ├── import.go │ │ │ ├── inspect.go │ │ │ ├── layer_tree.go │ │ │ ├── load.go │ │ │ ├── manifest_list.go │ │ │ ├── manifests/ │ │ │ │ ├── copy.go │ │ │ │ └── manifests.go │ │ │ ├── normalize.go │ │ │ ├── oci.go │ │ │ ├── platform/ │ │ │ │ └── platform.go │ │ │ ├── platform.go │ │ │ ├── pull.go │ │ │ ├── push.go │ │ │ ├── runtime.go │ │ │ ├── save.go │ │ │ ├── search.go │ │ │ └── types.go │ │ ├── libnetwork/ │ │ │ ├── etchosts/ │ │ │ │ ├── hosts.go │ │ │ │ ├── ip.go │ │ │ │ └── util.go │ │ │ ├── internal/ │ │ │ │ ├── rootlessnetns/ │ │ │ │ │ ├── netns_freebsd.go │ │ │ │ │ └── netns_linux.go │ │ │ │ └── util/ │ │ │ │ ├── bridge.go │ │ │ │ ├── create.go │ │ │ │ ├── interface.go │ │ │ │ ├── interfaces.go │ │ │ │ ├── ip.go │ │ │ │ ├── parse.go │ │ │ │ ├── util.go │ │ │ │ └── validate.go │ │ │ ├── netavark/ │ │ │ │ ├── config.go │ │ │ │ ├── const.go │ │ │ │ ├── exec.go │ │ │ │ ├── ipam.go │ │ │ │ ├── network.go │ │ │ │ └── run.go │ │ │ ├── network/ │ │ │ │ ├── interface.go │ │ │ │ ├── interface_freebsd.go │ │ │ │ └── interface_linux.go │ │ │ ├── pasta/ │ │ │ │ ├── pasta_linux.go │ │ │ │ └── types.go │ │ │ ├── resolvconf/ │ │ │ │ ├── resolv.go │ │ │ │ └── resolvconf.go │ │ │ ├── slirp4netns/ │ │ │ │ ├── const.go │ │ │ │ ├── const_linux.go │ │ │ │ └── slirp4netns.go │ │ │ ├── types/ │ │ │ │ ├── const.go │ │ │ │ ├── define.go │ │ │ │ └── network.go │ │ │ └── util/ │ │ │ ├── filters.go │ │ │ ├── ip.go │ │ │ └── ip_calc.go │ │ ├── pkg/ │ │ │ ├── apparmor/ │ │ │ │ ├── apparmor.go │ │ │ │ ├── apparmor_linux.go │ │ │ │ ├── apparmor_linux_template.go │ │ │ │ ├── apparmor_unsupported.go │ │ │ │ └── internal/ │ │ │ │ └── supported/ │ │ │ │ └── supported.go │ │ │ ├── auth/ │ │ │ │ ├── auth.go │ │ │ │ └── cli.go │ │ │ ├── capabilities/ │ │ │ │ └── capabilities.go │ │ │ ├── cgroups/ │ │ │ │ ├── blkio_linux.go │ │ │ │ ├── cgroups_linux.go │ │ │ │ ├── cgroups_unsupported.go │ │ │ │ ├── cpu_linux.go │ │ │ │ ├── memory_linux.go │ │ │ │ ├── pids_linux.go │ │ │ │ ├── systemd_linux.go │ │ │ │ └── utils_linux.go │ │ │ ├── chown/ │ │ │ │ ├── chown.go │ │ │ │ ├── chown_unix.go │ │ │ │ └── chown_windows.go │ │ │ ├── completion/ │ │ │ │ └── completion.go │ │ │ ├── config/ │ │ │ │ ├── config.go │ │ │ │ ├── config_bsd.go │ │ │ │ ├── config_darwin.go │ │ │ │ ├── config_linux.go │ │ │ │ ├── config_local.go │ │ │ │ ├── config_remote.go │ │ │ │ ├── config_unix.go │ │ │ │ ├── config_unsupported.go │ │ │ │ ├── config_windows.go │ │ │ │ ├── connections.go │ │ │ │ ├── containers.conf │ │ │ │ ├── containers.conf-freebsd │ │ │ │ ├── db_backend.go │ │ │ │ ├── default.go │ │ │ │ ├── default_bsd.go │ │ │ │ ├── default_common.go │ │ │ │ ├── default_darwin.go │ │ │ │ ├── default_linux.go │ │ │ │ ├── default_unix_notdarwin.go │ │ │ │ ├── default_unsupported.go │ │ │ │ ├── default_windows.go │ │ │ │ ├── modules.go │ │ │ │ ├── new.go │ │ │ │ ├── nosystemd.go │ │ │ │ ├── pod_exit_policy.go │ │ │ │ ├── pull_policy.go │ │ │ │ └── systemd.go │ │ │ ├── download/ │ │ │ │ └── download.go │ │ │ ├── filters/ │ │ │ │ └── filters.go │ │ │ ├── formats/ │ │ │ │ ├── formats.go │ │ │ │ └── templates.go │ │ │ ├── hooks/ │ │ │ │ ├── 0.1.0/ │ │ │ │ │ └── hook.go │ │ │ │ ├── 1.0.0/ │ │ │ │ │ ├── hook.go │ │ │ │ │ └── when.go │ │ │ │ ├── README.md │ │ │ │ ├── exec/ │ │ │ │ │ ├── exec.go │ │ │ │ │ └── runtimeconfigfilter.go │ │ │ │ ├── hooks.go │ │ │ │ ├── monitor.go │ │ │ │ ├── read.go │ │ │ │ └── version.go │ │ │ ├── machine/ │ │ │ │ └── machine.go │ │ │ ├── manifests/ │ │ │ │ ├── errors.go │ │ │ │ └── manifests.go │ │ │ ├── netns/ │ │ │ │ └── netns_linux.go │ │ │ ├── parse/ │ │ │ │ ├── parse.go │ │ │ │ └── parse_unix.go │ │ │ ├── password/ │ │ │ │ ├── password_supported.go │ │ │ │ └── password_windows.go │ │ │ ├── retry/ │ │ │ │ ├── retry.go │ │ │ │ ├── retry_linux.go │ │ │ │ └── retry_unsupported.go │ │ │ ├── rootlessport/ │ │ │ │ └── rootlessport_linux.go │ │ │ ├── seccomp/ │ │ │ │ ├── conversion.go │ │ │ │ ├── default_linux.go │ │ │ │ ├── errno_list.go │ │ │ │ ├── filter_linux.go │ │ │ │ ├── seccomp.json │ │ │ │ ├── seccomp_linux.go │ │ │ │ ├── seccomp_unsupported.go │ │ │ │ ├── supported.go │ │ │ │ ├── types.go │ │ │ │ └── validate_linux.go │ │ │ ├── servicereaper/ │ │ │ │ └── service.go │ │ │ ├── signal/ │ │ │ │ ├── signal_common.go │ │ │ │ ├── signal_linux.go │ │ │ │ ├── signal_linux_mipsx.go │ │ │ │ └── signal_unsupported.go │ │ │ ├── subscriptions/ │ │ │ │ ├── mounts.conf │ │ │ │ └── subscriptions.go │ │ │ ├── supplemented/ │ │ │ │ ├── errors.go │ │ │ │ └── supplemented.go │ │ │ ├── systemd/ │ │ │ │ ├── systemd_linux.go │ │ │ │ └── systemd_unsupported.go │ │ │ ├── timetype/ │ │ │ │ └── timestamp.go │ │ │ ├── umask/ │ │ │ │ ├── umask.go │ │ │ │ ├── umask_unix.go │ │ │ │ └── umask_unsupported.go │ │ │ ├── util/ │ │ │ │ └── util.go │ │ │ └── version/ │ │ │ └── version.go │ │ └── version/ │ │ └── version.go │ ├── image/ │ │ └── v5/ │ │ ├── LICENSE │ │ ├── copy/ │ │ │ ├── blob.go │ │ │ ├── compression.go │ │ │ ├── copy.go │ │ │ ├── digesting_reader.go │ │ │ ├── encryption.go │ │ │ ├── manifest.go │ │ │ ├── multiple.go │ │ │ ├── progress_bars.go │ │ │ ├── progress_channel.go │ │ │ ├── sign.go │ │ │ └── single.go │ │ ├── directory/ │ │ │ ├── directory_dest.go │ │ │ ├── directory_src.go │ │ │ ├── directory_transport.go │ │ │ ├── explicitfilepath/ │ │ │ │ └── path.go │ │ │ └── version.go │ │ ├── docker/ │ │ │ ├── archive/ │ │ │ │ ├── dest.go │ │ │ │ ├── reader.go │ │ │ │ ├── src.go │ │ │ │ ├── transport.go │ │ │ │ └── writer.go │ │ │ ├── body_reader.go │ │ │ ├── cache.go │ │ │ ├── daemon/ │ │ │ │ ├── client.go │ │ │ │ ├── daemon_dest.go │ │ │ │ ├── daemon_src.go │ │ │ │ └── daemon_transport.go │ │ │ ├── distribution_error.go │ │ │ ├── docker_client.go │ │ │ ├── docker_image.go │ │ │ ├── docker_image_dest.go │ │ │ ├── docker_image_src.go │ │ │ ├── docker_transport.go │ │ │ ├── errors.go │ │ │ ├── internal/ │ │ │ │ └── tarfile/ │ │ │ │ ├── dest.go │ │ │ │ ├── reader.go │ │ │ │ ├── src.go │ │ │ │ ├── types.go │ │ │ │ └── writer.go │ │ │ ├── paths_common.go │ │ │ ├── paths_freebsd.go │ │ │ ├── policyconfiguration/ │ │ │ │ └── naming.go │ │ │ ├── reference/ │ │ │ │ ├── README.md │ │ │ │ ├── helpers.go │ │ │ │ ├── normalize.go │ │ │ │ ├── reference.go │ │ │ │ ├── regexp-additions.go │ │ │ │ └── regexp.go │ │ │ ├── registries_d.go │ │ │ └── wwwauthenticate.go │ │ ├── image/ │ │ │ ├── docker_schema2.go │ │ │ ├── sourced.go │ │ │ └── unparsed.go │ │ ├── internal/ │ │ │ ├── blobinfocache/ │ │ │ │ ├── blobinfocache.go │ │ │ │ └── types.go │ │ │ ├── digests/ │ │ │ │ └── digests.go │ │ │ ├── image/ │ │ │ │ ├── digest_validation.go │ │ │ │ ├── docker_list.go │ │ │ │ ├── docker_schema1.go │ │ │ │ ├── docker_schema2.go │ │ │ │ ├── manifest.go │ │ │ │ ├── memory.go │ │ │ │ ├── oci.go │ │ │ │ ├── oci_index.go │ │ │ │ ├── sourced.go │ │ │ │ └── unparsed.go │ │ │ ├── imagedestination/ │ │ │ │ ├── impl/ │ │ │ │ │ ├── compat.go │ │ │ │ │ ├── helpers.go │ │ │ │ │ └── properties.go │ │ │ │ ├── stubs/ │ │ │ │ │ ├── original_oci_config.go │ │ │ │ │ ├── put_blob_partial.go │ │ │ │ │ ├── signatures.go │ │ │ │ │ └── stubs.go │ │ │ │ └── wrapper.go │ │ │ ├── imagesource/ │ │ │ │ ├── impl/ │ │ │ │ │ ├── compat.go │ │ │ │ │ ├── layer_infos.go │ │ │ │ │ ├── properties.go │ │ │ │ │ └── signatures.go │ │ │ │ ├── stubs/ │ │ │ │ │ ├── get_blob_at.go │ │ │ │ │ └── stubs.go │ │ │ │ └── wrapper.go │ │ │ ├── iolimits/ │ │ │ │ └── iolimits.go │ │ │ ├── manifest/ │ │ │ │ ├── common.go │ │ │ │ ├── docker_schema2.go │ │ │ │ ├── docker_schema2_list.go │ │ │ │ ├── errors.go │ │ │ │ ├── list.go │ │ │ │ ├── manifest.go │ │ │ │ └── oci_index.go │ │ │ ├── multierr/ │ │ │ │ └── multierr.go │ │ │ ├── pkg/ │ │ │ │ └── platform/ │ │ │ │ └── platform_matcher.go │ │ │ ├── private/ │ │ │ │ └── private.go │ │ │ ├── putblobdigest/ │ │ │ │ └── put_blob_digest.go │ │ │ ├── rootless/ │ │ │ │ └── rootless.go │ │ │ ├── set/ │ │ │ │ └── set.go │ │ │ ├── signature/ │ │ │ │ ├── signature.go │ │ │ │ ├── sigstore.go │ │ │ │ └── simple.go │ │ │ ├── signer/ │ │ │ │ └── signer.go │ │ │ ├── streamdigest/ │ │ │ │ └── stream_digest.go │ │ │ ├── tmpdir/ │ │ │ │ └── tmpdir.go │ │ │ ├── unparsedimage/ │ │ │ │ └── wrapper.go │ │ │ ├── uploadreader/ │ │ │ │ └── upload_reader.go │ │ │ └── useragent/ │ │ │ └── useragent.go │ │ ├── manifest/ │ │ │ ├── common.go │ │ │ ├── docker_schema1.go │ │ │ ├── docker_schema2.go │ │ │ ├── docker_schema2_list.go │ │ │ ├── list.go │ │ │ ├── manifest.go │ │ │ ├── oci.go │ │ │ └── oci_index.go │ │ ├── oci/ │ │ │ ├── archive/ │ │ │ │ ├── oci_dest.go │ │ │ │ ├── oci_src.go │ │ │ │ └── oci_transport.go │ │ │ ├── internal/ │ │ │ │ └── oci_util.go │ │ │ └── layout/ │ │ │ ├── oci_delete.go │ │ │ ├── oci_dest.go │ │ │ ├── oci_src.go │ │ │ ├── oci_transport.go │ │ │ └── reader.go │ │ ├── openshift/ │ │ │ ├── openshift-copies.go │ │ │ ├── openshift.go │ │ │ ├── openshift_dest.go │ │ │ ├── openshift_src.go │ │ │ └── openshift_transport.go │ │ ├── pkg/ │ │ │ ├── blobcache/ │ │ │ │ ├── blobcache.go │ │ │ │ ├── dest.go │ │ │ │ └── src.go │ │ │ ├── blobinfocache/ │ │ │ │ ├── default.go │ │ │ │ ├── internal/ │ │ │ │ │ └── prioritize/ │ │ │ │ │ └── prioritize.go │ │ │ │ ├── memory/ │ │ │ │ │ └── memory.go │ │ │ │ ├── none/ │ │ │ │ │ └── none.go │ │ │ │ └── sqlite/ │ │ │ │ └── sqlite.go │ │ │ ├── compression/ │ │ │ │ ├── compression.go │ │ │ │ ├── internal/ │ │ │ │ │ └── types.go │ │ │ │ ├── types/ │ │ │ │ │ └── types.go │ │ │ │ └── zstd.go │ │ │ ├── docker/ │ │ │ │ └── config/ │ │ │ │ └── config.go │ │ │ ├── shortnames/ │ │ │ │ └── shortnames.go │ │ │ ├── strslice/ │ │ │ │ ├── README.md │ │ │ │ └── strslice.go │ │ │ ├── sysregistriesv2/ │ │ │ │ ├── paths_common.go │ │ │ │ ├── paths_freebsd.go │ │ │ │ ├── shortnames.go │ │ │ │ └── system_registries_v2.go │ │ │ └── tlsclientconfig/ │ │ │ └── tlsclientconfig.go │ │ ├── sif/ │ │ │ ├── load.go │ │ │ ├── src.go │ │ │ └── transport.go │ │ ├── signature/ │ │ │ ├── docker.go │ │ │ ├── fulcio_cert.go │ │ │ ├── internal/ │ │ │ │ ├── errors.go │ │ │ │ ├── json.go │ │ │ │ ├── rekor_api_types.go │ │ │ │ ├── rekor_set.go │ │ │ │ ├── sequoia/ │ │ │ │ │ ├── gosequoia.c │ │ │ │ │ ├── gosequoia.h │ │ │ │ │ ├── gosequoiafuncs.h │ │ │ │ │ ├── sequoia.go │ │ │ │ │ └── sequoia.h │ │ │ │ └── sigstore_payload.go │ │ │ ├── mechanism.go │ │ │ ├── mechanism_gpgme.go │ │ │ ├── mechanism_gpgme_only.go │ │ │ ├── mechanism_openpgp.go │ │ │ ├── mechanism_sequoia.go │ │ │ ├── pki_cert.go │ │ │ ├── policy_config.go │ │ │ ├── policy_config_sigstore.go │ │ │ ├── policy_eval.go │ │ │ ├── policy_eval_baselayer.go │ │ │ ├── policy_eval_signedby.go │ │ │ ├── policy_eval_sigstore.go │ │ │ ├── policy_eval_simple.go │ │ │ ├── policy_paths_common.go │ │ │ ├── policy_paths_freebsd.go │ │ │ ├── policy_reference_match.go │ │ │ ├── policy_types.go │ │ │ ├── signer/ │ │ │ │ └── signer.go │ │ │ ├── sigstore/ │ │ │ │ ├── copied.go │ │ │ │ ├── generate.go │ │ │ │ ├── internal/ │ │ │ │ │ └── signer.go │ │ │ │ └── signer.go │ │ │ ├── simple.go │ │ │ └── simplesigning/ │ │ │ └── signer.go │ │ ├── storage/ │ │ │ ├── storage_dest.go │ │ │ ├── storage_image.go │ │ │ ├── storage_reference.go │ │ │ ├── storage_src.go │ │ │ └── storage_transport.go │ │ ├── tarball/ │ │ │ ├── doc.go │ │ │ ├── tarball_reference.go │ │ │ ├── tarball_src.go │ │ │ └── tarball_transport.go │ │ ├── transports/ │ │ │ ├── alltransports/ │ │ │ │ ├── alltransports.go │ │ │ │ ├── docker_daemon.go │ │ │ │ ├── docker_daemon_stub.go │ │ │ │ ├── storage.go │ │ │ │ └── storage_stub.go │ │ │ ├── stub.go │ │ │ └── transports.go │ │ ├── types/ │ │ │ └── types.go │ │ └── version/ │ │ └── version.go │ └── storage/ │ ├── .dockerignore │ ├── .gitignore │ ├── .golangci.yml │ ├── .mailmap │ ├── AUTHORS │ ├── LICENSE │ ├── Makefile │ ├── NOTICE │ ├── OWNERS │ ├── README.md │ ├── VERSION │ ├── check.go │ ├── containers.go │ ├── deprecated.go │ ├── drivers/ │ │ ├── btrfs/ │ │ │ ├── btrfs.go │ │ │ ├── dummy_unsupported.go │ │ │ └── version.go │ │ ├── chown.go │ │ ├── chown_darwin.go │ │ ├── chown_unix.go │ │ ├── chown_windows.go │ │ ├── chroot_unix.go │ │ ├── chroot_windows.go │ │ ├── copy/ │ │ │ ├── copy_linux.go │ │ │ └── copy_unsupported.go │ │ ├── counter.go │ │ ├── driver.go │ │ ├── driver_darwin.go │ │ ├── driver_freebsd.go │ │ ├── driver_linux.go │ │ ├── driver_solaris.go │ │ ├── driver_unsupported.go │ │ ├── fsdiff.go │ │ ├── jsoniter.go │ │ ├── overlay/ │ │ │ ├── check.go │ │ │ ├── check_116.go │ │ │ ├── composefs.go │ │ │ ├── jsoniter.go │ │ │ ├── mount.go │ │ │ ├── overlay.go │ │ │ ├── overlay_disk_quota.go │ │ │ ├── overlay_disk_quota_unsupported.go │ │ │ ├── overlay_unsupported.go │ │ │ └── randomid.go │ │ ├── overlayutils/ │ │ │ └── overlayutils.go │ │ ├── quota/ │ │ │ ├── projectquota.go │ │ │ ├── projectquota_supported.go │ │ │ └── projectquota_unsupported.go │ │ ├── register/ │ │ │ ├── register_btrfs.go │ │ │ ├── register_overlay.go │ │ │ ├── register_vfs.go │ │ │ └── register_zfs.go │ │ ├── vfs/ │ │ │ ├── copy_linux.go │ │ │ ├── copy_unsupported.go │ │ │ └── driver.go │ │ └── zfs/ │ │ ├── MAINTAINERS │ │ ├── zfs.go │ │ ├── zfs_freebsd.go │ │ ├── zfs_linux.go │ │ └── zfs_unsupported.go │ ├── errors.go │ ├── idset.go │ ├── images.go │ ├── internal/ │ │ ├── dedup/ │ │ │ ├── dedup.go │ │ │ ├── dedup_linux.go │ │ │ └── dedup_unsupported.go │ │ ├── rawfilelock/ │ │ │ ├── rawfilelock.go │ │ │ ├── rawfilelock_unix.go │ │ │ └── rawfilelock_windows.go │ │ ├── staging_lockfile/ │ │ │ └── staging_lockfile.go │ │ └── tempdir/ │ │ └── tempdir.go │ ├── jsoniter.go │ ├── layers.go │ ├── lockfile_compat.go │ ├── pkg/ │ │ ├── archive/ │ │ │ ├── README.md │ │ │ ├── archive.go │ │ │ ├── archive_110.go │ │ │ ├── archive_19.go │ │ │ ├── archive_bsd.go │ │ │ ├── archive_linux.go │ │ │ ├── archive_other.go │ │ │ ├── archive_unix.go │ │ │ ├── archive_windows.go │ │ │ ├── archive_zstd.go │ │ │ ├── changes.go │ │ │ ├── changes_linux.go │ │ │ ├── changes_other.go │ │ │ ├── changes_unix.go │ │ │ ├── changes_windows.go │ │ │ ├── copy.go │ │ │ ├── copy_unix.go │ │ │ ├── copy_windows.go │ │ │ ├── diff.go │ │ │ ├── fflags_bsd.go │ │ │ ├── fflags_unsupported.go │ │ │ ├── filter.go │ │ │ ├── time_linux.go │ │ │ ├── time_unsupported.go │ │ │ ├── whiteouts.go │ │ │ └── wrap.go │ │ ├── chrootarchive/ │ │ │ ├── archive.go │ │ │ ├── archive_darwin.go │ │ │ ├── archive_unix.go │ │ │ ├── archive_windows.go │ │ │ ├── chroot_linux.go │ │ │ ├── chroot_unix.go │ │ │ ├── diff.go │ │ │ ├── diff_darwin.go │ │ │ ├── diff_unix.go │ │ │ ├── diff_windows.go │ │ │ ├── init_unix.go │ │ │ └── jsoniter.go │ │ ├── chunked/ │ │ │ ├── bloom_filter_linux.go │ │ │ ├── cache_linux.go │ │ │ ├── compression.go │ │ │ ├── compression_linux.go │ │ │ ├── compressor/ │ │ │ │ ├── compressor.go │ │ │ │ └── rollsum.go │ │ │ ├── dump/ │ │ │ │ └── dump.go │ │ │ ├── filesystem_linux.go │ │ │ ├── internal/ │ │ │ │ ├── minimal/ │ │ │ │ │ └── compression.go │ │ │ │ └── path/ │ │ │ │ └── path.go │ │ │ ├── storage.go │ │ │ ├── storage_linux.go │ │ │ ├── storage_unsupported.go │ │ │ └── toc/ │ │ │ └── toc.go │ │ ├── config/ │ │ │ └── config.go │ │ ├── configfile/ │ │ │ ├── doc.go │ │ │ ├── parse.go │ │ │ ├── path.go │ │ │ ├── path_freebsd.go │ │ │ ├── path_unix.go │ │ │ └── path_windows.go │ │ ├── directory/ │ │ │ ├── directory.go │ │ │ ├── directory_unix.go │ │ │ └── directory_windows.go │ │ ├── fileutils/ │ │ │ ├── exists_freebsd.go │ │ │ ├── exists_unix.go │ │ │ ├── exists_windows.go │ │ │ ├── fileutils.go │ │ │ ├── fileutils_darwin.go │ │ │ ├── fileutils_solaris.go │ │ │ ├── fileutils_unix.go │ │ │ ├── fileutils_windows.go │ │ │ ├── reflink_linux.go │ │ │ └── reflink_unsupported.go │ │ ├── fsutils/ │ │ │ └── fsutils_linux.go │ │ ├── fsverity/ │ │ │ ├── fsverity_linux.go │ │ │ └── fsverity_unsupported.go │ │ ├── homedir/ │ │ │ ├── homedir.go │ │ │ ├── homedir_unix.go │ │ │ └── homedir_windows.go │ │ ├── idmap/ │ │ │ ├── idmapped_utils.go │ │ │ └── idmapped_utils_unsupported.go │ │ ├── idtools/ │ │ │ ├── idtools.go │ │ │ ├── idtools_supported.go │ │ │ ├── idtools_unix.go │ │ │ ├── idtools_unsupported.go │ │ │ ├── idtools_windows.go │ │ │ ├── parser.go │ │ │ ├── usergroupadd_linux.go │ │ │ ├── usergroupadd_unsupported.go │ │ │ └── utils_unix.go │ │ ├── ioutils/ │ │ │ ├── buffer.go │ │ │ ├── bytespipe.go │ │ │ ├── fswriters.go │ │ │ ├── fswriters_linux.go │ │ │ ├── fswriters_other.go │ │ │ ├── readers.go │ │ │ ├── temp_unix.go │ │ │ ├── temp_windows.go │ │ │ ├── writeflusher.go │ │ │ └── writers.go │ │ ├── lockfile/ │ │ │ ├── lastwrite.go │ │ │ ├── lockfile.go │ │ │ ├── lockfile_unix.go │ │ │ └── lockfile_windows.go │ │ ├── longpath/ │ │ │ └── longpath.go │ │ ├── loopback/ │ │ │ ├── attach_loopback.go │ │ │ ├── ioctl.go │ │ │ ├── loop_wrapper.go │ │ │ ├── loopback.go │ │ │ └── loopback_unsupported.go │ │ ├── mount/ │ │ │ ├── flags.go │ │ │ ├── flags_freebsd.go │ │ │ ├── flags_linux.go │ │ │ ├── flags_unsupported.go │ │ │ ├── mount.go │ │ │ ├── mounter_freebsd.go │ │ │ ├── mounter_linux.go │ │ │ ├── mounter_unsupported.go │ │ │ ├── mountinfo.go │ │ │ ├── mountinfo_linux.go │ │ │ ├── sharedsubtree_linux.go │ │ │ ├── unmount_unix.go │ │ │ └── unmount_unsupported.go │ │ ├── parsers/ │ │ │ └── parsers.go │ │ ├── pools/ │ │ │ └── pools.go │ │ ├── promise/ │ │ │ └── promise.go │ │ ├── reexec/ │ │ │ ├── README.md │ │ │ ├── command_freebsd.go │ │ │ ├── command_linux.go │ │ │ ├── command_unix.go │ │ │ ├── command_unsupported.go │ │ │ ├── command_windows.go │ │ │ └── reexec.go │ │ ├── regexp/ │ │ │ ├── regexp.go │ │ │ ├── regexp_dontprecompile.go │ │ │ └── regexp_precompile.go │ │ ├── stringid/ │ │ │ ├── README.md │ │ │ └── stringid.go │ │ ├── stringutils/ │ │ │ ├── README.md │ │ │ └── stringutils.go │ │ ├── system/ │ │ │ ├── chmod.go │ │ │ ├── chtimes.go │ │ │ ├── chtimes_unix.go │ │ │ ├── chtimes_windows.go │ │ │ ├── errors.go │ │ │ ├── exitcode.go │ │ │ ├── extattr_freebsd.go │ │ │ ├── extattr_unsupported.go │ │ │ ├── init.go │ │ │ ├── init_windows.go │ │ │ ├── lchflags_bsd.go │ │ │ ├── lchown.go │ │ │ ├── lcow_unix.go │ │ │ ├── lcow_windows.go │ │ │ ├── lstat_unix.go │ │ │ ├── lstat_windows.go │ │ │ ├── meminfo.go │ │ │ ├── meminfo_freebsd.go │ │ │ ├── meminfo_linux.go │ │ │ ├── meminfo_solaris.go │ │ │ ├── meminfo_unsupported.go │ │ │ ├── meminfo_windows.go │ │ │ ├── mknod.go │ │ │ ├── mknod_freebsd.go │ │ │ ├── mknod_windows.go │ │ │ ├── path.go │ │ │ ├── path_unix.go │ │ │ ├── path_windows.go │ │ │ ├── process_unix.go │ │ │ ├── rm.go │ │ │ ├── rm_common.go │ │ │ ├── rm_freebsd.go │ │ │ ├── stat_common.go │ │ │ ├── stat_darwin.go │ │ │ ├── stat_freebsd.go │ │ │ ├── stat_linux.go │ │ │ ├── stat_netbsd.go │ │ │ ├── stat_openbsd.go │ │ │ ├── stat_solaris.go │ │ │ ├── stat_unix.go │ │ │ ├── stat_windows.go │ │ │ ├── syscall_unix.go │ │ │ ├── syscall_windows.go │ │ │ ├── umask.go │ │ │ ├── umask_windows.go │ │ │ ├── utimes_freebsd.go │ │ │ ├── utimes_linux.go │ │ │ ├── utimes_unsupported.go │ │ │ ├── xattrs_darwin.go │ │ │ ├── xattrs_freebsd.go │ │ │ ├── xattrs_linux.go │ │ │ └── xattrs_unsupported.go │ │ ├── tarlog/ │ │ │ └── tarlogger.go │ │ ├── truncindex/ │ │ │ └── truncindex.go │ │ └── unshare/ │ │ ├── getenv_linux_cgo.go │ │ ├── getenv_linux_nocgo.go │ │ ├── unshare.c │ │ ├── unshare.go │ │ ├── unshare_cgo.go │ │ ├── unshare_darwin.go │ │ ├── unshare_freebsd.c │ │ ├── unshare_freebsd.go │ │ ├── unshare_gccgo.go │ │ ├── unshare_linux.go │ │ ├── unshare_unsupported.go │ │ └── unshare_unsupported_cgo.go │ ├── storage.conf │ ├── storage.conf-freebsd │ ├── store.go │ ├── types/ │ │ ├── default_override_test.conf │ │ ├── errors.go │ │ ├── idmappings.go │ │ ├── options.go │ │ ├── options_bsd.go │ │ ├── options_darwin.go │ │ ├── options_linux.go │ │ ├── options_windows.go │ │ ├── storage_broken.conf │ │ ├── storage_test.conf │ │ └── utils.go │ ├── userns.go │ ├── userns_unsupported.go │ └── utils.go ├── go.yaml.in/ │ └── yaml/ │ └── v2/ │ ├── .travis.yml │ ├── LICENSE │ ├── LICENSE.libyaml │ ├── NOTICE │ ├── README.md │ ├── apic.go │ ├── decode.go │ ├── emitterc.go │ ├── encode.go │ ├── parserc.go │ ├── readerc.go │ ├── resolve.go │ ├── scannerc.go │ ├── sorter.go │ ├── writerc.go │ ├── yaml.go │ ├── yamlh.go │ └── yamlprivateh.go ├── golang.org/ │ └── x/ │ ├── crypto/ │ │ ├── LICENSE │ │ ├── PATENTS │ │ ├── argon2/ │ │ │ ├── argon2.go │ │ │ ├── blake2b.go │ │ │ ├── blamka_amd64.go │ │ │ ├── blamka_amd64.s │ │ │ ├── blamka_generic.go │ │ │ └── blamka_ref.go │ │ ├── bcrypt/ │ │ │ ├── base64.go │ │ │ └── bcrypt.go │ │ ├── blake2b/ │ │ │ ├── blake2b.go │ │ │ ├── blake2bAVX2_amd64.go │ │ │ ├── blake2bAVX2_amd64.s │ │ │ ├── blake2b_amd64.s │ │ │ ├── blake2b_generic.go │ │ │ ├── blake2b_ref.go │ │ │ ├── blake2x.go │ │ │ ├── go125.go │ │ │ └── register.go │ │ ├── blowfish/ │ │ │ ├── block.go │ │ │ ├── cipher.go │ │ │ └── const.go │ │ ├── cast5/ │ │ │ └── cast5.go │ │ ├── chacha20/ │ │ │ ├── chacha_arm64.go │ │ │ ├── chacha_arm64.s │ │ │ ├── chacha_generic.go │ │ │ ├── chacha_noasm.go │ │ │ ├── chacha_ppc64x.go │ │ │ ├── chacha_ppc64x.s │ │ │ ├── chacha_s390x.go │ │ │ ├── chacha_s390x.s │ │ │ └── xor.go │ │ ├── cryptobyte/ │ │ │ ├── asn1/ │ │ │ │ └── asn1.go │ │ │ ├── asn1.go │ │ │ ├── builder.go │ │ │ └── string.go │ │ ├── curve25519/ │ │ │ └── curve25519.go │ │ ├── internal/ │ │ │ ├── alias/ │ │ │ │ ├── alias.go │ │ │ │ └── alias_purego.go │ │ │ └── poly1305/ │ │ │ ├── mac_noasm.go │ │ │ ├── poly1305.go │ │ │ ├── sum_amd64.s │ │ │ ├── sum_asm.go │ │ │ ├── sum_generic.go │ │ │ ├── sum_loong64.s │ │ │ ├── sum_ppc64x.s │ │ │ ├── sum_s390x.go │ │ │ └── sum_s390x.s │ │ ├── nacl/ │ │ │ └── secretbox/ │ │ │ └── secretbox.go │ │ ├── openpgp/ │ │ │ ├── armor/ │ │ │ │ ├── armor.go │ │ │ │ └── encode.go │ │ │ ├── canonical_text.go │ │ │ ├── elgamal/ │ │ │ │ └── elgamal.go │ │ │ ├── errors/ │ │ │ │ └── errors.go │ │ │ ├── keys.go │ │ │ ├── packet/ │ │ │ │ ├── compressed.go │ │ │ │ ├── config.go │ │ │ │ ├── encrypted_key.go │ │ │ │ ├── literal.go │ │ │ │ ├── ocfb.go │ │ │ │ ├── one_pass_signature.go │ │ │ │ ├── opaque.go │ │ │ │ ├── packet.go │ │ │ │ ├── private_key.go │ │ │ │ ├── public_key.go │ │ │ │ ├── public_key_v3.go │ │ │ │ ├── reader.go │ │ │ │ ├── signature.go │ │ │ │ ├── signature_v3.go │ │ │ │ ├── symmetric_key_encrypted.go │ │ │ │ ├── symmetrically_encrypted.go │ │ │ │ ├── userattribute.go │ │ │ │ └── userid.go │ │ │ ├── read.go │ │ │ ├── s2k/ │ │ │ │ └── s2k.go │ │ │ └── write.go │ │ ├── pbkdf2/ │ │ │ └── pbkdf2.go │ │ ├── ripemd160/ │ │ │ ├── ripemd160.go │ │ │ └── ripemd160block.go │ │ ├── salsa20/ │ │ │ └── salsa/ │ │ │ ├── hsalsa20.go │ │ │ ├── salsa208.go │ │ │ ├── salsa20_amd64.go │ │ │ ├── salsa20_amd64.s │ │ │ ├── salsa20_noasm.go │ │ │ └── salsa20_ref.go │ │ ├── scrypt/ │ │ │ └── scrypt.go │ │ ├── ssh/ │ │ │ ├── agent/ │ │ │ │ ├── client.go │ │ │ │ ├── forward.go │ │ │ │ ├── keyring.go │ │ │ │ └── server.go │ │ │ ├── buffer.go │ │ │ ├── certs.go │ │ │ ├── channel.go │ │ │ ├── cipher.go │ │ │ ├── client.go │ │ │ ├── client_auth.go │ │ │ ├── common.go │ │ │ ├── connection.go │ │ │ ├── doc.go │ │ │ ├── handshake.go │ │ │ ├── internal/ │ │ │ │ └── bcrypt_pbkdf/ │ │ │ │ └── bcrypt_pbkdf.go │ │ │ ├── kex.go │ │ │ ├── keys.go │ │ │ ├── mac.go │ │ │ ├── messages.go │ │ │ ├── mlkem.go │ │ │ ├── mux.go │ │ │ ├── server.go │ │ │ ├── session.go │ │ │ ├── ssh_gss.go │ │ │ ├── streamlocal.go │ │ │ ├── tcpip.go │ │ │ └── transport.go │ │ ├── twofish/ │ │ │ └── twofish.go │ │ └── xts/ │ │ └── xts.go │ ├── mod/ │ │ ├── LICENSE │ │ ├── PATENTS │ │ └── semver/ │ │ └── semver.go │ ├── net/ │ │ ├── LICENSE │ │ ├── PATENTS │ │ ├── http/ │ │ │ └── httpguts/ │ │ │ ├── guts.go │ │ │ └── httplex.go │ │ ├── http2/ │ │ │ ├── .gitignore │ │ │ ├── ascii.go │ │ │ ├── ciphers.go │ │ │ ├── client_conn_pool.go │ │ │ ├── client_priority_go126.go │ │ │ ├── client_priority_go127.go │ │ │ ├── config.go │ │ │ ├── config_go125.go │ │ │ ├── config_go126.go │ │ │ ├── databuffer.go │ │ │ ├── errors.go │ │ │ ├── flow.go │ │ │ ├── frame.go │ │ │ ├── gotrack.go │ │ │ ├── hpack/ │ │ │ │ ├── encode.go │ │ │ │ ├── hpack.go │ │ │ │ ├── huffman.go │ │ │ │ ├── static_table.go │ │ │ │ └── tables.go │ │ │ ├── http2.go │ │ │ ├── pipe.go │ │ │ ├── server.go │ │ │ ├── transport.go │ │ │ ├── unencrypted.go │ │ │ ├── write.go │ │ │ ├── writesched.go │ │ │ ├── writesched_priority_rfc7540.go │ │ │ ├── writesched_priority_rfc9218.go │ │ │ ├── writesched_random.go │ │ │ └── writesched_roundrobin.go │ │ ├── idna/ │ │ │ ├── go118.go │ │ │ ├── idna10.0.0.go │ │ │ ├── idna9.0.0.go │ │ │ ├── pre_go118.go │ │ │ ├── punycode.go │ │ │ ├── tables10.0.0.go │ │ │ ├── tables11.0.0.go │ │ │ ├── tables12.0.0.go │ │ │ ├── tables13.0.0.go │ │ │ ├── tables15.0.0.go │ │ │ ├── tables9.0.0.go │ │ │ ├── trie.go │ │ │ ├── trie12.0.0.go │ │ │ ├── trie13.0.0.go │ │ │ └── trieval.go │ │ ├── internal/ │ │ │ ├── httpcommon/ │ │ │ │ ├── ascii.go │ │ │ │ ├── headermap.go │ │ │ │ └── request.go │ │ │ ├── httpsfv/ │ │ │ │ └── httpsfv.go │ │ │ └── timeseries/ │ │ │ └── timeseries.go │ │ └── trace/ │ │ ├── events.go │ │ ├── histogram.go │ │ └── trace.go │ ├── sync/ │ │ ├── LICENSE │ │ ├── PATENTS │ │ ├── errgroup/ │ │ │ └── errgroup.go │ │ └── semaphore/ │ │ └── semaphore.go │ ├── sys/ │ │ ├── LICENSE │ │ ├── PATENTS │ │ ├── cpu/ │ │ │ ├── asm_aix_ppc64.s │ │ │ ├── asm_darwin_arm64_gc.s │ │ │ ├── asm_darwin_x86_gc.s │ │ │ ├── byteorder.go │ │ │ ├── cpu.go │ │ │ ├── cpu_aix.go │ │ │ ├── cpu_arm.go │ │ │ ├── cpu_arm64.go │ │ │ ├── cpu_arm64.s │ │ │ ├── cpu_darwin_arm64.go │ │ │ ├── cpu_darwin_arm64_other.go │ │ │ ├── cpu_darwin_x86.go │ │ │ ├── cpu_gc_arm64.go │ │ │ ├── cpu_gc_s390x.go │ │ │ ├── cpu_gc_x86.go │ │ │ ├── cpu_gc_x86.s │ │ │ ├── cpu_gccgo_arm64.go │ │ │ ├── cpu_gccgo_s390x.go │ │ │ ├── cpu_gccgo_x86.c │ │ │ ├── cpu_gccgo_x86.go │ │ │ ├── cpu_linux.go │ │ │ ├── cpu_linux_arm.go │ │ │ ├── cpu_linux_arm64.go │ │ │ ├── cpu_linux_loong64.go │ │ │ ├── cpu_linux_mips64x.go │ │ │ ├── cpu_linux_noinit.go │ │ │ ├── cpu_linux_ppc64x.go │ │ │ ├── cpu_linux_riscv64.go │ │ │ ├── cpu_linux_s390x.go │ │ │ ├── cpu_loong64.go │ │ │ ├── cpu_loong64.s │ │ │ ├── cpu_mips64x.go │ │ │ ├── cpu_mipsx.go │ │ │ ├── cpu_netbsd_arm64.go │ │ │ ├── cpu_openbsd_arm64.go │ │ │ ├── cpu_openbsd_arm64.s │ │ │ ├── cpu_other_arm.go │ │ │ ├── cpu_other_arm64.go │ │ │ ├── cpu_other_mips64x.go │ │ │ ├── cpu_other_ppc64x.go │ │ │ ├── cpu_other_riscv64.go │ │ │ ├── cpu_other_x86.go │ │ │ ├── cpu_ppc64x.go │ │ │ ├── cpu_riscv64.go │ │ │ ├── cpu_s390x.go │ │ │ ├── cpu_s390x.s │ │ │ ├── cpu_wasm.go │ │ │ ├── cpu_windows_arm64.go │ │ │ ├── cpu_x86.go │ │ │ ├── cpu_zos.go │ │ │ ├── cpu_zos_s390x.go │ │ │ ├── endian_big.go │ │ │ ├── endian_little.go │ │ │ ├── hwcap_linux.go │ │ │ ├── parse.go │ │ │ ├── proc_cpuinfo_linux.go │ │ │ ├── runtime_auxv.go │ │ │ ├── runtime_auxv_go121.go │ │ │ ├── syscall_aix_gccgo.go │ │ │ ├── syscall_aix_ppc64_gc.go │ │ │ ├── syscall_darwin_arm64_gc.go │ │ │ └── syscall_darwin_x86_gc.go │ │ ├── plan9/ │ │ │ ├── asm.s │ │ │ ├── asm_plan9_386.s │ │ │ ├── asm_plan9_amd64.s │ │ │ ├── asm_plan9_arm.s │ │ │ ├── const_plan9.go │ │ │ ├── dir_plan9.go │ │ │ ├── env_plan9.go │ │ │ ├── errors_plan9.go │ │ │ ├── mkall.sh │ │ │ ├── mkerrors.sh │ │ │ ├── mksysnum_plan9.sh │ │ │ ├── pwd_plan9.go │ │ │ ├── race.go │ │ │ ├── race0.go │ │ │ ├── str.go │ │ │ ├── syscall.go │ │ │ ├── syscall_plan9.go │ │ │ ├── zsyscall_plan9_386.go │ │ │ ├── zsyscall_plan9_amd64.go │ │ │ ├── zsyscall_plan9_arm.go │ │ │ └── zsysnum_plan9.go │ │ ├── unix/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── affinity_linux.go │ │ │ ├── aliases.go │ │ │ ├── asm_aix_ppc64.s │ │ │ ├── asm_bsd_386.s │ │ │ ├── asm_bsd_amd64.s │ │ │ ├── asm_bsd_arm.s │ │ │ ├── asm_bsd_arm64.s │ │ │ ├── asm_bsd_ppc64.s │ │ │ ├── asm_bsd_riscv64.s │ │ │ ├── asm_linux_386.s │ │ │ ├── asm_linux_amd64.s │ │ │ ├── asm_linux_arm.s │ │ │ ├── asm_linux_arm64.s │ │ │ ├── asm_linux_loong64.s │ │ │ ├── asm_linux_mips64x.s │ │ │ ├── asm_linux_mipsx.s │ │ │ ├── asm_linux_ppc64x.s │ │ │ ├── asm_linux_riscv64.s │ │ │ ├── asm_linux_s390x.s │ │ │ ├── asm_openbsd_mips64.s │ │ │ ├── asm_solaris_amd64.s │ │ │ ├── asm_zos_s390x.s │ │ │ ├── auxv.go │ │ │ ├── auxv_unsupported.go │ │ │ ├── bluetooth_linux.go │ │ │ ├── bpxsvc_zos.go │ │ │ ├── bpxsvc_zos.s │ │ │ ├── cap_freebsd.go │ │ │ ├── constants.go │ │ │ ├── dev_aix_ppc.go │ │ │ ├── dev_aix_ppc64.go │ │ │ ├── dev_darwin.go │ │ │ ├── dev_dragonfly.go │ │ │ ├── dev_freebsd.go │ │ │ ├── dev_linux.go │ │ │ ├── dev_netbsd.go │ │ │ ├── dev_openbsd.go │ │ │ ├── dev_zos.go │ │ │ ├── dirent.go │ │ │ ├── endian_big.go │ │ │ ├── endian_little.go │ │ │ ├── env_unix.go │ │ │ ├── fcntl.go │ │ │ ├── fcntl_darwin.go │ │ │ ├── fcntl_linux_32bit.go │ │ │ ├── fdset.go │ │ │ ├── gccgo.go │ │ │ ├── gccgo_c.c │ │ │ ├── gccgo_linux_amd64.go │ │ │ ├── ifreq_linux.go │ │ │ ├── ioctl_linux.go │ │ │ ├── ioctl_signed.go │ │ │ ├── ioctl_unsigned.go │ │ │ ├── ioctl_zos.go │ │ │ ├── mkall.sh │ │ │ ├── mkerrors.sh │ │ │ ├── mmap_nomremap.go │ │ │ ├── mremap.go │ │ │ ├── pagesize_unix.go │ │ │ ├── pledge_openbsd.go │ │ │ ├── ptrace_darwin.go │ │ │ ├── ptrace_ios.go │ │ │ ├── race.go │ │ │ ├── race0.go │ │ │ ├── readdirent_getdents.go │ │ │ ├── readdirent_getdirentries.go │ │ │ ├── sockcmsg_dragonfly.go │ │ │ ├── sockcmsg_linux.go │ │ │ ├── sockcmsg_unix.go │ │ │ ├── sockcmsg_unix_other.go │ │ │ ├── sockcmsg_zos.go │ │ │ ├── symaddr_zos_s390x.s │ │ │ ├── syscall.go │ │ │ ├── syscall_aix.go │ │ │ ├── syscall_aix_ppc.go │ │ │ ├── syscall_aix_ppc64.go │ │ │ ├── syscall_bsd.go │ │ │ ├── syscall_darwin.go │ │ │ ├── syscall_darwin_amd64.go │ │ │ ├── syscall_darwin_arm64.go │ │ │ ├── syscall_darwin_libSystem.go │ │ │ ├── syscall_dragonfly.go │ │ │ ├── syscall_dragonfly_amd64.go │ │ │ ├── syscall_freebsd.go │ │ │ ├── syscall_freebsd_386.go │ │ │ ├── syscall_freebsd_amd64.go │ │ │ ├── syscall_freebsd_arm.go │ │ │ ├── syscall_freebsd_arm64.go │ │ │ ├── syscall_freebsd_riscv64.go │ │ │ ├── syscall_hurd.go │ │ │ ├── syscall_hurd_386.go │ │ │ ├── syscall_illumos.go │ │ │ ├── syscall_linux.go │ │ │ ├── syscall_linux_386.go │ │ │ ├── syscall_linux_alarm.go │ │ │ ├── syscall_linux_amd64.go │ │ │ ├── syscall_linux_amd64_gc.go │ │ │ ├── syscall_linux_arm.go │ │ │ ├── syscall_linux_arm64.go │ │ │ ├── syscall_linux_gc.go │ │ │ ├── syscall_linux_gc_386.go │ │ │ ├── syscall_linux_gc_arm.go │ │ │ ├── syscall_linux_gccgo_386.go │ │ │ ├── syscall_linux_gccgo_arm.go │ │ │ ├── syscall_linux_loong64.go │ │ │ ├── syscall_linux_mips64x.go │ │ │ ├── syscall_linux_mipsx.go │ │ │ ├── syscall_linux_ppc.go │ │ │ ├── syscall_linux_ppc64x.go │ │ │ ├── syscall_linux_riscv64.go │ │ │ ├── syscall_linux_s390x.go │ │ │ ├── syscall_linux_sparc64.go │ │ │ ├── syscall_netbsd.go │ │ │ ├── syscall_netbsd_386.go │ │ │ ├── syscall_netbsd_amd64.go │ │ │ ├── syscall_netbsd_arm.go │ │ │ ├── syscall_netbsd_arm64.go │ │ │ ├── syscall_openbsd.go │ │ │ ├── syscall_openbsd_386.go │ │ │ ├── syscall_openbsd_amd64.go │ │ │ ├── syscall_openbsd_arm.go │ │ │ ├── syscall_openbsd_arm64.go │ │ │ ├── syscall_openbsd_libc.go │ │ │ ├── syscall_openbsd_mips64.go │ │ │ ├── syscall_openbsd_ppc64.go │ │ │ ├── syscall_openbsd_riscv64.go │ │ │ ├── syscall_solaris.go │ │ │ ├── syscall_solaris_amd64.go │ │ │ ├── syscall_unix.go │ │ │ ├── syscall_unix_gc.go │ │ │ ├── syscall_unix_gc_ppc64x.go │ │ │ ├── syscall_zos_s390x.go │ │ │ ├── sysvshm_linux.go │ │ │ ├── sysvshm_unix.go │ │ │ ├── sysvshm_unix_other.go │ │ │ ├── timestruct.go │ │ │ ├── unveil_openbsd.go │ │ │ ├── vgetrandom_linux.go │ │ │ ├── vgetrandom_unsupported.go │ │ │ ├── xattr_bsd.go │ │ │ ├── zerrors_aix_ppc.go │ │ │ ├── zerrors_aix_ppc64.go │ │ │ ├── zerrors_darwin_amd64.go │ │ │ ├── zerrors_darwin_arm64.go │ │ │ ├── zerrors_dragonfly_amd64.go │ │ │ ├── zerrors_freebsd_386.go │ │ │ ├── zerrors_freebsd_amd64.go │ │ │ ├── zerrors_freebsd_arm.go │ │ │ ├── zerrors_freebsd_arm64.go │ │ │ ├── zerrors_freebsd_riscv64.go │ │ │ ├── zerrors_linux.go │ │ │ ├── zerrors_linux_386.go │ │ │ ├── zerrors_linux_amd64.go │ │ │ ├── zerrors_linux_arm.go │ │ │ ├── zerrors_linux_arm64.go │ │ │ ├── zerrors_linux_loong64.go │ │ │ ├── zerrors_linux_mips.go │ │ │ ├── zerrors_linux_mips64.go │ │ │ ├── zerrors_linux_mips64le.go │ │ │ ├── zerrors_linux_mipsle.go │ │ │ ├── zerrors_linux_ppc.go │ │ │ ├── zerrors_linux_ppc64.go │ │ │ ├── zerrors_linux_ppc64le.go │ │ │ ├── zerrors_linux_riscv64.go │ │ │ ├── zerrors_linux_s390x.go │ │ │ ├── zerrors_linux_sparc64.go │ │ │ ├── zerrors_netbsd_386.go │ │ │ ├── zerrors_netbsd_amd64.go │ │ │ ├── zerrors_netbsd_arm.go │ │ │ ├── zerrors_netbsd_arm64.go │ │ │ ├── zerrors_openbsd_386.go │ │ │ ├── zerrors_openbsd_amd64.go │ │ │ ├── zerrors_openbsd_arm.go │ │ │ ├── zerrors_openbsd_arm64.go │ │ │ ├── zerrors_openbsd_mips64.go │ │ │ ├── zerrors_openbsd_ppc64.go │ │ │ ├── zerrors_openbsd_riscv64.go │ │ │ ├── zerrors_solaris_amd64.go │ │ │ ├── zerrors_zos_s390x.go │ │ │ ├── zptrace_armnn_linux.go │ │ │ ├── zptrace_linux_arm64.go │ │ │ ├── zptrace_mipsnn_linux.go │ │ │ ├── zptrace_mipsnnle_linux.go │ │ │ ├── zptrace_x86_linux.go │ │ │ ├── zsymaddr_zos_s390x.s │ │ │ ├── zsyscall_aix_ppc.go │ │ │ ├── zsyscall_aix_ppc64.go │ │ │ ├── zsyscall_aix_ppc64_gc.go │ │ │ ├── zsyscall_aix_ppc64_gccgo.go │ │ │ ├── zsyscall_darwin_amd64.go │ │ │ ├── zsyscall_darwin_amd64.s │ │ │ ├── zsyscall_darwin_arm64.go │ │ │ ├── zsyscall_darwin_arm64.s │ │ │ ├── zsyscall_dragonfly_amd64.go │ │ │ ├── zsyscall_freebsd_386.go │ │ │ ├── zsyscall_freebsd_amd64.go │ │ │ ├── zsyscall_freebsd_arm.go │ │ │ ├── zsyscall_freebsd_arm64.go │ │ │ ├── zsyscall_freebsd_riscv64.go │ │ │ ├── zsyscall_illumos_amd64.go │ │ │ ├── zsyscall_linux.go │ │ │ ├── zsyscall_linux_386.go │ │ │ ├── zsyscall_linux_amd64.go │ │ │ ├── zsyscall_linux_arm.go │ │ │ ├── zsyscall_linux_arm64.go │ │ │ ├── zsyscall_linux_loong64.go │ │ │ ├── zsyscall_linux_mips.go │ │ │ ├── zsyscall_linux_mips64.go │ │ │ ├── zsyscall_linux_mips64le.go │ │ │ ├── zsyscall_linux_mipsle.go │ │ │ ├── zsyscall_linux_ppc.go │ │ │ ├── zsyscall_linux_ppc64.go │ │ │ ├── zsyscall_linux_ppc64le.go │ │ │ ├── zsyscall_linux_riscv64.go │ │ │ ├── zsyscall_linux_s390x.go │ │ │ ├── zsyscall_linux_sparc64.go │ │ │ ├── zsyscall_netbsd_386.go │ │ │ ├── zsyscall_netbsd_amd64.go │ │ │ ├── zsyscall_netbsd_arm.go │ │ │ ├── zsyscall_netbsd_arm64.go │ │ │ ├── zsyscall_openbsd_386.go │ │ │ ├── zsyscall_openbsd_386.s │ │ │ ├── zsyscall_openbsd_amd64.go │ │ │ ├── zsyscall_openbsd_amd64.s │ │ │ ├── zsyscall_openbsd_arm.go │ │ │ ├── zsyscall_openbsd_arm.s │ │ │ ├── zsyscall_openbsd_arm64.go │ │ │ ├── zsyscall_openbsd_arm64.s │ │ │ ├── zsyscall_openbsd_mips64.go │ │ │ ├── zsyscall_openbsd_mips64.s │ │ │ ├── zsyscall_openbsd_ppc64.go │ │ │ ├── zsyscall_openbsd_ppc64.s │ │ │ ├── zsyscall_openbsd_riscv64.go │ │ │ ├── zsyscall_openbsd_riscv64.s │ │ │ ├── zsyscall_solaris_amd64.go │ │ │ ├── zsyscall_zos_s390x.go │ │ │ ├── zsysctl_openbsd_386.go │ │ │ ├── zsysctl_openbsd_amd64.go │ │ │ ├── zsysctl_openbsd_arm.go │ │ │ ├── zsysctl_openbsd_arm64.go │ │ │ ├── zsysctl_openbsd_mips64.go │ │ │ ├── zsysctl_openbsd_ppc64.go │ │ │ ├── zsysctl_openbsd_riscv64.go │ │ │ ├── zsysnum_darwin_amd64.go │ │ │ ├── zsysnum_darwin_arm64.go │ │ │ ├── zsysnum_dragonfly_amd64.go │ │ │ ├── zsysnum_freebsd_386.go │ │ │ ├── zsysnum_freebsd_amd64.go │ │ │ ├── zsysnum_freebsd_arm.go │ │ │ ├── zsysnum_freebsd_arm64.go │ │ │ ├── zsysnum_freebsd_riscv64.go │ │ │ ├── zsysnum_linux_386.go │ │ │ ├── zsysnum_linux_amd64.go │ │ │ ├── zsysnum_linux_arm.go │ │ │ ├── zsysnum_linux_arm64.go │ │ │ ├── zsysnum_linux_loong64.go │ │ │ ├── zsysnum_linux_mips.go │ │ │ ├── zsysnum_linux_mips64.go │ │ │ ├── zsysnum_linux_mips64le.go │ │ │ ├── zsysnum_linux_mipsle.go │ │ │ ├── zsysnum_linux_ppc.go │ │ │ ├── zsysnum_linux_ppc64.go │ │ │ ├── zsysnum_linux_ppc64le.go │ │ │ ├── zsysnum_linux_riscv64.go │ │ │ ├── zsysnum_linux_s390x.go │ │ │ ├── zsysnum_linux_sparc64.go │ │ │ ├── zsysnum_netbsd_386.go │ │ │ ├── zsysnum_netbsd_amd64.go │ │ │ ├── zsysnum_netbsd_arm.go │ │ │ ├── zsysnum_netbsd_arm64.go │ │ │ ├── zsysnum_openbsd_386.go │ │ │ ├── zsysnum_openbsd_amd64.go │ │ │ ├── zsysnum_openbsd_arm.go │ │ │ ├── zsysnum_openbsd_arm64.go │ │ │ ├── zsysnum_openbsd_mips64.go │ │ │ ├── zsysnum_openbsd_ppc64.go │ │ │ ├── zsysnum_openbsd_riscv64.go │ │ │ ├── zsysnum_zos_s390x.go │ │ │ ├── ztypes_aix_ppc.go │ │ │ ├── ztypes_aix_ppc64.go │ │ │ ├── ztypes_darwin_amd64.go │ │ │ ├── ztypes_darwin_arm64.go │ │ │ ├── ztypes_dragonfly_amd64.go │ │ │ ├── ztypes_freebsd_386.go │ │ │ ├── ztypes_freebsd_amd64.go │ │ │ ├── ztypes_freebsd_arm.go │ │ │ ├── ztypes_freebsd_arm64.go │ │ │ ├── ztypes_freebsd_riscv64.go │ │ │ ├── ztypes_linux.go │ │ │ ├── ztypes_linux_386.go │ │ │ ├── ztypes_linux_amd64.go │ │ │ ├── ztypes_linux_arm.go │ │ │ ├── ztypes_linux_arm64.go │ │ │ ├── ztypes_linux_loong64.go │ │ │ ├── ztypes_linux_mips.go │ │ │ ├── ztypes_linux_mips64.go │ │ │ ├── ztypes_linux_mips64le.go │ │ │ ├── ztypes_linux_mipsle.go │ │ │ ├── ztypes_linux_ppc.go │ │ │ ├── ztypes_linux_ppc64.go │ │ │ ├── ztypes_linux_ppc64le.go │ │ │ ├── ztypes_linux_riscv64.go │ │ │ ├── ztypes_linux_s390x.go │ │ │ ├── ztypes_linux_sparc64.go │ │ │ ├── ztypes_netbsd_386.go │ │ │ ├── ztypes_netbsd_amd64.go │ │ │ ├── ztypes_netbsd_arm.go │ │ │ ├── ztypes_netbsd_arm64.go │ │ │ ├── ztypes_openbsd_386.go │ │ │ ├── ztypes_openbsd_amd64.go │ │ │ ├── ztypes_openbsd_arm.go │ │ │ ├── ztypes_openbsd_arm64.go │ │ │ ├── ztypes_openbsd_mips64.go │ │ │ ├── ztypes_openbsd_ppc64.go │ │ │ ├── ztypes_openbsd_riscv64.go │ │ │ ├── ztypes_solaris_amd64.go │ │ │ └── ztypes_zos_s390x.go │ │ └── windows/ │ │ ├── aliases.go │ │ ├── dll_windows.go │ │ ├── env_windows.go │ │ ├── eventlog.go │ │ ├── exec_windows.go │ │ ├── memory_windows.go │ │ ├── mkerrors.bash │ │ ├── mkknownfolderids.bash │ │ ├── mksyscall.go │ │ ├── race.go │ │ ├── race0.go │ │ ├── security_windows.go │ │ ├── service.go │ │ ├── setupapi_windows.go │ │ ├── str.go │ │ ├── syscall.go │ │ ├── syscall_windows.go │ │ ├── types_windows.go │ │ ├── types_windows_386.go │ │ ├── types_windows_amd64.go │ │ ├── types_windows_arm.go │ │ ├── types_windows_arm64.go │ │ ├── zerrors_windows.go │ │ ├── zknownfolderids_windows.go │ │ └── zsyscall_windows.go │ ├── term/ │ │ ├── CONTRIBUTING.md │ │ ├── LICENSE │ │ ├── PATENTS │ │ ├── README.md │ │ ├── codereview.cfg │ │ ├── term.go │ │ ├── term_plan9.go │ │ ├── term_unix.go │ │ ├── term_unix_bsd.go │ │ ├── term_unix_other.go │ │ ├── term_unsupported.go │ │ ├── term_windows.go │ │ └── terminal.go │ └── text/ │ ├── LICENSE │ ├── PATENTS │ ├── secure/ │ │ └── bidirule/ │ │ └── bidirule.go │ ├── transform/ │ │ └── transform.go │ └── unicode/ │ ├── bidi/ │ │ ├── bidi.go │ │ ├── bracket.go │ │ ├── core.go │ │ ├── prop.go │ │ ├── tables15.0.0.go │ │ ├── tables17.0.0.go │ │ └── trieval.go │ └── norm/ │ ├── composition.go │ ├── forminfo.go │ ├── input.go │ ├── iter.go │ ├── normalize.go │ ├── readwriter.go │ ├── tables15.0.0.go │ ├── tables17.0.0.go │ ├── transform.go │ └── trie.go ├── google.golang.org/ │ ├── genproto/ │ │ └── googleapis/ │ │ ├── api/ │ │ │ ├── LICENSE │ │ │ ├── annotations/ │ │ │ │ ├── annotations.pb.go │ │ │ │ ├── client.pb.go │ │ │ │ ├── field_behavior.pb.go │ │ │ │ ├── field_info.pb.go │ │ │ │ ├── http.pb.go │ │ │ │ ├── resource.pb.go │ │ │ │ └── routing.pb.go │ │ │ └── launch_stage.pb.go │ │ └── rpc/ │ │ ├── LICENSE │ │ └── status/ │ │ └── status.pb.go │ ├── grpc/ │ │ ├── AUTHORS │ │ ├── CODE-OF-CONDUCT.md │ │ ├── CONTRIBUTING.md │ │ ├── GOVERNANCE.md │ │ ├── LICENSE │ │ ├── MAINTAINERS.md │ │ ├── Makefile │ │ ├── NOTICE.txt │ │ ├── README.md │ │ ├── SECURITY.md │ │ ├── attributes/ │ │ │ └── attributes.go │ │ ├── backoff/ │ │ │ └── backoff.go │ │ ├── backoff.go │ │ ├── balancer/ │ │ │ ├── balancer.go │ │ │ ├── base/ │ │ │ │ ├── balancer.go │ │ │ │ └── base.go │ │ │ ├── conn_state_evaluator.go │ │ │ ├── endpointsharding/ │ │ │ │ └── endpointsharding.go │ │ │ ├── grpclb/ │ │ │ │ └── state/ │ │ │ │ └── state.go │ │ │ ├── pickfirst/ │ │ │ │ ├── internal/ │ │ │ │ │ └── internal.go │ │ │ │ └── pickfirst.go │ │ │ ├── roundrobin/ │ │ │ │ └── roundrobin.go │ │ │ └── subconn.go │ │ ├── balancer_wrapper.go │ │ ├── binarylog/ │ │ │ └── grpc_binarylog_v1/ │ │ │ └── binarylog.pb.go │ │ ├── call.go │ │ ├── channelz/ │ │ │ └── channelz.go │ │ ├── clientconn.go │ │ ├── codec.go │ │ ├── codes/ │ │ │ ├── code_string.go │ │ │ └── codes.go │ │ ├── connectivity/ │ │ │ └── connectivity.go │ │ ├── credentials/ │ │ │ ├── credentials.go │ │ │ ├── insecure/ │ │ │ │ └── insecure.go │ │ │ └── tls.go │ │ ├── dialoptions.go │ │ ├── doc.go │ │ ├── encoding/ │ │ │ ├── encoding.go │ │ │ ├── encoding_v2.go │ │ │ ├── internal/ │ │ │ │ └── internal.go │ │ │ └── proto/ │ │ │ └── proto.go │ │ ├── experimental/ │ │ │ └── stats/ │ │ │ ├── metricregistry.go │ │ │ └── metrics.go │ │ ├── grpclog/ │ │ │ ├── component.go │ │ │ ├── grpclog.go │ │ │ ├── internal/ │ │ │ │ ├── grpclog.go │ │ │ │ ├── logger.go │ │ │ │ └── loggerv2.go │ │ │ ├── logger.go │ │ │ └── loggerv2.go │ │ ├── interceptor.go │ │ ├── internal/ │ │ │ ├── backoff/ │ │ │ │ └── backoff.go │ │ │ ├── balancer/ │ │ │ │ ├── gracefulswitch/ │ │ │ │ │ ├── config.go │ │ │ │ │ └── gracefulswitch.go │ │ │ │ └── weight/ │ │ │ │ └── weight.go │ │ │ ├── balancerload/ │ │ │ │ └── load.go │ │ │ ├── binarylog/ │ │ │ │ ├── binarylog.go │ │ │ │ ├── binarylog_testutil.go │ │ │ │ ├── env_config.go │ │ │ │ ├── method_logger.go │ │ │ │ └── sink.go │ │ │ ├── buffer/ │ │ │ │ └── unbounded.go │ │ │ ├── channelz/ │ │ │ │ ├── channel.go │ │ │ │ ├── channelmap.go │ │ │ │ ├── funcs.go │ │ │ │ ├── logging.go │ │ │ │ ├── server.go │ │ │ │ ├── socket.go │ │ │ │ ├── subchannel.go │ │ │ │ ├── syscall_linux.go │ │ │ │ ├── syscall_nonlinux.go │ │ │ │ └── trace.go │ │ │ ├── credentials/ │ │ │ │ ├── credentials.go │ │ │ │ ├── spiffe.go │ │ │ │ ├── syscallconn.go │ │ │ │ └── util.go │ │ │ ├── envconfig/ │ │ │ │ ├── envconfig.go │ │ │ │ ├── observability.go │ │ │ │ └── xds.go │ │ │ ├── experimental.go │ │ │ ├── grpclog/ │ │ │ │ └── prefix_logger.go │ │ │ ├── grpcsync/ │ │ │ │ ├── callback_serializer.go │ │ │ │ ├── event.go │ │ │ │ └── pubsub.go │ │ │ ├── grpcutil/ │ │ │ │ ├── compressor.go │ │ │ │ ├── encode_duration.go │ │ │ │ ├── grpcutil.go │ │ │ │ ├── metadata.go │ │ │ │ ├── method.go │ │ │ │ └── regex.go │ │ │ ├── idle/ │ │ │ │ └── idle.go │ │ │ ├── internal.go │ │ │ ├── metadata/ │ │ │ │ └── metadata.go │ │ │ ├── pretty/ │ │ │ │ └── pretty.go │ │ │ ├── proxyattributes/ │ │ │ │ └── proxyattributes.go │ │ │ ├── resolver/ │ │ │ │ ├── config_selector.go │ │ │ │ ├── delegatingresolver/ │ │ │ │ │ └── delegatingresolver.go │ │ │ │ ├── dns/ │ │ │ │ │ ├── dns_resolver.go │ │ │ │ │ └── internal/ │ │ │ │ │ └── internal.go │ │ │ │ ├── passthrough/ │ │ │ │ │ └── passthrough.go │ │ │ │ └── unix/ │ │ │ │ └── unix.go │ │ │ ├── serviceconfig/ │ │ │ │ ├── duration.go │ │ │ │ └── serviceconfig.go │ │ │ ├── stats/ │ │ │ │ ├── labels.go │ │ │ │ ├── metrics_recorder_list.go │ │ │ │ └── stats.go │ │ │ ├── status/ │ │ │ │ └── status.go │ │ │ ├── syscall/ │ │ │ │ ├── syscall_linux.go │ │ │ │ └── syscall_nonlinux.go │ │ │ ├── tcp_keepalive_others.go │ │ │ ├── tcp_keepalive_unix.go │ │ │ ├── tcp_keepalive_windows.go │ │ │ └── transport/ │ │ │ ├── bdp_estimator.go │ │ │ ├── client_stream.go │ │ │ ├── controlbuf.go │ │ │ ├── defaults.go │ │ │ ├── flowcontrol.go │ │ │ ├── handler_server.go │ │ │ ├── http2_client.go │ │ │ ├── http2_server.go │ │ │ ├── http_util.go │ │ │ ├── logging.go │ │ │ ├── networktype/ │ │ │ │ └── networktype.go │ │ │ ├── proxy.go │ │ │ ├── server_stream.go │ │ │ └── transport.go │ │ ├── keepalive/ │ │ │ └── keepalive.go │ │ ├── mem/ │ │ │ ├── buffer_pool.go │ │ │ ├── buffer_slice.go │ │ │ └── buffers.go │ │ ├── metadata/ │ │ │ └── metadata.go │ │ ├── peer/ │ │ │ └── peer.go │ │ ├── picker_wrapper.go │ │ ├── preloader.go │ │ ├── reflection/ │ │ │ ├── README.md │ │ │ ├── adapt.go │ │ │ ├── grpc_reflection_v1/ │ │ │ │ ├── reflection.pb.go │ │ │ │ └── reflection_grpc.pb.go │ │ │ ├── grpc_reflection_v1alpha/ │ │ │ │ ├── reflection.pb.go │ │ │ │ └── reflection_grpc.pb.go │ │ │ ├── internal/ │ │ │ │ └── internal.go │ │ │ └── serverreflection.go │ │ ├── resolver/ │ │ │ ├── dns/ │ │ │ │ └── dns_resolver.go │ │ │ ├── map.go │ │ │ └── resolver.go │ │ ├── resolver_wrapper.go │ │ ├── rpc_util.go │ │ ├── server.go │ │ ├── service_config.go │ │ ├── serviceconfig/ │ │ │ └── serviceconfig.go │ │ ├── stats/ │ │ │ ├── handlers.go │ │ │ ├── metrics.go │ │ │ └── stats.go │ │ ├── status/ │ │ │ └── status.go │ │ ├── stream.go │ │ ├── stream_interfaces.go │ │ ├── tap/ │ │ │ └── tap.go │ │ ├── trace.go │ │ ├── trace_notrace.go │ │ ├── trace_withtrace.go │ │ └── version.go │ └── protobuf/ │ ├── LICENSE │ ├── PATENTS │ ├── encoding/ │ │ ├── protojson/ │ │ │ ├── decode.go │ │ │ ├── doc.go │ │ │ ├── encode.go │ │ │ └── well_known_types.go │ │ ├── prototext/ │ │ │ ├── decode.go │ │ │ ├── doc.go │ │ │ └── encode.go │ │ └── protowire/ │ │ └── wire.go │ ├── internal/ │ │ ├── descfmt/ │ │ │ └── stringer.go │ │ ├── descopts/ │ │ │ └── options.go │ │ ├── detrand/ │ │ │ └── rand.go │ │ ├── editiondefaults/ │ │ │ ├── defaults.go │ │ │ └── editions_defaults.binpb │ │ ├── editionssupport/ │ │ │ └── editions.go │ │ ├── encoding/ │ │ │ ├── defval/ │ │ │ │ └── default.go │ │ │ ├── json/ │ │ │ │ ├── decode.go │ │ │ │ ├── decode_number.go │ │ │ │ ├── decode_string.go │ │ │ │ ├── decode_token.go │ │ │ │ └── encode.go │ │ │ ├── messageset/ │ │ │ │ └── messageset.go │ │ │ ├── tag/ │ │ │ │ └── tag.go │ │ │ └── text/ │ │ │ ├── decode.go │ │ │ ├── decode_number.go │ │ │ ├── decode_string.go │ │ │ ├── decode_token.go │ │ │ ├── doc.go │ │ │ └── encode.go │ │ ├── errors/ │ │ │ └── errors.go │ │ ├── filedesc/ │ │ │ ├── build.go │ │ │ ├── desc.go │ │ │ ├── desc_init.go │ │ │ ├── desc_lazy.go │ │ │ ├── desc_list.go │ │ │ ├── desc_list_gen.go │ │ │ ├── editions.go │ │ │ ├── placeholder.go │ │ │ └── presence.go │ │ ├── filetype/ │ │ │ └── build.go │ │ ├── flags/ │ │ │ ├── flags.go │ │ │ ├── proto_legacy_disable.go │ │ │ └── proto_legacy_enable.go │ │ ├── genid/ │ │ │ ├── any_gen.go │ │ │ ├── api_gen.go │ │ │ ├── descriptor_gen.go │ │ │ ├── doc.go │ │ │ ├── duration_gen.go │ │ │ ├── empty_gen.go │ │ │ ├── field_mask_gen.go │ │ │ ├── go_features_gen.go │ │ │ ├── goname.go │ │ │ ├── map_entry.go │ │ │ ├── name.go │ │ │ ├── source_context_gen.go │ │ │ ├── struct_gen.go │ │ │ ├── timestamp_gen.go │ │ │ ├── type_gen.go │ │ │ ├── wrappers.go │ │ │ └── wrappers_gen.go │ │ ├── impl/ │ │ │ ├── api_export.go │ │ │ ├── api_export_opaque.go │ │ │ ├── bitmap.go │ │ │ ├── bitmap_race.go │ │ │ ├── checkinit.go │ │ │ ├── codec_extension.go │ │ │ ├── codec_field.go │ │ │ ├── codec_field_opaque.go │ │ │ ├── codec_gen.go │ │ │ ├── codec_map.go │ │ │ ├── codec_message.go │ │ │ ├── codec_message_opaque.go │ │ │ ├── codec_messageset.go │ │ │ ├── codec_tables.go │ │ │ ├── codec_unsafe.go │ │ │ ├── convert.go │ │ │ ├── convert_list.go │ │ │ ├── convert_map.go │ │ │ ├── decode.go │ │ │ ├── encode.go │ │ │ ├── enum.go │ │ │ ├── equal.go │ │ │ ├── extension.go │ │ │ ├── lazy.go │ │ │ ├── legacy_enum.go │ │ │ ├── legacy_export.go │ │ │ ├── legacy_extension.go │ │ │ ├── legacy_file.go │ │ │ ├── legacy_message.go │ │ │ ├── merge.go │ │ │ ├── merge_gen.go │ │ │ ├── message.go │ │ │ ├── message_opaque.go │ │ │ ├── message_opaque_gen.go │ │ │ ├── message_reflect.go │ │ │ ├── message_reflect_field.go │ │ │ ├── message_reflect_field_gen.go │ │ │ ├── message_reflect_gen.go │ │ │ ├── pointer_unsafe.go │ │ │ ├── pointer_unsafe_opaque.go │ │ │ ├── presence.go │ │ │ └── validate.go │ │ ├── order/ │ │ │ ├── order.go │ │ │ └── range.go │ │ ├── pragma/ │ │ │ └── pragma.go │ │ ├── protolazy/ │ │ │ ├── bufferreader.go │ │ │ ├── lazy.go │ │ │ └── pointer_unsafe.go │ │ ├── set/ │ │ │ └── ints.go │ │ ├── strs/ │ │ │ ├── strings.go │ │ │ └── strings_unsafe.go │ │ └── version/ │ │ └── version.go │ ├── proto/ │ │ ├── checkinit.go │ │ ├── decode.go │ │ ├── decode_gen.go │ │ ├── doc.go │ │ ├── encode.go │ │ ├── encode_gen.go │ │ ├── equal.go │ │ ├── extension.go │ │ ├── merge.go │ │ ├── messageset.go │ │ ├── proto.go │ │ ├── proto_methods.go │ │ ├── proto_reflect.go │ │ ├── reset.go │ │ ├── size.go │ │ ├── size_gen.go │ │ ├── wrapperopaque.go │ │ └── wrappers.go │ ├── protoadapt/ │ │ └── convert.go │ ├── reflect/ │ │ ├── protodesc/ │ │ │ ├── desc.go │ │ │ ├── desc_init.go │ │ │ ├── desc_resolve.go │ │ │ ├── desc_validate.go │ │ │ ├── editions.go │ │ │ └── proto.go │ │ ├── protoreflect/ │ │ │ ├── methods.go │ │ │ ├── proto.go │ │ │ ├── source.go │ │ │ ├── source_gen.go │ │ │ ├── type.go │ │ │ ├── value.go │ │ │ ├── value_equal.go │ │ │ ├── value_union.go │ │ │ └── value_unsafe.go │ │ └── protoregistry/ │ │ └── registry.go │ ├── runtime/ │ │ ├── protoiface/ │ │ │ ├── legacy.go │ │ │ └── methods.go │ │ └── protoimpl/ │ │ ├── impl.go │ │ └── version.go │ └── types/ │ ├── descriptorpb/ │ │ └── descriptor.pb.go │ ├── gofeaturespb/ │ │ └── go_features.pb.go │ └── known/ │ ├── anypb/ │ │ └── any.pb.go │ ├── durationpb/ │ │ └── duration.pb.go │ └── timestamppb/ │ └── timestamp.pb.go ├── gopkg.in/ │ └── yaml.v3/ │ ├── LICENSE │ ├── NOTICE │ ├── README.md │ ├── apic.go │ ├── decode.go │ ├── emitterc.go │ ├── encode.go │ ├── parserc.go │ ├── readerc.go │ ├── resolve.go │ ├── scannerc.go │ ├── sorter.go │ ├── writerc.go │ ├── yaml.go │ ├── yamlh.go │ └── yamlprivateh.go ├── k8s.io/ │ └── klog/ │ ├── .travis.yml │ ├── CONTRIBUTING.md │ ├── LICENSE │ ├── OWNERS │ ├── README.md │ ├── RELEASE.md │ ├── SECURITY_CONTACTS │ ├── code-of-conduct.md │ ├── klog.go │ └── klog_file.go ├── modules.txt ├── sigs.k8s.io/ │ └── yaml/ │ ├── .gitignore │ ├── CONTRIBUTING.md │ ├── LICENSE │ ├── OWNERS │ ├── README.md │ ├── RELEASE.md │ ├── SECURITY_CONTACTS │ ├── code-of-conduct.md │ ├── fields.go │ └── yaml.go └── tags.cncf.io/ └── container-device-interface/ ├── LICENSE ├── internal/ │ └── validation/ │ ├── k8s/ │ │ ├── objectmeta.go │ │ └── validation.go │ └── validate.go ├── pkg/ │ ├── cdi/ │ │ ├── annotations.go │ │ ├── cache.go │ │ ├── container-edits.go │ │ ├── container-edits_unix.go │ │ ├── container-edits_windows.go │ │ ├── default-cache.go │ │ ├── device.go │ │ ├── doc.go │ │ ├── oci.go │ │ ├── spec-dirs.go │ │ ├── spec.go │ │ ├── spec_linux.go │ │ └── spec_other.go │ └── parser/ │ └── parser.go └── specs-go/ ├── LICENSE ├── config.go └── version.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cirrus.yml ================================================ --- # Main collection of env. vars to set for all tasks and scripts. env: #### #### Global variables used for all tasks #### # Name of the ultimate destination branch for this CI run, PR or post-merge. DEST_BRANCH: "main" GOPATH: "/var/tmp/go" GOSRC: "${GOPATH}/src/github.com/containers/buildah" GOCACHE: "/tmp/go-build" # Overrides default location (/tmp/cirrus) for repo clone CIRRUS_WORKING_DIR: "${GOSRC}" # Shell used to execute all script commands CIRRUS_SHELL: "/bin/bash" # Automation script path relative to $CIRRUS_WORKING_DIR) SCRIPT_BASE: "./contrib/cirrus" # No need to go crazy, but grab enough to cover most PRs CIRRUS_CLONE_DEPTH: 50 # Unless set by in_podman.sh, default to operating outside of a podman container IN_PODMAN: 'false' # root or rootless PRIV_NAME: root # default "mention the $BUILDAH_RUNTIME in the task alias, with initial whitespace" value RUNTIME_N: "" #### #### Cache-image names to test with #### # GCE project where images live IMAGE_PROJECT: "libpod-218412" FEDORA_NAME: "fedora-43" PRIOR_FEDORA_NAME: "fedora-42" RAWHIDE_NAME: "rawhide" DEBIAN_NAME: "debian-14" # Image identifiers IMAGE_SUFFIX: "c20260310t170224z-f43f42d14" FEDORA_CACHE_IMAGE_NAME: "fedora-${IMAGE_SUFFIX}" PRIOR_FEDORA_CACHE_IMAGE_NAME: "prior-fedora-${IMAGE_SUFFIX}" RAWHIDE_CACHE_IMAGE_NAME: "rawhide-${IMAGE_SUFFIX}" # Used temporarily for rust-podman-sequoia. After that RPM is available in stable Fedora releases, we can stop testing against Rawhide again. DEBIAN_CACHE_IMAGE_NAME: "debian-${IMAGE_SUFFIX}" IN_PODMAN_IMAGE: "quay.io/libpod/fedora_podman:${IMAGE_SUFFIX}" #### #### Command variables to help avoid duplication #### # Command to prefix every output line with a timestamp # (can't do inline awk script, Cirrus-CI or YAML mangles quoting) _TIMESTAMP: 'awk -f ${CIRRUS_WORKING_DIR}/${SCRIPT_BASE}/timestamp.awk' gcp_credentials: ENCRYPTED[ae0bf7370f0b6e446bc61d0865a2c55d3e166b3fab9466eb0393e38e1c66a31ca4c71ddc7e0139d47d075c36dd6d3fd7] # Default timeout for each task timeout_in: 30m # Default VM to use unless set or modified by task gce_instance: &standardvm image_project: "${IMAGE_PROJECT}" zone: "us-central1-c" # Required by Cirrus for the time being cpu: 2 memory: "4G" disk: 200 # Gigabytes, do not set less than 200 per obscure GCE docs re: I/O performance image_name: "${FEDORA_CACHE_IMAGE_NAME}" # Update metadata on VM images referenced by this repository state meta_task: name: "VM img. keepalive" alias: meta container: image: "quay.io/libpod/imgts:latest" cpu: 1 memory: "1G" env: # Space-separated list of images used by this repository state IMGNAMES: |- ${FEDORA_CACHE_IMAGE_NAME} ${PRIOR_FEDORA_CACHE_IMAGE_NAME} ${RAWHIDE_CACHE_IMAGE_NAME} ${DEBIAN_CACHE_IMAGE_NAME} build-push-${IMAGE_SUFFIX} BUILDID: "${CIRRUS_BUILD_ID}" REPOREF: "${CIRRUS_CHANGE_IN_REPO}" GCPJSON: ENCRYPTED[d3614d6f5cc0e66be89d4252b3365fd84f14eee0259d4eb47e25fc0bc2842c7937f5ee8c882b7e547b4c5ec4b6733b14] GCPNAME: ENCRYPTED[8509e6a681b859479ce6aa275bd3c4ac82de5beec6df6057925afc4cd85b7ef2e879066ae8baaa2d453b82958e434578] GCPPROJECT: ENCRYPTED[cc09b62d0ec6746a3df685e663ad25d9d5af95ef5fd843c96f3d0ec9d7f065dc63216b9c685c9f43a776a1d403991494] clone_script: 'true' script: '/usr/local/bin/entrypoint.sh' smoke_task: alias: 'smoke' name: "Smoke Test" gce_instance: memory: "12G" cpu: 8 # Don't bother running on branches (including cron), or for tags. skip: $CIRRUS_PR == '' timeout_in: 10m setup_script: '${SCRIPT_BASE}/setup.sh |& ${_TIMESTAMP}' build_script: '${SCRIPT_BASE}/build.sh |& ${_TIMESTAMP}' validate_test_script: '${SCRIPT_BASE}/test.sh validate |& ${_TIMESTAMP}' binary_artifacts: path: ./bin/* # Check that all included go modules from other sources match # # what is expected in `vendor/modules.txt` vs `go.mod`. vendor_task: name: "Test Vendoring" alias: vendor env: CIRRUS_WORKING_DIR: "/var/tmp/go/src/github.com/containers/buildah" GOPATH: "/var/tmp/go" GOSRC: "/var/tmp/go/src/github.com/containers/buildah" # Runs within Cirrus's "community cluster" container: image: docker.io/library/golang:1.25 cpu: 1 memory: 1 timeout_in: 5m vendor_script: - './hack/check_vendor_toolchain.sh Try updating the image used by the vendor_task in .cirrus.yml.' - 'make vendor' - './hack/tree_status.sh' # Confirm cross-compile ALL architectures on a Mac OS-X VM. cross_build_task: name: "Cross Compile" gce_instance: cpu: 8 memory: "24G" alias: cross_build skip: >- $CIRRUS_CHANGE_TITLE =~ '.*CI:DOCS.*' env: HOME: /root script: - go version - make -j cross CGO_ENABLED=0 binary_artifacts: path: ./bin/* unit_task: name: 'Unit tests w/ $STORAGE_DRIVER' gce_instance: cpu: 4 alias: unit skip: ¬_build_docs >- $CIRRUS_CHANGE_TITLE =~ '.*CI:DOCS.*' || $CIRRUS_CHANGE_TITLE =~ '.*CI:BUILD.*' depends_on: &smoke_vendor - smoke - vendor matrix: - env: STORAGE_DRIVER: 'vfs' - env: STORAGE_DRIVER: 'overlay' setup_script: '${SCRIPT_BASE}/setup.sh |& ${_TIMESTAMP}' unit_test_script: '${SCRIPT_BASE}/test.sh unit |& ${_TIMESTAMP}' always: &standardlogs audit_log_script: '$GOSRC/$SCRIPT_BASE/logcollector.sh audit' df_script: '$GOSRC/$SCRIPT_BASE/logcollector.sh df' journal_script: '$GOSRC/$SCRIPT_BASE/logcollector.sh journal' podman_system_info_script: '$GOSRC/$SCRIPT_BASE/logcollector.sh podman' buildah_version_script: '$GOSRC/$SCRIPT_BASE/logcollector.sh buildah_version' buildah_info_script: '$GOSRC/$SCRIPT_BASE/logcollector.sh buildah_info' package_versions_script: '$GOSRC/$SCRIPT_BASE/logcollector.sh packages' golang_version_script: '$GOSRC/$SCRIPT_BASE/logcollector.sh golang' conformance_task: name: 'Debian Conformance w/ $STORAGE_DRIVER' alias: conformance skip: *not_build_docs depends_on: *smoke_vendor gce_instance: cpu: 4 image_name: "${DEBIAN_CACHE_IMAGE_NAME}" matrix: - env: STORAGE_DRIVER: 'vfs' TMPDIR: '/var/tmp' - env: STORAGE_DRIVER: 'overlay' setup_script: '${SCRIPT_BASE}/setup.sh conformance |& ${_TIMESTAMP}' conformance_test_script: '${SCRIPT_BASE}/test.sh conformance |& ${_TIMESTAMP}' always: <<: *standardlogs integration_task: name: "Integration $DISTRO_NV$RUNTIME_N w/ $STORAGE_DRIVER" alias: integration skip: *not_build_docs depends_on: *smoke_vendor matrix: # VFS - env: DISTRO_NV: "${FEDORA_NAME}" IMAGE_NAME: "${FEDORA_CACHE_IMAGE_NAME}" STORAGE_DRIVER: 'vfs' BUILDAH_RUNTIME: crun RUNTIME_N: " using crun" - env: DISTRO_NV: "${PRIOR_FEDORA_NAME}" IMAGE_NAME: "${PRIOR_FEDORA_CACHE_IMAGE_NAME}" STORAGE_DRIVER: 'vfs' BUILDAH_RUNTIME: crun RUNTIME_N: " using crun" - env: DISTRO_NV: "${DEBIAN_NAME}" IMAGE_NAME: "${DEBIAN_CACHE_IMAGE_NAME}" STORAGE_DRIVER: 'vfs' - env: DISTRO_NV: "${FEDORA_NAME}" IMAGE_NAME: "${FEDORA_CACHE_IMAGE_NAME}" STORAGE_DRIVER: 'vfs' BUILDAH_RUNTIME: runc RUNTIME_N: " using runc" - env: DISTRO_NV: "${PRIOR_FEDORA_NAME}" IMAGE_NAME: "${PRIOR_FEDORA_CACHE_IMAGE_NAME}" STORAGE_DRIVER: 'vfs' BUILDAH_RUNTIME: runc RUNTIME_N: " using runc" # OVERLAY - env: DISTRO_NV: "${FEDORA_NAME}" IMAGE_NAME: "${FEDORA_CACHE_IMAGE_NAME}" STORAGE_DRIVER: 'overlay' BUILDAH_RUNTIME: crun RUNTIME_N: " using crun" - env: DISTRO_NV: "${PRIOR_FEDORA_NAME}" IMAGE_NAME: "${PRIOR_FEDORA_CACHE_IMAGE_NAME}" STORAGE_DRIVER: 'overlay' BUILDAH_RUNTIME: crun RUNTIME_N: " using crun" - env: DISTRO_NV: "${DEBIAN_NAME}" IMAGE_NAME: "${DEBIAN_CACHE_IMAGE_NAME}" STORAGE_DRIVER: 'overlay' - env: DISTRO_NV: "${RAWHIDE_NAME}" IMAGE_NAME: "${RAWHIDE_CACHE_IMAGE_NAME}" STORAGE_DRIVER: 'overlay' TEST_BUILD_TAGS: 'containers_image_sequoia' - env: DISTRO_NV: "${FEDORA_NAME}" IMAGE_NAME: "${FEDORA_CACHE_IMAGE_NAME}" STORAGE_DRIVER: 'overlay' BUILDAH_RUNTIME: runc RUNTIME_N: " using runc" - env: DISTRO_NV: "${PRIOR_FEDORA_NAME}" IMAGE_NAME: "${PRIOR_FEDORA_CACHE_IMAGE_NAME}" STORAGE_DRIVER: 'overlay' BUILDAH_RUNTIME: runc RUNTIME_N: " using runc" gce_instance: &integration_gce_instance image_name: "$IMAGE_NAME" cpu: 8 memory: "8G" # Separate scripts for separate outputs, makes debugging easier. setup_script: '${SCRIPT_BASE}/setup.sh |& ${_TIMESTAMP}' build_script: '${SCRIPT_BASE}/build.sh |& ${_TIMESTAMP}' integration_test_script: '${SCRIPT_BASE}/test.sh integration |& ${_TIMESTAMP}' binary_artifacts: path: ./bin/* always: <<: *standardlogs integration_rootless_task: name: "Integration rootless $DISTRO_NV$RUNTIME_N w/ $STORAGE_DRIVER" alias: integration_rootless skip: *not_build_docs depends_on: *smoke_vendor matrix: # Running rootless tests on overlay # OVERLAY - env: DISTRO_NV: "${FEDORA_NAME}" IMAGE_NAME: "${FEDORA_CACHE_IMAGE_NAME}" STORAGE_DRIVER: 'overlay' PRIV_NAME: rootless BUILDAH_RUNTIME: crun RUNTIME_N: " using crun" - env: DISTRO_NV: "${PRIOR_FEDORA_NAME}" IMAGE_NAME: "${PRIOR_FEDORA_CACHE_IMAGE_NAME}" STORAGE_DRIVER: 'overlay' PRIV_NAME: rootless BUILDAH_RUNTIME: crun RUNTIME_N: " using crun" - env: DISTRO_NV: "${DEBIAN_NAME}" IMAGE_NAME: "${DEBIAN_CACHE_IMAGE_NAME}" STORAGE_DRIVER: 'overlay' PRIV_NAME: rootless - env: DISTRO_NV: "${FEDORA_NAME}" IMAGE_NAME: "${FEDORA_CACHE_IMAGE_NAME}" STORAGE_DRIVER: 'overlay' PRIV_NAME: rootless BUILDAH_RUNTIME: runc RUNTIME_N: " using runc" - env: DISTRO_NV: "${PRIOR_FEDORA_NAME}" IMAGE_NAME: "${PRIOR_FEDORA_CACHE_IMAGE_NAME}" STORAGE_DRIVER: 'overlay' PRIV_NAME: rootless BUILDAH_RUNTIME: runc RUNTIME_N: " using runc" gce_instance: <<: *integration_gce_instance # Separate scripts for separate outputs, makes debugging easier. setup_script: '${SCRIPT_BASE}/setup.sh |& ${_TIMESTAMP}' build_script: '${SCRIPT_BASE}/build.sh |& ${_TIMESTAMP}' integration_test_script: '${SCRIPT_BASE}/test.sh integration |& ${_TIMESTAMP}' binary_artifacts: path: ./bin/* always: <<: *standardlogs in_podman_task: name: "Containerized Integration" alias: in_podman skip: *not_build_docs depends_on: *smoke_vendor gce_instance: cpu: 8 memory: "8G" env: # This is key, cause the scripts to re-execute themselves inside a container. IN_PODMAN: 'true' BUILDAH_ISOLATION: 'chroot' STORAGE_DRIVER: 'vfs' # Separate scripts for separate outputs, makes debugging easier. setup_script: '${SCRIPT_BASE}/setup.sh |& ${_TIMESTAMP}' build_script: '${SCRIPT_BASE}/build.sh |& ${_TIMESTAMP}' integration_test_script: '${SCRIPT_BASE}/test.sh integration |& ${_TIMESTAMP}' binary_artifacts: path: ./bin/* always: <<: *standardlogs # Status aggregator for all tests. This task simply ensures a defined # set of tasks all passed, and allows confirming that based on the status # of this task. success_task: # N/B: The prow merge-bot (tide) is sensitized to this exact name, DO NOT CHANGE IT. # Ref: https://github.com/openshift/release/pull/48909 name: "Total Success" alias: success depends_on: - meta - smoke - unit - conformance - vendor - cross_build - integration - integration_rootless - in_podman container: image: "quay.io/libpod/alpine:latest" cpu: 1 memory: 1 env: CIRRUS_SHELL: direct # execute command directly clone_script: mkdir -p $CIRRUS_WORKING_DIR script: /bin/true ================================================ FILE: .codespellrc ================================================ [codespell] skip = ./vendor,./.git,./go.sum,./docs/*.1,./docker/AUTHORS,./CHANGELOG.md,./changelog.txt,./tests/tools/vendor,./tests/tools/go.mod,./tests/tools/go.sum ignore-words-list = fo,passt,secon,erro ================================================ FILE: .fmf/version ================================================ 1 ================================================ FILE: .github/ISSUE_TEMPLATE/buildah_bug_report.yaml ================================================ --- name: Buildah Bug Report description: File a Buildah bug report labels: ["kind/bug", "triage-needed"] body: - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report! **NOTE** A large number of issues reported against Buildah are often found to already be fixed in more current versions of the project. Before reporting an issue, please verify the version you are running with `buildah version` and compare it to the latest released version under [releases](https://github.com/containers/buildah/releases). If they differ, please update your version of Buildah to the latest possible and retry your command before creating an issue. Commands you might need to run to create the issue $ `buildah version` $ `buildah info` $ `rpm -q buildah` or `apt list buildah` - type: textarea id: description attributes: label: Issue Description description: Please explain your issue value: "Describe your issue" validations: required: true - type: textarea id: reproducer attributes: label: Steps to reproduce the issue description: Please explain the steps to reproduce the issue value: "Steps to reproduce the issue\n1.\n2.\n3.\n" validations: required: true - type: textarea id: received_results attributes: label: Describe the results you received description: Please explain the results you are noticing value: "Describe the results you received" validations: required: true - type: textarea id: expected_results attributes: label: Describe the results you expected description: Please explain the results you are expecting value: "Describe the results you expected" validations: required: true - type: textarea id: buildah_version attributes: label: buildah version output description: Please copy and paste `buildah version` output. value: If you are unable to run `buildah version` for any reason, please provide the output of `rpm -q buildah` or `apt list buildah`. render: yaml validations: required: true - type: textarea id: buildah_info attributes: label: buildah info output description: Please copy and paste `buildah info` output. value: If you are unable to run `buildah info` for any reason, please provide the operating system and its version and the architecture you are running. render: yaml validations: required: true - type: textarea id: storage_conf attributes: label: Provide your storage.conf description: "Please provide the relevant [storage.conf file](https://github.com/containers/storage/blob/main/docs/containers-storage.conf.5.md#files)" render: toml validations: required: true - type: dropdown id: upstream_latest attributes: label: Upstream Latest Release description: Have you tried running the [latest upstream release](https://github.com/containers/buildah/releases/latest) options: - "Yes" - "No" validations: required: true - type: textarea id: additional_environment attributes: label: Additional environment details description: Please describe any additional environment details like (AWS, VirtualBox,...) value: "Additional environment details" - type: textarea id: additional_info attributes: label: Additional information description: Please explain the additional information you deem important value: "Additional information like issue happens only occasionally or issue happens with a particular architecture or on a particular setting" ================================================ FILE: .github/ISSUE_TEMPLATE/config.yaml ================================================ --- blank_issues_enabled: true contact_links: - name: Ask a question url: https://github.com/containers/buildah/discussions/new about: Ask a question about Buildah - name: If your issue is a general Podman issue unrelated to `podman build`, please open an issue in the Podman repository. If the issue is with the `podman build` command, please report it here. url: https://github.com/containers/podman/issues about: Please report issues with Podman here. ================================================ FILE: .github/ISSUE_TEMPLATE/podman_build_bug_report.yaml ================================================ --- name: Podman Build Bug Report description: File a Podman build bug report labels: ["kind/bug", "triage-needed"] body: - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report! **NOTE** A large number of issues reported against Buildah are often found to already be fixed in more current versions of the project. Before reporting an issue, please verify the version you are running with `podman version` and compare it to the latest released version under [releases](https://github.com/containers/podman/releases). If they differ, please update your version of Podman to the latest possible and retry your command before creating an issue. Commands you might need to run to create the issue $ `podman version` $ `podman info` $ `rpm -q podman` or `apt list podman` - type: textarea id: description attributes: label: Issue Description description: Please explain your issue value: "Describe your issue" validations: required: true - type: textarea id: reproducer attributes: label: Steps to reproduce the issue description: Please explain the steps to reproduce the issue value: "Steps to reproduce the issue\n1.\n2.\n3.\n" validations: required: true - type: textarea id: received_results attributes: label: Describe the results you received description: Please explain the results you are noticing value: "Describe the results you received" validations: required: true - type: textarea id: expected_results attributes: label: Describe the results you expected description: Please explain the results you are expecting value: "Describe the results you expected" validations: required: true - type: textarea id: podman_version attributes: label: podman version output description: Please copy and paste `podman version` output. value: If you are unable to run `podman version` for any reason, please provide the output of `rpm -q podman` or `apt list podman`. render: yaml validations: required: true - type: textarea id: podman_info attributes: label: podman info output description: Please copy and paste `podman info` output. value: If you are unable to run `podman info` for any reason, please provide the operating system and its version and the architecture you are running. render: yaml validations: required: true - type: textarea id: storage_conf attributes: label: Provide your storage.conf description: "Please provide the relevant [storage.conf file](https://github.com/containers/storage/blob/main/docs/containers-storage.conf.5.md#files)" render: toml validations: required: true - type: dropdown id: podman_in_a_container attributes: label: Podman in a container description: Please select Yes if you are running Podman in a container options: - "No" - "Yes" validations: required: true - type: dropdown id: privileged_rootless attributes: label: Privileged Or Rootless description: Are you running the containers as privileged or non-root user? Note that using `su` or `sudo` does not establish a proper login session required for running Podman as a non-root user. Please refer to the [troubleshooting guide](https://github.com/containers/podman/blob/main/troubleshooting.md#solution-28) for alternatives. options: - Privileged - Rootless - type: dropdown id: upstream_latest attributes: label: Upstream Latest Release description: Have you tried running the [latest upstream release](https://github.com/containers/podman/releases/latest) options: - "Yes" - "No" validations: required: true - type: dropdown id: installation_source attributes: label: Installation Source description: What installion type did you use? multiple: false options: - Distribution package (DNF, apt, yay) - Brew - Offical Podman Installer (Mac) - Podman Desktop default: 0 validations: required: true - type: textarea id: additional_environment attributes: label: Additional environment details description: Please describe any additional environment details like (AWS, VirtualBox,...) value: "Additional environment details" - type: textarea id: additional_info attributes: label: Additional information description: Please explain the additional information you deem important value: "Additional information like issue happens only occasionally or issue happens with a particular architecture or on a particular setting" ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ #### What type of PR is this? > /kind api-change > /kind bug > /kind cleanup > /kind deprecation > /kind design > /kind documentation > /kind failing-test > /kind feature > /kind flake > /kind other #### What this PR does / why we need it: #### How to verify it #### Which issue(s) this PR fixes: #### Special notes for your reviewer: #### Does this PR introduce a user-facing change? ```release-note ``` ================================================ FILE: .github/release.yml ================================================ # https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes#configuring-automatically-generated-release-notes changelog: categories: - title: Notable changes labels: - '*' exclude: labels: - dependencies - title: Dependency updates labels: - dependencies ================================================ FILE: .github/renovate.json5 ================================================ /* Renovate is a service similar to GitHub Dependabot, but with (fantastically) more configuration options. So many options in fact, if you're new I recommend glossing over this cheat-sheet prior to the official documentation: https://www.augmentedmind.de/2021/07/25/renovate-bot-cheat-sheet Configuration Update/Change Procedure: 1. Make changes 2. Manually validate changes (from repo-root): podman run -it \ -v ./.github/renovate.json5:/usr/src/app/renovate.json5:z \ ghcr.io/renovatebot/renovate:latest \ renovate-config-validator 3. Commit. Configuration Reference: https://docs.renovatebot.com/configuration-options/ Monitoring Dashboard: https://app.renovatebot.com/dashboard#github/containers Note: The Renovate bot will create/manage it's business on branches named 'renovate/*'. Otherwise, and by default, the only the copy of this file that matters is the one on the `main` branch. No other branches will be monitored or touched in any way. */ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", /************************************************* ****** Global/general configuration options ***** *************************************************/ // Re-use predefined sets of configuration options to DRY "extends": [ // https://github.com/containers/automation/blob/main/renovate/defaults.json5 "github>containers/automation//renovate/defaults.json5" ], /************************************************* *** Repository-specific configuration options *** *************************************************/ // Don't leave dep. update. PRs "hanging", assign them to people. "assignees": ["containers/buildah-maintainers"], "ignorePaths": [ "**/vendor/**", "**/docs/**", "**/examples/**", "**/tests/**" ] } ================================================ FILE: .github/workflows/check_cirrus_cron.yml ================================================ --- # See also: # https://github.com/containers/podman/blob/main/.github/workflows/check_cirrus_cron.yml on: # Note: This only applies to the default branch. schedule: # N/B: This should correspond to a period slightly after # the last job finishes running. See job defs. at: # https://cirrus-ci.com/settings/repository/6706677464432640 - cron: '03 03 * * 1-5' # Debug: Allow triggering job manually in github-actions WebUI workflow_dispatch: {} jobs: # Ref: https://docs.github.com/en/actions/using-workflows/reusing-workflows call_cron_failures: uses: containers/podman/.github/workflows/check_cirrus_cron.yml@main secrets: SECRET_CIRRUS_API_KEY: ${{secrets.SECRET_CIRRUS_API_KEY}} ACTION_MAIL_SERVER: ${{secrets.ACTION_MAIL_SERVER}} ACTION_MAIL_USERNAME: ${{secrets.ACTION_MAIL_USERNAME}} ACTION_MAIL_PASSWORD: ${{secrets.ACTION_MAIL_PASSWORD}} ACTION_MAIL_SENDER: ${{secrets.ACTION_MAIL_SENDER}} ================================================ FILE: .github/workflows/issue_pr_lock.yml ================================================ --- # See also: # https://github.com/containers/podman/blob/main/.github/workflows/issue_pr_lock.yml on: schedule: - cron: '0 0 * * *' # Debug: Allow triggering job manually in github-actions WebUI workflow_dispatch: {} jobs: # Ref: https://docs.github.com/en/actions/using-workflows/reusing-workflows closed_issue_discussion_lock: uses: containers/podman/.github/workflows/issue_pr_lock.yml@main secrets: inherit permissions: contents: read issues: write pull-requests: write ================================================ FILE: .github/workflows/pr.yml ================================================ name: validate on: pull_request: jobs: commit: runs-on: ubuntu-24.04 # Only check commits on pull requests. if: github.event_name == 'pull_request' steps: - name: get pr commits id: 'get-pr-commits' uses: tim-actions/get-pr-commits@198af03565609bb4ed924d1260247b4881f09e7d # v1.3.1 with: token: ${{ secrets.GITHUB_TOKEN }} - if: contains(github.head_ref, 'renovate/') != true name: check subject line length uses: tim-actions/commit-message-checker-with-regex@094fc16ff83d04e2ec73edb5eaf6aa267db33791 # v0.3.2 with: commits: ${{ steps.get-pr-commits.outputs.commits }} pattern: '^.{0,72}(\n.*)*$' error: 'Subject too long (max 72)' ================================================ FILE: .github/workflows/stale.yml ================================================ name: Mark stale issues and pull requests # Please refer to https://github.com/actions/stale/blob/master/action.yml # to see all config knobs of the stale action. on: schedule: - cron: "0 0 * * *" permissions: contents: read jobs: stale: permissions: issues: write # for actions/stale to close stale issues pull-requests: write # for actions/stale to close stale PRs runs-on: ubuntu-latest steps: - uses: actions/stale@v10 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: 'A friendly reminder that this issue had no activity for 30 days.' stale-pr-message: 'A friendly reminder that this PR had no activity for 30 days.' stale-issue-label: 'stale-issue' stale-pr-label: 'stale-pr' days-before-stale: 30 days-before-close: 365 remove-stale-when-updated: true ================================================ FILE: .gitignore ================================================ docs/buildah*.1 docs/*.5 /bin /buildah /imgtype /build/ /tests/tools/build Dockerfile* !/tests/bud/*/Dockerfile* !/tests/conformance/**/Dockerfile* *.swp /result/ internal/mkcw/embed/entrypoint_amd64.o internal/mkcw/embed/entrypoint_amd64 ================================================ FILE: .golangci.yml ================================================ version: "2" run: build-tags: - apparmor - seccomp - selinux formatters: enable: - gofumpt linters: enable: - nolintlint - revive - unconvert - unparam - unused - whitespace exclusions: presets: - comments - std-error-handling settings: staticcheck: checks: - all - -QF1008 # https://staticcheck.dev/docs/checks/#QF1008 Omit embedded fields from selector expression. ================================================ FILE: .packit.yaml ================================================ --- # See the documentation for more information: # https://packit.dev/docs/configuration/ downstream_package_name: buildah upstream_tag_template: v{version} # These files get synced from upstream to downstream (Fedora / CentOS Stream) on every # propose-downstream job. This is done so tests maintained upstream can be run # downstream in Zuul CI and Bodhi. # Ref: https://packit.dev/docs/configuration#files_to_sync files_to_sync: - src: rpm/gating.yaml dest: gating.yaml delete: true - src: plans/ dest: plans/ delete: true mkpath: true - src: tests/tmt/ dest: tests/tmt/ delete: true mkpath: true - src: .fmf/ dest: .fmf/ delete: true - .packit.yaml packages: buildah-fedora: pkg_tool: fedpkg specfile_path: rpm/buildah.spec buildah-centos: pkg_tool: centpkg specfile_path: rpm/buildah.spec buildah-eln: specfile_path: rpm/buildah.spec srpm_build_deps: - make jobs: - job: copr_build trigger: pull_request packages: [buildah-fedora] notifications: &copr_build_failure_notification failure_comment: message: "Ephemeral COPR build failed. @containers/packit-build please check." # Fedora aliases documentation: https://packit.dev/docs/configuration#aliases # python3-fedora-distro-aliases provides `resolve-fedora-aliases` command targets: &fedora_copr_targets - fedora-all-x86_64 - fedora-all-aarch64 enable_net: true # Disable osh diff scan until Go support is available # Ref: https://github.com/openscanhub/known-false-positives/pull/30#issuecomment-2858698495 osh_diff_scan_after_copr_build: false # Ignore until golang is updated in distro buildroot to 1.23.3+ - job: copr_build trigger: ignore packages: [buildah-eln] notifications: *copr_build_failure_notification targets: fedora-eln-x86_64: additional_repos: - "https://kojipkgs.fedoraproject.org/repos/eln-build/latest/x86_64/" fedora-eln-aarch64: additional_repos: - "https://kojipkgs.fedoraproject.org/repos/eln-build/latest/aarch64/" enable_net: true # Ignore until golang is updated in distro buildroot to 1.23.3+ - job: copr_build trigger: ignore packages: [buildah-centos] notifications: *copr_build_failure_notification targets: ¢os_copr_targets - centos-stream-9-x86_64 - centos-stream-9-aarch64 - centos-stream-10-x86_64 - centos-stream-10-aarch64 enable_net: true # Run on commit to main branch - job: copr_build trigger: commit packages: [buildah-fedora] notifications: failure_comment: message: "podman-next COPR build failed. @containers/packit-build please check." branch: main owner: rhcontainerbot project: podman-next enable_net: true # Tests on Fedora for main branch PRs - job: tests trigger: pull_request packages: [buildah-fedora] targets: - fedora-all-x86_64 tf_extra_params: environments: - artifacts: - type: repository-file id: https://copr.fedorainfracloud.org/coprs/rhcontainerbot/podman-next/repo/fedora-$releasever/rhcontainerbot-podman-next-fedora-$releasever.repo # Ignore until golang is updated in distro buildroot to 1.23.3+ # Tests on CentOS Stream for main branch PRs - job: tests trigger: ignore packages: [buildah-centos] targets: - centos-stream-9-x86_64 - centos-stream-10-x86_64 tf_extra_params: environments: - artifacts: - type: repository-file id: https://copr.fedorainfracloud.org/coprs/rhcontainerbot/podman-next/repo/centos-stream-$releasever/rhcontainerbot-podman-next-centos-stream-$releasever.repo # Sync to Fedora - job: propose_downstream trigger: release packages: [buildah-fedora] update_release: false dist_git_branches: &fedora_targets - fedora-all # Sync to CentOS Stream - job: propose_downstream trigger: release packages: [buildah-centos] update_release: false dist_git_branches: - c10s # Fedora Koji build - job: koji_build trigger: commit packages: [buildah-fedora] sidetag_group: podman-releases # Dependents are not rpm dependencies, but the package whose bodhi update # should include this package. # Ref: https://packit.dev/docs/fedora-releases-guide/releasing-multiple-packages dependents: - podman dist_git_branches: *fedora_targets ================================================ FILE: CHANGELOG.md ================================================ ![buildah logo](https://cdn.rawgit.com/containers/buildah/main/logos/buildah-logo_large.png) # Changelog ## v1.42.0 (2025-10-17) Bump to storage v1.61.0, image v5.38.0, common v0.66.0 fix(deps): update module github.com/openshift/imagebuilder to v1.2.19 fix(deps): update module github.com/openshift/imagebuilder to v1.2.18 copier: ignore user.overlay.* xattrs commit: always return the config digest as the image ID fix(deps): update module golang.org/x/crypto to v0.43.0 fix(deps): update module golang.org/x/sys to v0.37.0 fix(deps): update module github.com/docker/docker to v28.5.1+incompatible fix(deps): update module github.com/moby/buildkit to v0.25.1 fix(deps): update module github.com/opencontainers/runc to v1.3.2 fix(deps): update module github.com/docker/docker to v28.5.0+incompatible fix(deps): update module github.com/moby/buildkit to v0.25.0 fix(deps): update github.com/containers/luksy digest to 2cf5bc9 Make some test files different from each other Revert "fix(deps): update module github.com/cyphar/filepath-securejoin to v0.5.0" Also run integration tests with the Sequoia backend Allow users to build against podman-sequoia in non-default locations fix(deps): update module github.com/cyphar/filepath-securejoin to v0.5.0 .cirrus.yml: Test Vendoring bump golang vendor: bump go.podman.io/{common,image,storage} to main fix(deps): update module golang.org/x/crypto to v0.42.0 fix(deps): update module github.com/docker/docker to v28.4.0+incompatible fix(deps): update module github.com/moby/buildkit to v0.24.0 fix(deps): update module github.com/spf13/pflag to v1.0.10 fix(deps): update module github.com/fsouza/go-dockerclient to v1.12.2 fix(deps): update module github.com/opencontainers/runc to v1.3.1 fix(deps): update module github.com/opencontainers/cgroups to v0.0.5 fix(deps): update module golang.org/x/sync to v0.17.0 tests/run.bats: "run masks" test: accept "unreadable" masked directories Run: create parent directories of mount targets with mode 0755 tests/run.bats: "run masks" test: accept "unreadable" masked directories New VM images Suppress a linter warning modernize: JSON doesn't do "omitempty" structs, so stop asking modernize: use maps.Copy() instead of iterating over a map to copy it modernize: use strings.CutPrefix/SplitSeq/FieldsSeq Update expected/minimum version of Go to 1.24 chroot: use $PATH when finding commands [skip-ci] Update actions/stale action to v10 Update module github.com/ulikunitz/xz to v0.5.15 [SECURITY] Update go.sum New VM images Update module github.com/openshift/imagebuilder to v1 Update module github.com/spf13/cobra to v1.10.1 Switch common, storage and image to monorepo. Update module github.com/stretchr/testify to v1.11.1 Update module go.etcd.io/bbolt to v1.4.3 Handle tagged+digested references when processing --all-platforms Update module github.com/stretchr/testify to v1.11.0 Add --transient-store global option Support "--imagestore" global flags Commit: don't depend on MountImage(), because .imagestore Adding mohanboddu as community manager to MAINTAINERS.md Rework how we decide what to filter out of layer diffs Note that we have to build `true` first for the sake of its tests copier.Stat(): return owner UID and GID if available copier.Get(): ensure that directory entries end in "/" copier.Get(): strip user and group names from entries imagebuildah.Executor/StageExecutor: check numeric --from= values Losen the dependency on go-connections/tlsconfig fix(deps): update module golang.org/x/crypto to v0.41.0 fix(deps): update module golang.org/x/term to v0.34.0 fix(deps): update module github.com/docker/go-connections to v0.6.0 fix(deps): update module golang.org/x/sys to v0.35.0 copy: assume a destination with basename "." is a directory generatePathChecksum: ignore ModTime, AccessTime and ChangeTime fix(deps): update module github.com/seccomp/libseccomp-golang to v0.11.1 fix(deps): update module github.com/containers/common to v0.64.1 History should note unset-label, timestamp, and rewrite-timestamp pkg/cli.GenBuildOptions(): don't hardwire optional bools fix(deps): update module github.com/containers/image/v5 to v5.36.1 imagebuildah.StageExecutor.Execute: commit more "no instructions" cases fix(deps): update module github.com/containers/storage to v1.59.1 Only suppress "noted" items when not squashing Reap stray processes fix(deps): update github.com/containers/luksy digest to 8fccf78 fix(deps): update module github.com/docker/docker to v28.3.3+incompatible Restore the default meaning of `--pull` (should be `always`). Test that pulled up parent directories are excluded at commit Exclude pulled up parent directories at commit-time copier.Ensure(): also return parent directories copier.MkdirOptions: add ModTimeNew fix(deps): update module github.com/containers/common to v0.64.0 Bump to Buildah v1.42.0-dev fix(deps): update module github.com/spf13/pflag to v1.0.7 CI: make runc tests non-blocking build,add: add support for corporate proxies ## v1.41.0 (2025-07-16) Bump to c/storage v1.59.0, c/image v5.36.0, ... c/common v0.64.0 stage_executor: check platform of cache candidates fix(deps): update module golang.org/x/crypto to v0.40.0 fix(deps): update module golang.org/x/term to v0.33.0 fix(deps): update module golang.org/x/sync to v0.16.0 fix(deps): update module github.com/docker/docker to v28.3.2+incompatible ADD/COPY --link support added RPM/TMT: account for passwd binary moving to tests buildah: move passwd command to tests Update "bud with --cpu-shares" test, and rename it Remove BUILDTAG btrfs_noversion as no longer effective fix(deps): update module github.com/docker/docker to v28.3.1+incompatible fix(deps): update module github.com/moby/buildkit to v0.23.2 fix(deps): update github.com/containers/luksy digest to bc60f96 chore(typos): fix typos vendor: update c/{common,image,storage} to main chore(deps): update module github.com/go-viper/mapstructure/v2 to v2.3.0 [security] fix(deps): update module go.etcd.io/bbolt to v1.4.2 Update Neil Smith's GitHub username in MAINTAINERS.md Accept SOURCE_DATE_EPOCH as a build-arg fix(deps): update module github.com/docker/docker to v28.3.0+incompatible Add conditional release-checking system test info,inspect: use the "formats" package to get some builtins Use containers/common's formats package instead of our own build, commit: set the OCI ...created annotation on OCI images commit: exclude parents of mount targets, too run: clean up parents of mount targets, too tarFilterer: always flush after writing Builder: drop the TempVolumes field Update module github.com/moby/buildkit to v0.23.1 Update module github.com/opencontainers/cgroups to v0.0.3 Add CommitOptions.OmitLayerHistoryEntry, for skipping the new bits Update module github.com/fsouza/go-dockerclient to v1.12.1 conformance: use mirrored frontend and base images commit-with-extra-files test: use $TEST_SCRATCH_DIR fix(deps): update module github.com/moby/buildkit to v0.23.0 "root fs only mounted once" test: accept root with only the rw option Run with --device /dev/fuse and not just -v /dev/fuse:/dev/fuse CI: pass $BUILDAH_RUNTIME through to in-container test runs CI: ensure rootless groups aren't duplicates build: add support for --inherit-annotations CI: give the rootless test user some supplemental groups bud,run: runc does not support keep-groups Fix lint issue in TestCommitCompression Add a unit test for compression types in OCI images Support zstd compression in image commit fix(deps): update module go.etcd.io/bbolt to v1.4.1 rpm: build rpm with libsqlite3 tag Makefile: use libsqlite3 build when possible commit,build: --source-date-epoch/--timestamp omit identity label docs: add --setopt "*.countme=false" to dnf examples Builder.sbomScan(): don't break non-root scanners build: --source-date-epoch/--timestamp use static hostname/cid fix(deps): update module golang.org/x/crypto to v0.39.0 fix(deps): update module golang.org/x/sync to v0.15.0 build: add --source-date-epoch and --rewrite-timestamp flags build,config: add support for --unsetannotation commit: add --source-date-epoch and --rewrite-timestamp flags fix(deps): update module github.com/openshift/imagebuilder to v1.2.16 vendor latest c/{common,image,storage} Tweak our handling of variant values, again Don't BuildRequires: ostree-devel parse, validateExtraHost: honor Hostgateway in format remove static nix build Ensure extendedGlob returns paths in lexical order CI: run integration tests on Fedora with both crun and runc buildah-build(1): clarify that --cgroup-parent affects RUN instructions runUsingRuntime: use named constants for runtime states Add a dummy "runtime" that just dumps its config file run: handle relabeling bind mounts ourselves fix link to Maintainers file Update to avoid deprecated types fix(deps): update module github.com/docker/docker to v28.2.0+incompatible [skip-ci] Packit: cleanup redundant targets and unused anchors [skip-ci] Packit: set fedora-all after F40 EOL Use Fedora 42 instead of 41 in that one conformance test [CI:DOCS] README.md: add openssf passing badge fix(deps): update module github.com/moby/buildkit to v0.22.0 copier: add Ensure and ConditionalRemove [CI:DOCS] update a couple of lists in the build man page build: allow --output to be specified multiple times add: add a new --timestamp flag tests/helpers.bash: add some helpers for parsing images pkg/parse.GetBuildOutput(): use strings.Cut() [skip-ci] Packit: Disable osh_diff_scan internal/util.SetHas(): handle maps of [generic]generic Refactor NewImageSource to add a manifest type abstraction (#5743) [skip-ci] Packit: Ignore ELN and CentOS Stream jobs imagebuildah: select most recent layer for cache [CI:DOCS] Add CNCF roadmap, touchup other CNCF files fix(deps): update module golang.org/x/crypto to v0.38.0 Fix typo in comment (#6167) Support label_users in buildah fix(deps): update module golang.org/x/sync to v0.14.0 fix(deps): update github.com/containers/luksy digest to 4bb4c3f test/serve: fix a descriptor leak, add preliminary directory support fix(deps): update module github.com/opencontainers/cgroups to v0.0.2 fix(deps): update module github.com/moby/buildkit to v0.21.1 Update to avoid deprecated types fix(deps): update module github.com/opencontainers/runc to v1.3.0 Only filter if containerImageRef.created != nil Drop superfluous cast Remove UID/GID scrubbing. fix(deps): update module github.com/seccomp/libseccomp-golang to v0.11.0 cirrus: turn prior fedora testing back on chore(deps): update dependency containers/automation_images to v20250422 fix(deps): update module github.com/docker/docker to v28.1.1+incompatible Bump to Buildah v1.41.0-dev CI vendor_task: pin to go 1.23.3 for now fix(deps): update module github.com/containers/common to v0.63.0 ## v1.40.0 (2025-04-17) Bump c/storage to v1.58.0, c/image v5.35.0, c/common v0.63.0 fix(deps): update module github.com/docker/docker to v28.1.0+incompatible fix(deps): update module github.com/containers/storage to v1.58.0 cirrus: make Total Success wait for rootless integration chroot: use symbolic names when complaining about mount() errors cli: hide the `completion` command instead of disabling it outright Document rw and src options for --mount flag in buildah-run(1) fix(deps): update module github.com/moby/buildkit to v0.21.0 build: add support for inherit-labels chore(deps): update dependency golangci/golangci-lint to v2.1.0 .github: check_cirrus_cron work around github bug stage_executor,getCreatedBy: expand buildArgs for sources correctly Add a link to project governance and MAINTAINERS file fix(deps): update github.com/containers/storage digest to b1d1b45 generateHostname: simplify Use maps.Copy Use slices.Concat Use slices.Clone Use slices.Contains Use for range over integers tests/testreport: don't copy os.Environ Use any instead of interface{} ci: add golangci-lint run with --tests=false ci: add nolintlint, fix found issues copier: rm nolint:unparam annotation .golangci.yml: add unused linter chroot: fix unused warnings copier: fix unused warnings tests/conformance: fix unused warning ci: switch to golangci-lint v2 internal/mkcw: disable ST1003 warnings tests/conformance: do not double import (fix ST1019) cmd/buildah: don't double import (fix ST1019) Do not capitalize error strings cmd/buildah: do not capitalize error strings tests/conformance: fix QF1012 warnings tests/serve: fix QF1012 warning Use strings.ReplaceAll to fix QF1004 warnings Use switch to fix QF1003 warnings Apply De Morgan's law to fix QF1001 warnings Fix QF1007 staticcheck warnings imagebuildah: fix revive warning Rename max variable tests/tools: install lint from binary, use renovate fix(deps): update module github.com/containernetworking/cni to v1.3.0 Update Buildah issue template to new version and support podman build fix(deps): update module golang.org/x/crypto to v0.37.0 stage_executor: reset platform in systemcontext for stages fix(deps): update github.com/opencontainers/runtime-tools digest to 260e151 cmd/buildah: rm unused containerOutputUsingTemplate cmd/buildah: rm unused getDateAndDigestAndSize build: return ExecErrorCodeGeneric when git operation fails add: report error while creating dir for URL source. createPlatformContainer: drop MS_REMOUNT|MS_BIND fix(deps): update module github.com/docker/docker to v28.0.3+incompatible fix: bats won't fail on ! without cleverness feat: use HistoryTimestamp, if set, for oci-archive entries Allow extendedGlob to work with Windows paths fix(deps): update module github.com/moby/buildkit to v0.20.2 fix(deps): update github.com/openshift/imagebuilder digest to e87e4e1 fix(deps): update module github.com/docker/docker to v28.0.2+incompatible fix(deps): update module tags.cncf.io/container-device-interface to v1.0.1 chore(deps): update dependency containers/automation_images to v20250324 vendor: update github.com/opencontainers/selinux to v1.12.0 replace deprecated selinux/label calls vendor: bump c/common to dbeb17e40c80 Use builtin arg defaults from imagebuilder linux: accept unmask paths as glob values vendor: update containers/common Add --parents option for COPY in Dockerfiles fix(deps): update module github.com/opencontainers/runc to v1.2.6 update go.sum from the previous commit fix(deps): update module tags.cncf.io/container-device-interface to v1 chore(deps): update module golang.org/x/net to v0.36.0 [security] packit: remove f40 from copr builds cirrus: update to go 1.23 image vendor bump to golang.org/x/crypto v0.36.0 cirrus: update PRIOR_FEDORA comment github: remove cirrus rerun action fix(deps): update module github.com/containers/common to v0.62.2 fix(deps): update module github.com/containers/image/v5 to v5.34.2 fix: close files properly when BuildDockerfiles exits fix(deps): update module github.com/containers/storage to v1.57.2 stage_executor: history should include heredoc summary correctly fix(deps): update module github.com/containers/common to v0.62.1 github: disable cron rerun action fix(deps): update module github.com/moby/buildkit to v0.20.1 internal/mkcw.Archive(): use github.com/containers/storage/pkg/ioutils [skip-ci] TMT: system tests buildah-build.1.md: secret examples fix(deps): update github.com/containers/luksy digest to 40bd943 fix(deps): update module github.com/opencontainers/image-spec to v1.1.1 fix(deps): update module github.com/containers/image/v5 to v5.34.1 Use UnparsedInstance.Manifest instead of ImageSource.GetManifest fix(deps): update module github.com/opencontainers/runtime-spec to v1.2.1 tests/conformance/testdata/Dockerfile.add: update some URLs Vendor imagebuilder Fix source of OS, architecture and variant chore(deps): update module github.com/go-jose/go-jose/v4 to v4.0.5 [security] fix(deps): update module tags.cncf.io/container-device-interface to v0.8.1 fix(deps): update module github.com/moby/buildkit to v0.20.0 chroot createPlatformContainer: use MS_REMOUNT conformance: make TestCommit and TestConformance parallel cirrus: reduce task timeout mkcw: mkcw_check_image use bats run_with_log test: use /tmp as TMPDIR heredoc: create temp subdirs for each build test: heredoc remove python dependency from test Support the containers.conf container_name_as_hostname option fix(deps): update module github.com/opencontainers/runc to v1.2.5 fix(deps): update module github.com/spf13/cobra to v1.9.0 .cirrus: use more cores for smoke Switch to the CNCF Code of Conduct .cirrus: bump ci resources fix(deps): update module golang.org/x/crypto to v0.33.0 Distinguish --mount=type=cache locations by ownership, too fix(deps): update module golang.org/x/term to v0.29.0 .cirrus: run -race only on non-PR branch unit: deparallize some tests .cirrus: use multiple cpu for unit tests Makefile: use -parallel for go test unit_test: use Parallel test where possible Update module golang.org/x/sys to v0.30.0 Update module golang.org/x/sync to v0.11.0 Update dependency containers/automation_images to v20250131 Bump to Buildah v1.40.0-dev ## v1.39.0 (2025-01-31) Bump c/storage v1.57.1, c/image 5.34.0, c/common v0.62.0 Update module github.com/containers/storage to v1.57.0 CI, .cirrus: parallelize containerized integration ed's comment: cleanup use seperate blobinfocache for flaky test bump CI VMs to 4 CPUs (was: 2) for integration tests cleanup, debug, and disable parallel in blobcache tests bats tests - parallelize pkg/overlay: cleanups RPM: include check section to silence rpmlint RPM: use default gobuild macro on RHEL tests: remove masked /sys/dev/block check vendor to latest c/{common,image,storage} build, run: record hash or digest in image history Accept image names as sources for cache mounts Run(): always clean up options.ExternalImageMounts refactor: replace golang.org/x/exp with stdlib Update to c/image @main fix broken doc link run_freebsd.go: only import runtime-spec once fix(deps): update module github.com/docker/docker to v27.5.1+incompatible bump github.com/vbatts/tar-split Add more checks to the --mount flag parsing logic chroot mount flags integration test: copy binaries fix(deps): update module github.com/moby/buildkit to v0.19.0 relabel(): correct a misleading parameter name Fix TOCTOU error when bind and cache mounts use "src" values define.TempDirForURL(): always use an intermediate subdirectory internal/volume.GetBindMount(): discard writes in bind mounts pkg/overlay: add a MountLabel flag to Options pkg/overlay: add a ForceMount flag to Options Add internal/volumes.bindFromChroot() Add an internal/open package fix(deps): update module github.com/containers/common to v0.61.1 fix(deps): update module github.com/containers/image/v5 to v5.33.1 [CI:DOCS] Touch up changelogs fix(deps): update module github.com/docker/docker to v27.5.0+incompatible copy-preserving-extended-attributes: use a different base image fix(deps): update github.com/containers/luksy digest to a3a812d chore(deps): update module golang.org/x/net to v0.33.0 [security] fix(deps): update module golang.org/x/crypto to v0.32.0 New VM Images fix(deps): update module github.com/opencontainers/runc to v1.2.4 fix(deps): update module github.com/docker/docker to v27.4.1+incompatible fix(deps): update module github.com/containers/ocicrypt to v1.2.1 Add support for --security-opt mask and unmask Allow cache mounts to be stages or additional build contexts [skip-ci] RPM: cleanup changelog conditionals fix(deps): update module github.com/cyphar/filepath-securejoin to v0.3.6 fix(deps): update module github.com/moby/buildkit to v0.18.2 Fix an error message in the chroot unit test copier: use .PAXRecords instead of .Xattrs chroot: on Linux, try to pivot_root before falling back to chroot manifest add: add --artifact-annotation Add context to an error message Update module golang.org/x/crypto to v0.31.0 Update module github.com/opencontainers/runc to v1.2.3 Update module github.com/docker/docker to v27.4.0+incompatible Update module github.com/cyphar/filepath-securejoin to v0.3.5 CI: don't build a binary in the unit tests task CI: use /tmp for $GOCACHE CI: remove dependencies on the cross-build task CI: run cross-compile task with make -j Update module github.com/docker/docker to v27.4.0-rc.4+incompatible Update module github.com/moby/buildkit to v0.18.1 Update module golang.org/x/crypto to v0.30.0 Update golang.org/x/exp digest to 2d47ceb Update github.com/opencontainers/runtime-tools digest to f7e3563 [skip-ci] Packit: remove rhel copr build jobs [skip-ci] Packit: switch to fedora-all for copr Update module github.com/stretchr/testify to v1.10.0 Update module github.com/moby/buildkit to v0.17.2 Makefile: use `find` to detect source files Tests: make _prefetch() parallel-safe Update module github.com/opencontainers/runc to v1.2.2 executor: allow to specify --no-pivot-root Update module github.com/moby/sys/capability to v0.4.0 Makefile: mv codespell config to .codespellrc Fix some codespell errors Makefile,install.md: rm gopath stuff Makefile: rm targets working on .. build: rm exclude_graphdriver_devicemapper tag Makefile: rm unused var Finish updating to go 1.22 CI VMs: bump again Bump to Buidah v1.39.0-dev stage_executor: set avoidLookingCache only if mounting stage imagebuildah: additionalContext is not a local built stage ## v1.38.0 (2024-11-08) Bump to c/common v0.61.0, c/image v5.33.0, c/storage v1.56.0 fix(deps): update module golang.org/x/crypto to v0.29.0 fix(deps): update module github.com/moby/buildkit to v0.17.1 fix(deps): update module github.com/containers/storage to v1.56.0 tests: skip two ulimit tests CI VMs: bump f40 -> f41 tests/tools: rebuild tools when we change versions tests/tools: update golangci-lint to v1.61.0 fix(deps): update module github.com/moby/buildkit to v0.17.0 Handle RUN --mount with relative targets and no configured workdir tests: bud: make parallel-safe fix(deps): update module github.com/opencontainers/runc to v1.2.1 fix(deps): update golang.org/x/exp digest to f66d83c fix(deps): update github.com/opencontainers/runtime-tools digest to 6c9570a tests: blobcache: use unique image name tests: sbom: never write to cwd tests: mkcw: bug fixes, refactor deps: bump runc to v1.2.0 deps: switch to moby/sys/userns tests/test_runner.sh: remove some redundancies Integration tests: run git daemon on a random-but-bind()able port fix(deps): update module github.com/opencontainers/selinux to v1.11.1 go.mod: remove unnecessary replace Document more buildah build --secret options Add support for COPY --exclude and ADD --exclude options fix(deps): update github.com/containers/luksy digest to e2530d6 chore(deps): update dependency containers/automation_images to v20241010 fix(deps): update module github.com/cyphar/filepath-securejoin to v0.3.4 Properly validate cache IDs and sources [skip-ci] Packit: constrain koji job to fedora package to avoid dupes Audit and tidy OWNERS fix(deps): update module golang.org/x/crypto to v0.28.0 tests: add quotes to names vendor: update c/common to latest CVE-2024-9407: validate "bind-propagation" flag settings vendor: switch to moby/sys/capability Don't set ambient capabilities Document that zstd:chunked is downgraded to zstd when encrypting fix(deps): update module github.com/cyphar/filepath-securejoin to v0.3.3 buildah-manifest-create.1: Fix manpage section chore(deps): update dependency ubuntu to v24 Make `buildah manifest push --all` true by default chroot: add newlines at the end of printed error messages Do not error on trying to write IMA xattr as rootless fix: remove duplicate conditions fix(deps): update module github.com/moby/buildkit to v0.16.0 fix(deps): update module github.com/cyphar/filepath-securejoin to v0.3.2 Document how entrypoint is configured in buildah config In a container, try to register binfmt_misc imagebuildah.StageExecutor: clean up volumes/volumeCache build: fall back to parsing a TARGETPLATFORM build-arg `manifest add --artifact`: handle multiple values Packit: split out ELN jobs and reuse fedora downstream targets Packit: Enable sidetags for bodhi updates fix(deps): update module github.com/docker/docker to v27.2.1+incompatible tests/bud.bats: add git source add: add support for git source Add support for the new c/common pasta options vendor latest c/common fix(deps): update module golang.org/x/term to v0.24.0 fix(deps): update module github.com/fsouza/go-dockerclient to v1.12.0 packit: update fedora and epel targets cirrus: disable f39 testing cirrus: fix fedora names update to go 1.22 Vendor c/common:9d025e4cb348 copier: handle globbing with "**" path components fix(deps): update golang.org/x/exp digest to 9b4947d fix(deps): update github.com/containers/luksy digest to 2e7307c imagebuildah: make scratch config handling toggleable fix(deps): update module github.com/docker/docker to v27.2.0+incompatible Add a validation script for Makefile $(SOURCES) fix(deps): update module github.com/openshift/imagebuilder to v1.2.15 New VMs Update some godocs, use 0o to prefix an octal in a comment buildah-build.1.md: expand the --layer-label description fix(deps): update module github.com/containers/common to v0.60.2 run: fix a nil pointer dereference on FreeBSD CI: enable the whitespace linter Fix some govet linter warnings Commit(): retry committing to local storage on storage.LayerUnknown CI: enable the gofumpt linter conformance: move weirdly-named files out of the repository fix(deps): update module github.com/docker/docker to v27.1.2+incompatible fix(deps): update module github.com/containers/common to v0.60.1 *: use gofmt -s, add gofmt linter *: fix build tags fix(deps): update module github.com/containers/image/v5 to v5.32.1 Add(): re-escape any globbed items that included escapes conformance tests: use mirror.gcr.io for most images unit tests: use test-specific policy.json and registries.conf fix(deps): update module golang.org/x/sys to v0.24.0 Update to spun-out "github.com/containerd/platforms" Bump github.com/containerd/containerd test/tools/Makefile: duplicate the vendor-in-container target linters: unchecked error linters: don't end loop iterations with "else" when "then" would linters: unused arguments shouldn't have names linters: rename checkIdsGreaterThan5() to checkIDsGreaterThan5() linters: don't name variables "cap" `make lint`: use --timeout instead of --deadline Drop the e2e test suite fix(deps): update module golang.org/x/crypto to v0.26.0 fix(deps): update module github.com/onsi/gomega to v1.34.1 `make vendor-in-container`: use the caller's Go cache if it exists fix(deps): fix test/tools ginkgo typo fix(deps): update module github.com/onsi/ginkgo/v2 to v2.19.1 Update to keep up with API changes in storage fix(deps): update github.com/containers/luksy digest to 1f482a9 install: On Debian/Ubuntu, add installation of libbtrfs-dev fix(deps): update module golang.org/x/sys to v0.23.0 fix(deps): update golang.org/x/exp digest to 8a7402a fix(deps): update module github.com/fsouza/go-dockerclient to v1.11.2 Use Epoch: 2 and respect the epoch in dependencies. Bump to Buildah v1.38.0-dev AddAndCopyOptions: add CertPath, InsecureSkipTLSVerify, Retry fields Add PrependedLinkedLayers/AppendedLinkedLayers to CommitOptions integration tests: teach starthttpd() about TLS and pid files ## v1.37.0 (2024-07-26) Bump c/storage, c/image, c/common for v1.37.0 "build with basename resolving user arg" tests: correct ARG use bud-multiple-platform-no-run test: correct ARG use imagebuildah: always have default values for $TARGET... args ready bump github.com/openshift/imagebuilder to v1.2.14 fix(deps): update module github.com/docker/docker to v27.1.1+incompatible fix(deps): update module github.com/cyphar/filepath-securejoin to v0.3.1 fix(deps): update module github.com/docker/docker to v27.1.0+incompatible CI: use local registry, part 2 of 2 CI: use local registry, part 1 of 2 fix(deps): update module github.com/fsouza/go-dockerclient to v1.11.1 Revert "fix(deps): update github.com/containers/image/v5 to v5.31.1" Replace libimage.LookupReferenceFunc with the manifests version conformance tests: enable testing CompatVolumes conformance tests: add a test that tries to chown a volume imagebuildah: make traditional volume handling not the default StageExecutor.prepare(): mark base image volumes for preservation fix(deps): update module github.com/containers/image/v5 to v5.31.1 Vendor in latest containers/(common, storage, image) fix(deps): update module golang.org/x/term to v0.22.0 fix(deps): update module golang.org/x/sys to v0.22.0 fix(deps): update golang.org/x/exp digest to 7f521ea fix(deps): update github.com/containers/luksy digest to a8846e2 imagebuildah.StageExecutor.Copy(): reject new flags for now bump github.com/openshift/imagebuilder to v1.2.11 Rework parsing of --pull flags fix(deps): update module github.com/containers/image/v5 to v5.31.1 imagebuildah.StageExecutor.prepare(): log the --platform flag CI VMs: bump buildah copy: preserve owner info with --from= a container or image conformance tests: enable testing CompatSetParent containerImageRef.NewImageSource(): move the FROM comment to first commit: set "parent" for docker format only when requested Update godoc for Builder.EnsureContainerPathAs fix(deps): update module github.com/spf13/cobra to v1.8.1 fix(deps): update module github.com/containernetworking/cni to v1.2.0 fix(deps): update module github.com/opencontainers/runc to v1.1.13 Change default for podman build to --pull missing fix(deps): update module github.com/containers/common to v0.59.1 Clarify definition of --pull options buildah: fix a nil pointer reference on FreeBSD Use /var/tmp for $TMPDIR for vfs conformance jobs Cirrus: run `df` during job setup conformance: use quay.io/libpod/centos:7 instead of centos:8 Stop setting "parent" in docker format conformance: check if workdir trims path separator suffixes push integration test: pass password to docker login via stdin Re-enable the "copy with chown" conformance test healthcheck: Add support for `--start-interval` fix(deps): update module github.com/docker/docker to v26.1.4+incompatible fix(deps): update module github.com/containerd/containerd to v1.7.18 tests: set _CONTAINERS_USERNS_CONFIGURED=done for libnetwork Cross-build on Fedora Drop copyStringSlice() and copyStringStringMap() fix(deps): update module golang.org/x/crypto to v0.24.0 fix(deps): update module github.com/openshift/imagebuilder to v1.2.10 Provide an uptime_netbsd.go Spell unix as "!windows" Add netbsd to lists-of-OSes fix(deps): update golang.org/x/exp digest to fd00a4e [skip-ci] Packit: enable c10s downstream sync CI VMs: bump, to debian with cgroups v2 Document when BlobDirectory is overridden fix secret mounts for env vars when using chroot isolation Change to take a types.ImageReference arg imagebuildah: Support custom image reference lookup for cache push/pull fix(deps): update module github.com/onsi/ginkgo/v2 to v2.19.0 Bump to v1.37.0-dev CI: Clarify Debian use for conformance tests ## v1.36.0 (2024-05-23) build: be more selective about specifying the default OS Bump to c/common v0.59.0 Fix buildah prune --help showing the same example twice fix(deps): update module github.com/onsi/ginkgo/v2 to v2.18.0 fix(deps): update module github.com/containers/image/v5 to v5.31.0 bud tests: fix breakage when vendoring into podman Integration tests: fake up a replacement for nixery.dev/shell copierWithSubprocess(): try to capture stderr on io.ErrClosedPipe Don't expand RUN heredocs ourselves, let the shell do it Don't leak temp files on failures Add release note template to split dependency chores fix CentOS/RHEL build - no BATS there fix(deps): update module github.com/containers/luksy to v0.0.0-20240506205542-84b50f50f3ee Address CVE-2024-3727 chore(deps): update module github.com/opencontainers/runtime-spec to v1.2.0 Builder.cdiSetupDevicesInSpecdefConfig(): use configured CDI dirs Setting --arch should set the TARGETARCH build arg fix(deps): update module golang.org/x/exp to v0.0.0-20240416160154-fe59bbe5cc7f [CI:DOCS] Add link to Buildah image page to README.md Don't set GOTOOLCHAIN=local fix(deps): update module github.com/cyphar/filepath-securejoin to v0.2.5 Makefile: set GOTOOLCHAIN=local Integration tests: switch some base images containerImageRef.NewImageSource: merge the tar filters fix(deps): update module github.com/onsi/ginkgo/v2 to v2.17.2 fix(deps): update module github.com/containers/luksy to v0.0.0-20240408185936-afd8e7619947 Disable packit builds for centos-stream+epel-next-8 Makefile: add missing files to $(SOURCES) CI VMs: bump to new versions with tmpfs /tmp chore(deps): update module golang.org/x/net to v0.23.0 [security] integration test: handle new labels in "bud and test --unsetlabel" Switch packit configuration to use epel-9-$arch ... Give unit tests a bit more time Integration tests: remove a couple of duplicated tests Integration tests: whitespace tweaks Integration tests: don't remove images at start or end of test Integration tests: use cached images more Integration tests _prefetch: use registry configs internal: use fileutils.(Le|E)xists pkg/parse: use fileutils.(Le|E)xists buildah: use fileutils.(Le|E)xists chroot: use fileutils.(Le|E)xists vendor: update containers/(common|storage) Fix issue/pr lock workflow [CI:DOCS] Add golang 1.21 update warning heredoc: honor inline COPY irrespective of ignorefiles Update install.md source-push: add support for --digestfile Fix caching when mounting a cached stage with COPY/ADD fix(deps): update github.com/containers/luksy digest to 3d2cf0e Makefile: softcode `strip`, use it from env var Man page updates Add support for passing CDI specs to --device Update comments on some API objects pkg/parse.DeviceFromPath(): dereference src symlinks fix(deps): update module github.com/onsi/ginkgo/v2 to v2.17.1 ## v1.35.0 (2024-03-06) fix(deps): update module github.com/stretchr/testify to v1.9.0 cgroups: reuse version check from c/common Update vendor of containers/(common,image) fix(deps): update github.com/containers/storage digest to eadc620 fix(deps): update github.com/containers/luksy digest to ceb12d4 fix(deps): update github.com/containers/image/v5 digest to cdc6802 manifest add: complain if we get artifact flags without --artifact Use retry logic from containers/common Vendor in containers/(storage,image,common) Update module golang.org/x/crypto to v0.20.0 Add comment re: Total Success task name tests: skip_if_no_unshare(): check for --setuid Properly handle build --pull=false [skip-ci] Update tim-actions/get-pr-commits action to v1.3.1 Update module go.etcd.io/bbolt to v1.3.9 Revert "Reduce official image size" Update module github.com/opencontainers/image-spec to v1.1.0 Reduce official image size Build with CNI support on FreeBSD build --all-platforms: skip some base "image" platforms Bump main to v1.35.0-dev Vendor in latest containers/(storage,image,common) Split up error messages for missing --sbom related flags `buildah manifest`: add artifact-related options cmd/buildah/manifest.go: lock lists before adding/annotating/pushing cmd/buildah/manifest.go: don't make struct declarations aliases Use golang.org/x/exp/slices.Contains Disable loong64 again Fix a couple of typos in one-line comments egrep is obsolescent; use grep -E Try Cirrus with a newer VM version Set CONTAINERS_CONF in the chroot-mount-flags integration test Update to match dependency API update Update github.com/openshift/imagebuilder and containers/common docs: correct default authfile path fix(deps): update module github.com/containerd/containerd to v1.7.13 tests: retrofit test for heredoc summary build, heredoc: show heredoc summary in build output manifest, push: add support for --retry and --retry-delay fix(deps): update github.com/openshift/imagebuilder digest to b767bc3 imagebuildah: fix crash with empty RUN fix(deps): update github.com/containers/luksy digest to b62d551 fix(deps): update module github.com/opencontainers/runc to v1.1.12 [security] fix(deps): update module github.com/moby/buildkit to v0.12.5 [security] Make buildah match podman for handling of ulimits docs: move footnotes to where they're applicable Allow users to specify no-dereference Run codespell on code Fix FreeBSD version parsing Fix a build break on FreeBSD Remove a bad FROM line fix(deps): update module github.com/onsi/gomega to v1.31.1 fix(deps): update module github.com/opencontainers/image-spec to v1.1.0-rc6 docs: use reversed logo for dark theme in README build,commit: add --sbom to scan and produce SBOMs when committing commit: force omitHistory if the parent has layers but no history docs: fix a couple of typos internal/mkcw.Archive(): handle extra image content stage_executor,heredoc: honor interpreter in heredoc stage_executor,layers: burst cache if heredoc content is changed fix(deps): update module golang.org/x/crypto to v0.18.0 Replace map[K]bool with map[K]struct{} where it makes sense fix(deps): update module golang.org/x/sync to v0.6.0 fix(deps): update module golang.org/x/term to v0.16.0 Bump CI VMs Replace strings.SplitN with strings.Cut fix(deps): update github.com/containers/storage digest to ef81e9b fix(deps): update github.com/containers/image/v5 digest to 1b221d4 fix(deps): update module github.com/fsouza/go-dockerclient to v1.10.1 Document use of containers-transports values in buildah fix(deps): update module golang.org/x/crypto to v0.17.0 [security] chore(deps): update dependency containers/automation_images to v20231208 manifest: addCompression use default from containers.conf commit: add a --add-file flag mkcw: populate the rootfs using an overlay chore(deps): update dependency containers/automation_images to v20230517 [skip-ci] Update actions/stale action to v9 fix(deps): update module github.com/containernetworking/plugins to v1.4.0 fix(deps): update github.com/containers/image/v5 digest to 7a40fee Bump to v1.34.1-dev Ignore errors if label.Relabel returns ENOSUP ## v1.34.0 (2023-12-11) vendor: update c/{common,image,storage} run: Allow using just one jail per container on FreeBSD Remove makefile targets entrypoint{,.gz} for non x86_64 ## v1.33.2 (2023-11-22) Update minimum to golang 1.20 fix(deps): update module github.com/fsouza/go-dockerclient to v1.10.0 fix(deps): update module github.com/moby/buildkit to v0.12.3 Bump to v1.33.2-dev ## v1.33.1 (2023-11-18) fix(deps): update module github.com/moby/buildkit to v0.11.4 [security] test,heredoc: use fedora instead of docker.io/library/python:latest Bump to v1.33.1-dev ## v1.33.0 (2023-11-17) Never omit layers for emptyLayer instructions when squashing/cwing Add OverrideChanges and OverrideConfig to CommitOptions buildah: add heredoc support for RUN, COPY and ADD vendor: bump imagebuilder to v1.2.6-0.20231110114814-35a50d57f722 conformance tests: archive the context directory as 0:0 (#5171) blobcacheinfo,test: blobs must be resued when pushing across registry Bump c/storage v1.51.0, c/image v5.29.0, c/common v0.57.0 pkg/util.MirrorToTempFileIfPathIsDescriptor(): don't leak an fd StageExecutor.Execute: force a commit for --unsetenv, too Increase a copier+chroot test timeout Add support for --compat-auth-file in login/logout Update existing tests for error message change Update c/image and c/common to latest fix(deps): update module github.com/containerd/containerd to v1.7.9 build: downgrade to go 1.20 Add godoc for pkg/parse.GetTempDir conformance tests: use go-dockerclient for BuildKit builds Make TEE types case-insensitive fix(deps): update module golang.org/x/crypto to v0.15.0 Tweak some help descriptions Stop using DefaultNetworkSysctl and use containers.conf only Implement ADD checksum flag #5135 vendor of openshift/imagebuilder #5135 Pass secrets from the host down to internal podman containers Update cirrus and version of golang image: replace GetStoreImage with ResolveReference vendor: bump c/image to 373c52a9466f pkg/parse.Platform(): minor simplification createConfigsAndManifests: clear history before cw-specific logic Use a constant definition instead of "scratch" conformance: use require.NoErrorf() more fix(deps): update module golang.org/x/term to v0.14.0 fix(deps): update module golang.org/x/sync to v0.5.0 fix(deps): update module github.com/spf13/cobra to v1.8.0 fix(deps): update module golang.org/x/sys to v0.14.0 fix(deps): update github.com/containers/common digest to 8354404 fix(deps): update module github.com/opencontainers/runc to v1.1.10 fix(deps): update github.com/containers/luksy digest to b5a7f79 Log the platform for build errors during multi-platform builds Use mask definitions from containers/common Vendor in latest containers/common fix(deps): update module github.com/containerd/containerd to v1.7.8 fix(deps): update module go.etcd.io/bbolt to v1.3.8 container.conf: support attributed string slices fix(deps): update module sigs.k8s.io/yaml to v1.4.0 Use cutil.StringInSlice rather then contains Add --no-hostname option to buildah containers vendor c/common: appendable containers.conf strings, Part 1 fix(deps): update module github.com/onsi/gomega to v1.28.1 chroot.setupChrootBindMounts: pay more attention to flags chore(deps): update dependency containers/automation_images to v20231004 Vendor containers/common chore(deps): update module golang.org/x/net to v0.17.0 [security] run: use internal.GetTempDir with os.MkdirTemp fix(deps): update module github.com/containerd/containerd to v1.7.7 imagebuildah,multi-stage: do not remove base images gitignore: add mkcw binary mkcw: remove entrypoint binaries fix(deps): update module golang.org/x/crypto to v0.14.0 fix(deps): update module golang.org/x/sys to v0.13.0 fix(deps): update module golang.org/x/sync to v0.4.0 Update some comments related to confidential workload Use the parent's image ID in the config that we pass to imagebuilder fix(deps): update github.com/containers/common digest to 8892536 fix(deps): update github.com/containers/luksy digest to 6df88cb bug: Ensure the mount type is always BindMount by default Protocol can be specified with --port. Ex. --port 514/udp fix(deps): update module github.com/onsi/gomega to v1.28.0 build,config: add support for --unsetlabel tests/bud: add tests [CI:BUILD] Packit: tag @containers/packit-build on copr build failures stage_executor: allow images without layers vendor of containers/common Removing selinux_tag.sh as no longer needed after 580356f [NO NEW TESTS NEEDED] add/copy: make sure we handle relative path names correctly fix(deps): update module github.com/opencontainers/image-spec to v1.1.0-rc5 Bump to v1.33.0-dev imagebuildah: consider ignorefile with --build-context ## v1.32.0 (2023-09-14) GetTmpDir is not using ImageCopyTmpdir correctly Run codespell on code Bump vendor containers/(common, storage, image) Cirrus: Remove multi-arch buildah image builds fix(deps): update module github.com/containerd/containerd to v1.7.6 Split GetTempDir from internal/util Move most of internal/parse to internal/volumes copier: remove libimage dependency via util package Add some docs for `build --cw`, `commit --cw`, and `mkcw` Add `buildah mkcw`, add `--cw` to `buildah commit` and `buildah build` Make sure that pathnames picked up from the environment are absolute fix(deps): update module github.com/cyphar/filepath-securejoin to v0.2.4 fix(deps): update module github.com/docker/docker to v24.0.6+incompatible Don't try to look up names when committing images fix(deps): update module golang.org/x/crypto to v0.13.0 docs: use valid github repo fix(deps): update module golang.org/x/sys to v0.12.0 vendor containers/common@12405381ff45 push: --force-compression should be true with --compression-format Update module github.com/containerd/containerd to v1.7.5 [skip-ci] Update tim-actions/commit-message-checker-with-regex action to v0.3.2 docs: add reference to oci-hooks Support passing of ULimits as -1 to mean max GHA: Attempt to fix discussion_lock workflow Fixing the owner of the storage.conf. pkg/chrootuser: Ignore comments when parsing /etc/group on FreeBSD Use buildah repo rather then podman repo GHA: Closed issue/PR comment-lock test fix(deps): update module github.com/containers/storage to v1.49.0 chore(deps): update dependency containers/automation_images to v20230816 Replace troff code with markdown in buildah-{copy,add}.1.md [CI:BUILD] rpm: spdx compatible license field executor: build-arg warnings must honor global args fix(deps): update module github.com/containers/ocicrypt to v1.1.8 chroot: `setSeccomp` add support for `ArchPARISC(64)` and `ArchRISCV64` make,cross: restore loong64 Clear CommonBuildOpts when loading Builder status buildah/push/manifest-push: add support for --force-compression vendor: bump c/common to v0.55.1-0.20230811093040-524b4d5c12f9 chore(deps): update dependency containers/automation_images to v20230809 [CI:BUILD] RPM: fix buildtags fix(deps): update module github.com/opencontainers/runc to v1.1.9 chore(deps): update dependency ubuntu to v22 chore(deps): update dependency containers/automation_images to v20230807 [CI:BUILD] Packit: add fedora-eln targets [CI:BUILD] RPM: build docs with vendored go-md2man packit: Build PRs into default packit COPRs Update install.md Update install.md changes current Debian stable version name fix(deps): update module golang.org/x/term to v0.11.0 fix(deps): update module golang.org/x/crypto to v0.12.0 tests: fix layer-label tests buildah: add --layer-label for setting labels on layers Cirrus: container/rootless env. var. passthrough Cirrus: Remove duplicate env. var. definitions fix(deps): update github.com/containers/storage digest to c3da76f Add a missing .Close() call on an ImageSource Create only a reference when that's all we need Add a missing .Close() call on an ImageDestination CI:BUILD] RPM: define gobuild macro for rhel/centos stream manifest/push: add support for --add-compression manifest/inspect: add support for tls-verify and authfile vendor: bump c/common to v0.55.1-0.20230727095721-647ed1d4d79a vendor: bump c/image to v5.26.1-0.20230726142307-8c387a14f4ac fix(deps): update module github.com/containerd/containerd to v1.7.3 fix(deps): update module github.com/onsi/gomega to v1.27.10 fix(deps): update module github.com/docker/docker to v24.0.5+incompatible fix(deps): update module github.com/containers/image/v5 to v5.26.1 fix(deps): update module github.com/opencontainers/runtime-spec to v1.1.0 Update vendor of containers/(storage,image,common) fix(deps): update module github.com/opencontainers/runc to v1.1.8 [CI:BUILD] Packit: remove pre-sync action fix(deps): update module github.com/containers/common to v0.55.2 [CI:BUILD] Packit: downstream task script needs GOPATH Vendor in containers/(common, image, storage) fix(deps): update module golang.org/x/term to v0.10.0 [CI:BUILD] Packit: fix pre-sync action for downstream tasks contrib/buildahimage: set config correctly for rootless build user fix(deps): update module github.com/opencontainers/image-spec to v1.1.0-rc4 Bump to v1.32.0-dev Update debian install instructions pkg/overlay: add limited support for FreeBSD ## v1.31.0 (2023-06-30) Bump c/common to 0.55.1 and c/image to 5.26.1 Bump c/image to 5.26.0 and c/common to 0.54.0 vendor: update c/{common,image,storage} to latest chore: pkg imported more than once buildah: add pasta(1) support use slirp4netns package from c/common update c/common to latest add hostname to /etc/hosts when running with host network vendor: update c/common to latest [CI:BUILD] Packit: add jobs for downstream Fedora package builds fix(deps): update module golang.org/x/sync to v0.3.0 fix(deps): update module golang.org/x/crypto to v0.10.0 Add smoke tests for encryption CLI helpers fix(deps): update module golang.org/x/term to v0.9.0 fix(deps): update module github.com/opencontainers/runtime-spec to v1.1.0-rc.3 Remove device mapper support Remove use of deprecated tar.TypeRegA Update tooling to support newer golangci-lint Make cli.EncryptConfig,DecryptConfig, GetFormat public Don't decrypt images by default fix(deps): update module github.com/onsi/gomega to v1.27.8 fix(deps): update github.com/containers/storage digest to 3f3fb2f Renovate: Don't touch fragile test stuffs [CI:DOCS] Update comment to remove ambiguity fix(deps): update github.com/containers/image/v5 digest to abe5133 fix(deps): update module github.com/sirupsen/logrus to v1.9.3 fix(deps): update module github.com/containerd/containerd to v1.7.2 Explicitly ref. quay images for CI At startup, log the effective capabilities for debugging parse: use GetTempDir from internal utils GetTmpDir: honor image_copy_tmp_dir from containers.conf docs/Makefile: don't show sed invocations CI: Support testing w/ podman-next COPR packages intermediate-images inherit-label test: make it debuggable fix(deps): update github.com/containers/common digest to 462ccdd Add a warning to `--secret` docs vendor: bump c/storage to v1.46.2-0.20230526114421-55ee2d19292f executor: apply label to only final stage remove registry.centos.org Go back to setting SysProcAttr.Pdeathsig for child processes Fix auth.json path (validated on Fedora 38) wq Signed-off-by: Andreas Mack fix(deps): update module github.com/stretchr/testify to v1.8.3 CI: fix test broken by renovatebot chore(deps): update quay.io/libpod/testimage docker tag to v20221018 fix(deps): update module github.com/onsi/gomega to v1.27.7 test: use debian instead of docker.io/library/debian:testing-slim vendor: bump logrus to 1.9.2 [skip-ci] Update tim-actions/get-pr-commits action to v1.3.0 Revert "Proof of concept: nightly dependency treadmill" fix(deps): update module github.com/sirupsen/logrus to v1.9.1 vendor in containers/(common,storage,image) fix(deps): update module github.com/docker/distribution to v2.8.2+incompatible run: drop Pdeathsig chroot: lock thread before setPdeathsig tests: add a case for required=false fix(deps): update module github.com/openshift/imagebuilder to v1.2.5 build: validate volumes on backend secret: accept required flag w/o value fix(deps): update module github.com/containerd/containerd to v1.7.1 fix(deps): update module golang.org/x/crypto to v0.9.0 Update the demos README file to fix minor typos fix(deps): update module golang.org/x/sync to v0.2.0 fix(deps): update module golang.org/x/term to v0.8.0 manifest, push: use source as destination if not specified run,mount: remove path only if they didnt pre-exist Cirrus: Fix meta task failing to find commit parse: filter edge-case for podman-remote fix(deps): update module github.com/opencontainers/runc to v1.1.7 fix(deps): update module github.com/docker/docker to v23.0.5+incompatible build: --platform must accept only arch fix(deps): update module github.com/containers/common to v0.53.0 makefile: increase conformance timeout Cap suffixDigitsModulo to a 9-digits suffix. Rename conflict to suffixDigitsModulo fix(deps): update module github.com/opencontainers/runtime-spec to v1.1.0-rc.2 fix(deps): update module github.com/opencontainers/runc to v1.1.6 chore(deps): update centos docker tag to v8 Clarify the need for qemu-user-static package chore(deps): update quay.io/centos/centos docker tag to v8 Renovate: Ensure test/tools/go.mod is managed Revert "buildah image should not enable fuse-overlayfs for rootful mode" Bump to v1.31.0-dev parse: add support for relabel bind mount option ## v1.30.0 (2023-04-06) fix(deps): update module github.com/opencontainers/runc to v1.1.5 fix(deps): update module github.com/fsouza/go-dockerclient to v1.9.7 buildah image should not enable fuse-overlayfs for rootful mode stage_executor: inline network add default string fix(deps): update module github.com/containers/common to v0.51.2 chore(deps): update dependency containers/automation_images to v20230330 fix(deps): update module github.com/docker/docker to v23.0.2+incompatible chore(deps): update dependency containers/automation_images to v20230320 fix(deps): update module github.com/onsi/gomega to v1.27.6 fix(deps): update github.com/opencontainers/runtime-tools digest to e931285 [skip-ci] Update actions/stale action to v8 test: don't allow to override io.buildah.version executor: only apply label on the final stage Update docs/buildah-build.1.md update build instruction for Ubuntu code review build: accept arguments from file with --build-arg-file run_linux: Update heuristic for mounting /sys [CI:BUILD] Packit: Enable Copr builds on PR and commit to main fix(deps): update module github.com/fsouza/go-dockerclient to v1.9.6 Update to Go 1.18 Disable dependabot in favor of renovate chore(deps): update dependency containers/automation_images to v20230314 Fix requiring tests on Makefile changes Vendor in latest containers/(storage, common, image) imagebuildah: set len(short_image_id) to 12 Re-enable conformance tests Skip conformance test failures with Docker 23.0.1 Cirrus: Replace Ubuntu -> Debian SID run: add support for inline --network in RUN stmt vendor: bump imagebuilder to a3c3f8358ca31b1e4daa6 stage_executor: attempt to push cache only when cacheKey is valid Add "ifnewer" as option in help message for pull command build: document behaviour of buildah's distributed cache fix(deps): update module golang.org/x/term to v0.6.0 Add default list of capabilities required to run buildah in a container executor,copy: honor default ARG value while eval stage sshagent: use ExtendedAgent instead of Agent tests/bud: remove unwated test executor: do not warn on builtin default args executor: don't warn about unused TARGETARCH,TARGETOS,TARGETPLATFORM Fix tutorial for rootless mode Vendor in latest containers/(common, storage, image) Ignore the base image's base image annotations fix(deps): update module github.com/fsouza/go-dockerclient to v1.9.5 build(deps): bump github.com/containers/storage from 1.45.3 to 1.45.4 Vendor in latest containers/common docs/tutorials/04: add defaults for Run() imagebuildah.StageExecutor: suppress bogus "Pushing cache []:..." executor: also add stage with no children to cleanupStages [CI:BUILD] copr: fix el8 builds Fix documentation on which Capabilities are allowed by default Skip subject-length validation for renovate PRs Temporarily hard-skip bud-multiple-platform-values test fix(deps): update github.com/openshift/imagebuilder digest to 86828bf build(deps): bump github.com/containerd/containerd from 1.6.16 to 1.6.17 build(deps): bump tim-actions/get-pr-commits from 1.1.0 to 1.2.0 build(deps): bump github.com/containers/image/v5 from 5.24.0 to 5.24.1 [skip-ci] Update tim-actions/get-pr-commits digest to 55b867b build(deps): bump github.com/opencontainers/selinux build(deps): bump golang.org/x/crypto from 0.5.0 to 0.6.0 Add renovate configuration Run codespell on codebase login: support interspersed args for password conformance: use scratch for minimal test pkg/parse: expose public CleanCacheMount API build(deps): bump go.etcd.io/bbolt from 1.3.6 to 1.3.7 build(deps): bump github.com/containerd/containerd from 1.6.15 to 1.6.16 docs: specify order preference for FROM Bump to v1.30.0-dev ## v1.29.0 (2023-01-25) tests: improve build-with-network-test Bump c/storagev1.45.3, c/imagev5.24.0, c/commonv0.51.0 build(deps): bump github.com/onsi/gomega from 1.25.0 to 1.26.0 Flake 3710 has been closed. Reenable the test. [CI:DOCS] Fix two diversity issues in a tutorial build(deps): bump github.com/fsouza/go-dockerclient from 1.9.2 to 1.9.3 vendor in latests containers/(storage, common, image) fix bud-multiple-platform-with-base-as-default-arg flake stage_executor: while mounting stages use freshly built stage build(deps): bump github.com/fsouza/go-dockerclient from 1.9.0 to 1.9.2 build(deps): bump github.com/onsi/gomega from 1.24.2 to 1.25.0 vendor in latests containers/(storage, common, image, ocicyrpt) [Itests: change the runtime-flag test for crun [CI:DOCS] README: drop sudo Fix multi-arch manifest-list build timeouts Cirrus: Update VM Images bud: Consolidate multiple synthetic LABEL instructions build, secret: allow realtive mountpoints wrt to work dir fixed squash documentation build(deps): bump github.com/containerd/containerd from 1.6.14 to 1.6.15 Correct minor comment Vendor in latest containers/(common, image, storage) system tests: remove unhelpful assertions buildah: add prune command and expose CleanCacheMount API vendor: bump c/storage to a747b27 Add support for --group-add to buildah from build(deps): bump actions/stale from 6 to 7 Add documentation for buildah build --pull=missing build(deps): bump github.com/containerd/containerd from 1.6.12 to 1.6.14 build(deps): bump github.com/docker/docker parse: default ignorefile must not point to symlink outside context buildah: wrap network setup errors build, mount: allow realtive mountpoints wrt to work dir Update to F37 CI VM Images, re-enable prior-fedora Update vendor or containers/(image, storage, common) build(deps): bump golang.org/x/crypto from 0.3.0 to 0.4.0 Update contact information build(deps): bump golang.org/x/term from 0.2.0 to 0.3.0 Replace io/ioutil calls with os calls [skip-ci] GHA/Cirrus-cron: Fix execution order Vendor in containers/common build(deps): bump golang.org/x/sys from 0.2.0 to 0.3.0 remote-cache: support multiple sources and destinations Update c/storage after https://github.com/containers/storage/pull/1436 util.SortMounts(): make the returned order more stable version: Bump to 1.29.0-dev [CI:BUILD] Cirrus: Migrate OSX task to M1 Update vendor of containers/(common, storage, image) mount=type=cache: seperate cache parent on host for each user Fix installation instructions for Gentoo Linux build(deps): bump github.com/containerd/containerd from 1.6.9 to 1.6.10 GHA: Reuse both cirrus rerun and check workflows Vendor in latest containers/(common,image,storage) build(deps): bump github.com/onsi/gomega from 1.24.0 to 1.24.1 copier.Put(): clear up os/syscall mode bit confusion build(deps): bump golang.org/x/sys from 0.1.0 to 0.2.0 Use TypeBind consistently to name bind/nullfs mounts Add no-new-privileges flag Update vendor of containers/(common, image, storage) imagebuildah:build with --all-platforms must honor args for base images codespell code Expand args and env when using --all-platforms build(deps): bump github.com/onsi/gomega from 1.23.0 to 1.24.0 GHA: Simplify Cirrus-Cron check slightly Stop using ubi8 remove unnecessary (hence misleading) rmi chroot: fix mounting of ro bind mounts executor: honor default ARG value while eval base name userns: add arbitrary steps/stage to --userns=auto test Don't set allow.mount in the vnet jail on Freebsd copier: Preserve file flags when copying archives on FreeBSD Remove quiet flag, so that it works in podman-remote test: fix preserve rootfs with --mount for podman-remote test: fix prune logic for cache-from after adding content summary vendor in latest containers/(storage, common, image) Fix RUN --mount=type=bind,from= not preserving rootfs of stage Define and use a safe, reliable test image Fix word missing in Container Tools Guide Makefile: Use $(MAKE) to start sub-makes in install.tools imagebuildah: pull cache from remote repo after adding content summary Makefile: Fix install on FreeBSD Ensure the cache volume locks are unlocked on all paths Vendor in latest containers/(common,storage) Simplify the interface of GetCacheMount and getCacheMount Fix cache locks with multiple mounts Remove calls to Lockfile.Locked() Maintain cache mount locks as lock objects instead of paths test: cleaning cache must not clean lockfiles run: honor lockfiles for multiple --mount instruction mount,cache: lockfiles must not be part of users cache content Update vendor containers/(common,image,storage) [CI:BUILD] copr: buildah rpm should depend on containers-common-extra pr-should-include-tests: allow specfile, golangci build(deps): bump dawidd6/action-send-mail from 3.7.0 to 3.7.1 build(deps): bump github.com/docker/docker build(deps): bump github.com/fsouza/go-dockerclient from 1.8.3 to 1.9.0 Update vendor containers/(common,image,storage) build(deps): bump actions/upload-artifact from 2 to 3 build(deps): bump actions/checkout from 2 to 3 build(deps): bump actions/stale from 1 to 6 build(deps): bump dawidd6/action-send-mail from 2.2.2 to 3.7.0 build(deps): bump tim-actions/get-pr-commits from 1.1.0 to 1.2.0 sshagent: LockOSThread before setting SocketLabel Update tests for error message changes Update c/image after https://github.com/containers/image/pull/1299 Fix ident for dependabot gha block build(deps): bump github.com/containers/ocicrypt from 1.1.5 to 1.1.6 Fix man pages to match latest cobra settings build(deps): bump github.com/spf13/cobra from 1.5.0 to 1.6.0 build(deps): bump github.com/onsi/gomega from 1.20.2 to 1.22.1 test: retrofit 'bud with undefined build arg directory' imagebuildah: warnOnUnsetBuildArgs while processing stages from executor Update contrib/buildahimage/Containerfile Cirrus CI add flavor parameter Correction - `FLAVOR` not `FLAVOUR` Changed build argument from `RELEASE` to `FLAVOUR` Combine buildahimage Containerfiles bud.bats refactoring: $TEST_SCRATCH_DIR, part 2 of 2 bud.bats refactoring: $TEST_SCRATCH_DIR, part 1 of 2 System test cleanup: document, clarify, fix test: removing unneeded/expensive COPY test: warning behaviour for unset/set TARGETOS,TARGETARCH,TARGETPLATFORM Bump to v1.28.1-dev ## v1.28.0 (2022-09-30) Update vendor containers/(common,image) [CI:DOCS] Add quay-description update reminder vendor: bump c/common to v0.49.2-0.20220929111928-2d1b45ae2423 build(deps): bump github.com/opencontainers/selinux Vendor in latest containers/storage Changing shell list operators from `;` to `&&` Fix buildahimage container.conf permissions regression Set sysctls from containers.conf refactor: stop using Normalize directly from containerd package config,builder: process variant while populating image spec Proof of concept: nightly dependency treadmill Run codespell on code Check for unset build args after TARGET args pkg/cli: improve completion test vendor in latest containers/(common,storage,image) copier: work around freebsd bug for "mkdir /" vendor: update c/image test: run in the host cgroup namespace vendor: update c/storage vendor: update c/common cmd: check for user UID instead of privileges run,build: conflict --isolation=chroot and --network Fix broken dns test (from merge collision) Fix stutters Fix broken command completion buildah bud --network=none should have no network build: support --skip-unused-stages for multi-stage builds Prevent use of --dns* options with --net=none buildah: make --cache-ttl=0s equivalent to --no-cache parse: make processing flags in --mount order agnostic Minor test fix for podman-remote build: honor .containerignore as ignore file Update install.md: Debian 11 (Bullseye) is stable build(deps): bump github.com/docker/docker Use constants from containers/common for finding seccomp.json Don't call os.Exit(1) from manifest exist manifest: add support for buildah manifest exists Buildah should ignore /etc/crio/seccomp.json chroot: Fix cross build break chroot: Move isDevNull to run_common.go chroot: Fix setRlimit build on FreeBSD chroot: Move parseRLimits and setRlimits to run_common.go chroot: Fix runUsingChrootExecMain on FreeBSD chroot: Move runUsingChrootExecMain to run_common.go chroot: Factor out Linux-specific unshare options from runUsingChroot chroot: Move runUsingChroot to run_common.go chroot: Move RunUsingChroot and runUsingChrootMain to run_common.go chroot: Factor out /dev/ptmx pty implementation chroot: Add FreeBSD support for run with chroot isolation build(deps): bump github.com/docker/go-units from 0.4.0 to 0.5.0 Replace k8s.gcr.io/pause in tests with registry.k8s.io/pause build(deps): bump github.com/onsi/gomega from 1.20.0 to 1.20.1 Cirrus: use image with fewer downloaded dependencies build(deps): bump github.com/opencontainers/runc from 1.1.3 to 1.1.4 run: add container gid to additional groups buildah: support for --retry and --retry-delay for push/pull failures Makefile: always call $(GO) instead of `go` build(deps): bump github.com/fsouza/go-dockerclient from 1.8.2 to 1.8.3 test: use `T.TempDir` to create temporary test directory mount,cache: enable SElinux shared content label option by default commit: use race-free RemoveNames instead of SetNames Drop util/util.Cause() cmd/buildah: add "manifest create --amend" build(deps): bump github.com/fsouza/go-dockerclient from 1.8.1 to 1.8.2 docs: specify git protocol is not supported for github hosted repo Scrub user and group names from layer diffs build(deps): bump github.com/containerd/containerd from 1.6.6 to 1.6.8 version: bump to 1.28.0-dev ## v1.27.0 (2022-08-01) build: support filtering cache by duration using `--cache-ttl`. build: support building from commit when using git repo as build context. build: clean up git repos correctly when using subdirs. build: add support for distributing cache to remote sources using `--cache-to` and `--cache-from`. imagebuildah: optimize cache hits for `COPY` and `ADD` instructions. build: support OCI hooks for ephemeral build containers. build: add support for `--userns=auto`. copier: add NoOverwriteNonDirDir option . add initial support for building images using Buildah on FreeBSD. multistage: this now skips the computing of unwanted stages to improve performance. multiarch: support splitting build logs for `--platform` using `--logsplit`. build: add support for building images where the base image has no history. commit: allow disabling image history with `--omit-history`. build: add support for renaming a device in rootless setups. build: now supports additionalBuildContext in builds via the `--build-context` option. build: `--output` produces artifacts even if the build container is not committed. build: now accepts `-cpp-flag`, allowing users to pass in CPP flags when processing a Containerfile with C Preprocessor-like syntax. build: now accepts a branch and a subdirectory when the build context is a git repository. build: output now shows a progress bar while pushing and pulling images build: now errors out if the path to Containerfile is a directory. build: support building container images on environments that are rootless and without any valid login sessions. fix: `--output` now generates artifacts even if the entire build is cached. fix: `--output` generates artifacts only for the target stage in multi-stage builds. fix,add: now fails on a bad HTTP response instead of writing to container fix,squash: never use build cache when computing the last step of the last stage fix,build,run: allow reusing secret more than once in different RUN steps fix: compatibility with Docker build by making its --label and --annotate options set empty labels and annotations when given a name but no `=` or label value. ## v1.26.0 (2022-05-04) imagebuildah,build: move deepcopy of args before we spawn goroutine Vendor in containers/storage v1.40.2 buildah.BuilderOptions.DefaultEnv is ignored, so mark it as deprecated help output: get more consistent about option usage text Handle OS version and features flags buildah build: --annotation and --label should remove values buildah build: add a --env buildah: deep copy options.Args before performing concurrent build/stage test: inline platform and builtinargs behaviour vendor: bump imagebuilder to master/009dbc6 build: automatically set correct TARGETPLATFORM where expected build(deps): bump github.com/fsouza/go-dockerclient Vendor in containers/(common, storage, image) imagebuildah, executor: process arg variables while populating baseMap buildkit: add support for custom build output with --output Cirrus: Update CI VMs to F36 fix staticcheck linter warning for deprecated function Fix docs build on FreeBSD build(deps): bump github.com/containernetworking/cni from 1.0.1 to 1.1.0 copier.unwrapError(): update for Go 1.16 copier.PutOptions: add StripSetuidBit/StripSetgidBit/StripStickyBit copier.Put(): write to read-only directories build(deps): bump github.com/cpuguy83/go-md2man/v2 in /tests/tools Rename $TESTSDIR (the plural one), step 4 of 3 Rename $TESTSDIR (the plural one), step 3 of 3 Rename $TESTSDIR (the plural one), step 2 of 3 Rename $TESTSDIR (the plural one), step 1 of 3 build(deps): bump github.com/containerd/containerd from 1.6.2 to 1.6.3 Ed's periodic test cleanup using consistent lowercase 'invalid' word in returned err msg Update vendor of containers/(common,storage,image) use etchosts package from c/common run: set actual hostname in /etc/hostname to match docker parity update c/common to latest main Update vendor of containers/(common,storage,image) Stop littering manifest-create: allow creating manifest list from local image Update vendor of storage,common,image Bump golang.org/x/crypto to 7b82a4e Initialize network backend before first pull oci spec: change special mount points for namespaces tests/helpers.bash: assert handle corner cases correctly buildah: actually use containers.conf settings integration tests: learn to start a dummy registry Fix error check to work on Podman buildah build should accept at most one arg tests: reduce concurrency for flaky bud-multiple-platform-no-run vendor in latest containers/common,image,storage manifest-add: allow override arch,variant while adding image Remove a stray `\` from .containerenv Vendor in latest opencontainers/selinux v1.10.1 build, commit: allow removing default identity labels Create shorter names for containers based on image IDs test: skip rootless on cgroupv2 in root env fix hang when oci runtime fails Set permissions for GitHub actions copier test: use correct UID/GID in test archives run: set parent-death signals and forward SIGHUP/SIGINT/SIGTERM Bump back to v1.26.0-dev build(deps): bump github.com/opencontainers/runc from 1.1.0 to 1.1.1 Included the URL to check the SHA ## v1.25.1 (2022-03-30) buildah: create WORKDIR with USER permissions vendor: update github.com/openshift/imagebuilder copier: attempt to open the dir before adding it Updated dependabot to get updates for GitHub actions. Switch most calls to filepath.Walk to filepath.WalkDir build: allow --no-cache and --layers so build cache can be overrided build(deps): bump github.com/onsi/gomega from 1.18.1 to 1.19.0 Bump to v1.26.0-dev build(deps): bump github.com/golangci/golangci-lint in /tests/tools ## v1.25.0 (2022-03-25) install: drop RHEL/CentOS 7 doc build(deps): bump github.com/containers/common from 0.47.4 to 0.47.5 Bump c/storage to v1.39.0 in main Add a test for CVE-2022-27651 build(deps): bump github.com/docker/docker Bump github.com/prometheus/client_golang to v1.11.1 [CI:DOCS] man pages: sort flags, and keep them that way build(deps): bump github.com/containerd/containerd from 1.6.1 to 1.6.2 Don't pollute network setup: increase timeout to 4 minutes do not set the inheritable capabilities build(deps): bump github.com/golangci/golangci-lint in /tests/tools build(deps): bump github.com/containers/ocicrypt from 1.1.2 to 1.1.3 parse: convert exposed GetVolumes to internal only buildkit: mount=type=cache support locking external cache store .in support: improve error message when cpp is not installed buildah image: install cpp build(deps): bump github.com/stretchr/testify from 1.7.0 to 1.7.1 build(deps): bump github.com/spf13/cobra from 1.3.0 to 1.4.0 build(deps): bump github.com/docker/docker Add --no-hosts flag to eliminate use of /etc/hosts within containers test: remove skips for rootless users test: unshare mount/umount if test is_rootless tests/copy: read correct containers.conf build(deps): bump github.com/docker/distribution cirrus: add seperate task and matrix for rootless tests: skip tests for rootless which need unshare buildah: test rootless integration vendor: bump c/storage to main/93ce26691863 build(deps): bump github.com/fsouza/go-dockerclient from 1.7.9 to 1.7.10 tests/copy: initialize the network, too [CI:DOCS] remove references to Kubic for CentOS and Ubuntu build(deps): bump github.com/containerd/containerd from 1.6.0 to 1.6.1 use c/image/pkg/blobcache vendor c/image/v5@v5.20.0 add: ensure the context directory is an absolute path executor: docker builds must inherit healthconfig from base if any docs: Remove Containerfile and containeringore build(deps): bump github.com/fsouza/go-dockerclient from 1.7.8 to 1.7.9 helpers.bash: Use correct syntax speed up combination-namespaces test build(deps): bump github.com/golangci/golangci-lint in /tests/tools Bump back to 1.25.0-dev build(deps): bump github.com/containerd/containerd from 1.5.9 to 1.6.0 ## v1.24.2 (2022-02-16) Increase subuid/subgid to 65535 history: only add proxy vars to history if specified run_linux: use --systemd-cgroup buildah: new global option --cgroup-manager Makefile: build with systemd when available build(deps): bump github.com/fsouza/go-dockerclient from 1.7.7 to 1.7.8 Bump c/common to v0.47.4 Cirrus: Use updated VM images conformance: add a few "replace-directory-with-symlink" tests Bump back to v1.25.0-dev ## v1.24.1 (2022-02-03) executor: Add support for inline --platform within Dockerfile caps: fix buildah run --cap-add=all Update vendor of openshift/imagebuilder Bump version of containers/image and containers/common Update vendor of containers/common System tests: fix accidental vandalism of source dir build(deps): bump github.com/containers/storage from 1.38.1 to 1.38.2 imagebuildah.BuildDockerfiles(): create the jobs semaphore build(deps): bump github.com/onsi/gomega from 1.18.0 to 1.18.1 overlay: always honor mountProgram overlay: move mount program invocation to separate function overlay: move mount program lookup to separate function Bump to v1.25.0-dev [NO TESTS NEEDED] ## v1.24.0 (2022-01-26) Update vendor of containers/common build(deps): bump github.com/golangci/golangci-lint in /tests/tools Github-workflow: Report both failures and errors. build(deps): bump github.com/containers/image/v5 from 5.18.0 to 5.19.0 Update docs/buildah-build.1.md [CI:DOCS] Fix typos and improve language buildah bud --network add support for custom networks Make pull commands be consistent docs/buildah-build.1.md: don't imply that -v isn't just a RUN thing build(deps): bump github.com/onsi/gomega from 1.17.0 to 1.18.0 Vendor in latest containers/image Run codespell on code .github/dependabot.yml: add tests/tools go.mod CI: rm git-validation, add GHA job to validate PRs tests/tools: bump go-md2man to v2.0.1 tests/tools/Makefile: simplify tests/tools: bump onsi/ginkgo to v1.16.5 vendor: bump c/common and others mount: add support for custom upper and workdir with overlay mounts linux: fix lookup for runtime overlay: add MountWithOptions to API which extends support for advanced overlay Allow processing of SystemContext from FlagSet .golangci.yml: enable unparam linter util/resolveName: rm bool return tests/tools: bump golangci-lint .gitignore: fixups all: fix capabilities.NewPid deprecation warnings bind/mount.go: fix linter comment all: fix gosimple warning S1039 tests/e2e/buildah_suite_test.go: fix gosimple warnings imagebuildah/executor.go: fix gosimple warning util.go: fix gosimple warning build(deps): bump github.com/opencontainers/runc from 1.0.3 to 1.1.0 Enable git-daemon tests Allow processing of id options from FlagSet Cirrus: Re-order tasks for more parallelism Cirrus: Freshen VM images Fix platform handling for empty os/arch values Allow processing of network options from FlagSet Fix permissions on secrets directory Update containers/image and containers/common bud.bats: use a local git daemon for the git protocol test Allow processing of common options from FlagSet Cirrus: Run int. tests in parallel with unit vendor c/common Fix default CNI paths build(deps): bump github.com/fsouza/go-dockerclient from 1.7.6 to 1.7.7 multi-stage: enable mounting stages across each other with selinux enabled executor: Share selinux label of first stage with other stages in a build buildkit: add from field to bind and cache mounts so images can be used as source Use config.ProxyEnv from containers/common use libnetwork from c/common for networking setup the netns in the buildah parent process build(deps): bump github.com/containerd/containerd from 1.5.8 to 1.5.9 build(deps): bump github.com/fsouza/go-dockerclient from 1.7.4 to 1.7.6 build: fix libsubid test Allow callers to replace the ContainerSuffix parse: allow parsing anomaly non-human value for memory control group .cirrus: remove static_build from ci stage_executor: re-use all possible layers from cache for squashed builds build(deps): bump github.com/spf13/cobra from 1.2.1 to 1.3.0 Allow rootless buildah to set resource limits on cgroup V2 build(deps): bump github.com/docker/docker tests: move buildkit mount tests files from TESTSDIR to TESTDIR before modification build(deps): bump github.com/opencontainers/runc from 1.0.2 to 1.0.3 Wire logger through to config copier.Put: check for is-not-a-directory using lstat, not stat Turn on rootless cgroupv2 tests Grab all of the containers.conf settings for namespaces. image: set MediaType in OCI manifests copier: RemoveAll possibly-directories Simple README fix images: accept multiple filter with logical AND build(deps): bump github.com/containernetworking/cni from 0.8.1 to 1.0.1 UPdate vendor of container/storage build(deps): bump github.com/onsi/gomega from 1.16.0 to 1.17.0 build(deps): bump github.com/containers/image/v5 from 5.16.1 to 5.17.0 Make LocalIP public function so Podman can use it Fix UnsetEnv for buildah bud Tests should rely only on static/unchanging images run: ensure that stdio pipes are labeled correctly build(deps): bump github.com/docker/docker Cirrus: Bump up to Fedora 35 & Ubuntu 21.10 chroot: don't use the generate default seccomp filter for unit tests build(deps): bump github.com/containerd/containerd from 1.5.7 to 1.5.8 ssh-agent: Increase timeout before we explicitly close connection docs/tutorials: update Clarify that manifest defaults to localhost as the registry name "config": remove a stray bit of debug output "commit": fix a flag typo Fix an error message: unlocking vs locking Expand the godoc for CommonBuildOptions.Secrets chroot: accept an "rw" option Add --unsetenv option to buildah commit and build define.TempDirForURL(): show CombinedOutput when a command fails config: support the variant field rootless: do not bind mount /sys if not needed Fix tutorial to specify command on buildah run line build: history should not contain ARG values docs: Use guaranteed path for go-md2man run: honor --network=none from builder if nothing specified networkpolicy: Should be enabled instead of default when explictly set Add support for env var secret sources build(deps): bump github.com/docker/docker fix: another non-portable shebang Rootless containers users should use additional groups Support overlayfs path contains colon Report ignorefile location when no content added Add support for host.containers.internal in the /etc/hosts build(deps): bump github.com/onsi/ginkgo from 1.16.4 to 1.16.5 imagebuildah: fix nil deref buildkit: add support for mount=type=cache Default secret mode to 400 [CI:DOCS] Include manifest example usage docs: update buildah-from, buildah-pull 'platform' option compatibility notes docs: update buildah-build 'platform' option compatibility notes De-dockerize the man page as much as possible [CI:DOCS] Touch up Containerfile man page to show ARG can be 1st docs: Fix and Update Containerfile man page with supported mount types mount: add tmpcopyup to tmpfs mount option buildkit: Add support for --mount=type=tmpfs build(deps): bump github.com/opencontainers/selinux from 1.8.5 to 1.9.1 Fix command doc links in README.md build(deps): bump github.com/containers/image/v5 from 5.16.0 to 5.16.1 build: Add support for buildkit like --mount=type=bind Bump containerd to v1.5.7 build(deps): bump github.com/docker/docker tests: stop pulling php, composer Fix .containerignore link file Cirrus: Fix defunct package metadata breaking cache build(deps): bump github.com/containers/storage from 1.36.0 to 1.37.0 buildah build: add --all-platforms Add man page for Containerfile and .containerignore Plumb the remote logger throughut Buildah Replace fmt.Sprintf("%d", x) with strconv.Itoa(x) Run: Cleanup run directory after every RUN step build(deps): bump github.com/containers/common from 0.45.0 to 0.46.0 Makefile: adjust -ldflags/-gcflags/-gccgoflags depending on the go implementation Makefile: check for `-race` using `-mod=vendor` imagebuildah: fix an attempt to write to a nil map push: support to specify the compression format conformance: allow test cases to specify dockerUseBuildKit build(deps): bump github.com/containers/common from 0.44.1 to 0.45.0 build(deps): bump github.com/containers/common from 0.44.0 to 0.44.1 unmarshalConvertedConfig(): handle zstd compression tests/copy/copy: wire up compression options Update to github.com/vbauerster/mpb v7.1.5 Add flouthoc to OWNERS build: Add additional step nodes when labels are modified Makefile: turn on race detection whenever it's available conformance: add more tests for exclusion short-circuiting Update VM Images + Drop prior-ubuntu testing Bump to v1.24.0-dev ## v1.23.0 (2021-09-13) Vendor in containers/common v0.44.0 build(deps): bump github.com/containers/storage from 1.35.0 to 1.36.0 Update 05-openshift-rootless-build.md build(deps): bump github.com/opencontainers/selinux from 1.8.4 to 1.8.5 .cirrus.yml: run cross_build_task on Big Sur Makefile: update cross targets Add support for rootless overlay mounts Cirrus: Increase unit-test timeout Docs: Clarify rmi w/ manifest/index use build: mirror --authfile to filesystem if pointing to FD instead of file Fix build with .git url with branch manifest: rm should remove only manifests not referenced images. vendor: bump c/common to v0.43.3-0.20210902095222-a7acc160fb25 Avoid rehashing and noop compression writer corrected man page section; .conf file to mention its man page copy: add --max-parallel-downloads to tune that copy option copier.Get(): try to avoid descending into directories tag: Support tagging manifest list instead of resolving to images Install new manpages to correct sections conformance: tighten up exception specifications Add support for libsubid Add epoch time field to buildah images Fix ownership of /home/build/.local/share/containers build(deps): bump github.com/containers/image/v5 from 5.15.2 to 5.16.0 Rename bud to build, while keeping an alias for to bud. Replace golang.org/x/crypto/ssh/terminal with golang.org/x/term build(deps): bump github.com/opencontainers/runc from 1.0.1 to 1.0.2 build(deps): bump github.com/onsi/gomega from 1.15.0 to 1.16.0 build(deps): bump github.com/fsouza/go-dockerclient from 1.7.3 to 1.7.4 build(deps): bump github.com/containers/common from 0.43.1 to 0.43.2 Move DiscoverContainerfile to pkg/util directory build(deps): bump github.com/containers/image/v5 from 5.15.1 to 5.15.2 Remove some references to Docker build(deps): bump github.com/containers/image/v5 from 5.15.0 to 5.15.1 imagebuildah: handle --manifest directly build(deps): bump github.com/containers/common from 0.42.1 to 0.43.1 build(deps): bump github.com/opencontainers/selinux from 1.8.3 to 1.8.4 executor: make sure imageMap is updated with terminatedStage tests/serve/serve.go: use a kernel-assigned port Bump go for vendor-in-container from 1.13 to 1.16 imagebuildah: move multiple-platform building internal Adds GenerateStructure helper function to support rootfs-overlay. Run codespell to fix spelling Implement SSH RUN mount build(deps): bump github.com/onsi/gomega from 1.14.0 to 1.15.0 Fix resolv.conf content with run --net=private run: fix nil deref using the option's logger build(deps): bump github.com/containerd/containerd from 1.5.1 to 1.5.5 make vendor-in-container bud: teach --platform to take a list set base-image annotations build(deps): bump github.com/opencontainers/selinux from 1.8.2 to 1.8.3 [CI:DOCS] Fix CHANGELOG.md Bump to v1.23.0-dev [NO TESTS NEEDED] Accept repositories on login/logout ## v1.22.0 (2021-08-02) c/image, c/storage, c/common vendor before Podman 3.3 release WIP: tests: new assert() Proposed patch for 3399 (shadowutils) Fix handling of --restore shadow-utils build(deps): bump github.com/containers/image/v5 from 5.13.2 to 5.14.0 runtime-flag (debug) test: handle old & new runc build(deps): bump github.com/containers/storage from 1.32.6 to 1.33.0 Allow dst and destination for target in secret mounts Multi-arch: Always push updated version-tagged img Add a few tests on cgroups V2 imagebuildah.stageExecutor.prepare(): remove pseudonym check refine dangling filter Chown with environment variables not set should fail Just restore protections of shadow-utils build(deps): bump github.com/opencontainers/runc from 1.0.0 to 1.0.1 Remove specific kernel version number requirement from install.md Multi-arch image workflow: Make steps generic chroot: fix environment value leakage to intermediate processes Update nix pin with `make nixpkgs` buildah source - create and manage source images Update cirrus-cron notification GH workflow Reuse code from containers/common/pkg/parse Cirrus: Freshen VM images build(deps): bump github.com/containers/storage from 1.32.5 to 1.32.6 Fix excludes exception begining with / or ./ Fix syntax for --manifest example build(deps): bump github.com/onsi/gomega from 1.13.0 to 1.14.0 vendor containers/common@main Cirrus: Drop dependence on fedora-minimal Adjust conformance-test error-message regex Workaround appearance of differing debug messages Cirrus: Install docker from package cache build(deps): bump github.com/containers/ocicrypt from 1.1.1 to 1.1.2 Switch rusagelogfile to use options.Out build(deps): bump github.com/containers/storage from 1.32.4 to 1.32.5 Turn stdio back to blocking when command finishes Add support for default network creation Cirrus: Updates for master->main rename Change references from master to main Add `--env` and `--workingdir` flags to run command build(deps): bump github.com/opencontainers/runc [CI:DOCS] buildah bud: spelling --ignore-file requires parameter [CI:DOCS] push/pull: clarify supported transports Remove unused function arguments Create mountOptions for mount command flags Extract version command implementation to function Add --json flags to `mount` and `version` commands build(deps): bump github.com/containers/storage from 1.32.2 to 1.32.3 build(deps): bump github.com/containers/common from 0.40.0 to 0.40.1 copier.Put(): set xattrs after ownership buildah add/copy: spelling build(deps): bump github.com/containers/common from 0.39.0 to 0.40.0 buildah copy and buildah add should support .containerignore Remove unused util.StartsWithValidTransport Fix documentation of the --format option of buildah push Don't use alltransports.ParseImageName with known transports build(deps): bump github.com/containers/image/v5 from 5.13.0 to 5.13.1 man pages: clarify `rmi` removes dangling parents tests: make it easer to override the location of the copy helper build(deps): bump github.com/containers/image/v5 from 5.12.0 to 5.13.0 [CI:DOCS] Fix links to c/image master branch imagebuildah: use the specified logger for logging preprocessing warnings Fix copy into workdir for a single file Fix docs links due to branch rename Update nix pin with `make nixpkgs` build(deps): bump github.com/fsouza/go-dockerclient from 1.7.2 to 1.7.3 build(deps): bump github.com/opencontainers/selinux from 1.8.1 to 1.8.2 build(deps): bump go.etcd.io/bbolt from 1.3.5 to 1.3.6 build(deps): bump github.com/containers/storage from 1.32.1 to 1.32.2 build(deps): bump github.com/mattn/go-shellwords from 1.0.11 to 1.0.12 build(deps): bump github.com/onsi/ginkgo from 1.16.3 to 1.16.4 fix(docs): typo Move to v1.22.0-dev Fix handling of auth.json file while in a user namespace Add rusage-logfile flag to optionally send rusage to a file imagebuildah: redo step logging build(deps): bump github.com/onsi/ginkgo from 1.16.2 to 1.16.3 build(deps): bump github.com/containers/storage from 1.32.0 to 1.32.1 Add volumes to make running buildah within a container easier build(deps): bump github.com/onsi/gomega from 1.12.0 to 1.13.0 Add and use a "copy" helper instead of podman load/save Bump github.com/containers/common from 0.38.4 to 0.39.0 containerImageRef/containerImageSource: don't buffer uncompressed layers containerImageRef(): squashed images have no parent images Sync. workflow across skopeo, buildah, and podman Bump github.com/containers/storage from 1.31.1 to 1.31.2 Bump github.com/opencontainers/runc from 1.0.0-rc94 to 1.0.0-rc95 Bump to v1.21.1-dev [NO TESTS NEEDED] ## v1.21.0 (2021-05-19) Don't blow up if cpp detects errors Vendor in containers/common v0.38.4 Remove 'buildah run --security-opt' from completion update c/common Fix handling of --default-mounts-file update vendor of containers/storage v1.31.1 Bump github.com/containers/storage from 1.30.3 to 1.31.0 Send logrus messages back to caller when building github: Fix bad repo. ref in workflow config Check earlier for bad image tags name buildah bud: fix containers/podman/issues/10307 Bump github.com/containers/storage from 1.30.1 to 1.30.3 Cirrus: Support [CI:DOCS] test skipping Notification email for cirrus-cron build failures Bump github.com/opencontainers/runc from 1.0.0-rc93 to 1.0.0-rc94 Fix race condition Fix copy race while walking paths Preserve ownership of lower directory when doing an overlay mount Bump github.com/onsi/gomega from 1.11.0 to 1.12.0 Update nix pin with `make nixpkgs` codespell cleanup Multi-arch github-action workflow unification Bump github.com/containers/image/v5 from 5.11.1 to 5.12.0 Bump github.com/onsi/ginkgo from 1.16.1 to 1.16.2 imagebuildah: ignore signatures when tagging images update to latest libimage Bump github.com/containers/common from 0.37.0 to 0.37.1 Bump github.com/containers/storage from 1.30.0 to 1.30.1 Upgrade to GitHub-native Dependabot Document location of auth.json file if XDG_RUNTIME_DIR is not set run.bats: fix flake in run-user test Cirrus: Update F34beta -> F34 pr-should-include-tests: try to make work in buildah runUsingRuntime: when relaying error from the runtime, mention that Run(): avoid Mkdir() into the rootfs imagebuildah: replace archive with chrootarchive imagebuildah.StageExecutor.volumeCacheSaveVFS(): set up bind mounts conformance: use :Z with transient mounts when SELinux is enabled bud.bats: fix a bats warning imagebuildah: create volume directories when using overlays imagebuildah: drop resolveSymlink() namespaces test - refactoring and cleanup Refactor 'idmapping' system test Cirrus: Update Ubuntu images to 21.04 Tiny fixes in bud system tests Add compabitility wrappers for removed packages Fix expected message at pulling image Fix system tests of 'bud' subcommand [CI:DOCS] Update steps for CentOS runc users Add support for secret mounts Add buildah manifest rm command restore push/pull and util API [CI:DOCS] Remove older distro docs Rename rhel secrets to subscriptions vendor in openshift/imagebuilder Remove buildah bud --loglevel ... use new containers/common/libimage package Fix copier when using globs Test namespace flags of 'bud' subcommand Add system test of 'bud' subcommand Output names of multiple tags in buildah bud push to docker test: don't get fooled by podman copier: add Remove() build(deps): bump github.com/containers/image/v5 from 5.10.5 to 5.11.1 Restore log timestamps Add system test of 'buildah help' with a tiny fix tests: copy.bats: fix infinite hang Do not force hard code to crun in rootless mode build(deps): bump github.com/openshift/imagebuilder from 1.2.0 to 1.2.1 build(deps): bump github.com/containers/ocicrypt from 1.1.0 to 1.1.1 build(deps): bump github.com/containers/common from 0.35.4 to 0.36.0 Fix arg missing warning in bud Check without flag in 'from --cgroup-parent' test Minor fixes to Buildah as a library tutorial documentation Add system test of 'buildah version' for packaged buildah Add a few system tests of 'buildah from' Log the final error with %+v at logging level "trace" copier: add GetOptions.NoCrossDevice Update nix pin with `make nixpkgs` Bump to v1.20.2-dev ## v1.20.1 (2021-04-13) Run container with isolation type set at 'from' bats helpers.bash - minor refactoring Bump containers/storage vendor to v1.29.0 build(deps): bump github.com/onsi/ginkgo from 1.16.0 to 1.16.1 Cirrus: Update VMs w/ F34beta CLI add/copy: add a --from option build(deps): bump github.com/onsi/ginkgo from 1.15.2 to 1.16.0 Add authentication system tests for 'commit' and 'bud' fix local image lookup for custom platform Double-check existence of OCI runtimes Cirrus: Make use of shared get_ci_vm container Add system tests of "buildah run" Update nix pin with `make nixpkgs` Remove some stuttering on returns errors Setup alias for --tty to --terminal Add conformance tests for COPY /... Put a few more minutes on the clock for the CI conformance test Add a conformance test for COPY --from $symlink Add conformance tests for COPY "" Check for symlink in builtin volume Sort all mounts by destination directory System-test cleanup Export parse.Platform string to be used by podman-remote blobcache: fix sequencing error build(deps): bump github.com/containers/common from 0.35.3 to 0.35.4 Fix URL in demos/buildah_multi_stage.sh Add a few system tests [NO TESTS NEEDED] Use --recurse-modules when building git context Bump to v1.20.1-dev ## v1.20.0 (2021-03-25) * vendor in containers/storage v1.28.1 * build(deps): bump github.com/containers/common from 0.35.2 to 0.35.3 * tests: prefetch: use buildah, not podman, for pulls * Use faster way to check image tag existence during multi-arch build * Add information about multi-arch images to the Readme * COPY --chown: expand the conformance test * pkg/chrootuser: use a bufio.Scanner * [CI:DOCS] Fix rootful typo in docs * build(deps): bump github.com/onsi/ginkgo from 1.15.1 to 1.15.2 * Add documentation and testing for .containerignore * build(deps): bump github.com/sirupsen/logrus from 1.8.0 to 1.8.1 * build(deps): bump github.com/hashicorp/go-multierror from 1.1.0 to 1.1.1 * Lookup Containerfile if user specifies a directory * Add Tag format placeholder to docs * copier: ignore sockets * image: propagate errors from extractRootfs * Remove system test of 'buildah containers -a' * Clarify userns options are usable only as root in man pages * Fix system test of 'containers -a' * Remove duplicated code in addcopy * build(deps): bump github.com/onsi/ginkgo from 1.15.0 to 1.15.1 * build(deps): bump github.com/onsi/gomega from 1.10.5 to 1.11.0 * build(deps): bump github.com/fsouza/go-dockerclient from 1.7.1 to 1.7.2 * Update multi-arch buildah build setup with new logic * Update nix pin with `make nixpkgs` * overlay.bats: fix the "overlay source permissions" test * imagebuildah: use overlay for volumes when using overlay * Make PolicyMap and PullPolicy names align * copier: add GetOptions.IgnoreUnreadable * Check local image to match system context * fix: Containerfiles - smaller set of userns u/gids * Set upperdir permissions based on source * Shrink the vendoring size of pkc/cli * Clarify image name match failure message * ADD/COPY: create the destination directory first, chroot to it * copier.GetOptions: add NoDerefSymLinks * copier: add an Eval function * Update system test for 'from --cap-add/drop' * copier: fix a renaming bug * copier: return child process stderr if we can't JSON decode the response * Add some system tests * build(deps): bump github.com/containers/storage from 1.26.0 to 1.27.0 * complement add/copy --chmod documentation * buildah login and logout, do not need to enter user namespace * Add multi-arch image build * chmod/chown added/fixed in bash completions * OWNERS: add @lsm5 * buildah add/copy --chmod dockerfile implementation * bump github.com/openshift/imagebuilder from 1.1.8 to 1.2.0 * buildah add/copy --chmod cli implementation for files and urls * Make sure we set the buildah version label * Isolation strings, should match user input * [CI:DOCS] buildah-from.md: remove dup arch,os * build(deps): bump github.com/containers/image/v5 from 5.10.2 to 5.10.3 * Cirrus: Temp. disable prior-fedora (F32) testing * pr-should-include-tests: recognized "renamed" tests * build(deps): bump github.com/sirupsen/logrus from 1.7.0 to 1.8.0 * build(deps): bump github.com/fsouza/go-dockerclient from 1.7.0 to 1.7.1 * build(deps): bump github.com/containers/common from 0.34.2 to 0.35.0 * Fix reaping of stages with no instructions * add stale bot * Add base image name to comment * build(deps): bump github.com/spf13/cobra from 1.1.1 to 1.1.3 * Don't fail copy to emptydir * buildah: use volatile containers * vendor: update containers/storage * Eliminate the use of containers/building import in pkg subdirs * Add more support for removing config * Improve messages about --cache-from not being supported * Revert patch to allow COPY/ADD of empty dirs. * Don't fail copy to emptydir * Fix tutorial for rootless mode * Fix caching layers with build args * Vendor in containers/image v5.10.2 * build(deps): bump github.com/containers/common from 0.34.0 to 0.34.2 * build(deps): bump github.com/onsi/ginkgo from 1.14.2 to 1.15.0 * 'make validate': require PRs to include tests * build(deps): bump github.com/onsi/gomega from 1.10.4 to 1.10.5 * build(deps): bump github.com/containers/storage from 1.24.5 to 1.25.0 * Use chown function for U volume flag from containers/common repository * --iidfile: print hash prefix * bump containernetworking/cni to v0.8.1 - fix for CVE-2021-20206 * run: fix check for host pid namespace * Finish plumbing for buildah bud --manifest * buildah manifest add localimage should work * Stop testing directory permissions with latest docker * Fix build arg check * build(deps): bump github.com/containers/ocicrypt from 1.0.3 to 1.1.0 * [ci:docs] Fix man page for buildah push * Update nix pin with `make nixpkgs` * Bump to containers/image v5.10.1 * Rebuild layer if a change in ARG is detected * Bump golang.org/x/crypto to the latest * Add Ashley and Urvashi to Approvers * local image lookup by digest * Use build-arg ENV val from local environment if set * Pick default OCI Runtime from containers.conf * Added required devel packages * Cirrus: Native OSX Build * Cirrus: Two minor cleanup items * Workaround for RHEL gating test failure * build(deps): bump github.com/stretchr/testify from 1.6.1 to 1.7.0 * build(deps): bump github.com/mattn/go-shellwords from 1.0.10 to 1.0.11 * Reset upstream branch to dev version * If destination does not exists, do not throw error ## v1.19.0 (2021-01-08) Update vendor of containers/storage and containers/common Buildah inspect should be able to inspect manifests Make buildah push support pushing manifests lists and digests Fix handling of TMPDIR environment variable Add support for --manifest flags Upper directory should match mode of destination directory Only grab the OS, Arch if the user actually specified them Use --arch and --os and --variant options to select architecture and os Cirrus: Track libseccomp and golang version copier.PutOptions: add an "IgnoreDevices" flag fix: `rmi --prune` when parent image is in store. build(deps): bump github.com/containers/storage from 1.24.3 to 1.24.4 build(deps): bump github.com/containers/common from 0.31.1 to 0.31.2 Allow users to specify stdin into containers Drop log message on failure to mount on /sys file systems to info Spelling SELinux no longer requires a tag. build(deps): bump github.com/opencontainers/selinux from 1.6.0 to 1.8.0 build(deps): bump github.com/containers/common from 0.31.0 to 0.31.1 Update nix pin with `make nixpkgs` Switch references of /var/run -> /run Allow FROM to be overriden with from option copier: don't assume we can chroot() on Unixy systems copier: add PutOptions.NoOverwriteDirNonDir, Get/PutOptions.Rename copier: handle replacing directories with not-directories copier: Put: skip entries with zero-length names build(deps): bump github.com/containers/storage from 1.24.2 to 1.24.3 Add U volume flag to chown source volumes Turn off PRIOR_UBUNTU Test until vm is updated pkg, cli: rootless uses correct isolation build(deps): bump github.com/onsi/gomega from 1.10.3 to 1.10.4 update installation doc to reflect current status Move away from using docker.io enable short-name aliasing build(deps): bump github.com/containers/storage from 1.24.1 to 1.24.2 build(deps): bump github.com/containers/common from 0.30.0 to 0.31.0 Throw errors when using bogus --network flags pkg/supplemented test: replace our null blobinfocache build(deps): bump github.com/containers/common from 0.29.0 to 0.30.0 inserts forgotten quotation mark Not prefer use local image create/add manifest Add container information to .containerenv Add --ignorefile flag to use alternate .dockerignore flags Add a source debug build Fix crash on invalid filter commands build(deps): bump github.com/containers/common from 0.27.0 to 0.29.0 Switch to using containers/common pkg's fix: non-portable shebang #2812 Remove copy/paste errors that leaked `Podman` into man pages. Add suggests cpp to spec file Apply suggestions from code review update docs for debian testing and unstable imagebuildah: disable pseudo-terminals for RUN Compute diffID for mapped-layer at creating image source intermediateImageExists: ignore images whose history we can't read Bump to v1.19.0-dev build(deps): bump github.com/containers/common from 0.26.3 to 0.27.0 ## v1.18.0 (2020-11-16) Fix testing error caused by simultanious merge Vendor in containers/storage v1.24.0 short-names aliasing Add --policy flag to buildah pull Stop overwrapping and stuttering copier.Get(): ignore ENOTSUP/ENOSYS when listing xattrs Run: don't forcibly disable UTS namespaces in rootless mode test: ensure non-directory in a Dockerfile path is handled correctly Add a few tests for `pull` command Fix buildah config --cmd to handle array build(deps): bump github.com/containers/storage from 1.23.8 to 1.23.9 Fix NPE when Dockerfile path contains non-directory entries Update buildah bud man page from podman build man page Move declaration of decryption-keys to common cli Run: correctly call copier.Mkdir util: digging UID/GID out of os.FileInfo should work on Unix imagebuildah.getImageTypeAndHistoryAndDiffIDs: cache results Verify userns-uid-map and userns-gid-map input Use CPP, CC and flags in dep check scripts Avoid overriding LDFLAGS in Makefile ADD: handle --chown on URLs Update nix pin with `make nixpkgs` (*Builder).Run: MkdirAll: handle EEXIST error copier: try to force loading of nsswitch modules before chroot() fix MkdirAll usage build(deps): bump github.com/containers/common from 0.26.2 to 0.26.3 build(deps): bump github.com/containers/storage from 1.23.7 to 1.23.8 Use osusergo build tag for static build imagebuildah: cache should take image format into account Bump to v1.18.0-dev ## v1.17.0 (2020-10-29) Handle cases where other tools mount/unmount containers overlay.MountReadOnly: support RO overlay mounts overlay: use fusermount for rootless umounts overlay: fix umount Switch default log level of Buildah to Warn. Users need to see these messages Drop error messages about OCI/Docker format to Warning level build(deps): bump github.com/containers/common from 0.26.0 to 0.26.2 tests/testreport: adjust for API break in storage v1.23.6 build(deps): bump github.com/containers/storage from 1.23.5 to 1.23.7 build(deps): bump github.com/fsouza/go-dockerclient from 1.6.5 to 1.6.6 copier: put: ignore Typeflag="g" Use curl to get repo file (fix #2714) build(deps): bump github.com/containers/common from 0.25.0 to 0.26.0 build(deps): bump github.com/spf13/cobra from 1.0.0 to 1.1.1 Remove docs that refer to bors, since we're not using it Buildah bud should not use stdin by default bump containerd, docker, and golang.org/x/sys Makefile: cross: remove windows.386 target copier.copierHandlerPut: don't check length when there are errors Stop excessive wrapping CI: require that conformance tests pass bump(github.com/openshift/imagebuilder) to v1.1.8 Skip tlsVerify insecure BUILD_REGISTRY_SOURCES Fix build path wrong https://github.com/containers/podman/issues/7993 refactor pullpolicy to avoid deps build(deps): bump github.com/containers/common from 0.24.0 to 0.25.0 CI: run gating tasks with a lot more memory ADD and COPY: descend into excluded directories, sometimes copier: add more context to a couple of error messages copier: check an error earlier copier: log stderr output as debug on success Update nix pin with `make nixpkgs` Set directory ownership when copied with ID mapping build(deps): bump github.com/sirupsen/logrus from 1.6.0 to 1.7.0 build(deps): bump github.com/containers/common from 0.23.0 to 0.24.0 Cirrus: Remove bors artifacts Sort build flag definitions alphabetically ADD: only expand archives at the right time Remove configuration for bors Shell Completion for podman build flags Bump c/common to v0.24.0 New CI check: xref --help vs man pages CI: re-enable several linters Move --userns-uid-map/--userns-gid-map description into buildah man page add: preserve ownerships and permissions on ADDed archives Makefile: tweak the cross-compile target Bump containers/common to v0.23.0 chroot: create bind mount targets 0755 instead of 0700 Change call to Split() to safer SplitN() chroot: fix handling of errno seccomp rules build(deps): bump github.com/containers/image/v5 from 5.5.2 to 5.6.0 Add In Progress section to contributing integration tests: make sure tests run in ${topdir}/tests Run(): ignore containers.conf's environment configuration Warn when setting healthcheck in OCI format Cirrus: Skip git-validate on branches tools: update git-validation to the latest commit tools: update golangci-lint to v1.18.0 Add a few tests of push command Add(): fix handling of relative paths with no ContextDir build(deps): bump github.com/containers/common from 0.21.0 to 0.22.0 Lint: Use same linters as podman Validate: reference HEAD Fix buildah mount to display container names not ids Update nix pin with `make nixpkgs` Add missing --format option in buildah from man page Fix up code based on codespell build(deps): bump github.com/openshift/imagebuilder from 1.1.6 to 1.1.7 build(deps): bump github.com/containers/storage from 1.23.4 to 1.23.5 Improve buildah completions Cirrus: Fix validate commit epoch Fix bash completion of manifest flags Uniform some man pages Update Buildah Tutorial to address BZ1867426 Update bash completion of `manifest add` sub command copier.Get(): hard link targets shouldn't be relative paths build(deps): bump github.com/onsi/gomega from 1.10.1 to 1.10.2 Pass timestamp down to history lines Timestamp gets updated everytime you inspect an image bud.bats: use absolute paths in newly-added tests contrib/cirrus/lib.sh: don't use CN for the hostname tests: Add some tests Update `manifest add` man page Extend flags of `manifest add` build(deps): bump github.com/containers/storage from 1.23.3 to 1.23.4 build(deps): bump github.com/onsi/ginkgo from 1.14.0 to 1.14.1 Bump to v1.17.0-dev CI: expand cross-compile checks ## v1.16.0 (2020-09-03) fix build on 32bit arches containerImageRef.NewImageSource(): don't always force timestamps Add fuse module warning to image readme Heed our retry delay option values when retrying commit/pull/push Switch to containers/common for seccomp Use --timestamp rather then --omit-timestamp docs: remove outdated notice docs: remove outdated notice build-using-dockerfile: add a hidden --log-rusage flag build(deps): bump github.com/containers/image/v5 from 5.5.1 to 5.5.2 Discard ReportWriter if user sets options.Quiet build(deps): bump github.com/containers/common from 0.19.0 to 0.20.3 Fix ownership of content copied using COPY --from newTarDigester: zero out timestamps in tar headers Update nix pin with `make nixpkgs` bud.bats: correct .dockerignore integration tests Use pipes for copying run: include stdout in error message run: use the correct error for errors.Wrapf copier: un-export internal types copier: add Mkdir() in_podman: don't get tripped up by $CIRRUS_CHANGE_TITLE docs/buildah-commit.md: tweak some wording, add a --rm example imagebuildah: don’t blank out destination names when COPYing Replace retry functions with common/pkg/retry StageExecutor.historyMatches: compare timestamps using .Equal Update vendor of containers/common Fix errors found in coverity scan Change namespace handling flags to better match podman commands conformance testing: ignore buildah.BuilderIdentityAnnotation labels Vendor in containers/storage v1.23.0 Add buildah.IsContainer interface Avoid feeding run_buildah to pipe fix(buildahimage): add xz dependency in buildah image Bump github.com/containers/common from 0.15.2 to 0.18.0 Howto for rootless image building from OpenShift Add --omit-timestamp flag to buildah bud Update nix pin with `make nixpkgs` Shutdown storage on failures Handle COPY --from when an argument is used Bump github.com/seccomp/containers-golang from 0.5.0 to 0.6.0 Cirrus: Use newly built VM images Bump github.com/opencontainers/runc from 1.0.0-rc91 to 1.0.0-rc92 Enhance the .dockerignore man pages conformance: add a test for COPY from subdirectory fix bug manifest inspct Add documentation for .dockerignore Add BuilderIdentityAnnotation to identify buildah version DOC: Add quay.io/containers/buildah image to README.md Update buildahimages readme fix spelling mistake in "info" command result display Don't bind /etc/host and /etc/resolv.conf if network is not present blobcache: avoid an unnecessary NewImage() Build static binary with `buildGoModule` copier: split StripSetidBits into StripSetuidBit/StripSetgidBit/StripStickyBit tarFilterer: handle multiple archives Fix a race we hit during conformance tests Rework conformance testing Update 02-registries-repositories.md test-unit: invoke cmd/buildah tests with --flags parse: fix a type mismatch in a test Fix compilation of tests/testreport/testreport build.sh: log the version of Go that we're using test-unit: increase the test timeout to 40/45 minutes Add the "copier" package Fix & add notes regarding problematic language in codebase Add dependency on github.com/stretchr/testify/require CompositeDigester: add the ability to filter tar streams BATS tests: make more robust vendor golang.org/x/text@v0.3.3 Switch golang 1.12 to golang 1.13 imagebuildah: wait for stages that might not have even started yet chroot, run: not fail on bind mounts from /sys chroot: do not use setgroups if it is blocked Set engine env from containers.conf imagebuildah: return the right stage's image as the "final" image Fix a help string Deduplicate environment variables switch containers/libpod to containers/podman Bump github.com/containers/ocicrypt from 1.0.2 to 1.0.3 Bump github.com/opencontainers/selinux from 1.5.2 to 1.6.0 Mask out /sys/dev to prevent information leak linux: skip errors from the runtime kill Mask over the /sys/fs/selinux in mask branch Add VFS additional image store to container tests: add auth tests Allow "readonly" as alias to "ro" in mount options Ignore OS X specific consistency mount option Bump github.com/onsi/ginkgo from 1.13.0 to 1.14.0 Bump github.com/containers/common from 0.14.0 to 0.15.2 Rootless Buildah should default to IsolationOCIRootless imagebuildah: fix inheriting multi-stage builds Make imagebuildah.BuildOptions.Architecture/OS optional Make imagebuildah.BuildOptions.Jobs optional Resolve a possible race in imagebuildah.Executor.startStage() Switch scripts to use containers.conf Bump openshift/imagebuilder to v1.1.6 Bump go.etcd.io/bbolt from 1.3.4 to 1.3.5 buildah, bud: support --jobs=N for parallel execution executor: refactor build code inside new function Add bud regression tests Cirrus: Fix missing htpasswd in registry img docs: clarify the 'triples' format CHANGELOG.md: Fix markdown formatting Add nix derivation for static builds Bump to v1.16.0-dev version centos7 for compatible ## v1.15.0 (2020-06-17) Bump github.com/containers/common from 0.12.0 to 0.13.1 Bump github.com/containers/storage from 1.20.1 to 1.20.2 Bump github.com/seccomp/containers-golang from 0.4.1 to 0.5.0 Bump github.com/stretchr/testify from 1.6.0 to 1.6.1 Bump github.com/opencontainers/runc from 1.0.0-rc9 to 1.0.0-rc90 Add CVE-2020-10696 to CHANGELOG.md and changelog.txt Bump github.com/stretchr/testify from 1.5.1 to 1.6.0 Bump github.com/onsi/ginkgo from 1.12.2 to 1.12.3 Vendor in containers/common v0.12.0 fix lighttpd example Vendor in new go.etcd.io/bbolt Bump github.com/onsi/ginkgo from 1.12.1 to 1.12.2 Bump imagebuilder for ARG fix Bump github.com/containers/common from 0.11.2 to 0.11.4 remove dependency on openshift struct Warn on unset build arguments vendor: update seccomp/containers-golang to v0.4.1 Ammended docs Updated docs clean up comments update exit code for tests Implement commit for encryption implementation of encrypt/decrypt push/pull/bud/from fix resolve docker image name as transport Bump github.com/opencontainers/go-digest from 1.0.0-rc1 to 1.0.0 Bump github.com/onsi/ginkgo from 1.12.0 to 1.12.1 Bump github.com/containers/storage from 1.19.1 to 1.19.2 Bump github.com/containers/image/v5 from 5.4.3 to 5.4.4 Add preliminary profiling support to the CLI Bump github.com/containers/common from 0.10.0 to 0.11.2 Evaluate symlinks in build context directory fix error info about get signatures for containerImageSource Add Security Policy Cirrus: Fixes from review feedback Bump github.com/containers/storage from 1.19.0 to 1.19.1 Bump github.com/sirupsen/logrus from 1.5.0 to 1.6.0 imagebuildah: stages shouldn't count as their base images Update containers/common v0.10.0 Bump github.com/fsouza/go-dockerclient from 1.6.4 to 1.6.5 Add registry to buildahimage Dockerfiles Cirrus: Use pre-installed VM packages + F32 Cirrus: Re-enable all distro versions Cirrus: Update to F31 + Use cache images golangci-lint: Disable gosimple Lower number of golangci-lint threads Fix permissions on containers.conf Don't force tests to use runc Bump github.com/containers/common from 0.9.1 to 0.9.5 Return exit code from failed containers Bump github.com/containers/storage from 1.18.2 to 1.19.0 Bump github.com/containers/common from 0.9.0 to 0.9.1 cgroup_manager should be under [engine] Use c/common/pkg/auth in login/logout Cirrus: Temporarily disable Ubuntu 19 testing Add containers.conf to stablebyhand build Update gitignore to exclude test Dockerfiles Bump github.com/fsouza/go-dockerclient from 1.6.3 to 1.6.4 Bump github.com/containers/common from 0.8.1 to 0.9.0 Bump back to v1.15.0-dev Remove warning for systemd inside of container ## v1.14.8 (2020-04-09) Run (make vendor) Run (make -C tests/tools vendor) Run (go mod tidy) before (go mod vendor) again Fix (make vendor) Bump validation Bump back to v1.15.0-dev ## v1.14.7 (2020-04-07) Bump github.com/containers/image/v5 from 5.3.1 to 5.4.3 make vendor: run `tidy` after `vendor` Do not skip the directory when the ignore pattern matches Bump github.com/containers/common from 0.7.0 to 0.8.1 Downgrade siruspen/logrus from 1.4.2 Fix errorf conventions dockerignore tests : remove symlinks, rework Bump back to v1.15.0-dev ## v1.14.6 (2020-04-02) bud.bats - cleanup, refactoring vendor in latest containers/storage 1.18.0 and containers/common v0.7.0 Bump github.com/spf13/cobra from 0.0.6 to 0.0.7 Bump github.com/containers/storage from 1.16.5 to 1.17.0 Bump github.com/containers/image/v5 from 5.2.1 to 5.3.1 Fix Amazon install step Bump back to v1.15.0-dev Fix bud-build-arg-cache test Make image history work correctly with new args handling Don't add args to the RUN environment from the Builder Update github.com/openshift/imagebuilder to v1.1.4 Add .swp files to .gitignore ## v1.14.5 (2020-03-26) revert #2246 FIPS mode change Bump back to v1.15.0-dev image with dup layers: we now have one on quay digest test : make more robust ## v1.14.4 (2020-03-25) Fix fips-mode check for RHEL8 boxes Fix potential CVE in tarfile w/ symlink (Edit 02-Jun-2020: Addresses CVE-2020-10696) Fix .dockerignore with globs and ! commands update install steps for Amazon Linux 2 Bump github.com/openshift/imagebuilder from 1.1.2 to 1.1.3 Add comment for RUN command in volume ownership test Run stat command directly for volume ownership test vendor in containers/common v0.6.1 Cleanup go.sum Bump back to v1.15.0-dev ## v1.14.3 (2020-03-17) Update containers/storage to v1.16.5 Bump github.com/containers/storage from 1.16.2 to 1.16.4 Bump github.com/openshift/imagebuilder from 1.1.1 to 1.1.2 Update github.com/openshift/imagebuilder vendoring Update unshare man page to fix script example Fix compilation errors on non linux platforms Bump containers/common and opencontainers/selinux versions Add tests for volume ownership Preserve volume uid and gid through subsequent commands Fix FORWARD_NULL errors found by Coverity Bump github.com/containers/storage from 1.16.1 to 1.16.2 Fix errors found by codespell Bump back to v1.15.0-dev Add Pull Request Template ## v1.14.2 (2020-03-03) Add Buildah pull request template Bump to containers/storage v1.16.1 run_linux: fix tight loop if file is not pollable Bump github.com/opencontainers/selinux from 1.3.2 to 1.3.3 Bump github.com/containers/common from 0.4.1 to 0.4.2 Bump back to v1.15.0-dev Add Containerfile to build a versioned stable image on quay.io ## v1.14.1 (2020-02-27) Search for local runtime per values in containers.conf Set correct ownership on working directory BATS : in teardown, umount stale mounts Bump github.com/spf13/cobra from 0.0.5 to 0.0.6 Bump github.com/fsouza/go-dockerclient from 1.6.1 to 1.6.3 Bump github.com/stretchr/testify from 1.4.0 to 1.5.1 Replace unix with syscall to allow vendoring into libpod Update to containers/common v0.4.1 Improve remote manifest retrieval Fix minor spelling errors in containertools README Clear the right variable in buildahimage Correct a couple of incorrect format specifiers Update to containers/common v0.3.0 manifest push --format: force an image type, not a list type run: adjust the order in which elements are added to $PATH getDateAndDigestAndSize(): handle creation time not being set Bump github.com/containers/common from 0.2.0 to 0.2.1 include installation steps for CentOS 8 and Stream include installation steps for CentOS7 and forks Adjust Ubuntu install info to also work on Pop!_OS Make the commit id clear like Docker Show error on copied file above context directory in build Bump github.com/containers/image/v5 from 5.2.0 to 5.2.1 pull/from/commit/push: retry on most failures Makefile: fix install.cni.sudo Repair buildah so it can use containers.conf on the server side Bump github.com/mattn/go-shellwords from 1.0.9 to 1.0.10 Bump github.com/fsouza/go-dockerclient from 1.6.0 to 1.6.1 Fixing formatting & build instructions Add Code of Conduct Bors: Fix no. req. github reviews Cirrus+Bors: Simplify temp branch skipping Bors-ng: Add documentation and status-icon Bump github.com/onsi/ginkgo from 1.11.0 to 1.12.0 fix XDG_RUNTIME_DIR for authfile Cirrus: Disable F29 testing Cirrus: Add jq package Cirrus: Fix lint + validation using wrong epoch Stop using fedorproject registry Bors: Workaround ineffective required statuses Bors: Enable app + Disable Travis Cirrus: Add standardized log-collection Cirrus: Improve automated lint + validation Allow passing options to golangci-lint Cirrus: Fixes from review feedback Cirrus: Temporarily ignore VM testing failures Cirrus: Migrate off papr + implement VM testing Cirrus: Update packages + fixes for get_ci_vm.sh Show validation command-line Skip overlay test w/ vfs driver use alpine, not centos, for various tests Flake handling: cache and prefetch images Bump to v1.15.0-dev ## v1.14.0 (2020-02-05) bump github.com/mtrmac/gpgme Update containers/common to v0.1.4 manifest push: add --format option Bump github.com/onsi/gomega from 1.8.1 to 1.9.0 vendor github.com/containers/image/v5@v5.2.0 info test: deal with random key order Bump back to v1.14.0-dev ## v1.13.2 (2020-01-29) sign.bats: set GPG_TTY=/dev/null Fix parse_unsupported.go getDateAndDigestAndSize(): use manifest.Digest Bump github.com/opencontainers/selinux from 1.3.0 to 1.3.1 Bump github.com/containers/common from 0.1.0 to 0.1.2 Touch up os/arch doc chroot: handle slightly broken seccomp defaults buildahimage: specify fuse-overlayfs mount options Bump github.com/mattn/go-shellwords from 1.0.7 to 1.0.9 copy.bats: make sure we detect failures due to missing source parse: don't complain about not being able to rename something to itself Makefile: use a $(GO_TEST) macro, fix a typo manifests: unit test fix Fix build for 32bit platforms Allow users to set OS and architecture on bud Fix COPY in containerfile with envvar Bump c/storage to v1.15.7 add --sign-by to bud/commit/push, --remove-signatures for pull/push Remove cut/paste error in CHANGELOG.md Update vendor of containers/common to v0.1.0 update install instructions for Debian, Raspbian and Ubuntu Add support for containers.conf Bump back to v1.14.0-dev ## v1.13.1 (2020-01-14) Bump github.com/containers/common from 0.0.5 to 0.0.7 Bump github.com/onsi/ginkgo from 1.10.3 to 1.11.0 Bump github.com/pkg/errors from 0.8.1 to 0.9.0 Bump github.com/onsi/gomega from 1.7.1 to 1.8.1 Add codespell support copyFileWithTar: close source files at the right time copy: don't digest files that we ignore Check for .dockerignore specifically Travis: rm go 1.12.x Don't setup excludes, if their is only one pattern to match set HOME env to /root on chroot-isolation by default docs: fix references to containers-*.5 update openshift/api fix bug Add check .dockerignore COPY file buildah bud --volume: run from tmpdir, not source dir Fix imageNamePrefix to give consistent names in buildah-from cpp: use -traditional and -undef flags Fix image reference in tutorial 4 discard outputs coming from onbuild command on buildah-from --quiet make --format columnizing consistent with buildah images Bump to v1.14.0-dev ## v1.13.0 (2019-12-27) Bump to c/storage v1.15.5 Update container/storage to v1.15.4 Fix option handling for volumes in build Rework overlay pkg for use with libpod Fix buildahimage builds for buildah Add support for FIPS-Mode backends Set the TMPDIR for pulling/pushing image to $TMPDIR WIP: safer test for pull --all-tags BATS major cleanup: blobcache.bats: refactor BATS major cleanup: part 4: manual stuff BATS major cleanup, step 3: yet more run_buildah BATS major cleanup, part 2: use more run_buildah BATS major cleanup, part 1: log-level Bump github.com/containers/image/v5 from 5.0.0 to 5.1.0 Bump github.com/containers/common from 0.0.3 to 0.0.5 Bump to v1.13.0-dev ## v1.12.0 (2019-12-13) Allow ADD to use http src Bump to c/storage v.1.15.3 install.md: update golang dependency imgtype: reset storage opts if driver overridden Start using containers/common overlay.bats typo: fuse-overlays should be fuse-overlayfs chroot: Unmount with MNT_DETACH instead of UnmountMountpoints() bind: don't complain about missing mountpoints imgtype: check earlier for expected manifest type Vendor containers/storage fix Vendor containers/storage v1.15.1 Add history names support PR takeover of #1966 Tests: Add inspect test check steps Tests: Add container name and id check in containers test steps Test: Get permission in add test Tests: Add a test for tag by id Tests: Add test cases for push test Tests: Add image digest test Tests: Add some buildah from tests Tests: Add two commit test Tests: Add buildah bud with --quiet test Tests: Add two test for buildah add Bump back to v1.12.0-dev ## v1.11.6 (2019-12-03) Handle missing equal sign in --from and --chown flags for COPY/ADD bud COPY does not download URL Bump github.com/onsi/gomega from 1.7.0 to 1.7.1 Fix .dockerignore exclude regression Ran buildah through codespell commit(docker): always set ContainerID and ContainerConfig Touch up commit man page image parameter Add builder identity annotations. info: use util.Runtime() Bump github.com/onsi/ginkgo from 1.10.2 to 1.10.3 Bump back to v1.12.0-dev ## v1.11.5 (2019-11-11) Enhance error on unsafe symbolic link targets Add OCIRuntime to info Check nonexsit authfile Only output image id if running buildah bud --quiet Fix --pull=true||false and add --pull-never to bud and from (retry) cgroups v2: tweak or skip tests Prepwork: new 'skip' helpers for tests Handle configuration blobs for manifest lists unmarshalConvertedConfig: avoid using the updated image's ref Add completions for Manifest commands Add disableFips option to secrets pkg Update bud.bats test archive test Add test for caching based on content digest Builder.untarPath(): always evaluate b.ContentDigester.Hash() Bump github.com/onsi/ginkgo from 1.10.1 to 1.10.2 Fix another broken test: copy-url-mtime yet more fixes Actual bug fix for 'add' test: fix the expected mode BATS tests - lots of mostly minor cleanup build: drop support for ostree Add support for make vendor-in-container imgtype: exit with error if storage fails remove XDG_RUNTIME_DIR from default authfile path fix troubleshooting redirect instructions Bump back to v1.12.0-dev ## v1.11.4 (2019-10-28) buildah: add a "manifest" command manifests: add the module pkg/supplemented: add a package for grouping images together pkg/manifests: add a manifest list build/manipulation API Update for ErrUnauthorizedForCredentials API change in containers/image Update for manifest-lists API changes in containers/image version: also note the version of containers/image Move to containers/image v5.0.0 Enable --device directory as src device Fix git build with branch specified Bump github.com/openshift/imagebuilder from 1.1.0 to 1.1.1 Bump github.com/fsouza/go-dockerclient from 1.4.4 to 1.5.0 Add clarification to the Tutorial for new users Silence "using cache" to ensure -q is fully quiet Add OWNERS File to Buildah Bump github.com/containers/storage from 1.13.4 to 1.13.5 Move runtime flag to bud from common Commit: check for storage.ErrImageUnknown using errors.Cause() Fix crash when invalid COPY --from flag is specified. Bump back to v1.12.0-dev ## v1.11.3 (2019-10-04) Update c/image to v4.0.1 Bump github.com/spf13/pflag from 1.0.3 to 1.0.5 Fix --build-args handling Bump github.com/spf13/cobra from 0.0.3 to 0.0.5 Bump github.com/cyphar/filepath-securejoin from 0.2.1 to 0.2.2 Bump github.com/onsi/ginkgo from 1.8.0 to 1.10.1 Bump github.com/fsouza/go-dockerclient from 1.3.0 to 1.4.4 Add support for retrieving context from stdin "-" Ensure bud remote context cleans up on error info: add cgroups2 Bump github.com/seccomp/libseccomp-golang from 0.9.0 to 0.9.1 Bump github.com/mattn/go-shellwords from 1.0.5 to 1.0.6 Bump github.com/stretchr/testify from 1.3.0 to 1.4.0 Bump github.com/opencontainers/selinux from 1.2.2 to 1.3.0 Bump github.com/etcd-io/bbolt from 1.3.2 to 1.3.3 Bump github.com/onsi/gomega from 1.5.0 to 1.7.0 update c/storage to v1.13.4 Print build 'STEP' line to stdout, not stderr Fix travis-ci on forks Vendor c/storage v1.13.3 Use Containerfile by default Added tutorial on how to include Buildah as library util/util: Fix "configuraitno" -> "configuration" log typo Bump back to v1.12.0-dev ## v1.11.2 (2019-09-13) Add some cleanup code Move devices code to unit specific directory. Bump back to v1.12.0-dev ## v1.11.1 (2019-09-11) Add --devices flag to bud and from Downgrade .papr to highest atomic verion Add support for /run/.containerenv Truncate output of too long image names Preserve file and directory mount permissions Bump fedora version from 28 to 30 makeImageRef: ignore EmptyLayer if Squash is set Set TMPDIR to /var/tmp by default replace --debug=false with --log-level=error Allow mounts.conf entries for equal source and destination paths fix label and annotation for 1-line Dockerfiles Enable interfacer linter and fix lints install.md: mention goproxy Makefile: use go proxy Bump to v1.12.0-dev ## v1.11.0 (2019-08-29) tests/bud.bats: add --signature-policy to some tests Vendor github.com/openshift/api pull/commit/push: pay attention to $BUILD_REGISTRY_SOURCES Add `--log-level` command line option and deprecate `--debug` add support for cgroupsV2 Correctly detect ExitError values from Run() Disable empty logrus timestamps to reduce logger noise Remove outdated deps Makefile target Remove gofmt.sh in favor of golangci-lint Remove govet.sh in favor of golangci-lint Allow to override build date with SOURCE_DATE_EPOCH Update shebangs to take env into consideration Fix directory pull image names Add --digestfile and Re-add push statement as debug README: mention that Podman uses Buildah's API Use content digests in ADD/COPY history entries add: add a DryRun flag to AddAndCopyOptions Fix possible runtime panic on bud Add security-related volume options to validator use correct path for ginkgo Add bud 'without arguments' integration tests Update documentation about bud add: handle hard links when copying with .dockerignore add: teach copyFileWithTar() about symlinks and directories Allow buildah bud to be called without arguments imagebuilder: fix detection of referenced stage roots Touch up go mod instructions in install run_linux: fix mounting /sys in a userns Vendor Storage v1.13.2 Cirrus: Update VM images Fix handling of /dev/null masked devices Update `bud`/`from` help to contain indicator for `--dns=none` Bump back to v1.11.0-dev ## v1.10.1 (2019-08-08) Bump containers/image to v3.0.2 to fix keyring issue Bug fix for volume minus syntax Bump container/storage v1.13.1 and containers/image v3.0.1 bump github.com/containernetworking/cni to v0.7.1 Add overlayfs to fuse-overlayfs tip Add automatic apparmor tag discovery Fix bug whereby --get-login has no effect Bump to v1.11.0-dev ## v1.10.0 (2019-08-02) vendor github.com/containers/image@v3.0.0 Remove GO111MODULE in favor of `-mod=vendor` Vendor in containers/storage v1.12.16 Add '-' minus syntax for removal of config values tests: enable overlay tests for rootless rootless, overlay: use fuse-overlayfs vendor github.com/containers/image@v2.0.1 Added '-' syntax to remove volume config option delete `successfully pushed` message Add golint linter and apply fixes vendor github.com/containers/storage@v1.12.15 Change wait to sleep in buildahimage readme Handle ReadOnly images when deleting images Add support for listing read/only images ## v1.9.2 (2019-07-19) from/import: record the base image's digest, if it has one Fix CNI version retrieval to not require network connection Add misspell linter and apply fixes Add goimports linter and apply fixes Add stylecheck linter and apply fixes Add unconvert linter and apply fixes image: make sure we don't try to use zstd compression run.bats: skip the "z" flag when testing --mount Update to runc v1.0.0-rc8 Update to match updated runtime-tools API bump github.com/opencontainers/runtime-tools to v0.9.0 Build e2e tests using the proper build tags Add unparam linter and apply fixes Run: correct a typo in the --cap-add help text unshare: add a --mount flag fix push check image name is not empty Bump to v1.9.2-dev ## v1.9.1 (2019-07-12) add: fix slow copy with no excludes Add errcheck linter and fix missing error check Improve tests/tools/Makefile parallelism and abstraction Fix response body not closed resource leak Switch to golangci-lint Add gomod instructions and mailing list links On Masked path, check if /dev/null already mounted before mounting Update to containers/storage v1.12.13 Refactor code in package imagebuildah Add rootless podman with NFS issue in documentation Add --mount for buildah run import method ValidateVolumeOpts from libpod Fix typo Makefile: set GO111MODULE=off rootless: add the built-in slirp DNS server Update docker/libnetwork to get rid of outdated sctp package Update buildah-login.md migrate to go modules install.md: mention go modules tests/tools: go module for test binaries fix --volume splits comma delimited option Add bud test for RUN with a priv'd command vendor logrus v1.4.2 pkg/cli: panic when flags can't be hidden pkg/unshare: check all errors pull: check error during report write run_linux.go: ignore unchecked errors conformance test: catch copy error chroot/run_test.go: export funcs to actually be executed tests/imgtype: ignore error when shutting down the store testreport: check json error bind/util.go: remove unused func rm chroot/util.go imagebuildah: remove unused `dedupeStringSlice` StageExecutor: EnsureContainerPath: catch error from SecureJoin() imagebuildah/build.go: return instead of branching rmi: avoid redundant branching conformance tests: nilness: allocate map imagebuildah/build.go: avoid redundant `filepath.Join()` imagebuildah/build.go: avoid redundant `os.Stat()` imagebuildah: omit comparison to bool fix "ineffectual assignment" lint errors docker: ignore "repeats json tag" lint error pkg/unshare: use `...` instead of iterating a slice conformance: bud test: use raw strings for regexes conformance suite: remove unused func/var buildah test suite: remove unused vars/funcs testreport: fix golangci-lint errors util: remove redundant `return` statement chroot: only log clean-up errors images_test: ignore golangci-lint error blobcache: log error when draining the pipe imagebuildah: check errors in deferred calls chroot: fix error handling in deferred funcs cmd: check all errors chroot/run_test.go: check errors chroot/run.go: check errors in deferred calls imagebuildah.Executor: remove unused onbuild field docker/types.go: remove unused struct fields util: use strings.ContainsRune instead of index check Cirrus: Initial implementation Bump to v1.9.1-dev ## v1.9.0 (2019-06-15) buildah-run: fix-out-of-range panic (2) Bump back to v1.9.0-dev ## v1.8.4 (2019-06-13) Update containers/image to v2.0.0 run: fix hang with run and --isolation=chroot run: fix hang when using run chroot: drop unused function call remove --> before imgageID on build Always close stdin pipe Write deny to setgroups when doing single user mapping Avoid including linux/memfd.h Add a test for the symlink pointing to a directory Add missing continue Fix the handling of symlinks to absolute paths Only set default network sysctls if not rootless Support --dns=none like podman fix bug --cpu-shares parsing typo Fix validate complaint Update vendor on containers/storage to v1.12.10 Create directory paths for COPY thereby ensuring correct perms imagebuildah: use a stable sort for comparing build args imagebuildah: tighten up cache checking bud.bats: add a test verying the order of --build-args add -t to podman run imagebuildah: simplify screening by top layers imagebuildah: handle ID mappings for COPY --from imagebuildah: apply additionalTags ourselves bud.bats: test additional tags with cached images bud.bats: add a test for WORKDIR and COPY with absolute destinations Cleanup Overlay Mounts content ## v1.8.3 (2019-06-04) Add support for file secret mounts Add ability to skip secrets in mounts file allow 32bit builds fix tutorial instructions imagebuilder: pass the right contextDir to Add() add: use fileutils.PatternMatcher for .dockerignore bud.bats: add another .dockerignore test unshare: fallback to single usermapping addHelperSymlink: clear the destination on os.IsExist errors bud.bats: test replacing symbolic links imagebuildah: fix handling of destinations that end with '/' bud.bats: test COPY with a final "/" in the destination linux: add check for sysctl before using it unshare: set _CONTAINERS_ROOTLESS_GID Rework buildahimamges build context: support https git repos Add a test for ENV special chars behaviour Check in new Dockerfiles Apply custom SHELL during build time config: expand variables only at the command line SetEnv: we only need to expand v once Add default /root if empty on chroot iso Add support for Overlay volumes into the container. Export buildah validate volume functions so it can share code with libpod Bump baseline test to F30 Fix rootless handling of /dev/shm size Avoid fmt.Printf() in the library imagebuildah: tighten cache checking back up Handle WORKDIR with dangling target Default Authfile to proper path Make buildah run --isolation follow BUILDAH_ISOLATION environment Vendor in latest containers/storage and containers/image getParent/getChildren: handle layerless images imagebuildah: recognize cache images for layerless images bud.bats: test scratch images with --layers caching Get CHANGELOG.md updates Add some symlinks to test our .dockerignore logic imagebuildah: addHelper: handle symbolic links commit/push: use an everything-allowed policy Correct manpage formatting in files section Remove must be root statement from buildah doc Change image names to stable, testing and upstream Bump back to v1.9.0-dev ## v1.8.2 (2019-05-02) Vendor Storage 1.12.6 Create scratch file in TESTDIR Test bud-copy-dot with --layers picks up changed file Bump back to 1.9.0-dev ## v1.8.1 (2019-05-01) Don't create directory on container Replace kubernetes/pause in tests with k8s.gcr.io/pause imagebuildah: don't remove intermediate images if we need them Rework buildahimagegit to buildahimageupstream Fix Transient Mounts Handle WORKDIRs that are symlinks allow podman to build a client for windows Touch up 1.9-dev to 1.9.0-dev Bump to 1.9-dev ## v1.8.0 (2019-04-26) Resolve symlink when checking container path commit: commit on every instruction, but not always with layers CommitOptions: drop the unused OnBuild field makeImageRef: pass in the whole CommitOptions structure cmd: API cleanup: stores before images run: check if SELinux is enabled Fix buildahimages Dockerfiles to include support for additionalimages mounted from host. Detect changes in rootdir Fix typo in buildah-pull(1) Vendor in latest containers/storage Keep track of any build-args used during buildah bud --layers commit: always set a parent ID imagebuildah: rework unused-argument detection fix bug dest path when COPY .dockerignore Move Host IDMAppings code from util to unshare Add BUILDAH_ISOLATION rootless back Travis CI: fail fast, upon error in any step imagebuildah: only commit images for intermediate stages if we have to Use errors.Cause() when checking for IsNotExist errors auto pass http_proxy to container Bump back to 1.8-dev ## v1.7.3 (2019-04-16) imagebuildah: don't leak image structs Add Dockerfiles for buildahimages Bump to Replace golang 1.10 with 1.12 add --dns* flags to buildah bud Add hack/build_speed.sh test speeds on building container images Create buildahimage Dockerfile for Quay rename 'is' to 'expect_output' squash.bats: test squashing in multi-layered builds bud.bats: test COPY --from in a Dockerfile while using the cache commit: make target image names optional Fix bud-args to allow comma separation oops, missed some tests in commit.bats new helper: expect_line_count New tests for #1467 (string slices in cmdline opts) Workarounds for dealing with travis; review feedback BATS tests - extensive but minor cleanup imagebuildah: defer pulling images for COPY --from imagebuildah: centralize COMMIT and image ID output Travis: do not use traviswait imagebuildah: only initialize imagebuilder configuration once per stage Make cleaner error on Dockerfile build errors unshare: move to pkg/ unshare: move some code from cmd/buildah/unshare Fix handling of Slices versus Arrays imagebuildah: reorganize stage and per-stage logic imagebuildah: add empty layers for instructions Add missing step in installing into Ubuntu fix bug in .dockerignore support imagebuildah: deduplicate prepended "FROM" instructions Touch up intro commit: set created-by to the shell if it isn't set commit: check that we always set a "created-by" docs/buildah.md: add "containers-" prefixes under "SEE ALSO" Bump back to 1.8-dev ## v1.7.2 (2019-03-28) mount: do not create automatically a namespace buildah: correctly create the userns if euid!=0 imagebuildah.Build: consolidate cleanup logic CommitOptions: drop the redundant Store field Move pkg/chrootuser from libpod to buildah. imagebuildah: record image IDs and references more often vendor imagebuilder v1.1.0 imagebuildah: fix requiresStart/noRunsRemaining confusion imagebuildah: check for unused args across stages bump github.com/containernetworking/cni to v0.7.0-rc2 imagebuildah: use "useCache" instead of "noCache" imagebuildah.resolveNameToImageRef(): take name as a parameter Export fields of the DokcerIgnore struct imagebuildah: drop the duplicate containerIDs list rootless: by default use the host network namespace imagebuildah: split Executor and per-stage execution imagebuildah: move some fields around golint: make golint happy docs: 01-intro.md: add missing . in Dockerfile examples fix bug using .dockerignore Do not create empty mounts.conf file images: suppress a spurious blank line with no images from: distinguish between ADD and COPY fix bug to not separate each --label value with comma buildah-bud.md: correct a typo, note a default Remove mistaken code that got merged in other PR add sample registries.conf to docs escape shell variables in README example slirp4netns: set mtu to 65520 images: imageReposToMap() already adds : imagebuildah.ReposToMap: move to cmd Build: resolve copyFrom references earlier Allow rootless users to use the cache directory in homedir bud.bats: use the per-test temp directory bud.bats: log output before counting length Simplify checks for leftover args Print commitID with --layers fix bug images use the template to print results rootless: honor --net host onsi/gomeage add missing files vendor latest openshift/imagebuilder Remove noop from squash help Prepend a comment to files setup in container imagebuildah resolveSymlink: fix handling of relative links Errors should be printed to stderr Add recommends for slirp4netns and fuse-overlay Update pull and pull-always flags Hide from users command options that we don't want them to use. Update secrets fipsmode patch to work on rootless containers fix unshare option handling and documentation Vendor in latest containers/storage Hard-code docker.Transport use in pull --all-tags Use a types.ImageReference instead of (transport, name) strings in pullImage etc. Move the computation of srcRef before first pullAndFindImage Don't throw away user-specified tag for pull --all-tags CHANGES BEHAVIOR: Remove the string format input to localImageNameForReference Don't try to parse imageName as transport:image in pullImage Use reference.WithTag instead of manual string manipulation in Pull Don't pass image = transport:repo:tag, transport=transport to pullImage Fix confusing variable naming in Pull Don't try to parse image name as a transport:image Fix error reporting when parsing trans+image Remove 'transport == ""' handling from the pull path Clean up "pulls" of local image IDs / ID prefixes Simplify ExpandNames Document the semantics of transport+name returned by ResolveName UPdate gitvalidation epoch Bump back to 1.8-dev ## v1.7.1 (2019-02-26) vendor containers/image v1.5 Move secrets code from libpod into buildah Update CHANGELOG.md with the past changes README.md: fix typo Fix a few issues found by tests/validate/gometalinter.sh Neutralize buildah/unshare on non-Linux platforms Explicitly specify a directory to find(1) README.md: rephrase Buildah description Stop printing default twice in cli --help install.md: add section about vendoring Bump to 1.8-dev ## v1.7 (2019-02-21) vendor containers/image v1.4 Make "images --all" faster Remove a misleading comment Remove quiet option from pull options Make sure buildah pull --all-tags only works with docker transport Support oci layout format Fix pulling of images within buildah Fix tls-verify polarity Travis: execute make vendor and hack/tree_status.sh vendor.conf: remove unused dependencies add missing vendor/github.com/containers/libpod/vendor.conf vendor.conf: remove github.com/inconshreveable/mousetrap make vendor: always fetch the latest vndr add hack/tree_status.sh script Bump c/Storage to 1.10 Add --all-tags test to pull mount: make error clearer Remove global flags from cli help Set --disable-compression to true as documented Help document using buildah mount in rootless mode healthcheck start-period: update documentation Vendor in latest c/storage and c/image dumpbolt: handle nested buckets Fix buildah commit compress by default Test on xenial, not trusty unshare: reexec using a memfd copy instead of the binary Add --target to bud command Fix example for setting multiple environment variables main: fix rootless mode buildah: force umask 022 pull.bats: specify registry config when using registries pull.bats: use the temporary directory, not /tmp unshare: do not set rootless mode if euid=0 Touch up cli help examples and a few nits Add an undocumented dumpbolt command Move tar commands into containers/storage Fix bud issue with 2 line Dockerfile Add package install descriptions Note configuration file requirements Replace urfave/cli with cobra cleanup vendor.conf Vendor in latest containers/storage Add Quiet to PullOptions and PushOptions cmd/commit: add flag omit-timestamp to allow for deterministic builds Add options for empty-layer history entries Make CLI help descriptions and usage a bit more consistent vndr opencontainers/selinux Bump baseline test Fedora to 29 Bump to v1.7-dev-1 Bump to v1.6-1 Add support for ADD --chown imagebuildah: make EnsureContainerPath() check/create the right one Bump 1.7-dev Fix contrib/rpm/bulidah.spec changelog date ## v1.6-1 (2019-01-18) Add support for ADD --chown imagebuildah: make EnsureContainerPath() check/create the right one Fix contrib/rpm/bulidah.spec changelog date Vendor in latest containers/storage Revendor everything Revendor in latest code by release unshare: do not set USER=root run: ignore EIO when flushing at the end, avoid double log build-using-dockerfile,commit: disable compression by default Update some comments Make rootless work under no_pivot_root Add CreatedAtRaw date field for use with Format Properly format images JSON output pull: add all-tags option Fix support for multiple Short options pkg/blobcache: add synchronization Skip empty files in file check of conformance test Use NoPivot also for RUN, not only for run Remove no longer used isReferenceInsecure / isRegistryInsecure Do not set OCIInsecureSkipTLSVerify based on registries.conf Remove duplicate entries from images JSON output vendor parallel-copy from containers/image blobcache.bats: adjust explicit push tests Handle one line Dockerfile with layers We should only warn if user actually requests Hostname be set in image Fix compiler Warning about comparing different size types imagebuildah: don't walk if rootdir and path are equal Add aliases for buildah containers, so buildah list, ls and ps work vendor: use faster version instead compress/gzip vendor: update libpod Properly handle Hostname inside of RUN command docs: mention how to mount in rootless mode tests: use fully qualified name for centos image travis.yml: use the fully qualified name for alpine mount: allow mount only when using vfs Add some tests for buildah pull Touch up images -q processing Refactor: Use library shared idtools.ParseIDMap() instead of bundling it bump GITVALIDATE_EPOCH cli.BudFlags: add `--platform` nop Makefile: allow packagers to more easily add tags Makefile: soften the requirement on git tests: add containers json test Inline blobCache.putBlob into blobCacheDestination.PutBlob Move saveStream and putBlob near blobCacheDestination.PutBlob Remove BlobCache.PutBlob Update for API changes Vendor c/image after merging c/image#536 Handle 'COPY --from' in Dockerfile Vendor in latest content from github.com/containers/storage Clarify docker.io default in push with docker-daemon Test blob caching Wire in a hidden --blob-cache option Use a blob cache when we're asked to use one Add --disable-compression to 'build-using-dockerfile' Add a blob cache implementation vendor: update containers/storage Update for sysregistriesv2 API changes Update containers/image to 63a1cbdc5e6537056695cf0d627c0a33b334df53 clean up makefile variables Fix file permission Complete the instructions for the command Show warning when a build arg not used Assume user 0 group 0, if /etc/passwd file in container. Add buildah info command Enable -q when --filter is used for images command Add v1.5 Release Announcement Fix dangling filter for images command Fix completions to print Names as well as IDs tests: Fix file permissions Bump 1.6-dev ## v1.5-1 (2018-11-21) Bump min go to 1.10 in install.md vendor: update ostree-go Update docker build command line in conformance test Print command in SystemExec as debug information Add some skip word for inspect check in conformance test Update regex for multi stage base test Sort CLI flags vendor: update containers/storage Add note to install about non-root on RHEL/CentOS Update imagebuild depdency to support heading ARGs in Dockerfile rootless: do not specify --rootless to the OCI runtime Export resolvesymlink function Exclude --force-rm from common bud cli flags run: bind mount /etc/hosts and /etc/resolv.conf if not in a volume rootless: use slirp4netns to setup the network namespace Instructions for completing the pull command Fix travis to not run environment variable patch rootless: only discard network configuration names run: only set up /etc/hosts or /etc/resolv.conf with network common: getFormat: match entire string not only the prefix vendor: update libpod Change validation EPOCH Fixing broken link for container-registries.conf Restore rootless isolation test for from volume ro test ostree: fix tag for build constraint Handle directories better in bud -f vndr in latest containers/storage Fix unshare gofmt issue runSetupBuiltinVolumes(): break up volume setup common: support a per-user registries conf file unshare: do not override the configuration common: honor the rootless configuration file unshare: create a new mount namespace unshare: support libpod rootless pkg Use libpod GetDefaultStorage to report proper storage config Allow container storage to manage the SELinux labels Resolve image names with default transport in from command run: When the value of isolation is set, use the set value instead of the default value. Vendor in latest containers/storage and opencontainers/selinux Remove no longer valid todo Check for empty buildTime in version Change gofmt so it runs on all but 1.10 Run gofmt only on Go 1.11 Walk symlinks when checking cached images for copied/added files ReserveSELinuxLabels(): handle wrapped errors from OpenBuilder Set WorkingDir to empty, not / for conformance Update calls in e2e to addres 1101 imagebuilder.BuildDockerfiles: return the image ID Update for changes in the containers/image API bump(github.com/containers/image) Allow setting --no-pivot default with an env var Add man page and bash completion, for --no-pivot Add the --no-pivot flag to the run command Improve reporting about individual pull failures Move the "short name but no search registries" error handling to resolveImage Return a "search registries were needed but empty" indication in util.ResolveName Simplify handling of the "tried to pull an image but found nothing" case in newBuilder Don't even invoke the pull loop if options.FromImage == "" Eliminate the long-running ref and img variables in resolveImage In resolveImage, return immediately on success Fix From As in Dockerfile Vendor latest containers/image Vendor in latest libpod Sort CLI flags of buildah bud Change from testing with golang 1.9 to 1.11. unshare: detect when unprivileged userns are disabled Optimize redundant code fix missing format param chroot: fix the args check imagebuildah: make ResolveSymLink public Update copy chown test buildah: use the same logic for XDG_RUNTIME_DIR as podman V1.4 Release Announcement Podman --privileged selinux is broken papr: mount source at gopath parse: Modify the return value parse: modify the verification of the isolation value Make sure we log or return every error pullImage(): when completing an image name, try docker:// Fix up Tutorial 3 to account for format Vendor in latest containers/storage and containers/image docs/tutorials/01-intro.md: enhanced installation instructions Enforce "blocked" for registries for the "docker" transport Correctly set DockerInsecureSkipTLSVerify when pulling images chroot: set up seccomp and capabilities after supplemental groups chroot: fix capabilities list setup and application .papr.yml: log the podman version namespaces.bats: fix handling of uidmap/gidmap options in pairs chroot: only create user namespaces when we know we need them Check /proc/sys/user/max_user_namespaces on unshare(NEWUSERNS) bash/buildah: add isolation option to the from command ## v1.4 (2018-10-02) from: fix isolation option Touchup pull manpage Export buildah ReserveSELinuxLables so podman can use it Add buildah.io to README.md and doc fixes Update rmi man for prune changes Ignore file not found removal error in bud bump(github.com/containers/{storage,image}) NewImageSource(): only create one Diff() at a time Copy ExposedPorts from base image into the config tests: run conformance test suite in Travis Change rmi --prune to not accept an imageID Clear intermediate container IDs after each stage Request podman version for build issues unshare: keep the additional groups of the user Builtin volumes should be owned by the UID/GID of the container Get rid of dangling whitespace in markdown files Move buildah from projecatatomic/buildah to containers/buildah nitpick: parse.validateFlags loop in bud cli bash: Completion options Add signature policy to push tests vendor in latest containers/image Fix grammar in Container Tools Guide Don't build btrfs if it is not installed new: Return image-pulling errors from resolveImage pull: Return image-pulling errors from pullImage Add more volume mount tests chroot: create missing parent directories for volume mounts Push: Allow an empty destination Add Podman relationship to readme, create container tools guide Fix arg usage in buildah-tag Add flags/arguments order verification to other commands Handle ErrDuplicateName errors from store.CreateContainer() Evaluate symbolic links on Add/Copy Commands Vendor in latest containers/image and containers/storage Retain bounding set when running containers as non root run container-diff tests in Travis buildah-images.md: Fix option contents push: show image digest after push succeed Vendor in latest containers/storage,image,libpod and runc Change references to cri-o to point at new repository Exclude --layers from the common bug cli flags demos: Increase the executable permissions run: clear default seccomp filter if not enabled Bump maximum cyclomatic complexity to 45 stdin: on HUP, read everything nitpick: use tabs in tests/helpers.bash Add flags/arguments order verification to one arg commands nitpick: decrease cognitive complexity in buildah-bud rename: Avoid renaming the same name as other containers chroot isolation: chroot() before setting up seccomp Small nitpick at the "if" condition in tag.go cmd/images: Modify json option cmd/images: Disallow the input of image when using the -a option Fix examples to include context directory Update containers/image to fix commit layer issue cmd/containers: End loop early when using the json option Make buildah-from error message clear when flags are after arg Touch up README.md for conformance tests Update container/storage for lock fix cmd/rm: restore the correct containerID display Remove debug lines Remove docker build image after each test Add README for conformance test Update the MakeOptions to accept all command options for buildah Update regrex to fit the docker output in test "run with JSON" cmd/buildah: Remove redundant variable declarations Warn about using Commands in Dockerfile that are not supported by OCI. Add buildah bud conformance test Fix rename to also change container name in builder Makefile: use $(GO) env-var everywhere Cleanup code to more closely match Docker Build images Document BUILDAH_* environment variables in buildah bud --help output Return error immediately if error occurs in Prepare step Fix --layers ADD from url issue Add "Sign your PRs" TOC item to contributing.md. Display the correct ID after deleting image rmi: Modify the handling of errors Let util.ResolveName() return parsing errors Explain Open Container Initiative (OCI) acronym, add link Update vendor for urfave/cli back to master Handle COPY --chown in Dockerfile Switch to Recommends container-selinux Update vendor for containernetworking, imagebuildah and podman Document STORAGE_DRIVER and STORAGE_OPTS environment variable Change references to projectatomic/libpod to containers/libpod Add container PATH retrieval example Expand variables names for --env imagebuildah: provide a way to provide stdin for RUN Remove an unused srcRef.NewImageSource in pullImage chroot: correct a comment chroot: bind mount an empty directory for masking Don't bother with --no-pivot for rootless isolation CentOS need EPEL repo Export a Pull() function Remove stream options, since docker build does not have it release v1.3: mention openSUSE Add Release Announcements directory Bump to v1.4-dev ## 1.3 (2018-08-4) Revert pull error handling from 881 bud should not search context directory for Dockerfile Set BUILDAH_ISOLATION=rootless when running unprivileged .papr.sh: Also test with BUILDAH_ISOLATION=rootless Skip certain tests when we're using "rootless" isolation .travis.yml: run integration tests with BUILDAH_ISOLATION=chroot Add and implement IsolationOCIRootless Add a value for IsolationOCIRootless Fix rmi to remove intermediate images associated with an image Return policy error on pull Update containers/image to 216acb1bcd2c1abef736ee322e17147ee2b7d76c Switch to github.com/containers/image/pkg/sysregistriesv2 unshare: make adjusting the OOM score optional Add flags validation chroot: handle raising process limits chroot: make the resource limits name map module-global Remove rpm.bats, we need to run this manually Set the default ulimits to match Docker buildah: no args is out of bounds unshare: error message missed the pid preprocess ".in" suffixed Dockerfiles Fix the the in buildah-config man page Only test rpmbuild on latest fedora Add support for multiple Short options Update to latest urvave/cli Add additional SELinux tests Vendor in latest github.com/containers/{image;storage} Stop testing with golang 1.8 Fix volume cache issue with buildah bud --layers Create buildah pull command Increase the deadline for gometalinter during 'make validate' .papr.sh: Also test with BUILDAH_ISOLATION=chroot .travis.yml: run integration tests with BUILDAH_ISOLATION=chroot Add a Dockerfile Set BUILDAH_ISOLATION=chroot when running unprivileged Add and implement IsolationChroot Update github.com/opencontainers/runc maybeReexecUsingUserNamespace: add a default for root Allow ping command without NET_RAW Capabilities rmi.storageImageID: fix Wrapf format warning Allow Dockerfile content to come from stdin Vendor latest container/storage to fix overlay mountopt userns: assign additional IDs sequentially Remove default dev/pts Add OnBuild test to baseline test tests/run.bats(volumes): use :z when SELinux is enabled Avoid a stall in runCollectOutput() Use manifest from container/image Vendor in latest containers/image and containers/storage add rename command Completion command Update CHANGELOG.md Update vendor for runc to fix 32 bit builds bash completion: remove shebang Update vendor for runc to fix 32 bit builds ## 1.2 (2018-07-14) Vendor in lates containers/image build-using-dockerfile: let -t include transports again Block use of /proc/acpi and /proc/keys from inside containers Fix handling of --registries-conf Fix becoming a maintainer link add optional CI test fo darwin Don't pass a nil error to errors.Wrapf() image filter test: use kubernetes/pause as a "since" Add --cidfile option to from vendor: update containers/storage Contributors need to find the CONTRIBUTOR.md file easier Add a --loglevel option to build-with-dockerfile Create Development plan cmd: Code improvement allow buildah cross compile for a darwin target Add unused function param lint check docs: Follow man-pages(7) suggestions for SYNOPSIS Start using github.com/seccomp/containers-golang umount: add all option to umount all mounted containers runConfigureNetwork(): remove an unused parameter Update github.com/opencontainers/selinux Fix buildah bud --layers Force ownership of /etc/hosts and /etc/resolv.conf to 0:0 main: if unprivileged, reexec in a user namespace Vendor in latest imagebuilder Reduce the complexity of the buildah.Run function mount: output it before replacing lastError Vendor in latest selinux-go code Implement basic recognition of the "--isolation" option Run(): try to resolve non-absolute paths using $PATH Run(): don't include any default environment variables build without seccomp vendor in latest runtime-tools bind/mount_unsupported.go: remove import errors Update github.com/opencontainers/runc Add Capabilities lists to BuilderInfo Tweaks for commit tests commit: recognize committing to second storage locations Fix ARGS parsing for run commands Add info on registries.conf to from manpage Switch from using docker to podman for testing in .papr buildah: set the HTTP User-Agent ONBUILD tutorial Add information about the configuration files to the install docs Makefile: add uninstall Add tilde info for push to troubleshooting mount: support multiple inputs Use the right formatting when adding entries to /etc/hosts Vendor in latest go-selinux bindings Allow --userns-uid-map/--userns-gid-map to be global options bind: factor out UnmountMountpoints Run(): simplify runCopyStdio() Run(): handle POLLNVAL results Run(): tweak terminal mode handling Run(): rename 'copyStdio' to 'copyPipes' Run(): don't set a Pdeathsig for the runtime Run(): add options for adding and removing capabilities Run(): don't use a callback when a slice will do setupSeccomp(): refactor Change RunOptions.Stdin/Stdout/Stderr to just be Reader/Writers Escape use of '_' in .md docs Break out getProcIDMappings() Break out SetupIntermediateMountNamespace() Add Multi From Demo Use the c/image conversion code instead of converting configs manually Don't throw away the manifest MIME type and guess again Consolidate loading manifest and config in initConfig Pass a types.Image to Builder.initConfig Require an image ID in importBuilderDataFromImage Use c/image/manifest.GuessMIMEType instead of a custom heuristic Do not ignore any parsing errors in initConfig Explicitly handle "from scratch" images in Builder.initConfig Fix parsing of OCI images Simplify dead but dangerous-looking error handling Don't ignore v2s1 history if docker_version is not set Add --rm and --force-rm to buildah bud Add --all,-a flag to buildah images Separate stdio buffering from writing Remove tty check from images --format Add environment variable BUILDAH_RUNTIME Add --layers and --no-cache to buildah bud Touch up images man version.md: fix DESCRIPTION tests: add containers test tests: add images test images: fix usage fix make clean error Change 'registries' to 'container registries' in man add commit test Add(): learn to record hashes of what we add Minor update to buildah config documentation for entrypoint Bump to v1.2-dev Add registries.conf link to a few man pages ## 1.1 (2018-06-08) Drop capabilities if running container processes as non root Print Warning message if cmd will not be used based on entrypoint Update 01-intro.md Shouldn't add insecure registries to list of search registries Report errors on bad transports specification when pushing images Move parsing code out of common for namespaces and into pkg/parse.go Add disable-content-trust noop flag to bud Change freenode chan to buildah runCopyStdio(): don't close stdin unless we saw POLLHUP Add registry errors for pull runCollectOutput(): just read until the pipes are closed on us Run(): provide redirection for stdio rmi, rm: add test add mount test Add parameter judgment for commands that do not require parameters Add context dir to bud command in baseline test run.bats: check that we can run with symlinks in the bundle path Give better messages to users when image can not be found use absolute path for bundlePath Add environment variable to buildah --format rm: add validation to args and all option Accept json array input for config entrypoint Run(): process RunOptions.Mounts, and its flags Run(): only collect error output from stdio pipes if we created some Add OnBuild support for Dockerfiles Quick fix on demo readme run: fix validate flags buildah bud should require a context directory or URL Touchup tutorial for run changes Validate common bud and from flags images: Error if the specified imagename does not exist inspect: Increase err judgments to avoid panic add test to inspect buildah bud picks up ENV from base image Extend the amount of time travis_wait should wait Add a make target for Installing CNI plugins Add tests for namespace control flags copy.bats: check ownerships in the container Fix SELinux test errors when SELinux is enabled Add example CNI configurations Run: set supplemental group IDs Run: use a temporary mount namespace Use CNI to configure container networks add/secrets/commit: Use mappings when setting permissions on added content Add CLI options for specifying namespace and cgroup setup Always set mappings when using user namespaces Run(): break out creation of stdio pipe descriptors Read UID/GID mapping information from containers and images Additional bud CI tests Run integration tests under travis_wait in Travis build-using-dockerfile: add --annotation Implement --squash for build-using-dockerfile and commit Vendor in latest container/storage for devicemapper support add test to inspect Vendor github.com/onsi/ginkgo and github.com/onsi/gomega Test with Go 1.10, too Add console syntax highlighting to troubleshooting page bud.bats: print "$output" before checking its contents Manage "Run" containers more closely Break Builder.Run()'s "run runc" bits out util.ResolveName(): handle completion for tagged/digested image names Handle /etc/hosts and /etc/resolv.conf properly in container Documentation fixes Make it easier to parse our temporary directory as an image name Makefile: list new pkg/ subdirectoris as dependencies for buildah containerImageSource: return more-correct errors API cleanup: PullPolicy and TerminalPolicy should be types Make "run --terminal" and "run -t" aliases for "run --tty" Vendor github.com/containernetworking/cni v0.6.0 Update github.com/containers/storage Update github.com/containers/libpod Add support for buildah bud --label buildah push/from can push and pull images with no reference Vendor in latest containers/image Update gometalinter to fix install.tools error Update troubleshooting with new run workaround Added a bud demo and tidied up Attempt to download file from url, if fails assume Dockerfile Add buildah bud CI tests for ENV variables Re-enable rpm .spec version check and new commit test Update buildah scratch demo to support el7 Added Docker compatibility demo Update to F28 and new run format in baseline test Touchup man page short options across man pages Added demo dir and a demo. chged distrorlease builder-inspect: fix format option Add cpu-shares short flag (-c) and cpu-shares CI tests Minor fixes to formatting in rpm spec changelog Fix rpm .spec changelog formatting CI tests and minor fix for cache related noop flags buildah-from: add effective value to mount propagation ## 1.0 (2018-05-06) Declare Buildah 1.0 Add cache-from and no-cache noops, and fix doco Update option and documentation for --force-rm Adding noop for --force-rm to match --rm Add buildah bud ENTRYPOINT,CMD,RUN tests Adding buildah bud RUN test scenarios Extend tests for empty buildah run command Fix formatting error in run.go Update buildah run to make command required Expanding buildah run cmd/entrypoint tests Update test cases for buildah run behaviour Remove buildah run cmd and entrypoint execution Add Files section with registries.conf to pertinent man pages tests/config: perfect test tests/from: add name test Do not print directly to stdout in Commit() Touch up auth test commands Force "localhost" as a default registry Drop util.GetLocalTime() Vendor in latest containers/image Validate host and container paths passed to --volume test/from: add add-host test Add --compress, --rm, --squash flags as a noop for bud Add FIPS mode secret to buildah run and bud Add config --comment/--domainname/--history-comment/--hostname 'buildah config': stop replacing Created-By whenever it's not specified Modify man pages so they compile correctly in mandb Add description on how to do --isolation to buildah-bud man page Add support for --iidfile to bud and commit Refactor buildah bud for vendoring Fail if date or git not installed Revert update of entrypoint behaviour to match docker Vendor in latest imagebuilder code to fix multiple stage builds Add /bin/sh -c to entrypoint in config image_test: Improve the test Fix README example of buildah config buildah-image: add validation to 'format' Simple changes to allow buildah to pass make validate Clarify the use of buildah config options containers_test: Perfect testing buildah images and podman images are listing different sizes buildah-containers: add tests and example to the man page buildah-containers: add validation to 'format' Clarify the use of buildah config options Minor fix for lighttpd example in README Add tls-verification to troubleshooting Modify buildah rmi to account for changes in containers/storage Vendor in latest containers/image and containers/storage addcopy: add src validation Remove tarball as an option from buildah push --help Fix secrets patch Update entrypoint behaviour to match docker Display imageId after commit config: add support for StopSignal Fix docker login issue in travis.yml Allow referencing stages as index and names Add multi-stage builds tests Add multi-stage builds support Add accessor functions for comment and stop signal Vendor in latest imagebuilder, to get mixed case AS support Allow umount to have multi-containers Update buildah push doc buildah bud walks symlinks Imagename is required for commit atm, update manpage ## 0.16.0 (2018-04-08) Bump to v0.16.0 Remove requires for ostree-lib in rpm spec file Add support for shell buildah.spec should require ostree-libs Vendor in latest containers/image bash: prefer options Change image time to locale, add troubleshooting.md, add logo to other mds buildah-run.md: fix error SYNOPSIS docs: fix error example Allow --cmd parameter to have commands as values Touchup README to re-enable logo Clean up README.md Make default-mounts-file a hidden option Document the mounts.conf file Fix man pages to format correctly Add various transport support to buildah from Add unit tests to run.go If the user overrides the storage driver, the options should be dropped Show Config/Manifest as JSON string in inspect when format is not set Switch which for that in README.md Remove COPR Fix wrong order of parameters Vendor in latest containers/image Remove shallowCopy(), which shouldn't be saving us time any more shallowCopy: avoid a second read of the container's layer ## 0.5 - 2017-11-07 Add secrets patch to buildah Add proper SELinux labeling to buildah run Add tls-verify to bud command Make filtering by date use the image's date images: don't list unnamed images twice Fix timeout issue Add further tty verbiage to buildah run Make inspect try an image on failure if type not specified Add support for `buildah run --hostname` Tons of bug fixes and code cleanup ## 0.4 - 2017-09-22 ### Added Update buildah spec file to match new version Bump to version 0.4 Add default transport to push if not provided Add authentication to commit and push Remove --transport flag Run: don't complain about missing volume locations Add credentials to buildah from Remove export command Bump containers/storage and containers/image ## 0.3 - 2017-07-20 ## 0.2 - 2017-07-18 ### Added Vendor in latest containers/image and containers/storage Update image-spec and runtime-spec to v1.0.0 Add support for -- ending options parsing to buildah run Add/Copy need to support glob syntax Add flag to remove containers on commit Add buildah export support update 'buildah images' and 'buildah rmi' commands buildah containers/image: Add JSON output option Add 'buildah version' command Handle "run" without an explicit command correctly Ensure volume points get created, and with perms Add a -a/--all option to "buildah containers" ## 0.1 - 2017-06-14 ### Added Vendor in latest container/storage container/image Add a "push" command Add an option to specify a Create date for images Allow building a source image from another image Improve buildah commit performance Add a --volume flag to "buildah run" Fix inspect/tag-by-truncated-image-ID Include image-spec and runtime-spec versions buildah mount command should list mounts when no arguments are given. Make the output image format selectable commit images in multiple formats Also import configurations from V2S1 images Add a "tag" command Add an "inspect" command Update reference comments for docker types origins Improve configuration preservation in imagebuildah Report pull/commit progress by default Contribute buildah.spec Remove --mount from buildah-from Add a build-using-dockerfile command (alias: bud) Create manpages for the buildah project Add installation for buildah and bash completions Rename "list"/"delete" to "containers"/"rm" Switch `buildah list quiet` option to only list container id's buildah delete should be able to delete multiple containers Correctly set tags on the names of pulled images Don't mix "config" in with "run" and "commit" Add a "list" command, for listing active builders Add "add" and "copy" commands Add a "run" command, using runc Massive refactoring Make a note to distinguish compression of layers ## 0.0 - 2017-01-26 ### Added Initial version, needs work ================================================ FILE: CODE-OF-CONDUCT.md ================================================ ## The Buildah Project Community Code of Conduct The Buildah Project, as part of Podman Container Tools, follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). ================================================ FILE: CONTRIBUTING.md ================================================ ![buildah logo](https://cdn.rawgit.com/containers/buildah/main/logos/buildah-logo_large.png) # Contributing to Buildah We'd love to have you join the community! Below summarizes the processes that we follow. ## Topics * [Reporting Issues](#reporting-issues) * [Working On Issues](#working-on-issues) * [Submitting Pull Requests](#submitting-pull-requests) * [Sign your PRs](#sign-your-prs) * [Merge bot interaction](#merge-bot-interaction) * [Communications](#communications) * [Becoming a Maintainer](#becoming-a-maintainer) ## Reporting Issues Before reporting an issue, check our backlog of [open issues](https://github.com/containers/buildah/issues) to see if someone else has already reported it. If so, feel free to add your scenario, or additional information, to the discussion. Or simply "subscribe" to it to be notified when it is updated. If you find a new issue with the project we'd love to hear about it! The most important aspect of a bug report is that it includes enough information for us to reproduce it. So, please include as much detail as possible and try to remove the extra stuff that doesn't really relate to the issue itself. The easier it is for us to reproduce it, the faster it'll be fixed! Please don't include any private/sensitive information in your issue! ## Working On Issues Once you have decided to contribute to Buildah by working on an issue, check our backlog of [open issues](https://github.com/containers/buildah/issues) looking for any that do not have an "In Progress" label attached to it. Often issues will be assigned to someone, to be worked on at a later time. If you have the time to work on the issue now, add yourself as an assignee, and set the "In Progress" label if you’re a member of the “Containers” GitHub organization. If you can not set the label, just add a quick comment in the issue asking that the “In Progress” label be set and a member will do so for you. ## Submitting Pull Requests No Pull Request (PR) is too small! Typos, additional comments in the code, new testcases, bug fixes, new features, more documentation, ... it's all welcome! While bug fixes can first be identified via an "issue", that is not required. It's ok to just open up a PR with the fix, but make sure you include the same information you would have included in an issue - like how to reproduce it. PRs for new features should include some background on what use cases the new code is trying to address. When possible and when it makes sense, try to break-up larger PRs into smaller ones - it's easier to review smaller code changes. But only if those smaller ones make sense as stand-alone PRs. Regardless of the type of PR, all PRs should include: * well documented code changes * additional testcases. Ideally, they should fail w/o your code change applied * documentation changes Squash your commits into logical pieces of work that might want to be reviewed separate from the rest of the PRs. But, squashing down to just one commit is ok too since in the end the entire PR will be reviewed anyway. When in doubt, squash. PRs that fix issues should include a reference like `Closes #XXXX` in the commit message so that github will automatically close the referenced issue when the PR is merged. ### Sign your PRs The sign-off is a line at the end of the explanation for the patch. Your signature certifies that you wrote the patch or otherwise have the right to pass it on as an open-source patch. The rules are simple: if you can certify the below (from [developercertificate.org](http://developercertificate.org/)): ``` Developer Certificate of Origin Version 1.1 Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 660 York Street, Suite 102, San Francisco, CA 94110 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. ``` Then you just add a line to every git commit message: Signed-off-by: Joe Smith Use your real name (sorry, no pseudonyms or anonymous contributions.) If you set your `user.name` and `user.email` git configs, you can sign your commit automatically with `git commit -s`. ## Merge bot interaction Maintainers should never merge anything directly into upstream branches. Instead, interact with the [openshift-ci-robot](https://github.com/openshift-ci-robot/) through PR comments as summarized [here](https://prow.ci.openshift.org/command-help?repo=containers%2Fbuildah). This ensures all upstream branches contain commits in a predictable order, and that every commit has passed automated testing at some point in the past. A [Maintainer portal](https://prow.ci.openshift.org/pr?query=is%3Apr%20state%3Aopen%20repo%3Acontainers%2Fbuildah) is available, showing all PRs awaiting review and approval. ## Communications For general questions or discussions, please use the IRC channel `#podman` on `irc.libera.chat`. If you are unfamiliar with IRC you can start a web client at https://web.libera.chat/#podman. Alternatively, [\[matrix\]](https://matrix.org) can be used to access the same channel via federation at https://matrix.to/#/#podman:chat.fedoraproject.org. ### For discussions around issues/bugs and features: #### GitHub You can also use GitHub [issues](https://github.com/containers/buildah/issues) and [PRs](https://github.com/containers/buildah/pulls) tracking system. #### Buildah Mailing List You can join the Buildah mailing list by sending an email to `buildah-join@lists.buildah.io` with the word `subscribe` in the subject. You can also go to this [page](https://lists.podman.io/admin/lists/buildah.lists.buildah.io/), then scroll down to the bottom of the page and enter your email and optionally name, then click on the "Subscribe" button. ## Becoming a Maintainer To become a maintainer you must first be nominated by an existing maintainer. If a majority (>50%) of maintainers agree then the proposal is adopted and you will be added to the list. Removing a maintainer requires at least 75% of the remaining maintainers approval, or if the person requests to be removed then it is automatic. Normally, a maintainer will only be removed if they are considered to be inactive for a long period of time or are viewed as disruptive to the community. The current list of maintainers can be found in the [MAINTAINERS](./MAINTAINERS.md) file. ================================================ FILE: GOVERNANCE.md ================================================ ## The Buildah Project Community Governance The Buildah project, as part of Podman Container Tools, follows the [Podman Project Governance](https://github.com/containers/podman/blob/main/GOVERNANCE.md) except sections found in this document, which override those found in Podman's Governance. --- # Maintainers File The definitive source of truth for maintainers of this repository is the local [MAINTAINERS.md](./MAINTAINERS.md) file. The [MAINTAINERS.md](https://github.com/containers/podman/blob/main/MAINTAINERS.md) file in the main Podman repository is used for project-spanning roles, including Core Maintainer and Community Manager. Some repositories in the project will also have a local [OWNERS](./OWNERS) file, which the CI system uses to map users to roles. Any changes to the [OWNERS](./OWNERS) file must make a corresponding change to the [MAINTAINERS.md](./MAINTAINERS.md) file to ensure that the file remains up to date. Most changes to [MAINTAINERS.md](./MAINTAINERS.md) will require a change to the repository’s [OWNERS](.OWNERS) file (e.g., adding a Reviewer), but some will not (e.g., promoting a Maintainer to a Core Maintainer, which comes with no additional CI-related privileges). Any Core Maintainers listed in Podman’s [MAINTAINERS.md](https://github.com/containers/podman/blob/main/MAINTAINERS.md) file should also be added to the list of “approvers” in the local [OWNERS](./OWNERS) file and as a Core Maintainer in the list of “Maintainers” in the local [MAINTAINERS.md](./MAINTAINERS.md) file. ================================================ 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.md ================================================ # Buildah Maintainers [GOVERNANCE.md](GOVERNANCE.md) describes the project's governance and the Project Roles used below. ## Maintainers | Maintainer | GitHub ID | Project Roles | Affiliation | |-------------------|----------------------------------------------------------|----------------------------------|----------------------------------------------| | Brent Baude | [baude](https://github.com/baude) | Core Maintainer | [Red Hat](https://github.com/RedHatOfficial) | | Nalin Dahyabhai | [nalind](https://github.com/nalind) | Core Maintainer | [Red Hat](https://github.com/RedHatOfficial) | | Matthew Heon | [mheon](https://github.com/mheon) | Core Maintainer | [Red Hat](https://github.com/RedHatOfficial) | | Paul Holzinger | [Luap99](https://github.com/Luap99) | Core Maintainer | [Red Hat](https://github.com/RedHatOfficial) | | Giuseppe Scrivano | [giuseppe](https://github.com/giuseppe) | Core Maintainer | [Red Hat](https://github.com/RedHatOfficial) | | Miloslav Trmač | [mtrmac](https://github.com/mtrmac) | Core Maintainer | [Red Hat](https://github.com/RedHatOfficial) | | Mohan Boddu | [mohanboddu](https://github.com/mohanboddu) | Community Manager | [Red Hat](https://github.com/RedHatOfficial) | | Neil Smith | [actionmancan](https://github.com/actionmancan) | Community Manager | [Red Hat](https://github.com/RedHatOfficial) | | Tom Sweeney | [TomSweeneyRedHat](https://github.com/TomSweeneyRedHat/) | Maintainer and Community Manager | [Red Hat](https://github.com/RedHatOfficial) | | Lokesh Mandvekar | [lsm5](https://github.com/lsm5) | Maintainer | [Red Hat](https://github.com/RedHatOfficial) | | Aditya Rajan | [flouthoc](https://github.com/flouthoc) | Maintainer | [Red Hat](https://github.com/RedHatOfficial) | | Dan Walsh | [rhatdan](https://github.com/rhatdan) | Maintainer | [Red Hat](https://github.com/RedHatOfficial) | | Ashley Cui | [ashley-cui](https://github.com/ashley-cui) | Reviewer | [Red Hat](https://github.com/RedHatOfficial) | | Jan Rodák | [Honny1](https://github.com/Honny1) | Reviewer | [Red Hat](https://github.com/RedHatOfficial) | | Valentin Rothberg | [vrothberg](https://github.com/vrothberg) | Reviewer | [Red Hat](https://github.com/RedHatOfficial) | ## Alumni None at present ## Credits The structure of this document was based off of the equivalent one in the [CRI-O Project](https://github.com/cri-o/cri-o/blob/main/MAINTAINERS.md). ## Note If there is a discrepancy between the [MAINTAINERS.md](https://github.com/containers/podman/blob/main/MAINTAINERS.md) file in the main Podman repository and this file regarding Core Maintainers or Community Managers, the file in the Podman Repository is considered the source of truth. ================================================ FILE: Makefile ================================================ export GOPROXY=https://proxy.golang.org APPARMORTAG := $(shell hack/apparmor_tag.sh) STORAGETAGS := $(shell ./btrfs_installed_tag.sh) $(shell ./hack/libsubid_tag.sh) SECURITYTAGS ?= seccomp $(APPARMORTAG) TAGS ?= $(SECURITYTAGS) $(STORAGETAGS) $(shell ./hack/systemd_tag.sh) $(shell ./hack/sqlite_tag.sh) ifeq ($(shell uname -s),FreeBSD) endif BUILDTAGS += $(TAGS) $(EXTRA_BUILD_TAGS) PREFIX := /usr/local BINDIR := $(PREFIX)/bin BASHINSTALLDIR = $(PREFIX)/share/bash-completion/completions BUILDFLAGS := -tags "$(BUILDTAGS)" BUILDAH := buildah SELINUXOPT ?= $(shell test -x /usr/sbin/selinuxenabled && selinuxenabled && echo -Z) SELINUXTYPE=container_runtime_exec_t AS ?= as STRIP ?= strip GO := go GO_LDFLAGS := $(shell if $(GO) version|grep -q gccgo; then echo "-gccgoflags"; else echo "-ldflags"; fi) GO_GCFLAGS := $(shell if $(GO) version|grep -q gccgo; then echo "-gccgoflags"; else echo "-gcflags"; fi) NPROCS := $(shell nproc) export GO_BUILD=$(GO) build export GO_TEST=$(GO) test -parallel=$(NPROCS) RACEFLAGS ?= $(shell $(GO_TEST) -race ./pkg/dummy > /dev/null 2>&1 && echo -race) COMMIT_NO ?= $(shell git rev-parse HEAD 2> /dev/null || true) GIT_COMMIT ?= $(if $(shell git status --porcelain --untracked-files=no),${COMMIT_NO}-dirty,${COMMIT_NO}) SOURCE_DATE_EPOCH ?= $(if $(shell date +%s),$(shell date +%s),$(error "date failed")) # we get GNU make 3.x in MacOS build envs, which wants # to be escaped in # strings, while the 4.x we have on Linux doesn't. this is the documented # workaround COMMENT := \# SEQUOIA_SONAME_DIR = EXTRA_LDFLAGS ?= BUILDAH_LDFLAGS := $(GO_LDFLAGS) '-X main.GitCommit=$(GIT_COMMIT) -X main.buildInfo=$(SOURCE_DATE_EPOCH) -X go.podman.io/image/v5/signature/internal/sequoia.sequoiaLibraryDir="$(SEQUOIA_SONAME_DIR)" $(EXTRA_LDFLAGS)' # This isn't what we actually build; it's a superset, used for target # dependencies. Basically: all *.go and *.c files, except *_test.go, # and except anything in a dot subdirectory. If any of these files is # newer than our target (bin/buildah), a rebuild is triggered. SOURCES=$(shell find . -path './.*' -prune -o \( \( -name '*.go' -o -name '*.c' \) -a ! -name '*_test.go' \) -print) LINTFLAGS ?= ifeq ($(BUILDDEBUG), 1) override GOGCFLAGS += -N -l endif # Managed by renovate. export GOLANGCI_LINT_VERSION := 2.11.1 # make all BUILDDEBUG=1 # Note: Uses the -N -l go compiler options to disable compiler optimizations # and inlining. Using these build options allows you to subsequently # use source debugging tools like delve. all: bin/buildah bin/imgtype bin/copy bin/inet bin/tutorial bin/dumpspec bin/passwd bin/crash bin/wait bin/grpcnoop docs bin/buildah: $(SOURCES) internal/mkcw/embed/entrypoint_amd64.gz $(GO_BUILD) $(BUILDAH_LDFLAGS) $(GO_GCFLAGS) "$(GOGCFLAGS)" -o $@ $(BUILDFLAGS) ./cmd/buildah test -z "${SELINUXOPT}" || chcon --verbose -t $(SELINUXTYPE) $@ internal/mkcw/embed/entrypoint_amd64.gz: internal/mkcw/embed/entrypoint_amd64 gzip -k9nf $^ internal/mkcw/embed/entrypoint_arm64.gz: internal/mkcw/embed/entrypoint_arm64 gzip -k9nf $^ internal/mkcw/embed/entrypoint_ppc64le.gz: internal/mkcw/embed/entrypoint_ppc64le gzip -k9nf $^ internal/mkcw/embed/entrypoint_s390x.gz: internal/mkcw/embed/entrypoint_s390x gzip -k9nf $^ ifneq ($(shell $(AS) --version | grep -E 'x86_64-([^-]+-)?linux'),) internal/mkcw/embed/entrypoint_amd64: internal/mkcw/embed/asm/entrypoint_amd64.s $(AS) -o $(patsubst %.s,%.o,$^) $^ $(LD) -o $@ $(patsubst %.s,%.o,$^) $(STRIP) $@ else internal/mkcw/embed/entrypoint_amd64: internal/mkcw/embed/entrypoint_amd64.s internal/mkcw/embed/entrypoint.go GOOS=linux GOARCH=amd64 $(GO) build -ldflags "-E _start -s" -o $@ ./internal/mkcw/embed endif internal/mkcw/embed/entrypoint_arm64: internal/mkcw/embed/entrypoint_arm64.s internal/mkcw/embed/entrypoint.go GOOS=linux GOARCH=arm64 $(GO) build -ldflags "-E _start -s" -o $@ ./internal/mkcw/embed internal/mkcw/embed/entrypoint_ppc64le: internal/mkcw/embed/entrypoint_ppc64le.s internal/mkcw/embed/entrypoint.go GOOS=linux GOARCH=ppc64le $(GO) build -ldflags "-E _start -s" -o $@ ./internal/mkcw/embed internal/mkcw/embed/entrypoint_s390x: internal/mkcw/embed/entrypoint_s390x.s internal/mkcw/embed/entrypoint.go GOOS=linux GOARCH=s390x $(GO) build -ldflags "-E _start -s" -o $@ ./internal/mkcw/embed .PHONY: buildah buildah: bin/buildah ALL_CROSS_TARGETS := $(addprefix bin/buildah.,$(subst /,.,$(shell $(GO) tool dist list))) LINUX_CROSS_TARGETS := $(filter-out %.loong64,$(filter bin/buildah.linux.%,$(ALL_CROSS_TARGETS))) DARWIN_CROSS_TARGETS := $(filter bin/buildah.darwin.%,$(ALL_CROSS_TARGETS)) WINDOWS_CROSS_TARGETS := $(addsuffix .exe,$(filter bin/buildah.windows.%,$(ALL_CROSS_TARGETS))) FREEBSD_CROSS_TARGETS := $(filter bin/buildah.freebsd.%,$(ALL_CROSS_TARGETS)) .PHONY: cross cross: $(LINUX_CROSS_TARGETS) $(DARWIN_CROSS_TARGETS) $(WINDOWS_CROSS_TARGETS) $(FREEBSD_CROSS_TARGETS) bin/buildah.%: $(SOURCES) internal/mkcw/embed/entrypoint_amd64.gz mkdir -p ./bin GOOS=$(word 2,$(subst ., ,$@)) GOARCH=$(word 3,$(subst ., ,$@)) $(GO_BUILD) $(BUILDAH_LDFLAGS) -o $@ -tags "containers_image_openpgp" ./cmd/buildah bin/crash: $(SOURCES) $(GO_BUILD) $(BUILDAH_LDFLAGS) -o $@ $(BUILDFLAGS) ./tests/crash bin/wait: $(SOURCES) $(GO_BUILD) $(BUILDAH_LDFLAGS) -o $@ $(BUILDFLAGS) ./tests/wait bin/dumpspec: $(SOURCES) $(GO_BUILD) $(BUILDAH_LDFLAGS) -o $@ $(BUILDFLAGS) ./tests/dumpspec bin/imgtype: $(SOURCES) $(GO_BUILD) $(BUILDAH_LDFLAGS) -o $@ $(BUILDFLAGS) ./tests/imgtype/imgtype.go bin/copy: $(SOURCES) $(GO_BUILD) $(BUILDAH_LDFLAGS) -o $@ $(BUILDFLAGS) ./tests/copy/copy.go bin/tutorial: $(SOURCES) $(GO_BUILD) $(BUILDAH_LDFLAGS) -o $@ $(BUILDFLAGS) ./tests/tutorial/tutorial.go bin/inet: tests/inet/inet.go $(GO_BUILD) $(BUILDAH_LDFLAGS) -o $@ $(BUILDFLAGS) ./tests/inet/inet.go bin/passwd: tests/passwd/passwd.go $(GO_BUILD) $(BUILDAH_LDFLAGS) -o $@ $(BUILDFLAGS) ./tests/passwd/passwd.go bin/grpcnoop: tests/rpc/noop/noop.go $(GO_BUILD) $(BUILDAH_LDFLAGS) -o $@ $(BUILDFLAGS) ./tests/rpc/noop/noop.go .PHONY: clean clean: $(RM) -r bin tests/testreport/testreport tests/conformance/testdata/mount-targets/true internal/mkcw/embed/entrypoint_amd64 internal/mkcw/embed/entrypoint_arm64 internal/mkcw/embed/entrypoint_ppc64le internal/mkcw/embed/entrypoint_s390x internal/mkcw/embed/*.gz internal/mkcw/embed/asm/*.o $(MAKE) -C docs clean .PHONY: docs docs: install.tools ## build the docs on the host $(MAKE) -C docs codespell: codespell -w .PHONY: validate validate: install.tools ./tests/validate/whitespace.sh ./hack/xref-helpmsgs-manpages ./tests/validate/pr-should-include-tests .PHONY: install.tools install.tools: $(MAKE) -C tests/tools .PHONY: install install: install -d -m 755 $(DESTDIR)/$(BINDIR) install -m 755 bin/buildah $(DESTDIR)/$(BINDIR)/buildah $(MAKE) -C docs install .PHONY: uninstall uninstall: rm -f $(DESTDIR)/$(BINDIR)/buildah rm -f $(PREFIX)/share/man/man1/buildah*.1 rm -f $(DESTDIR)/$(BASHINSTALLDIR)/buildah .PHONY: install.completions install.completions: install -m 755 -d $(DESTDIR)/$(BASHINSTALLDIR) install -m 644 contrib/completions/bash/buildah $(DESTDIR)/$(BASHINSTALLDIR)/buildah .PHONY: test-conformance test-conformance: tests/conformance/testdata/mount-targets/true $(GO_TEST) -v -tags "$(STORAGETAGS) $(SECURITYTAGS)" -cover -timeout 60m ./tests/conformance .PHONY: test-integration test-integration: install.tools cd tests; ./test_runner.sh tests/testreport/testreport: tests/testreport/testreport.go $(GO_BUILD) $(GO_LDFLAGS) "-linkmode external -extldflags -static" -tags "$(STORAGETAGS) $(SECURITYTAGS)" -o tests/testreport/testreport ./tests/testreport/testreport.go tests/conformance/testdata/mount-targets/true: tests/conformance/testdata/mount-targets/true.go $(GO_BUILD) $(GO_LDFLAGS) "-linkmode external -extldflags -static" -o tests/conformance/testdata/mount-targets/true tests/conformance/testdata/mount-targets/true.go .PHONY: test-unit test-unit: tests/testreport/testreport $(GO_TEST) -v -tags "$(STORAGETAGS) $(SECURITYTAGS)" -cover $(RACEFLAGS) $(shell $(GO) list ./... | grep -v vendor | grep -v tests | grep -v cmd | grep -v chroot | grep -v copier) -timeout 45m $(GO_TEST) -v -tags "$(STORAGETAGS) $(SECURITYTAGS)" $(RACEFLAGS) ./chroot ./copier -timeout 60m tmp=$(shell mktemp -d) ; \ mkdir -p $$tmp/root $$tmp/runroot; \ $(GO_TEST) -v -tags "$(STORAGETAGS) $(SECURITYTAGS)" -cover $(RACEFLAGS) ./cmd/buildah -args --root $$tmp/root --runroot $$tmp/runroot --storage-driver vfs --signature-policy $(shell pwd)/tests/policy.json --registries-conf $(shell pwd)/tests/registries.conf vendor-in-container: goversion=$(shell sed -e '/^go /!d' -e '/^go /s,.* ,,g' go.mod) ; \ if test -d `$(GO) env GOCACHE` && test -w `$(GO) env GOCACHE` ; then \ podman run --privileged --rm --env HOME=/root -v `$(GO) env GOCACHE`:/root/.cache/go-build --env GOCACHE=/root/.cache/go-build -v `pwd`:/src -w /src docker.io/library/golang:$$goversion make vendor ; \ else \ podman run --privileged --rm --env HOME=/root -v `pwd`:/src -w /src docker.io/library/golang:$$goversion make vendor ; \ fi .PHONY: vendor vendor: $(GO) mod tidy $(GO) mod vendor $(GO) mod verify if test -n "$(strip $(shell $(GO) env GOTOOLCHAIN))"; then go mod edit -toolchain none ; fi .PHONY: lint lint: install.tools ./tests/tools/build/golangci-lint run $(LINTFLAGS) ./tests/tools/build/golangci-lint run --tests=false $(LINTFLAGS) .PHONY: fmt fmt: install.tools ./tests/tools/build/golangci-lint fmt $(LINTFLAGS) # CAUTION: This is not a replacement for RPMs provided by your distro. # Only intended to build and test the latest unreleased changes. .PHONY: rpm rpm: ## Build rpm packages $(MAKE) -C rpm # Remember that rpms install exec to /usr/bin/buildah while a `make install` # installs them to /usr/local/bin/buildah which is likely before. Always use # a full path to test installed buildah or you risk to call another executable. .PHONY: rpm-install rpm-install: package ## Install rpm packages $(call err_if_empty,PKG_MANAGER) -y install rpm/RPMS/*/*.rpm /usr/bin/buildah version ================================================ FILE: OWNERS ================================================ approvers: - baude - flouthoc - giuseppe - lsm5 - Luap99 - mheon - mtrmac - nalind - rhatdan - TomSweeneyRedHat reviewers: - ashley-cui - baude - flouthoc - giuseppe - Honny1 - lsm5 - Luap99 - mheon - mtrmac - nalind - rhatdan - TomSweeneyRedHat - vrothberg ================================================ FILE: README.md ================================================ ![buildah logo (light)](logos/buildah-logo_large.png#gh-light-mode-only) ![buildah logo (dark)](logos/buildah-logo_reverse_large.png#gh-dark-mode-only) # [Buildah](https://www.youtube.com/embed/YVk5NgSiUw8) - a tool that facilitates building [Open Container Initiative (OCI)](https://www.opencontainers.org/) container images [![Go Report Card](https://goreportcard.com/badge/github.com/containers/buildah)](https://goreportcard.com/report/github.com/containers/buildah) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/10579/badge)](https://www.bestpractices.dev/projects/10579) The Buildah package provides a command line tool that can be used to * create a working container, either from scratch or using an image as a starting point * create an image, either from a working container or via the instructions in a Dockerfile * images can be built in either the OCI image format or the traditional upstream docker image format * mount a working container's root filesystem for manipulation * unmount a working container's root filesystem * use the updated contents of a container's root filesystem as a filesystem layer to create a new image * delete a working container or an image * rename a local container ## Buildah Information for Developers For blogs, release announcements and more, please checkout the [buildah.io](https://buildah.io) website! **[Buildah Container Images](https://github.com/containers/image_build/blob/main/buildah/README.md)** **[Buildah Demos](demos)** **[Changelog](CHANGELOG.md)** **[Contributing](CONTRIBUTING.md)** **[Development Plan](developmentplan.md)** **[Installation notes](install.md)** **[Troubleshooting Guide](troubleshooting.md)** **[Tutorials](docs/tutorials)** ## Buildah and Podman relationship Buildah and Podman are two complementary open-source projects that are available on most Linux platforms and both projects reside at [GitHub.com](https://github.com) with Buildah [here](https://github.com/containers/buildah) and Podman [here](https://github.com/containers/podman). Both, Buildah and Podman are command line tools that work on Open Container Initiative (OCI) images and containers. The two projects differentiate in their specialization. Buildah specializes in building OCI images. Buildah's commands replicate all of the commands that are found in a Dockerfile. This allows building images with and without Dockerfiles while not requiring any root privileges. Buildah’s ultimate goal is to provide a lower-level coreutils interface to build images. The flexibility of building images without Dockerfiles allows for the integration of other scripting languages into the build process. Buildah follows a simple fork-exec model and does not run as a daemon but it is based on a comprehensive API in golang, which can be vendored into other tools. Podman specializes in all of the commands and functions that help you to maintain and modify OCI images, such as pulling and tagging. It also allows you to create, run, and maintain those containers created from those images. For building container images via Dockerfiles, Podman uses Buildah's golang API and can be installed independently from Buildah. A major difference between Podman and Buildah is their concept of a container. Podman allows users to create "traditional containers" where the intent of these containers is to be long lived. While Buildah containers are really just created to allow content to be added back to the container image. An easy way to think of it is the `buildah run` command emulates the RUN command in a Dockerfile while the `podman run` command emulates the `docker run` command in functionality. Because of this and their underlying storage differences, you can not see Podman containers from within Buildah or vice versa. In short, Buildah is an efficient way to create OCI images while Podman allows you to manage and maintain those images and containers in a production environment using familiar container cli commands. For more details, see the [Container Tools Guide](https://github.com/containers/buildah/tree/main/docs/containertools). ## Example From [`./examples/lighttpd.sh`](examples/lighttpd.sh): ```bash $ cat > lighttpd.sh <<"EOF" #!/usr/bin/env bash set -x ctr1=$(buildah from "${1:-fedora}") ## Get all updates and install our minimal httpd server buildah run "$ctr1" -- dnf update -y buildah run "$ctr1" -- dnf install -y lighttpd ## Include some buildtime annotations buildah config --annotation "com.example.build.host=$(uname -n)" "$ctr1" ## Run our server and expose the port buildah config --cmd "/usr/sbin/lighttpd -D -f /etc/lighttpd/lighttpd.conf" "$ctr1" buildah config --port 80 "$ctr1" ## Commit this container to an image name buildah commit "$ctr1" "${2:-$USER/lighttpd}" EOF $ chmod +x lighttpd.sh $ ./lighttpd.sh ``` ## Commands | Command | Description | | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | [buildah-add(1)](/docs/buildah-add.1.md) | Add the contents of a file, URL, or a directory to the container. | | [buildah-build(1)](/docs/buildah-build.1.md) | Build an image using instructions from Containerfiles or Dockerfiles. | | [buildah-commit(1)](/docs/buildah-commit.1.md) | Create an image from a working container. | | [buildah-config(1)](/docs/buildah-config.1.md) | Update image configuration settings. | | [buildah-containers(1)](/docs/buildah-containers.1.md) | List the working containers and their base images. | | [buildah-copy(1)](/docs/buildah-copy.1.md) | Copies the contents of a file, URL, or directory into a container's working directory. | | [buildah-from(1)](/docs/buildah-from.1.md) | Creates a new working container, either from scratch or using a specified image as a starting point. | | [buildah-images(1)](/docs/buildah-images.1.md) | List images in local storage. | | [buildah-info(1)](/docs/buildah-info.1.md) | Display Buildah system information. | | [buildah-inspect(1)](/docs/buildah-inspect.1.md) | Inspects the configuration of a container or image. | | [buildah-mount(1)](/docs/buildah-mount.1.md) | Mount the working container's root filesystem. | | [buildah-pull(1)](/docs/buildah-pull.1.md) | Pull an image from the specified location. | | [buildah-push(1)](/docs/buildah-push.1.md) | Push an image from local storage to elsewhere. | | [buildah-rename(1)](/docs/buildah-rename.1.md) | Rename a local container. | | [buildah-rm(1)](/docs/buildah-rm.1.md) | Removes one or more working containers. | | [buildah-rmi(1)](/docs/buildah-rmi.1.md) | Removes one or more images. | | [buildah-run(1)](/docs/buildah-run.1.md) | Run a command inside of the container. | | [buildah-tag(1)](/docs/buildah-tag.1.md) | Add an additional name to a local image. | | [buildah-umount(1)](/docs/buildah-umount.1.md) | Unmount a working container's root file system. | | [buildah-unshare(1)](/docs/buildah-unshare.1.md) | Launch a command in a user namespace with modified ID mappings. | | [buildah-version(1)](/docs/buildah-version.1.md) | Display the Buildah Version Information | **Future goals include:** * more CI tests * additional CLI commands (?) ================================================ FILE: ROADMAP.md ================================================ ![buildah logo (light)](logos/buildah-logo_large.png#gh-light-mode-only) ![buildah logo (dark)](logos/buildah-logo_reverse_large.png#gh-dark-mode-only) # Buildah Roadmap The Buildah development team reviews feature requests from its various stakeholders for consideration quarterly along with the Podman Development team. These features are then prioritized and the top features are then assigned to one or more engineers. ## Future feature considerations The following features are of general importantance to Buildah. While these features have no timeline associated with them yet, they will likely be on future quarterly milestones. * Ongoing work around partial pull support (zstd:chunked) * Improved support for the BuildKit API. * Performance and stability improvements. * Reductions to the size of the Buildah binary. ## Milestones and commitments by quarter This section is a historical account of what features were prioritized by quarter. Results of the prioritization will be added at start of each quarter (Jan, Apr, July, Oct). ### 2025 Q2 #### #### Releases #### - [ ] Buildah 1.40 #### Features #### - [ ] Reduce binary size of Buildah - [ ] Additional Containerfile command options #### CNCF #### - [ ] Add and adhere to Governance model - [ ] Update Maintainers file ### 2025 Q1 #### #### Releases #### - [x] Buildah 1.39 #### Features #### - [x] Artifact add --options #### CNCF #### - [x] Create Governance documentation - [x] Create Maintainers file ================================================ FILE: SECURITY.md ================================================ ## Security and Disclosure Information Policy for the Buildah Project The Buildah Project follows the [Security and Disclosure Information Policy](https://github.com/containers/common/blob/main/SECURITY.md) for the Containers Projects. ================================================ FILE: add.go ================================================ package buildah import ( "archive/tar" "context" "crypto/tls" "errors" "fmt" "io" "net/http" "net/url" "os" "path" "path/filepath" "slices" "strconv" "strings" "sync" "syscall" "time" "github.com/containers/buildah/copier" "github.com/containers/buildah/define" "github.com/containers/buildah/internal/tmpdir" "github.com/containers/buildah/pkg/chrootuser" "github.com/docker/go-connections/tlsconfig" "github.com/hashicorp/go-multierror" "github.com/moby/sys/userns" digest "github.com/opencontainers/go-digest" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" "go.podman.io/common/pkg/retry" "go.podman.io/image/v5/pkg/tlsclientconfig" "go.podman.io/image/v5/types" "go.podman.io/storage/pkg/fileutils" "go.podman.io/storage/pkg/idtools" "go.podman.io/storage/pkg/regexp" ) // AddAndCopyOptions holds options for add and copy commands. type AddAndCopyOptions struct { // Chmod sets the access permissions of the destination content. Chmod string // Chown is a spec for the user who should be given ownership over the // newly-added content, potentially overriding permissions which would // otherwise be set to 0:0. Chown string // Checksum is a standard container digest string (e.g. :) // and is the expected hash of the content being copied. Checksum string // PreserveOwnership, if Chown is not set, tells us to avoid setting // ownership of copied items to 0:0, instead using whatever ownership // information is already set. Not meaningful for remote sources or // local archives that we extract. PreserveOwnership bool // All of the data being copied will pass through Hasher, if set. // If the sources are URLs or files, their contents will be passed to // Hasher. // If the sources include directory trees, Hasher will be passed // tar-format archives of the directory trees. Hasher io.Writer // Excludes is the contents of the .containerignore file. Excludes []string // IgnoreFile is the path to the .containerignore file. IgnoreFile string // ContextDir is the base directory for content being copied and // Excludes patterns. ContextDir string // ID mapping options to use when contents to be copied are part of // another container, and need ownerships to be mapped from the host to // that container's values before copying them into the container. IDMappingOptions *define.IDMappingOptions // DryRun indicates that the content should be digested, but not actually // copied into the container. DryRun bool // Clear the setuid bit on items being copied. Has no effect on // archives being extracted, where the bit is always preserved. StripSetuidBit bool // Clear the setgid bit on items being copied. Has no effect on // archives being extracted, where the bit is always preserved. StripSetgidBit bool // Clear the sticky bit on items being copied. Has no effect on // archives being extracted, where the bit is always preserved. StripStickyBit bool // If not "", a directory containing a CA certificate (ending with // ".crt"), a client certificate (ending with ".cert") and a client // certificate key (ending with ".key") used when downloading sources // from locations protected with TLS. CertPath string // Allow downloading sources from HTTPS where TLS verification fails. InsecureSkipTLSVerify types.OptionalBool // MaxRetries is the maximum number of attempts we'll make to retrieve // contents from a remote location. MaxRetries int // RetryDelay is how long to wait before retrying attempts to retrieve // remote contents. RetryDelay time.Duration // Parents specifies that we should preserve either all of the parent // directories of source locations, or the ones which follow "/./" in // the source paths for source locations which include such a // component. Parents bool // Timestamp is a timestamp to override on all content as it is being read. Timestamp *time.Time // Link, when set to true, creates an independent layer containing the copied content // that sits on top of existing layers. This layer can be cached and reused // separately, and is not affected by filesystem changes from previous instructions. Link bool // BuildMetadata is consulted only when Link is true. Contains metadata used by // imagebuildah for cache evaluation of linked layers (inheritLabels, unsetAnnotations, // inheritAnnotations, newAnnotations). This field is internally managed and should // not be set by external API users. BuildMetadata string } // gitURLFragmentSuffix matches fragments to use as Git reference and build // context from the Git repository e.g. // // github.com/containers/buildah.git // github.com/containers/buildah.git#main // github.com/containers/buildah.git#v1.35.0 var gitURLFragmentSuffix = regexp.Delayed(`\.git(?:#.+)?$`) // sourceIsGit returns true if "source" is a git location. func sourceIsGit(source string) bool { return isURL(source) && gitURLFragmentSuffix.MatchString(source) } func isURL(url string) bool { return strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") } // sourceIsRemote returns true if "source" is a remote location // and *not* a git repo. Certain github urls such as raw.github.* are allowed. func sourceIsRemote(source string) bool { return isURL(source) && !gitURLFragmentSuffix.MatchString(source) } // getURL writes a tar archive containing the named content func getURL(src string, chown *idtools.IDPair, mountpoint, renameTarget string, writer io.Writer, chmod *os.FileMode, srcDigest digest.Digest, certPath string, insecureSkipTLSVerify types.OptionalBool, timestamp *time.Time) error { url, err := url.Parse(src) if err != nil { return err } tlsClientConfig := &tls.Config{ // As of 2025-08, tlsconfig.ClientDefault() differs from Go 1.23 defaults only in CipherSuites; // so, limit us to only using that value. If go-connections/tlsconfig changes its policy, we // will want to consider that and make a decision whether to follow suit. // There is some chance that eventually the Go default will be to require TLS 1.3, and that point // we might want to drop the dependency on go-connections entirely. CipherSuites: tlsconfig.ClientDefault().CipherSuites, } if err := tlsclientconfig.SetupCertificates(certPath, tlsClientConfig); err != nil { return err } tlsClientConfig.InsecureSkipVerify = insecureSkipTLSVerify == types.OptionalBoolTrue tr := &http.Transport{ TLSClientConfig: tlsClientConfig, Proxy: http.ProxyFromEnvironment, } httpClient := &http.Client{Transport: tr} response, err := httpClient.Get(src) if err != nil { return err } defer response.Body.Close() if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusBadRequest { return fmt.Errorf("invalid response status %d", response.StatusCode) } // Figure out what to name the new content. name := renameTarget if name == "" { name = path.Base(url.Path) } // If there's a date on the content, use it. If not, use the Unix epoch // or a specified value for compatibility. date := time.Unix(0, 0).UTC() if timestamp != nil { date = timestamp.UTC() } else { lastModified := response.Header.Get("Last-Modified") if lastModified != "" { d, err := time.Parse(time.RFC1123, lastModified) if err != nil { return fmt.Errorf("parsing last-modified time %q: %w", lastModified, err) } date = d.UTC() } } // Figure out the size of the content. size := response.ContentLength var responseBody io.Reader = response.Body if size < 0 { // Create a temporary file and copy the content to it, so that // we can figure out how much content there is. f, err := os.CreateTemp(mountpoint, "download") if err != nil { return fmt.Errorf("creating temporary file to hold %q: %w", src, err) } defer os.Remove(f.Name()) defer f.Close() size, err = io.Copy(f, response.Body) if err != nil { return fmt.Errorf("writing %q to temporary file %q: %w", src, f.Name(), err) } _, err = f.Seek(0, io.SeekStart) if err != nil { return fmt.Errorf("setting up to read %q from temporary file %q: %w", src, f.Name(), err) } responseBody = f } var digester digest.Digester if srcDigest != "" { digester = srcDigest.Algorithm().Digester() responseBody = io.TeeReader(responseBody, digester.Hash()) } // Write the output archive. Set permissions for compatibility. tw := tar.NewWriter(writer) defer tw.Close() uid := 0 gid := 0 if chown != nil { uid = chown.UID gid = chown.GID } var mode int64 = 0o600 if chmod != nil { mode = int64(*chmod) } hdr := tar.Header{ Typeflag: tar.TypeReg, Name: name, Size: size, Uid: uid, Gid: gid, Mode: mode, ModTime: date, } err = tw.WriteHeader(&hdr) if err != nil { return fmt.Errorf("writing header: %w", err) } if _, err := io.Copy(tw, responseBody); err != nil { return fmt.Errorf("writing content from %q to tar stream: %w", src, err) } if digester != nil { if responseDigest := digester.Digest(); responseDigest != srcDigest { return fmt.Errorf("unexpected response digest for %q: %s, want %s", src, responseDigest, srcDigest) } } return nil } // includeDirectoryAnyway returns true if "path" is a prefix for an exception // known to "pm". If "path" is a directory that "pm" claims matches its list // of patterns, but "pm"'s list of exclusions contains a pattern for which // "path" is a prefix, then IncludeDirectoryAnyway() will return true. // This is not always correct, because it relies on the directory part of any // exception paths to be specified without wildcards. func includeDirectoryAnyway(path string, pm *fileutils.PatternMatcher) bool { if !pm.Exclusions() { return false } prefix := strings.TrimPrefix(path, string(os.PathSeparator)) + string(os.PathSeparator) for _, pattern := range pm.Patterns() { if !pattern.Exclusion() { continue } spec := strings.TrimPrefix(pattern.String(), string(os.PathSeparator)) if strings.HasPrefix(spec, prefix) { return true } } return false } // globbedToGlobbable takes a pathname which might include the '[', *, or ? // characters, and converts it into a glob pattern that matches itself by // marking the '[' characters as _not_ the beginning of match ranges and // escaping the * and ? characters. func globbedToGlobbable(glob string) string { result := glob result = strings.ReplaceAll(result, "[", "[[]") result = strings.ReplaceAll(result, "?", "\\?") result = strings.ReplaceAll(result, "*", "\\*") return result } // getParentsPrefixToRemoveAndParentsToSkip gets from the pattern the prefix before the "pivot point", // the location in the source path marked by the path component named "." // (i.e. where "/./" occurs in the path). And list of parents to skip. // In case "/./" is not present is returned "/". func getParentsPrefixToRemoveAndParentsToSkip(pattern string, contextDir string) (string, []string) { prefix, _, found := strings.Cut(strings.TrimPrefix(pattern, contextDir), "/./") if !found { return string(filepath.Separator), []string{} } prefix = strings.TrimPrefix(filepath.Clean(string(filepath.Separator)+prefix), string(filepath.Separator)) out := []string{} parentPath := prefix for parentPath != "/" && parentPath != "." { out = append(out, parentPath) parentPath = filepath.Dir(parentPath) } return prefix, out } // Add copies the contents of the specified sources into the container's root // filesystem, optionally extracting contents of local files that look like // non-empty archives. func (b *Builder) Add(destination string, extract bool, options AddAndCopyOptions, sources ...string) error { mountPoint, err := b.Mount(b.MountLabel) if err != nil { return err } defer func() { if err2 := b.Unmount(); err2 != nil { logrus.Errorf("error unmounting container: %v", err2) } }() contextDir := options.ContextDir currentDir := options.ContextDir if options.ContextDir == "" { contextDir = string(os.PathSeparator) currentDir, err = os.Getwd() if err != nil { return fmt.Errorf("determining current working directory: %w", err) } } else { if !filepath.IsAbs(options.ContextDir) { contextDir, err = filepath.Abs(options.ContextDir) if err != nil { return fmt.Errorf("converting context directory path %q to an absolute path: %w", options.ContextDir, err) } } } // Figure out what sorts of sources we have. var localSources, remoteSources, gitSources []string for i, src := range sources { if src == "" { return errors.New("empty source location") } if sourceIsRemote(src) { remoteSources = append(remoteSources, src) continue } if sourceIsGit(src) { gitSources = append(gitSources, src) continue } if !filepath.IsAbs(src) && options.ContextDir == "" { sources[i] = filepath.Join(currentDir, src) } localSources = append(localSources, sources[i]) } // Treat git sources as a subset of remote sources // differentiating only in how we fetch the two later on. if len(gitSources) > 0 { remoteSources = append(remoteSources, gitSources...) } // Check how many items our local source specs matched. Each spec // should have matched at least one item, otherwise we consider it an // error. var localSourceStats []*copier.StatsForGlob if len(localSources) > 0 { statOptions := copier.StatOptions{ CheckForArchives: extract, } localSourceStats, err = copier.Stat(contextDir, contextDir, statOptions, localSources) if err != nil { return fmt.Errorf("checking on sources under %q: %w", contextDir, err) } } numLocalSourceItems := 0 for _, localSourceStat := range localSourceStats { if localSourceStat.Error != "" { errorText := localSourceStat.Error rel, err := filepath.Rel(contextDir, localSourceStat.Glob) if err != nil { errorText = fmt.Sprintf("%v; %s", err, errorText) } if strings.HasPrefix(rel, ".."+string(os.PathSeparator)) { errorText = fmt.Sprintf("possible escaping context directory error: %s", errorText) } return fmt.Errorf("checking on sources under %q: %v", contextDir, errorText) } if len(localSourceStat.Globbed) == 0 { return fmt.Errorf("checking source under %q: no glob matches: %w", contextDir, syscall.ENOENT) } numLocalSourceItems += len(localSourceStat.Globbed) } if numLocalSourceItems+len(remoteSources)+len(gitSources) == 0 { return fmt.Errorf("no sources %v found: %w", sources, syscall.ENOENT) } // Find out which user (and group) the destination should belong to. var chownDirs, chownFiles *idtools.IDPair var userUID, userGID uint32 if options.Chown != "" { userUID, userGID, err = b.userForCopy(mountPoint, options.Chown) if err != nil { return fmt.Errorf("looking up UID/GID for %q: %w", options.Chown, err) } } var chmodDirsFiles *os.FileMode if options.Chmod != "" { p, err := strconv.ParseUint(options.Chmod, 8, 32) if err != nil { return fmt.Errorf("parsing chmod %q: %w", options.Chmod, err) } perm := os.FileMode(p) chmodDirsFiles = &perm } chownDirs = &idtools.IDPair{UID: int(userUID), GID: int(userGID)} chownFiles = &idtools.IDPair{UID: int(userUID), GID: int(userGID)} if options.Chown == "" && options.PreserveOwnership { chownDirs = nil chownFiles = nil } // If we have a single source archive to extract, or more than one // source item, or the destination has a path separator at the end of // it, and it's not a remote URL, the destination needs to be a // directory. destMustBeDirectory := strings.HasSuffix(destination, string(os.PathSeparator)) || strings.HasSuffix(destination, string(os.PathSeparator)+".") // keep this in sync with github.com/openshift/imagebuilder.hasSlash() destMustBeDirectory = destMustBeDirectory || destination == "" || (len(sources) > 1) if destination == "" || !filepath.IsAbs(destination) { tmpDestination := filepath.Join(string(os.PathSeparator)+b.WorkDir(), destination) if destMustBeDirectory { destination = tmpDestination + string(os.PathSeparator) } else { destination = tmpDestination } } destMustBeDirectory = destMustBeDirectory || (filepath.Clean(destination) == filepath.Clean(b.WorkDir())) destCanBeFile := false if len(sources) == 1 { if len(remoteSources) == 1 { destCanBeFile = sourceIsRemote(sources[0]) } if len(localSources) == 1 { item := localSourceStats[0].Results[localSourceStats[0].Globbed[0]] if item.IsDir || (item.IsArchive && extract) { destMustBeDirectory = true } if item.IsRegular { destCanBeFile = true } } if len(gitSources) > 0 { destMustBeDirectory = true } } // We care if the destination either doesn't exist, or exists and is a // file. If the source can be a single file, for those cases we treat // the destination as a file rather than as a directory tree. renameTarget := "" extractDirectory := filepath.Join(mountPoint, destination) statOptions := copier.StatOptions{ CheckForArchives: extract, } destStats, err := copier.Stat(mountPoint, filepath.Join(mountPoint, b.WorkDir()), statOptions, []string{extractDirectory}) if err != nil { return fmt.Errorf("checking on destination %v: %w", extractDirectory, err) } if (len(destStats) == 0 || len(destStats[0].Globbed) == 0) && !destMustBeDirectory && destCanBeFile { // destination doesn't exist - extract to parent and rename the incoming file to the destination's name renameTarget = filepath.Base(extractDirectory) extractDirectory = filepath.Dir(extractDirectory) } // if the destination is a directory that doesn't yet exist, let's copy it. newDestDirFound := (len(destStats) == 1 || len(destStats[0].Globbed) == 0) && destMustBeDirectory && !destCanBeFile if len(destStats) == 1 && len(destStats[0].Globbed) == 1 && destStats[0].Results[destStats[0].Globbed[0]].IsRegular { if destMustBeDirectory { return fmt.Errorf("destination %v already exists but is not a directory", destination) } // destination exists - it's a file, we need to extract to parent and rename the incoming file to the destination's name renameTarget = filepath.Base(extractDirectory) extractDirectory = filepath.Dir(extractDirectory) } pm, err := fileutils.NewPatternMatcher(options.Excludes) if err != nil { return fmt.Errorf("processing excludes list %v: %w", options.Excludes, err) } // Make sure that, if it's a symlink, we'll chroot to the target of the link; // knowing that target requires that we resolve it within the chroot. evalOptions := copier.EvalOptions{} evaluated, err := copier.Eval(mountPoint, extractDirectory, evalOptions) if err != nil { return fmt.Errorf("checking on destination %v: %w", extractDirectory, err) } extractDirectory = evaluated // Set up ID maps. var srcUIDMap, srcGIDMap []idtools.IDMap if options.IDMappingOptions != nil { srcUIDMap, srcGIDMap = convertRuntimeIDMaps(options.IDMappingOptions.UIDMap, options.IDMappingOptions.GIDMap) } destUIDMap, destGIDMap := convertRuntimeIDMaps(b.IDMappingOptions.UIDMap, b.IDMappingOptions.GIDMap) var putRoot, putDir, stagingDir string var createdDirs []string var latestTimestamp time.Time mkdirOptions := copier.MkdirOptions{ UIDMap: destUIDMap, GIDMap: destGIDMap, ChownNew: chownDirs, } // If --link is specified, we create a staging directory to hold the content // that will then become an independent layer if options.Link { containerDir, err := b.store.ContainerDirectory(b.ContainerID) if err != nil { return fmt.Errorf("getting container directory for %q: %w", b.ContainerID, err) } stagingDir, err = os.MkdirTemp(containerDir, "link-stage-") if err != nil { return fmt.Errorf("creating staging directory for link %q: %w", b.ContainerID, err) } putRoot = stagingDir cleanDest := filepath.Clean(destination) if strings.Contains(cleanDest, "..") { return fmt.Errorf("invalid destination path %q: contains path traversal", destination) } if renameTarget != "" { putDir = filepath.Dir(filepath.Join(stagingDir, cleanDest)) } else { putDir = filepath.Join(stagingDir, cleanDest) } putDirAbs, err := filepath.Abs(putDir) if err != nil { return fmt.Errorf("failed to resolve absolute path: %w", err) } stagingDirAbs, err := filepath.Abs(stagingDir) if err != nil { return fmt.Errorf("failed to resolve staging directory absolute path: %w", err) } if !strings.HasPrefix(putDirAbs, stagingDirAbs+string(os.PathSeparator)) && putDirAbs != stagingDirAbs { return fmt.Errorf("destination path %q escapes staging directory", destination) } if err := copier.Mkdir(putRoot, putDirAbs, mkdirOptions); err != nil { return fmt.Errorf("ensuring target directory exists: %w", err) } tempPath := putDir for tempPath != stagingDir && tempPath != filepath.Dir(tempPath) { if _, err := os.Stat(tempPath); err == nil { createdDirs = append(createdDirs, tempPath) } tempPath = filepath.Dir(tempPath) } } else { if err := copier.Mkdir(mountPoint, extractDirectory, mkdirOptions); err != nil { return fmt.Errorf("ensuring target directory exists: %w", err) } putRoot = extractDirectory putDir = extractDirectory } // Copy each source in turn. for _, src := range sources { var multiErr *multierror.Error var getErr, closeErr, renameErr, putErr error var wg sync.WaitGroup if sourceIsRemote(src) || sourceIsGit(src) { pipeReader, pipeWriter := io.Pipe() var srcDigest digest.Digest if options.Checksum != "" { srcDigest, err = digest.Parse(options.Checksum) if err != nil { return fmt.Errorf("invalid checksum flag: %w", err) } } wg.Add(1) if sourceIsGit(src) { go func() { defer wg.Done() defer pipeWriter.Close() var cloneDir, subdir string cloneDir, subdir, getErr = define.TempDirForURL(tmpdir.GetTempDir(), "", src) if getErr != nil { return } getOptions := copier.GetOptions{ UIDMap: srcUIDMap, GIDMap: srcGIDMap, Excludes: options.Excludes, ExpandArchives: extract, ChownDirs: chownDirs, ChmodDirs: chmodDirsFiles, ChownFiles: chownFiles, ChmodFiles: chmodDirsFiles, StripSetuidBit: options.StripSetuidBit, StripSetgidBit: options.StripSetgidBit, StripStickyBit: options.StripStickyBit, Timestamp: options.Timestamp, } writer := io.WriteCloser(pipeWriter) repositoryDir := filepath.Join(cloneDir, subdir) getErr = copier.Get(repositoryDir, repositoryDir, getOptions, []string{"."}, writer) }() } else { go func() { getErr = retry.IfNecessary(context.TODO(), func() error { return getURL(src, chownFiles, mountPoint, renameTarget, pipeWriter, chmodDirsFiles, srcDigest, options.CertPath, options.InsecureSkipTLSVerify, options.Timestamp) }, &retry.Options{ MaxRetry: options.MaxRetries, Delay: options.RetryDelay, }) pipeWriter.Close() wg.Done() }() } wg.Add(1) go func() { b.ContentDigester.Start("") hashCloser := b.ContentDigester.Hash() hasher := io.Writer(hashCloser) if options.Hasher != nil { hasher = io.MultiWriter(hasher, options.Hasher) } if options.DryRun { _, putErr = io.Copy(hasher, pipeReader) } else { putOptions := copier.PutOptions{ UIDMap: destUIDMap, GIDMap: destGIDMap, ChownDirs: nil, ChmodDirs: nil, ChownFiles: nil, ChmodFiles: nil, IgnoreDevices: userns.RunningInUserNS(), } putErr = copier.Put(putRoot, putDir, putOptions, io.TeeReader(pipeReader, hasher)) } hashCloser.Close() pipeReader.Close() wg.Done() }() wg.Wait() if getErr != nil { getErr = fmt.Errorf("reading %q: %w", src, getErr) } if putErr != nil { putErr = fmt.Errorf("storing %q: %w", src, putErr) } multiErr = multierror.Append(getErr, putErr) if multiErr != nil && multiErr.ErrorOrNil() != nil { if len(multiErr.Errors) > 1 { return multiErr.ErrorOrNil() } return multiErr.Errors[0] } continue } if options.Checksum != "" { return fmt.Errorf("checksum flag is not supported for local sources") } // Dig out the result of running glob+stat on this source spec. var localSourceStat *copier.StatsForGlob for _, st := range localSourceStats { if st.Glob == src { localSourceStat = st break } } if localSourceStat == nil { continue } // Iterate through every item that matched the glob. itemsCopied := 0 for _, globbed := range localSourceStat.Globbed { rel := globbed if filepath.IsAbs(globbed) { if rel, err = filepath.Rel(contextDir, globbed); err != nil { return fmt.Errorf("computing path of %q relative to %q: %w", globbed, contextDir, err) } } if strings.HasPrefix(rel, ".."+string(os.PathSeparator)) { return fmt.Errorf("possible escaping context directory error: %q is outside of %q", globbed, contextDir) } // Check for dockerignore-style exclusion of this item. if rel != "." { excluded, err := pm.Matches(filepath.ToSlash(rel)) //nolint:staticcheck if err != nil { return fmt.Errorf("checking if %q(%q) is excluded: %w", globbed, rel, err) } if excluded { // non-directories that are excluded are excluded, no question, but // directories can only be skipped if we don't have to allow for the // possibility of finding things to include under them globInfo := localSourceStat.Results[globbed] if !globInfo.IsDir || !includeDirectoryAnyway(rel, pm) { continue } } else { // if the destination is a directory that doesn't yet exist, and is not excluded, let's copy it. if newDestDirFound { itemsCopied++ } } } else { // Make sure we don't trigger a "copied nothing" error for an empty context // directory if we were told to copy the context directory itself. We won't // actually copy it, but we need to make sure that we don't produce an error // due to potentially not having anything in the tarstream that we passed. itemsCopied++ } st := localSourceStat.Results[globbed] if options.Link && st.ModTime.After(latestTimestamp) { latestTimestamp = st.ModTime } pipeReader, pipeWriter := io.Pipe() wg.Add(1) go func() { renamedItems := 0 writer := io.WriteCloser(pipeWriter) if renameTarget != "" { writer = newTarFilterer(writer, func(hdr *tar.Header) (bool, bool, io.Reader) { hdr.Name = renameTarget renamedItems++ return false, false, nil }) } if options.Parents { parentsPrefixToRemove, parentsToSkip := getParentsPrefixToRemoveAndParentsToSkip(src, options.ContextDir) writer = newTarFilterer(writer, func(hdr *tar.Header) (bool, bool, io.Reader) { if slices.Contains(parentsToSkip, hdr.Name) && hdr.Typeflag == tar.TypeDir { return true, false, nil } hdr.Name = strings.TrimPrefix(hdr.Name, parentsPrefixToRemove) hdr.Name = strings.TrimPrefix(hdr.Name, "/") if hdr.Typeflag == tar.TypeLink { hdr.Linkname = strings.TrimPrefix(hdr.Linkname, parentsPrefixToRemove) hdr.Linkname = strings.TrimPrefix(hdr.Linkname, "/") } if hdr.Name == "" { return true, false, nil } return false, false, nil }) } writer = newTarFilterer(writer, func(_ *tar.Header) (bool, bool, io.Reader) { itemsCopied++ return false, false, nil }) getOptions := copier.GetOptions{ UIDMap: srcUIDMap, GIDMap: srcGIDMap, Excludes: options.Excludes, ExpandArchives: extract, ChownDirs: chownDirs, ChmodDirs: chmodDirsFiles, ChownFiles: chownFiles, ChmodFiles: chmodDirsFiles, StripSetuidBit: options.StripSetuidBit, StripSetgidBit: options.StripSetgidBit, StripStickyBit: options.StripStickyBit, Parents: options.Parents, Timestamp: options.Timestamp, } getErr = copier.Get(contextDir, contextDir, getOptions, []string{globbedToGlobbable(globbed)}, writer) closeErr = writer.Close() if renameTarget != "" && renamedItems > 1 { renameErr = fmt.Errorf("internal error: renamed %d items when we expected to only rename 1", renamedItems) } wg.Done() }() wg.Add(1) go func() { if st.IsDir { b.ContentDigester.Start("dir") } else { b.ContentDigester.Start("file") } hashCloser := b.ContentDigester.Hash() hasher := io.Writer(hashCloser) if options.Hasher != nil { hasher = io.MultiWriter(hasher, options.Hasher) } if options.DryRun { _, putErr = io.Copy(hasher, pipeReader) } else { putOptions := copier.PutOptions{ UIDMap: destUIDMap, GIDMap: destGIDMap, DefaultDirOwner: chownDirs, DefaultDirMode: nil, ChownDirs: nil, ChmodDirs: nil, ChownFiles: nil, ChmodFiles: nil, IgnoreDevices: userns.RunningInUserNS(), } putErr = copier.Put(putRoot, putDir, putOptions, io.TeeReader(pipeReader, hasher)) } hashCloser.Close() pipeReader.Close() wg.Done() }() wg.Wait() if getErr != nil { getErr = fmt.Errorf("reading %q: %w", src, getErr) } if closeErr != nil { closeErr = fmt.Errorf("closing %q: %w", src, closeErr) } if renameErr != nil { renameErr = fmt.Errorf("renaming %q: %w", src, renameErr) } if putErr != nil { putErr = fmt.Errorf("storing %q: %w", src, putErr) } multiErr = multierror.Append(getErr, closeErr, renameErr, putErr) if multiErr != nil && multiErr.ErrorOrNil() != nil { if len(multiErr.Errors) > 1 { return multiErr.ErrorOrNil() } return multiErr.Errors[0] } } if itemsCopied == 0 { excludesFile := "" if options.IgnoreFile != "" { excludesFile = " using " + options.IgnoreFile } return fmt.Errorf("no items matching glob %q copied (%d filtered out%s): %w", localSourceStat.Glob, len(localSourceStat.Globbed), excludesFile, syscall.ENOENT) } } if options.Link { if !latestTimestamp.IsZero() { for _, dir := range createdDirs { if err := os.Chtimes(dir, latestTimestamp, latestTimestamp); err != nil { logrus.Warnf("failed to set timestamp on directory %q: %v", dir, err) } } } var created time.Time if options.Timestamp != nil { created = *options.Timestamp } else if !latestTimestamp.IsZero() { created = latestTimestamp } else { created = time.Unix(0, 0).UTC() } command := "ADD" if !extract { command = "COPY" } contentType, digest := b.ContentDigester.Digest() summary := contentType if digest != "" { if summary != "" { summary = summary + ":" } summary = summary + digest.Encoded() logrus.Debugf("added content from --link %s", summary) } createdBy := "/bin/sh -c #(nop) " + command + " --link " + summary + " in " + destination + " " + options.BuildMetadata history := v1.History{ Created: &created, CreatedBy: createdBy, Comment: b.HistoryComment(), } linkedLayer := LinkedLayer{ History: history, BlobPath: stagingDir, } b.AppendedLinkedLayers = append(b.AppendedLinkedLayers, linkedLayer) if err := b.Save(); err != nil { return fmt.Errorf("saving builder state after queuing linked layer: %w", err) } } return nil } // userForRun returns the user (and group) information which we should use for // running commands func (b *Builder) userForRun(mountPoint string, userspec string) (specs.User, string, error) { if userspec == "" { userspec = b.User() } uid, gid, homeDir, err := chrootuser.GetUser(mountPoint, userspec) u := specs.User{ UID: uid, GID: gid, Username: userspec, } if !strings.Contains(userspec, ":") { groups, err2 := chrootuser.GetAdditionalGroupsForUser(mountPoint, uint64(u.UID)) if err2 != nil { if !errors.Is(err2, chrootuser.ErrNoSuchUser) && err == nil { err = err2 } } else { u.AdditionalGids = groups } } return u, homeDir, err } // userForCopy returns the user (and group) information which we should use for // setting ownership of contents being copied. It's just like what // userForRun() does, except for the case where we're passed a single numeric // value, where we need to use that value for both the UID and the GID. func (b *Builder) userForCopy(mountPoint string, userspec string) (uint32, uint32, error) { var ( user, group string uid, gid uint64 err error ) split := strings.SplitN(userspec, ":", 2) user = split[0] if len(split) > 1 { group = split[1] } // If userspec did not specify any values for user or group, then fail if user == "" && group == "" { return 0, 0, fmt.Errorf("can't find uid for user %s", userspec) } // If userspec specifies values for user or group, check for numeric values // and return early. If not, then translate username/groupname if user != "" { uid, err = strconv.ParseUint(user, 10, 32) } if err == nil { // default gid to uid gid = uid if group != "" { gid, err = strconv.ParseUint(group, 10, 32) } } // If err != nil, then user or group not numeric, check filesystem if err == nil { return uint32(uid), uint32(gid), nil } owner, _, err := b.userForRun(mountPoint, userspec) if err != nil { return 0xffffffff, 0xffffffff, err } return owner.UID, owner.GID, nil } // EnsureContainerPathAs creates the specified directory if it doesn't exist, // setting a newly-created directory's owner to USER and its permissions to MODE. func (b *Builder) EnsureContainerPathAs(path, user string, mode *os.FileMode) error { mountPoint, err := b.Mount(b.MountLabel) if err != nil { return err } defer func() { if err2 := b.Unmount(); err2 != nil { logrus.Errorf("error unmounting container: %v", err2) } }() uid, gid := uint32(0), uint32(0) if user != "" { if uidForCopy, gidForCopy, err := b.userForCopy(mountPoint, user); err == nil { uid = uidForCopy gid = gidForCopy } } destUIDMap, destGIDMap := convertRuntimeIDMaps(b.IDMappingOptions.UIDMap, b.IDMappingOptions.GIDMap) idPair := &idtools.IDPair{UID: int(uid), GID: int(gid)} opts := copier.MkdirOptions{ ChmodNew: mode, ChownNew: idPair, UIDMap: destUIDMap, GIDMap: destGIDMap, } return copier.Mkdir(mountPoint, filepath.Join(mountPoint, path), opts) } ================================================ FILE: bind/mount.go ================================================ //go:build linux package bind import ( "errors" "fmt" "os" "path/filepath" "slices" "syscall" "github.com/containers/buildah/util" "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" "go.podman.io/storage/pkg/idtools" "go.podman.io/storage/pkg/mount" "golang.org/x/sys/unix" ) // SetupIntermediateMountNamespace creates a new mount namespace and bind // mounts all bind-mount sources into a subdirectory of bundlePath that can // only be reached by the root user of the container's user namespace, except // for Mounts which include the NoBindOption option in their options list. The // NoBindOption will then merely be removed. func SetupIntermediateMountNamespace(spec *specs.Spec, bundlePath string) (unmountAll func() error, err error) { defer stripNoBindOption(spec) // We expect a root directory to be defined. if spec.Root == nil { return nil, errors.New("configuration has no root filesystem?") } rootPath := spec.Root.Path // Create a new mount namespace in which to do the things we're doing. if err := unix.Unshare(unix.CLONE_NEWNS); err != nil { return nil, fmt.Errorf("creating new mount namespace for %v: %w", spec.Process.Args, err) } // Make all of our mounts private to our namespace. if err := mount.MakeRPrivate("/"); err != nil { return nil, fmt.Errorf("making mounts private to mount namespace for %v: %w", spec.Process.Args, err) } // Make sure the bundle directory is searchable. We created it with // TempDir(), so it should have started with permissions set to 0700. info, err := os.Stat(bundlePath) if err != nil { return nil, fmt.Errorf("checking permissions on %q: %w", bundlePath, err) } if err = os.Chmod(bundlePath, info.Mode()|0o111); err != nil { return nil, fmt.Errorf("loosening permissions on %q: %w", bundlePath, err) } // Figure out who needs to be able to reach these bind mounts in order // for the container to be started. rootUID, rootGID, err := util.GetHostRootIDs(spec) if err != nil { return nil, err } // Hand back a callback that the caller can use to clean up everything // we're doing here. unmount := []string{} unmountAll = func() (err error) { for _, mountpoint := range unmount { // Unmount it and anything under it. if err2 := UnmountMountpoints(mountpoint, nil); err2 != nil { logrus.Warnf("pkg/bind: error unmounting %q: %v", mountpoint, err2) if err == nil { err = err2 } } if err2 := unix.Unmount(mountpoint, unix.MNT_DETACH); err2 != nil { if errno, ok := err2.(syscall.Errno); !ok || errno != syscall.EINVAL { logrus.Warnf("pkg/bind: error detaching %q: %v", mountpoint, err2) if err == nil { err = err2 } } } // Remove just the mountpoint. retry := 10 remove := unix.Unlink err2 := remove(mountpoint) for err2 != nil && retry > 0 { if errno, ok := err2.(syscall.Errno); ok { switch errno { default: retry = 0 continue case syscall.EISDIR: remove = unix.Rmdir err2 = remove(mountpoint) case syscall.EBUSY: if err3 := unix.Unmount(mountpoint, unix.MNT_DETACH); err3 == nil { err2 = remove(mountpoint) } } retry-- } } if err2 != nil { logrus.Warnf("pkg/bind: error removing %q: %v", mountpoint, err2) if err == nil { err = err2 } } } return err } // Create a top-level directory that the "root" user will be able to // access, that "root" from containers which use different mappings, or // other unprivileged users outside of containers, shouldn't be able to // access. mnt := filepath.Join(bundlePath, "mnt") if err = idtools.MkdirAndChown(mnt, 0o100, idtools.IDPair{UID: int(rootUID), GID: int(rootGID)}); err != nil { return unmountAll, fmt.Errorf("creating %q owned by the container's root user: %w", mnt, err) } // Make that directory private, and add it to the list of locations we // unmount at cleanup time. if err = mount.MakeRPrivate(mnt); err != nil { return unmountAll, fmt.Errorf("marking filesystem at %q as private: %w", mnt, err) } unmount = append([]string{mnt}, unmount...) // Create a bind mount for the root filesystem and add it to the list. rootfs := filepath.Join(mnt, "rootfs") if err = os.Mkdir(rootfs, 0o000); err != nil { return unmountAll, fmt.Errorf("creating directory %q: %w", rootfs, err) } if err = unix.Mount(rootPath, rootfs, "", unix.MS_BIND|unix.MS_REC|unix.MS_PRIVATE, ""); err != nil { return unmountAll, fmt.Errorf("bind mounting root filesystem from %q to %q: %w", rootPath, rootfs, err) } logrus.Debugf("bind mounted %q to %q", rootPath, rootfs) unmount = append([]string{rootfs}, unmount...) spec.Root.Path = rootfs // Do the same for everything we're binding in. mounts := make([]specs.Mount, 0, len(spec.Mounts)) for i := range spec.Mounts { // If we're not using an intermediate, leave it in the list. if leaveBindMountAlone(spec.Mounts[i]) { mounts = append(mounts, spec.Mounts[i]) continue } // Check if the source is a directory or something else. info, err := os.Stat(spec.Mounts[i].Source) if err != nil { if errors.Is(err, os.ErrNotExist) { logrus.Warnf("couldn't find %q on host to bind mount into container", spec.Mounts[i].Source) continue } return unmountAll, fmt.Errorf("checking if %q is a directory: %w", spec.Mounts[i].Source, err) } stage := filepath.Join(mnt, fmt.Sprintf("buildah-bind-target-%d", i)) if info.IsDir() { // If the source is a directory, make one to use as the // mount target. if err = os.Mkdir(stage, 0o000); err != nil { return unmountAll, fmt.Errorf("creating directory %q: %w", stage, err) } } else { // If the source is not a directory, create an empty // file to use as the mount target. file, err := os.OpenFile(stage, os.O_WRONLY|os.O_CREATE, 0o000) if err != nil { return unmountAll, fmt.Errorf("creating file %q: %w", stage, err) } file.Close() } // Bind mount the source from wherever it is to a place where // we know the runtime helper will be able to get to it... if err = unix.Mount(spec.Mounts[i].Source, stage, "", unix.MS_BIND|unix.MS_REC|unix.MS_PRIVATE, ""); err != nil { return unmountAll, fmt.Errorf("bind mounting bind object from %q to %q: %w", spec.Mounts[i].Source, stage, err) } logrus.Debugf("bind mounted %q to %q", spec.Mounts[i].Source, stage) spec.Mounts[i].Source = stage // ... and update the source location that we'll pass to the // runtime to our intermediate location. mounts = append(mounts, spec.Mounts[i]) unmount = append([]string{stage}, unmount...) } spec.Mounts = mounts return unmountAll, nil } // Decide if the mount should not be redirected to an intermediate location first. func leaveBindMountAlone(mount specs.Mount) bool { // If we know we shouldn't do a redirection for this mount, skip it. if slices.Contains(mount.Options, NoBindOption) { return true } // If we're not bind mounting it in, we don't need to do anything for it. if mount.Type != "bind" && !slices.Contains(mount.Options, "bind") && !slices.Contains(mount.Options, "rbind") { return true } return false } // UnmountMountpoints unmounts the given mountpoints and anything that's hanging // off of them, rather aggressively. If a mountpoint also appears in the // mountpointsToRemove slice, the mountpoints are removed after they are // unmounted. func UnmountMountpoints(mountpoint string, mountpointsToRemove []string) error { mounts, err := mount.GetMounts() if err != nil { return fmt.Errorf("retrieving list of mounts: %w", err) } // getChildren returns the list of mount IDs that hang off of the // specified ID. getChildren := func(id int) []int { var list []int for _, info := range mounts { if info.Parent == id { list = append(list, info.ID) } } return list } // getTree returns the list of mount IDs that hang off of the specified // ID, and off of those mount IDs, etc. getTree := func(id int) []int { mounts := []int{id} i := 0 for i < len(mounts) { children := getChildren(mounts[i]) mounts = append(mounts, children...) i++ } return mounts } // getMountByID looks up the mount info with the specified ID getMountByID := func(id int) *mount.Info { for i := range mounts { if mounts[i].ID == id { return mounts[i] } } return nil } // getMountByPoint looks up the mount info with the specified mountpoint getMountByPoint := func(mountpoint string) *mount.Info { for i := range mounts { if mounts[i].Mountpoint == mountpoint { return mounts[i] } } return nil } // find the top of the tree we're unmounting top := getMountByPoint(mountpoint) if top == nil { if err != nil { return fmt.Errorf("%q is not mounted: %w", mountpoint, err) } return nil } // add all of the mounts that are hanging off of it tree := getTree(top.ID) // unmount each mountpoint, working from the end of the list (leaf nodes) to the top for i := range tree { var st unix.Stat_t id := tree[len(tree)-i-1] mount := getMountByID(id) // check if this mountpoint is mounted if err := unix.Lstat(mount.Mountpoint, &st); err != nil { if errors.Is(err, os.ErrNotExist) { logrus.Debugf("mountpoint %q is not present(?), skipping", mount.Mountpoint) continue } return fmt.Errorf("checking if %q is mounted: %w", mount.Mountpoint, err) } if uint64(mount.Major) != uint64(st.Dev) || uint64(mount.Minor) != uint64(st.Dev) { //nolint:unconvert // (required for some OS/arch combinations) logrus.Debugf("%q is apparently not really mounted, skipping", mount.Mountpoint) continue } // do the unmount if err := unix.Unmount(mount.Mountpoint, 0); err != nil { // if it was busy, detach it if errno, ok := err.(syscall.Errno); ok && errno == syscall.EBUSY { err = unix.Unmount(mount.Mountpoint, unix.MNT_DETACH) } if err != nil { // if it was invalid (not mounted), hide the error, else return it if errno, ok := err.(syscall.Errno); !ok || errno != syscall.EINVAL { logrus.Warnf("error unmounting %q: %v", mount.Mountpoint, err) continue } } } // if we're also supposed to remove this thing, do that, too if slices.Contains(mountpointsToRemove, mount.Mountpoint) { if err := os.Remove(mount.Mountpoint); err != nil { return fmt.Errorf("removing %q: %w", mount.Mountpoint, err) } } } return nil } ================================================ FILE: bind/mount_unsupported.go ================================================ //go:build !linux package bind import ( "github.com/opencontainers/runtime-spec/specs-go" ) // SetupIntermediateMountNamespace returns a no-op unmountAll() and no error. func SetupIntermediateMountNamespace(spec *specs.Spec, bundlePath string) (unmountAll func() error, err error) { stripNoBindOption(spec) return func() error { return nil }, nil } ================================================ FILE: bind/util.go ================================================ package bind import ( "slices" "github.com/opencontainers/runtime-spec/specs-go" ) const ( // NoBindOption is an option which, if present in a Mount structure's // options list, will cause SetupIntermediateMountNamespace to not // redirect it through a bind mount. NoBindOption = "nobuildahbind" ) func stripNoBindOption(spec *specs.Spec) { for i := range spec.Mounts { if slices.Contains(spec.Mounts[i].Options, NoBindOption) { prunedOptions := make([]string, 0, len(spec.Mounts[i].Options)) for _, option := range spec.Mounts[i].Options { if option != NoBindOption { prunedOptions = append(prunedOptions, option) } } spec.Mounts[i].Options = prunedOptions } } } ================================================ FILE: btrfs_installed_tag.sh ================================================ #!/usr/bin/env bash ${CPP:-${CC:-cc} -E} ${CPPFLAGS} - > /dev/null 2> /dev/null << EOF #include EOF if test $? -ne 0 ; then echo exclude_graphdriver_btrfs fi ================================================ FILE: buildah.go ================================================ package buildah import ( "context" "encoding/json" "errors" "fmt" "io" "os" "path/filepath" "sort" "time" "github.com/containers/buildah/define" "github.com/containers/buildah/docker" encconfig "github.com/containers/ocicrypt/config" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" nettypes "go.podman.io/common/libnetwork/types" "go.podman.io/image/v5/types" "go.podman.io/storage" "go.podman.io/storage/pkg/ioutils" ) const ( // Package is the name of this package, used in help output and to // identify working containers. Package = define.Package // Version for the Package. Version = define.Version // The value we use to identify what type of information, currently a // serialized Builder structure, we are using as per-container state. // This should only be changed when we make incompatible changes to // that data structure, as it's used to distinguish containers which // are "ours" from ones that aren't. containerType = Package + " 0.0.1" // The file in the per-container directory which we use to store our // per-container state. If it isn't there, then the container isn't // one of our build containers. stateFile = Package + ".json" ) // PullPolicy takes the value PullIfMissing, PullAlways, PullIfNewer, or PullNever. type PullPolicy = define.PullPolicy const ( // PullIfMissing is one of the values that BuilderOptions.PullPolicy // can take, signalling that the source image should be pulled from a // registry if a local copy of it is not already present. PullIfMissing = define.PullIfMissing // PullAlways is one of the values that BuilderOptions.PullPolicy can // take, signalling that a fresh, possibly updated, copy of the image // should be pulled from a registry before the build proceeds. PullAlways = define.PullAlways // PullIfNewer is one of the values that BuilderOptions.PullPolicy // can take, signalling that the source image should only be pulled // from a registry if a local copy is not already present or if a // newer version the image is present on the repository. PullIfNewer = define.PullIfNewer // PullNever is one of the values that BuilderOptions.PullPolicy can // take, signalling that the source image should not be pulled from a // registry if a local copy of it is not already present. PullNever = define.PullNever ) // NetworkConfigurationPolicy takes the value NetworkDefault, NetworkDisabled, // or NetworkEnabled. type NetworkConfigurationPolicy = define.NetworkConfigurationPolicy const ( // NetworkDefault is one of the values that BuilderOptions.ConfigureNetwork // can take, signalling that the default behavior should be used. NetworkDefault = define.NetworkDefault // NetworkDisabled is one of the values that BuilderOptions.ConfigureNetwork // can take, signalling that network interfaces should NOT be configured for // newly-created network namespaces. NetworkDisabled = define.NetworkDisabled // NetworkEnabled is one of the values that BuilderOptions.ConfigureNetwork // can take, signalling that network interfaces should be configured for // newly-created network namespaces. NetworkEnabled = define.NetworkEnabled ) // Builder objects are used to represent containers which are being used to // build images. They also carry potential updates which will be applied to // the image's configuration when the container's contents are used to build an // image. type Builder struct { store storage.Store // Logger is the logrus logger to write log messages with Logger *logrus.Logger `json:"-"` // Args define variables that users can pass at build-time to the builder. Args map[string]string // Type is used to help identify a build container's metadata. It // should not be modified. Type string `json:"type"` // FromImage is the name of the source image which was used to create // the container, if one was used. It should not be modified. FromImage string `json:"image,omitempty"` // FromImageID is the ID of the source image which was used to create // the container, if one was used. It should not be modified. FromImageID string `json:"image-id"` // FromImageDigest is the digest of the source image which was used to // create the container, if one was used. It should not be modified. FromImageDigest string `json:"image-digest"` // Config is the source image's configuration. It should not be // modified. Config []byte `json:"config,omitempty"` // Manifest is the source image's manifest. It should not be modified. Manifest []byte `json:"manifest,omitempty"` // Container is the name of the build container. It should not be modified. Container string `json:"container-name,omitempty"` // ContainerID is the ID of the build container. It should not be modified. ContainerID string `json:"container-id,omitempty"` // MountPoint is the last location where the container's root // filesystem was mounted. It should not be modified. MountPoint string `json:"mountpoint,omitempty"` // ProcessLabel is the SELinux process label to use during subsequent Run() calls. ProcessLabel string `json:"process-label,omitempty"` // MountLabel is the SELinux mount label associated with the container MountLabel string `json:"mount-label,omitempty"` // ImageAnnotations is a set of key-value pairs which is stored in the // image's manifest. ImageAnnotations map[string]string `json:"annotations,omitempty"` // ImageCreatedBy is a description of how this container was built. ImageCreatedBy string `json:"created-by,omitempty"` // ImageHistoryComment is a description of how our added layers were built. ImageHistoryComment string `json:"history-comment,omitempty"` // Image metadata and runtime settings, in multiple formats. OCIv1 v1.Image `json:"ociv1"` Docker docker.V2Image `json:"docker"` // DefaultMountsFilePath is the file path holding the mounts to be mounted in "host-path:container-path" format. DefaultMountsFilePath string `json:"defaultMountsFilePath,omitempty"` // Isolation controls how we handle "RUN" statements and the Run() method. Isolation define.Isolation // NamespaceOptions controls how we set up the namespaces for processes that we Run(). NamespaceOptions define.NamespaceOptions // ConfigureNetwork controls whether or not network interfaces and // routing are configured for a new network namespace (i.e., when not // joining another's namespace and not just using the host's // namespace), effectively deciding whether or not the process has a // usable network. ConfigureNetwork define.NetworkConfigurationPolicy // Deprecated: CNIPluginPath was the location of CNI plugin helpers. // It is no longer used and is expected to be empty. CNIPluginPath string // Deprecated: CNIConfigDir was the location of CNI configuration files. // It is no longer used and is expected to be empty. CNIConfigDir string // NetworkInterface is the libnetwork network interface used to setup netavark networks. NetworkInterface nettypes.ContainerNetwork `json:"-"` // GroupAdd is a list of groups to add to the primary process when Run() is // called. The magic 'keep-groups' value indicates that the process should // be allowed to inherit the current set of supplementary groups. GroupAdd []string // ID mapping options to use when running processes with non-host user namespaces. IDMappingOptions define.IDMappingOptions // Capabilities is a list of capabilities to use when running commands in the container. Capabilities []string // PrependedEmptyLayers are history entries that we'll add to a // committed image, after any history items that we inherit from a base // image, but before the history item for the layer that we're // committing. PrependedEmptyLayers []v1.History // AppendedEmptyLayers are history entries that we'll add to a // committed image after the history item for the layer that we're // committing. AppendedEmptyLayers []v1.History CommonBuildOpts *define.CommonBuildOptions // TopLayer is the top layer of the image TopLayer string // Format to use for a container image we eventually commit, when we do. Format string // TempVolumes are temporary mount points created during Run() calls. // Deprecated: do not use. TempVolumes map[string]bool // ContentDigester counts the digest of all Add()ed content since it was // last restarted. ContentDigester CompositeDigester // Devices are parsed additional devices to provide to Run() calls. Devices define.ContainerDevices // DeviceSpecs are unparsed additional devices to provide to Run() calls. DeviceSpecs []string // CDIConfigDir is the location of CDI configuration files, if the files in // the default configuration locations shouldn't be used. CDIConfigDir string // PrependedLinkedLayers and AppendedLinkedLayers are combinations of // history entries and locations of either directory trees (if // directories, per os.Stat()) or uncompressed layer blobs which should // be added to the image at commit-time. The order of these relative // to PrependedEmptyLayers and AppendedEmptyLayers in the committed // image is not guaranteed. PrependedLinkedLayers, AppendedLinkedLayers []LinkedLayer } // BuilderInfo are used as objects to display container information type BuilderInfo struct { Type string FromImage string FromImageID string FromImageDigest string GroupAdd []string Config string Manifest string Container string ContainerID string MountPoint string ProcessLabel string MountLabel string ImageAnnotations map[string]string ImageCreatedBy string OCIv1 v1.Image Docker docker.V2Image DefaultMountsFilePath string Isolation string NamespaceOptions define.NamespaceOptions Capabilities []string ConfigureNetwork string // Deprecated: CNIPluginPath is no longer used and is expected to be empty. CNIPluginPath string // Deprecated: CNIConfigDir is no longer used and is expected to be empty. CNIConfigDir string IDMappingOptions define.IDMappingOptions History []v1.History Devices define.ContainerDevices DeviceSpecs []string CDIConfigDir string } // GetBuildInfo gets a pointer to a Builder object and returns a BuilderInfo object from it. // This is used in the inspect command to display Manifest and Config as string and not []byte. func GetBuildInfo(b *Builder) BuilderInfo { history := copyHistory(b.OCIv1.History) history = append(history, copyHistory(b.PrependedEmptyLayers)...) history = append(history, copyHistory(b.AppendedEmptyLayers)...) sort.Strings(b.Capabilities) return BuilderInfo{ Type: b.Type, FromImage: b.FromImage, FromImageID: b.FromImageID, FromImageDigest: b.FromImageDigest, Config: string(b.Config), Manifest: string(b.Manifest), Container: b.Container, ContainerID: b.ContainerID, GroupAdd: b.GroupAdd, MountPoint: b.MountPoint, ProcessLabel: b.ProcessLabel, MountLabel: b.MountLabel, ImageAnnotations: b.ImageAnnotations, ImageCreatedBy: b.ImageCreatedBy, OCIv1: b.OCIv1, Docker: b.Docker, DefaultMountsFilePath: b.DefaultMountsFilePath, Isolation: b.Isolation.String(), NamespaceOptions: b.NamespaceOptions, ConfigureNetwork: fmt.Sprintf("%v", b.ConfigureNetwork), IDMappingOptions: b.IDMappingOptions, Capabilities: b.Capabilities, History: history, Devices: b.Devices, DeviceSpecs: b.DeviceSpecs, CDIConfigDir: b.CDIConfigDir, } } // CommonBuildOptions are resources that can be defined by flags for both buildah from and build type CommonBuildOptions = define.CommonBuildOptions // BuilderOptions are used to initialize a new Builder. type BuilderOptions struct { // Args define variables that users can pass at build-time to the builder Args map[string]string // FromImage is the name of the image which should be used as the // starting point for the container. It can be set to an empty value // or "scratch" to indicate that the container should not be based on // an image. FromImage string // ContainerSuffix is the suffix to add for generated container names ContainerSuffix string // Container is a desired name for the build container. Container string // PullPolicy decides whether or not we should pull the image that // we're using as a base image. It should be PullIfMissing, // PullAlways, or PullNever. PullPolicy define.PullPolicy // Registry is a value which is prepended to the image's name, if it // needs to be pulled and the image name alone can not be resolved to a // reference to a source image. No separator is implicitly added. Registry string // BlobDirectory is the name of a directory in which we'll attempt // to store copies of layer blobs that we pull down, if any. It should // already exist. BlobDirectory string GroupAdd []string // Logger is the logrus logger to write log messages with Logger *logrus.Logger `json:"-"` // Mount signals to NewBuilder() that the container should be mounted // immediately. Mount bool // SignaturePolicyPath specifies an override location for the signature // policy which should be used for verifying the new image as it is // being written. Except in specific circumstances, no value should be // specified, indicating that the shared, system-wide default policy // should be used. SignaturePolicyPath string // ReportWriter is an io.Writer which will be used to log the reading // of the source image from a registry, if we end up pulling the image. ReportWriter io.Writer // github.com/containers/image/types SystemContext to hold credentials // and other authentication/authorization information. SystemContext *types.SystemContext // DefaultMountsFilePath is the file path holding the mounts to be // mounted in "host-path:container-path" format DefaultMountsFilePath string // Isolation controls how we handle "RUN" statements and the Run() // method. Isolation define.Isolation // NamespaceOptions controls how we set up namespaces for processes that // we might need to run using the container's root filesystem. NamespaceOptions define.NamespaceOptions // ConfigureNetwork controls whether or not network interfaces and // routing are configured for a new network namespace (i.e., when not // joining another's namespace and not just using the host's // namespace), effectively deciding whether or not the process has a // usable network. ConfigureNetwork define.NetworkConfigurationPolicy // Deprecated: CNIPluginPath was the location of CNI plugin helpers. // It is no longer used and is expected to be empty. CNIPluginPath string // Deprecated: CNIConfigDir was the location of CNI configuration files. // It is no longer used and is expected to be empty. CNIConfigDir string // NetworkInterface is the libnetwork network interface used to setup netavark networks. NetworkInterface nettypes.ContainerNetwork `json:"-"` // ID mapping options to use if we're setting up our own user namespace. IDMappingOptions *define.IDMappingOptions // Capabilities is a list of capabilities to use when // running commands for Run(). Capabilities []string CommonBuildOpts *define.CommonBuildOptions // Format to use for a container image we eventually commit, when we do. Format string // Devices are additional parsed devices to provide for Run() calls. Devices define.ContainerDevices // DeviceSpecs are additional unparsed devices to provide for Run() calls. DeviceSpecs []string // DefaultEnv is deprecated and ignored. DefaultEnv []string // MaxPullRetries is the maximum number of attempts we'll make to pull // any one image from the external registry if the first attempt fails. MaxPullRetries int // PullRetryDelay is how long to wait before retrying a pull attempt. PullRetryDelay time.Duration // OciDecryptConfig contains the config that can be used to decrypt an image if it is // encrypted if non-nil. If nil, it does not attempt to decrypt an image. OciDecryptConfig *encconfig.DecryptConfig // ProcessLabel is the SELinux process label associated with commands we Run() ProcessLabel string // MountLabel is the SELinux mount label associated with the working container MountLabel string // PreserveBaseImageAnns indicates that we should preserve base // image information (Annotations) that are present in our base image, // rather than overwriting them with information about the base image // itself. Useful as an internal implementation detail of multistage // builds, and does not need to be set by most callers. PreserveBaseImageAnns bool // CDIConfigDir is the location of CDI configuration files, if the files in // the default configuration locations shouldn't be used. CDIConfigDir string // CompatScratchConfig controls whether a "scratch" image is created // with a truly empty configuration, as would have happened in the past // (when set to true), or with a minimal initial configuration which // has a working directory set in it. CompatScratchConfig types.OptionalBool } // ImportOptions are used to initialize a Builder from an existing container // which was created elsewhere. type ImportOptions struct { // Container is the name of the build container. Container string // SignaturePolicyPath specifies an override location for the signature // policy which should be used for verifying the new image as it is // being written. Except in specific circumstances, no value should be // specified, indicating that the shared, system-wide default policy // should be used. SignaturePolicyPath string } // ImportFromImageOptions are used to initialize a Builder from an image. type ImportFromImageOptions struct { // Image is the name or ID of the image we'd like to examine. Image string // SignaturePolicyPath specifies an override location for the signature // policy which should be used for verifying the new image as it is // being written. Except in specific circumstances, no value should be // specified, indicating that the shared, system-wide default policy // should be used. SignaturePolicyPath string // github.com/containers/image/types SystemContext to hold information // about which registries we should check for completing image names // that don't include a domain portion. SystemContext *types.SystemContext } // ConfidentialWorkloadOptions encapsulates options which control whether or not // we output an image whose rootfs contains a LUKS-compatibly-encrypted disk image // instead of the usual rootfs contents. type ConfidentialWorkloadOptions = define.ConfidentialWorkloadOptions // SBOMScanOptions encapsulates options which control whether or not we run a // scanner on the rootfs that we're about to commit, and how. type SBOMScanOptions = define.SBOMScanOptions // NewBuilder creates a new build container. func NewBuilder(ctx context.Context, store storage.Store, options BuilderOptions) (*Builder, error) { if options.CommonBuildOpts == nil { options.CommonBuildOpts = &CommonBuildOptions{} } return newBuilder(ctx, store, options) } // ImportBuilder creates a new build configuration using an already-present // container. func ImportBuilder(ctx context.Context, store storage.Store, options ImportOptions) (*Builder, error) { return importBuilder(ctx, store, options) } // ImportBuilderFromImage creates a new builder configuration using an image. // The returned object can be modified and examined, but it can not be saved // or committed because it is not associated with a working container. func ImportBuilderFromImage(ctx context.Context, store storage.Store, options ImportFromImageOptions) (*Builder, error) { return importBuilderFromImage(ctx, store, options) } // OpenBuilder loads information about a build container given its name or ID. func OpenBuilder(store storage.Store, container string) (*Builder, error) { cdir, err := store.ContainerDirectory(container) if err != nil { return nil, err } buildstate, err := os.ReadFile(filepath.Join(cdir, stateFile)) if err != nil { return nil, err } b := &Builder{} if err = json.Unmarshal(buildstate, &b); err != nil { return nil, fmt.Errorf("parsing %q, read from %q: %w", string(buildstate), filepath.Join(cdir, stateFile), err) } if b.Type != containerType { return nil, fmt.Errorf("container %q is not a %s container (is a %q container)", container, define.Package, b.Type) } netInt, err := getNetworkInterface(store) if err != nil { return nil, err } b.NetworkInterface = netInt b.store = store b.fixupConfig(nil) b.setupLogger() if b.CommonBuildOpts == nil { b.CommonBuildOpts = &CommonBuildOptions{} } return b, nil } // OpenBuilderByPath loads information about a build container given a // path to the container's root filesystem func OpenBuilderByPath(store storage.Store, path string) (*Builder, error) { containers, err := store.Containers() if err != nil { return nil, err } abs, err := filepath.Abs(path) if err != nil { return nil, err } builderMatchesPath := func(b *Builder, path string) bool { return (b.MountPoint == path) } for _, container := range containers { cdir, err := store.ContainerDirectory(container.ID) if err != nil { return nil, err } buildstate, err := os.ReadFile(filepath.Join(cdir, stateFile)) if err != nil { if errors.Is(err, os.ErrNotExist) { logrus.Debugf("error reading %q: %v, ignoring container %q", filepath.Join(cdir, stateFile), err, container.ID) continue } return nil, err } b := &Builder{} err = json.Unmarshal(buildstate, &b) if err == nil && b.Type == containerType && builderMatchesPath(b, abs) { b.store = store b.fixupConfig(nil) b.setupLogger() if b.CommonBuildOpts == nil { b.CommonBuildOpts = &CommonBuildOptions{} } return b, nil } if err != nil { logrus.Debugf("error parsing %q, read from %q: %v", string(buildstate), filepath.Join(cdir, stateFile), err) } else if b.Type != containerType { logrus.Debugf("container %q is not a %s container (is a %q container)", container.ID, define.Package, b.Type) } } return nil, storage.ErrContainerUnknown } // OpenAllBuilders loads all containers which have a state file that we use in // their data directory, typically so that they can be listed. func OpenAllBuilders(store storage.Store) (builders []*Builder, err error) { containers, err := store.Containers() if err != nil { return nil, err } for _, container := range containers { cdir, err := store.ContainerDirectory(container.ID) if err != nil { return nil, err } buildstate, err := os.ReadFile(filepath.Join(cdir, stateFile)) if err != nil { if errors.Is(err, os.ErrNotExist) { logrus.Debugf("%v, ignoring container %q", err, container.ID) continue } return nil, err } b := &Builder{} err = json.Unmarshal(buildstate, &b) if err == nil && b.Type == containerType { b.store = store b.setupLogger() b.fixupConfig(nil) if b.CommonBuildOpts == nil { b.CommonBuildOpts = &CommonBuildOptions{} } builders = append(builders, b) continue } if err != nil { logrus.Debugf("error parsing %q, read from %q: %v", string(buildstate), filepath.Join(cdir, stateFile), err) } else if b.Type != containerType { logrus.Debugf("container %q is not a %s container (is a %q container)", container.ID, define.Package, b.Type) } } return builders, nil } // Save saves the builder's current state to the build container's metadata. // This should not need to be called directly, as other methods of the Builder // object take care of saving their state. func (b *Builder) Save() error { buildstate, err := json.Marshal(b) if err != nil { return err } cdir, err := b.store.ContainerDirectory(b.ContainerID) if err != nil { return err } if err = ioutils.AtomicWriteFile(filepath.Join(cdir, stateFile), buildstate, 0o600); err != nil { return fmt.Errorf("saving builder state to %q: %w", filepath.Join(cdir, stateFile), err) } return nil } ================================================ FILE: buildah_test.go ================================================ package buildah import ( "context" "flag" "os" "testing" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" imagetypes "go.podman.io/image/v5/types" "go.podman.io/storage" "go.podman.io/storage/types" ) var testSystemContext = imagetypes.SystemContext{ SignaturePolicyPath: "tests/policy.json", SystemRegistriesConfPath: "tests/registries.conf", } func TestMain(m *testing.M) { var logLevel string debug := false if InitReexec() { return } flag.BoolVar(&debug, "debug", false, "turn on debug logging") flag.StringVar(&logLevel, "log-level", "error", "log level") flag.Parse() level, err := logrus.ParseLevel(logLevel) if err != nil { logrus.Fatalf("error parsing log level %q: %v", logLevel, err) } if debug && level < logrus.DebugLevel { level = logrus.DebugLevel } logrus.SetLevel(level) os.Exit(m.Run()) } func TestOpenBuilderCommonBuildOpts(t *testing.T) { // This test cannot be parallelized as this uses NewBuilder() // which eventually and indirectly accesses a global variable // defined in `go-selinux`, this must be fixed at `go-selinux` // or builder must enable sometime of locking mechanism i.e if // routine is creating Builder other's must wait for it. // Tracked here: https://github.com/containers/buildah/issues/5967 ctx := context.TODO() store, err := storage.GetStore(types.StoreOptions{ RunRoot: t.TempDir(), GraphRoot: t.TempDir(), GraphDriverName: "vfs", }) require.NoError(t, err) t.Cleanup(func() { _, err := store.Shutdown(true); assert.NoError(t, err) }) b, err := NewBuilder(ctx, store, BuilderOptions{}) require.NoError(t, err) require.NotNil(t, b.CommonBuildOpts) b.CommonBuildOpts = nil builderContainerID := b.ContainerID err = b.Save() require.NoError(t, err) b, err = OpenBuilder(store, builderContainerID) require.NoError(t, err) require.NotNil(t, b.CommonBuildOpts) builders, err := OpenAllBuilders(store) require.NoError(t, err) for _, b := range builders { require.NotNil(t, b.CommonBuildOpts) } imageID, _, _, err := b.Commit(ctx, nil, CommitOptions{}) require.NoError(t, err) b, err = ImportBuilderFromImage(ctx, store, ImportFromImageOptions{ Image: imageID, }) require.NoError(t, err) require.NotNil(t, b.CommonBuildOpts) container, err := store.CreateContainer("", nil, imageID, "", "", &storage.ContainerOptions{}) require.NoError(t, err) require.NotNil(t, container) b, err = ImportBuilder(ctx, store, ImportOptions{ Container: container.ID, SignaturePolicyPath: testSystemContext.SignaturePolicyPath, }) require.NoError(t, err) require.NotNil(t, b.CommonBuildOpts) } ================================================ FILE: changelog.txt ================================================ - Changelog for v1.42.0 (2025-10-17) * Bump to storage v1.61.0, image v5.38.0, common v0.66.0 * fix(deps): update module github.com/openshift/imagebuilder to v1.2.19 * fix(deps): update module github.com/openshift/imagebuilder to v1.2.18 * copier: ignore user.overlay.* xattrs * commit: always return the config digest as the image ID * fix(deps): update module golang.org/x/crypto to v0.43.0 * fix(deps): update module golang.org/x/sys to v0.37.0 * fix(deps): update module github.com/docker/docker to v28.5.1+incompatible * fix(deps): update module github.com/moby/buildkit to v0.25.1 * fix(deps): update module github.com/opencontainers/runc to v1.3.2 * fix(deps): update module github.com/docker/docker to v28.5.0+incompatible * fix(deps): update module github.com/moby/buildkit to v0.25.0 * fix(deps): update github.com/containers/luksy digest to 2cf5bc9 * Make some test files different from each other * Revert "fix(deps): update module github.com/cyphar/filepath-securejoin to v0.5.0" * Also run integration tests with the Sequoia backend * Allow users to build against podman-sequoia in non-default locations * fix(deps): update module github.com/cyphar/filepath-securejoin to v0.5.0 * .cirrus.yml: Test Vendoring bump golang * vendor: bump go.podman.io/{common,image,storage} to main * fix(deps): update module golang.org/x/crypto to v0.42.0 * fix(deps): update module github.com/docker/docker to v28.4.0+incompatible * fix(deps): update module github.com/moby/buildkit to v0.24.0 * fix(deps): update module github.com/spf13/pflag to v1.0.10 * fix(deps): update module github.com/fsouza/go-dockerclient to v1.12.2 * fix(deps): update module github.com/opencontainers/runc to v1.3.1 * fix(deps): update module github.com/opencontainers/cgroups to v0.0.5 * fix(deps): update module golang.org/x/sync to v0.17.0 * tests/run.bats: "run masks" test: accept "unreadable" masked directories * Run: create parent directories of mount targets with mode 0755 * tests/run.bats: "run masks" test: accept "unreadable" masked directories * New VM images * Suppress a linter warning * modernize: JSON doesn't do "omitempty" structs, so stop asking * modernize: use maps.Copy() instead of iterating over a map to copy it * modernize: use strings.CutPrefix/SplitSeq/FieldsSeq * Update expected/minimum version of Go to 1.24 * chroot: use $PATH when finding commands * [skip-ci] Update actions/stale action to v10 * Update module github.com/ulikunitz/xz to v0.5.15 [SECURITY] * Update go.sum * New VM images * Update module github.com/openshift/imagebuilder to v1 * Update module github.com/spf13/cobra to v1.10.1 * Switch common, storage and image to monorepo. * Update module github.com/stretchr/testify to v1.11.1 * Update module go.etcd.io/bbolt to v1.4.3 * Handle tagged+digested references when processing --all-platforms * Update module github.com/stretchr/testify to v1.11.0 * Add --transient-store global option * Support "--imagestore" global flags * Commit: don't depend on MountImage(), because .imagestore * Adding mohanboddu as community manager to MAINTAINERS.md * Rework how we decide what to filter out of layer diffs * Note that we have to build `true` first for the sake of its tests * copier.Stat(): return owner UID and GID if available * copier.Get(): ensure that directory entries end in "/" * copier.Get(): strip user and group names from entries * imagebuildah.Executor/StageExecutor: check numeric --from= values * Losen the dependency on go-connections/tlsconfig * fix(deps): update module golang.org/x/crypto to v0.41.0 * fix(deps): update module golang.org/x/term to v0.34.0 * fix(deps): update module github.com/docker/go-connections to v0.6.0 * fix(deps): update module golang.org/x/sys to v0.35.0 * copy: assume a destination with basename "." is a directory * generatePathChecksum: ignore ModTime, AccessTime and ChangeTime * fix(deps): update module github.com/seccomp/libseccomp-golang to v0.11.1 * fix(deps): update module github.com/containers/common to v0.64.1 * History should note unset-label, timestamp, and rewrite-timestamp * pkg/cli.GenBuildOptions(): don't hardwire optional bools * fix(deps): update module github.com/containers/image/v5 to v5.36.1 * imagebuildah.StageExecutor.Execute: commit more "no instructions" cases * fix(deps): update module github.com/containers/storage to v1.59.1 * Only suppress "noted" items when not squashing * Reap stray processes * fix(deps): update github.com/containers/luksy digest to 8fccf78 * fix(deps): update module github.com/docker/docker to v28.3.3+incompatible * Restore the default meaning of `--pull` (should be `always`). * Test that pulled up parent directories are excluded at commit * Exclude pulled up parent directories at commit-time * copier.Ensure(): also return parent directories * copier.MkdirOptions: add ModTimeNew * fix(deps): update module github.com/containers/common to v0.64.0 * Bump to Buildah v1.42.0-dev * fix(deps): update module github.com/spf13/pflag to v1.0.7 * CI: make runc tests non-blocking * build,add: add support for corporate proxies - Changelog for v1.41.0 (2025-07-16) * Bump to c/storage v1.59.0, c/image v5.36.0, ... c/common v0.64.0 * stage_executor: check platform of cache candidates * fix(deps): update module golang.org/x/crypto to v0.40.0 * fix(deps): update module golang.org/x/term to v0.33.0 * fix(deps): update module golang.org/x/sync to v0.16.0 * fix(deps): update module github.com/docker/docker to v28.3.2+incompatible * ADD/COPY --link support added * RPM/TMT: account for passwd binary moving to tests * buildah: move passwd command to tests * Update "bud with --cpu-shares" test, and rename it * Remove BUILDTAG btrfs_noversion as no longer effective * fix(deps): update module github.com/docker/docker to v28.3.1+incompatible * fix(deps): update module github.com/moby/buildkit to v0.23.2 * fix(deps): update github.com/containers/luksy digest to bc60f96 * chore(typos): fix typos * vendor: update c/{common,image,storage} to main * chore(deps): update module github.com/go-viper/mapstructure/v2 to v2.3.0 [security] * fix(deps): update module go.etcd.io/bbolt to v1.4.2 * Update Neil Smith's GitHub username in MAINTAINERS.md * Accept SOURCE_DATE_EPOCH as a build-arg * fix(deps): update module github.com/docker/docker to v28.3.0+incompatible * Add conditional release-checking system test * info,inspect: use the "formats" package to get some builtins * Use containers/common's formats package instead of our own * build, commit: set the OCI ...created annotation on OCI images * commit: exclude parents of mount targets, too * run: clean up parents of mount targets, too * tarFilterer: always flush after writing * Builder: drop the TempVolumes field * Update module github.com/moby/buildkit to v0.23.1 * Update module github.com/opencontainers/cgroups to v0.0.3 * Add CommitOptions.OmitLayerHistoryEntry, for skipping the new bits * Update module github.com/fsouza/go-dockerclient to v1.12.1 * conformance: use mirrored frontend and base images * commit-with-extra-files test: use $TEST_SCRATCH_DIR * fix(deps): update module github.com/moby/buildkit to v0.23.0 * "root fs only mounted once" test: accept root with only the rw option * Run with --device /dev/fuse and not just -v /dev/fuse:/dev/fuse * CI: pass $BUILDAH_RUNTIME through to in-container test runs * CI: ensure rootless groups aren't duplicates * build: add support for --inherit-annotations * CI: give the rootless test user some supplemental groups * bud,run: runc does not support keep-groups * Fix lint issue in TestCommitCompression * Add a unit test for compression types in OCI images * Support zstd compression in image commit * fix(deps): update module go.etcd.io/bbolt to v1.4.1 * rpm: build rpm with libsqlite3 tag * Makefile: use libsqlite3 build when possible * commit,build: --source-date-epoch/--timestamp omit identity label * docs: add --setopt "*.countme=false" to dnf examples * Builder.sbomScan(): don't break non-root scanners * build: --source-date-epoch/--timestamp use static hostname/cid * fix(deps): update module golang.org/x/crypto to v0.39.0 * fix(deps): update module golang.org/x/sync to v0.15.0 * build: add --source-date-epoch and --rewrite-timestamp flags * build,config: add support for --unsetannotation * commit: add --source-date-epoch and --rewrite-timestamp flags * fix(deps): update module github.com/openshift/imagebuilder to v1.2.16 * vendor latest c/{common,image,storage} * Tweak our handling of variant values, again * Don't BuildRequires: ostree-devel * parse, validateExtraHost: honor Hostgateway in format * remove static nix build * Ensure extendedGlob returns paths in lexical order * CI: run integration tests on Fedora with both crun and runc * buildah-build(1): clarify that --cgroup-parent affects RUN instructions * runUsingRuntime: use named constants for runtime states * Add a dummy "runtime" that just dumps its config file * run: handle relabeling bind mounts ourselves * fix link to Maintainers file * Update to avoid deprecated types * fix(deps): update module github.com/docker/docker to v28.2.0+incompatible * [skip-ci] Packit: cleanup redundant targets and unused anchors * [skip-ci] Packit: set fedora-all after F40 EOL * Use Fedora 42 instead of 41 in that one conformance test * [CI:DOCS] README.md: add openssf passing badge * fix(deps): update module github.com/moby/buildkit to v0.22.0 * copier: add Ensure and ConditionalRemove * [CI:DOCS] update a couple of lists in the build man page * build: allow --output to be specified multiple times * add: add a new --timestamp flag * tests/helpers.bash: add some helpers for parsing images * pkg/parse.GetBuildOutput(): use strings.Cut() * [skip-ci] Packit: Disable osh_diff_scan * internal/util.SetHas(): handle maps of [generic]generic * Refactor NewImageSource to add a manifest type abstraction (#5743) * [skip-ci] Packit: Ignore ELN and CentOS Stream jobs * imagebuildah: select most recent layer for cache * [CI:DOCS] Add CNCF roadmap, touchup other CNCF files * fix(deps): update module golang.org/x/crypto to v0.38.0 * Fix typo in comment (#6167) * Support label_users in buildah * fix(deps): update module golang.org/x/sync to v0.14.0 * fix(deps): update github.com/containers/luksy digest to 4bb4c3f * test/serve: fix a descriptor leak, add preliminary directory support * fix(deps): update module github.com/opencontainers/cgroups to v0.0.2 * fix(deps): update module github.com/moby/buildkit to v0.21.1 * Update to avoid deprecated types * fix(deps): update module github.com/opencontainers/runc to v1.3.0 * Only filter if containerImageRef.created != nil * Drop superfluous cast * Remove UID/GID scrubbing. * fix(deps): update module github.com/seccomp/libseccomp-golang to v0.11.0 * cirrus: turn prior fedora testing back on * chore(deps): update dependency containers/automation_images to v20250422 * fix(deps): update module github.com/docker/docker to v28.1.1+incompatible * Bump to Buildah v1.41.0-dev * CI vendor_task: pin to go 1.23.3 for now * fix(deps): update module github.com/containers/common to v0.63.0 - Changelog for v1.40.0 (2025-04-17) * Bump c/storage to v1.58.0, c/image v5.35.0, c/common v0.63.0 * fix(deps): update module github.com/docker/docker to v28.1.0+incompatible * fix(deps): update module github.com/containers/storage to v1.58.0 * cirrus: make Total Success wait for rootless integration * chroot: use symbolic names when complaining about mount() errors * cli: hide the `completion` command instead of disabling it outright * Document rw and src options for --mount flag in buildah-run(1) * fix(deps): update module github.com/moby/buildkit to v0.21.0 * build: add support for inherit-labels * chore(deps): update dependency golangci/golangci-lint to v2.1.0 * .github: check_cirrus_cron work around github bug * stage_executor,getCreatedBy: expand buildArgs for sources correctly * Add a link to project governance and MAINTAINERS file * fix(deps): update github.com/containers/storage digest to b1d1b45 * generateHostname: simplify * Use maps.Copy * Use slices.Concat * Use slices.Clone * Use slices.Contains * Use for range over integers * tests/testreport: don't copy os.Environ * Use any instead of interface{} * ci: add golangci-lint run with --tests=false * ci: add nolintlint, fix found issues * copier: rm nolint:unparam annotation * .golangci.yml: add unused linter * chroot: fix unused warnings * copier: fix unused warnings * tests/conformance: fix unused warning * ci: switch to golangci-lint v2 * internal/mkcw: disable ST1003 warnings * tests/conformance: do not double import (fix ST1019) * cmd/buildah: don't double import (fix ST1019) * Do not capitalize error strings * cmd/buildah: do not capitalize error strings * tests/conformance: fix QF1012 warnings * tests/serve: fix QF1012 warning * Use strings.ReplaceAll to fix QF1004 warnings * Use switch to fix QF1003 warnings * Apply De Morgan's law to fix QF1001 warnings * Fix QF1007 staticcheck warnings * imagebuildah: fix revive warning * Rename max variable * tests/tools: install lint from binary, use renovate * fix(deps): update module github.com/containernetworking/cni to v1.3.0 * Update Buildah issue template to new version and support podman build * fix(deps): update module golang.org/x/crypto to v0.37.0 * stage_executor: reset platform in systemcontext for stages * fix(deps): update github.com/opencontainers/runtime-tools digest to 260e151 * cmd/buildah: rm unused containerOutputUsingTemplate * cmd/buildah: rm unused getDateAndDigestAndSize * build: return ExecErrorCodeGeneric when git operation fails * add: report error while creating dir for URL source. * createPlatformContainer: drop MS_REMOUNT|MS_BIND * fix(deps): update module github.com/docker/docker to v28.0.3+incompatible * fix: bats won't fail on ! without cleverness * feat: use HistoryTimestamp, if set, for oci-archive entries * Allow extendedGlob to work with Windows paths * fix(deps): update module github.com/moby/buildkit to v0.20.2 * fix(deps): update github.com/openshift/imagebuilder digest to e87e4e1 * fix(deps): update module github.com/docker/docker to v28.0.2+incompatible * fix(deps): update module tags.cncf.io/container-device-interface to v1.0.1 * chore(deps): update dependency containers/automation_images to v20250324 * vendor: update github.com/opencontainers/selinux to v1.12.0 * replace deprecated selinux/label calls * vendor: bump c/common to dbeb17e40c80 * Use builtin arg defaults from imagebuilder * linux: accept unmask paths as glob values * vendor: update containers/common * Add --parents option for COPY in Dockerfiles * fix(deps): update module github.com/opencontainers/runc to v1.2.6 * update go.sum from the previous commit * fix(deps): update module tags.cncf.io/container-device-interface to v1 * chore(deps): update module golang.org/x/net to v0.36.0 [security] * packit: remove f40 from copr builds * cirrus: update to go 1.23 image * vendor bump to golang.org/x/crypto v0.36.0 * cirrus: update PRIOR_FEDORA comment * github: remove cirrus rerun action * fix(deps): update module github.com/containers/common to v0.62.2 * fix(deps): update module github.com/containers/image/v5 to v5.34.2 * fix: close files properly when BuildDockerfiles exits * fix(deps): update module github.com/containers/storage to v1.57.2 * stage_executor: history should include heredoc summary correctly * fix(deps): update module github.com/containers/common to v0.62.1 * github: disable cron rerun action * fix(deps): update module github.com/moby/buildkit to v0.20.1 * internal/mkcw.Archive(): use github.com/containers/storage/pkg/ioutils * [skip-ci] TMT: system tests * buildah-build.1.md: secret examples * fix(deps): update github.com/containers/luksy digest to 40bd943 * fix(deps): update module github.com/opencontainers/image-spec to v1.1.1 * fix(deps): update module github.com/containers/image/v5 to v5.34.1 * Use UnparsedInstance.Manifest instead of ImageSource.GetManifest * fix(deps): update module github.com/opencontainers/runtime-spec to v1.2.1 * tests/conformance/testdata/Dockerfile.add: update some URLs * Vendor imagebuilder * Fix source of OS, architecture and variant * chore(deps): update module github.com/go-jose/go-jose/v4 to v4.0.5 [security] * fix(deps): update module tags.cncf.io/container-device-interface to v0.8.1 * fix(deps): update module github.com/moby/buildkit to v0.20.0 * chroot createPlatformContainer: use MS_REMOUNT * conformance: make TestCommit and TestConformance parallel * cirrus: reduce task timeout * mkcw: mkcw_check_image use bats run_with_log * test: use /tmp as TMPDIR * heredoc: create temp subdirs for each build * test: heredoc remove python dependency from test * Support the containers.conf container_name_as_hostname option * fix(deps): update module github.com/opencontainers/runc to v1.2.5 * fix(deps): update module github.com/spf13/cobra to v1.9.0 * .cirrus: use more cores for smoke * Switch to the CNCF Code of Conduct * .cirrus: bump ci resources * fix(deps): update module golang.org/x/crypto to v0.33.0 * Distinguish --mount=type=cache locations by ownership, too * fix(deps): update module golang.org/x/term to v0.29.0 * .cirrus: run -race only on non-PR branch * unit: deparallize some tests * .cirrus: use multiple cpu for unit tests * Makefile: use -parallel for go test * unit_test: use Parallel test where possible * Update module golang.org/x/sys to v0.30.0 * Update module golang.org/x/sync to v0.11.0 * Update dependency containers/automation_images to v20250131 * Bump to Buildah v1.40.0-dev - Changelog for v1.39.0 (2025-01-31) * Bump c/storage v1.57.1, c/image 5.34.0, c/common v0.62.0 * Update module github.com/containers/storage to v1.57.0 * CI, .cirrus: parallelize containerized integration * ed's comment: cleanup * use seperate blobinfocache for flaky test * bump CI VMs to 4 CPUs (was: 2) for integration tests * cleanup, debug, and disable parallel in blobcache tests * bats tests - parallelize * pkg/overlay: cleanups * RPM: include check section to silence rpmlint * RPM: use default gobuild macro on RHEL * tests: remove masked /sys/dev/block check * vendor to latest c/{common,image,storage} * build, run: record hash or digest in image history * Accept image names as sources for cache mounts * Run(): always clean up options.ExternalImageMounts * refactor: replace golang.org/x/exp with stdlib * Update to c/image @main * fix broken doc link * run_freebsd.go: only import runtime-spec once * fix(deps): update module github.com/docker/docker to v27.5.1+incompatible * bump github.com/vbatts/tar-split * Add more checks to the --mount flag parsing logic * chroot mount flags integration test: copy binaries * fix(deps): update module github.com/moby/buildkit to v0.19.0 * relabel(): correct a misleading parameter name * Fix TOCTOU error when bind and cache mounts use "src" values * define.TempDirForURL(): always use an intermediate subdirectory * internal/volume.GetBindMount(): discard writes in bind mounts * pkg/overlay: add a MountLabel flag to Options * pkg/overlay: add a ForceMount flag to Options * Add internal/volumes.bindFromChroot() * Add an internal/open package * fix(deps): update module github.com/containers/common to v0.61.1 * fix(deps): update module github.com/containers/image/v5 to v5.33.1 * [CI:DOCS] Touch up changelogs * fix(deps): update module github.com/docker/docker to v27.5.0+incompatible * copy-preserving-extended-attributes: use a different base image * fix(deps): update github.com/containers/luksy digest to a3a812d * chore(deps): update module golang.org/x/net to v0.33.0 [security] * fix(deps): update module golang.org/x/crypto to v0.32.0 * New VM Images * fix(deps): update module github.com/opencontainers/runc to v1.2.4 * fix(deps): update module github.com/docker/docker to v27.4.1+incompatible * fix(deps): update module github.com/containers/ocicrypt to v1.2.1 * Add support for --security-opt mask and unmask * Allow cache mounts to be stages or additional build contexts * [skip-ci] RPM: cleanup changelog conditionals * fix(deps): update module github.com/cyphar/filepath-securejoin to v0.3.6 * fix(deps): update module github.com/moby/buildkit to v0.18.2 * Fix an error message in the chroot unit test * copier: use .PAXRecords instead of .Xattrs * chroot: on Linux, try to pivot_root before falling back to chroot * manifest add: add --artifact-annotation * Add context to an error message * Update module golang.org/x/crypto to v0.31.0 * Update module github.com/opencontainers/runc to v1.2.3 * Update module github.com/docker/docker to v27.4.0+incompatible * Update module github.com/cyphar/filepath-securejoin to v0.3.5 * CI: don't build a binary in the unit tests task * CI: use /tmp for $GOCACHE * CI: remove dependencies on the cross-build task * CI: run cross-compile task with make -j * Update module github.com/docker/docker to v27.4.0-rc.4+incompatible * Update module github.com/moby/buildkit to v0.18.1 * Update module golang.org/x/crypto to v0.30.0 * Update golang.org/x/exp digest to 2d47ceb * Update github.com/opencontainers/runtime-tools digest to f7e3563 * [skip-ci] Packit: remove rhel copr build jobs * [skip-ci] Packit: switch to fedora-all for copr * Update module github.com/stretchr/testify to v1.10.0 * Update module github.com/moby/buildkit to v0.17.2 * Makefile: use `find` to detect source files * Tests: make _prefetch() parallel-safe * Update module github.com/opencontainers/runc to v1.2.2 * executor: allow to specify --no-pivot-root * Update module github.com/moby/sys/capability to v0.4.0 * Makefile: mv codespell config to .codespellrc * Fix some codespell errors * Makefile,install.md: rm gopath stuff * Makefile: rm targets working on .. * build: rm exclude_graphdriver_devicemapper tag * Makefile: rm unused var * Finish updating to go 1.22 * CI VMs: bump again * Bump to Buidah v1.39.0-dev * stage_executor: set avoidLookingCache only if mounting stage * imagebuildah: additionalContext is not a local built stage - Changelog for v1.38.0 (2024-11-08) * Bump to c/common v0.61.0, c/image v5.33.0, c/storage v1.56.0 * fix(deps): update module golang.org/x/crypto to v0.29.0 * fix(deps): update module github.com/moby/buildkit to v0.17.1 * fix(deps): update module github.com/containers/storage to v1.56.0 * tests: skip two ulimit tests * CI VMs: bump f40 -> f41 * tests/tools: rebuild tools when we change versions * tests/tools: update golangci-lint to v1.61.0 * fix(deps): update module github.com/moby/buildkit to v0.17.0 * Handle RUN --mount with relative targets and no configured workdir * tests: bud: make parallel-safe * fix(deps): update module github.com/opencontainers/runc to v1.2.1 * fix(deps): update golang.org/x/exp digest to f66d83c * fix(deps): update github.com/opencontainers/runtime-tools digest to 6c9570a * tests: blobcache: use unique image name * tests: sbom: never write to cwd * tests: mkcw: bug fixes, refactor * deps: bump runc to v1.2.0 * deps: switch to moby/sys/userns * tests/test_runner.sh: remove some redundancies * Integration tests: run git daemon on a random-but-bind()able port * fix(deps): update module github.com/opencontainers/selinux to v1.11.1 * go.mod: remove unnecessary replace * Document more buildah build --secret options * Add support for COPY --exclude and ADD --exclude options * fix(deps): update github.com/containers/luksy digest to e2530d6 * chore(deps): update dependency containers/automation_images to v20241010 * fix(deps): update module github.com/cyphar/filepath-securejoin to v0.3.4 * Properly validate cache IDs and sources * [skip-ci] Packit: constrain koji job to fedora package to avoid dupes * Audit and tidy OWNERS * fix(deps): update module golang.org/x/crypto to v0.28.0 * tests: add quotes to names * vendor: update c/common to latest * CVE-2024-9407: validate "bind-propagation" flag settings * vendor: switch to moby/sys/capability * Don't set ambient capabilities * Document that zstd:chunked is downgraded to zstd when encrypting * fix(deps): update module github.com/cyphar/filepath-securejoin to v0.3.3 * buildah-manifest-create.1: Fix manpage section * chore(deps): update dependency ubuntu to v24 * Make `buildah manifest push --all` true by default * chroot: add newlines at the end of printed error messages * Do not error on trying to write IMA xattr as rootless * fix: remove duplicate conditions * fix(deps): update module github.com/moby/buildkit to v0.16.0 * fix(deps): update module github.com/cyphar/filepath-securejoin to v0.3.2 * Document how entrypoint is configured in buildah config * In a container, try to register binfmt_misc * imagebuildah.StageExecutor: clean up volumes/volumeCache * build: fall back to parsing a TARGETPLATFORM build-arg * `manifest add --artifact`: handle multiple values * Packit: split out ELN jobs and reuse fedora downstream targets * Packit: Enable sidetags for bodhi updates * fix(deps): update module github.com/docker/docker to v27.2.1+incompatible * tests/bud.bats: add git source * add: add support for git source * Add support for the new c/common pasta options * vendor latest c/common * fix(deps): update module golang.org/x/term to v0.24.0 * fix(deps): update module github.com/fsouza/go-dockerclient to v1.12.0 * packit: update fedora and epel targets * cirrus: disable f39 testing * cirrus: fix fedora names * update to go 1.22 * Vendor c/common:9d025e4cb348 * copier: handle globbing with "**" path components * fix(deps): update golang.org/x/exp digest to 9b4947d * fix(deps): update github.com/containers/luksy digest to 2e7307c * imagebuildah: make scratch config handling toggleable * fix(deps): update module github.com/docker/docker to v27.2.0+incompatible * Add a validation script for Makefile $(SOURCES) * fix(deps): update module github.com/openshift/imagebuilder to v1.2.15 * New VMs * Update some godocs, use 0o to prefix an octal in a comment * buildah-build.1.md: expand the --layer-label description * fix(deps): update module github.com/containers/common to v0.60.2 * run: fix a nil pointer dereference on FreeBSD * CI: enable the whitespace linter * Fix some govet linter warnings * Commit(): retry committing to local storage on storage.LayerUnknown * CI: enable the gofumpt linter * conformance: move weirdly-named files out of the repository * fix(deps): update module github.com/docker/docker to v27.1.2+incompatible * fix(deps): update module github.com/containers/common to v0.60.1 * *: use gofmt -s, add gofmt linter * *: fix build tags * fix(deps): update module github.com/containers/image/v5 to v5.32.1 * Add(): re-escape any globbed items that included escapes * conformance tests: use mirror.gcr.io for most images * unit tests: use test-specific policy.json and registries.conf * fix(deps): update module golang.org/x/sys to v0.24.0 * Update to spun-out "github.com/containerd/platforms" * Bump github.com/containerd/containerd * test/tools/Makefile: duplicate the vendor-in-container target * linters: unchecked error * linters: don't end loop iterations with "else" when "then" would * linters: unused arguments shouldn't have names * linters: rename checkIdsGreaterThan5() to checkIDsGreaterThan5() * linters: don't name variables "cap" * `make lint`: use --timeout instead of --deadline * Drop the e2e test suite * fix(deps): update module golang.org/x/crypto to v0.26.0 * fix(deps): update module github.com/onsi/gomega to v1.34.1 * `make vendor-in-container`: use the caller's Go cache if it exists * fix(deps): fix test/tools ginkgo typo * fix(deps): update module github.com/onsi/ginkgo/v2 to v2.19.1 * Update to keep up with API changes in storage * fix(deps): update github.com/containers/luksy digest to 1f482a9 * install: On Debian/Ubuntu, add installation of libbtrfs-dev * fix(deps): update module golang.org/x/sys to v0.23.0 * fix(deps): update golang.org/x/exp digest to 8a7402a * fix(deps): update module github.com/fsouza/go-dockerclient to v1.11.2 * Use Epoch: 2 and respect the epoch in dependencies. * Bump to Buildah v1.38.0-dev * AddAndCopyOptions: add CertPath, InsecureSkipTLSVerify, Retry fields * Add PrependedLinkedLayers/AppendedLinkedLayers to CommitOptions * integration tests: teach starthttpd() about TLS and pid files - Changelog for v1.37.0 (2024-07-26) * Bump c/storage, c/image, c/common for v1.37.0 * "build with basename resolving user arg" tests: correct ARG use * bud-multiple-platform-no-run test: correct ARG use * imagebuildah: always have default values for $TARGET... args ready * bump github.com/openshift/imagebuilder to v1.2.14 * fix(deps): update module github.com/docker/docker to v27.1.1+incompatible * fix(deps): update module github.com/cyphar/filepath-securejoin to v0.3.1 * fix(deps): update module github.com/docker/docker to v27.1.0+incompatible * CI: use local registry, part 2 of 2 * CI: use local registry, part 1 of 2 * fix(deps): update module github.com/fsouza/go-dockerclient to v1.11.1 * Revert "fix(deps): update github.com/containers/image/v5 to v5.31.1" * Replace libimage.LookupReferenceFunc with the manifests version * conformance tests: enable testing CompatVolumes * conformance tests: add a test that tries to chown a volume * imagebuildah: make traditional volume handling not the default * StageExecutor.prepare(): mark base image volumes for preservation * fix(deps): update module github.com/containers/image/v5 to v5.31.1 * Vendor in latest containers/(common, storage, image) * fix(deps): update module golang.org/x/term to v0.22.0 * fix(deps): update module golang.org/x/sys to v0.22.0 * fix(deps): update golang.org/x/exp digest to 7f521ea * fix(deps): update github.com/containers/luksy digest to a8846e2 * imagebuildah.StageExecutor.Copy(): reject new flags for now * bump github.com/openshift/imagebuilder to v1.2.11 * Rework parsing of --pull flags * fix(deps): update module github.com/containers/image/v5 to v5.31.1 * imagebuildah.StageExecutor.prepare(): log the --platform flag * CI VMs: bump * buildah copy: preserve owner info with --from= a container or image * conformance tests: enable testing CompatSetParent * containerImageRef.NewImageSource(): move the FROM comment to first * commit: set "parent" for docker format only when requested * Update godoc for Builder.EnsureContainerPathAs * fix(deps): update module github.com/spf13/cobra to v1.8.1 * fix(deps): update module github.com/containernetworking/cni to v1.2.0 * fix(deps): update module github.com/opencontainers/runc to v1.1.13 * Change default for podman build to --pull missing * fix(deps): update module github.com/containers/common to v0.59.1 * Clarify definition of --pull options * buildah: fix a nil pointer reference on FreeBSD * Use /var/tmp for $TMPDIR for vfs conformance jobs * Cirrus: run `df` during job setup * conformance: use quay.io/libpod/centos:7 instead of centos:8 * Stop setting "parent" in docker format * conformance: check if workdir trims path separator suffixes * push integration test: pass password to docker login via stdin * Re-enable the "copy with chown" conformance test * healthcheck: Add support for `--start-interval` * fix(deps): update module github.com/docker/docker to v26.1.4+incompatible * fix(deps): update module github.com/containerd/containerd to v1.7.18 * tests: set _CONTAINERS_USERNS_CONFIGURED=done for libnetwork * Cross-build on Fedora * Drop copyStringSlice() and copyStringStringMap() * fix(deps): update module golang.org/x/crypto to v0.24.0 * fix(deps): update module github.com/openshift/imagebuilder to v1.2.10 * Provide an uptime_netbsd.go * Spell unix as "!windows" * Add netbsd to lists-of-OSes * fix(deps): update golang.org/x/exp digest to fd00a4e * [skip-ci] Packit: enable c10s downstream sync * CI VMs: bump, to debian with cgroups v2 * Document when BlobDirectory is overridden * fix secret mounts for env vars when using chroot isolation * Change to take a types.ImageReference arg * imagebuildah: Support custom image reference lookup for cache push/pull * fix(deps): update module github.com/onsi/ginkgo/v2 to v2.19.0 * Bump to v1.37.0-dev * CI: Clarify Debian use for conformance tests - Changelog for v1.36.0 (2024-05-23) * build: be more selective about specifying the default OS * Bump to c/common v0.59.0 * Fix buildah prune --help showing the same example twice * fix(deps): update module github.com/onsi/ginkgo/v2 to v2.18.0 * fix(deps): update module github.com/containers/image/v5 to v5.31.0 * bud tests: fix breakage when vendoring into podman * Integration tests: fake up a replacement for nixery.dev/shell * copierWithSubprocess(): try to capture stderr on io.ErrClosedPipe * Don't expand RUN heredocs ourselves, let the shell do it * Don't leak temp files on failures * Add release note template to split dependency chores * fix CentOS/RHEL build - no BATS there * fix(deps): update module github.com/containers/luksy to v0.0.0-20240506205542-84b50f50f3ee * Address CVE-2024-3727 * chore(deps): update module github.com/opencontainers/runtime-spec to v1.2.0 * Builder.cdiSetupDevicesInSpecdefConfig(): use configured CDI dirs * Setting --arch should set the TARGETARCH build arg * fix(deps): update module golang.org/x/exp to v0.0.0-20240416160154-fe59bbe5cc7f * [CI:DOCS] Add link to Buildah image page to README.md * Don't set GOTOOLCHAIN=local * fix(deps): update module github.com/cyphar/filepath-securejoin to v0.2.5 * Makefile: set GOTOOLCHAIN=local * Integration tests: switch some base images * containerImageRef.NewImageSource: merge the tar filters * fix(deps): update module github.com/onsi/ginkgo/v2 to v2.17.2 * fix(deps): update module github.com/containers/luksy to v0.0.0-20240408185936-afd8e7619947 * Disable packit builds for centos-stream+epel-next-8 * Makefile: add missing files to $(SOURCES) * CI VMs: bump to new versions with tmpfs /tmp * chore(deps): update module golang.org/x/net to v0.23.0 [security] * integration test: handle new labels in "bud and test --unsetlabel" * Switch packit configuration to use epel-9-$arch ... * Give unit tests a bit more time * Integration tests: remove a couple of duplicated tests * Integration tests: whitespace tweaks * Integration tests: don't remove images at start or end of test * Integration tests: use cached images more * Integration tests _prefetch: use registry configs * internal: use fileutils.(Le|E)xists * pkg/parse: use fileutils.(Le|E)xists * buildah: use fileutils.(Le|E)xists * chroot: use fileutils.(Le|E)xists * vendor: update containers/(common|storage) * Fix issue/pr lock workflow * [CI:DOCS] Add golang 1.21 update warning * heredoc: honor inline COPY irrespective of ignorefiles * Update install.md * source-push: add support for --digestfile * Fix caching when mounting a cached stage with COPY/ADD * fix(deps): update github.com/containers/luksy digest to 3d2cf0e * Makefile: softcode `strip`, use it from env var * Man page updates * Add support for passing CDI specs to --device * Update comments on some API objects * pkg/parse.DeviceFromPath(): dereference src symlinks * fix(deps): update module github.com/onsi/ginkgo/v2 to v2.17.1 - Changelog for v1.35.0 (2024-03-06) * fix(deps): update module github.com/stretchr/testify to v1.9.0 * cgroups: reuse version check from c/common * Update vendor of containers/(common,image) * fix(deps): update github.com/containers/storage digest to eadc620 * fix(deps): update github.com/containers/luksy digest to ceb12d4 * fix(deps): update github.com/containers/image/v5 digest to cdc6802 * manifest add: complain if we get artifact flags without --artifact * Use retry logic from containers/common * Vendor in containers/(storage,image,common) * Update module golang.org/x/crypto to v0.20.0 * Add comment re: Total Success task name * tests: skip_if_no_unshare(): check for --setuid * Properly handle build --pull=false * [skip-ci] Update tim-actions/get-pr-commits action to v1.3.1 * Update module go.etcd.io/bbolt to v1.3.9 * Revert "Reduce official image size" * Update module github.com/opencontainers/image-spec to v1.1.0 * Reduce official image size * Build with CNI support on FreeBSD * build --all-platforms: skip some base "image" platforms * Bump main to v1.35.0-dev * Vendor in latest containers/(storage,image,common) * Split up error messages for missing --sbom related flags * `buildah manifest`: add artifact-related options * cmd/buildah/manifest.go: lock lists before adding/annotating/pushing * cmd/buildah/manifest.go: don't make struct declarations aliases * Use golang.org/x/exp/slices.Contains * Disable loong64 again * Fix a couple of typos in one-line comments * egrep is obsolescent; use grep -E * Try Cirrus with a newer VM version * Set CONTAINERS_CONF in the chroot-mount-flags integration test * Update to match dependency API update * Update github.com/openshift/imagebuilder and containers/common * docs: correct default authfile path * fix(deps): update module github.com/containerd/containerd to v1.7.13 * tests: retrofit test for heredoc summary * build, heredoc: show heredoc summary in build output * manifest, push: add support for --retry and --retry-delay * fix(deps): update github.com/openshift/imagebuilder digest to b767bc3 * imagebuildah: fix crash with empty RUN * fix(deps): update github.com/containers/luksy digest to b62d551 * fix(deps): update module github.com/opencontainers/runc to v1.1.12 [security] * fix(deps): update module github.com/moby/buildkit to v0.12.5 [security] * Make buildah match podman for handling of ulimits * docs: move footnotes to where they're applicable * Allow users to specify no-dereference * Run codespell on code * Fix FreeBSD version parsing * Fix a build break on FreeBSD * Remove a bad FROM line * fix(deps): update module github.com/onsi/gomega to v1.31.1 * fix(deps): update module github.com/opencontainers/image-spec to v1.1.0-rc6 * docs: use reversed logo for dark theme in README * build,commit: add --sbom to scan and produce SBOMs when committing * commit: force omitHistory if the parent has layers but no history * docs: fix a couple of typos * internal/mkcw.Archive(): handle extra image content * stage_executor,heredoc: honor interpreter in heredoc * stage_executor,layers: burst cache if heredoc content is changed * fix(deps): update module golang.org/x/crypto to v0.18.0 * Replace map[K]bool with map[K]struct{} where it makes sense * fix(deps): update module golang.org/x/sync to v0.6.0 * fix(deps): update module golang.org/x/term to v0.16.0 * Bump CI VMs * Replace strings.SplitN with strings.Cut * fix(deps): update github.com/containers/storage digest to ef81e9b * fix(deps): update github.com/containers/image/v5 digest to 1b221d4 * fix(deps): update module github.com/fsouza/go-dockerclient to v1.10.1 * Document use of containers-transports values in buildah * fix(deps): update module golang.org/x/crypto to v0.17.0 [security] * chore(deps): update dependency containers/automation_images to v20231208 * manifest: addCompression use default from containers.conf * commit: add a --add-file flag * mkcw: populate the rootfs using an overlay * chore(deps): update dependency containers/automation_images to v20230517 * [skip-ci] Update actions/stale action to v9 * fix(deps): update module github.com/containernetworking/plugins to v1.4.0 * fix(deps): update github.com/containers/image/v5 digest to 7a40fee * Bump to v1.34.1-dev * Ignore errors if label.Relabel returns ENOSUP - Changelog for v1.34.0 (2023-12-11) * vendor: update c/{common,image,storage} * run: Allow using just one jail per container on FreeBSD * Remove makefile targets entrypoint{,.gz} for non x86_64 - Changelog for v1.33.2 (2023-11-22) * Update minimum to golang 1.20 * fix(deps): update module github.com/fsouza/go-dockerclient to v1.10.0 * fix(deps): update module github.com/moby/buildkit to v0.12.3 * Bump to v1.33.2-dev - Changelog for v1.33.1 (2023-11-18) * fix(deps): update module github.com/moby/buildkit to v0.11.4 [security] * test,heredoc: use fedora instead of docker.io/library/python:latest * Bump to v1.33.1-dev - Changelog for v1.33.0 (2023-11-17) * Never omit layers for emptyLayer instructions when squashing/cwing * Add OverrideChanges and OverrideConfig to CommitOptions * buildah: add heredoc support for RUN, COPY and ADD * vendor: bump imagebuilder to v1.2.6-0.20231110114814-35a50d57f722 * conformance tests: archive the context directory as 0:0 (#5171) * blobcacheinfo,test: blobs must be resued when pushing across registry * Bump c/storage v1.51.0, c/image v5.29.0, c/common v0.57.0 * pkg/util.MirrorToTempFileIfPathIsDescriptor(): don't leak an fd * StageExecutor.Execute: force a commit for --unsetenv, too * Increase a copier+chroot test timeout * Add support for --compat-auth-file in login/logout * Update existing tests for error message change * Update c/image and c/common to latest * fix(deps): update module github.com/containerd/containerd to v1.7.9 * build: downgrade to go 1.20 * Add godoc for pkg/parse.GetTempDir * conformance tests: use go-dockerclient for BuildKit builds * Make TEE types case-insensitive * fix(deps): update module golang.org/x/crypto to v0.15.0 * Tweak some help descriptions * Stop using DefaultNetworkSysctl and use containers.conf only * Implement ADD checksum flag #5135 * vendor of openshift/imagebuilder #5135 * Pass secrets from the host down to internal podman containers * Update cirrus and version of golang * image: replace GetStoreImage with ResolveReference * vendor: bump c/image to 373c52a9466f * pkg/parse.Platform(): minor simplification * createConfigsAndManifests: clear history before cw-specific logic * Use a constant definition instead of "scratch" * conformance: use require.NoErrorf() more * fix(deps): update module golang.org/x/term to v0.14.0 * fix(deps): update module golang.org/x/sync to v0.5.0 * fix(deps): update module github.com/spf13/cobra to v1.8.0 * fix(deps): update module golang.org/x/sys to v0.14.0 * fix(deps): update github.com/containers/common digest to 8354404 * fix(deps): update module github.com/opencontainers/runc to v1.1.10 * fix(deps): update github.com/containers/luksy digest to b5a7f79 * Log the platform for build errors during multi-platform builds * Use mask definitions from containers/common * Vendor in latest containers/common * fix(deps): update module github.com/containerd/containerd to v1.7.8 * fix(deps): update module go.etcd.io/bbolt to v1.3.8 * container.conf: support attributed string slices * fix(deps): update module sigs.k8s.io/yaml to v1.4.0 * Use cutil.StringInSlice rather then contains * Add --no-hostname option to buildah containers * vendor c/common: appendable containers.conf strings, Part 1 * fix(deps): update module github.com/onsi/gomega to v1.28.1 * chroot.setupChrootBindMounts: pay more attention to flags * chore(deps): update dependency containers/automation_images to v20231004 * Vendor containers/common * chore(deps): update module golang.org/x/net to v0.17.0 [security] * run: use internal.GetTempDir with os.MkdirTemp * fix(deps): update module github.com/containerd/containerd to v1.7.7 * imagebuildah,multi-stage: do not remove base images * gitignore: add mkcw binary * mkcw: remove entrypoint binaries * fix(deps): update module golang.org/x/crypto to v0.14.0 * fix(deps): update module golang.org/x/sys to v0.13.0 * fix(deps): update module golang.org/x/sync to v0.4.0 * Update some comments related to confidential workload * Use the parent's image ID in the config that we pass to imagebuilder * fix(deps): update github.com/containers/common digest to 8892536 * fix(deps): update github.com/containers/luksy digest to 6df88cb * bug: Ensure the mount type is always BindMount by default * Protocol can be specified with --port. Ex. --port 514/udp * fix(deps): update module github.com/onsi/gomega to v1.28.0 * build,config: add support for --unsetlabel * tests/bud: add tests * [CI:BUILD] Packit: tag @containers/packit-build on copr build failures * stage_executor: allow images without layers * vendor of containers/common * Removing selinux_tag.sh as no longer needed after 580356f [NO NEW TESTS NEEDED] * add/copy: make sure we handle relative path names correctly * fix(deps): update module github.com/opencontainers/image-spec to v1.1.0-rc5 * Bump to v1.33.0-dev * imagebuildah: consider ignorefile with --build-context - Changelog for v1.32.0 (2023-09-14) * GetTmpDir is not using ImageCopyTmpdir correctly * Run codespell on code * Bump vendor containers/(common, storage, image) * Cirrus: Remove multi-arch buildah image builds * fix(deps): update module github.com/containerd/containerd to v1.7.6 * Split GetTempDir from internal/util * Move most of internal/parse to internal/volumes * copier: remove libimage dependency via util package * Add some docs for `build --cw`, `commit --cw`, and `mkcw` * Add `buildah mkcw`, add `--cw` to `buildah commit` and `buildah build` * Make sure that pathnames picked up from the environment are absolute * fix(deps): update module github.com/cyphar/filepath-securejoin to v0.2.4 * fix(deps): update module github.com/docker/docker to v24.0.6+incompatible * Don't try to look up names when committing images * fix(deps): update module golang.org/x/crypto to v0.13.0 * docs: use valid github repo * fix(deps): update module golang.org/x/sys to v0.12.0 * vendor containers/common@12405381ff45 * push: --force-compression should be true with --compression-format * Update module github.com/containerd/containerd to v1.7.5 * [skip-ci] Update tim-actions/commit-message-checker-with-regex action to v0.3.2 * docs: add reference to oci-hooks * Support passing of ULimits as -1 to mean max * GHA: Attempt to fix discussion_lock workflow * Fixing the owner of the storage.conf. * pkg/chrootuser: Ignore comments when parsing /etc/group on FreeBSD * Use buildah repo rather then podman repo * GHA: Closed issue/PR comment-lock test * fix(deps): update module github.com/containers/storage to v1.49.0 * chore(deps): update dependency containers/automation_images to v20230816 * Replace troff code with markdown in buildah-{copy,add}.1.md * [CI:BUILD] rpm: spdx compatible license field * executor: build-arg warnings must honor global args * fix(deps): update module github.com/containers/ocicrypt to v1.1.8 * chroot: `setSeccomp` add support for `ArchPARISC(64)` and `ArchRISCV64` * make,cross: restore loong64 * Clear CommonBuildOpts when loading Builder status * buildah/push/manifest-push: add support for --force-compression * vendor: bump c/common to v0.55.1-0.20230811093040-524b4d5c12f9 * chore(deps): update dependency containers/automation_images to v20230809 * [CI:BUILD] RPM: fix buildtags * fix(deps): update module github.com/opencontainers/runc to v1.1.9 * chore(deps): update dependency ubuntu to v22 * chore(deps): update dependency containers/automation_images to v20230807 * [CI:BUILD] Packit: add fedora-eln targets * [CI:BUILD] RPM: build docs with vendored go-md2man * packit: Build PRs into default packit COPRs * Update install.md * Update install.md changes current Debian stable version name * fix(deps): update module golang.org/x/term to v0.11.0 * fix(deps): update module golang.org/x/crypto to v0.12.0 * tests: fix layer-label tests * buildah: add --layer-label for setting labels on layers * Cirrus: container/rootless env. var. passthrough * Cirrus: Remove duplicate env. var. definitions * fix(deps): update github.com/containers/storage digest to c3da76f * Add a missing .Close() call on an ImageSource * Create only a reference when that's all we need * Add a missing .Close() call on an ImageDestination * CI:BUILD] RPM: define gobuild macro for rhel/centos stream * manifest/push: add support for --add-compression * manifest/inspect: add support for tls-verify and authfile * vendor: bump c/common to v0.55.1-0.20230727095721-647ed1d4d79a * vendor: bump c/image to v5.26.1-0.20230726142307-8c387a14f4ac * fix(deps): update module github.com/containerd/containerd to v1.7.3 * fix(deps): update module github.com/onsi/gomega to v1.27.10 * fix(deps): update module github.com/docker/docker to v24.0.5+incompatible * fix(deps): update module github.com/containers/image/v5 to v5.26.1 * fix(deps): update module github.com/opencontainers/runtime-spec to v1.1.0 * Update vendor of containers/(storage,image,common) * fix(deps): update module github.com/opencontainers/runc to v1.1.8 * [CI:BUILD] Packit: remove pre-sync action * fix(deps): update module github.com/containers/common to v0.55.2 * [CI:BUILD] Packit: downstream task script needs GOPATH * Vendor in containers/(common, image, storage) * fix(deps): update module golang.org/x/term to v0.10.0 * [CI:BUILD] Packit: fix pre-sync action for downstream tasks * contrib/buildahimage: set config correctly for rootless build user * fix(deps): update module github.com/opencontainers/image-spec to v1.1.0-rc4 * Bump to v1.32.0-dev * Update debian install instructions * pkg/overlay: add limited support for FreeBSD - Changelog for v1.31.0 (2023-06-30) * Bump c/common to 0.55.1 and c/image to 5.26.1 * Bump c/image to 5.26.0 and c/common to 0.54.0 * vendor: update c/{common,image,storage} to latest * chore: pkg imported more than once * buildah: add pasta(1) support * use slirp4netns package from c/common * update c/common to latest * add hostname to /etc/hosts when running with host network * vendor: update c/common to latest * [CI:BUILD] Packit: add jobs for downstream Fedora package builds * fix(deps): update module golang.org/x/sync to v0.3.0 * fix(deps): update module golang.org/x/crypto to v0.10.0 * Add smoke tests for encryption CLI helpers * fix(deps): update module golang.org/x/term to v0.9.0 * fix(deps): update module github.com/opencontainers/runtime-spec to v1.1.0-rc.3 * Remove device mapper support * Remove use of deprecated tar.TypeRegA * Update tooling to support newer golangci-lint * Make cli.EncryptConfig,DecryptConfig, GetFormat public * Don't decrypt images by default * fix(deps): update module github.com/onsi/gomega to v1.27.8 * fix(deps): update github.com/containers/storage digest to 3f3fb2f * Renovate: Don't touch fragile test stuffs * [CI:DOCS] Update comment to remove ambiguity * fix(deps): update github.com/containers/image/v5 digest to abe5133 * fix(deps): update module github.com/sirupsen/logrus to v1.9.3 * fix(deps): update module github.com/containerd/containerd to v1.7.2 * Explicitly ref. quay images for CI * At startup, log the effective capabilities for debugging * parse: use GetTempDir from internal utils * GetTmpDir: honor image_copy_tmp_dir from containers.conf * docs/Makefile: don't show sed invocations * CI: Support testing w/ podman-next COPR packages * intermediate-images inherit-label test: make it debuggable * fix(deps): update github.com/containers/common digest to 462ccdd * Add a warning to `--secret` docs * vendor: bump c/storage to v1.46.2-0.20230526114421-55ee2d19292f * executor: apply label to only final stage * remove registry.centos.org * Go back to setting SysProcAttr.Pdeathsig for child processes * Fix auth.json path (validated on Fedora 38) wq Signed-off-by: Andreas Mack * fix(deps): update module github.com/stretchr/testify to v1.8.3 * CI: fix test broken by renovatebot * chore(deps): update quay.io/libpod/testimage docker tag to v20221018 * fix(deps): update module github.com/onsi/gomega to v1.27.7 * test: use debian instead of docker.io/library/debian:testing-slim * vendor: bump logrus to 1.9.2 * [skip-ci] Update tim-actions/get-pr-commits action to v1.3.0 * Revert "Proof of concept: nightly dependency treadmill" * fix(deps): update module github.com/sirupsen/logrus to v1.9.1 * vendor in containers/(common,storage,image) * fix(deps): update module github.com/docker/distribution to v2.8.2+incompatible * run: drop Pdeathsig * chroot: lock thread before setPdeathsig * tests: add a case for required=false * fix(deps): update module github.com/openshift/imagebuilder to v1.2.5 * build: validate volumes on backend * secret: accept required flag w/o value * fix(deps): update module github.com/containerd/containerd to v1.7.1 * fix(deps): update module golang.org/x/crypto to v0.9.0 * Update the demos README file to fix minor typos * fix(deps): update module golang.org/x/sync to v0.2.0 * fix(deps): update module golang.org/x/term to v0.8.0 * manifest, push: use source as destination if not specified * run,mount: remove path only if they didnt pre-exist * Cirrus: Fix meta task failing to find commit * parse: filter edge-case for podman-remote * fix(deps): update module github.com/opencontainers/runc to v1.1.7 * fix(deps): update module github.com/docker/docker to v23.0.5+incompatible * build: --platform must accept only arch * fix(deps): update module github.com/containers/common to v0.53.0 * makefile: increase conformance timeout * Cap suffixDigitsModulo to a 9-digits suffix. * Rename conflict to suffixDigitsModulo * fix(deps): update module github.com/opencontainers/runtime-spec to v1.1.0-rc.2 * fix(deps): update module github.com/opencontainers/runc to v1.1.6 * chore(deps): update centos docker tag to v8 * Clarify the need for qemu-user-static package * chore(deps): update quay.io/centos/centos docker tag to v8 * Renovate: Ensure test/tools/go.mod is managed * Revert "buildah image should not enable fuse-overlayfs for rootful mode" * Bump to v1.31.0-dev * parse: add support for relabel bind mount option - Changelog for v1.30.0 (2023-04-06) * fix(deps): update module github.com/opencontainers/runc to v1.1.5 * fix(deps): update module github.com/fsouza/go-dockerclient to v1.9.7 * buildah image should not enable fuse-overlayfs for rootful mode * stage_executor: inline network add default string * fix(deps): update module github.com/containers/common to v0.51.2 * chore(deps): update dependency containers/automation_images to v20230330 * fix(deps): update module github.com/docker/docker to v23.0.2+incompatible * chore(deps): update dependency containers/automation_images to v20230320 * fix(deps): update module github.com/onsi/gomega to v1.27.6 * fix(deps): update github.com/opencontainers/runtime-tools digest to e931285 * [skip-ci] Update actions/stale action to v8 * test: don't allow to override io.buildah.version * executor: only apply label on the final stage * Update docs/buildah-build.1.md * update build instruction for Ubuntu * code review * build: accept arguments from file with --build-arg-file * run_linux: Update heuristic for mounting /sys * [CI:BUILD] Packit: Enable Copr builds on PR and commit to main * fix(deps): update module github.com/fsouza/go-dockerclient to v1.9.6 * Update to Go 1.18 * Disable dependabot in favor of renovate * chore(deps): update dependency containers/automation_images to v20230314 * Fix requiring tests on Makefile changes * Vendor in latest containers/(storage, common, image) * imagebuildah: set len(short_image_id) to 12 * Re-enable conformance tests * Skip conformance test failures with Docker 23.0.1 * Cirrus: Replace Ubuntu -> Debian SID * run: add support for inline --network in RUN stmt * vendor: bump imagebuilder to a3c3f8358ca31b1e4daa6 * stage_executor: attempt to push cache only when cacheKey is valid * Add "ifnewer" as option in help message for pull command * build: document behaviour of buildah's distributed cache * fix(deps): update module golang.org/x/term to v0.6.0 * Add default list of capabilities required to run buildah in a container * executor,copy: honor default ARG value while eval stage * sshagent: use ExtendedAgent instead of Agent * tests/bud: remove unwated test * executor: do not warn on builtin default args * executor: don't warn about unused TARGETARCH,TARGETOS,TARGETPLATFORM * Fix tutorial for rootless mode * Vendor in latest containers/(common, storage, image) * Ignore the base image's base image annotations * fix(deps): update module github.com/fsouza/go-dockerclient to v1.9.5 * build(deps): bump github.com/containers/storage from 1.45.3 to 1.45.4 * Vendor in latest containers/common * docs/tutorials/04: add defaults for Run() * imagebuildah.StageExecutor: suppress bogus "Pushing cache []:..." * executor: also add stage with no children to cleanupStages * [CI:BUILD] copr: fix el8 builds * Fix documentation on which Capabilities are allowed by default * Skip subject-length validation for renovate PRs * Temporarily hard-skip bud-multiple-platform-values test * fix(deps): update github.com/openshift/imagebuilder digest to 86828bf * build(deps): bump github.com/containerd/containerd from 1.6.16 to 1.6.17 * build(deps): bump tim-actions/get-pr-commits from 1.1.0 to 1.2.0 * build(deps): bump github.com/containers/image/v5 from 5.24.0 to 5.24.1 * [skip-ci] Update tim-actions/get-pr-commits digest to 55b867b * build(deps): bump github.com/opencontainers/selinux * build(deps): bump golang.org/x/crypto from 0.5.0 to 0.6.0 * Add renovate configuration * Run codespell on codebase * login: support interspersed args for password * conformance: use scratch for minimal test * pkg/parse: expose public CleanCacheMount API * build(deps): bump go.etcd.io/bbolt from 1.3.6 to 1.3.7 * build(deps): bump github.com/containerd/containerd from 1.6.15 to 1.6.16 * docs: specify order preference for FROM * Bump to v1.30.0-dev - Changelog for v1.29.0 (2023-01-25) * tests: improve build-with-network-test * Bump c/storagev1.45.3, c/imagev5.24.0, c/commonv0.51.0 * build(deps): bump github.com/onsi/gomega from 1.25.0 to 1.26.0 * Flake 3710 has been closed. Reenable the test. * [CI:DOCS] Fix two diversity issues in a tutorial * build(deps): bump github.com/fsouza/go-dockerclient from 1.9.2 to 1.9.3 * vendor in latests containers/(storage, common, image) * fix bud-multiple-platform-with-base-as-default-arg flake * stage_executor: while mounting stages use freshly built stage * build(deps): bump github.com/fsouza/go-dockerclient from 1.9.0 to 1.9.2 * build(deps): bump github.com/onsi/gomega from 1.24.2 to 1.25.0 * vendor in latests containers/(storage, common, image, ocicyrpt) * [Itests: change the runtime-flag test for crun * [CI:DOCS] README: drop sudo * Fix multi-arch manifest-list build timeouts * Cirrus: Update VM Images * bud: Consolidate multiple synthetic LABEL instructions * build, secret: allow realtive mountpoints wrt to work dir * fixed squash documentation * build(deps): bump github.com/containerd/containerd from 1.6.14 to 1.6.15 * Correct minor comment * Vendor in latest containers/(common, image, storage) * system tests: remove unhelpful assertions * buildah: add prune command and expose CleanCacheMount API * vendor: bump c/storage to a747b27 * Add support for --group-add to buildah from * build(deps): bump actions/stale from 6 to 7 * Add documentation for buildah build --pull=missing * build(deps): bump github.com/containerd/containerd from 1.6.12 to 1.6.14 * build(deps): bump github.com/docker/docker * parse: default ignorefile must not point to symlink outside context * buildah: wrap network setup errors * build, mount: allow realtive mountpoints wrt to work dir * Update to F37 CI VM Images, re-enable prior-fedora * Update vendor or containers/(image, storage, common) * build(deps): bump golang.org/x/crypto from 0.3.0 to 0.4.0 * Update contact information * build(deps): bump golang.org/x/term from 0.2.0 to 0.3.0 * Replace io/ioutil calls with os calls * [skip-ci] GHA/Cirrus-cron: Fix execution order * Vendor in containers/common * build(deps): bump golang.org/x/sys from 0.2.0 to 0.3.0 * remote-cache: support multiple sources and destinations * Update c/storage after https://github.com/containers/storage/pull/1436 * util.SortMounts(): make the returned order more stable * version: Bump to 1.29.0-dev * [CI:BUILD] Cirrus: Migrate OSX task to M1 * Update vendor of containers/(common, storage, image) * mount=type=cache: seperate cache parent on host for each user * Fix installation instructions for Gentoo Linux * build(deps): bump github.com/containerd/containerd from 1.6.9 to 1.6.10 * GHA: Reuse both cirrus rerun and check workflows * Vendor in latest containers/(common,image,storage) * build(deps): bump github.com/onsi/gomega from 1.24.0 to 1.24.1 * copier.Put(): clear up os/syscall mode bit confusion * build(deps): bump golang.org/x/sys from 0.1.0 to 0.2.0 * Use TypeBind consistently to name bind/nullfs mounts * Add no-new-privileges flag * Update vendor of containers/(common, image, storage) * imagebuildah:build with --all-platforms must honor args for base images * codespell code * Expand args and env when using --all-platforms * build(deps): bump github.com/onsi/gomega from 1.23.0 to 1.24.0 * GHA: Simplify Cirrus-Cron check slightly * Stop using ubi8 * remove unnecessary (hence misleading) rmi * chroot: fix mounting of ro bind mounts * executor: honor default ARG value while eval base name * userns: add arbitrary steps/stage to --userns=auto test * Don't set allow.mount in the vnet jail on Freebsd * copier: Preserve file flags when copying archives on FreeBSD * Remove quiet flag, so that it works in podman-remote * test: fix preserve rootfs with --mount for podman-remote * test: fix prune logic for cache-from after adding content summary * vendor in latest containers/(storage, common, image) * Fix RUN --mount=type=bind,from= not preserving rootfs of stage * Define and use a safe, reliable test image * Fix word missing in Container Tools Guide * Makefile: Use $(MAKE) to start sub-makes in install.tools * imagebuildah: pull cache from remote repo after adding content summary * Makefile: Fix install on FreeBSD * Ensure the cache volume locks are unlocked on all paths * Vendor in latest containers/(common,storage) * Simplify the interface of GetCacheMount and getCacheMount * Fix cache locks with multiple mounts * Remove calls to Lockfile.Locked() * Maintain cache mount locks as lock objects instead of paths * test: cleaning cache must not clean lockfiles * run: honor lockfiles for multiple --mount instruction * mount,cache: lockfiles must not be part of users cache content * Update vendor containers/(common,image,storage) * [CI:BUILD] copr: buildah rpm should depend on containers-common-extra * pr-should-include-tests: allow specfile, golangci * build(deps): bump dawidd6/action-send-mail from 3.7.0 to 3.7.1 * build(deps): bump github.com/docker/docker * build(deps): bump github.com/fsouza/go-dockerclient from 1.8.3 to 1.9.0 * Update vendor containers/(common,image,storage) * build(deps): bump actions/upload-artifact from 2 to 3 * build(deps): bump actions/checkout from 2 to 3 * build(deps): bump actions/stale from 1 to 6 * build(deps): bump dawidd6/action-send-mail from 2.2.2 to 3.7.0 * build(deps): bump tim-actions/get-pr-commits from 1.1.0 to 1.2.0 * sshagent: LockOSThread before setting SocketLabel * Update tests for error message changes * Update c/image after https://github.com/containers/image/pull/1299 * Fix ident for dependabot gha block * build(deps): bump github.com/containers/ocicrypt from 1.1.5 to 1.1.6 * Fix man pages to match latest cobra settings * build(deps): bump github.com/spf13/cobra from 1.5.0 to 1.6.0 * build(deps): bump github.com/onsi/gomega from 1.20.2 to 1.22.1 * test: retrofit 'bud with undefined build arg directory' * imagebuildah: warnOnUnsetBuildArgs while processing stages from executor * Update contrib/buildahimage/Containerfile * Cirrus CI add flavor parameter * Correction - `FLAVOR` not `FLAVOUR` * Changed build argument from `RELEASE` to `FLAVOUR` * Combine buildahimage Containerfiles * bud.bats refactoring: $TEST_SCRATCH_DIR, part 2 of 2 * bud.bats refactoring: $TEST_SCRATCH_DIR, part 1 of 2 * System test cleanup: document, clarify, fix * test: removing unneeded/expensive COPY * test: warning behaviour for unset/set TARGETOS,TARGETARCH,TARGETPLATFORM * Bump to v1.28.1-dev - Changelog for v1.28.0 (2022-09-30) * Update vendor containers/(common,image) * [CI:DOCS] Add quay-description update reminder * vendor: bump c/common to v0.49.2-0.20220929111928-2d1b45ae2423 * build(deps): bump github.com/opencontainers/selinux * Vendor in latest containers/storage * Changing shell list operators from `;` to `&&` * Fix buildahimage container.conf permissions regression * Set sysctls from containers.conf * refactor: stop using Normalize directly from containerd package * config,builder: process variant while populating image spec * Proof of concept: nightly dependency treadmill * Run codespell on code * Check for unset build args after TARGET args * pkg/cli: improve completion test * vendor in latest containers/(common,storage,image) * copier: work around freebsd bug for "mkdir /" * vendor: update c/image * test: run in the host cgroup namespace * vendor: update c/storage * vendor: update c/common * cmd: check for user UID instead of privileges * run,build: conflict --isolation=chroot and --network * Fix broken dns test (from merge collision) * Fix stutters * Fix broken command completion * buildah bud --network=none should have no network * build: support --skip-unused-stages for multi-stage builds * Prevent use of --dns* options with --net=none * buildah: make --cache-ttl=0s equivalent to --no-cache * parse: make processing flags in --mount order agnostic * Minor test fix for podman-remote * build: honor .containerignore as ignore file * Update install.md: Debian 11 (Bullseye) is stable * build(deps): bump github.com/docker/docker * Use constants from containers/common for finding seccomp.json * Don't call os.Exit(1) from manifest exist * manifest: add support for buildah manifest exists * Buildah should ignore /etc/crio/seccomp.json * chroot: Fix cross build break * chroot: Move isDevNull to run_common.go * chroot: Fix setRlimit build on FreeBSD * chroot: Move parseRLimits and setRlimits to run_common.go * chroot: Fix runUsingChrootExecMain on FreeBSD * chroot: Move runUsingChrootExecMain to run_common.go * chroot: Factor out Linux-specific unshare options from runUsingChroot * chroot: Move runUsingChroot to run_common.go * chroot: Move RunUsingChroot and runUsingChrootMain to run_common.go * chroot: Factor out /dev/ptmx pty implementation * chroot: Add FreeBSD support for run with chroot isolation * build(deps): bump github.com/docker/go-units from 0.4.0 to 0.5.0 * Replace k8s.gcr.io/pause in tests with registry.k8s.io/pause * build(deps): bump github.com/onsi/gomega from 1.20.0 to 1.20.1 * Cirrus: use image with fewer downloaded dependencies * build(deps): bump github.com/opencontainers/runc from 1.1.3 to 1.1.4 * run: add container gid to additional groups * buildah: support for --retry and --retry-delay for push/pull failures * Makefile: always call $(GO) instead of `go` * build(deps): bump github.com/fsouza/go-dockerclient from 1.8.2 to 1.8.3 * test: use `T.TempDir` to create temporary test directory * mount,cache: enable SElinux shared content label option by default * commit: use race-free RemoveNames instead of SetNames * Drop util/util.Cause() * cmd/buildah: add "manifest create --amend" * build(deps): bump github.com/fsouza/go-dockerclient from 1.8.1 to 1.8.2 * docs: specify git protocol is not supported for github hosted repo * Scrub user and group names from layer diffs * build(deps): bump github.com/containerd/containerd from 1.6.6 to 1.6.8 * version: bump to 1.28.0-dev - Changelog for v1.27.0 (2022-08-01) * build: support filtering cache by duration using `--cache-ttl`. * build: support building from commit when using git repo as build context. * build: clean up git repos correctly when using subdirs. * build: add support for distributing cache to remote sources using `--cache-to` and `--cache-from`. * imagebuildah: optimize cache hits for `COPY` and `ADD` instructions. * build: support OCI hooks for ephemeral build containers. * build: add support for `--userns=auto`. * copier: add NoOverwriteNonDirDir option . * add initial support for building images using Buildah on FreeBSD. * multistage: this now skips the computing of unwanted stages to improve performance. * multiarch: support splitting build logs for `--platform` using `--logsplit`. * build: add support for building images where the base image has no history. * commit: allow disabling image history with `--omit-history`. * build: add support for renaming a device in rootless setups. * build: now supports additionalBuildContext in builds via the `--build-context` option. * build: `--output` produces artifacts even if the build container is not committed. * build: now accepts `-cpp-flag`, allowing users to pass in CPP flags when processing a Containerfile with C Preprocessor-like syntax. * build: now accepts a branch and a subdirectory when the build context is a git repository. * build: output now shows a progress bar while pushing and pulling images * build: now errors out if the path to Containerfile is a directory. * build: support building container images on environments that are rootless and without any valid login sessions. * fix: `--output` now generates artifacts even if the entire build is cached. * fix: `--output` generates artifacts only for the target stage in multi-stage builds. * fix,add: now fails on a bad HTTP response instead of writing to container * fix,squash: never use build cache when computing the last step of the last stage * fix,build,run: allow reusing secret more than once in different RUN steps * fix: compatibility with Docker build by making its --label and --annotate options set empty labels and annotations when given a name but no `=` or label value. - Changelog for v1.26.0 (2022-05-04) * imagebuildah,build: move deepcopy of args before we spawn goroutine * Vendor in containers/storage v1.40.2 * buildah.BuilderOptions.DefaultEnv is ignored, so mark it as deprecated * help output: get more consistent about option usage text * Handle OS version and features flags * buildah build: --annotation and --label should remove values * buildah build: add a --env * buildah: deep copy options.Args before performing concurrent build/stage * test: inline platform and builtinargs behaviour * vendor: bump imagebuilder to master/009dbc6 * build: automatically set correct TARGETPLATFORM where expected * build(deps): bump github.com/fsouza/go-dockerclient * Vendor in containers/(common, storage, image) * imagebuildah, executor: process arg variables while populating baseMap * buildkit: add support for custom build output with --output * Cirrus: Update CI VMs to F36 * fix staticcheck linter warning for deprecated function * Fix docs build on FreeBSD * build(deps): bump github.com/containernetworking/cni from 1.0.1 to 1.1.0 * copier.unwrapError(): update for Go 1.16 * copier.PutOptions: add StripSetuidBit/StripSetgidBit/StripStickyBit * copier.Put(): write to read-only directories * build(deps): bump github.com/cpuguy83/go-md2man/v2 in /tests/tools * Rename $TESTSDIR (the plural one), step 4 of 3 * Rename $TESTSDIR (the plural one), step 3 of 3 * Rename $TESTSDIR (the plural one), step 2 of 3 * Rename $TESTSDIR (the plural one), step 1 of 3 * build(deps): bump github.com/containerd/containerd from 1.6.2 to 1.6.3 * Ed's periodic test cleanup * using consistent lowercase 'invalid' word in returned err msg * Update vendor of containers/(common,storage,image) * use etchosts package from c/common * run: set actual hostname in /etc/hostname to match docker parity * update c/common to latest main * Update vendor of containers/(common,storage,image) * Stop littering * manifest-create: allow creating manifest list from local image * Update vendor of storage,common,image * Bump golang.org/x/crypto to 7b82a4e * Initialize network backend before first pull * oci spec: change special mount points for namespaces * tests/helpers.bash: assert handle corner cases correctly * buildah: actually use containers.conf settings * integration tests: learn to start a dummy registry * Fix error check to work on Podman * buildah build should accept at most one arg * tests: reduce concurrency for flaky bud-multiple-platform-no-run * vendor in latest containers/common,image,storage * manifest-add: allow override arch,variant while adding image * Remove a stray `\` from .containerenv * Vendor in latest opencontainers/selinux v1.10.1 * build, commit: allow removing default identity labels * Create shorter names for containers based on image IDs * test: skip rootless on cgroupv2 in root env * fix hang when oci runtime fails * Set permissions for GitHub actions * copier test: use correct UID/GID in test archives * run: set parent-death signals and forward SIGHUP/SIGINT/SIGTERM * Bump back to v1.26.0-dev * build(deps): bump github.com/opencontainers/runc from 1.1.0 to 1.1.1 * Included the URL to check the SHA - Changelog for v1.25.1 (2022-03-30) * buildah: create WORKDIR with USER permissions * vendor: update github.com/openshift/imagebuilder * copier: attempt to open the dir before adding it * Updated dependabot to get updates for GitHub actions. * Switch most calls to filepath.Walk to filepath.WalkDir * build: allow --no-cache and --layers so build cache can be overrided * build(deps): bump github.com/onsi/gomega from 1.18.1 to 1.19.0 * Bump to v1.26.0-dev * build(deps): bump github.com/golangci/golangci-lint in /tests/tools - Changelog for v1.25.0 (2022-03-25) * install: drop RHEL/CentOS 7 doc * build(deps): bump github.com/containers/common from 0.47.4 to 0.47.5 * Bump c/storage to v1.39.0 in main * Add a test for CVE-2022-27651 * build(deps): bump github.com/docker/docker * Bump github.com/prometheus/client_golang to v1.11.1 * [CI:DOCS] man pages: sort flags, and keep them that way * build(deps): bump github.com/containerd/containerd from 1.6.1 to 1.6.2 * Don't pollute * network setup: increase timeout to 4 minutes * do not set the inheritable capabilities * build(deps): bump github.com/golangci/golangci-lint in /tests/tools * build(deps): bump github.com/containers/ocicrypt from 1.1.2 to 1.1.3 * parse: convert exposed GetVolumes to internal only * buildkit: mount=type=cache support locking external cache store * .in support: improve error message when cpp is not installed * buildah image: install cpp * build(deps): bump github.com/stretchr/testify from 1.7.0 to 1.7.1 * build(deps): bump github.com/spf13/cobra from 1.3.0 to 1.4.0 * build(deps): bump github.com/docker/docker * Add --no-hosts flag to eliminate use of /etc/hosts within containers * test: remove skips for rootless users * test: unshare mount/umount if test is_rootless * tests/copy: read correct containers.conf * build(deps): bump github.com/docker/distribution * cirrus: add seperate task and matrix for rootless * tests: skip tests for rootless which need unshare * buildah: test rootless integration * vendor: bump c/storage to main/93ce26691863 * build(deps): bump github.com/fsouza/go-dockerclient from 1.7.9 to 1.7.10 * tests/copy: initialize the network, too * [CI:DOCS] remove references to Kubic for CentOS and Ubuntu * build(deps): bump github.com/containerd/containerd from 1.6.0 to 1.6.1 * use c/image/pkg/blobcache * vendor c/image/v5@v5.20.0 * add: ensure the context directory is an absolute path * executor: docker builds must inherit healthconfig from base if any * docs: Remove Containerfile and containeringore * build(deps): bump github.com/fsouza/go-dockerclient from 1.7.8 to 1.7.9 * helpers.bash: Use correct syntax * speed up combination-namespaces test * build(deps): bump github.com/golangci/golangci-lint in /tests/tools * Bump back to 1.25.0-dev * build(deps): bump github.com/containerd/containerd from 1.5.9 to 1.6.0 - Changelog for v1.24.2 (2022-02-16) * Increase subuid/subgid to 65535 * history: only add proxy vars to history if specified * run_linux: use --systemd-cgroup * buildah: new global option --cgroup-manager * Makefile: build with systemd when available * build(deps): bump github.com/fsouza/go-dockerclient from 1.7.7 to 1.7.8 * Bump c/common to v0.47.4 * Cirrus: Use updated VM images * conformance: add a few "replace-directory-with-symlink" tests * Bump back to v1.25.0-dev - Changelog for v1.24.1 (2022-02-03) * executor: Add support for inline --platform within Dockerfile * caps: fix buildah run --cap-add=all * Update vendor of openshift/imagebuilder * Bump version of containers/image and containers/common * Update vendor of containers/common * System tests: fix accidental vandalism of source dir * build(deps): bump github.com/containers/storage from 1.38.1 to 1.38.2 * imagebuildah.BuildDockerfiles(): create the jobs semaphore * build(deps): bump github.com/onsi/gomega from 1.18.0 to 1.18.1 * overlay: always honor mountProgram * overlay: move mount program invocation to separate function * overlay: move mount program lookup to separate function * Bump to v1.25.0-dev [NO TESTS NEEDED] - Changelog for v1.24.0 (2022-01-26) * Update vendor of containers/common * build(deps): bump github.com/golangci/golangci-lint in /tests/tools * Github-workflow: Report both failures and errors. * build(deps): bump github.com/containers/image/v5 from 5.18.0 to 5.19.0 * Update docs/buildah-build.1.md * [CI:DOCS] Fix typos and improve language * buildah bud --network add support for custom networks * Make pull commands be consistent * docs/buildah-build.1.md: don't imply that -v isn't just a RUN thing * build(deps): bump github.com/onsi/gomega from 1.17.0 to 1.18.0 * Vendor in latest containers/image * Run codespell on code * .github/dependabot.yml: add tests/tools go.mod * CI: rm git-validation, add GHA job to validate PRs * tests/tools: bump go-md2man to v2.0.1 * tests/tools/Makefile: simplify * tests/tools: bump onsi/ginkgo to v1.16.5 * vendor: bump c/common and others * mount: add support for custom upper and workdir with overlay mounts * linux: fix lookup for runtime * overlay: add MountWithOptions to API which extends support for advanced overlay * Allow processing of SystemContext from FlagSet * .golangci.yml: enable unparam linter * util/resolveName: rm bool return * tests/tools: bump golangci-lint * .gitignore: fixups * all: fix capabilities.NewPid deprecation warnings * bind/mount.go: fix linter comment * all: fix gosimple warning S1039 * tests/e2e/buildah_suite_test.go: fix gosimple warnings * imagebuildah/executor.go: fix gosimple warning * util.go: fix gosimple warning * build(deps): bump github.com/opencontainers/runc from 1.0.3 to 1.1.0 * Enable git-daemon tests * Allow processing of id options from FlagSet * Cirrus: Re-order tasks for more parallelism * Cirrus: Freshen VM images * Fix platform handling for empty os/arch values * Allow processing of network options from FlagSet * Fix permissions on secrets directory * Update containers/image and containers/common * bud.bats: use a local git daemon for the git protocol test * Allow processing of common options from FlagSet * Cirrus: Run int. tests in parallel with unit * vendor c/common * Fix default CNI paths * build(deps): bump github.com/fsouza/go-dockerclient from 1.7.6 to 1.7.7 * multi-stage: enable mounting stages across each other with selinux enabled * executor: Share selinux label of first stage with other stages in a build * buildkit: add from field to bind and cache mounts so images can be used as source * Use config.ProxyEnv from containers/common * use libnetwork from c/common for networking * setup the netns in the buildah parent process * build(deps): bump github.com/containerd/containerd from 1.5.8 to 1.5.9 * build(deps): bump github.com/fsouza/go-dockerclient from 1.7.4 to 1.7.6 * build: fix libsubid test * Allow callers to replace the ContainerSuffix * parse: allow parsing anomaly non-human value for memory control group * .cirrus: remove static_build from ci * stage_executor: re-use all possible layers from cache for squashed builds * build(deps): bump github.com/spf13/cobra from 1.2.1 to 1.3.0 * Allow rootless buildah to set resource limits on cgroup V2 * build(deps): bump github.com/docker/docker * tests: move buildkit mount tests files from TESTSDIR to TESTDIR before modification * build(deps): bump github.com/opencontainers/runc from 1.0.2 to 1.0.3 * Wire logger through to config * copier.Put: check for is-not-a-directory using lstat, not stat * Turn on rootless cgroupv2 tests * Grab all of the containers.conf settings for namespaces. * image: set MediaType in OCI manifests * copier: RemoveAll possibly-directories * Simple README fix * images: accept multiple filter with logical AND * build(deps): bump github.com/containernetworking/cni from 0.8.1 to 1.0.1 * UPdate vendor of container/storage * build(deps): bump github.com/onsi/gomega from 1.16.0 to 1.17.0 * build(deps): bump github.com/containers/image/v5 from 5.16.1 to 5.17.0 * Make LocalIP public function so Podman can use it * Fix UnsetEnv for buildah bud * Tests should rely only on static/unchanging images * run: ensure that stdio pipes are labeled correctly * build(deps): bump github.com/docker/docker * Cirrus: Bump up to Fedora 35 & Ubuntu 21.10 * chroot: don't use the generate default seccomp filter for unit tests * build(deps): bump github.com/containerd/containerd from 1.5.7 to 1.5.8 * ssh-agent: Increase timeout before we explicitly close connection * docs/tutorials: update * Clarify that manifest defaults to localhost as the registry name * "config": remove a stray bit of debug output * "commit": fix a flag typo * Fix an error message: unlocking vs locking * Expand the godoc for CommonBuildOptions.Secrets * chroot: accept an "rw" option * Add --unsetenv option to buildah commit and build * define.TempDirForURL(): show CombinedOutput when a command fails * config: support the variant field * rootless: do not bind mount /sys if not needed * Fix tutorial to specify command on buildah run line * build: history should not contain ARG values * docs: Use guaranteed path for go-md2man * run: honor --network=none from builder if nothing specified * networkpolicy: Should be enabled instead of default when explictly set * Add support for env var secret sources * build(deps): bump github.com/docker/docker * fix: another non-portable shebang * Rootless containers users should use additional groups * Support overlayfs path contains colon * Report ignorefile location when no content added * Add support for host.containers.internal in the /etc/hosts * build(deps): bump github.com/onsi/ginkgo from 1.16.4 to 1.16.5 * imagebuildah: fix nil deref * buildkit: add support for mount=type=cache * Default secret mode to 400 * [CI:DOCS] Include manifest example usage * docs: update buildah-from, buildah-pull 'platform' option compatibility notes * docs: update buildah-build 'platform' option compatibility notes * De-dockerize the man page as much as possible * [CI:DOCS] Touch up Containerfile man page to show ARG can be 1st * docs: Fix and Update Containerfile man page with supported mount types * mount: add tmpcopyup to tmpfs mount option * buildkit: Add support for --mount=type=tmpfs * build(deps): bump github.com/opencontainers/selinux from 1.8.5 to 1.9.1 * Fix command doc links in README.md * build(deps): bump github.com/containers/image/v5 from 5.16.0 to 5.16.1 * build: Add support for buildkit like --mount=type=bind * Bump containerd to v1.5.7 * build(deps): bump github.com/docker/docker * tests: stop pulling php, composer * Fix .containerignore link file * Cirrus: Fix defunct package metadata breaking cache * build(deps): bump github.com/containers/storage from 1.36.0 to 1.37.0 * buildah build: add --all-platforms * Add man page for Containerfile and .containerignore * Plumb the remote logger throughut Buildah * Replace fmt.Sprintf("%d", x) with strconv.Itoa(x) * Run: Cleanup run directory after every RUN step * build(deps): bump github.com/containers/common from 0.45.0 to 0.46.0 * Makefile: adjust -ldflags/-gcflags/-gccgoflags depending on the go implementation * Makefile: check for `-race` using `-mod=vendor` * imagebuildah: fix an attempt to write to a nil map * push: support to specify the compression format * conformance: allow test cases to specify dockerUseBuildKit * build(deps): bump github.com/containers/common from 0.44.1 to 0.45.0 * build(deps): bump github.com/containers/common from 0.44.0 to 0.44.1 * unmarshalConvertedConfig(): handle zstd compression * tests/copy/copy: wire up compression options * Update to github.com/vbauerster/mpb v7.1.5 * Add flouthoc to OWNERS * build: Add additional step nodes when labels are modified * Makefile: turn on race detection whenever it's available * conformance: add more tests for exclusion short-circuiting * Update VM Images + Drop prior-ubuntu testing * Bump to v1.24.0-dev - Changelog for v1.23.0 (2021-09-13) * Vendor in containers/common v0.44.0 * build(deps): bump github.com/containers/storage from 1.35.0 to 1.36.0 * Update 05-openshift-rootless-build.md * build(deps): bump github.com/opencontainers/selinux from 1.8.4 to 1.8.5 * .cirrus.yml: run cross_build_task on Big Sur * Makefile: update cross targets * Add support for rootless overlay mounts * Cirrus: Increase unit-test timeout * Docs: Clarify rmi w/ manifest/index use * build: mirror --authfile to filesystem if pointing to FD instead of file * Fix build with .git url with branch * manifest: rm should remove only manifests not referenced images. * vendor: bump c/common to v0.43.3-0.20210902095222-a7acc160fb25 * Avoid rehashing and noop compression writer * corrected man page section; .conf file to mention its man page * copy: add --max-parallel-downloads to tune that copy option * copier.Get(): try to avoid descending into directories * tag: Support tagging manifest list instead of resolving to images * Install new manpages to correct sections * conformance: tighten up exception specifications * Add support for libsubid * Add epoch time field to buildah images * Fix ownership of /home/build/.local/share/containers * build(deps): bump github.com/containers/image/v5 from 5.15.2 to 5.16.0 * Rename bud to build, while keeping an alias for to bud. * Replace golang.org/x/crypto/ssh/terminal with golang.org/x/term * build(deps): bump github.com/opencontainers/runc from 1.0.1 to 1.0.2 * build(deps): bump github.com/onsi/gomega from 1.15.0 to 1.16.0 * build(deps): bump github.com/fsouza/go-dockerclient from 1.7.3 to 1.7.4 * build(deps): bump github.com/containers/common from 0.43.1 to 0.43.2 * Move DiscoverContainerfile to pkg/util directory * build(deps): bump github.com/containers/image/v5 from 5.15.1 to 5.15.2 * Remove some references to Docker * build(deps): bump github.com/containers/image/v5 from 5.15.0 to 5.15.1 * imagebuildah: handle --manifest directly * build(deps): bump github.com/containers/common from 0.42.1 to 0.43.1 * build(deps): bump github.com/opencontainers/selinux from 1.8.3 to 1.8.4 * executor: make sure imageMap is updated with terminatedStage * tests/serve/serve.go: use a kernel-assigned port * Bump go for vendor-in-container from 1.13 to 1.16 * imagebuildah: move multiple-platform building internal * Adds GenerateStructure helper function to support rootfs-overlay. * Run codespell to fix spelling * Implement SSH RUN mount * build(deps): bump github.com/onsi/gomega from 1.14.0 to 1.15.0 * Fix resolv.conf content with run --net=private * run: fix nil deref using the option's logger * build(deps): bump github.com/containerd/containerd from 1.5.1 to 1.5.5 * make vendor-in-container * bud: teach --platform to take a list * set base-image annotations * build(deps): bump github.com/opencontainers/selinux from 1.8.2 to 1.8.3 * [CI:DOCS] Fix CHANGELOG.md * Bump to v1.23.0-dev [NO TESTS NEEDED] * Accept repositories on login/logout - Changelog for v1.22.0 (2021-08-02) * c/image, c/storage, c/common vendor before Podman 3.3 release * WIP: tests: new assert() * Proposed patch for 3399 (shadowutils) * Fix handling of --restore shadow-utils * build(deps): bump github.com/containers/image/v5 from 5.13.2 to 5.14.0 * runtime-flag (debug) test: handle old & new runc * build(deps): bump github.com/containers/storage from 1.32.6 to 1.33.0 * Allow dst and destination for target in secret mounts * Multi-arch: Always push updated version-tagged img * Add a few tests on cgroups V2 * imagebuildah.stageExecutor.prepare(): remove pseudonym check * refine dangling filter * Chown with environment variables not set should fail * Just restore protections of shadow-utils * build(deps): bump github.com/opencontainers/runc from 1.0.0 to 1.0.1 * Remove specific kernel version number requirement from install.md * Multi-arch image workflow: Make steps generic * chroot: fix environment value leakage to intermediate processes * Update nix pin with `make nixpkgs` * buildah source - create and manage source images * Update cirrus-cron notification GH workflow * Reuse code from containers/common/pkg/parse * Cirrus: Freshen VM images * build(deps): bump github.com/containers/storage from 1.32.5 to 1.32.6 * Fix excludes exception begining with / or ./ * Fix syntax for --manifest example * build(deps): bump github.com/onsi/gomega from 1.13.0 to 1.14.0 * vendor containers/common@main * Cirrus: Drop dependence on fedora-minimal * Adjust conformance-test error-message regex * Workaround appearance of differing debug messages * Cirrus: Install docker from package cache * build(deps): bump github.com/containers/ocicrypt from 1.1.1 to 1.1.2 * Switch rusagelogfile to use options.Out * build(deps): bump github.com/containers/storage from 1.32.4 to 1.32.5 * Turn stdio back to blocking when command finishes * Add support for default network creation * Cirrus: Updates for master->main rename * Change references from master to main * Add `--env` and `--workingdir` flags to run command * build(deps): bump github.com/opencontainers/runc * [CI:DOCS] buildah bud: spelling --ignore-file requires parameter * [CI:DOCS] push/pull: clarify supported transports * Remove unused function arguments * Create mountOptions for mount command flags * Extract version command implementation to function * Add --json flags to `mount` and `version` commands * build(deps): bump github.com/containers/storage from 1.32.2 to 1.32.3 * build(deps): bump github.com/containers/common from 0.40.0 to 0.40.1 * copier.Put(): set xattrs after ownership * buildah add/copy: spelling * build(deps): bump github.com/containers/common from 0.39.0 to 0.40.0 * buildah copy and buildah add should support .containerignore * Remove unused util.StartsWithValidTransport * Fix documentation of the --format option of buildah push * Don't use alltransports.ParseImageName with known transports * build(deps): bump github.com/containers/image/v5 from 5.13.0 to 5.13.1 * man pages: clarify `rmi` removes dangling parents * tests: make it easer to override the location of the copy helper * build(deps): bump github.com/containers/image/v5 from 5.12.0 to 5.13.0 * [CI:DOCS] Fix links to c/image master branch * imagebuildah: use the specified logger for logging preprocessing warnings * Fix copy into workdir for a single file * Fix docs links due to branch rename * Update nix pin with `make nixpkgs` * build(deps): bump github.com/fsouza/go-dockerclient from 1.7.2 to 1.7.3 * build(deps): bump github.com/opencontainers/selinux from 1.8.1 to 1.8.2 * build(deps): bump go.etcd.io/bbolt from 1.3.5 to 1.3.6 * build(deps): bump github.com/containers/storage from 1.32.1 to 1.32.2 * build(deps): bump github.com/mattn/go-shellwords from 1.0.11 to 1.0.12 * build(deps): bump github.com/onsi/ginkgo from 1.16.3 to 1.16.4 * fix(docs): typo * Move to v1.22.0-dev * Fix handling of auth.json file while in a user namespace * Add rusage-logfile flag to optionally send rusage to a file * imagebuildah: redo step logging * build(deps): bump github.com/onsi/ginkgo from 1.16.2 to 1.16.3 * build(deps): bump github.com/containers/storage from 1.32.0 to 1.32.1 * Add volumes to make running buildah within a container easier * build(deps): bump github.com/onsi/gomega from 1.12.0 to 1.13.0 * Add and use a "copy" helper instead of podman load/save * Bump github.com/containers/common from 0.38.4 to 0.39.0 * containerImageRef/containerImageSource: don't buffer uncompressed layers * containerImageRef(): squashed images have no parent images * Sync. workflow across skopeo, buildah, and podman * Bump github.com/containers/storage from 1.31.1 to 1.31.2 * Bump github.com/opencontainers/runc from 1.0.0-rc94 to 1.0.0-rc95 * Bump to v1.21.1-dev [NO TESTS NEEDED] - Changelog for v1.21.0 (2021-05-19) * Don't blow up if cpp detects errors * Vendor in containers/common v0.38.4 * Remove 'buildah run --security-opt' from completion * update c/common * Fix handling of --default-mounts-file * update vendor of containers/storage v1.31.1 * Bump github.com/containers/storage from 1.30.3 to 1.31.0 * Send logrus messages back to caller when building * github: Fix bad repo. ref in workflow config * Check earlier for bad image tags name * buildah bud: fix containers/podman/issues/10307 * Bump github.com/containers/storage from 1.30.1 to 1.30.3 * Cirrus: Support [CI:DOCS] test skipping * Notification email for cirrus-cron build failures * Bump github.com/opencontainers/runc from 1.0.0-rc93 to 1.0.0-rc94 * Fix race condition * Fix copy race while walking paths * Preserve ownership of lower directory when doing an overlay mount * Bump github.com/onsi/gomega from 1.11.0 to 1.12.0 * Update nix pin with `make nixpkgs` * codespell cleanup * Multi-arch github-action workflow unification * Bump github.com/containers/image/v5 from 5.11.1 to 5.12.0 * Bump github.com/onsi/ginkgo from 1.16.1 to 1.16.2 * imagebuildah: ignore signatures when tagging images * update to latest libimage * Bump github.com/containers/common from 0.37.0 to 0.37.1 * Bump github.com/containers/storage from 1.30.0 to 1.30.1 * Upgrade to GitHub-native Dependabot * Document location of auth.json file if XDG_RUNTIME_DIR is not set * run.bats: fix flake in run-user test * Cirrus: Update F34beta -> F34 * pr-should-include-tests: try to make work in buildah * runUsingRuntime: when relaying error from the runtime, mention that * Run(): avoid Mkdir() into the rootfs * imagebuildah: replace archive with chrootarchive * imagebuildah.StageExecutor.volumeCacheSaveVFS(): set up bind mounts * conformance: use :Z with transient mounts when SELinux is enabled * bud.bats: fix a bats warning * imagebuildah: create volume directories when using overlays * imagebuildah: drop resolveSymlink() * namespaces test - refactoring and cleanup * Refactor 'idmapping' system test * Cirrus: Update Ubuntu images to 21.04 * Tiny fixes in bud system tests * Add compabitility wrappers for removed packages * Fix expected message at pulling image * Fix system tests of 'bud' subcommand * [CI:DOCS] Update steps for CentOS runc users * Add support for secret mounts * Add buildah manifest rm command * restore push/pull and util API * [CI:DOCS] Remove older distro docs * Rename rhel secrets to subscriptions * vendor in openshift/imagebuilder * Remove buildah bud --loglevel ... * use new containers/common/libimage package * Fix copier when using globs * Test namespace flags of 'bud' subcommand * Add system test of 'bud' subcommand * Output names of multiple tags in buildah bud * push to docker test: don't get fooled by podman * copier: add Remove() * build(deps): bump github.com/containers/image/v5 from 5.10.5 to 5.11.1 * Restore log timestamps * Add system test of 'buildah help' with a tiny fix * tests: copy.bats: fix infinite hang * Do not force hard code to crun in rootless mode * build(deps): bump github.com/openshift/imagebuilder from 1.2.0 to 1.2.1 * build(deps): bump github.com/containers/ocicrypt from 1.1.0 to 1.1.1 * build(deps): bump github.com/containers/common from 0.35.4 to 0.36.0 * Fix arg missing warning in bud * Check without flag in 'from --cgroup-parent' test * Minor fixes to Buildah as a library tutorial documentation * Add system test of 'buildah version' for packaged buildah * Add a few system tests of 'buildah from' * Log the final error with %+v at logging level "trace" * copier: add GetOptions.NoCrossDevice * Update nix pin with `make nixpkgs` * Bump to v1.20.2-dev - Changelog for v1.20.1 (2021-04-13) * Run container with isolation type set at 'from' * bats helpers.bash - minor refactoring * Bump containers/storage vendor to v1.29.0 * build(deps): bump github.com/onsi/ginkgo from 1.16.0 to 1.16.1 * Cirrus: Update VMs w/ F34beta * CLI add/copy: add a --from option * build(deps): bump github.com/onsi/ginkgo from 1.15.2 to 1.16.0 * Add authentication system tests for 'commit' and 'bud' * fix local image lookup for custom platform * Double-check existence of OCI runtimes * Cirrus: Make use of shared get_ci_vm container * Add system tests of "buildah run" * Update nix pin with `make nixpkgs` * Remove some stuttering on returns errors * Setup alias for --tty to --terminal * Add conformance tests for COPY /... * Put a few more minutes on the clock for the CI conformance test * Add a conformance test for COPY --from $symlink * Add conformance tests for COPY "" * Check for symlink in builtin volume * Sort all mounts by destination directory * System-test cleanup * Export parse.Platform string to be used by podman-remote * blobcache: fix sequencing error * build(deps): bump github.com/containers/common from 0.35.3 to 0.35.4 * Fix URL in demos/buildah_multi_stage.sh * Add a few system tests * [NO TESTS NEEDED] Use --recurse-modules when building git context * Bump to v1.20.1-dev - Changelog for v1.20.0 (2021-03-25) * vendor in containers/storage v1.28.1 * build(deps): bump github.com/containers/common from 0.35.2 to 0.35.3 * tests: prefetch: use buildah, not podman, for pulls * Use faster way to check image tag existence during multi-arch build * Add information about multi-arch images to the Readme * COPY --chown: expand the conformance test * pkg/chrootuser: use a bufio.Scanner * [CI:DOCS] Fix rootful typo in docs * build(deps): bump github.com/onsi/ginkgo from 1.15.1 to 1.15.2 * Add documentation and testing for .containerignore * build(deps): bump github.com/sirupsen/logrus from 1.8.0 to 1.8.1 * build(deps): bump github.com/hashicorp/go-multierror from 1.1.0 to 1.1.1 * Lookup Containerfile if user specifies a directory * Add Tag format placeholder to docs * copier: ignore sockets * image: propagate errors from extractRootfs * Remove system test of 'buildah containers -a' * Clarify userns options are usable only as root in man pages * Fix system test of 'containers -a' * Remove duplicated code in addcopy * build(deps): bump github.com/onsi/ginkgo from 1.15.0 to 1.15.1 * build(deps): bump github.com/onsi/gomega from 1.10.5 to 1.11.0 * build(deps): bump github.com/fsouza/go-dockerclient from 1.7.1 to 1.7.2 * Update multi-arch buildah build setup with new logic * Update nix pin with `make nixpkgs` * overlay.bats: fix the "overlay source permissions" test * imagebuildah: use overlay for volumes when using overlay * Make PolicyMap and PullPolicy names align * copier: add GetOptions.IgnoreUnreadable * Check local image to match system context * fix: Containerfiles - smaller set of userns u/gids * Set upperdir permissions based on source * Shrink the vendoring size of pkc/cli * Clarify image name match failure message * ADD/COPY: create the destination directory first, chroot to it * copier.GetOptions: add NoDerefSymLinks * copier: add an Eval function * Update system test for 'from --cap-add/drop' * copier: fix a renaming bug * copier: return child process stderr if we can't JSON decode the response * Add some system tests * build(deps): bump github.com/containers/storage from 1.26.0 to 1.27.0 * complement add/copy --chmod documentation * buildah login and logout, do not need to enter user namespace * Add multi-arch image build * chmod/chown added/fixed in bash completions * OWNERS: add @lsm5 * buildah add/copy --chmod dockerfile implementation * bump github.com/openshift/imagebuilder from 1.1.8 to 1.2.0 * buildah add/copy --chmod cli implementation for files and urls * Make sure we set the buildah version label * Isolation strings, should match user input * [CI:DOCS] buildah-from.md: remove dup arch,os * build(deps): bump github.com/containers/image/v5 from 5.10.2 to 5.10.3 * Cirrus: Temp. disable prior-fedora (F32) testing * pr-should-include-tests: recognized "renamed" tests * build(deps): bump github.com/sirupsen/logrus from 1.7.0 to 1.8.0 * build(deps): bump github.com/fsouza/go-dockerclient from 1.7.0 to 1.7.1 * build(deps): bump github.com/containers/common from 0.34.2 to 0.35.0 * Fix reaping of stages with no instructions * add stale bot * Add base image name to comment * build(deps): bump github.com/spf13/cobra from 1.1.1 to 1.1.3 * Don't fail copy to emptydir * buildah: use volatile containers * vendor: update containers/storage * Eliminate the use of containers/building import in pkg subdirs * Add more support for removing config * Improve messages about --cache-from not being supported * Revert patch to allow COPY/ADD of empty dirs. * Don't fail copy to emptydir * Fix tutorial for rootless mode * Fix caching layers with build args * Vendor in containers/image v5.10.2 * build(deps): bump github.com/containers/common from 0.34.0 to 0.34.2 * build(deps): bump github.com/onsi/ginkgo from 1.14.2 to 1.15.0 * 'make validate': require PRs to include tests * build(deps): bump github.com/onsi/gomega from 1.10.4 to 1.10.5 * build(deps): bump github.com/containers/storage from 1.24.5 to 1.25.0 * Use chown function for U volume flag from containers/common repository * --iidfile: print hash prefix * bump containernetworking/cni to v0.8.1 - fix for CVE-2021-20206 * run: fix check for host pid namespace * Finish plumbing for buildah bud --manifest * buildah manifest add localimage should work * Stop testing directory permissions with latest docker * Fix build arg check * build(deps): bump github.com/containers/ocicrypt from 1.0.3 to 1.1.0 * [ci:docs] Fix man page for buildah push * Update nix pin with `make nixpkgs` * Bump to containers/image v5.10.1 * Rebuild layer if a change in ARG is detected * Bump golang.org/x/crypto to the latest * Add Ashley and Urvashi to Approvers * local image lookup by digest * Use build-arg ENV val from local environment if set * Pick default OCI Runtime from containers.conf * Added required devel packages * Cirrus: Native OSX Build * Cirrus: Two minor cleanup items * Workaround for RHEL gating test failure * build(deps): bump github.com/stretchr/testify from 1.6.1 to 1.7.0 * build(deps): bump github.com/mattn/go-shellwords from 1.0.10 to 1.0.11 * Reset upstream branch to dev version * If destination does not exists, do not throw error - Changelog for v1.19.0 (2021-01-08) * Update vendor of containers/storage and containers/common * Buildah inspect should be able to inspect manifests * Make buildah push support pushing manifests lists and digests * Fix handling of TMPDIR environment variable * Add support for --manifest flags * Upper directory should match mode of destination directory * Only grab the OS, Arch if the user actually specified them * Use --arch and --os and --variant options to select architecture and os * Cirrus: Track libseccomp and golang version * copier.PutOptions: add an "IgnoreDevices" flag * fix: `rmi --prune` when parent image is in store. * build(deps): bump github.com/containers/storage from 1.24.3 to 1.24.4 * build(deps): bump github.com/containers/common from 0.31.1 to 0.31.2 * Allow users to specify stdin into containers * Drop log message on failure to mount on /sys file systems to info * Spelling * SELinux no longer requires a tag. * build(deps): bump github.com/opencontainers/selinux from 1.6.0 to 1.8.0 * build(deps): bump github.com/containers/common from 0.31.0 to 0.31.1 * Update nix pin with `make nixpkgs` * Switch references of /var/run -> /run * Allow FROM to be overriden with from option * copier: don't assume we can chroot() on Unixy systems * copier: add PutOptions.NoOverwriteDirNonDir, Get/PutOptions.Rename * copier: handle replacing directories with not-directories * copier: Put: skip entries with zero-length names * build(deps): bump github.com/containers/storage from 1.24.2 to 1.24.3 * Add U volume flag to chown source volumes * Turn off PRIOR_UBUNTU Test until vm is updated * pkg, cli: rootless uses correct isolation * build(deps): bump github.com/onsi/gomega from 1.10.3 to 1.10.4 * update installation doc to reflect current status * Move away from using docker.io * enable short-name aliasing * build(deps): bump github.com/containers/storage from 1.24.1 to 1.24.2 * build(deps): bump github.com/containers/common from 0.30.0 to 0.31.0 * Throw errors when using bogus --network flags * pkg/supplemented test: replace our null blobinfocache * build(deps): bump github.com/containers/common from 0.29.0 to 0.30.0 * inserts forgotten quotation mark * Not prefer use local image create/add manifest * Add container information to .containerenv * Add --ignorefile flag to use alternate .dockerignore flags * Add a source debug build * Fix crash on invalid filter commands * build(deps): bump github.com/containers/common from 0.27.0 to 0.29.0 * Switch to using containers/common pkg's * fix: non-portable shebang #2812 * Remove copy/paste errors that leaked `Podman` into man pages. * Add suggests cpp to spec file * Apply suggestions from code review * update docs for debian testing and unstable * imagebuildah: disable pseudo-terminals for RUN * Compute diffID for mapped-layer at creating image source * intermediateImageExists: ignore images whose history we can't read * Bump to v1.19.0-dev * build(deps): bump github.com/containers/common from 0.26.3 to 0.27.0 - Changelog for v1.18.0 (2020-11-16) * Fix testing error caused by simultanious merge * Vendor in containers/storage v1.24.0 * short-names aliasing * Add --policy flag to buildah pull * Stop overwrapping and stuttering * copier.Get(): ignore ENOTSUP/ENOSYS when listing xattrs * Run: don't forcibly disable UTS namespaces in rootless mode * test: ensure non-directory in a Dockerfile path is handled correctly * Add a few tests for `pull` command * Fix buildah config --cmd to handle array * build(deps): bump github.com/containers/storage from 1.23.8 to 1.23.9 * Fix NPE when Dockerfile path contains non-directory entries * Update buildah bud man page from podman build man page * Move declaration of decryption-keys to common cli * Run: correctly call copier.Mkdir * util: digging UID/GID out of os.FileInfo should work on Unix * imagebuildah.getImageTypeAndHistoryAndDiffIDs: cache results * Verify userns-uid-map and userns-gid-map input * Use CPP, CC and flags in dep check scripts * Avoid overriding LDFLAGS in Makefile * ADD: handle --chown on URLs * Update nix pin with `make nixpkgs` * (*Builder).Run: MkdirAll: handle EEXIST error * copier: try to force loading of nsswitch modules before chroot() * fix MkdirAll usage * build(deps): bump github.com/containers/common from 0.26.2 to 0.26.3 * build(deps): bump github.com/containers/storage from 1.23.7 to 1.23.8 * Use osusergo build tag for static build * imagebuildah: cache should take image format into account * Bump to v1.18.0-dev - Changelog for v1.17.0 (2020-10-29) * Handle cases where other tools mount/unmount containers * overlay.MountReadOnly: support RO overlay mounts * overlay: use fusermount for rootless umounts * overlay: fix umount * Switch default log level of Buildah to Warn. Users need to see these messages * Drop error messages about OCI/Docker format to Warning level * build(deps): bump github.com/containers/common from 0.26.0 to 0.26.2 * tests/testreport: adjust for API break in storage v1.23.6 * build(deps): bump github.com/containers/storage from 1.23.5 to 1.23.7 * build(deps): bump github.com/fsouza/go-dockerclient from 1.6.5 to 1.6.6 * copier: put: ignore Typeflag="g" * Use curl to get repo file (fix #2714) * build(deps): bump github.com/containers/common from 0.25.0 to 0.26.0 * build(deps): bump github.com/spf13/cobra from 1.0.0 to 1.1.1 * Remove docs that refer to bors, since we're not using it * Buildah bud should not use stdin by default * bump containerd, docker, and golang.org/x/sys * Makefile: cross: remove windows.386 target * copier.copierHandlerPut: don't check length when there are errors * Stop excessive wrapping * CI: require that conformance tests pass * bump(github.com/openshift/imagebuilder) to v1.1.8 * Skip tlsVerify insecure BUILD_REGISTRY_SOURCES * Fix build path wrong https://github.com/containers/podman/issues/7993 * refactor pullpolicy to avoid deps * build(deps): bump github.com/containers/common from 0.24.0 to 0.25.0 * CI: run gating tasks with a lot more memory * ADD and COPY: descend into excluded directories, sometimes * copier: add more context to a couple of error messages * copier: check an error earlier * copier: log stderr output as debug on success * Update nix pin with `make nixpkgs` * Set directory ownership when copied with ID mapping * build(deps): bump github.com/sirupsen/logrus from 1.6.0 to 1.7.0 * build(deps): bump github.com/containers/common from 0.23.0 to 0.24.0 * Cirrus: Remove bors artifacts * Sort build flag definitions alphabetically * ADD: only expand archives at the right time * Remove configuration for bors * Shell Completion for podman build flags * Bump c/common to v0.24.0 * New CI check: xref --help vs man pages * CI: re-enable several linters * Move --userns-uid-map/--userns-gid-map description into buildah man page * add: preserve ownerships and permissions on ADDed archives * Makefile: tweak the cross-compile target * Bump containers/common to v0.23.0 * chroot: create bind mount targets 0755 instead of 0700 * Change call to Split() to safer SplitN() * chroot: fix handling of errno seccomp rules * build(deps): bump github.com/containers/image/v5 from 5.5.2 to 5.6.0 * Add In Progress section to contributing * integration tests: make sure tests run in ${topdir}/tests * Run(): ignore containers.conf's environment configuration * Warn when setting healthcheck in OCI format * Cirrus: Skip git-validate on branches * tools: update git-validation to the latest commit * tools: update golangci-lint to v1.18.0 * Add a few tests of push command * Add(): fix handling of relative paths with no ContextDir * build(deps): bump github.com/containers/common from 0.21.0 to 0.22.0 * Lint: Use same linters as podman * Validate: reference HEAD * Fix buildah mount to display container names not ids * Update nix pin with `make nixpkgs` * Add missing --format option in buildah from man page * Fix up code based on codespell * build(deps): bump github.com/openshift/imagebuilder from 1.1.6 to 1.1.7 * build(deps): bump github.com/containers/storage from 1.23.4 to 1.23.5 * Improve buildah completions * Cirrus: Fix validate commit epoch * Fix bash completion of manifest flags * Uniform some man pages * Update Buildah Tutorial to address BZ1867426 * Update bash completion of `manifest add` sub command * copier.Get(): hard link targets shouldn't be relative paths * build(deps): bump github.com/onsi/gomega from 1.10.1 to 1.10.2 * Pass timestamp down to history lines * Timestamp gets updated everytime you inspect an image * bud.bats: use absolute paths in newly-added tests * contrib/cirrus/lib.sh: don't use CN for the hostname * tests: Add some tests * Update `manifest add` man page * Extend flags of `manifest add` * build(deps): bump github.com/containers/storage from 1.23.3 to 1.23.4 * build(deps): bump github.com/onsi/ginkgo from 1.14.0 to 1.14.1 * Bump to v1.17.0-dev * CI: expand cross-compile checks - Changelog for v1.16.0 (2020-09-03) * fix build on 32bit arches * containerImageRef.NewImageSource(): don't always force timestamps * Add fuse module warning to image readme * Heed our retry delay option values when retrying commit/pull/push * Switch to containers/common for seccomp * Use --timestamp rather then --omit-timestamp * docs: remove outdated notice * docs: remove outdated notice * build-using-dockerfile: add a hidden --log-rusage flag * build(deps): bump github.com/containers/image/v5 from 5.5.1 to 5.5.2 * Discard ReportWriter if user sets options.Quiet * build(deps): bump github.com/containers/common from 0.19.0 to 0.20.3 * Fix ownership of content copied using COPY --from * newTarDigester: zero out timestamps in tar headers * Update nix pin with `make nixpkgs` * bud.bats: correct .dockerignore integration tests * Use pipes for copying * run: include stdout in error message * run: use the correct error for errors.Wrapf * copier: un-export internal types * copier: add Mkdir() * in_podman: don't get tripped up by $CIRRUS_CHANGE_TITLE * docs/buildah-commit.md: tweak some wording, add a --rm example * imagebuildah: don’t blank out destination names when COPYing * Replace retry functions with common/pkg/retry * StageExecutor.historyMatches: compare timestamps using .Equal * Update vendor of containers/common * Fix errors found in coverity scan * Change namespace handling flags to better match podman commands * conformance testing: ignore buildah.BuilderIdentityAnnotation labels * Vendor in containers/storage v1.23.0 * Add buildah.IsContainer interface * Avoid feeding run_buildah to pipe * fix(buildahimage): add xz dependency in buildah image * Bump github.com/containers/common from 0.15.2 to 0.18.0 * Howto for rootless image building from OpenShift * Add --omit-timestamp flag to buildah bud * Update nix pin with `make nixpkgs` * Shutdown storage on failures * Handle COPY --from when an argument is used * Bump github.com/seccomp/containers-golang from 0.5.0 to 0.6.0 * Cirrus: Use newly built VM images * Bump github.com/opencontainers/runc from 1.0.0-rc91 to 1.0.0-rc92 * Enhance the .dockerignore man pages * conformance: add a test for COPY from subdirectory * fix bug manifest inspct * Add documentation for .dockerignore * Add BuilderIdentityAnnotation to identify buildah version * DOC: Add quay.io/containers/buildah image to README.md * Update buildahimages readme * fix spelling mistake in "info" command result display * Don't bind /etc/host and /etc/resolv.conf if network is not present * blobcache: avoid an unnecessary NewImage() * Build static binary with `buildGoModule` * copier: split StripSetidBits into StripSetuidBit/StripSetgidBit/StripStickyBit * tarFilterer: handle multiple archives * Fix a race we hit during conformance tests * Rework conformance testing * Update 02-registries-repositories.md * test-unit: invoke cmd/buildah tests with --flags * parse: fix a type mismatch in a test * Fix compilation of tests/testreport/testreport * build.sh: log the version of Go that we're using * test-unit: increase the test timeout to 40/45 minutes * Add the "copier" package * Fix & add notes regarding problematic language in codebase * Add dependency on github.com/stretchr/testify/require * CompositeDigester: add the ability to filter tar streams * BATS tests: make more robust * vendor golang.org/x/text@v0.3.3 * Switch golang 1.12 to golang 1.13 * imagebuildah: wait for stages that might not have even started yet * chroot, run: not fail on bind mounts from /sys * chroot: do not use setgroups if it is blocked * Set engine env from containers.conf * imagebuildah: return the right stage's image as the "final" image * Fix a help string * Deduplicate environment variables * switch containers/libpod to containers/podman * Bump github.com/containers/ocicrypt from 1.0.2 to 1.0.3 * Bump github.com/opencontainers/selinux from 1.5.2 to 1.6.0 * Mask out /sys/dev to prevent information leak * linux: skip errors from the runtime kill * Mask over the /sys/fs/selinux in mask branch * Add VFS additional image store to container * tests: add auth tests * Allow "readonly" as alias to "ro" in mount options * Ignore OS X specific consistency mount option * Bump github.com/onsi/ginkgo from 1.13.0 to 1.14.0 * Bump github.com/containers/common from 0.14.0 to 0.15.2 * Rootless Buildah should default to IsolationOCIRootless * imagebuildah: fix inheriting multi-stage builds * Make imagebuildah.BuildOptions.Architecture/OS optional * Make imagebuildah.BuildOptions.Jobs optional * Resolve a possible race in imagebuildah.Executor.startStage() * Switch scripts to use containers.conf * Bump openshift/imagebuilder to v1.1.6 * Bump go.etcd.io/bbolt from 1.3.4 to 1.3.5 * buildah, bud: support --jobs=N for parallel execution * executor: refactor build code inside new function * Add bud regression tests * Cirrus: Fix missing htpasswd in registry img * docs: clarify the 'triples' format * CHANGELOG.md: Fix markdown formatting * Add nix derivation for static builds * Bump to v1.16.0-dev * add version centos7 for compatible - Changelog for v1.15.0 (2020-06-17) * Bump github.com/containers/common from 0.12.0 to 0.13.1 * Bump github.com/containers/storage from 1.20.1 to 1.20.2 * Bump github.com/seccomp/containers-golang from 0.4.1 to 0.5.0 * Bump github.com/stretchr/testify from 1.6.0 to 1.6.1 * Bump github.com/opencontainers/runc from 1.0.0-rc9 to 1.0.0-rc90 * Add CVE-2020-10696 to CHANGELOG.md and changelog.txt * Bump github.com/stretchr/testify from 1.5.1 to 1.6.0 * Bump github.com/onsi/ginkgo from 1.12.2 to 1.12.3 * Vendor in containers/common v0.12.0 * fix lighttpd example * Vendor in new go.etcd.io/bbolt * Bump github.com/onsi/ginkgo from 1.12.1 to 1.12.2 * Bump imagebuilder for ARG fix * Bump github.com/containers/common from 0.11.2 to 0.11.4 * remove dependency on openshift struct * Warn on unset build arguments * vendor: update seccomp/containers-golang to v0.4.1 * Ammended docs * Updated docs * clean up comments * update exit code for tests * Implement commit for encryption * implementation of encrypt/decrypt push/pull/bud/from * fix resolve docker image name as transport * Bump github.com/opencontainers/go-digest from 1.0.0-rc1 to 1.0.0 * Bump github.com/onsi/ginkgo from 1.12.0 to 1.12.1 * Bump github.com/containers/storage from 1.19.1 to 1.19.2 * Bump github.com/containers/image/v5 from 5.4.3 to 5.4.4 * Add preliminary profiling support to the CLI * Bump github.com/containers/common from 0.10.0 to 0.11.2 * Evaluate symlinks in build context directory * fix error info about get signatures for containerImageSource * Add Security Policy * Cirrus: Fixes from review feedback * Bump github.com/containers/storage from 1.19.0 to 1.19.1 * Bump github.com/sirupsen/logrus from 1.5.0 to 1.6.0 * imagebuildah: stages shouldn't count as their base images * Update containers/common v0.10.0 * Bump github.com/fsouza/go-dockerclient from 1.6.4 to 1.6.5 * Add registry to buildahimage Dockerfiles * Cirrus: Use pre-installed VM packages + F32 * Cirrus: Re-enable all distro versions * Cirrus: Update to F31 + Use cache images * golangci-lint: Disable gosimple * Lower number of golangci-lint threads * Fix permissions on containers.conf * Don't force tests to use runc * Bump github.com/containers/common from 0.9.1 to 0.9.5 * Return exit code from failed containers * Bump github.com/containers/storage from 1.18.2 to 1.19.0 * Bump github.com/containers/common from 0.9.0 to 0.9.1 * cgroup_manager should be under [engine] * Use c/common/pkg/auth in login/logout * Cirrus: Temporarily disable Ubuntu 19 testing * Add containers.conf to stablebyhand build * Update gitignore to exclude test Dockerfiles * Bump github.com/fsouza/go-dockerclient from 1.6.3 to 1.6.4 * Bump github.com/containers/common from 0.8.1 to 0.9.0 * Bump back to v1.15.0-dev * Remove warning for systemd inside of container - Changelog for v1.14.8 (2020-04-09) * Run (make vendor) * Run (make -C tests/tools vendor) * Run (go mod tidy) before (go mod vendor) again * Fix (make vendor) * Bump validation * Bump back to v1.15.0-dev - Changelog for v1.14.7 (2020-04-07) * Bump github.com/containers/image/v5 from 5.3.1 to 5.4.3 * make vendor: run `tidy` after `vendor` * Do not skip the directory when the ignore pattern matches * Bump github.com/containers/common from 0.7.0 to 0.8.1 * Downgrade siruspen/logrus from 1.4.2 * Fix errorf conventions * dockerignore tests : remove symlinks, rework * Bump back to v1.15.0-dev - Changelog for v1.14.6 (2020-04-02) * bud.bats - cleanup, refactoring * vendor in latest containers/storage 1.18.0 and containers/common v0.7.0 * Bump github.com/spf13/cobra from 0.0.6 to 0.0.7 * Bump github.com/containers/storage from 1.16.5 to 1.17.0 * Bump github.com/containers/image/v5 from 5.2.1 to 5.3.1 * Fix Amazon install step * Bump back to v1.15.0-dev * Fix bud-build-arg-cache test * Make image history work correctly with new args handling * Don't add args to the RUN environment from the Builder * Update github.com/openshift/imagebuilder to v1.1.4 * Add .swp files to .gitignore - Changelog for v1.14.5 (2020-03-26) * revert #2246 FIPS mode change * Bump back to v1.15.0-dev * image with dup layers: we now have one on quay * digest test : make more robust - Changelog for v1.14.4 (2020-03-25) * Fix fips-mode check for RHEL8 boxes * Fix potential CVE in tarfile w/ symlink (Edit 02-Jun-2020: Addresses CVE-2020-10696) * Fix .dockerignore with globs and ! commands * update install steps for Amazon Linux 2 * Bump github.com/openshift/imagebuilder from 1.1.2 to 1.1.3 * Add comment for RUN command in volume ownership test * Run stat command directly for volume ownership test * vendor in containers/common v0.6.1 * Cleanup go.sum * Bump back to v1.15.0-dev - Changelog for v1.14.3 (2020-03-17) * Update containers/storage to v1.16.5 * Bump github.com/containers/storage from 1.16.2 to 1.16.4 * Bump github.com/openshift/imagebuilder from 1.1.1 to 1.1.2 * Update github.com/openshift/imagebuilder vendoring * Update unshare man page to fix script example * Fix compilation errors on non linux platforms * Bump containers/common and opencontainers/selinux versions * Add tests for volume ownership * Preserve volume uid and gid through subsequent commands * Fix FORWARD_NULL errors found by Coverity * Bump github.com/containers/storage from 1.16.1 to 1.16.2 * Fix errors found by codespell * Bump back to v1.15.0-dev * Add Pull Request Template - Changelog for v1.14.2 (2020-03-03) * Add Buildah pull request template * Bump to containers/storage v1.16.1 * run_linux: fix tight loop if file is not pollable * Bump github.com/opencontainers/selinux from 1.3.2 to 1.3.3 * Bump github.com/containers/common from 0.4.1 to 0.4.2 * Bump back to v1.15.0-dev * Add Containerfile to build a versioned stable image on quay.io - Changelog for v1.14.1 (2020-02-27) * Search for local runtime per values in containers.conf * Set correct ownership on working directory * BATS : in teardown, umount stale mounts * Bump github.com/spf13/cobra from 0.0.5 to 0.0.6 * Bump github.com/fsouza/go-dockerclient from 1.6.1 to 1.6.3 * Bump github.com/stretchr/testify from 1.4.0 to 1.5.1 * Replace unix with syscall to allow vendoring into libpod * Update to containers/common v0.4.1 * Improve remote manifest retrieval * Fix minor spelling errors in containertools README * Clear the right variable in buildahimage * Correct a couple of incorrect format specifiers * Update to containers/common v0.3.0 * manifest push --format: force an image type, not a list type * run: adjust the order in which elements are added to $PATH * getDateAndDigestAndSize(): handle creation time not being set * Bump github.com/containers/common from 0.2.0 to 0.2.1 * include installation steps for CentOS 8 and Stream * include installation steps for CentOS7 and forks * Adjust Ubuntu install info to also work on Pop!_OS * Make the commit id clear like Docker * Show error on copied file above context directory in build * Bump github.com/containers/image/v5 from 5.2.0 to 5.2.1 * pull/from/commit/push: retry on most failures * Makefile: fix install.cni.sudo * Repair buildah so it can use containers.conf on the server side * Bump github.com/mattn/go-shellwords from 1.0.9 to 1.0.10 * Bump github.com/fsouza/go-dockerclient from 1.6.0 to 1.6.1 * Fixing formatting & build instructions * Add Code of Conduct * Bors: Fix no. req. github reviews * Cirrus+Bors: Simplify temp branch skipping * Bors-ng: Add documentation and status-icon * Bump github.com/onsi/ginkgo from 1.11.0 to 1.12.0 * fix XDG_RUNTIME_DIR for authfile * Cirrus: Disable F29 testing * Cirrus: Add jq package * Cirrus: Fix lint + validation using wrong epoch * Stop using fedorproject registry * Bors: Workaround ineffective required statuses * Bors: Enable app + Disable Travis * Cirrus: Add standardized log-collection * Cirrus: Improve automated lint + validation * Allow passing options to golangci-lint * Cirrus: Fixes from review feedback * Cirrus: Temporarily ignore VM testing failures * Cirrus: Migrate off papr + implement VM testing * Cirrus: Update packages + fixes for get_ci_vm.sh * Show validation command-line * Skip overlay test w/ vfs driver * use alpine, not centos, for various tests * Flake handling: cache and prefetch images * Bump to v1.15.0-dev - Changelog for v1.14.0 (2020-02-05) * bump github.com/mtrmac/gpgme * Update containers/common to v0.1.4 * manifest push: add --format option * Bump github.com/onsi/gomega from 1.8.1 to 1.9.0 * vendor github.com/containers/image/v5@v5.2.0 * info test: deal with random key order * Bump back to v1.14.0-dev - Changelog for v1.13.2 (2020-01-29) * sign.bats: set GPG_TTY=/dev/null * Fix parse_unsupported.go * getDateAndDigestAndSize(): use manifest.Digest * Bump github.com/opencontainers/selinux from 1.3.0 to 1.3.1 * Bump github.com/containers/common from 0.1.0 to 0.1.2 * Touch up os/arch doc * chroot: handle slightly broken seccomp defaults * buildahimage: specify fuse-overlayfs mount options * Bump github.com/mattn/go-shellwords from 1.0.7 to 1.0.9 * copy.bats: make sure we detect failures due to missing source * parse: don't complain about not being able to rename something to itself * Makefile: use a $(GO_TEST) macro, fix a typo * manifests: unit test fix * Fix build for 32bit platforms * Allow users to set OS and architecture on bud * Fix COPY in containerfile with envvar * Bump c/storage to v1.15.7 * add --sign-by to bud/commit/push, --remove-signatures for pull/push * Remove cut/paste error in CHANGELOG.md * Update vendor of containers/common to v0.1.0 * update install instructions for Debian, Raspbian and Ubuntu * Add support for containers.conf * Bump back to v1.14.0-dev - Changelog for v1.13.1 (2020-01-14) * Bump github.com/containers/common from 0.0.5 to 0.0.7 * Bump github.com/onsi/ginkgo from 1.10.3 to 1.11.0 * Bump github.com/pkg/errors from 0.8.1 to 0.9.0 * Bump github.com/onsi/gomega from 1.7.1 to 1.8.1 * Add codespell support * copyFileWithTar: close source files at the right time * copy: don't digest files that we ignore * Check for .dockerignore specifically * Travis: rm go 1.12.x * Don't setup excludes, if their is only one pattern to match * set HOME env to /root on chroot-isolation by default * docs: fix references to containers-*.5 * update openshift/api * fix bug Add check .dockerignore COPY file * buildah bud --volume: run from tmpdir, not source dir * Fix imageNamePrefix to give consistent names in buildah-from * cpp: use -traditional and -undef flags * Fix image reference in tutorial 4 * discard outputs coming from onbuild command on buildah-from --quiet * make --format columnizing consistent with buildah images * Bump to v1.14.0-dev - Changelog for v1.13.0 (2019-12-27) * Bump to c/storage v1.15.5 * Update container/storage to v1.15.4 * Fix option handling for volumes in build * Rework overlay pkg for use with libpod * Fix buildahimage builds for buildah * Add support for FIPS-Mode backends * Set the TMPDIR for pulling/pushing image to $TMPDIR * WIP: safer test for pull --all-tags * BATS major cleanup: blobcache.bats: refactor * BATS major cleanup: part 4: manual stuff * BATS major cleanup, step 3: yet more run_buildah * BATS major cleanup, part 2: use more run_buildah * BATS major cleanup, part 1: log-level * Bump github.com/containers/image/v5 from 5.0.0 to 5.1.0 * Bump github.com/containers/common from 0.0.3 to 0.0.5 * Bump to v1.13.0-dev - Changelog for v1.12.0 (2019-12-13) * Allow ADD to use http src * Bump to c/storage v.1.15.3 * install.md: update golang dependency * imgtype: reset storage opts if driver overridden * Start using containers/common * overlay.bats typo: fuse-overlays should be fuse-overlayfs * chroot: Unmount with MNT_DETACH instead of UnmountMountpoints() * bind: don't complain about missing mountpoints * imgtype: check earlier for expected manifest type * Vendor containers/storage fix * Vendor containers/storage v1.15.1 * Add history names support * PR takeover of #1966 * Tests: Add inspect test check steps * Tests: Add container name and id check in containers test steps * Test: Get permission in add test * Tests: Add a test for tag by id * Tests: Add test cases for push test * Tests: Add image digest test * Tests: Add some buildah from tests * Tests: Add two commit test * Tests: Add buildah bud with --quiet test * Tests: Add two test for buildah add * Bump back to v1.12.0-dev - Changelog for v1.11.6 (2019-12-03) * Handle missing equal sign in --from and --chown flags for COPY/ADD * bud COPY does not download URL * Bump github.com/onsi/gomega from 1.7.0 to 1.7.1 * Fix .dockerignore exclude regression * Ran buildah through codespell * commit(docker): always set ContainerID and ContainerConfig * Touch up commit man page image parameter * Add builder identity annotations. * info: use util.Runtime() * Bump github.com/onsi/ginkgo from 1.10.2 to 1.10.3 * Bump back to v1.12.0-dev - Changelog for v1.11.5 (2019-11-11) * Enhance error on unsafe symbolic link targets * Add OCIRuntime to info * Check nonexsit authfile * Only output image id if running buildah bud --quiet * Fix --pull=true||false and add --pull-never to bud and from (retry) * cgroups v2: tweak or skip tests * Prepwork: new 'skip' helpers for tests * Handle configuration blobs for manifest lists * unmarshalConvertedConfig: avoid using the updated image's ref * Add completions for Manifest commands * Add disableFips option to secrets pkg * Update bud.bats test archive test * Add test for caching based on content digest * Builder.untarPath(): always evaluate b.ContentDigester.Hash() * Bump github.com/onsi/ginkgo from 1.10.1 to 1.10.2 * Fix another broken test: copy-url-mtime * yet more fixes * Actual bug fix for 'add' test: fix the expected mode * BATS tests - lots of mostly minor cleanup * build: drop support for ostree * Add support for make vendor-in-container * imgtype: exit with error if storage fails * remove XDG_RUNTIME_DIR from default authfile path * fix troubleshooting redirect instructions * Bump back to v1.12.0-dev - Changelog for v1.11.4 (2019-10-28) * buildah: add a "manifest" command * manifests: add the module * pkg/supplemented: add a package for grouping images together * pkg/manifests: add a manifest list build/manipulation API * Update for ErrUnauthorizedForCredentials API change in containers/image * Update for manifest-lists API changes in containers/image * version: also note the version of containers/image * Move to containers/image v5.0.0 * Enable --device directory as src device * Fix git build with branch specified * Bump github.com/openshift/imagebuilder from 1.1.0 to 1.1.1 * Bump github.com/fsouza/go-dockerclient from 1.4.4 to 1.5.0 * Add clarification to the Tutorial for new users * Silence "using cache" to ensure -q is fully quiet * Add OWNERS File to Buildah * Bump github.com/containers/storage from 1.13.4 to 1.13.5 * Move runtime flag to bud from common * Commit: check for storage.ErrImageUnknown using errors.Cause() * Fix crash when invalid COPY --from flag is specified. * Bump back to v1.12.0-dev - Changelog for v1.11.3 (2019-10-04) * Update c/image to v4.0.1 * Bump github.com/spf13/pflag from 1.0.3 to 1.0.5 * Fix --build-args handling * Bump github.com/spf13/cobra from 0.0.3 to 0.0.5 * Bump github.com/cyphar/filepath-securejoin from 0.2.1 to 0.2.2 * Bump github.com/onsi/ginkgo from 1.8.0 to 1.10.1 * Bump github.com/fsouza/go-dockerclient from 1.3.0 to 1.4.4 * Add support for retrieving context from stdin "-" * Ensure bud remote context cleans up on error * info: add cgroups2 * Bump github.com/seccomp/libseccomp-golang from 0.9.0 to 0.9.1 * Bump github.com/mattn/go-shellwords from 1.0.5 to 1.0.6 * Bump github.com/stretchr/testify from 1.3.0 to 1.4.0 * Bump github.com/opencontainers/selinux from 1.2.2 to 1.3.0 * Bump github.com/etcd-io/bbolt from 1.3.2 to 1.3.3 * Bump github.com/onsi/gomega from 1.5.0 to 1.7.0 * update c/storage to v1.13.4 * Print build 'STEP' line to stdout, not stderr * Fix travis-ci on forks * Vendor c/storage v1.13.3 * Use Containerfile by default * Added tutorial on how to include Buildah as library * util/util: Fix "configuraitno" -> "configuration" log typo * Bump back to v1.12.0-dev - Changelog for v1.11.2 (2019-09-13) * Add some cleanup code * Move devices code to unit specific directory. * Bump back to v1.12.0-dev - Changelog for v1.11.1 (2019-09-11) * Add --devices flag to bud and from * Downgrade .papr to highest atomic verion * Add support for /run/.containerenv * Truncate output of too long image names * Preserve file and directory mount permissions * Bump fedora version from 28 to 30 * makeImageRef: ignore EmptyLayer if Squash is set * Set TMPDIR to /var/tmp by default * replace --debug=false with --log-level=error * Allow mounts.conf entries for equal source and destination paths * fix label and annotation for 1-line Dockerfiles * Enable interfacer linter and fix lints * install.md: mention goproxy * Makefile: use go proxy * Bump to v1.12.0-dev - Changelog for v1.11.0 (2019-08-29) * tests/bud.bats: add --signature-policy to some tests * Vendor github.com/openshift/api * pull/commit/push: pay attention to $BUILD_REGISTRY_SOURCES * Add `--log-level` command line option and deprecate `--debug` * add support for cgroupsV2 * Correctly detect ExitError values from Run() * Disable empty logrus timestamps to reduce logger noise * Remove outdated deps Makefile target * Remove gofmt.sh in favor of golangci-lint * Remove govet.sh in favor of golangci-lint * Allow to override build date with SOURCE_DATE_EPOCH * Update shebangs to take env into consideration * Fix directory pull image names * Add --digestfile and Re-add push statement as debug * README: mention that Podman uses Buildah's API * Use content digests in ADD/COPY history entries * add: add a DryRun flag to AddAndCopyOptions * Fix possible runtime panic on bud * Add security-related volume options to validator * use correct path for ginkgo * Add bud 'without arguments' integration tests * Update documentation about bud * add: handle hard links when copying with .dockerignore * add: teach copyFileWithTar() about symlinks and directories * Allow buildah bud to be called without arguments * imagebuilder: fix detection of referenced stage roots * Touch up go mod instructions in install * run_linux: fix mounting /sys in a userns * Vendor Storage v1.13.2 * Cirrus: Update VM images * Fix handling of /dev/null masked devices * Update `bud`/`from` help to contain indicator for `--dns=none` * Bump back to v1.11.0-dev - Changelog for v1.10.1 (2019-08-08) * Bump containers/image to v3.0.2 to fix keyring issue * Bug fix for volume minus syntax * Bump container/storage v1.13.1 and containers/image v3.0.1 * bump github.com/containernetworking/cni to v0.7.1 * Add overlayfs to fuse-overlayfs tip * Add automatic apparmor tag discovery * Fix bug whereby --get-login has no effect * Bump to v1.11.0-dev - Changelog for v1.10.0 (2019-08-02) * vendor github.com/containers/image@v3.0.0 * Remove GO111MODULE in favor of `-mod=vendor` * Vendor in containers/storage v1.12.16 * Add '-' minus syntax for removal of config values * tests: enable overlay tests for rootless * rootless, overlay: use fuse-overlayfs * vendor github.com/containers/image@v2.0.1 * Added '-' syntax to remove volume config option * delete `successfully pushed` message * Add golint linter and apply fixes * vendor github.com/containers/storage@v1.12.15 * Change wait to sleep in buildahimage readme * Handle ReadOnly images when deleting images * Add support for listing read/only images - Changelog for v1.9.2 (2019-07-19) * from/import: record the base image's digest, if it has one * Fix CNI version retrieval to not require network connection * Add misspell linter and apply fixes * Add goimports linter and apply fixes * Add stylecheck linter and apply fixes * Add unconvert linter and apply fixes * image: make sure we don't try to use zstd compression * run.bats: skip the "z" flag when testing --mount * Update to runc v1.0.0-rc8 * Update to match updated runtime-tools API * bump github.com/opencontainers/runtime-tools to v0.9.0 * Build e2e tests using the proper build tags * Add unparam linter and apply fixes * Run: correct a typo in the --cap-add help text * unshare: add a --mount flag * fix push check image name is not empty * Bump to v1.9.2-dev - Changelog for v1.9.1 (2019-07-12) * add: fix slow copy with no excludes * Add errcheck linter and fix missing error check * Improve tests/tools/Makefile parallelism and abstraction * Fix response body not closed resource leak * Switch to golangci-lint * Add gomod instructions and mailing list links * On Masked path, check if /dev/null already mounted before mounting * Update to containers/storage v1.12.13 * Refactor code in package imagebuildah * Add rootless podman with NFS issue in documentation * Add --mount for buildah run * import method ValidateVolumeOpts from libpod * Fix typo * Makefile: set GO111MODULE=off * rootless: add the built-in slirp DNS server * Update docker/libnetwork to get rid of outdated sctp package * Update buildah-login.md * migrate to go modules * install.md: mention go modules * tests/tools: go module for test binaries * fix --volume splits comma delimited option * Add bud test for RUN with a priv'd command * vendor logrus v1.4.2 * pkg/cli: panic when flags can't be hidden * pkg/unshare: check all errors * pull: check error during report write * run_linux.go: ignore unchecked errors * conformance test: catch copy error * chroot/run_test.go: export funcs to actually be executed * tests/imgtype: ignore error when shutting down the store * testreport: check json error * bind/util.go: remove unused func * rm chroot/util.go * imagebuildah: remove unused `dedupeStringSlice` * StageExecutor: EnsureContainerPath: catch error from SecureJoin() * imagebuildah/build.go: return instead of branching * rmi: avoid redundant branching * conformance tests: nilness: allocate map * imagebuildah/build.go: avoid redundant `filepath.Join()` * imagebuildah/build.go: avoid redundant `os.Stat()` * imagebuildah: omit comparison to bool * fix "ineffectual assignment" lint errors * docker: ignore "repeats json tag" lint error * pkg/unshare: use `...` instead of iterating a slice * conformance: bud test: use raw strings for regexes * conformance suite: remove unused func/var * buildah test suite: remove unused vars/funcs * testreport: fix golangci-lint errors * util: remove redundant `return` statement * chroot: only log clean-up errors * images_test: ignore golangci-lint error * blobcache: log error when draining the pipe * imagebuildah: check errors in deferred calls * chroot: fix error handling in deferred funcs * cmd: check all errors * chroot/run_test.go: check errors * chroot/run.go: check errors in deferred calls * imagebuildah.Executor: remove unused onbuild field * docker/types.go: remove unused struct fields * util: use strings.ContainsRune instead of index check * Cirrus: Initial implementation * Bump to v1.9.1-dev - Changelog for v1.9.0 (2019-06-15) * buildah-run: fix-out-of-range panic (2) * Bump back to v1.9.0-dev - Changelog for v1.8.4 (2019-06-13) Update containers/image to v2.0.0 run: fix hang with run and --isolation=chroot run: fix hang when using run chroot: drop unused function call remove --> before imgageID on build Always close stdin pipe Write deny to setgroups when doing single user mapping Avoid including linux/memfd.h Add a test for the symlink pointing to a directory Add missing continue Fix the handling of symlinks to absolute paths Only set default network sysctls if not rootless Support --dns=none like podman fix bug --cpu-shares parsing typo Fix validate complaint Update vendor on containers/storage to v1.12.10 Create directory paths for COPY thereby ensuring correct perms imagebuildah: use a stable sort for comparing build args imagebuildah: tighten up cache checking bud.bats: add a test verying the order of --build-args add -t to podman run imagebuildah: simplify screening by top layers imagebuildah: handle ID mappings for COPY --from imagebuildah: apply additionalTags ourselves bud.bats: test additional tags with cached images bud.bats: add a test for WORKDIR and COPY with absolute destinations Cleanup Overlay Mounts content - Changelog for v1.8.3 (2019-06-04) * Add support for file secret mounts * Add ability to skip secrets in mounts file * allow 32bit builds * fix tutorial instructions * imagebuilder: pass the right contextDir to Add() * add: use fileutils.PatternMatcher for .dockerignore * bud.bats: add another .dockerignore test * unshare: fallback to single usermapping * addHelperSymlink: clear the destination on os.IsExist errors * bud.bats: test replacing symbolic links * imagebuildah: fix handling of destinations that end with '/' * bud.bats: test COPY with a final "/" in the destination * linux: add check for sysctl before using it * unshare: set _CONTAINERS_ROOTLESS_GID * Rework buildahimamges * build context: support https git repos * Add a test for ENV special chars behaviour * Check in new Dockerfiles * Apply custom SHELL during build time * config: expand variables only at the command line * SetEnv: we only need to expand v once * Add default /root if empty on chroot iso * Add support for Overlay volumes into the container. * Export buildah validate volume functions so it can share code with libpod * Bump baseline test to F30 * Fix rootless handling of /dev/shm size * Avoid fmt.Printf() in the library * imagebuildah: tighten cache checking back up * Handle WORKDIR with dangling target * Default Authfile to proper path * Make buildah run --isolation follow BUILDAH_ISOLATION environment * Vendor in latest containers/storage and containers/image * getParent/getChildren: handle layerless images * imagebuildah: recognize cache images for layerless images * bud.bats: test scratch images with --layers caching * Get CHANGELOG.md updates * Add some symlinks to test our .dockerignore logic * imagebuildah: addHelper: handle symbolic links * commit/push: use an everything-allowed policy * Correct manpage formatting in files section * Remove must be root statement from buildah doc * Change image names to stable, testing and upstream * Bump back to v1.9.0-dev - Changelog for v1.8.2 (2019-05-02) * Vendor Storage 1.12.6 * Create scratch file in TESTDIR * Test bud-copy-dot with --layers picks up changed file * Bump back to 1.9.0-dev - Changelog for v1.8.1 (2019-05-01) * Don't create directory on container * Replace kubernetes/pause in tests with k8s.gcr.io/pause * imagebuildah: don't remove intermediate images if we need them * Rework buildahimagegit to buildahimageupstream * Fix Transient Mounts * Handle WORKDIRs that are symlinks * allow podman to build a client for windows * Touch up 1.9-dev to 1.9.0-dev * Bump to 1.9-dev - Changelog for v1.8.0 (2019-04-26) * Resolve symlink when checking container path * commit: commit on every instruction, but not always with layers * CommitOptions: drop the unused OnBuild field * makeImageRef: pass in the whole CommitOptions structure * cmd: API cleanup: stores before images * run: check if SELinux is enabled * Fix buildahimages Dockerfiles to include support for additionalimages mounted from host. * Detect changes in rootdir * Fix typo in buildah-pull(1) * Vendor in latest containers/storage * Keep track of any build-args used during buildah bud --layers * commit: always set a parent ID * imagebuildah: rework unused-argument detection * fix bug dest path when COPY .dockerignore * Move Host IDMAppings code from util to unshare * Add BUILDAH_ISOLATION rootless back * Travis CI: fail fast, upon error in any step * imagebuildah: only commit images for intermediate stages if we have to * Use errors.Cause() when checking for IsNotExist errors * auto pass http_proxy to container * Bump back to 1.8-dev - Changelog for v1.7.3 (2019-04-16) * imagebuildah: don't leak image structs * Add Dockerfiles for buildahimages * Bump to Replace golang 1.10 with 1.12 * add --dns* flags to buildah bud * Add hack/build_speed.sh test speeds on building container images * Create buildahimage Dockerfile for Quay * rename 'is' to 'expect_output' * squash.bats: test squashing in multi-layered builds * bud.bats: test COPY --from in a Dockerfile while using the cache * commit: make target image names optional * Fix bud-args to allow comma separation * oops, missed some tests in commit.bats * new helper: expect_line_count * New tests for #1467 (string slices in cmdline opts) * Workarounds for dealing with travis; review feedback * BATS tests - extensive but minor cleanup * imagebuildah: defer pulling images for COPY --from * imagebuildah: centralize COMMIT and image ID output * Travis: do not use traviswait * imagebuildah: only initialize imagebuilder configuration once per stage * Make cleaner error on Dockerfile build errors * unshare: move to pkg/ * unshare: move some code from cmd/buildah/unshare * Fix handling of Slices versus Arrays * imagebuildah: reorganize stage and per-stage logic * imagebuildah: add empty layers for instructions * Add missing step in installing into Ubuntu * fix bug in .dockerignore support * imagebuildah: deduplicate prepended "FROM" instructions * Touch up intro * commit: set created-by to the shell if it isn't set * commit: check that we always set a "created-by" * docs/buildah.md: add "containers-" prefixes under "SEE ALSO" * Bump back to 1.8-dev - Changelog for v1.7.2 (2019-03-28) * mount: do not create automatically a namespace * buildah: correctly create the userns if euid!=0 * imagebuildah.Build: consolidate cleanup logic * CommitOptions: drop the redundant Store field * Move pkg/chrootuser from libpod to buildah. * imagebuildah: record image IDs and references more often * vendor imagebuilder v1.1.0 * imagebuildah: fix requiresStart/noRunsRemaining confusion * imagebuildah: check for unused args across stages * bump github.com/containernetworking/cni to v0.7.0-rc2 * imagebuildah: use "useCache" instead of "noCache" * imagebuildah.resolveNameToImageRef(): take name as a parameter * Export fields of the DokcerIgnore struct * imagebuildah: drop the duplicate containerIDs list * rootless: by default use the host network namespace * imagebuildah: split Executor and per-stage execution * imagebuildah: move some fields around * golint: make golint happy * docs: 01-intro.md: add missing . in Dockerfile examples * fix bug using .dockerignore * Do not create empty mounts.conf file * images: suppress a spurious blank line with no images * from: distinguish between ADD and COPY * fix bug to not separate each --label value with comma * buildah-bud.md: correct a typo, note a default * Remove mistaken code that got merged in other PR * add sample registries.conf to docs * escape shell variables in README example * slirp4netns: set mtu to 65520 * images: imageReposToMap() already adds : * imagebuildah.ReposToMap: move to cmd * Build: resolve copyFrom references earlier * Allow rootless users to use the cache directory in homedir * bud.bats: use the per-test temp directory * bud.bats: log output before counting length * Simplify checks for leftover args * Print commitID with --layers * fix bug images use the template to print results * rootless: honor --net host * onsi/gomeage add missing files * vendor latest openshift/imagebuilder * Remove noop from squash help * Prepend a comment to files setup in container * imagebuildah resolveSymlink: fix handling of relative links * Errors should be printed to stderr * Add recommends for slirp4netns and fuse-overlay * Update pull and pull-always flags * Hide from users command options that we don't want them to use. * Update secrets fipsmode patch to work on rootless containers * fix unshare option handling and documentation * Vendor in latest containers/storage * Hard-code docker.Transport use in pull --all-tags * Use a types.ImageReference instead of (transport, name) strings in pullImage etc. * Move the computation of srcRef before first pullAndFindImage * Don't throw away user-specified tag for pull --all-tags * CHANGES BEHAVIOR: Remove the string format input to localImageNameForReference * Don't try to parse imageName as transport:image in pullImage * Use reference.WithTag instead of manual string manipulation in Pull * Don't pass image = transport:repo:tag, transport=transport to pullImage * Fix confusing variable naming in Pull * Don't try to parse image name as a transport:image * Fix error reporting when parsing trans+image * Remove 'transport == ""' handling from the pull path * Clean up "pulls" of local image IDs / ID prefixes * Simplify ExpandNames * Document the semantics of transport+name returned by ResolveName * UPdate gitvalidation epoch * Bump back to 1.8-dev - Changelog for v1.7.1 (2019-02-26) * vendor containers/image v1.5 * Move secrets code from libpod into buildah * Update CHANGELOG.md with the past changes * README.md: fix typo * Fix a few issues found by tests/validate/gometalinter.sh * Neutralize buildah/unshare on non-Linux platforms * Explicitly specify a directory to find(1) * README.md: rephrase Buildah description * Stop printing default twice in cli --help * install.md: add section about vendoring * Bump to 1.8-dev - Changelog for v1.7 (2019-02-21) * vendor containers/image v1.4 * Make "images --all" faster * Remove a misleading comment * Remove quiet option from pull options * Make sure buildah pull --all-tags only works with docker transport * Support oci layout format * Fix pulling of images within buildah * Fix tls-verify polarity * Travis: execute make vendor and hack/tree_status.sh * vendor.conf: remove unused dependencies * add missing vendor/github.com/containers/libpod/vendor.conf * vendor.conf: remove github.com/inconshreveable/mousetrap * make vendor: always fetch the latest vndr * add hack/tree_status.sh script * Bump c/Storage to 1.10 * Add --all-tags test to pull * mount: make error clearer * Remove global flags from cli help * Set --disable-compression to true as documented * Help document using buildah mount in rootless mode * healthcheck start-period: update documentation * Vendor in latest c/storage and c/image * dumpbolt: handle nested buckets * Fix buildah commit compress by default * Test on xenial, not trusty * unshare: reexec using a memfd copy instead of the binary * Add --target to bud command * Fix example for setting multiple environment variables * main: fix rootless mode * buildah: force umask 022 * pull.bats: specify registry config when using registries * pull.bats: use the temporary directory, not /tmp * unshare: do not set rootless mode if euid=0 * Touch up cli help examples and a few nits * Add an undocumented dumpbolt command * Move tar commands into containers/storage * Fix bud issue with 2 line Dockerfile * Add package install descriptions * Note configuration file requirements * Replace urfave/cli with cobra * cleanup vendor.conf * Vendor in latest containers/storage * Add Quiet to PullOptions and PushOptions * cmd/commit: add flag omit-timestamp to allow for deterministic builds * Add options for empty-layer history entries * Make CLI help descriptions and usage a bit more consistent * vndr opencontainers/selinux * Bump baseline test Fedora to 29 * Bump to v1.7-dev-1 * Bump to v1.6-1 * Add support for ADD --chown * imagebuildah: make EnsureContainerPath() check/create the right one * Bump 1.7-dev * Fix contrib/rpm/bulidah.spec changelog date - Changelog for v1.6-1 (2019-01-18) * Add support for ADD --chown * imagebuildah: make EnsureContainerPath() check/create the right one * Fix contrib/rpm/bulidah.spec changelog date * Vendor in latest containers/storage * Revendor everything * Revendor in latest code by release * unshare: do not set USER=root * run: ignore EIO when flushing at the end, avoid double log * build-using-dockerfile,commit: disable compression by default * Update some comments * Make rootless work under no_pivot_root * Add CreatedAtRaw date field for use with Format * Properly format images JSON output * pull: add all-tags option * Fix support for multiple Short options * pkg/blobcache: add synchronization * Skip empty files in file check of conformance test * Use NoPivot also for RUN, not only for run * Remove no longer used isReferenceInsecure / isRegistryInsecure * Do not set OCIInsecureSkipTLSVerify based on registries.conf * Remove duplicate entries from images JSON output * vendor parallel-copy from containers/image * blobcache.bats: adjust explicit push tests * Handle one line Dockerfile with layers * We should only warn if user actually requests Hostname be set in image * Fix compiler Warning about comparing different size types * imagebuildah: don't walk if rootdir and path are equal * Add aliases for buildah containers, so buildah list, ls and ps work * vendor: use faster version instead compress/gzip * vendor: update libpod * Properly handle Hostname inside of RUN command * docs: mention how to mount in rootless mode * tests: use fully qualified name for centos image * travis.yml: use the fully qualified name for alpine * mount: allow mount only when using vfs * Add some tests for buildah pull * Touch up images -q processing * Refactor: Use library shared idtools.ParseIDMap() instead of bundling it * bump GITVALIDATE_EPOCH * cli.BudFlags: add `--platform` nop * Makefile: allow packagers to more easily add tags * Makefile: soften the requirement on git * tests: add containers json test * Inline blobCache.putBlob into blobCacheDestination.PutBlob * Move saveStream and putBlob near blobCacheDestination.PutBlob * Remove BlobCache.PutBlob * Update for API changes * Vendor c/image after merging c/image#536 * Handle 'COPY --from' in Dockerfile * Vendor in latest content from github.com/containers/storage * Clarify docker.io default in push with docker-daemon * Test blob caching * Wire in a hidden --blob-cache option * Use a blob cache when we're asked to use one * Add --disable-compression to 'build-using-dockerfile' * Add a blob cache implementation * vendor: update containers/storage * Update for sysregistriesv2 API changes * Update containers/image to 63a1cbdc5e6537056695cf0d627c0a33b334df53 * clean up makefile variables * Fix file permission * Complete the instructions for the command * Show warning when a build arg not used * Assume user 0 group 0, if /etc/passwd file in container. * Add buildah info command * Enable -q when --filter is used for images command * Add v1.5 Release Announcement * Fix dangling filter for images command * Fix completions to print Names as well as IDs * tests: Fix file permissions * Bump 1.6-dev - Changelog for v1.5-1 (2018-11-21) * Bump min go to 1.10 in install.md * vendor: update ostree-go * Update docker build command line in conformance test * Print command in SystemExec as debug information * Add some skip word for inspect check in conformance test * Update regex for multi stage base test * Sort CLI flags * vendor: update containers/storage * Add note to install about non-root on RHEL/CentOS * Update imagebuild depdency to support heading ARGs in Dockerfile * rootless: do not specify --rootless to the OCI runtime * Export resolvesymlink function * Exclude --force-rm from common bud cli flags * run: bind mount /etc/hosts and /etc/resolv.conf if not in a volume * rootless: use slirp4netns to setup the network namespace * Instructions for completing the pull command * Fix travis to not run environment variable patch * rootless: only discard network configuration names * run: only set up /etc/hosts or /etc/resolv.conf with network * common: getFormat: match entire string not only the prefix * vendor: update libpod * Change validation EPOCH * Fixing broken link for container-registries.conf * Restore rootless isolation test for from volume ro test * ostree: fix tag for build constraint * Handle directories better in bud -f * vndr in latest containers/storage * Fix unshare gofmt issue * runSetupBuiltinVolumes(): break up volume setup * common: support a per-user registries conf file * unshare: do not override the configuration * common: honor the rootless configuration file * unshare: create a new mount namespace * unshare: support libpod rootless pkg * Use libpod GetDefaultStorage to report proper storage config * Allow container storage to manage the SELinux labels * Resolve image names with default transport in from command * run: When the value of isolation is set, use the set value instead of the default value. * Vendor in latest containers/storage and opencontainers/selinux * Remove no longer valid todo * Check for empty buildTime in version * Change gofmt so it runs on all but 1.10 * Run gofmt only on Go 1.11 * Walk symlinks when checking cached images for copied/added files * ReserveSELinuxLabels(): handle wrapped errors from OpenBuilder * Set WorkingDir to empty, not / for conformance * Update calls in e2e to addres 1101 * imagebuilder.BuildDockerfiles: return the image ID * Update for changes in the containers/image API * bump(github.com/containers/image) * Allow setting --no-pivot default with an env var * Add man page and bash completion, for --no-pivot * Add the --no-pivot flag to the run command * Improve reporting about individual pull failures * Move the "short name but no search registries" error handling to resolveImage * Return a "search registries were needed but empty" indication in util.ResolveName * Simplify handling of the "tried to pull an image but found nothing" case in newBuilder * Don't even invoke the pull loop if options.FromImage == "" * Eliminate the long-running ref and img variables in resolveImage * In resolveImage, return immediately on success * Fix From As in Dockerfile * Vendor latest containers/image * Vendor in latest libpod * Sort CLI flags of buildah bud * Change from testing with golang 1.9 to 1.11. * unshare: detect when unprivileged userns are disabled * Optimize redundant code * fix missing format param * chroot: fix the args check * imagebuildah: make ResolveSymLink public * Update copy chown test * buildah: use the same logic for XDG_RUNTIME_DIR as podman * V1.4 Release Announcement * Podman --privileged selinux is broken * papr: mount source at gopath * parse: Modify the return value * parse: modify the verification of the isolation value * Make sure we log or return every error * pullImage(): when completing an image name, try docker:// * Fix up Tutorial 3 to account for format * Vendor in latest containers/storage and containers/image * docs/tutorials/01-intro.md: enhanced installation instructions * Enforce "blocked" for registries for the "docker" transport * Correctly set DockerInsecureSkipTLSVerify when pulling images * chroot: set up seccomp and capabilities after supplemental groups * chroot: fix capabilities list setup and application * .papr.yml: log the podman version * namespaces.bats: fix handling of uidmap/gidmap options in pairs * chroot: only create user namespaces when we know we need them * Check /proc/sys/user/max_user_namespaces on unshare(NEWUSERNS) * bash/buildah: add isolation option to the from command - Changelog for v1.4 (2018-10-02) * from: fix isolation option * Touchup pull manpage * Export buildah ReserveSELinuxLables so podman can use it * Add buildah.io to README.md and doc fixes * Update rmi man for prune changes * Ignore file not found removal error in bud * bump(github.com/containers/{storage,image}) * NewImageSource(): only create one Diff() at a time * Copy ExposedPorts from base image into the config * tests: run conformance test suite in Travis * Change rmi --prune to not accept an imageID * Clear intermediate container IDs after each stage * Request podman version for build issues * unshare: keep the additional groups of the user * Builtin volumes should be owned by the UID/GID of the container * Get rid of dangling whitespace in markdown files * Move buildah from projecatatomic/buildah to containers/buildah * nitpick: parse.validateFlags loop in bud cli * bash: Completion options * Add signature policy to push tests * vendor in latest containers/image * Fix grammar in Container Tools Guide * Don't build btrfs if it is not installed * new: Return image-pulling errors from resolveImage * pull: Return image-pulling errors from pullImage * Add more volume mount tests * chroot: create missing parent directories for volume mounts * Push: Allow an empty destination * Add Podman relationship to readme, create container tools guide * Fix arg usage in buildah-tag * Add flags/arguments order verification to other commands * Handle ErrDuplicateName errors from store.CreateContainer() * Evaluate symbolic links on Add/Copy Commands * Vendor in latest containers/image and containers/storage * Retain bounding set when running containers as non root * run container-diff tests in Travis * buildah-images.md: Fix option contents * push: show image digest after push succeed * Vendor in latest containers/storage,image,libpod and runc * Change references to cri-o to point at new repository * Exclude --layers from the common bug cli flags * demos: Increase the executable permissions * run: clear default seccomp filter if not enabled * Bump maximum cyclomatic complexity to 45 * stdin: on HUP, read everything * nitpick: use tabs in tests/helpers.bash * Add flags/arguments order verification to one arg commands * nitpick: decrease cognitive complexity in buildah-bud * rename: Avoid renaming the same name as other containers * chroot isolation: chroot() before setting up seccomp * Small nitpick at the "if" condition in tag.go * cmd/images: Modify json option * cmd/images: Disallow the input of image when using the -a option * Fix examples to include context directory * Update containers/image to fix commit layer issue * cmd/containers: End loop early when using the json option * Make buildah-from error message clear when flags are after arg * Touch up README.md for conformance tests * Update container/storage for lock fix * cmd/rm: restore the correct containerID display * Remove debug lines * Remove docker build image after each test * Add README for conformance test * Update the MakeOptions to accept all command options for buildah * Update regrex to fit the docker output in test "run with JSON" * cmd/buildah: Remove redundant variable declarations * Warn about using Commands in Dockerfile that are not supported by OCI. * Add buildah bud conformance test * Fix rename to also change container name in builder * Makefile: use $(GO) env-var everywhere * Cleanup code to more closely match Docker Build images * Document BUILDAH_* environment variables in buildah bud --help output * Return error immediately if error occurs in Prepare step * Fix --layers ADD from url issue * Add "Sign your PRs" TOC item to contributing.md. * Display the correct ID after deleting image * rmi: Modify the handling of errors * Let util.ResolveName() return parsing errors * Explain Open Container Initiative (OCI) acronym, add link * Update vendor for urfave/cli back to master * Handle COPY --chown in Dockerfile * Switch to Recommends container-selinux * Update vendor for containernetworking, imagebuildah and podman * Document STORAGE_DRIVER and STORAGE_OPTS environment variable * Change references to projectatomic/libpod to containers/libpod * Add container PATH retrieval example * Expand variables names for --env * imagebuildah: provide a way to provide stdin for RUN * Remove an unused srcRef.NewImageSource in pullImage * chroot: correct a comment * chroot: bind mount an empty directory for masking * Don't bother with --no-pivot for rootless isolation * CentOS need EPEL repo * Export a Pull() function * Remove stream options, since docker build does not have it * release v1.3: mention openSUSE * Add Release Announcements directory * Bump to v1.4-dev - Changelog for v1.3 (2018-08-04) * Revert pull error handling from 881 * bud should not search context directory for Dockerfile * Set BUILDAH_ISOLATION=rootless when running unprivileged * .papr.sh: Also test with BUILDAH_ISOLATION=rootless * Skip certain tests when we're using "rootless" isolation * .travis.yml: run integration tests with BUILDAH_ISOLATION=chroot * Add and implement IsolationOCIRootless * Add a value for IsolationOCIRootless * Fix rmi to remove intermediate images associated with an image * Return policy error on pull * Update containers/image to 216acb1bcd2c1abef736ee322e17147ee2b7d76c * Switch to github.com/containers/image/pkg/sysregistriesv2 * unshare: make adjusting the OOM score optional * Add flags validation * chroot: handle raising process limits * chroot: make the resource limits name map module-global * Remove rpm.bats, we need to run this manually * Set the default ulimits to match Docker * buildah: no args is out of bounds * unshare: error message missed the pid * preprocess ".in" suffixed Dockerfiles * Fix the the in buildah-config man page * Only test rpmbuild on latest fedora * Add support for multiple Short options * Update to latest urvave/cli * Add additional SELinux tests * Vendor in latest github.com/containers/{image;storage} * Stop testing with golang 1.8 * Fix volume cache issue with buildah bud --layers * Create buildah pull command * Increase the deadline for gometalinter during 'make validate' * .papr.sh: Also test with BUILDAH_ISOLATION=chroot * .travis.yml: run integration tests with BUILDAH_ISOLATION=chroot * Add a Dockerfile * Set BUILDAH_ISOLATION=chroot when running unprivileged * Add and implement IsolationChroot * Update github.com/opencontainers/runc * maybeReexecUsingUserNamespace: add a default for root * Allow ping command without NET_RAW Capabilities * rmi.storageImageID: fix Wrapf format warning * Allow Dockerfile content to come from stdin * Vendor latest container/storage to fix overlay mountopt * userns: assign additional IDs sequentially * Remove default dev/pts * Add OnBuild test to baseline test * tests/run.bats(volumes): use :z when SELinux is enabled * Avoid a stall in runCollectOutput() * Use manifest from container/image * Vendor in latest containers/image and containers/storage * add rename command * Completion command * Update CHANGELOG.md * Update vendor for runc to fix 32 bit builds * bash completion: remove shebang * Update vendor for runc to fix 32 bit builds - Changelog for v1.2 (2018-07-14) * Vendor in lates containers/image * build-using-dockerfile: let -t include transports again * Block use of /proc/acpi and /proc/keys from inside containers * Fix handling of --registries-conf * Fix becoming a maintainer link * add optional CI test fo darwin * Don't pass a nil error to errors.Wrapf() * image filter test: use kubernetes/pause as a "since" * Add --cidfile option to from * vendor: update containers/storage * Contributors need to find the CONTRIBUTOR.md file easier * Add a --loglevel option to build-with-dockerfile * Create Development plan * cmd: Code improvement * allow buildah cross compile for a darwin target * Add unused function param lint check * docs: Follow man-pages(7) suggestions for SYNOPSIS * Start using github.com/seccomp/containers-golang * umount: add all option to umount all mounted containers * runConfigureNetwork(): remove an unused parameter * Update github.com/opencontainers/selinux * Fix buildah bud --layers * Force ownership of /etc/hosts and /etc/resolv.conf to 0:0 * main: if unprivileged, reexec in a user namespace * Vendor in latest imagebuilder * Reduce the complexity of the buildah.Run function * mount: output it before replacing lastError * Vendor in latest selinux-go code * Implement basic recognition of the "--isolation" option * Run(): try to resolve non-absolute paths using $PATH * Run(): don't include any default environment variables * build without seccomp * vendor in latest runtime-tools * bind/mount_unsupported.go: remove import errors * Update github.com/opencontainers/runc * Add Capabilities lists to BuilderInfo * Tweaks for commit tests * commit: recognize committing to second storage locations * Fix ARGS parsing for run commands * Add info on registries.conf to from manpage * Switch from using docker to podman for testing in .papr * buildah: set the HTTP User-Agent * ONBUILD tutorial * Add information about the configuration files to the install docs * Makefile: add uninstall * Add tilde info for push to troubleshooting * mount: support multiple inputs * Use the right formatting when adding entries to /etc/hosts * Vendor in latest go-selinux bindings * Allow --userns-uid-map/--userns-gid-map to be global options * bind: factor out UnmountMountpoints * Run(): simplify runCopyStdio() * Run(): handle POLLNVAL results * Run(): tweak terminal mode handling * Run(): rename 'copyStdio' to 'copyPipes' * Run(): don't set a Pdeathsig for the runtime * Run(): add options for adding and removing capabilities * Run(): don't use a callback when a slice will do * setupSeccomp(): refactor * Change RunOptions.Stdin/Stdout/Stderr to just be Reader/Writers * Escape use of '_' in .md docs * Break out getProcIDMappings() * Break out SetupIntermediateMountNamespace() * Add Multi From Demo * Use the c/image conversion code instead of converting configs manually * Don't throw away the manifest MIME type and guess again * Consolidate loading manifest and config in initConfig * Pass a types.Image to Builder.initConfig * Require an image ID in importBuilderDataFromImage * Use c/image/manifest.GuessMIMEType instead of a custom heuristic * Do not ignore any parsing errors in initConfig * Explicitly handle "from scratch" images in Builder.initConfig * Fix parsing of OCI images * Simplify dead but dangerous-looking error handling * Don't ignore v2s1 history if docker_version is not set * Add --rm and --force-rm to buildah bud * Add --all,-a flag to buildah images * Separate stdio buffering from writing * Remove tty check from images --format * Add environment variable BUILDAH_RUNTIME * Add --layers and --no-cache to buildah bud * Touch up images man * version.md: fix DESCRIPTION * tests: add containers test * tests: add images test * images: fix usage * fix make clean error * Change 'registries' to 'container registries' in man * add commit test * Add(): learn to record hashes of what we add * Minor update to buildah config documentation for entrypoint * Bump to v1.2-dev * Add registries.conf link to a few man pages - Changelog for v1.1 (2018-06-08) * Drop capabilities if running container processes as non root * Print Warning message if cmd will not be used based on entrypoint * Update 01-intro.md * Shouldn't add insecure registries to list of search registries * Report errors on bad transports specification when pushing images * Move parsing code out of common for namespaces and into pkg/parse.go * Add disable-content-trust noop flag to bud * Change freenode chan to buildah * runCopyStdio(): don't close stdin unless we saw POLLHUP * Add registry errors for pull * runCollectOutput(): just read until the pipes are closed on us * Run(): provide redirection for stdio * rmi, rm: add test * add mount test * Add parameter judgment for commands that do not require parameters * Add context dir to bud command in baseline test * run.bats: check that we can run with symlinks in the bundle path * Give better messages to users when image can not be found * use absolute path for bundlePath * Add environment variable to buildah --format * rm: add validation to args and all option * Accept json array input for config entrypoint * Run(): process RunOptions.Mounts, and its flags * Run(): only collect error output from stdio pipes if we created some * Add OnBuild support for Dockerfiles * Quick fix on demo readme * run: fix validate flags * buildah bud should require a context directory or URL * Touchup tutorial for run changes * Validate common bud and from flags * images: Error if the specified imagename does not exist * inspect: Increase err judgments to avoid panic * add test to inspect * buildah bud picks up ENV from base image * Extend the amount of time travis_wait should wait * Add a make target for Installing CNI plugins * Add tests for namespace control flags * copy.bats: check ownerships in the container * Fix SELinux test errors when SELinux is enabled * Add example CNI configurations * Run: set supplemental group IDs * Run: use a temporary mount namespace * Use CNI to configure container networks * add/secrets/commit: Use mappings when setting permissions on added content * Add CLI options for specifying namespace and cgroup setup * Always set mappings when using user namespaces * Run(): break out creation of stdio pipe descriptors * Read UID/GID mapping information from containers and images * Additional bud CI tests * Run integration tests under travis_wait in Travis * build-using-dockerfile: add --annotation * Implement --squash for build-using-dockerfile and commit * Vendor in latest container/storage for devicemapper support * add test to inspect * Vendor github.com/onsi/ginkgo and github.com/onsi/gomega * Test with Go 1.10, too * Add console syntax highlighting to troubleshooting page * bud.bats: print "$output" before checking its contents * Manage "Run" containers more closely * Break Builder.Run()'s "run runc" bits out * util.ResolveName(): handle completion for tagged/digested image names * Handle /etc/hosts and /etc/resolv.conf properly in container * Documentation fixes * Make it easier to parse our temporary directory as an image name * Makefile: list new pkg/ subdirectoris as dependencies for buildah * containerImageSource: return more-correct errors * API cleanup: PullPolicy and TerminalPolicy should be types * Make "run --terminal" and "run -t" aliases for "run --tty" * Vendor github.com/containernetworking/cni v0.6.0 * Update github.com/containers/storage * Update github.com/projectatomic/libpod * Add support for buildah bud --label * buildah push/from can push and pull images with no reference * Vendor in latest containers/image * Update gometalinter to fix install.tools error * Update troubleshooting with new run workaround * Added a bud demo and tidied up * Attempt to download file from url, if fails assume Dockerfile * Add buildah bud CI tests for ENV variables * Re-enable rpm .spec version check and new commit test * Update buildah scratch demo to support el7 * Added Docker compatibility demo * Update to F28 and new run format in baseline test * Touchup man page short options across man pages * Added demo dir and a demo. chged distrorlease * builder-inspect: fix format option * Add cpu-shares short flag (-c) and cpu-shares CI tests * Minor fixes to formatting in rpm spec changelog * Fix rpm .spec changelog formatting * CI tests and minor fix for cache related noop flags * buildah-from: add effective value to mount propagation - Changelog for v1.0 (2018-05-06) * Declare Buildah 1.0 * Add cache-from and no-cache noops, and fix doco * Update option and documentation for --force-rm * Adding noop for --force-rm to match --rm * Add buildah bud ENTRYPOINT,CMD,RUN tests * Adding buildah bud RUN test scenarios * Extend tests for empty buildah run command * Fix formatting error in run.go * Update buildah run to make command required * Expanding buildah run cmd/entrypoint tests * Update test cases for buildah run behaviour * Remove buildah run cmd and entrypoint execution * Add Files section with registries.conf to pertinent man pages * tests/config: perfect test * tests/from: add name test * Do not print directly to stdout in Commit() * Touch up auth test commands * Force "localhost" as a default registry * Drop util.GetLocalTime() * Vendor in latest containers/image * Validate host and container paths passed to --volume * test/from: add add-host test * Add --compress, --rm, --squash flags as a noop for bud * Add FIPS mode secret to buildah run and bud * Add config --comment/--domainname/--history-comment/--hostname * 'buildah config': stop replacing Created-By whenever it's not specified * Modify man pages so they compile correctly in mandb * Add description on how to do --isolation to buildah-bud man page * Add support for --iidfile to bud and commit * Refactor buildah bud for vendoring * Fail if date or git not installed * Revert update of entrypoint behaviour to match docker * Vendor in latest imagebuilder code to fix multiple stage builds * Add /bin/sh -c to entrypoint in config * image_test: Improve the test * Fix README example of buildah config * buildah-image: add validation to 'format' * Simple changes to allow buildah to pass make validate * Clarify the use of buildah config options * containers_test: Perfect testing * buildah images and podman images are listing different sizes * buildah-containers: add tests and example to the man page * buildah-containers: add validation to 'format' * Clarify the use of buildah config options * Minor fix for lighttpd example in README * Add tls-verification to troubleshooting * Modify buildah rmi to account for changes in containers/storage * Vendor in latest containers/image and containers/storage * addcopy: add src validation * Remove tarball as an option from buildah push --help * Fix secrets patch * Update entrypoint behaviour to match docker * Display imageId after commit * config: add support for StopSignal * Fix docker login issue in travis.yml * Allow referencing stages as index and names * Add multi-stage builds tests * Add multi-stage builds support * Add accessor functions for comment and stop signal * Vendor in latest imagebuilder, to get mixed case AS support * Allow umount to have multi-containers * Update buildah push doc * buildah bud walks symlinks * Imagename is required for commit atm, update manpage - Changelog for v0.16.0 (2018-04-08) * Bump to v0.16.0 * Remove requires for ostree-lib in rpm spec file * Add support for shell * buildah.spec should require ostree-libs * Vendor in latest containers/image * bash: prefer options * Change image time to locale, add troubleshooting.md, add logo to other mds * buildah-run.md: fix error SYNOPSIS * docs: fix error example * Allow --cmd parameter to have commands as values * Touchup README to re-enable logo * Clean up README.md * Make default-mounts-file a hidden option * Document the mounts.conf file * Fix man pages to format correctly * Add various transport support to buildah from * Add unit tests to run.go * If the user overrides the storage driver, the options should be dropped * Show Config/Manifest as JSON string in inspect when format is not set * Switch which for that in README.md * Remove COPR * Fix wrong order of parameters * Vendor in latest containers/image * Remove shallowCopy(), which shouldn't be saving us time any more * shallowCopy: avoid a second read of the container's layer ================================================ FILE: chroot/run_common.go ================================================ //go:build linux || freebsd package chroot import ( "bytes" "encoding/json" "fmt" "io" "os" "os/exec" "os/signal" "path/filepath" "runtime" "slices" "strconv" "strings" "sync" "syscall" "github.com/containers/buildah/bind" "github.com/containers/buildah/internal/pty" "github.com/containers/buildah/util" "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" "go.podman.io/storage/pkg/ioutils" "go.podman.io/storage/pkg/reexec" "go.podman.io/storage/pkg/unshare" "golang.org/x/sys/unix" "golang.org/x/term" ) const ( // runUsingChrootCommand is a command we use as a key for reexec runUsingChrootCommand = "buildah-chroot-runtime" // runUsingChrootExec is a command we use as a key for reexec runUsingChrootExecCommand = "buildah-chroot-exec" ) func init() { reexec.Register(runUsingChrootCommand, runUsingChrootMain) reexec.Register(runUsingChrootExecCommand, runUsingChrootExecMain) for limitName, limitNumber := range rlimitsMap { rlimitsReverseMap[limitNumber] = limitName } } type runUsingChrootExecSubprocOptions struct { Spec *specs.Spec BundlePath string NoPivot bool } // RunUsingChroot runs a chrooted process, using some of the settings from the // passed-in spec, and using the specified bundlePath to hold temporary files, // directories, and mountpoints. func RunUsingChroot(spec *specs.Spec, bundlePath, homeDir string, stdin io.Reader, stdout, stderr io.Writer, noPivot bool) (err error) { var confwg sync.WaitGroup var homeFound bool for _, env := range spec.Process.Env { if strings.HasPrefix(env, "HOME=") { homeFound = true break } } if !homeFound { spec.Process.Env = append(spec.Process.Env, fmt.Sprintf("HOME=%s", homeDir)) } runtime.LockOSThread() defer runtime.UnlockOSThread() // Write the runtime configuration, mainly for debugging. specbytes, err := json.Marshal(spec) if err != nil { return err } if err = ioutils.AtomicWriteFile(filepath.Join(bundlePath, "config.json"), specbytes, 0o600); err != nil { return fmt.Errorf("storing runtime configuration: %w", err) } logrus.Debugf("config = %v", string(specbytes)) // Default to using stdin/stdout/stderr if we weren't passed objects to use. if stdin == nil { stdin = os.Stdin } if stdout == nil { stdout = os.Stdout } if stderr == nil { stderr = os.Stderr } // Create a pipe for passing configuration down to the next process. preader, pwriter, err := os.Pipe() if err != nil { return fmt.Errorf("creating configuration pipe: %w", err) } config, conferr := json.Marshal(runUsingChrootSubprocOptions{ Spec: spec, BundlePath: bundlePath, NoPivot: noPivot, }) if conferr != nil { return fmt.Errorf("encoding configuration for %q: %w", runUsingChrootCommand, conferr) } // Set our terminal's mode to raw, to pass handling of special // terminal input to the terminal in the container. if spec.Process.Terminal && term.IsTerminal(unix.Stdin) { state, err := term.MakeRaw(unix.Stdin) if err != nil { logrus.Warnf("error setting terminal state: %v", err) } else { defer func() { if err = term.Restore(unix.Stdin, state); err != nil { logrus.Errorf("unable to restore terminal state: %v", err) } }() } } // Raise any resource limits that are higher than they are now, before // we drop any more privileges. if err = setRlimits(spec, false, true); err != nil { return err } // Start the grandparent subprocess. cmd := unshare.Command(runUsingChrootCommand) setPdeathsig(cmd.Cmd) cmd.Stdin, cmd.Stdout, cmd.Stderr = stdin, stdout, stderr cmd.Dir = "/" cmd.Env = []string{fmt.Sprintf("LOGLEVEL=%d", logrus.GetLevel())} interrupted := make(chan os.Signal, 100) cmd.Hook = func(int) error { signal.Notify(interrupted, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM) go func() { for receivedSignal := range interrupted { if err := cmd.Process.Signal(receivedSignal); err != nil { logrus.Infof("%v while attempting to forward %v to child process", err, receivedSignal) } } }() return nil } logrus.Debugf("Running %#v in %#v", cmd.Cmd, cmd) confwg.Add(1) go func() { _, conferr = io.Copy(pwriter, bytes.NewReader(config)) pwriter.Close() confwg.Done() }() cmd.ExtraFiles = append([]*os.File{preader}, cmd.ExtraFiles...) err = cmd.Run() confwg.Wait() signal.Stop(interrupted) close(interrupted) if err == nil { return conferr } return err } // main() for grandparent subprocess. Its main job is to shuttle stdio back // and forth, managing a pseudo-terminal if we want one, for our child, the // parent subprocess. func runUsingChrootMain() { var options runUsingChrootSubprocOptions runtime.LockOSThread() // Set logging. if level := os.Getenv("LOGLEVEL"); level != "" { if ll, err := strconv.Atoi(level); err == nil { logrus.SetLevel(logrus.Level(ll)) } os.Unsetenv("LOGLEVEL") } // Unpack our configuration. confPipe := os.NewFile(3, "confpipe") if confPipe == nil { fmt.Fprintf(os.Stderr, "error reading options pipe\n") os.Exit(1) } defer confPipe.Close() if err := json.NewDecoder(confPipe).Decode(&options); err != nil { fmt.Fprintf(os.Stderr, "error decoding options: %v\n", err) os.Exit(1) } if options.Spec == nil || options.Spec.Process == nil { fmt.Fprintf(os.Stderr, "invalid options spec in runUsingChrootMain\n") os.Exit(1) } noPivot := options.NoPivot // Prepare to shuttle stdio back and forth. rootUID32, rootGID32, err := util.GetHostRootIDs(options.Spec) if err != nil { logrus.Errorf("error determining ownership for container stdio") os.Exit(1) } rootUID := int(rootUID32) rootGID := int(rootGID32) relays := make(map[int]int) closeOnceRunning := []*os.File{} var ctty *os.File var stdin io.Reader var stdinCopy io.WriteCloser var stdout io.Writer var stderr io.Writer fdDesc := make(map[int]string) if options.Spec.Process.Terminal { ptyMasterFd, ptyFd, err := pty.GetPtyDescriptors() if err != nil { logrus.Errorf("error opening PTY descriptors: %v", err) os.Exit(1) } // Make notes about what's going where. relays[ptyMasterFd] = unix.Stdout relays[unix.Stdin] = ptyMasterFd fdDesc[ptyMasterFd] = "container terminal" fdDesc[unix.Stdin] = "stdin" fdDesc[unix.Stdout] = "stdout" winsize := &unix.Winsize{} // Set the pseudoterminal's size to the configured size, or our own. if options.Spec.Process.ConsoleSize != nil { // Use configured sizes. winsize.Row = uint16(options.Spec.Process.ConsoleSize.Height) winsize.Col = uint16(options.Spec.Process.ConsoleSize.Width) } else { if term.IsTerminal(unix.Stdin) { // Use the size of our terminal. winsize, err = unix.IoctlGetWinsize(unix.Stdin, unix.TIOCGWINSZ) if err != nil { logrus.Debugf("error reading current terminal's size") winsize.Row = 0 winsize.Col = 0 } } } if winsize.Row != 0 && winsize.Col != 0 { if err = unix.IoctlSetWinsize(ptyFd, unix.TIOCSWINSZ, winsize); err != nil { logrus.Warnf("error setting terminal size for pty") } // FIXME - if we're connected to a terminal, we should // be passing the updated terminal size down when we // receive a SIGWINCH. } // Open an *os.File object that we can pass to our child. ctty = os.NewFile(uintptr(ptyFd), "/dev/tty") // Set ownership for the PTY. if err = ctty.Chown(rootUID, rootGID); err != nil { var cttyInfo unix.Stat_t err2 := unix.Fstat(ptyFd, &cttyInfo) from := "" op := "setting" if err2 == nil { op = "changing" from = fmt.Sprintf("from %d/%d ", cttyInfo.Uid, cttyInfo.Gid) } logrus.Warnf("error %s ownership of container PTY %sto %d/%d: %v", op, from, rootUID, rootGID, err) } // Set permissions on the PTY. if err = ctty.Chmod(0o620); err != nil { logrus.Errorf("error setting permissions of container PTY: %v", err) os.Exit(1) } // Make a note that our child (the parent subprocess) should // have the PTY connected to its stdio, and that we should // close it once it's running. stdin = ctty stdout = ctty stderr = ctty closeOnceRunning = append(closeOnceRunning, ctty) } else { // Create pipes for stdio. stdinRead, stdinWrite, err := os.Pipe() if err != nil { logrus.Errorf("error opening pipe for stdin: %v", err) } stdoutRead, stdoutWrite, err := os.Pipe() if err != nil { logrus.Errorf("error opening pipe for stdout: %v", err) } stderrRead, stderrWrite, err := os.Pipe() if err != nil { logrus.Errorf("error opening pipe for stderr: %v", err) } // Make notes about what's going where. relays[unix.Stdin] = int(stdinWrite.Fd()) relays[int(stdoutRead.Fd())] = unix.Stdout relays[int(stderrRead.Fd())] = unix.Stderr fdDesc[int(stdinWrite.Fd())] = "container stdin pipe" fdDesc[int(stdoutRead.Fd())] = "container stdout pipe" fdDesc[int(stderrRead.Fd())] = "container stderr pipe" fdDesc[unix.Stdin] = "stdin" fdDesc[unix.Stdout] = "stdout" fdDesc[unix.Stderr] = "stderr" // Set ownership for the pipes. if err = stdinRead.Chown(rootUID, rootGID); err != nil { logrus.Errorf("error setting ownership of container stdin pipe: %v", err) os.Exit(1) } if err = stdoutWrite.Chown(rootUID, rootGID); err != nil { logrus.Errorf("error setting ownership of container stdout pipe: %v", err) os.Exit(1) } if err = stderrWrite.Chown(rootUID, rootGID); err != nil { logrus.Errorf("error setting ownership of container stderr pipe: %v", err) os.Exit(1) } // Make a note that our child (the parent subprocess) should // have the pipes connected to its stdio, and that we should // close its ends of them once it's running. stdin = stdinRead stdout = stdoutWrite stderr = stderrWrite closeOnceRunning = append(closeOnceRunning, stdinRead, stdoutWrite, stderrWrite) stdinCopy = stdinWrite defer stdoutRead.Close() defer stderrRead.Close() } for readFd, writeFd := range relays { if err := unix.SetNonblock(readFd, true); err != nil { logrus.Errorf("error setting descriptor %d (%s) non-blocking: %v", readFd, fdDesc[readFd], err) return } if err := unix.SetNonblock(writeFd, false); err != nil { logrus.Errorf("error setting descriptor %d (%s) blocking: %v", relays[writeFd], fdDesc[writeFd], err) return } } if err := unix.SetNonblock(relays[unix.Stdin], true); err != nil { logrus.Errorf("error setting %d to nonblocking: %v", relays[unix.Stdin], err) } go func() { buffers := make(map[int]*bytes.Buffer) for _, writeFd := range relays { buffers[writeFd] = new(bytes.Buffer) } pollTimeout := -1 stdinClose := false for len(relays) > 0 { fds := make([]unix.PollFd, 0, len(relays)) for fd := range relays { fds = append(fds, unix.PollFd{Fd: int32(fd), Events: unix.POLLIN | unix.POLLHUP}) } _, err := unix.Poll(fds, pollTimeout) if !util.LogIfNotRetryable(err, fmt.Sprintf("poll: %v", err)) { return } removeFds := make(map[int]struct{}) for _, rfd := range fds { if rfd.Revents&unix.POLLHUP == unix.POLLHUP { removeFds[int(rfd.Fd)] = struct{}{} } if rfd.Revents&unix.POLLNVAL == unix.POLLNVAL { logrus.Debugf("error polling descriptor %s: closed?", fdDesc[int(rfd.Fd)]) removeFds[int(rfd.Fd)] = struct{}{} } if rfd.Revents&unix.POLLIN == 0 { if stdinClose && stdinCopy == nil { continue } continue } b := make([]byte, 8192) nread, err := unix.Read(int(rfd.Fd), b) util.LogIfNotRetryable(err, fmt.Sprintf("read %s: %v", fdDesc[int(rfd.Fd)], err)) if nread > 0 { if wfd, ok := relays[int(rfd.Fd)]; ok { nwritten, err := buffers[wfd].Write(b[:nread]) if err != nil { logrus.Debugf("buffer: %v", err) continue } if nwritten != nread { logrus.Debugf("buffer: expected to buffer %d bytes, wrote %d", nread, nwritten) continue } } // If this is the last of the data we'll be able to read // from this descriptor, read as much as there is to read. for rfd.Revents&unix.POLLHUP == unix.POLLHUP { nr, err := unix.Read(int(rfd.Fd), b) util.LogIfUnexpectedWhileDraining(err, fmt.Sprintf("read %s: %v", fdDesc[int(rfd.Fd)], err)) if nr <= 0 { break } if wfd, ok := relays[int(rfd.Fd)]; ok { nwritten, err := buffers[wfd].Write(b[:nr]) if err != nil { logrus.Debugf("buffer: %v", err) break } if nwritten != nr { logrus.Debugf("buffer: expected to buffer %d bytes, wrote %d", nr, nwritten) break } } } } if nread == 0 { removeFds[int(rfd.Fd)] = struct{}{} } } pollTimeout = -1 for wfd, buffer := range buffers { if buffer.Len() > 0 { nwritten, err := unix.Write(wfd, buffer.Bytes()) util.LogIfNotRetryable(err, fmt.Sprintf("write %s: %v", fdDesc[wfd], err)) if nwritten >= 0 { _ = buffer.Next(nwritten) } } if buffer.Len() > 0 { pollTimeout = 100 } if wfd == relays[unix.Stdin] && stdinClose && buffer.Len() == 0 { stdinCopy.Close() delete(relays, unix.Stdin) } } for rfd := range removeFds { if rfd == unix.Stdin { buffer, found := buffers[relays[unix.Stdin]] if found && buffer.Len() > 0 { stdinClose = true continue } } if !options.Spec.Process.Terminal && rfd == unix.Stdin { stdinCopy.Close() } delete(relays, rfd) } } }() // Set up mounts and namespaces, and run the parent subprocess. status, err := runUsingChroot(options.Spec, options.BundlePath, ctty, stdin, stdout, stderr, noPivot, closeOnceRunning) if err != nil { fmt.Fprintf(os.Stderr, "error running subprocess: %v\n", err) os.Exit(1) } // Pass the process's exit status back to the caller by exiting with the same status. if status.Exited() { if status.ExitStatus() != 0 { fmt.Fprintf(os.Stderr, "subprocess exited with status %d\n", status.ExitStatus()) } os.Exit(status.ExitStatus()) } else if status.Signaled() { fmt.Fprintf(os.Stderr, "subprocess exited on %s\n", status.Signal()) os.Exit(1) } } // runUsingChroot, still in the grandparent process, sets up various bind // mounts and then runs the parent process in its own user namespace with the // necessary ID mappings. func runUsingChroot(spec *specs.Spec, bundlePath string, ctty *os.File, stdin io.Reader, stdout, stderr io.Writer, noPivot bool, closeOnceRunning []*os.File) (wstatus unix.WaitStatus, err error) { var confwg sync.WaitGroup // Create a new mount namespace for ourselves and bind mount everything to a new location. undoIntermediates, err := bind.SetupIntermediateMountNamespace(spec, bundlePath) if err != nil { return 1, err } defer func() { if undoErr := undoIntermediates(); undoErr != nil { logrus.Debugf("error cleaning up intermediate mount NS: %v", err) } }() // Bind mount in our filesystems. undoChroots, err := setupChrootBindMounts(spec, bundlePath) if err != nil { return 1, err } defer func() { if undoErr := undoChroots(); undoErr != nil { logrus.Debugf("error cleaning up intermediate chroot bind mounts: %v", err) } }() // Create a pipe for passing configuration down to the next process. preader, pwriter, err := os.Pipe() if err != nil { return 1, fmt.Errorf("creating configuration pipe: %w", err) } config, conferr := json.Marshal(runUsingChrootExecSubprocOptions{ Spec: spec, BundlePath: bundlePath, NoPivot: noPivot, }) if conferr != nil { fmt.Fprintf(os.Stderr, "error re-encoding configuration for %q\n", runUsingChrootExecCommand) os.Exit(1) } // Apologize for the namespace configuration that we're about to ignore. logNamespaceDiagnostics(spec) // We need to lock the thread so that PR_SET_PDEATHSIG won't trigger if the current thread exits. runtime.LockOSThread() defer runtime.UnlockOSThread() // Start the parent subprocess. cmd := unshare.Command(append([]string{runUsingChrootExecCommand}, spec.Process.Args...)...) setPdeathsig(cmd.Cmd) cmd.Stdin, cmd.Stdout, cmd.Stderr = stdin, stdout, stderr cmd.Dir = "/" cmd.Env = []string{fmt.Sprintf("LOGLEVEL=%d", logrus.GetLevel())} if ctty != nil { cmd.Setsid = true cmd.Ctty = ctty } cmd.ExtraFiles = append([]*os.File{preader}, cmd.ExtraFiles...) if err := setPlatformUnshareOptions(spec, cmd); err != nil { return 1, fmt.Errorf("setting platform unshare options: %w", err) } interrupted := make(chan os.Signal, 100) cmd.Hook = func(int) error { for _, f := range closeOnceRunning { f.Close() } signal.Notify(interrupted, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM) go func() { for receivedSignal := range interrupted { if err := cmd.Process.Signal(receivedSignal); err != nil { logrus.Infof("%v while attempting to forward %v to child process", err, receivedSignal) } } }() return nil } logrus.Debugf("Running %#v in %#v", cmd.Cmd, cmd) confwg.Add(1) go func() { _, conferr = io.Copy(pwriter, bytes.NewReader(config)) pwriter.Close() confwg.Done() }() err = cmd.Run() confwg.Wait() signal.Stop(interrupted) close(interrupted) if err != nil { if exitError, ok := err.(*exec.ExitError); ok { if waitStatus, ok := exitError.ProcessState.Sys().(syscall.WaitStatus); ok { if waitStatus.Exited() { if waitStatus.ExitStatus() != 0 { fmt.Fprintf(os.Stderr, "subprocess exited with status %d\n", waitStatus.ExitStatus()) } os.Exit(waitStatus.ExitStatus()) } else if waitStatus.Signaled() { fmt.Fprintf(os.Stderr, "subprocess exited on %s\n", waitStatus.Signal()) os.Exit(1) } } } fmt.Fprintf(os.Stderr, "process exited with error: %v\n", err) os.Exit(1) } return 0, nil } // main() for parent subprocess. Its main job is to try to make our // environment look like the one described by the runtime configuration blob, // and then launch the intended command as a child. func runUsingChrootExecMain() { args := os.Args[1:] var options runUsingChrootExecSubprocOptions var err error runtime.LockOSThread() // Set logging. if level := os.Getenv("LOGLEVEL"); level != "" { if ll, err := strconv.Atoi(level); err == nil { logrus.SetLevel(logrus.Level(ll)) } os.Unsetenv("LOGLEVEL") } // Unpack our configuration. confPipe := os.NewFile(3, "confpipe") if confPipe == nil { fmt.Fprintf(os.Stderr, "error reading options pipe\n") os.Exit(1) } defer confPipe.Close() if err := json.NewDecoder(confPipe).Decode(&options); err != nil { fmt.Fprintf(os.Stderr, "error decoding options: %v\n", err) os.Exit(1) } // Set the hostname. We're already in a distinct UTS namespace and are admins in the user // namespace which created it, so we shouldn't get a permissions error, but seccomp policy // might deny our attempt to call sethostname() anyway, so log a debug message for that. if options.Spec == nil || options.Spec.Process == nil { fmt.Fprintf(os.Stderr, "invalid options spec passed in\n") os.Exit(1) } if options.Spec.Hostname != "" { setContainerHostname(options.Spec.Hostname) } // Try to chroot into the root. Do this before we potentially // block the syscall via the seccomp profile. Allow the // platform to override this - on FreeBSD, we use a simple // jail to set the hostname in the container, and on Linux // we attempt to pivot_root. if err := createPlatformContainer(options); err != nil { logrus.Debugf("createPlatformContainer: %v", err) var oldst, newst unix.Stat_t if err := unix.Stat(options.Spec.Root.Path, &oldst); err != nil { fmt.Fprintf(os.Stderr, "error stat()ing intended root directory %q: %v\n", options.Spec.Root.Path, err) os.Exit(1) } if err := unix.Chdir(options.Spec.Root.Path); err != nil { fmt.Fprintf(os.Stderr, "error chdir()ing to intended root directory %q: %v\n", options.Spec.Root.Path, err) os.Exit(1) } if err := unix.Chroot(options.Spec.Root.Path); err != nil { fmt.Fprintf(os.Stderr, "error chroot()ing into directory %q: %v\n", options.Spec.Root.Path, err) os.Exit(1) } if err := unix.Stat("/", &newst); err != nil { fmt.Fprintf(os.Stderr, "error stat()ing current root directory: %v\n", err) os.Exit(1) } if oldst.Dev != newst.Dev || oldst.Ino != newst.Ino { fmt.Fprintf(os.Stderr, "unknown error chroot()ing into directory %q: %v\n", options.Spec.Root.Path, err) os.Exit(1) } logrus.Debugf("chrooted into %q", options.Spec.Root.Path) } // not doing because it's still shared: creating devices // not doing because it's not applicable: setting annotations // not doing because it's still shared: setting sysctl settings // not doing because cgroupfs is read only: configuring control groups // -> this means we can use the freezer to make sure there aren't any lingering processes // -> this means we ignore cgroups-based controls // not doing because we don't set any in the config: running hooks // not doing because we don't set it in the config: setting rootfs read-only // not doing because we don't set it in the config: setting rootfs propagation logrus.Debugf("setting apparmor profile") if err = setApparmorProfile(options.Spec); err != nil { fmt.Fprintf(os.Stderr, "error setting apparmor profile for process: %v\n", err) os.Exit(1) } if err = setSelinuxLabel(options.Spec); err != nil { fmt.Fprintf(os.Stderr, "error setting SELinux label for process: %v\n", err) os.Exit(1) } logrus.Debugf("setting resource limits") if err = setRlimits(options.Spec, false, false); err != nil { fmt.Fprintf(os.Stderr, "error setting process resource limits for process: %v\n", err) os.Exit(1) } // Try to change to the directory. cwd := options.Spec.Process.Cwd if !filepath.IsAbs(cwd) { cwd = "/" + cwd } cwd = filepath.Clean(cwd) if err := unix.Chdir("/"); err != nil { fmt.Fprintf(os.Stderr, "error chdir()ing into new root directory %q: %v\n", options.Spec.Root.Path, err) os.Exit(1) } if err := unix.Chdir(cwd); err != nil { fmt.Fprintf(os.Stderr, "error chdir()ing into directory %q under root %q: %v\n", cwd, options.Spec.Root.Path, err) os.Exit(1) } logrus.Debugf("changed working directory to %q", cwd) // Drop privileges. user := options.Spec.Process.User if len(user.AdditionalGids) > 0 { gids := make([]int, len(user.AdditionalGids)) for i := range user.AdditionalGids { gids[i] = int(user.AdditionalGids[i]) } logrus.Debugf("setting supplemental groups") if err = syscall.Setgroups(gids); err != nil { fmt.Fprintf(os.Stderr, "error setting supplemental groups list: %v\n", err) os.Exit(1) } } else { setgroups, _ := os.ReadFile("/proc/self/setgroups") if strings.Trim(string(setgroups), "\n") != "deny" { logrus.Debugf("clearing supplemental groups") if err = syscall.Setgroups([]int{}); err != nil { fmt.Fprintf(os.Stderr, "error clearing supplemental groups list: %v\n", err) os.Exit(1) } } } logrus.Debugf("setting gid") if err = unix.Setresgid(int(user.GID), int(user.GID), int(user.GID)); err != nil { fmt.Fprintf(os.Stderr, "error setting GID: %v\n", err) os.Exit(1) } if err = setSeccomp(options.Spec); err != nil { fmt.Fprintf(os.Stderr, "error setting seccomp filter for process: %v\n", err) os.Exit(1) } logrus.Debugf("setting capabilities") var keepCaps []string if user.UID != 0 { keepCaps = []string{"CAP_SETUID"} } if err := setCapabilities(options.Spec, keepCaps...); err != nil { fmt.Fprintf(os.Stderr, "error setting capabilities for process: %v\n", err) os.Exit(1) } logrus.Debugf("setting uid") if err = unix.Setresuid(int(user.UID), int(user.UID), int(user.UID)); err != nil { fmt.Fprintf(os.Stderr, "error setting UID: %v\n", err) os.Exit(1) } // Set $PATH to the value for the container, so that when args[0] is not an absolute path, // exec.Command() can find it using exec.LookPath(). for _, env := range slices.Backward(options.Spec.Process.Env) { if val, ok := strings.CutPrefix(env, "PATH="); ok { os.Setenv("PATH", val) break } } // Actually run the specified command. cmd := exec.Command(args[0], args[1:]...) setPdeathsig(cmd) cmd.Env = options.Spec.Process.Env cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr cmd.Dir = cwd logrus.Debugf("Running %#v (PATH = %q)", cmd, os.Getenv("PATH")) interrupted := make(chan os.Signal, 100) if err = cmd.Start(); err != nil { fmt.Fprintf(os.Stderr, "process failed to start with error: %v\n", err) } go func() { for range interrupted { if err := cmd.Process.Signal(syscall.SIGKILL); err != nil { logrus.Infof("%v while attempting to send SIGKILL to child process", err) } } }() signal.Notify(interrupted, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM) err = cmd.Wait() signal.Stop(interrupted) close(interrupted) if err != nil { if exitError, ok := err.(*exec.ExitError); ok { if waitStatus, ok := exitError.ProcessState.Sys().(syscall.WaitStatus); ok { if waitStatus.Exited() { if waitStatus.ExitStatus() != 0 { fmt.Fprintf(os.Stderr, "subprocess exited with status %d\n", waitStatus.ExitStatus()) } os.Exit(waitStatus.ExitStatus()) } else if waitStatus.Signaled() { fmt.Fprintf(os.Stderr, "subprocess exited on %s\n", waitStatus.Signal()) os.Exit(1) } } } fmt.Fprintf(os.Stderr, "process exited with error: %v\n", err) os.Exit(1) } } // parses the resource limits for ourselves and any processes that // we'll start into a format that's more in line with the kernel APIs func parseRlimits(spec *specs.Spec) (map[int]unix.Rlimit, error) { if spec.Process == nil { return nil, nil } parsed := make(map[int]unix.Rlimit) for _, limit := range spec.Process.Rlimits { resource, recognized := rlimitsMap[strings.ToUpper(limit.Type)] if !recognized { return nil, fmt.Errorf("parsing limit type %q", limit.Type) } parsed[resource] = makeRlimit(limit) } return parsed, nil } // setRlimits sets any resource limits that we want to apply to processes that // we'll start. func setRlimits(spec *specs.Spec, onlyLower, onlyRaise bool) error { limits, err := parseRlimits(spec) if err != nil { return err } for resource, desired := range limits { var current unix.Rlimit if err := unix.Getrlimit(resource, ¤t); err != nil { return fmt.Errorf("reading %q limit: %w", rlimitsReverseMap[resource], err) } if desired.Max > current.Max && onlyLower { // this would raise a hard limit, and we're only here to lower them continue } if desired.Max < current.Max && onlyRaise { // this would lower a hard limit, and we're only here to raise them continue } if err := unix.Setrlimit(resource, &desired); err != nil { return fmt.Errorf("setting %q limit to soft=%d,hard=%d (was soft=%d,hard=%d): %w", rlimitsReverseMap[resource], desired.Cur, desired.Max, current.Cur, current.Max, err) } } return nil } func isDevNull(dev os.FileInfo) bool { if dev.Mode()&os.ModeCharDevice != 0 { stat, _ := dev.Sys().(*syscall.Stat_t) nullStat := syscall.Stat_t{} if err := syscall.Stat(os.DevNull, &nullStat); err != nil { logrus.Warnf("unable to stat /dev/null: %v", err) return false } if stat.Rdev == nullStat.Rdev { return true } } return false } ================================================ FILE: chroot/run_freebsd.go ================================================ //go:build freebsd package chroot import ( "errors" "fmt" "io" "io/fs" "os" "os/exec" "path/filepath" "strings" "syscall" "github.com/containers/buildah/pkg/jail" "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" "go.podman.io/storage/pkg/fileutils" "go.podman.io/storage/pkg/mount" "go.podman.io/storage/pkg/unshare" "golang.org/x/sys/unix" ) var ( rlimitsMap = map[string]int{ "RLIMIT_AS": unix.RLIMIT_AS, "RLIMIT_CORE": unix.RLIMIT_CORE, "RLIMIT_CPU": unix.RLIMIT_CPU, "RLIMIT_DATA": unix.RLIMIT_DATA, "RLIMIT_FSIZE": unix.RLIMIT_FSIZE, "RLIMIT_MEMLOCK": unix.RLIMIT_MEMLOCK, "RLIMIT_NOFILE": unix.RLIMIT_NOFILE, "RLIMIT_NPROC": unix.RLIMIT_NPROC, "RLIMIT_RSS": unix.RLIMIT_RSS, "RLIMIT_STACK": unix.RLIMIT_STACK, } rlimitsReverseMap = map[int]string{} ) type runUsingChrootSubprocOptions struct { Spec *specs.Spec BundlePath string NoPivot bool } func setPlatformUnshareOptions(spec *specs.Spec, cmd *unshare.Cmd) error { return nil } func setContainerHostname(name string) { // On FreeBSD, we have to set this later when we create the // jail below in createPlatformContainer } func setSelinuxLabel(spec *specs.Spec) error { // Ignore this on FreeBSD return nil } func setApparmorProfile(spec *specs.Spec) error { // FreeBSD doesn't have apparmor` return nil } func setCapabilities(spec *specs.Spec, keepCaps ...string) error { // FreeBSD capabilities are nothing like Linux return nil } func makeRlimit(limit specs.POSIXRlimit) unix.Rlimit { return unix.Rlimit{Cur: int64(limit.Soft), Max: int64(limit.Hard)} } func createPlatformContainer(options runUsingChrootExecSubprocOptions) error { path := options.Spec.Root.Path jconf := jail.NewConfig() jconf.Set("name", filepath.Base(path)+"-chroot") jconf.Set("host.hostname", options.Spec.Hostname) jconf.Set("persist", false) jconf.Set("path", path) jconf.Set("ip4", jail.INHERIT) jconf.Set("ip6", jail.INHERIT) jconf.Set("allow.raw_sockets", true) jconf.Set("enforce_statfs", 1) _, err := jail.CreateAndAttach(jconf) if err != nil { return fmt.Errorf("creating jail: %w", err) } return nil } // logNamespaceDiagnostics knows which namespaces we want to create. // Output debug messages when that differs from what we're being asked to do. func logNamespaceDiagnostics(spec *specs.Spec) { // Nothing here for FreeBSD } func makeReadOnly(mntpoint string, flags uintptr) error { var fs unix.Statfs_t // Make sure it's read-only. if err := unix.Statfs(mntpoint, &fs); err != nil { return fmt.Errorf("checking if directory %q was bound read-only: %w", mntpoint, err) } return nil } func saveDir(spec *specs.Spec, path string) string { id := filepath.Base(spec.Root.Path) return filepath.Join(filepath.Dir(path), ".save-"+id) } func copyFile(source, dest string) error { in, err := os.Open(source) if err != nil { return err } defer in.Close() out, err := os.Create(dest) if err != nil { return err } defer out.Close() _, err = io.Copy(out, in) if err != nil { return err } return out.Close() } type rename struct { from, to string } // setupChrootBindMounts actually bind mounts things under the rootfs, and returns a // callback that will clean up its work. func setupChrootBindMounts(spec *specs.Spec, bundlePath string) (undoBinds func() error, err error) { renames := []rename{} unmounts := []string{} removes := []string{} undoBinds = func() error { for _, r := range renames { if err2 := os.Rename(r.to, r.from); err2 != nil { logrus.Warnf("pkg/chroot: error renaming %q to %q: %v", r.to, r.from, err2) if err == nil { err = err2 } } } for _, path := range unmounts { if err2 := mount.Unmount(path); err2 != nil { logrus.Warnf("pkg/chroot: error unmounting %q: %v", spec.Root.Path, err2) if err == nil { err = err2 } } } for _, path := range removes { if err2 := os.Remove(path); err2 != nil { logrus.Warnf("pkg/chroot: error removing %q: %v", path, err2) if err == nil { err = err2 } } } return err } // Now mount all of those things to be under the rootfs's location in this // mount namespace. for _, m := range spec.Mounts { // If the target is there, we can just mount it. var srcinfo os.FileInfo switch m.Type { case "nullfs": srcinfo, err = os.Stat(m.Source) if err != nil { return undoBinds, fmt.Errorf("examining %q for mounting in mount namespace: %w", m.Source, err) } } target := filepath.Join(spec.Root.Path, m.Destination) if err := fileutils.Exists(target); err != nil { // If the target can't be stat()ted, check the error. if !errors.Is(err, fs.ErrNotExist) { return undoBinds, fmt.Errorf("examining %q for mounting in mount namespace: %w", target, err) } // The target isn't there yet, so create it, and make a // note to remove it later. // XXX: This was copied from the linux version which supports bind mounting files. // Leaving it here since I plan to add this to FreeBSD's nullfs. if m.Type != "nullfs" || srcinfo.IsDir() { if err = os.MkdirAll(target, 0o111); err != nil { return undoBinds, fmt.Errorf("creating mountpoint %q in mount namespace: %w", target, err) } removes = append(removes, target) } else { if err = os.MkdirAll(filepath.Dir(target), 0o111); err != nil { return undoBinds, fmt.Errorf("ensuring parent of mountpoint %q (%q) is present in mount namespace: %w", target, filepath.Dir(target), err) } // Don't do this until we can support file mounts in nullfs /*var file *os.File if file, err = os.OpenFile(target, os.O_WRONLY|os.O_CREATE, 0); err != nil { return undoBinds, errors.Wrapf(err, "error creating mountpoint %q in mount namespace", target) } file.Close() removes = append(removes, target)*/ } } logrus.Debugf("mount: %v", m) switch m.Type { case "nullfs": // Do the bind mount. if !srcinfo.IsDir() { logrus.Debugf("emulating file mount %q on %q", m.Source, target) err := fileutils.Exists(target) if err == nil { save := saveDir(spec, target) if err := fileutils.Exists(save); err != nil { if errors.Is(err, fs.ErrNotExist) { err = os.MkdirAll(save, 0o111) } if err != nil { return undoBinds, fmt.Errorf("creating file mount save directory %q: %w", save, err) } removes = append(removes, save) } savePath := filepath.Join(save, filepath.Base(target)) if err := fileutils.Exists(target); err == nil { logrus.Debugf("moving %q to %q", target, savePath) if err := os.Rename(target, savePath); err != nil { return undoBinds, fmt.Errorf("moving %q to %q: %w", target, savePath, err) } renames = append(renames, rename{ from: target, to: savePath, }) } } else { removes = append(removes, target) } if err := copyFile(m.Source, target); err != nil { return undoBinds, fmt.Errorf("copying %q to %q: %w", m.Source, target, err) } } else { logrus.Debugf("bind mounting %q on %q", m.Destination, filepath.Join(spec.Root.Path, m.Destination)) if err := mount.Mount(m.Source, target, "nullfs", strings.Join(m.Options, ",")); err != nil { return undoBinds, fmt.Errorf("bind mounting %q from host to %q in mount namespace (%q): %w", m.Source, m.Destination, target, err) } logrus.Debugf("bind mounted %q to %q", m.Source, target) unmounts = append(unmounts, target) } case "devfs", "fdescfs", "tmpfs": // Mount /dev, /dev/fd. if err := mount.Mount(m.Source, target, m.Type, strings.Join(m.Options, ",")); err != nil { return undoBinds, fmt.Errorf("mounting %q to %q in mount namespace (%q, %q): %w", m.Type, m.Destination, target, strings.Join(m.Options, ","), err) } logrus.Debugf("mounted a %q to %q", m.Type, target) unmounts = append(unmounts, target) } } return undoBinds, nil } // setPdeathsig sets a parent-death signal for the process func setPdeathsig(cmd *exec.Cmd) { if cmd.SysProcAttr == nil { cmd.SysProcAttr = &syscall.SysProcAttr{} } cmd.SysProcAttr.Pdeathsig = syscall.SIGKILL } ================================================ FILE: chroot/run_linux.go ================================================ //go:build linux package chroot import ( "errors" "fmt" "os" "os/exec" "path/filepath" "slices" "strings" "syscall" "time" "github.com/containers/buildah/copier" "github.com/moby/sys/capability" "github.com/opencontainers/runc/libcontainer/apparmor" "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" "go.podman.io/storage/pkg/mount" "go.podman.io/storage/pkg/unshare" "golang.org/x/sys/unix" ) var ( rlimitsMap = map[string]int{ "RLIMIT_AS": unix.RLIMIT_AS, "RLIMIT_CORE": unix.RLIMIT_CORE, "RLIMIT_CPU": unix.RLIMIT_CPU, "RLIMIT_DATA": unix.RLIMIT_DATA, "RLIMIT_FSIZE": unix.RLIMIT_FSIZE, "RLIMIT_LOCKS": unix.RLIMIT_LOCKS, "RLIMIT_MEMLOCK": unix.RLIMIT_MEMLOCK, "RLIMIT_MSGQUEUE": unix.RLIMIT_MSGQUEUE, "RLIMIT_NICE": unix.RLIMIT_NICE, "RLIMIT_NOFILE": unix.RLIMIT_NOFILE, "RLIMIT_NPROC": unix.RLIMIT_NPROC, "RLIMIT_RSS": unix.RLIMIT_RSS, "RLIMIT_RTPRIO": unix.RLIMIT_RTPRIO, "RLIMIT_RTTIME": unix.RLIMIT_RTTIME, "RLIMIT_SIGPENDING": unix.RLIMIT_SIGPENDING, "RLIMIT_STACK": unix.RLIMIT_STACK, } rlimitsReverseMap = map[int]string{} mountFlagMap = map[int]string{ unix.MS_ACTIVE: "MS_ACTIVE", unix.MS_BIND: "MS_BIND", unix.MS_BORN: "MS_BORN", unix.MS_DIRSYNC: "MS_DIRSYNC", unix.MS_KERNMOUNT: "MS_KERNMOUNT", unix.MS_LAZYTIME: "MS_LAZYTIME", unix.MS_MANDLOCK: "MS_MANDLOCK", unix.MS_MOVE: "MS_MOVE", unix.MS_NOATIME: "MS_NOATIME", unix.MS_NODEV: "MS_NODEV", unix.MS_NODIRATIME: "MS_NODIRATIME", unix.MS_NOEXEC: "MS_NOEXEC", unix.MS_NOREMOTELOCK: "MS_NOREMOTELOCK", unix.MS_NOSEC: "MS_NOSEC", unix.MS_NOSUID: "MS_NOSUID", unix.MS_NOSYMFOLLOW: "MS_NOSYMFOLLOW", unix.MS_NOUSER: "MS_NOUSER", unix.MS_POSIXACL: "MS_POSIXACL", unix.MS_PRIVATE: "MS_PRIVATE", unix.MS_RDONLY: "MS_RDONLY", unix.MS_REC: "MS_REC", unix.MS_RELATIME: "MS_RELATIME", unix.MS_REMOUNT: "MS_REMOUNT", unix.MS_SHARED: "MS_SHARED", unix.MS_SILENT: "MS_SILENT", unix.MS_SLAVE: "MS_SLAVE", unix.MS_STRICTATIME: "MS_STRICTATIME", unix.MS_SUBMOUNT: "MS_SUBMOUNT", unix.MS_SYNCHRONOUS: "MS_SYNCHRONOUS", unix.MS_UNBINDABLE: "MS_UNBINDABLE", } statFlagMap = map[int]string{ unix.ST_MANDLOCK: "ST_MANDLOCK", unix.ST_NOATIME: "ST_NOATIME", unix.ST_NODEV: "ST_NODEV", unix.ST_NODIRATIME: "ST_NODIRATIME", unix.ST_NOEXEC: "ST_NOEXEC", unix.ST_NOSUID: "ST_NOSUID", unix.ST_RDONLY: "ST_RDONLY", unix.ST_RELATIME: "ST_RELATIME", unix.ST_SYNCHRONOUS: "ST_SYNCHRONOUS", } ) func mountFlagNames(flags uintptr) []string { var names []string for flag, name := range mountFlagMap { if int(flags)&flag == flag { names = append(names, name) flags = flags &^ (uintptr(flag)) } } if flags != 0 { // got some unknown leftovers names = append(names, fmt.Sprintf("%#x", flags)) } slices.Sort(names) return names } func statFlagNames(flags uintptr) []string { var names []string flags = flags & ^uintptr(0x20) // mask off ST_VALID for flag, name := range statFlagMap { if int(flags)&flag == flag { names = append(names, name) flags = flags &^ (uintptr(flag)) } } if flags != 0 { // got some unknown leftovers names = append(names, fmt.Sprintf("%#x", flags)) } slices.Sort(names) return names } type runUsingChrootSubprocOptions struct { Spec *specs.Spec BundlePath string NoPivot bool UIDMappings []syscall.SysProcIDMap GIDMappings []syscall.SysProcIDMap } func setPlatformUnshareOptions(spec *specs.Spec, cmd *unshare.Cmd) error { // If we have configured ID mappings, set them here so that they can apply to the child. hostUidmap, hostGidmap, err := unshare.GetHostIDMappings("") if err != nil { return err } uidmap, gidmap := spec.Linux.UIDMappings, spec.Linux.GIDMappings if len(uidmap) == 0 { // No UID mappings are configured for the container. Borrow our parent's mappings. uidmap = slices.Clone(hostUidmap) for i := range uidmap { uidmap[i].HostID = uidmap[i].ContainerID } } if len(gidmap) == 0 { // No GID mappings are configured for the container. Borrow our parent's mappings. gidmap = slices.Clone(hostGidmap) for i := range gidmap { gidmap[i].HostID = gidmap[i].ContainerID } } cmd.UnshareFlags = syscall.CLONE_NEWUTS | syscall.CLONE_NEWNS requestedUserNS := false for _, ns := range spec.Linux.Namespaces { if ns.Type == specs.UserNamespace { requestedUserNS = true } } if len(spec.Linux.UIDMappings) > 0 || len(spec.Linux.GIDMappings) > 0 || requestedUserNS { cmd.UnshareFlags = cmd.UnshareFlags | syscall.CLONE_NEWUSER cmd.UidMappings = uidmap cmd.GidMappings = gidmap cmd.GidMappingsEnableSetgroups = true } cmd.OOMScoreAdj = spec.Process.OOMScoreAdj return nil } func setContainerHostname(name string) { if err := unix.Sethostname([]byte(name)); err != nil { logrus.Debugf("failed to set hostname %q for process: %v", name, err) } } // logNamespaceDiagnostics knows which namespaces we want to create. // Output debug messages when that differs from what we're being asked to do. func logNamespaceDiagnostics(spec *specs.Spec) { sawMountNS := false sawUTSNS := false for _, ns := range spec.Linux.Namespaces { switch ns.Type { case specs.CgroupNamespace: if ns.Path != "" { logrus.Debugf("unable to join cgroup namespace, sorry about that") } else { logrus.Debugf("unable to create cgroup namespace, sorry about that") } case specs.IPCNamespace: if ns.Path != "" { logrus.Debugf("unable to join IPC namespace, sorry about that") } else { logrus.Debugf("unable to create IPC namespace, sorry about that") } case specs.MountNamespace: if ns.Path != "" { logrus.Debugf("unable to join mount namespace %q, creating a new one", ns.Path) } sawMountNS = true case specs.NetworkNamespace: if ns.Path != "" { logrus.Debugf("unable to join network namespace, sorry about that") } else { logrus.Debugf("unable to create network namespace, sorry about that") } case specs.PIDNamespace: if ns.Path != "" { logrus.Debugf("unable to join PID namespace, sorry about that") } else { logrus.Debugf("unable to create PID namespace, sorry about that") } case specs.UserNamespace: if ns.Path != "" { logrus.Debugf("unable to join user namespace, sorry about that") } case specs.UTSNamespace: if ns.Path != "" { logrus.Debugf("unable to join UTS namespace %q, creating a new one", ns.Path) } sawUTSNS = true } } if !sawMountNS { logrus.Debugf("mount namespace not requested, but creating a new one anyway") } if !sawUTSNS { logrus.Debugf("UTS namespace not requested, but creating a new one anyway") } } // setApparmorProfile sets the apparmor profile for ourselves, and hopefully any child processes that we'll start. func setApparmorProfile(spec *specs.Spec) error { if !apparmor.IsEnabled() || spec.Process.ApparmorProfile == "" { return nil } if err := apparmor.ApplyProfile(spec.Process.ApparmorProfile); err != nil { return fmt.Errorf("setting apparmor profile to %q: %w", spec.Process.ApparmorProfile, err) } return nil } // setCapabilities sets capabilities for ourselves, to be more or less inherited by any processes that we'll start. func setCapabilities(spec *specs.Spec, keepCaps ...string) error { currentCaps, err := capability.NewPid2(0) if err != nil { return fmt.Errorf("reading capabilities of current process: %w", err) } if err := currentCaps.Load(); err != nil { return fmt.Errorf("loading capabilities: %w", err) } caps, err := capability.NewPid2(0) if err != nil { return fmt.Errorf("reading capabilities of current process: %w", err) } capMap := map[capability.CapType][]string{ capability.BOUNDING: spec.Process.Capabilities.Bounding, capability.EFFECTIVE: spec.Process.Capabilities.Effective, capability.INHERITABLE: {}, capability.PERMITTED: spec.Process.Capabilities.Permitted, capability.AMBIENT: {}, } knownCaps := capability.ListKnown() noCap := capability.Cap(-1) for capType, capList := range capMap { for _, capSpec := range capList { capToSet := noCap for _, c := range knownCaps { if strings.EqualFold("CAP_"+c.String(), capSpec) { capToSet = c break } } if capToSet == noCap { return fmt.Errorf("mapping capability %q to a number", capSpec) } caps.Set(capType, capToSet) } for _, capSpec := range keepCaps { capToSet := noCap for _, c := range knownCaps { if strings.EqualFold("CAP_"+c.String(), capSpec) { capToSet = c break } } if capToSet == noCap { return fmt.Errorf("mapping capability %q to a number", capSpec) } if currentCaps.Get(capType, capToSet) { caps.Set(capType, capToSet) } } } if err = caps.Apply(capability.CAPS | capability.BOUNDS | capability.AMBS); err != nil { return fmt.Errorf("setting capabilities: %w", err) } return nil } func makeRlimit(limit specs.POSIXRlimit) unix.Rlimit { return unix.Rlimit{Cur: limit.Soft, Max: limit.Hard} } func createPlatformContainer(options runUsingChrootExecSubprocOptions) error { if options.NoPivot { return errors.New("not using pivot_root()") } // borrowing a technique from runc, who credit the LXC maintainers for this // open descriptors for the old and new root directories so that we can use fchdir() oldRootFd, err := unix.Open("/", unix.O_DIRECTORY, 0) if err != nil { return fmt.Errorf("opening host root directory: %w", err) } defer func() { if err := unix.Close(oldRootFd); err != nil { logrus.Warnf("closing host root directory: %v", err) } }() newRootFd, err := unix.Open(options.Spec.Root.Path, unix.O_DIRECTORY, 0) if err != nil { return fmt.Errorf("opening container root directory: %w", err) } defer func() { if err := unix.Close(newRootFd); err != nil { logrus.Warnf("closing container root directory: %v", err) } }() // change to the new root directory if err := unix.Fchdir(newRootFd); err != nil { return fmt.Errorf("changing to container root directory: %w", err) } // this makes the current directory the root directory. not actually // sure what happens to the other one if err := unix.PivotRoot(".", "."); err != nil { return fmt.Errorf("pivot_root: %w", err) } // go back and clean up the old one if err := unix.Fchdir(oldRootFd); err != nil { return fmt.Errorf("changing to host root directory: %w", err) } // make sure we only unmount things under this tree if err := unix.Mount(".", ".", "", unix.MS_SLAVE|unix.MS_REC, ""); err != nil { return fmt.Errorf("tweaking mount flags on host root directory before unmounting from mount namespace: %w", err) } // detach this (unnamed?) old directory if err := unix.Unmount(".", unix.MNT_DETACH); err != nil { return fmt.Errorf("unmounting host root directory in mount namespace: %w", err) } // go back to a named root directory if err := unix.Fchdir(newRootFd); err != nil { return fmt.Errorf("changing to container root directory at last: %w", err) } logrus.Debugf("pivot_root()ed into %q", options.Spec.Root.Path) return nil } func mountFlagsForFSFlags(fsFlags uintptr) uintptr { var mountFlags uintptr for _, mapping := range []struct { fsFlag uintptr mountFlag uintptr }{ {unix.ST_MANDLOCK, unix.MS_MANDLOCK}, {unix.ST_NOATIME, unix.MS_NOATIME}, {unix.ST_NODEV, unix.MS_NODEV}, {unix.ST_NODIRATIME, unix.MS_NODIRATIME}, {unix.ST_NOEXEC, unix.MS_NOEXEC}, {unix.ST_NOSUID, unix.MS_NOSUID}, {unix.ST_RDONLY, unix.MS_RDONLY}, {unix.ST_RELATIME, unix.MS_RELATIME}, {unix.ST_SYNCHRONOUS, unix.MS_SYNCHRONOUS}, } { if fsFlags&mapping.fsFlag == mapping.fsFlag { mountFlags |= mapping.mountFlag } } return mountFlags } func makeReadOnly(mntpoint string, flags uintptr) error { var fs unix.Statfs_t // Make sure it's read-only. if err := unix.Statfs(mntpoint, &fs); err != nil { return fmt.Errorf("checking if directory %q was bound read-only: %w", mntpoint, err) } if fs.Flags&unix.ST_RDONLY == 0 { // All callers currently pass MS_RDONLY in "flags", but in case they stop doing // that at some point in the future... if err := unix.Mount(mntpoint, mntpoint, "bind", flags|unix.MS_RDONLY|unix.MS_REMOUNT|unix.MS_BIND, ""); err != nil { return fmt.Errorf("remounting %s in mount namespace read-only: %w", mntpoint, err) } } return nil } // setupChrootBindMounts actually bind mounts things under the rootfs, and returns a // callback that will clean up its work. func setupChrootBindMounts(spec *specs.Spec, bundlePath string) (undoBinds func() error, err error) { var fs unix.Statfs_t undoBinds = func() error { if err2 := unix.Unmount(spec.Root.Path, unix.MNT_DETACH); err2 != nil { retries := 0 for (err2 == unix.EBUSY || err2 == unix.EAGAIN) && retries < 50 { time.Sleep(50 * time.Millisecond) err2 = unix.Unmount(spec.Root.Path, unix.MNT_DETACH) retries++ } if err2 != nil { logrus.Warnf("pkg/chroot: error unmounting %q (retried %d times): %v", spec.Root.Path, retries, err2) if err == nil { err = err2 } } } return err } // Now bind mount all of those things to be under the rootfs's location in this // mount namespace. commonFlags := uintptr(unix.MS_BIND | unix.MS_REC | unix.MS_PRIVATE) bindFlags := commonFlags devFlags := commonFlags | unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_RDONLY procFlags := devFlags | unix.MS_NODEV sysFlags := devFlags | unix.MS_NODEV // Bind /dev read-only. subDev := filepath.Join(spec.Root.Path, "/dev") if err := unix.Mount("/dev", subDev, "bind", devFlags, ""); err != nil { if errors.Is(err, os.ErrNotExist) { err = os.Mkdir(subDev, 0o755) if err == nil { err = unix.Mount("/dev", subDev, "bind", devFlags, "") } } if err != nil { return undoBinds, fmt.Errorf("bind mounting /dev from host into mount namespace: %w", err) } } // Make sure it's read-only. if err = unix.Statfs(subDev, &fs); err != nil { return undoBinds, fmt.Errorf("checking if directory %q was bound read-only: %w", subDev, err) } if fs.Flags&unix.ST_RDONLY == 0 { if err := unix.Mount(subDev, subDev, "bind", devFlags|unix.MS_REMOUNT|unix.MS_BIND, ""); err != nil { return undoBinds, fmt.Errorf("remounting /dev in mount namespace read-only: %w", err) } } logrus.Debugf("bind mounted %q to %q", "/dev", filepath.Join(spec.Root.Path, "/dev")) // Bind /proc read-only. subProc := filepath.Join(spec.Root.Path, "/proc") if err := unix.Mount("/proc", subProc, "bind", procFlags, ""); err != nil { if errors.Is(err, os.ErrNotExist) { err = os.Mkdir(subProc, 0o755) if err == nil { err = unix.Mount("/proc", subProc, "bind", procFlags, "") } } if err != nil { return undoBinds, fmt.Errorf("bind mounting /proc from host into mount namespace: %w", err) } } logrus.Debugf("bind mounted %q to %q", "/proc", filepath.Join(spec.Root.Path, "/proc")) // Bind /sys read-only. subSys := filepath.Join(spec.Root.Path, "/sys") if err := unix.Mount("/sys", subSys, "bind", sysFlags, ""); err != nil { if errors.Is(err, os.ErrNotExist) { err = os.Mkdir(subSys, 0o755) if err == nil { err = unix.Mount("/sys", subSys, "bind", sysFlags, "") } } if err != nil { return undoBinds, fmt.Errorf("bind mounting /sys from host into mount namespace: %w", err) } } if err := makeReadOnly(subSys, sysFlags); err != nil { return undoBinds, err } mnts, _ := mount.GetMounts() for _, m := range mnts { if !strings.HasPrefix(m.Mountpoint, "/sys/") && m.Mountpoint != "/sys" { continue } subSys := filepath.Join(spec.Root.Path, m.Mountpoint) if err := unix.Mount(m.Mountpoint, subSys, "bind", sysFlags, ""); err != nil { msg := fmt.Sprintf("could not bind mount %q, skipping: %v", m.Mountpoint, err) if strings.HasPrefix(m.Mountpoint, "/sys") { logrus.Info(msg) } else { logrus.Warning(msg) } continue } if err := makeReadOnly(subSys, sysFlags); err != nil { return undoBinds, err } } logrus.Debugf("bind mounted %q to %q", "/sys", filepath.Join(spec.Root.Path, "/sys")) // Bind, overlay, or tmpfs mount everything we've been asked to mount. for _, m := range spec.Mounts { // Skip anything that we just mounted. switch m.Destination { case "/dev", "/proc", "/sys": logrus.Debugf("already bind mounted %q on %q", m.Destination, filepath.Join(spec.Root.Path, m.Destination)) continue default: if strings.HasPrefix(m.Destination, "/dev/") { continue } if strings.HasPrefix(m.Destination, "/proc/") { continue } if strings.HasPrefix(m.Destination, "/sys/") { continue } } // Skip anything that isn't a bind or overlay or tmpfs mount. if m.Type != "bind" && m.Type != "tmpfs" && m.Type != "overlay" { logrus.Debugf("skipping mount of type %q on %q", m.Type, m.Destination) continue } // If the target is already there, we can just mount over it. var srcinfo os.FileInfo switch m.Type { case "bind": srcinfo, err = os.Stat(m.Source) if err != nil { return undoBinds, fmt.Errorf("examining %q for mounting in mount namespace: %w", m.Source, err) } case "overlay", "tmpfs": srcinfo, err = os.Stat("/") if err != nil { return undoBinds, fmt.Errorf("examining / to use as a template for a %s mount: %w", m.Type, err) } } target := filepath.Join(spec.Root.Path, m.Destination) // Check if target is a symlink. stat, err := os.Lstat(target) // If target is a symlink, follow the link and ensure the destination exists. if err == nil && stat != nil && (stat.Mode()&os.ModeSymlink != 0) { target, err = copier.Eval(spec.Root.Path, m.Destination, copier.EvalOptions{}) if err != nil { return nil, fmt.Errorf("evaluating symlink %q: %w", target, err) } // Stat the destination of the evaluated symlink. _, err = os.Stat(target) } if err != nil { // If the target can't be stat()ted, check the error. if !errors.Is(err, os.ErrNotExist) { return undoBinds, fmt.Errorf("examining %q for mounting in mount namespace: %w", target, err) } // The target isn't there yet, so create it. If the source is a directory, // we need a directory, otherwise we need a non-directory (i.e., a file). if srcinfo.IsDir() { if err = os.MkdirAll(target, 0o755); err != nil { return undoBinds, fmt.Errorf("creating mountpoint %q in mount namespace: %w", target, err) } } else { if err = os.MkdirAll(filepath.Dir(target), 0o755); err != nil { return undoBinds, fmt.Errorf("ensuring parent of mountpoint %q (%q) is present in mount namespace: %w", target, filepath.Dir(target), err) } var file *os.File if file, err = os.OpenFile(target, os.O_WRONLY|os.O_CREATE, 0o755); err != nil { return undoBinds, fmt.Errorf("creating mountpoint %q in mount namespace: %w", target, err) } file.Close() } } // Sort out which flags we're asking for, and what statfs() should be telling us // if we successfully mounted with them. requestFlags := uintptr(0) expectedImportantFlags := uintptr(0) importantFlags := uintptr(0) possibleImportantFlags := uintptr(unix.ST_NODEV | unix.ST_NOEXEC | unix.ST_NOSUID | unix.ST_RDONLY) for _, option := range m.Options { switch option { case "nodev": requestFlags |= unix.MS_NODEV importantFlags |= unix.ST_NODEV expectedImportantFlags |= unix.ST_NODEV case "dev": requestFlags &= ^uintptr(unix.MS_NODEV) importantFlags |= unix.ST_NODEV expectedImportantFlags &= ^uintptr(unix.ST_NODEV) case "noexec": requestFlags |= unix.MS_NOEXEC importantFlags |= unix.ST_NOEXEC expectedImportantFlags |= unix.ST_NOEXEC case "exec": requestFlags &= ^uintptr(unix.MS_NOEXEC) importantFlags |= unix.ST_NOEXEC expectedImportantFlags &= ^uintptr(unix.ST_NOEXEC) case "nosuid": requestFlags |= unix.MS_NOSUID importantFlags |= unix.ST_NOSUID expectedImportantFlags |= unix.ST_NOSUID case "suid": requestFlags &= ^uintptr(unix.MS_NOSUID) importantFlags |= unix.ST_NOSUID expectedImportantFlags &= ^uintptr(unix.ST_NOSUID) case "ro": requestFlags |= unix.MS_RDONLY importantFlags |= unix.ST_RDONLY expectedImportantFlags |= unix.ST_RDONLY case "rw": requestFlags &= ^uintptr(unix.MS_RDONLY) importantFlags |= unix.ST_RDONLY expectedImportantFlags &= ^uintptr(unix.ST_RDONLY) } } switch m.Type { case "bind": // Do the initial bind mount. We'll worry about the flags in a bit. logrus.Debugf("bind mounting %q on %q %v", m.Destination, filepath.Join(spec.Root.Path, m.Destination), m.Options) if err = unix.Mount(m.Source, target, "", bindFlags|requestFlags, ""); err != nil { return undoBinds, fmt.Errorf("bind mounting %q from host to %q in mount namespace (%q): %w", m.Source, m.Destination, target, err) } logrus.Debugf("bind mounted %q to %q", m.Source, target) case "tmpfs": // Mount a tmpfs. We'll worry about the flags in a bit. if err = mount.Mount(m.Source, target, m.Type, strings.Join(append(m.Options, "private"), ",")); err != nil { return undoBinds, fmt.Errorf("mounting tmpfs to %q in mount namespace (%q, %q): %w", m.Destination, target, strings.Join(append(m.Options, "private"), ","), err) } logrus.Debugf("mounted a tmpfs to %q", target) case "overlay": // Mount an overlay. We'll worry about the flags in a bit. if err = mount.Mount(m.Source, target, m.Type, strings.Join(append(m.Options, "private"), ",")); err != nil { return undoBinds, fmt.Errorf("mounting overlay to %q in mount namespace (%q, %q): %w", m.Destination, target, strings.Join(append(m.Options, "private"), ","), err) } logrus.Debugf("mounted a overlay to %q", target) } // Time to worry about the flags. if err = unix.Statfs(target, &fs); err != nil { return undoBinds, fmt.Errorf("checking if volume %q was mounted with requested flags: %w", target, err) } effectiveImportantFlags := uintptr(fs.Flags) & importantFlags if effectiveImportantFlags != expectedImportantFlags { // Do a remount to try to get the desired flags to stick. effectiveUnimportantFlags := uintptr(fs.Flags) & ^possibleImportantFlags remountFlags := unix.MS_REMOUNT | bindFlags | requestFlags | mountFlagsForFSFlags(effectiveUnimportantFlags) // If we are requesting a read-only mount, add any possibleImportantFlags present in fs.Flags to remountFlags. if requestFlags&unix.ST_RDONLY == unix.ST_RDONLY { remountFlags |= uintptr(fs.Flags) & possibleImportantFlags } if err = unix.Mount(target, target, m.Type, remountFlags, ""); err != nil { return undoBinds, fmt.Errorf("remounting %q in mount namespace with flags %v instead of %v: %w", target, mountFlagNames(requestFlags), statFlagNames(effectiveImportantFlags), err) } // Check if the desired flags stuck. if err = unix.Statfs(target, &fs); err != nil { return undoBinds, fmt.Errorf("checking if directory %q was remounted with requested flags %v instead of %v: %w", target, mountFlagNames(requestFlags), statFlagNames(effectiveImportantFlags), err) } newEffectiveImportantFlags := uintptr(fs.Flags) & importantFlags if newEffectiveImportantFlags != expectedImportantFlags { return undoBinds, fmt.Errorf("unable to remount %q with requested flags %v instead of %v, just got %v back", target, mountFlagNames(requestFlags), statFlagNames(effectiveImportantFlags), statFlagNames(newEffectiveImportantFlags)) } } } // Set up any read-only paths that we need to. If we're running inside // of a container, some of these locations will already be read-only, in // which case can declare victory and move on. for _, roPath := range spec.Linux.ReadonlyPaths { r := filepath.Join(spec.Root.Path, roPath) target, err := filepath.EvalSymlinks(r) if err != nil { if errors.Is(err, os.ErrNotExist) { // No target, no problem. continue } return undoBinds, fmt.Errorf("checking %q for symlinks before marking it read-only: %w", r, err) } // Check if the location is already read-only. var fs unix.Statfs_t if err = unix.Statfs(target, &fs); err != nil { if errors.Is(err, os.ErrNotExist) { // No target, no problem. continue } return undoBinds, fmt.Errorf("checking if directory %q is already read-only: %w", target, err) } if fs.Flags&unix.ST_RDONLY == unix.ST_RDONLY { continue } // Mount the location over itself, so that we can remount it as read-only, making // sure to preserve any combination of nodev/noexec/nosuid that's already in play. roFlags := mountFlagsForFSFlags(uintptr(fs.Flags)) | unix.MS_RDONLY if err := unix.Mount(target, target, "", bindFlags|roFlags, ""); err != nil { if errors.Is(err, os.ErrNotExist) { // No target, no problem. continue } return undoBinds, fmt.Errorf("bind mounting %q onto itself in preparation for making it read-only: %w", target, err) } // Remount the location read-only. if err = unix.Statfs(target, &fs); err != nil { return undoBinds, fmt.Errorf("checking if directory %q was bound read-only: %w", target, err) } if fs.Flags&unix.ST_RDONLY == 0 { if err := unix.Mount(target, target, "", unix.MS_REMOUNT|unix.MS_RDONLY|bindFlags|mountFlagsForFSFlags(uintptr(fs.Flags)), ""); err != nil { return undoBinds, fmt.Errorf("remounting %q in mount namespace read-only: %w", target, err) } } // Check again. if err = unix.Statfs(target, &fs); err != nil { return undoBinds, fmt.Errorf("checking if directory %q was remounted read-only: %w", target, err) } if fs.Flags&unix.ST_RDONLY == 0 { // Still not read only. return undoBinds, fmt.Errorf("verifying that %q in mount namespace was remounted read-only: %w", target, err) } } // Create an empty directory for to use for masking directories. roEmptyDir := filepath.Join(bundlePath, "empty") if len(spec.Linux.MaskedPaths) > 0 { if err := os.Mkdir(roEmptyDir, 0o700); err != nil { return undoBinds, fmt.Errorf("creating empty directory %q: %w", roEmptyDir, err) } } // Set up any masked paths that we need to. If we're running inside of // a container, some of these locations will already be read-only tmpfs // filesystems or bind mounted to os.DevNull. If we're not running // inside of a container, and nobody else has done that, we'll do it. for _, masked := range spec.Linux.MaskedPaths { t := filepath.Join(spec.Root.Path, masked) target, err := filepath.EvalSymlinks(t) if err != nil { target = t } // Get some info about the target. targetinfo, err := os.Stat(target) if err != nil { if errors.Is(err, os.ErrNotExist) { // No target, no problem. continue } return undoBinds, fmt.Errorf("examining %q for masking in mount namespace: %w", target, err) } if targetinfo.IsDir() { // The target's a directory. Check if it's a read-only filesystem. var statfs unix.Statfs_t if err = unix.Statfs(target, &statfs); err != nil { return undoBinds, fmt.Errorf("checking if directory %q is a mountpoint: %w", target, err) } isReadOnly := statfs.Flags&unix.ST_RDONLY == unix.ST_RDONLY // Check if any of the IDs we're mapping could read it. var stat unix.Stat_t if err = unix.Stat(target, &stat); err != nil { return undoBinds, fmt.Errorf("checking permissions on directory %q: %w", target, err) } isAccessible := false if stat.Mode&unix.S_IROTH|unix.S_IXOTH != 0 { isAccessible = true } if !isAccessible && stat.Mode&unix.S_IROTH|unix.S_IXOTH != 0 { if len(spec.Linux.GIDMappings) > 0 { for _, mapping := range spec.Linux.GIDMappings { if stat.Gid >= mapping.ContainerID && stat.Gid < mapping.ContainerID+mapping.Size { isAccessible = true break } } } } if !isAccessible && stat.Mode&unix.S_IRUSR|unix.S_IXUSR != 0 { if len(spec.Linux.UIDMappings) > 0 { for _, mapping := range spec.Linux.UIDMappings { if stat.Uid >= mapping.ContainerID && stat.Uid < mapping.ContainerID+mapping.Size { isAccessible = true break } } } } // Check if it's empty. hasContent := false directory, err := os.Open(target) if err != nil { if !os.IsPermission(err) { return undoBinds, fmt.Errorf("opening directory %q: %w", target, err) } } else { names, err := directory.Readdirnames(0) directory.Close() if err != nil { return undoBinds, fmt.Errorf("reading contents of directory %q: %w", target, err) } hasContent = false for _, name := range names { switch name { case ".", "..": continue default: hasContent = true } if hasContent { break } } } // The target's a directory, so read-only bind mount an empty directory on it. roFlags := uintptr(syscall.MS_BIND | syscall.MS_NOSUID | syscall.MS_NODEV | syscall.MS_NOEXEC | syscall.MS_RDONLY) if !isReadOnly || (hasContent && isAccessible) { if err = unix.Mount(roEmptyDir, target, "bind", roFlags, ""); err != nil { return undoBinds, fmt.Errorf("masking directory %q in mount namespace: %w", target, err) } if err = unix.Statfs(target, &fs); err != nil { return undoBinds, fmt.Errorf("checking if masked directory %q was mounted read-only in mount namespace: %w", target, err) } if fs.Flags&unix.ST_RDONLY == 0 { if err = unix.Mount(target, target, "", syscall.MS_REMOUNT|roFlags|mountFlagsForFSFlags(uintptr(fs.Flags)), ""); err != nil { return undoBinds, fmt.Errorf("making sure masked directory %q in mount namespace is read only: %w", target, err) } } } } else { // If the target's is not a directory or os.DevNull, bind mount os.DevNull over it. if !isDevNull(targetinfo) { if err = unix.Mount(os.DevNull, target, "", uintptr(syscall.MS_BIND|syscall.MS_RDONLY|syscall.MS_PRIVATE), ""); err != nil { return undoBinds, fmt.Errorf("masking non-directory %q in mount namespace: %w", target, err) } } } } return undoBinds, nil } // setPdeathsig sets a parent-death signal for the process func setPdeathsig(cmd *exec.Cmd) { if cmd.SysProcAttr == nil { cmd.SysProcAttr = &syscall.SysProcAttr{} } cmd.SysProcAttr.Pdeathsig = syscall.SIGKILL } ================================================ FILE: chroot/run_linux_test.go ================================================ package chroot import ( "slices" "testing" "github.com/stretchr/testify/assert" ) func TestStatFlagNames(t *testing.T) { var names []string var flags int for flag := range statFlagMap { flags |= flag names = append(names, statFlagMap[flag]) assert.Equal(t, []string{statFlagMap[flag]}, statFlagNames(uintptr(flag))) } slices.Sort(names) assert.Equal(t, names, statFlagNames(uintptr(flags))) } func TestMountFlagNames(t *testing.T) { var names []string var flags int for flag := range mountFlagMap { flags |= flag names = append(names, mountFlagMap[flag]) assert.Equal(t, []string{mountFlagMap[flag]}, mountFlagNames(uintptr(flag))) } slices.Sort(names) assert.Equal(t, names, mountFlagNames(uintptr(flags))) } ================================================ FILE: chroot/run_test.go ================================================ //go:build linux package chroot import ( "bytes" "encoding/json" "fmt" "io" "os" "path/filepath" "slices" "strconv" "strings" "testing" types "github.com/containers/buildah/tests/testreport/types" "github.com/containers/buildah/util" specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-tools/generate" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.podman.io/storage/pkg/mount" "go.podman.io/storage/pkg/reexec" "golang.org/x/sys/unix" ) const ( reportCommand = "testreport" ) func TestMain(m *testing.M) { if reexec.Init() { return } os.Exit(m.Run()) } func testMinimalWithPivot(t *testing.T, noPivot bool, modify func(g *generate.Generator, rootDir, bundleDir string), verify func(t *testing.T, report *types.TestReport)) { t.Helper() g, err := generate.New("linux") if err != nil { t.Fatalf("generate.New(%q): %v", "linux", err) } if err = setupSeccomp(g.Config, ""); err != nil { t.Fatalf("setupSeccomp(%q): %v", "", err) } // t.TempDir returns /tmp/TestName/001. // /tmp/TestName/001 has permission 0777, but /tmp/TestName is 0700 tempDir := t.TempDir() if err = os.Chmod(filepath.Dir(tempDir), 0o711); err != nil { t.Fatalf("error loosening permissions on %q: %v", tempDir, err) } rootDir := filepath.Join(tempDir, "root") if err := os.Mkdir(rootDir, 0o711); err != nil { t.Fatalf("os.Mkdir(%q): %v", rootDir, err) } rootTmpDir := filepath.Join(rootDir, "tmp") if err := os.Mkdir(rootTmpDir, 0o1777); err != nil { t.Fatalf("os.Mkdir(%q): %v", rootTmpDir, err) } specPath := filepath.Join("..", "tests", reportCommand, reportCommand) specBinarySource, err := os.Open(specPath) if err != nil { t.Fatalf("open(%q): %v", specPath, err) } defer specBinarySource.Close() specBinary, err := os.OpenFile(filepath.Join(rootDir, reportCommand), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o711) if err != nil { t.Fatalf("open(%q): %v", filepath.Join(rootDir, reportCommand), err) } if _, err := io.Copy(specBinary, specBinarySource); err != nil { t.Fatalf("io.Copy error: %v", err) } specBinary.Close() g.SetRootPath(rootDir) g.SetProcessArgs([]string{"/" + reportCommand}) bundleDir := filepath.Join(tempDir, "bundle") if err := os.Mkdir(bundleDir, 0o700); err != nil { t.Fatalf("os.Mkdir(%q): %v", bundleDir, err) } if modify != nil { modify(&g, rootDir, bundleDir) } uid, gid, err := util.GetHostRootIDs(g.Config) if err != nil { t.Fatalf("GetHostRootIDs: %v", err) } if err := os.Chown(rootDir, int(uid), int(gid)); err != nil { t.Fatalf("os.Chown(%q): %v", rootDir, err) } output := new(bytes.Buffer) if err := RunUsingChroot(g.Config, bundleDir, "/", new(bytes.Buffer), output, output, noPivot); err != nil { t.Fatalf("run(noPivot=%v): %v: %s", noPivot, err, output.String()) } var report types.TestReport if err := json.Unmarshal(output.Bytes(), &report); err != nil { t.Fatalf("decode: %v", err) } if verify != nil { verify(t, &report) } } func testMinimal(t *testing.T, modify func(g *generate.Generator, rootDir, bundleDir string), verify func(t *testing.T, report *types.TestReport)) { for _, noPivot := range []bool{false, true} { t.Run(fmt.Sprintf("noPivot=%v", noPivot), func(t *testing.T) { testMinimalWithPivot(t, noPivot, modify, verify) }) } } func TestNoop(t *testing.T) { t.Parallel() if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } testMinimal(t, nil, nil) } func TestMinimalSkeleton(t *testing.T) { t.Parallel() if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } testMinimal(t, func(_ *generate.Generator, _, _ string) { }, func(_ *testing.T, _ *types.TestReport) { }, ) } func TestProcessTerminal(t *testing.T) { t.Parallel() if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } for _, terminal := range []bool{false, true} { testMinimal(t, func(g *generate.Generator, _, _ string) { g.SetProcessTerminal(terminal) }, func(t *testing.T, report *types.TestReport) { if report.Spec.Process.Terminal != terminal { t.Fatalf("expected terminal = %v, got %v", terminal, report.Spec.Process.Terminal) } }, ) } } func TestProcessConsoleSize(t *testing.T) { t.Parallel() if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } for _, size := range [][2]uint{{80, 25}, {132, 50}} { testMinimal(t, func(g *generate.Generator, _, _ string) { g.SetProcessTerminal(true) g.SetProcessConsoleSize(size[0], size[1]) }, func(t *testing.T, report *types.TestReport) { if report.Spec.Process.ConsoleSize.Width != size[0] { t.Fatalf("expected console width = %v, got %v", size[0], report.Spec.Process.ConsoleSize.Width) } if report.Spec.Process.ConsoleSize.Height != size[1] { t.Fatalf("expected console height = %v, got %v", size[1], report.Spec.Process.ConsoleSize.Height) } }, ) } } func TestProcessUser(t *testing.T) { t.Parallel() if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } for _, id := range []uint32{0, 1000} { testMinimal(t, func(g *generate.Generator, _, _ string) { g.SetProcessUID(id) g.SetProcessGID(id + 1) g.AddProcessAdditionalGid(id + 2) }, func(t *testing.T, report *types.TestReport) { if report.Spec.Process.User.UID != id { t.Fatalf("expected UID %v, got %v", id, report.Spec.Process.User.UID) } if report.Spec.Process.User.GID != id+1 { t.Fatalf("expected GID %v, got %v", id+1, report.Spec.Process.User.GID) } }, ) } } func TestProcessEnv(t *testing.T) { t.Parallel() if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } e := fmt.Sprintf("PARENT_TEST_PID=%d", unix.Getpid()) testMinimal(t, func(g *generate.Generator, _, _ string) { g.ClearProcessEnv() g.AddProcessEnv("PARENT_TEST_PID", strconv.Itoa(unix.Getpid())) }, func(t *testing.T, report *types.TestReport) { if slices.Contains(report.Spec.Process.Env, e) { return } t.Fatalf("expected environment variable %q", e) }, ) } func TestProcessCwd(t *testing.T) { t.Parallel() if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } testMinimal(t, func(g *generate.Generator, rootDir, _ string) { if err := os.Mkdir(filepath.Join(rootDir, "/no-such-directory"), 0o700); err != nil { t.Fatalf("mkdir(%q): %v", filepath.Join(rootDir, "/no-such-directory"), err) } g.SetProcessCwd("/no-such-directory") }, func(t *testing.T, report *types.TestReport) { if report.Spec.Process.Cwd != "/no-such-directory" { t.Fatalf("expected %q, got %q", "/no-such-directory", report.Spec.Process.Cwd) } }, ) } func TestProcessCapabilities(t *testing.T) { t.Parallel() if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } testMinimal(t, func(g *generate.Generator, _, _ string) { g.ClearProcessCapabilities() }, func(t *testing.T, report *types.TestReport) { if len(report.Spec.Process.Capabilities.Permitted) != 0 { t.Fatalf("expected no permitted capabilities, got %#v", report.Spec.Process.Capabilities.Permitted) } }, ) testMinimal(t, func(g *generate.Generator, _, _ string) { g.ClearProcessCapabilities() if err := g.AddProcessCapabilityEffective("CAP_IPC_LOCK"); err != nil { t.Fatalf("%v", err) } if err := g.AddProcessCapabilityPermitted("CAP_IPC_LOCK"); err != nil { t.Fatalf("%v", err) } if err := g.AddProcessCapabilityInheritable("CAP_IPC_LOCK"); err != nil { t.Fatalf("%v", err) } if err := g.AddProcessCapabilityBounding("CAP_IPC_LOCK"); err != nil { t.Fatalf("%v", err) } if err := g.AddProcessCapabilityAmbient("CAP_IPC_LOCK"); err != nil { t.Fatalf("%v", err) } }, func(t *testing.T, report *types.TestReport) { if len(report.Spec.Process.Capabilities.Permitted) != 1 { t.Fatalf("expected one permitted capability, got %#v", report.Spec.Process.Capabilities.Permitted) } if report.Spec.Process.Capabilities.Permitted[0] != "CAP_IPC_LOCK" { t.Fatalf("expected one capability CAP_IPC_LOCK, got %#v", report.Spec.Process.Capabilities.Permitted) } }, ) } func TestProcessRlimits(t *testing.T) { t.Parallel() if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } for _, limit := range []uint64{100 * 1024 * 1024 * 1024, 200 * 1024 * 1024 * 1024, unix.RLIM_INFINITY} { testMinimal(t, func(g *generate.Generator, _, _ string) { g.ClearProcessRlimits() if limit != unix.RLIM_INFINITY { g.AddProcessRlimits("rlimit_as", limit, limit) } }, func(t *testing.T, report *types.TestReport) { var rlim *specs.POSIXRlimit for i := range report.Spec.Process.Rlimits { if strings.ToUpper(report.Spec.Process.Rlimits[i].Type) == "RLIMIT_AS" { rlim = &report.Spec.Process.Rlimits[i] } } if limit == unix.RLIM_INFINITY && rlim != nil && (rlim.Soft != unix.RLIM_INFINITY || rlim.Hard != unix.RLIM_INFINITY) { t.Fatalf("wasn't supposed to set limit on number of open files: %#v", rlim) } if limit != unix.RLIM_INFINITY && rlim == nil { t.Fatalf("was supposed to set limit on number of open files") } if rlim != nil { if rlim.Soft != limit { t.Fatalf("soft limit was set to %d, not %d", rlim.Soft, limit) } if rlim.Hard != limit { t.Fatalf("hard limit was set to %d, not %d", rlim.Hard, limit) } } }, ) } } func TestProcessNoNewPrivileges(t *testing.T) { t.Parallel() if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } if !seccompAvailable { t.Skip("not built with seccomp support") } for _, nope := range []bool{false, true} { testMinimal(t, func(g *generate.Generator, _, _ string) { g.SetProcessNoNewPrivileges(nope) }, func(t *testing.T, report *types.TestReport) { if report.Spec.Process.NoNewPrivileges != nope { t.Fatalf("expected no-new-privs to be %v, got %v", nope, report.Spec.Process.NoNewPrivileges) } }, ) } } func TestProcessOOMScoreAdj(t *testing.T) { t.Parallel() if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } for _, adj := range []int{0, 1, 2, 3} { testMinimal(t, func(g *generate.Generator, _, _ string) { g.SetProcessOOMScoreAdj(adj) }, func(t *testing.T, report *types.TestReport) { adjusted := 0 if report.Spec.Process.OOMScoreAdj != nil { adjusted = *report.Spec.Process.OOMScoreAdj } if adjusted != adj { t.Fatalf("expected oom-score-adj to be %v, got %v", adj, adjusted) } }, ) } } func TestHostname(t *testing.T) { t.Parallel() if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } hostname := fmt.Sprintf("host%d", unix.Getpid()) testMinimal(t, func(g *generate.Generator, _, _ string) { g.SetHostname(hostname) }, func(t *testing.T, report *types.TestReport) { if report.Spec.Hostname != hostname { t.Fatalf("expected %q, got %q", hostname, report.Spec.Hostname) } }, ) } func TestMounts(t *testing.T) { t.Parallel() if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } t.Run("tmpfs", func(t *testing.T) { testMinimal(t, func(g *generate.Generator, _, _ string) { g.AddMount(specs.Mount{ Source: "tmpfs", Destination: "/was-not-there-before", Type: "tmpfs", Options: []string{"ro", "size=0"}, }) }, func(t *testing.T, report *types.TestReport) { found := false for _, mount := range report.Spec.Mounts { if mount.Destination == "/was-not-there-before" && mount.Type == "tmpfs" { found = true } } if !found { t.Errorf("added mount for /was-not-there-before not found in %#v", report.Spec.Mounts) } }, ) }) // apparently we can do anything except turn read-only into read-write binds := []struct { name string tmpfsOptions string destination string fsType string options []string require []string reject []string }{ { name: "nodev", destination: "/nodev", options: []string{"nodev"}, reject: []string{"dev"}, }, { name: "noexec", destination: "/noexec", options: []string{"noexec"}, reject: []string{"exec"}, }, { name: "nosuid", destination: "/nosuid", options: []string{"nosuid"}, reject: []string{"suid"}, }, { name: "nodev,noexec", destination: "/nodev,noexec", options: []string{"nodev", "noexec"}, reject: []string{"dev", "exec"}, }, { name: "nodev,noexec,nosuid", destination: "/nodev,noexec,nosuid", options: []string{"nodev", "noexec", "nosuid"}, reject: []string{"dev", "exec", "suid"}, }, { name: "nodev,noexec,nosuid,ro", destination: "/nodev,noexec,nosuid,ro", options: []string{"nodev", "noexec", "nosuid", "ro"}, reject: []string{"dev", "exec", "suid", "rw"}, }, { name: "nodev,noexec,nosuid,rw", destination: "/nodev,noexec,nosuid,rw", options: []string{"nodev", "noexec", "nosuid", "rw"}, reject: []string{"dev", "exec", "suid", "ro"}, }, { name: "dev,exec,suid,rw", tmpfsOptions: "nodev,noexec,nosuid", destination: "/dev,exec,suid,rw", options: []string{"dev", "exec", "suid", "rw"}, require: []string{"rw"}, reject: []string{"nodev", "noexec", "nosuid", "ro"}, }, { name: "nodev,noexec,nosuid,ro,flip", tmpfsOptions: "dev,exec,suid,rw", destination: "/nodev,noexec,nosuid,ro", options: []string{"nodev", "noexec", "nosuid", "ro"}, reject: []string{"dev", "exec", "suid", "rw"}, }, } for _, bind := range binds { t.Run(bind.name, func(t *testing.T) { // mount a tmpfs over the temp dir, which may be on a nodev/noexec/nosuid filesystem tmpfsMount := t.TempDir() t.Cleanup(func() { _ = unix.Unmount(tmpfsMount, unix.MNT_FORCE|unix.MNT_DETACH) }) tmpfsOptions := "rw,size=1m" if bind.tmpfsOptions != "" { tmpfsOptions += ("," + bind.tmpfsOptions) } tmpfsFlags, tmpfsOptions := mount.ParseOptions(tmpfsOptions) require.NoErrorf(t, unix.Mount("none", tmpfsMount, "tmpfs", uintptr(tmpfsFlags), tmpfsOptions), "error mounting a tmpfs with flags=%#x,options=%q at %s", tmpfsFlags, tmpfsOptions, tmpfsMount) testMinimal(t, func(g *generate.Generator, _, _ string) { fsType := bind.fsType if fsType == "" { fsType = "bind" } g.AddMount(specs.Mount{ Source: tmpfsMount, Destination: bind.destination, Type: fsType, Options: bind.options, }) }, func(t *testing.T, report *types.TestReport) { foundBindDestinationMount := false for _, mount := range report.Spec.Mounts { if mount.Destination == bind.destination { allRequired := true requiredFlags := bind.require if len(requiredFlags) == 0 { requiredFlags = bind.options } for _, required := range requiredFlags { if !slices.Contains(mount.Options, required) { allRequired = false } } anyRejected := false for _, rejected := range bind.reject { if slices.Contains(mount.Options, rejected) { anyRejected = true } } if allRequired && !anyRejected { foundBindDestinationMount = true } } } if !foundBindDestinationMount { t.Errorf("added mount for %s not found with the right flags (%v) in %+v", bind.destination, bind.options, report.Spec.Mounts) } }, ) // okay, just make sure we didn't change anything about the tmpfs mount point outside of the chroot var fs unix.Statfs_t require.NoErrorf(t, unix.Statfs(tmpfsMount, &fs), "fstat") assert.Equalf(t, tmpfsFlags&unix.MS_NODEV == unix.MS_NODEV, fs.Flags&unix.ST_NODEV == unix.ST_NODEV, "nodev flag") assert.Equalf(t, tmpfsFlags&unix.MS_NOEXEC == unix.MS_NOEXEC, fs.Flags&unix.ST_NOEXEC == unix.ST_NOEXEC, "noexec flag") assert.Equalf(t, tmpfsFlags&unix.MS_NOSUID == unix.MS_NOSUID, fs.Flags&unix.ST_NOSUID == unix.ST_NOSUID, "nosuid flag") assert.Equalf(t, tmpfsFlags&unix.MS_RDONLY == unix.MS_RDONLY, fs.Flags&unix.ST_RDONLY == unix.ST_RDONLY, "readonly flag") }) } } func TestLinuxIDMapping(t *testing.T) { t.Parallel() if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } testMinimal(t, func(g *generate.Generator, _, _ string) { g.ClearLinuxUIDMappings() g.ClearLinuxGIDMappings() g.AddLinuxUIDMapping(uint32(unix.Getuid()), 0, 1) g.AddLinuxGIDMapping(uint32(unix.Getgid()), 0, 1) }, func(t *testing.T, report *types.TestReport) { if len(report.Spec.Linux.UIDMappings) != 1 { t.Fatalf("expected 1 uid mapping, got %q", len(report.Spec.Linux.UIDMappings)) } if report.Spec.Linux.UIDMappings[0].HostID != uint32(unix.Getuid()) { t.Fatalf("expected host uid mapping to be %d, got %d", unix.Getuid(), report.Spec.Linux.UIDMappings[0].HostID) } if report.Spec.Linux.UIDMappings[0].ContainerID != 0 { t.Fatalf("expected container uid mapping to be 0, got %d", report.Spec.Linux.UIDMappings[0].ContainerID) } if report.Spec.Linux.UIDMappings[0].Size != 1 { t.Fatalf("expected container uid map size to be 1, got %d", report.Spec.Linux.UIDMappings[0].Size) } if report.Spec.Linux.GIDMappings[0].HostID != uint32(unix.Getgid()) { t.Fatalf("expected host uid mapping to be %d, got %d", unix.Getgid(), report.Spec.Linux.GIDMappings[0].HostID) } if report.Spec.Linux.GIDMappings[0].ContainerID != 0 { t.Fatalf("expected container gid mapping to be 0, got %d", report.Spec.Linux.GIDMappings[0].ContainerID) } if report.Spec.Linux.GIDMappings[0].Size != 1 { t.Fatalf("expected container gid map size to be 1, got %d", report.Spec.Linux.GIDMappings[0].Size) } }, ) } func TestLinuxIDMappingShift(t *testing.T) { t.Parallel() if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } testMinimal(t, func(g *generate.Generator, _, _ string) { g.ClearLinuxUIDMappings() g.ClearLinuxGIDMappings() g.AddLinuxUIDMapping(uint32(unix.Getuid())+1, 0, 1) g.AddLinuxGIDMapping(uint32(unix.Getgid())+1, 0, 1) }, func(t *testing.T, report *types.TestReport) { if len(report.Spec.Linux.UIDMappings) != 1 { t.Fatalf("expected 1 uid mapping, got %q", len(report.Spec.Linux.UIDMappings)) } if report.Spec.Linux.UIDMappings[0].HostID != uint32(unix.Getuid()+1) { t.Fatalf("expected host uid mapping to be %d, got %d", unix.Getuid()+1, report.Spec.Linux.UIDMappings[0].HostID) } if report.Spec.Linux.UIDMappings[0].ContainerID != 0 { t.Fatalf("expected container uid mapping to be 0, got %d", report.Spec.Linux.UIDMappings[0].ContainerID) } if report.Spec.Linux.UIDMappings[0].Size != 1 { t.Fatalf("expected container uid map size to be 1, got %d", report.Spec.Linux.UIDMappings[0].Size) } if report.Spec.Linux.GIDMappings[0].HostID != uint32(unix.Getgid()+1) { t.Fatalf("expected host uid mapping to be %d, got %d", unix.Getgid()+1, report.Spec.Linux.GIDMappings[0].HostID) } if report.Spec.Linux.GIDMappings[0].ContainerID != 0 { t.Fatalf("expected container gid mapping to be 0, got %d", report.Spec.Linux.GIDMappings[0].ContainerID) } if report.Spec.Linux.GIDMappings[0].Size != 1 { t.Fatalf("expected container gid map size to be 1, got %d", report.Spec.Linux.GIDMappings[0].Size) } }, ) } ================================================ FILE: chroot/seccomp.go ================================================ //go:build linux && seccomp package chroot import ( "fmt" specs "github.com/opencontainers/runtime-spec/specs-go" libseccomp "github.com/seccomp/libseccomp-golang" "github.com/sirupsen/logrus" ) // setSeccomp sets the seccomp filter for ourselves and any processes that we'll start. func setSeccomp(spec *specs.Spec) error { logrus.Debugf("setting seccomp configuration") if spec.Linux.Seccomp == nil { return nil } mapAction := func(specAction specs.LinuxSeccompAction, errnoRet *uint) libseccomp.ScmpAction { switch specAction { case specs.ActKill: return libseccomp.ActKillThread case specs.ActTrap: return libseccomp.ActTrap case specs.ActErrno: action := libseccomp.ActErrno if errnoRet != nil { action = action.SetReturnCode(int16(*errnoRet)) } return action case specs.ActTrace: return libseccomp.ActTrace case specs.ActAllow: return libseccomp.ActAllow case specs.ActLog: return libseccomp.ActLog case specs.ActKillProcess: return libseccomp.ActKillProcess default: logrus.Errorf("unmappable action %v", specAction) } return libseccomp.ActInvalid } mapArch := func(specArch specs.Arch) libseccomp.ScmpArch { switch specArch { case specs.ArchX86: return libseccomp.ArchX86 case specs.ArchX86_64: return libseccomp.ArchAMD64 case specs.ArchX32: return libseccomp.ArchX32 case specs.ArchARM: return libseccomp.ArchARM case specs.ArchAARCH64: return libseccomp.ArchARM64 case specs.ArchMIPS: return libseccomp.ArchMIPS case specs.ArchMIPS64: return libseccomp.ArchMIPS64 case specs.ArchMIPS64N32: return libseccomp.ArchMIPS64N32 case specs.ArchMIPSEL: return libseccomp.ArchMIPSEL case specs.ArchMIPSEL64: return libseccomp.ArchMIPSEL64 case specs.ArchMIPSEL64N32: return libseccomp.ArchMIPSEL64N32 case specs.ArchPPC: return libseccomp.ArchPPC case specs.ArchPPC64: return libseccomp.ArchPPC64 case specs.ArchPPC64LE: return libseccomp.ArchPPC64LE case specs.ArchS390: return libseccomp.ArchS390 case specs.ArchS390X: return libseccomp.ArchS390X case specs.ArchPARISC: return libseccomp.ArchPARISC case specs.ArchPARISC64: return libseccomp.ArchPARISC64 case specs.ArchRISCV64: return libseccomp.ArchRISCV64 default: logrus.Errorf("unmappable arch %v", specArch) } return libseccomp.ArchInvalid } mapOp := func(op specs.LinuxSeccompOperator) libseccomp.ScmpCompareOp { switch op { case specs.OpNotEqual: return libseccomp.CompareNotEqual case specs.OpLessThan: return libseccomp.CompareLess case specs.OpLessEqual: return libseccomp.CompareLessOrEqual case specs.OpEqualTo: return libseccomp.CompareEqual case specs.OpGreaterEqual: return libseccomp.CompareGreaterEqual case specs.OpGreaterThan: return libseccomp.CompareGreater case specs.OpMaskedEqual: return libseccomp.CompareMaskedEqual default: logrus.Errorf("unmappable op %v", op) } return libseccomp.CompareInvalid } filter, err := libseccomp.NewFilter(mapAction(spec.Linux.Seccomp.DefaultAction, spec.Linux.Seccomp.DefaultErrnoRet)) if err != nil { return fmt.Errorf("creating seccomp filter with default action %q: %w", spec.Linux.Seccomp.DefaultAction, err) } for _, arch := range spec.Linux.Seccomp.Architectures { if err = filter.AddArch(mapArch(arch)); err != nil { return fmt.Errorf("adding architecture %q(%q) to seccomp filter: %w", arch, mapArch(arch), err) } } for _, rule := range spec.Linux.Seccomp.Syscalls { scnames := make(map[libseccomp.ScmpSyscall]string) for _, name := range rule.Names { scnum, err := libseccomp.GetSyscallFromName(name) if err != nil { logrus.Debugf("error mapping syscall %q to a syscall, ignoring %q rule for %q", name, rule.Action, name) continue } scnames[scnum] = name } for scnum := range scnames { if len(rule.Args) == 0 { if err = filter.AddRule(scnum, mapAction(rule.Action, rule.ErrnoRet)); err != nil { return fmt.Errorf("adding a rule (%q:%q) to seccomp filter: %w", scnames[scnum], rule.Action, err) } continue } var conditions []libseccomp.ScmpCondition opsAreAllEquality := true for _, arg := range rule.Args { condition, err := libseccomp.MakeCondition(arg.Index, mapOp(arg.Op), arg.Value, arg.ValueTwo) if err != nil { return fmt.Errorf("building a seccomp condition %d:%v:%d:%d: %w", arg.Index, arg.Op, arg.Value, arg.ValueTwo, err) } if arg.Op != specs.OpEqualTo { opsAreAllEquality = false } conditions = append(conditions, condition) } if err = filter.AddRuleConditional(scnum, mapAction(rule.Action, rule.ErrnoRet), conditions); err != nil { // Okay, if the rules specify multiple equality // checks, assume someone thought that they // were OR'd, when in fact they're ordinarily // supposed to be AND'd. Break them up into // different rules to get that OR effect. if len(rule.Args) > 1 && opsAreAllEquality && err.Error() == "two checks on same syscall argument" { for i := range conditions { if err = filter.AddRuleConditional(scnum, mapAction(rule.Action, rule.ErrnoRet), conditions[i:i+1]); err != nil { return fmt.Errorf("adding a conditional rule (%q:%q[%d]) to seccomp filter: %w", scnames[scnum], rule.Action, i, err) } } } else { return fmt.Errorf("adding a conditional rule (%q:%q) to seccomp filter: %w", scnames[scnum], rule.Action, err) } } } } if err = filter.SetNoNewPrivsBit(spec.Process.NoNewPrivileges); err != nil { return fmt.Errorf("setting no-new-privileges bit to %v: %w", spec.Process.NoNewPrivileges, err) } err = filter.Load() filter.Release() if err != nil { return fmt.Errorf("activating seccomp filter: %w", err) } return nil } ================================================ FILE: chroot/seccomp_freebsd.go ================================================ //go:build freebsd && seccomp package chroot import ( "github.com/opencontainers/runtime-spec/specs-go" ) const seccompAvailable = false func setSeccomp(spec *specs.Spec) error { // Ignore this on FreeBSD return nil } ================================================ FILE: chroot/seccomp_test.go ================================================ //go:build linux && seccomp package chroot import ( "fmt" "os" specs "github.com/opencontainers/runtime-spec/specs-go" "go.podman.io/common/pkg/seccomp" ) const seccompAvailable = true func setupSeccomp(spec *specs.Spec, seccompProfilePath string) error { switch seccompProfilePath { case "unconfined": spec.Linux.Seccomp = nil case "": seccompConfig, err := seccomp.GetDefaultProfile(spec) if err != nil { return fmt.Errorf("loading default seccomp profile failed: %w", err) } spec.Linux.Seccomp = seccompConfig default: seccompProfile, err := os.ReadFile(seccompProfilePath) if err != nil { return fmt.Errorf("opening seccomp profile failed: %w", err) } seccompConfig, err := seccomp.LoadProfile(string(seccompProfile), spec) if err != nil { return fmt.Errorf("loading seccomp profile (%s) failed: %w", seccompProfilePath, err) } spec.Linux.Seccomp = seccompConfig } return nil } ================================================ FILE: chroot/seccomp_unsupported.go ================================================ //go:build (!linux && !freebsd) || !seccomp package chroot import ( "errors" "github.com/opencontainers/runtime-spec/specs-go" ) func setSeccomp(spec *specs.Spec) error { if spec.Linux.Seccomp != nil { return errors.New("configured a seccomp filter without seccomp support?") } return nil } ================================================ FILE: chroot/seccomp_unsupported_test.go ================================================ //go:build (!linux && !freebsd) || !seccomp package chroot import ( "github.com/opencontainers/runtime-spec/specs-go" ) const seccompAvailable = false func setupSeccomp(spec *specs.Spec, _ string) error { if spec.Linux != nil { // runtime-tools may have supplied us with a default filter spec.Linux.Seccomp = nil } return nil } ================================================ FILE: chroot/selinux.go ================================================ //go:build linux package chroot import ( "fmt" "github.com/opencontainers/runtime-spec/specs-go" selinux "github.com/opencontainers/selinux/go-selinux" "github.com/sirupsen/logrus" ) // setSelinuxLabel sets the process label for child processes that we'll start. func setSelinuxLabel(spec *specs.Spec) error { logrus.Debugf("setting selinux label") if spec.Process.SelinuxLabel != "" && selinux.GetEnabled() { if err := selinux.SetExecLabel(spec.Process.SelinuxLabel); err != nil { return fmt.Errorf("setting process label to %q: %w", spec.Process.SelinuxLabel, err) } } return nil } ================================================ FILE: chroot/selinux_unsupported.go ================================================ //go:build !linux && !freebsd package chroot import ( "errors" "github.com/opencontainers/runtime-spec/specs-go" ) func setSelinuxLabel(spec *specs.Spec) error { if spec.Linux.MountLabel != "" { return errors.New("configured an SELinux mount label without SELinux support?") } if spec.Process.SelinuxLabel != "" { return errors.New("configured an SELinux process label without SELinux support?") } return nil } ================================================ FILE: chroot/unsupported.go ================================================ //go:build !linux && !freebsd package chroot import ( "fmt" "io" "github.com/opencontainers/runtime-spec/specs-go" ) // RunUsingChroot is not supported. func RunUsingChroot(spec *specs.Spec, bundlePath, homeDir string, stdin io.Reader, stdout, stderr io.Writer) (err error) { return fmt.Errorf("--isolation chroot is not supported on this platform") } ================================================ FILE: cmd/buildah/addcopy.go ================================================ package main import ( "errors" "fmt" "os" "path/filepath" "strconv" "strings" "time" "github.com/containers/buildah" "github.com/containers/buildah/pkg/cli" "github.com/containers/buildah/pkg/parse" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" "go.podman.io/common/pkg/auth" "go.podman.io/storage" ) type addCopyResults struct { addHistory bool chmod string chown string checksum string quiet bool ignoreFile string contextdir string from string blobCache string decryptionKeys []string removeSignatures bool signaturePolicy string authfile string creds string tlsVerify bool certDir string retry int retryDelay string excludes []string parents bool timestamp string link bool } func createCommand(addCopy string, desc string, short string, opts *addCopyResults) *cobra.Command { return &cobra.Command{ Use: addCopy, Short: short, Long: desc, RunE: func(cmd *cobra.Command, args []string) error { return addAndCopyCmd(cmd, args, strings.ToUpper(addCopy), *opts) }, Example: `buildah ` + addCopy + ` containerID '/myapp/app.conf' buildah ` + addCopy + ` containerID 'app.conf' '/myapp/app.conf' buildah ` + addCopy + ` containerID 'app.conf' 'drop-in.conf' '/myapp/app.conf.d/'`, Args: cobra.MinimumNArgs(1), } } func applyFlagVars(flags *pflag.FlagSet, opts *addCopyResults) { flags.SetInterspersed(false) flags.BoolVar(&opts.addHistory, "add-history", false, "add an entry for this operation to the image's history. Use BUILDAH_HISTORY environment variable to override. (default false)") flags.StringVar(&opts.authfile, "authfile", auth.GetDefaultAuthFile(), "path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") if err := flags.MarkHidden("authfile"); err != nil { panic(fmt.Sprintf("error marking authfile as hidden: %v", err)) } flags.StringVar(&opts.blobCache, "blob-cache", "", "store copies of pulled image blobs in the specified directory") if err := flags.MarkHidden("blob-cache"); err != nil { panic(fmt.Sprintf("error marking blob-cache as hidden: %v", err)) } flags.StringVar(&opts.certDir, "cert-dir", "", "use certificates at the specified path to access registries and sources in HTTPS locations") flags.StringVar(&opts.checksum, "checksum", "", "checksum the HTTP source content") flags.StringVar(&opts.chown, "chown", "", "set the user and group ownership of the destination content") flags.StringVar(&opts.chmod, "chmod", "", "set the access permissions of the destination content") flags.StringVar(&opts.creds, "creds", "", "use `[username[:password]]` for accessing registries when pulling images") flags.BoolVar(&opts.link, "link", false, "enable layer caching for this operation (creates an independent layer)") if err := flags.MarkHidden("creds"); err != nil { panic(fmt.Sprintf("error marking creds as hidden: %v", err)) } flags.StringVar(&opts.from, "from", "", "use the specified container's or image's root directory as the source root directory") flags.StringSliceVar(&opts.decryptionKeys, "decryption-key", nil, "key needed to decrypt a pulled image") if err := flags.MarkHidden("decryption-key"); err != nil { panic(fmt.Sprintf("error marking decryption-key as hidden: %v", err)) } flags.StringSliceVar(&opts.excludes, "exclude", nil, "exclude pattern when copying files") flags.StringVar(&opts.ignoreFile, "ignorefile", "", "path to .containerignore file") flags.StringVar(&opts.contextdir, "contextdir", "", "context directory path") flags.IntVar(&opts.retry, "retry", cli.MaxPullPushRetries, "number of times to retry in case of failure when performing pull") flags.StringVar(&opts.retryDelay, "retry-delay", cli.PullPushRetryDelay.String(), "delay between retries in case of pull failures") flags.BoolVarP(&opts.quiet, "quiet", "q", false, "don't output a digest of the newly-added/copied content") flags.BoolVar(&opts.tlsVerify, "tls-verify", true, "require HTTPS and verify certificates when accessing registries when pulling images, and when retrieving sources from HTTPS URLs. TLS verification cannot be used when talking to an insecure registry.") flags.BoolVarP(&opts.removeSignatures, "remove-signatures", "", false, "don't copy signatures when pulling image") if err := flags.MarkHidden("remove-signatures"); err != nil { panic(fmt.Sprintf("error marking remove-signatures as hidden: %v", err)) } flags.StringVar(&opts.signaturePolicy, "signature-policy", "", "`pathname` of signature policy file (not usually used)") if err := flags.MarkHidden("signature-policy"); err != nil { panic(fmt.Sprintf("error marking signature-policy as hidden: %v", err)) } flags.StringVar(&opts.timestamp, "timestamp", "", "set timestamps on new content to `seconds` after the epoch") } func addcopyInit() { var ( addDescription = "\n Adds the contents of a file, URL, or directory to a container's working\n directory. If a local file appears to be an archive, its contents are\n extracted and added instead of the archive file itself." copyDescription = "\n Copies the contents of a file, URL, or directory into a container's working\n directory." shortAdd = "Add content to the container" shortCopy = "Copy content into the container" addOpts addCopyResults copyOpts addCopyResults ) addCommand := createCommand("add", addDescription, shortAdd, &addOpts) addCommand.SetUsageTemplate(UsageTemplate()) copyCommand := createCommand("copy", copyDescription, shortCopy, ©Opts) copyCommand.SetUsageTemplate(UsageTemplate()) addFlags := addCommand.Flags() applyFlagVars(addFlags, &addOpts) copyFlags := copyCommand.Flags() applyFlagVars(copyFlags, ©Opts) copyFlags.BoolVar(©Opts.parents, "parents", false, "preserve leading directories in the paths of items being copied") rootCmd.AddCommand(addCommand) rootCmd.AddCommand(copyCommand) } func addAndCopyCmd(c *cobra.Command, args []string, verb string, iopts addCopyResults) error { if len(args) == 0 { return errors.New("container ID must be specified") } name := args[0] args = Tail(args) if len(args) == 0 { return errors.New("src must be specified") } if err := cli.VerifyFlagsArgsOrder(args); err != nil { return err } // If list is greater than one, the last item is the destination dest := "" size := len(args) if size > 1 { dest = args[size-1] args = args[:size-1] } store, err := getStore(c) if err != nil { return err } var from *buildah.Builder unmountFrom := false removeFrom := false var idMappingOptions *buildah.IDMappingOptions contextdir := iopts.contextdir if iopts.ignoreFile != "" && contextdir == "" { return errors.New("--ignorefile option requires that you specify a context dir using --contextdir") } systemContext, err := parse.SystemContextFromOptions(c) if err != nil { return fmt.Errorf("building system context: %w", err) } var preserveOwnership bool if iopts.from != "" { if from, err = openBuilder(getContext(), store, iopts.from); err != nil && errors.Is(err, storage.ErrContainerUnknown) { decryptConfig, err2 := cli.DecryptConfig(iopts.decryptionKeys) if err2 != nil { return fmt.Errorf("unable to obtain decrypt config: %w", err2) } options := buildah.BuilderOptions{ FromImage: iopts.from, BlobDirectory: iopts.blobCache, SignaturePolicyPath: iopts.signaturePolicy, SystemContext: systemContext, MaxPullRetries: iopts.retry, OciDecryptConfig: decryptConfig, } if iopts.retryDelay != "" { options.PullRetryDelay, err = time.ParseDuration(iopts.retryDelay) if err != nil { return fmt.Errorf("unable to parse value provided %q as --retry-delay: %w", iopts.retryDelay, err) } } if !iopts.quiet { options.ReportWriter = os.Stderr } if from, err = buildah.NewBuilder(getContext(), store, options); err != nil { return fmt.Errorf("no container named %q, error copying content from image %q: %w", iopts.from, iopts.from, err) } removeFrom = true defer func() { if !removeFrom { return } if err := from.Delete(); err != nil { logrus.Errorf("error deleting %q temporary working container %q", iopts.from, from.Container) } }() } if err != nil { return fmt.Errorf("reading build container %q: %w", iopts.from, err) } fromMountPoint, err := from.Mount(from.MountLabel) if err != nil { return fmt.Errorf("mounting %q container %q: %w", iopts.from, from.Container, err) } unmountFrom = true defer func() { if !unmountFrom { return } if err := from.Unmount(); err != nil { logrus.Errorf("error unmounting %q container %q", iopts.from, from.Container) } if err := from.Save(); err != nil { logrus.Errorf("error saving information about %q container %q", iopts.from, from.Container) } }() idMappingOptions = &from.IDMappingOptions preserveOwnership = true contextdir = filepath.Join(fromMountPoint, iopts.contextdir) for i := range args { args[i] = filepath.Join(fromMountPoint, args[i]) } } builder, err := openBuilder(getContext(), store, name) if err != nil { return fmt.Errorf("reading build container %q: %w", name, err) } builder.ContentDigester.Restart() var timestamp *time.Time if iopts.timestamp != "" { u, err := strconv.ParseInt(iopts.timestamp, 10, 64) if err != nil { return fmt.Errorf("parsing timestamp value %q: %w", iopts.timestamp, err) } t := time.Unix(u, 0).UTC() timestamp = &t } options := buildah.AddAndCopyOptions{ Chmod: iopts.chmod, Chown: iopts.chown, PreserveOwnership: preserveOwnership, Checksum: iopts.checksum, ContextDir: contextdir, Excludes: iopts.excludes, IDMappingOptions: idMappingOptions, // These next two fields are set based on command line flags // with more generic-sounding names. CertPath: systemContext.DockerCertPath, InsecureSkipTLSVerify: systemContext.DockerInsecureSkipTLSVerify, MaxRetries: iopts.retry, Parents: iopts.parents, Timestamp: timestamp, Link: iopts.link, } if iopts.contextdir != "" { var excludes []string excludes, options.IgnoreFile, err = parse.ContainerIgnoreFile(options.ContextDir, iopts.ignoreFile, []string{}) if err != nil { return err } options.Excludes = append(excludes, options.Excludes...) } if iopts.retryDelay != "" { retryDelay, err := time.ParseDuration(iopts.retryDelay) if err != nil { return fmt.Errorf("unable to parse value provided %q as --retry-delay: %w", iopts.retryDelay, err) } options.RetryDelay = retryDelay } extractLocalArchives := verb == "ADD" err = builder.Add(dest, extractLocalArchives, options, args...) if err != nil { return fmt.Errorf("adding content to container %q: %w", builder.Container, err) } if unmountFrom { if err := from.Unmount(); err != nil { return fmt.Errorf("unmounting %q container %q: %w", iopts.from, from.Container, err) } if err := from.Save(); err != nil { return fmt.Errorf("saving information about %q container %q: %w", iopts.from, from.Container, err) } unmountFrom = false } if removeFrom { if err := from.Delete(); err != nil { return fmt.Errorf("deleting %q temporary working container %q: %w", iopts.from, from.Container, err) } removeFrom = false } contentType, digest := builder.ContentDigester.Digest() if !iopts.quiet { fmt.Printf("%s\n", digest.Hex()) } if contentType != "" { contentType = contentType + ":" } conditionallyAddHistory(builder, c, "/bin/sh -c #(nop) %s %s%s", verb, contentType, digest.Hex()) return builder.Save() } ================================================ FILE: cmd/buildah/build.go ================================================ package main import ( "fmt" "os" "github.com/containers/buildah/imagebuildah" buildahcli "github.com/containers/buildah/pkg/cli" "github.com/containers/buildah/util" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) func buildInit() { buildDescription := ` Builds an OCI image using instructions in one or more Containerfiles. If no arguments are specified, Buildah will use the current working directory as the build context and look for a Containerfile. The build fails if no Containerfile nor Dockerfile is present.` layerFlagsResults := buildahcli.LayerResults{} buildFlagResults := buildahcli.BudResults{} fromAndBudResults := buildahcli.FromAndBudResults{} userNSResults := buildahcli.UserNSResults{} namespaceResults := buildahcli.NameSpaceResults{} buildCommand := &cobra.Command{ Use: "build [CONTEXT]", Aliases: []string{"build-using-dockerfile", "bud"}, Short: "Build an image using instructions in a Containerfile", Long: buildDescription, RunE: func(cmd *cobra.Command, args []string) error { br := buildahcli.BuildOptions{ LayerResults: &layerFlagsResults, BudResults: &buildFlagResults, UserNSResults: &userNSResults, FromAndBudResults: &fromAndBudResults, NameSpaceResults: &namespaceResults, } return buildCmd(cmd, args, br) }, Args: cobra.MaximumNArgs(1), Example: `buildah build buildah bud -f Containerfile.simple . buildah bud --volume /home/test:/myvol:ro,Z -t imageName . buildah bud -f Containerfile.simple -f Containerfile.notsosimple .`, } buildCommand.SetUsageTemplate(UsageTemplate()) flags := buildCommand.Flags() flags.SetInterspersed(false) // build is a all common flags buildFlags := buildahcli.GetBudFlags(&buildFlagResults) buildFlags.StringVar(&buildFlagResults.Runtime, "runtime", util.Runtime(), "`path` to an alternate runtime. Use BUILDAH_RUNTIME environment variable to override.") layerFlags := buildahcli.GetLayerFlags(&layerFlagsResults) fromAndBudFlags, err := buildahcli.GetFromAndBudFlags(&fromAndBudResults, &userNSResults, &namespaceResults) if err != nil { logrus.Errorf("failed to setup From and Build flags: %v", err) os.Exit(1) } flags.AddFlagSet(&buildFlags) flags.AddFlagSet(&layerFlags) flags.AddFlagSet(&fromAndBudFlags) flags.SetNormalizeFunc(buildahcli.AliasFlags) rootCmd.AddCommand(buildCommand) } func buildCmd(c *cobra.Command, inputArgs []string, iopts buildahcli.BuildOptions) error { if c.Flag("logfile").Changed { logfile, err := os.OpenFile(iopts.Logfile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600) if err != nil { return fmt.Errorf("opening log file: %w", err) } iopts.Logwriter = logfile defer iopts.Logwriter.Close() } options, containerfiles, removeAll, err := buildahcli.GenBuildOptions(c, inputArgs, iopts) if err != nil { return err } defer func() { for _, f := range removeAll { os.RemoveAll(f) } }() options.DefaultMountsFilePath = globalFlagResults.DefaultMountsFile store, err := getStore(c) if err != nil { return err } id, ref, err := imagebuildah.BuildDockerfiles(getContext(), store, options, containerfiles...) if err == nil && options.Manifest != "" { logrus.Debugf("manifest list id = %q, ref = %q", id, ref.String()) } return err } ================================================ FILE: cmd/buildah/commit.go ================================================ package main import ( "encoding/json" "errors" "fmt" "os" "strconv" "strings" "time" "github.com/containers/buildah" "github.com/containers/buildah/define" "github.com/containers/buildah/internal" "github.com/containers/buildah/pkg/cli" "github.com/containers/buildah/pkg/parse" "github.com/containers/buildah/util" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "go.podman.io/common/pkg/auth" "go.podman.io/common/pkg/completion" "go.podman.io/image/v5/manifest" "go.podman.io/image/v5/pkg/shortnames" storageTransport "go.podman.io/image/v5/storage" "go.podman.io/image/v5/transports/alltransports" "go.podman.io/image/v5/types" ) type commitInputOptions struct { authfile string omitHistory bool blobCache string certDir string changes []string configFile string creds string cwOptions string disableCompression bool format string iidfile string manifest string omitTimestamp bool timestamp int64 sourceDateEpoch string rewriteTimestamp bool quiet bool referenceTime string rm bool pull string pullAlways bool pullNever bool sbomImgOutput string sbomImgPurlOutput string sbomMergeStrategy string sbomOutput string sbomPreset string sbomPurlOutput string sbomScannerCommand []string sbomScannerImage string signaturePolicy string signBy string squash bool tlsVerify bool identityLabel bool encryptionKeys []string encryptLayers []int unsetenvs []string addFile []string unsetAnnotation []string annotation []string createdAnnotation bool metadataFile string } func commitInit() { var ( opts commitInputOptions commitDescription = "\n Writes a new image using the container's read-write layer and, if it is based\n on an image, the layers of that image." ) commitCommand := &cobra.Command{ Use: "commit", Short: "Create an image from a working container", Long: commitDescription, RunE: func(cmd *cobra.Command, args []string) error { return commitCmd(cmd, args, opts) }, Example: `buildah commit containerID buildah commit containerID newImageName buildah commit containerID docker://localhost:5000/imageId`, } commitCommand.SetUsageTemplate(UsageTemplate()) commitListFlagSet(commitCommand, &opts) rootCmd.AddCommand(commitCommand) } func commitListFlagSet(cmd *cobra.Command, opts *commitInputOptions) { flags := cmd.Flags() flags.SetInterspersed(false) flags.StringArrayVar(&opts.addFile, "add-file", nil, "add contents of a file to the image at a specified path (`source:destination`)") flags.StringVar(&opts.authfile, "authfile", auth.GetDefaultAuthFile(), "path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") _ = cmd.RegisterFlagCompletionFunc("authfile", completion.AutocompleteDefault) flags.StringVar(&opts.blobCache, "blob-cache", "", "assume image blobs in the specified directory will be available for pushing") if err := flags.MarkHidden("blob-cache"); err != nil { panic(fmt.Sprintf("error marking blob-cache as hidden: %v", err)) } flags.StringSliceVar(&opts.encryptionKeys, "encryption-key", nil, "key with the encryption protocol to use needed to encrypt the image (e.g. jwe:/path/to/key.pem)") _ = cmd.RegisterFlagCompletionFunc("encryption-key", completion.AutocompleteDefault) flags.IntSliceVar(&opts.encryptLayers, "encrypt-layer", nil, "layers to encrypt, 0-indexed layer indices with support for negative indexing (e.g. 0 is the first layer, -1 is the last layer). If not defined, will encrypt all layers if encryption-key flag is specified") _ = cmd.RegisterFlagCompletionFunc("encryption-key", completion.AutocompleteNone) flags.StringArrayVarP(&opts.changes, "change", "c", nil, "apply containerfile `instruction`s to the committed image") flags.StringVar(&opts.certDir, "cert-dir", "", "use certificates at the specified path to access the registry") _ = cmd.RegisterFlagCompletionFunc("cert-dir", completion.AutocompleteDefault) flags.StringVar(&opts.configFile, "config", "", "apply configuration JSON `file` to the committed image") _ = cmd.RegisterFlagCompletionFunc("config", completion.AutocompleteDefault) flags.StringVar(&opts.creds, "creds", "", "use `[username[:password]]` for accessing the registry") _ = cmd.RegisterFlagCompletionFunc("creds", completion.AutocompleteNone) flags.StringVar(&opts.cwOptions, "cw", "", "confidential workload `options`") flags.BoolVarP(&opts.disableCompression, "disable-compression", "D", true, "don't compress layers") flags.StringVarP(&opts.format, "format", "f", defaultFormat(), "`format` of the image manifest and metadata") _ = cmd.RegisterFlagCompletionFunc("format", completion.AutocompleteNone) flags.StringVar(&opts.manifest, "manifest", "", "adds created image to the specified manifest list. Creates manifest list if it does not exist") _ = cmd.RegisterFlagCompletionFunc("manifest", completion.AutocompleteNone) flags.StringVar(&opts.iidfile, "iidfile", "", "write the image ID to the file") _ = cmd.RegisterFlagCompletionFunc("iidfile", completion.AutocompleteDefault) flags.StringVar(&opts.metadataFile, "metadata-file", "", "`file` to write metadata about the image to") _ = cmd.RegisterFlagCompletionFunc("metadata-file", completion.AutocompleteDefault) flags.BoolVar(&opts.omitTimestamp, "omit-timestamp", false, "set created timestamp to epoch 0 to allow for deterministic builds") sourceDateEpochUsageDefault := "current time" if v := os.Getenv(internal.SourceDateEpochName); v != "" { sourceDateEpochUsageDefault = fmt.Sprintf("%q", v) } flags.StringVar(&opts.sourceDateEpoch, "source-date-epoch", os.Getenv(internal.SourceDateEpochName), "set new timestamps in image info to `seconds` after the epoch, defaults to "+sourceDateEpochUsageDefault) _ = cmd.RegisterFlagCompletionFunc("source-date-epoch", completion.AutocompleteNone) flags.BoolVar(&opts.rewriteTimestamp, "rewrite-timestamp", false, "set timestamps in layer to no later than the value for --source-date-epoch") flags.Int64Var(&opts.timestamp, "timestamp", 0, "set new timestamps in image info and layer to `seconds` after the epoch, defaults to current times") _ = cmd.RegisterFlagCompletionFunc("timestamp", completion.AutocompleteNone) flags.BoolVarP(&opts.quiet, "quiet", "q", false, "don't output progress information when writing images") flags.StringVar(&opts.referenceTime, "reference-time", "", "set the timestamp on the image to match the named `file`") _ = cmd.RegisterFlagCompletionFunc("reference-time", completion.AutocompleteNone) flags.StringVar(&opts.pull, "pull", "true", "pull SBOM scanner images from the registry if newer or not present in store, if false, only pull SBOM scanner images if not present, if always, pull SBOM scanner images even if the named images are present in store, if never, only use images present in store if available") flags.Lookup("pull").NoOptDefVal = "true" // allow `--pull ` to be set to `true` as expected. flags.BoolVar(&opts.pullAlways, "pull-always", false, "pull the image even if the named image is present in store") if err := flags.MarkHidden("pull-always"); err != nil { panic(fmt.Sprintf("error marking the pull-always flag as hidden: %v", err)) } flags.BoolVar(&opts.pullNever, "pull-never", false, "do not pull the image, use the image present in store if available") if err := flags.MarkHidden("pull-never"); err != nil { panic(fmt.Sprintf("error marking the pull-never flag as hidden: %v", err)) } flags.StringVar(&opts.sbomPreset, "sbom", "", "scan working container using `preset` configuration") _ = cmd.RegisterFlagCompletionFunc("sbom", completion.AutocompleteNone) flags.StringVar(&opts.sbomScannerImage, "sbom-scanner-image", "", "scan working container using scanner command from `image`") _ = cmd.RegisterFlagCompletionFunc("sbom-scanner-image", completion.AutocompleteNone) flags.StringArrayVar(&opts.sbomScannerCommand, "sbom-scanner-command", nil, "scan working container using `command` in scanner image") _ = cmd.RegisterFlagCompletionFunc("sbom-scanner-command", completion.AutocompleteNone) flags.StringVar(&opts.sbomMergeStrategy, "sbom-merge-strategy", "", "merge scan results using `strategy`") _ = cmd.RegisterFlagCompletionFunc("sbom-merge-strategy", completion.AutocompleteNone) flags.StringVar(&opts.sbomOutput, "sbom-output", "", "save scan results to `file`") _ = cmd.RegisterFlagCompletionFunc("sbom-output", completion.AutocompleteDefault) flags.StringVar(&opts.sbomImgOutput, "sbom-image-output", "", "add scan results to image as `path`") _ = cmd.RegisterFlagCompletionFunc("sbom-image-output", completion.AutocompleteNone) flags.StringVar(&opts.sbomPurlOutput, "sbom-purl-output", "", "save scan results to `file``") _ = cmd.RegisterFlagCompletionFunc("sbom-purl-output", completion.AutocompleteDefault) flags.StringVar(&opts.sbomImgPurlOutput, "sbom-image-purl-output", "", "add scan results to image as `path`") _ = cmd.RegisterFlagCompletionFunc("sbom-image-purl-output", completion.AutocompleteNone) flags.StringVar(&opts.signBy, "sign-by", "", "sign the image using a GPG key with the specified `FINGERPRINT`") _ = cmd.RegisterFlagCompletionFunc("sign-by", completion.AutocompleteNone) if err := flags.MarkHidden("omit-timestamp"); err != nil { panic(fmt.Sprintf("error marking omit-timestamp as hidden: %v", err)) } if err := flags.MarkHidden("reference-time"); err != nil { panic(fmt.Sprintf("error marking reference-time as hidden: %v", err)) } flags.BoolVar(&opts.omitHistory, "omit-history", false, "omit build history information from the built image (default false)") flags.BoolVar(&opts.identityLabel, "identity-label", true, "add default builder label (default true)") flags.BoolVar(&opts.rm, "rm", false, "remove the container and its content after committing it to an image. Default leaves the container and its content in place.") flags.StringVar(&opts.signaturePolicy, "signature-policy", "", "`pathname` of signature policy file (not usually used)") _ = cmd.RegisterFlagCompletionFunc("signature-policy", completion.AutocompleteDefault) if err := flags.MarkHidden("signature-policy"); err != nil { panic(fmt.Sprintf("error marking signature-policy as hidden: %v", err)) } flags.BoolVar(&opts.squash, "squash", false, "produce an image with only one layer") flags.BoolVar(&opts.tlsVerify, "tls-verify", true, "require HTTPS and verify certificates when accessing the registry. TLS verification cannot be used when talking to an insecure registry.") flags.StringSliceVar(&opts.unsetenvs, "unsetenv", nil, "unset env from final image") _ = cmd.RegisterFlagCompletionFunc("unsetenv", completion.AutocompleteNone) flags.StringSliceVar(&opts.unsetAnnotation, "unsetannotation", nil, "unset annotation when inheriting annotations from base image") _ = cmd.RegisterFlagCompletionFunc("unsetannotation", completion.AutocompleteNone) flags.StringArrayVar(&opts.annotation, "annotation", []string{}, "set metadata for an image (default [])") _ = cmd.RegisterFlagCompletionFunc("annotation", completion.AutocompleteNone) flags.BoolVar(&opts.createdAnnotation, "created-annotation", true, `set an "org.opencontainers.image.created" annotation in the image`) } func commitCmd(c *cobra.Command, args []string, iopts commitInputOptions) error { var dest types.ImageReference if len(args) == 0 { return errors.New("container ID must be specified") } if err := cli.VerifyFlagsArgsOrder(args); err != nil { return err } if err := auth.CheckAuthFile(iopts.authfile); err != nil { return err } name := args[0] args = Tail(args) if len(args) > 1 { return errors.New("too many arguments specified") } image := "" if len(args) > 0 { image = args[0] } compress := define.Gzip if iopts.disableCompression { compress = define.Uncompressed } format, err := cli.GetFormat(iopts.format) if err != nil { return err } store, err := getStore(c) if err != nil { return err } ctx := getContext() builder, err := openBuilder(ctx, store, name) if err != nil { return fmt.Errorf("reading build container %q: %w", name, err) } systemContext, err := parse.SystemContextFromOptions(c) if err != nil { return fmt.Errorf("building system context: %w", err) } // If the user specified an image, we may need to massage it a bit if // no transport is specified. if image != "" { if dest, err = alltransports.ParseImageName(image); err != nil { candidates, err2 := shortnames.ResolveLocally(systemContext, image) if err2 != nil { return err2 } if len(candidates) == 0 { return fmt.Errorf("parsing target image name %q", image) } dest2, err2 := storageTransport.Transport.ParseStoreReference(store, candidates[0].String()) if err2 != nil { return fmt.Errorf("parsing target image name %q: %w", image, err) } dest = dest2 } } encConfig, encLayers, err := cli.EncryptConfig(iopts.encryptionKeys, iopts.encryptLayers) if err != nil { return fmt.Errorf("unable to obtain encryption config: %w", err) } var overrideConfig *manifest.Schema2Config if c.Flag("config").Changed { configBytes, err := os.ReadFile(iopts.configFile) if err != nil { return fmt.Errorf("reading configuration blob from file: %w", err) } overrideConfig = &manifest.Schema2Config{} if err := json.Unmarshal(configBytes, &overrideConfig); err != nil { return fmt.Errorf("parsing configuration blob from %q: %w", iopts.configFile, err) } } var addFiles map[string]string if len(iopts.addFile) > 0 { addFiles = make(map[string]string) for _, spec := range iopts.addFile { specSlice := strings.SplitN(spec, ":", 2) if len(specSlice) == 1 { specSlice = []string{specSlice[0], specSlice[0]} } if len(specSlice) != 2 { return fmt.Errorf("parsing add-file argument %q: expected 1 or 2 parts, got %d", spec, len(strings.SplitN(spec, ":", 2))) } st, err := os.Stat(specSlice[0]) if err != nil { return fmt.Errorf("parsing add-file argument %q: source %q: %w", spec, specSlice[0], err) } if st.IsDir() { return fmt.Errorf("parsing add-file argument %q: source %q is not a regular file", spec, specSlice[0]) } addFiles[specSlice[1]] = specSlice[0] } } options := buildah.CommitOptions{ PreferredManifestType: format, Manifest: iopts.manifest, Compression: compress, SignaturePolicyPath: iopts.signaturePolicy, SystemContext: systemContext, IIDFile: iopts.iidfile, Squash: iopts.squash, BlobDirectory: iopts.blobCache, OmitHistory: iopts.omitHistory, SignBy: iopts.signBy, OciEncryptConfig: encConfig, OciEncryptLayers: encLayers, UnsetEnvs: iopts.unsetenvs, OverrideChanges: iopts.changes, OverrideConfig: overrideConfig, ExtraImageContent: addFiles, UnsetAnnotations: iopts.unsetAnnotation, Annotations: iopts.annotation, CreatedAnnotation: types.NewOptionalBool(iopts.createdAnnotation), } exclusiveFlags := 0 if c.Flag("reference-time").Changed { exclusiveFlags++ referenceFile := iopts.referenceTime finfo, err := os.Stat(referenceFile) if err != nil { return fmt.Errorf("reading timestamp of file %q: %w", referenceFile, err) } timestamp := finfo.ModTime().UTC() options.HistoryTimestamp = ×tamp } if iopts.sourceDateEpoch != "" { exclusiveFlags++ sourceDateEpochVal, err := strconv.ParseInt(iopts.sourceDateEpoch, 10, 64) if err != nil { return fmt.Errorf("parsing source date epoch %q: %w", iopts.sourceDateEpoch, err) } sourceDateEpoch := time.Unix(sourceDateEpochVal, 0).UTC() options.SourceDateEpoch = &sourceDateEpoch } options.RewriteTimestamp = iopts.rewriteTimestamp if c.Flag("timestamp").Changed { exclusiveFlags++ timestamp := time.Unix(iopts.timestamp, 0).UTC() options.HistoryTimestamp = ×tamp } if iopts.omitTimestamp { exclusiveFlags++ timestamp := time.Unix(0, 0).UTC() options.HistoryTimestamp = ×tamp } if exclusiveFlags > 1 { return errors.New("cannot use more then one timestamp option at at time") } // Add builder identity information. var identityLabel types.OptionalBool if c.Flag("identity-label").Changed { identityLabel = types.NewOptionalBool(iopts.identityLabel) } switch identityLabel { case types.OptionalBoolTrue: builder.SetLabel(buildah.BuilderIdentityAnnotation, define.Version) case types.OptionalBoolFalse: // nothing - don't clear it if there's a value set in the base image default: if options.HistoryTimestamp == nil && options.SourceDateEpoch == nil { builder.SetLabel(buildah.BuilderIdentityAnnotation, define.Version) } } if iopts.cwOptions != "" { confidentialWorkloadOptions, err := parse.GetConfidentialWorkloadOptions(iopts.cwOptions) if err != nil { return fmt.Errorf("parsing --cw arguments: %w", err) } options.ConfidentialWorkloadOptions = confidentialWorkloadOptions } pullPolicy, err := parse.PullPolicyFromOptions(c) if err != nil { return err } if c.Flag("sbom").Changed || c.Flag("sbom-scanner-command").Changed || c.Flag("sbom-scanner-image").Changed || c.Flag("sbom-image-output").Changed || c.Flag("sbom-merge-strategy").Changed || c.Flag("sbom-output").Changed || c.Flag("sbom-image-output").Changed || c.Flag("sbom-purl-output").Changed || c.Flag("sbom-image-purl-output").Changed { var sbomOptions []define.SBOMScanOptions sbomOption, err := parse.SBOMScanOptions(c) if err != nil { return err } sbomOption.PullPolicy = pullPolicy sbomOptions = append(sbomOptions, *sbomOption) options.SBOMScanOptions = sbomOptions } if !iopts.quiet { options.ReportWriter = os.Stderr } results, err := builder.CommitResults(ctx, dest, options) if err != nil { return util.GetFailureCause(err, fmt.Errorf("committing container %q to %q: %w", builder.Container, image, err)) } ref := results.Canonical id := results.ImageID if ref != nil && id != "" { logrus.Debugf("wrote image %s with ID %s", ref, id) } else if ref != nil { logrus.Debugf("wrote image %s", ref) } else if id != "" { logrus.Debugf("wrote image with ID %s", id) } else { logrus.Debugf("wrote image") } if options.IIDFile == "" && id != "" { fmt.Printf("%s\n", id) } if iopts.metadataFile != "" { metadataBytes, err := json.Marshal(results.Metadata) if err != nil { return fmt.Errorf("encoding contents for %q: %w", iopts.metadataFile, err) } if err := os.WriteFile(iopts.metadataFile, metadataBytes, 0o644); err != nil { return err } } if iopts.rm { return builder.Delete() } return nil } ================================================ FILE: cmd/buildah/common.go ================================================ package main import ( "context" "errors" "fmt" "os" "github.com/containers/buildah" "github.com/spf13/cobra" "github.com/spf13/pflag" "go.podman.io/common/pkg/umask" is "go.podman.io/image/v5/storage" "go.podman.io/image/v5/types" "go.podman.io/storage" "go.podman.io/storage/pkg/homedir" "go.podman.io/storage/pkg/unshare" ) // configuration, including customizations made in containers.conf var needToShutdownStore = false func getStore(c *cobra.Command) (storage.Store, error) { if err := setXDGRuntimeDir(); err != nil { return nil, err } options, err := storage.DefaultStoreOptions() if err != nil { return nil, err } if c.Flag("root").Changed || c.Flag("runroot").Changed { options.GraphRoot = globalFlagResults.Root options.RunRoot = globalFlagResults.RunRoot } if c.Flag("imagestore").Changed { options.ImageStore = globalFlagResults.ImageStore } if c.Flag("transient-store").Changed { options.TransientStore = globalFlagResults.TransientStore } if c.Flag("storage-driver").Changed { options.GraphDriverName = globalFlagResults.StorageDriver // If any options setup in config, these should be dropped if user overrode the driver options.GraphDriverOptions = []string{} } if c.Flag("storage-opt").Changed { if len(globalFlagResults.StorageOpts) > 0 { options.GraphDriverOptions = globalFlagResults.StorageOpts } } // Do not allow to mount a graphdriver that is not vfs if we are creating the userns as part // of the mount command. // Differently, allow the mount if we are already in a userns, as the mount point will still // be accessible once "buildah mount" exits. if os.Geteuid() != 0 && options.GraphDriverName != "vfs" { return nil, fmt.Errorf("cannot mount using driver %s in rootless mode. You need to run it in a `buildah unshare` session", options.GraphDriverName) } if len(globalFlagResults.UserNSUID) > 0 { uopts := globalFlagResults.UserNSUID gopts := globalFlagResults.UserNSGID if len(gopts) == 0 { gopts = uopts } uidmap, gidmap, err := unshare.ParseIDMappings(uopts, gopts) if err != nil { return nil, err } options.UIDMap = uidmap options.GIDMap = gidmap } else { if len(globalFlagResults.UserNSGID) > 0 { return nil, errors.New("option --userns-gid-map can not be used without --userns-uid-map") } } // If a subcommand has the flags, check if they are set; if so, override the global values if c.Flags().Lookup("userns-uid-map").Changed { uopts, _ := c.Flags().GetStringSlice("userns-uid-map") gopts, _ := c.Flags().GetStringSlice("userns-gid-map") if len(gopts) == 0 { gopts = uopts } uidmap, gidmap, err := unshare.ParseIDMappings(uopts, gopts) if err != nil { return nil, err } options.UIDMap = uidmap options.GIDMap = gidmap } else { if c.Flags().Lookup("userns-gid-map").Changed { return nil, errors.New("option --userns-gid-map can not be used without --userns-uid-map") } } umask.Check() store, err := storage.GetStore(options) if store != nil { is.Transport.SetStore(store) } needToShutdownStore = true return store, err } // setXDGRuntimeDir sets XDG_RUNTIME_DIR when if it is unset under rootless func setXDGRuntimeDir() error { if unshare.IsRootless() && os.Getenv("XDG_RUNTIME_DIR") == "" { runtimeDir, err := homedir.GetRuntimeDir() if err != nil { return err } if err := os.Setenv("XDG_RUNTIME_DIR", runtimeDir); err != nil { return errors.New("could not set XDG_RUNTIME_DIR") } } return nil } func openBuilder(ctx context.Context, store storage.Store, name string) (builder *buildah.Builder, err error) { if name != "" { builder, err = buildah.OpenBuilder(store, name) if errors.Is(err, os.ErrNotExist) { options := buildah.ImportOptions{ Container: name, } builder, err = buildah.ImportBuilder(ctx, store, options) } } if err != nil { return nil, err } if builder == nil { return nil, errors.New("finding build container") } return builder, nil } func openBuilders(store storage.Store) (builders []*buildah.Builder, err error) { return buildah.OpenAllBuilders(store) } func openImage(ctx context.Context, sc *types.SystemContext, store storage.Store, name string) (builder *buildah.Builder, err error) { options := buildah.ImportFromImageOptions{ Image: name, SystemContext: sc, } builder, err = buildah.ImportBuilderFromImage(ctx, store, options) if err != nil { return nil, err } if builder == nil { return nil, errors.New("mocking up build configuration") } return builder, nil } // getContext returns a context.TODO func getContext() context.Context { return context.TODO() } func getUserFlags() pflag.FlagSet { fs := pflag.FlagSet{} fs.String("user", "", "`user[:group]` to run the command as") return fs } func defaultFormat() string { format := os.Getenv("BUILDAH_FORMAT") if format != "" { return format } return buildah.OCI } // Tail returns a string slice after the first element unless there are // not enough elements, then it returns an empty slice. This is to replace // the urfavecli Tail method for args func Tail(a []string) []string { if len(a) >= 2 { return a[1:] } return []string{} } // UsageTemplate returns the usage template for buildah commands // This blocks the displaying of the global options. The main buildah // command should not use this. func UsageTemplate() string { return `Usage:{{if .Runnable}} {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}} Aliases: {{.NameAndAliases}}{{end}}{{if .HasExample}} Examples: {{.Example}}{{end}}{{if .HasAvailableSubCommands}} Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} Flags: {{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} {{end}} ` } ================================================ FILE: cmd/buildah/common_test.go ================================================ package main import ( "flag" "os" "os/user" "testing" "github.com/containers/buildah" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "go.podman.io/image/v5/types" "go.podman.io/storage" ) var ( signaturePolicyPath = "" storeOptions, _ = storage.DefaultStoreOptions() testSystemContext = types.SystemContext{ SignaturePolicyPath: "../../tests/policy.json", SystemRegistriesConfPath: "../../tests/registries.conf", } ) func TestMain(m *testing.M) { flag.StringVar(&signaturePolicyPath, "signature-policy", "", "pathname of signature policy file (not usually used)") options := storage.StoreOptions{} debug := false flag.StringVar(&options.GraphRoot, "root", "", "storage root dir") flag.StringVar(&options.RunRoot, "runroot", "", "storage state dir") flag.StringVar(&options.GraphDriverName, "storage-driver", "", "storage driver") flag.StringVar(&options.ImageStore, "imagestore", "", "storage imagestore") flag.BoolVar(&options.TransientStore, "transient-store", false, "use transient storage") flag.StringVar(&testSystemContext.SystemRegistriesConfPath, "registries-conf", "", "registries list") flag.BoolVar(&debug, "debug", false, "turn on debug logging") flag.Parse() if options.GraphRoot != "" || options.RunRoot != "" || options.GraphDriverName != "" { storeOptions = options } if buildah.InitReexec() { return } logrus.SetLevel(logrus.ErrorLevel) if debug { logrus.SetLevel(logrus.DebugLevel) } os.Exit(m.Run()) } func TestGetStore(t *testing.T) { // Make sure the tests are running as root failTestIfNotRoot(t) testCmd := &cobra.Command{ Use: "test", RunE: func(cmd *cobra.Command, _ []string) error { _, err := getStore(cmd) return err }, } flags := testCmd.PersistentFlags() flags.String("root", storeOptions.GraphRoot, "") flags.String("runroot", storeOptions.RunRoot, "") flags.String("imagestore", storeOptions.ImageStore, "") flags.Bool("transient-store", storeOptions.TransientStore, "") flags.String("storage-driver", storeOptions.GraphDriverName, "") flags.String("signature-policy", "", "") if err := flags.MarkHidden("signature-policy"); err != nil { t.Error(err) } // The following flags had to be added or we get panics in common.go when // the lookups occur flags.StringSlice("storage-opt", []string{}, "") flags.String("registries-conf", "", "") flags.String("userns-uid-map", "", "") flags.String("userns-gid-map", "", "") err := testCmd.Execute() if err != nil { t.Error(err) } } func failTestIfNotRoot(t *testing.T) { u, err := user.Current() if err != nil { t.Log("Could not determine user. Running without root may cause tests to fail") } else if u.Uid != "0" { t.Fatal("tests will fail unless run as root") } } ================================================ FILE: cmd/buildah/config.go ================================================ package main import ( "encoding/json" "fmt" "os" "strconv" "strings" "time" "github.com/containers/buildah" "github.com/containers/buildah/define" "github.com/containers/buildah/docker" buildahcli "github.com/containers/buildah/pkg/cli" "github.com/mattn/go-shellwords" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) type configResults struct { addHistory bool annotation []string arch string author string cmd string comment string createdBy string domainName string entrypoint string env []string healthcheck string healthcheckInterval string healthcheckRetries int healthcheckStartPeriod string healthcheckStartInterval string healthcheckTimeout string historyComment string hostname string label []string onbuild []string os string osfeature []string osversion string ports []string shell string stopSignal string user string variant string volume []string workingDir string unsetLabels []string unsetAnnotations []string } func configInit() { var ( configDescription = "\n Modifies the configuration values which will be saved to the image." opts configResults ) configCommand := &cobra.Command{ Use: "config", Short: "Update image configuration settings", Long: configDescription, RunE: func(cmd *cobra.Command, args []string) error { return configCmd(cmd, args, opts) }, Example: `buildah config --author='Jane Austen' --workingdir='/etc/mycontainers' containerID buildah config --entrypoint '[ "/entrypoint.sh", "dev" ]' containerID buildah config --env foo=bar --env PATH=$PATH containerID`, } configCommand.SetUsageTemplate(UsageTemplate()) flags := configCommand.Flags() flags.SetInterspersed(false) flags.BoolVar(&opts.addHistory, "add-history", false, "add an entry for this operation to the image's history. Use BUILDAH_HISTORY environment variable to override. (default false)") flags.StringArrayVarP(&opts.annotation, "annotation", "a", []string{}, "add `annotation` e.g. annotation=value, for the target image (default [])") flags.StringVar(&opts.arch, "arch", "", "set `architecture` of the target image") flags.StringVar(&opts.author, "author", "", "set image author contact `information`") flags.StringVar(&opts.cmd, "cmd", "", "set the default `command` to run for containers based on the image") flags.StringVar(&opts.comment, "comment", "", "set a `comment` in the target image") flags.StringVar(&opts.createdBy, "created-by", "", "set `description` of how the image was created") flags.StringVar(&opts.domainName, "domainname", "", "set a domain `name` for containers based on image") flags.StringVar(&opts.entrypoint, "entrypoint", "", "set `entry point` for containers based on image") flags.StringArrayVarP(&opts.env, "env", "e", []string{}, "add `environment variable` to be set when running containers based on image (default [])") flags.StringVar(&opts.healthcheck, "healthcheck", "", "set a `healthcheck` command for the target image") flags.StringVar(&opts.healthcheckInterval, "healthcheck-interval", "", "set the `interval` between runs of the `healthcheck` command for the target image") flags.IntVar(&opts.healthcheckRetries, "healthcheck-retries", 0, "set the `number` of times the `healthcheck` command has to fail") flags.StringVar(&opts.healthcheckStartPeriod, "healthcheck-start-period", "", "set the amount of `time` to wait after starting a container before a failed `healthcheck` command will count as a failure") flags.StringVar(&opts.healthcheckStartInterval, "healthcheck-start-interval", "", "set the time between health checks during the start period. Only available with format `docker`") flags.StringVar(&opts.healthcheckTimeout, "healthcheck-timeout", "", "set the maximum amount of `time` to wait for a `healthcheck` command for the target image") flags.StringVar(&opts.historyComment, "history-comment", "", "set a `comment` for the history of the target image") flags.StringVar(&opts.hostname, "hostname", "", "set a host`name` for containers based on image") flags.StringArrayVarP(&opts.label, "label", "l", []string{}, "add image configuration `label` e.g. label=value") flags.StringSliceVar(&opts.onbuild, "onbuild", []string{}, "add onbuild command to be run on images based on this image. Only supported on 'docker' formatted images") flags.StringVar(&opts.os, "os", "", "set `operating system` of the target image") flags.StringArrayVar(&opts.osfeature, "os-feature", []string{}, "set required OS `feature` for the target image") flags.StringVar(&opts.osversion, "os-version", "", "set required OS `version` for the target image") flags.StringSliceVarP(&opts.ports, "port", "p", []string{}, "add `port` to expose when running containers based on image (default [])") flags.StringVar(&opts.shell, "shell", "", "add `shell` to run in containers") flags.StringVar(&opts.stopSignal, "stop-signal", "", "set `stop signal` for containers based on image") flags.StringVarP(&opts.user, "user", "u", "", "set default `user` to run inside containers based on image") flags.StringVar(&opts.variant, "variant", "", "set architecture `variant` of the target image") flags.StringSliceVarP(&opts.volume, "volume", "v", []string{}, "add default `volume` path to be created for containers based on image (default [])") flags.StringVar(&opts.workingDir, "workingdir", "", "set working `directory` for containers based on image") flags.StringSliceVar(&opts.unsetLabels, "unsetlabel", nil, "remove image configuration label") flags.StringSliceVar(&opts.unsetAnnotations, "unsetannotation", nil, "remove image configuration annotation") rootCmd.AddCommand(configCommand) } func updateCmd(builder *buildah.Builder, cmd string) error { if len(strings.TrimSpace(cmd)) == 0 { builder.SetCmd(nil) return nil } var cmdJSON []string err := json.Unmarshal([]byte(cmd), &cmdJSON) if err == nil { builder.SetCmd(cmdJSON) return nil } cmdSpec, err := shellwords.Parse(cmd) if err != nil { return fmt.Errorf("parsing --cmd %q: %w", cmd, err) } builder.SetCmd(cmdSpec) return nil } func updateEntrypoint(builder *buildah.Builder, entrypoint string) { if len(strings.TrimSpace(entrypoint)) == 0 { builder.SetEntrypoint(nil) return } var entrypointJSON []string err := json.Unmarshal([]byte(entrypoint), &entrypointJSON) if err == nil { builder.SetEntrypoint(entrypointJSON) if len(builder.Cmd()) > 0 { logrus.Warnf("cmd %q exists and will be passed to entrypoint as a parameter", strings.Join(builder.Cmd(), " ")) } return } // it wasn't a valid json array, fall back to string entrypointSpec := make([]string, 3) entrypointSpec[0] = "/bin/sh" entrypointSpec[1] = "-c" entrypointSpec[2] = entrypoint if len(builder.Cmd()) > 0 { logrus.Warnf("cmd %q exists but will be ignored because of entrypoint settings", strings.Join(builder.Cmd(), " ")) } builder.SetEntrypoint(entrypointSpec) } func conditionallyAddHistory(builder *buildah.Builder, c *cobra.Command, createdByFmt string, args ...any) { history := buildahcli.DefaultHistory() if c.Flag("add-history").Changed { history, _ = c.Flags().GetBool("add-history") } if history { now := time.Now().UTC() created := &now createdBy := fmt.Sprintf(createdByFmt, args...) builder.AddPrependedEmptyLayer(created, createdBy, "", "") } } func updateConfig(builder *buildah.Builder, c *cobra.Command, iopts configResults) error { if c.Flag("author").Changed { builder.SetMaintainer(iopts.author) } if c.Flag("created-by").Changed { builder.SetCreatedBy(iopts.createdBy) } if c.Flag("arch").Changed { builder.SetArchitecture(iopts.arch) } if c.Flag("variant").Changed { builder.SetVariant(iopts.variant) } if c.Flag("os").Changed { builder.SetOS(iopts.os) } if c.Flag("os-feature").Changed { for _, osFeatureSpec := range iopts.osfeature { switch { case osFeatureSpec == "-": builder.ClearOSFeatures() case strings.HasSuffix(osFeatureSpec, "-"): builder.UnsetOSFeature(strings.TrimSuffix(osFeatureSpec, "-")) default: builder.SetOSFeature(osFeatureSpec) } } } if c.Flag("os-version").Changed { builder.SetOSVersion(iopts.osversion) } if c.Flag("user").Changed { builder.SetUser(iopts.user) conditionallyAddHistory(builder, c, "/bin/sh -c #(nop) USER %s", iopts.user) } if c.Flag("shell").Changed { shell := iopts.shell shellSpec, err := shellwords.Parse(shell) if err != nil { return fmt.Errorf("parsing --shell %q: %w", shell, err) } builder.SetShell(shellSpec) conditionallyAddHistory(builder, c, "/bin/sh -c #(nop) SHELL %s", shell) } if c.Flag("stop-signal").Changed { builder.SetStopSignal(iopts.stopSignal) conditionallyAddHistory(builder, c, "/bin/sh -c #(nop) STOPSIGNAL %s", iopts.stopSignal) } if c.Flag("port").Changed { for _, portSpec := range iopts.ports { switch { case string(portSpec[0]) == "-": builder.ClearPorts() case strings.HasSuffix(portSpec, "-"): builder.UnsetPort(strings.TrimSuffix(portSpec, "-")) default: builder.SetPort(portSpec) } } conditionallyAddHistory(builder, c, "/bin/sh -c #(nop) EXPOSE %s", strings.Join(iopts.ports, " ")) } for _, envSpec := range iopts.env { env := strings.SplitN(envSpec, "=", 2) switch { case len(env) > 1: var unexpanded []string getenv := func(name string) string { for _, envvar := range builder.Env() { val := strings.SplitN(envvar, "=", 2) if len(val) == 2 && val[0] == name { return val[1] } } logrus.Errorf("error expanding variable %q: no value set in configuration", name) unexpanded = append(unexpanded, name) return name } env[1] = os.Expand(env[1], getenv) builder.SetEnv(env[0], env[1]) case env[0] == "-": builder.ClearEnv() case strings.HasSuffix(env[0], "-"): builder.UnsetEnv(strings.TrimSuffix(env[0], "-")) default: value := os.Getenv(env[0]) if value == "" { return fmt.Errorf("setting env %q: no value given", env[0]) } builder.SetEnv(env[0], value) } } conditionallyAddHistory(builder, c, "/bin/sh -c #(nop) ENV %s", strings.Join(iopts.env, " ")) if c.Flag("entrypoint").Changed { updateEntrypoint(builder, iopts.entrypoint) conditionallyAddHistory(builder, c, "/bin/sh -c #(nop) ENTRYPOINT %s", iopts.entrypoint) } // cmd should always run after entrypoint; setting entrypoint clears cmd if c.Flag("cmd").Changed { if err := updateCmd(builder, iopts.cmd); err != nil { return err } conditionallyAddHistory(builder, c, "/bin/sh -c #(nop) CMD %s", iopts.cmd) } if c.Flag("volume").Changed { if volSpec := iopts.volume; len(volSpec) > 0 { for _, volVal := range volSpec { switch { case volVal == "-": builder.ClearVolumes() case strings.HasSuffix(volVal, "-"): builder.RemoveVolume(strings.TrimSuffix(volVal, "-")) default: builder.AddVolume(volVal) } conditionallyAddHistory(builder, c, "/bin/sh -c #(nop) VOLUME %s", volVal) } } } if err := updateHealthcheck(builder, c, iopts); err != nil { return err } if c.Flag("label").Changed { for _, labelSpec := range iopts.label { label := strings.SplitN(labelSpec, "=", 2) switch { case len(label) > 1: builder.SetLabel(label[0], label[1]) case label[0] == "-": builder.ClearLabels() case strings.HasSuffix(label[0], "-"): builder.UnsetLabel(strings.TrimSuffix(label[0], "-")) default: builder.SetLabel(label[0], "") } } conditionallyAddHistory(builder, c, "/bin/sh -c #(nop) LABEL %s", strings.Join(iopts.label, " ")) } // unset labels if any for _, key := range iopts.unsetLabels { builder.UnsetLabel(key) } // unset annotation if any for _, key := range iopts.unsetAnnotations { builder.UnsetAnnotation(key) } if c.Flag("workingdir").Changed { builder.SetWorkDir(iopts.workingDir) conditionallyAddHistory(builder, c, "/bin/sh -c #(nop) WORKDIR %s", iopts.workingDir) } if c.Flag("comment").Changed { builder.SetComment(iopts.comment) } if c.Flag("history-comment").Changed { builder.SetHistoryComment(iopts.historyComment) } if c.Flag("domainname").Changed { builder.SetDomainname(iopts.domainName) } if c.Flag("hostname").Changed { name := iopts.hostname if name != "" && builder.Format == define.OCIv1ImageManifest { logrus.Warnf("HOSTNAME is not supported for OCI V1 image format, hostname %s will be ignored. Must use `docker` format", name) } builder.SetHostname(name) } if c.Flag("onbuild").Changed { for _, onbuild := range iopts.onbuild { builder.SetOnBuild(onbuild) conditionallyAddHistory(builder, c, "/bin/sh -c #(nop) ONBUILD %s", onbuild) } } if c.Flag("annotation").Changed { for _, annotationSpec := range iopts.annotation { annotation := strings.SplitN(annotationSpec, "=", 2) switch { case len(annotation) > 1: builder.SetAnnotation(annotation[0], annotation[1]) case annotation[0] == "-": builder.ClearAnnotations() case strings.HasSuffix(annotation[0], "-"): builder.UnsetAnnotation(strings.TrimSuffix(annotation[0], "-")) default: builder.SetAnnotation(annotation[0], "") } } } return nil } func updateHealthcheck(builder *buildah.Builder, c *cobra.Command, iopts configResults) error { if c.Flag("healthcheck").Changed || c.Flag("healthcheck-interval").Changed || c.Flag("healthcheck-retries").Changed || c.Flag("healthcheck-start-period").Changed || c.Flag("healthcheck-timeout").Changed { healthcheck := builder.Healthcheck() args := "" if healthcheck == nil { healthcheck = &docker.HealthConfig{ Test: []string{"NONE"}, Interval: 30 * time.Second, StartPeriod: 0, Timeout: 30 * time.Second, Retries: 3, } } if c.Flag("healthcheck").Changed { test, err := shellwords.Parse(iopts.healthcheck) if err != nil { return fmt.Errorf("parsing --healthcheck %q: %w", iopts.healthcheck, err) } healthcheck.Test = test } if c.Flag("healthcheck-interval").Changed { duration, err := time.ParseDuration(iopts.healthcheckInterval) if err != nil { return fmt.Errorf("parsing --healthcheck-interval %q: %w", iopts.healthcheckInterval, err) } healthcheck.Interval = duration args = args + "--interval=" + iopts.healthcheckInterval + " " } if c.Flag("healthcheck-retries").Changed { healthcheck.Retries = iopts.healthcheckRetries args = args + "--retries=" + strconv.Itoa(iopts.healthcheckRetries) + " " // args = fmt.Sprintf("%s --retries=%d ", args, iopts.healthcheckRetries) } if c.Flag("healthcheck-start-period").Changed { duration, err := time.ParseDuration(iopts.healthcheckStartPeriod) if err != nil { return fmt.Errorf("parsing --healthcheck-start-period %q: %w", iopts.healthcheckStartPeriod, err) } healthcheck.StartPeriod = duration args = args + "--start-period=" + iopts.healthcheckStartPeriod + " " } if c.Flag("healthcheck-start-interval").Changed { duration, err := time.ParseDuration(iopts.healthcheckStartInterval) if err != nil { return fmt.Errorf("parsing --healthcheck-start-interval %q: %w", iopts.healthcheckStartInterval, err) } healthcheck.StartInterval = duration args = args + "--start-interval=" + iopts.healthcheckStartInterval + " " } if c.Flag("healthcheck-timeout").Changed { duration, err := time.ParseDuration(iopts.healthcheckTimeout) if err != nil { return fmt.Errorf("parsing --healthcheck-timeout %q: %w", iopts.healthcheckTimeout, err) } healthcheck.Timeout = duration args = args + "--timeout=" + iopts.healthcheckTimeout + " " } if len(healthcheck.Test) == 0 { builder.SetHealthcheck(nil) conditionallyAddHistory(builder, c, "/bin/sh -c #(nop) HEALTHCHECK NONE") } else { builder.SetHealthcheck(healthcheck) conditionallyAddHistory(builder, c, "/bin/sh -c #(nop) HEALTHCHECK %s%s", args, iopts.healthcheck) } } return nil } func configCmd(c *cobra.Command, args []string, iopts configResults) error { if len(args) == 0 { return fmt.Errorf("container ID must be specified") } if err := buildahcli.VerifyFlagsArgsOrder(args); err != nil { return err } if len(args) > 1 { return fmt.Errorf("too many arguments specified") } name := args[0] store, err := getStore(c) if err != nil { return err } builder, err := openBuilder(getContext(), store, name) if err != nil { return fmt.Errorf("reading build container %q: %w", name, err) } if err := updateConfig(builder, c, iopts); err != nil { return err } return builder.Save() } ================================================ FILE: cmd/buildah/containers.go ================================================ package main import ( "encoding/json" "errors" "fmt" "strings" "github.com/containers/buildah" "github.com/containers/buildah/define" "github.com/containers/buildah/util" "github.com/spf13/cobra" "go.podman.io/common/pkg/formats" "go.podman.io/storage" ) var containersHeader = map[string]string{ "ContainerName": "CONTAINER NAME", "ContainerID": "CONTAINER ID", "Builder": "BUILDER", "ImageID": "IMAGE ID", "ImageName": "IMAGE NAME", } type jsonContainer struct { ID string `json:"id"` Builder bool `json:"builder"` ImageID string `json:"imageid"` ImageName string `json:"imagename"` ContainerName string `json:"containername"` } type containerOutputParams struct { ContainerID string Builder string ImageID string ImageName string ContainerName string } type containerOptions struct { all bool format string json bool noHeading bool noTruncate bool quiet bool } type containerFilterParams struct { id string name string ancestor string } type containersResults struct { all bool filter string format string json bool noheading bool notruncate bool quiet bool } func containersInit() { var ( containersDescription = "\n Lists containers which appear to be " + define.Package + " working containers, their\n names and IDs, and the names and IDs of the images from which they were\n initialized." opts containersResults ) containersCommand := &cobra.Command{ Use: "containers", Aliases: []string{"list", "ls", "ps"}, Short: "List working containers and their base images", Long: containersDescription, // Flags: sortFlags(containersFlags), RunE: func(cmd *cobra.Command, args []string) error { return containersCmd(cmd, args, opts) }, Example: `buildah containers buildah containers --format "{{.ContainerID}} {{.ContainerName}}" buildah containers -q --noheading --notruncate`, } containersCommand.SetUsageTemplate(UsageTemplate()) flags := containersCommand.Flags() flags.BoolVarP(&opts.all, "all", "a", false, "also list non-buildah containers") flags.StringVarP(&opts.filter, "filter", "f", "", "filter output based on conditions provided") flags.StringVar(&opts.format, "format", "", "pretty-print containers using a Go template") flags.BoolVar(&opts.json, "json", false, "output in JSON format") flags.BoolVarP(&opts.noheading, "noheading", "n", false, "do not print column headings") flags.BoolVar(&opts.notruncate, "notruncate", false, "do not truncate output") flags.BoolVarP(&opts.quiet, "quiet", "q", false, "display only container IDs") rootCmd.AddCommand(containersCommand) } func containersCmd(c *cobra.Command, args []string, iopts containersResults) error { if len(args) > 0 { return errors.New("'buildah containers' does not accept arguments") } store, err := getStore(c) if err != nil { return err } if c.Flag("quiet").Changed && c.Flag("format").Changed { return errors.New("quiet and format are mutually exclusive") } opts := containerOptions{ all: iopts.all, format: iopts.format, json: iopts.json, noHeading: iopts.noheading, noTruncate: iopts.notruncate, quiet: iopts.quiet, } var params *containerFilterParams if c.Flag("filter").Changed { params, err = parseCtrFilter(iopts.filter) if err != nil { return fmt.Errorf("parsing filter: %w", err) } } if !opts.noHeading && !opts.quiet && opts.format == "" && !opts.json { containerOutputHeader(!opts.noTruncate) } return outputContainers(store, opts, params) } func outputContainers(store storage.Store, opts containerOptions, params *containerFilterParams) error { seenImages := make(map[string]string) imageNameForID := func(id string) string { if id == "" { return buildah.BaseImageFakeName } imageName, ok := seenImages[id] if ok { return imageName } img, err2 := store.Image(id) if err2 == nil && len(img.Names) > 0 { seenImages[id] = img.Names[0] } return seenImages[id] } builders, err := openBuilders(store) if err != nil { return fmt.Errorf("reading build containers: %w", err) } var ( containerOutput []containerOutputParams JSONContainers []jsonContainer ) if !opts.all { // only output containers created by buildah for _, builder := range builders { image := imageNameForID(builder.FromImageID) if !matchesCtrFilter(builder.ContainerID, builder.Container, builder.FromImageID, image, params) { continue } if opts.json { JSONContainers = append(JSONContainers, jsonContainer{ ID: builder.ContainerID, Builder: true, ImageID: builder.FromImageID, ImageName: image, ContainerName: builder.Container, }) continue } output := containerOutputParams{ ContainerID: builder.ContainerID, Builder: " *", ImageID: builder.FromImageID, ImageName: image, ContainerName: builder.Container, } containerOutput = append(containerOutput, output) } } else { // output all containers currently in storage builderMap := make(map[string]struct{}) for _, builder := range builders { builderMap[builder.ContainerID] = struct{}{} } containers, err2 := store.Containers() if err2 != nil { return fmt.Errorf("reading list of all containers: %w", err2) } for _, container := range containers { name := "" if len(container.Names) > 0 { name = container.Names[0] } _, ours := builderMap[container.ID] builder := "" if ours { builder = " *" } if !matchesCtrFilter(container.ID, name, container.ImageID, imageNameForID(container.ImageID), params) { continue } if opts.json { JSONContainers = append(JSONContainers, jsonContainer{ ID: container.ID, Builder: ours, ImageID: container.ImageID, ImageName: imageNameForID(container.ImageID), ContainerName: name, }) continue } output := containerOutputParams{ ContainerID: container.ID, Builder: builder, ImageID: container.ImageID, ImageName: imageNameForID(container.ImageID), ContainerName: name, } containerOutput = append(containerOutput, output) } } if opts.json { data, err := json.MarshalIndent(JSONContainers, "", " ") if err != nil { return err } fmt.Printf("%s\n", data) return nil } if opts.format != "" { out := formats.StdoutTemplateArray{Output: containersToGeneric(containerOutput), Template: opts.format, Fields: containersHeader} return formats.Writer(out).Out() } for _, ctr := range containerOutput { if opts.quiet { fmt.Printf("%-64s\n", ctr.ContainerID) continue } containerOutputUsingFormatString(!opts.noTruncate, ctr) } return nil } func containersToGeneric(templParams []containerOutputParams) (genericParams []any) { if len(templParams) > 0 { for _, v := range templParams { genericParams = append(genericParams, any(v)) } } return genericParams } func containerOutputUsingFormatString(truncate bool, params containerOutputParams) { if truncate { fmt.Printf("%-12.12s %-8s %-12.12s %-32s %s\n", params.ContainerID, params.Builder, params.ImageID, util.TruncateString(params.ImageName, 32), params.ContainerName) } else { fmt.Printf("%-64s %-8s %-64s %-32s %s\n", params.ContainerID, params.Builder, params.ImageID, params.ImageName, params.ContainerName) } } func containerOutputHeader(truncate bool) { if truncate { fmt.Printf("%-12s %-8s %-12s %-32s %s\n", "CONTAINER ID", "BUILDER", "IMAGE ID", "IMAGE NAME", "CONTAINER NAME") } else { fmt.Printf("%-64s %-8s %-64s %-32s %s\n", "CONTAINER ID", "BUILDER", "IMAGE ID", "IMAGE NAME", "CONTAINER NAME") } } func parseCtrFilter(filter string) (*containerFilterParams, error) { params := new(containerFilterParams) filters := strings.SplitSeq(filter, ",") for param := range filters { pair := strings.SplitN(param, "=", 2) if len(pair) != 2 { return nil, fmt.Errorf("incorrect filter value %q, should be of form filter=value", param) } switch strings.TrimSpace(pair[0]) { case "id": params.id = pair[1] case "name": params.name = pair[1] case "ancestor": params.ancestor = pair[1] default: return nil, fmt.Errorf("invalid filter %q", pair[0]) } } return params, nil } func matchesCtrName(ctrName, argName string) bool { return strings.Contains(ctrName, argName) } func matchesAncestor(imgName, imgID, argName string) bool { if matchesID(imgID, argName) { return true } return matchesReference(imgName, argName) } func matchesCtrFilter(ctrID, ctrName, imgID, imgName string, params *containerFilterParams) bool { if params == nil { return true } if params.id != "" && !matchesID(ctrID, params.id) { return false } if params.name != "" && !matchesCtrName(ctrName, params.name) { return false } if params.ancestor != "" && !matchesAncestor(imgName, imgID, params.ancestor) { return false } return true } ================================================ FILE: cmd/buildah/containers_test.go ================================================ package main import ( "bytes" "fmt" "io" "os" "testing" ) func TestContainerFormatStringOutput(t *testing.T) { params := containerOutputParams{ ContainerID: "e477836657bb", Builder: " ", ImageID: "f975c5035748", ImageName: "test/with/this/very/long/image:latest", ContainerName: "test-container", } const trimmedImageName = "test/with/this/very/long/imag..." output := captureOutput(func() { containerOutputUsingFormatString(true, params) }) expectedOutput := fmt.Sprintf("%-12.12s %-8s %-12.12s %-32s %s\n", params.ContainerID, params.Builder, params.ImageID, trimmedImageName, params.ContainerName) if output != expectedOutput { t.Errorf("Error outputting using format string:\n\texpected: %s\n\treceived: %s\n", expectedOutput, output) } output = captureOutput(func() { containerOutputUsingFormatString(false, params) }) expectedOutput = fmt.Sprintf("%-64s %-8s %-64s %-32s %s\n", params.ContainerID, params.Builder, params.ImageID, params.ImageName, params.ContainerName) if output != expectedOutput { t.Errorf("Error outputting using format string:\n\texpected: %s\n\treceived: %s\n", expectedOutput, output) } } func TestContainerHeaderOutput(t *testing.T) { output := captureOutput(func() { containerOutputHeader(true) }) expectedOutput := fmt.Sprintf("%-12s %-8s %-12s %-32s %s\n", "CONTAINER ID", "BUILDER", "IMAGE ID", "IMAGE NAME", "CONTAINER NAME") if output != expectedOutput { t.Errorf("Error outputting using format string:\n\texpected: %s\n\treceived: %s\n", expectedOutput, output) } output = captureOutput(func() { containerOutputHeader(false) }) expectedOutput = fmt.Sprintf("%-64s %-8s %-64s %-32s %s\n", "CONTAINER ID", "BUILDER", "IMAGE ID", "IMAGE NAME", "CONTAINER NAME") if output != expectedOutput { t.Errorf("Error outputting using format string:\n\texpected: %s\n\treceived: %s\n", expectedOutput, output) } } // Captures output so that it can be compared to expected values func captureOutput(f func()) string { old := os.Stdout r, w, _ := os.Pipe() os.Stdout = w f() w.Close() os.Stdout = old var buf bytes.Buffer io.Copy(&buf, r) //nolint return buf.String() } ================================================ FILE: cmd/buildah/dumpbolt.go ================================================ package main import ( "fmt" "strings" "github.com/spf13/cobra" bolt "go.etcd.io/bbolt" ) var ( dumpBoltDescription = `Dumps a bolt database. The output format should not be depended upon.` dumpBoltCommand = &cobra.Command{ Use: "dumpbolt", Short: "Dump a bolt database", Long: dumpBoltDescription, RunE: dumpBoltCmd, Example: "DATABASE", Args: cobra.ExactArgs(1), Hidden: true, } ) func dumpBoltCmd(_ *cobra.Command, args []string) error { db, err := bolt.Open(args[0], 0o600, &bolt.Options{ReadOnly: true}) if err != nil { return fmt.Errorf("opening database %q: %w", args[0], err) } defer db.Close() encode := func(value []byte) string { var b strings.Builder for i := range value { if value[i] <= 32 || value[i] >= 127 { fmt.Fprintf(&b, "\\%03o", value[i]) } else { b.WriteByte(value[i]) } } return b.String() } return db.View(func(tx *bolt.Tx) error { var dumpBucket func(string, []byte, *bolt.Bucket) error dumpBucket = func(indent string, name []byte, b *bolt.Bucket) error { var subs [][]byte indentMore := " " fmt.Printf("%s%s:\n", indent, encode(name)) err := b.ForEach(func(k, v []byte) (err error) { if v == nil { subs = append(subs, k) } else { _, err = fmt.Printf("%s%s: %s\n", indent+indentMore, encode(k), encode(v)) } return err }) if err != nil { return err } for _, sub := range subs { subbucket := b.Bucket(sub) if err = dumpBucket(indent+indentMore, sub, subbucket); err != nil { return err } } return err } return tx.ForEach(func(name []byte, b *bolt.Bucket) error { return dumpBucket("", name, b) }) }) } func dumpboltInit() { rootCmd.AddCommand(dumpBoltCommand) } ================================================ FILE: cmd/buildah/from.go ================================================ package main import ( "errors" "fmt" "io" "os" "strings" "time" "github.com/containers/buildah" "github.com/containers/buildah/pkg/cli" "github.com/containers/buildah/pkg/parse" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "go.podman.io/common/pkg/auth" ) type fromReply struct { authfile string certDir string cidfile string creds string format string name string pull string pullAlways bool pullNever bool quiet bool signaturePolicy string tlsVerify bool *cli.FromAndBudResults *cli.UserNSResults *cli.NameSpaceResults } var suffix string func fromInit() { var ( fromDescription = "\n Creates a new working container, either from scratch or using a specified\n image as a starting point." opts fromReply ) fromAndBudResults := cli.FromAndBudResults{} userNSResults := cli.UserNSResults{} namespaceResults := cli.NameSpaceResults{} fromCommand := &cobra.Command{ Use: "from", Short: "Create a working container based on an image", Long: fromDescription, RunE: func(cmd *cobra.Command, args []string) error { // Add in the results from the common cli commands opts.FromAndBudResults = &fromAndBudResults opts.UserNSResults = &userNSResults opts.NameSpaceResults = &namespaceResults return fromCmd(cmd, args, opts) }, Example: `buildah from --pull imagename buildah from docker-daemon:imagename:imagetag buildah from --name "myimagename" myregistry/myrepository/imagename:imagetag`, } fromCommand.SetUsageTemplate(UsageTemplate()) flags := fromCommand.Flags() flags.SetInterspersed(false) flags.StringVar(&opts.authfile, "authfile", auth.GetDefaultAuthFile(), "path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") flags.StringVar(&opts.certDir, "cert-dir", "", "use certificates at the specified path to access the registry") flags.StringVar(&opts.cidfile, "cidfile", "", "write the container ID to the file") flags.StringVar(&opts.creds, "creds", "", "use `[username[:password]]` for accessing the registry") flags.StringVarP(&opts.format, "format", "f", defaultFormat(), "`format` of the image manifest and metadata") flags.StringVar(&opts.name, "name", "", "`name` for the working container") flags.StringVar(&opts.pull, "pull", "missing", `pull images from the registry values: always: pull images even if the named images are present in store, missing: pull images if the named images are not present in store, never: only use images present in store if available, newer: only pull images when newer images exist on the registry than those in the store.`) flags.Lookup("pull").NoOptDefVal = "always" // treat a --pull with no argument like --pull=always flags.BoolVar(&opts.pullAlways, "pull-always", false, "pull the image even if the named image is present in store") if err := flags.MarkHidden("pull-always"); err != nil { panic(fmt.Sprintf("error marking the pull-always flag as hidden: %v", err)) } flags.BoolVar(&opts.pullNever, "pull-never", false, "do not pull the image, use the image present in store if available") if err := flags.MarkHidden("pull-never"); err != nil { panic(fmt.Sprintf("error marking the pull-never flag as hidden: %v", err)) } flags.BoolVarP(&opts.quiet, "quiet", "q", false, "don't output progress information when pulling images") flags.StringVar(&opts.signaturePolicy, "signature-policy", "", "`pathname` of signature policy file (not usually used)") flags.StringVar(&suffix, "suffix", "", "suffix to add to intermediate containers") if err := flags.MarkHidden("suffix"); err != nil { panic(fmt.Sprintf("error marking the suffix flag as hidden: %v", err)) } if err := flags.MarkHidden("signature-policy"); err != nil { panic(fmt.Sprintf("error marking signature-policy as hidden: %v", err)) } flags.BoolVar(&opts.tlsVerify, "tls-verify", true, "require HTTPS and verify certificates when accessing the registry. TLS verification cannot be used when talking to an insecure registry.") // Add in the common flags fromAndBudFlags, err := cli.GetFromAndBudFlags(&fromAndBudResults, &userNSResults, &namespaceResults) if err != nil { logrus.Errorf("failed to setup From and Bud flags: %v", err) os.Exit(1) } flags.AddFlagSet(&fromAndBudFlags) flags.SetNormalizeFunc(cli.AliasFlags) rootCmd.AddCommand(fromCommand) } func onBuild(builder *buildah.Builder, quiet bool) error { ctr := 0 for _, onBuildSpec := range builder.OnBuild() { ctr = ctr + 1 commands := strings.Split(onBuildSpec, " ") command := strings.ToUpper(commands[0]) args := commands[1:] if !quiet { fmt.Fprintf(os.Stderr, "STEP %d: %s\n", ctr, onBuildSpec) } switch command { case "ADD": case "COPY": dest := "" size := len(args) if size > 1 { dest = args[size-1] args = args[:size-1] } if err := builder.Add(dest, command == "ADD", buildah.AddAndCopyOptions{}, args...); err != nil { return err } case "ANNOTATION": annotation := strings.SplitN(args[0], "=", 2) if len(annotation) > 1 { builder.SetAnnotation(annotation[0], annotation[1]) } else { builder.UnsetAnnotation(annotation[0]) } case "CMD": builder.SetCmd(args) case "ENV": env := strings.SplitN(args[0], "=", 2) if len(env) > 1 { builder.SetEnv(env[0], env[1]) } else { builder.UnsetEnv(env[0]) } case "ENTRYPOINT": builder.SetEntrypoint(args) case "EXPOSE": builder.SetPort(strings.Join(args, " ")) case "HOSTNAME": builder.SetHostname(strings.Join(args, " ")) case "LABEL": label := strings.SplitN(args[0], "=", 2) if len(label) > 1 { builder.SetLabel(label[0], label[1]) } else { builder.UnsetLabel(label[0]) } case "MAINTAINER": builder.SetMaintainer(strings.Join(args, " ")) case "ONBUILD": builder.SetOnBuild(strings.Join(args, " ")) case "RUN": var stdout io.Writer if quiet { stdout = io.Discard } if err := builder.Run(args, buildah.RunOptions{Stdout: stdout}); err != nil { return err } case "SHELL": builder.SetShell(args) case "STOPSIGNAL": builder.SetStopSignal(strings.Join(args, " ")) case "USER": builder.SetUser(strings.Join(args, " ")) case "VOLUME": builder.AddVolume(strings.Join(args, " ")) case "WORKINGDIR": builder.SetWorkDir(strings.Join(args, " ")) default: logrus.Errorf("unknown OnBuild command %q; ignored", onBuildSpec) } } builder.ClearOnBuild() return nil } func fromCmd(c *cobra.Command, args []string, iopts fromReply) error { if len(args) == 0 { return errors.New("an image name (or \"scratch\") must be specified") } if err := cli.VerifyFlagsArgsOrder(args); err != nil { return err } if len(args) > 1 { return errors.New("too many arguments specified") } if err := auth.CheckAuthFile(iopts.authfile); err != nil { return err } systemContext, err := parse.SystemContextFromOptions(c) if err != nil { return fmt.Errorf("building system context: %w", err) } platforms, err := parse.PlatformsFromOptions(c) if err != nil { return err } if len(platforms) > 1 { logrus.Warnf("ignoring platforms other than %+v: %+v", platforms[0], platforms[1:]) } pullPolicy, err := parse.PullPolicyFromOptions(c) if err != nil { return err } signaturePolicy := iopts.signaturePolicy store, err := getStore(c) if err != nil { return err } commonOpts, err := parse.CommonBuildOptions(c) if err != nil { return err } isolation, err := parse.IsolationOption(iopts.Isolation) if err != nil { return err } namespaceOptions, networkPolicy, err := parse.NamespaceOptions(c) if err != nil { return fmt.Errorf("parsing namespace-related options: %w", err) } usernsOption, idmappingOptions, err := parse.IDMappingOptions(c, isolation) if err != nil { return fmt.Errorf("parsing ID mapping options: %w", err) } namespaceOptions.AddOrReplace(usernsOption...) format, err := cli.GetFormat(iopts.format) if err != nil { return err } capabilities, err := defaultContainerConfig.Capabilities("", iopts.CapAdd, iopts.CapDrop) if err != nil { return err } commonOpts.Ulimit = append(defaultContainerConfig.Containers.DefaultUlimits.Get(), commonOpts.Ulimit...) decConfig, err := cli.DecryptConfig(iopts.DecryptionKeys) if err != nil { return fmt.Errorf("unable to obtain decrypt config: %w", err) } options := buildah.BuilderOptions{ FromImage: args[0], Container: iopts.name, ContainerSuffix: suffix, GroupAdd: iopts.GroupAdd, PullPolicy: pullPolicy, SignaturePolicyPath: signaturePolicy, SystemContext: systemContext, DefaultMountsFilePath: globalFlagResults.DefaultMountsFile, Isolation: isolation, NamespaceOptions: namespaceOptions, ConfigureNetwork: networkPolicy, IDMappingOptions: idmappingOptions, Capabilities: capabilities, CommonBuildOpts: commonOpts, Format: format, BlobDirectory: iopts.BlobCache, DeviceSpecs: iopts.Devices, MaxPullRetries: iopts.Retry, OciDecryptConfig: decConfig, CDIConfigDir: iopts.CDIConfigDir, } if iopts.RetryDelay != "" { options.PullRetryDelay, err = time.ParseDuration(iopts.RetryDelay) if err != nil { return fmt.Errorf("unable to parse value provided %q as --retry-delay: %w", iopts.RetryDelay, err) } } if !iopts.quiet { options.ReportWriter = os.Stderr } builder, err := buildah.NewBuilder(getContext(), store, options) if err != nil { return err } if err := onBuild(builder, iopts.quiet); err != nil { return err } if iopts.cidfile != "" { filePath := iopts.cidfile if err := os.WriteFile(filePath, []byte(builder.ContainerID), 0o644); err != nil { return fmt.Errorf("failed to write container ID file %q: %w", filePath, err) } } fmt.Printf("%s\n", builder.Container) return builder.Save() } ================================================ FILE: cmd/buildah/images.go ================================================ package main import ( "context" "encoding/json" "errors" "fmt" "sort" "strings" "time" buildahcli "github.com/containers/buildah/pkg/cli" "github.com/containers/buildah/pkg/parse" "github.com/docker/go-units" "github.com/spf13/cobra" "go.podman.io/common/libimage" "go.podman.io/common/pkg/formats" ) const none = "" type jsonImage struct { ID string `json:"id"` Names []string `json:"names"` Digest string `json:"digest"` CreatedAt string `json:"createdat"` Size string `json:"size"` Created int64 `json:"created"` CreatedAtRaw time.Time `json:"createdatraw"` ReadOnly bool `json:"readonly"` History []string `json:"history"` } type imageOutputParams struct { Tag string ID string Name string Digest string Created int64 CreatedAt string Size string CreatedAtRaw time.Time ReadOnly bool History string } type imageOptions struct { all bool digests bool format string json bool noHeading bool truncate bool quiet bool readOnly bool history bool } type imageResults struct { imageOptions filter []string } var imagesHeader = map[string]string{ "Name": "REPOSITORY", "Tag": "TAG", "ID": "IMAGE ID", "CreatedAt": "CREATED", "Size": "SIZE", "ReadOnly": "R/O", "History": "HISTORY", } func imagesInit() { var ( opts imageResults imagesDescription = "\n Lists locally stored images." ) imagesCommand := &cobra.Command{ Use: "images", Short: "List images in local storage", Long: imagesDescription, RunE: func(cmd *cobra.Command, args []string) error { return imagesCmd(cmd, args, &opts) }, Example: `buildah images --all buildah images [imageName] buildah images --format '{{.ID}} {{.Name}} {{.Size}} {{.CreatedAtRaw}}'`, } imagesCommand.SetUsageTemplate(UsageTemplate()) flags := imagesCommand.Flags() flags.SetInterspersed(false) flags.BoolVarP(&opts.all, "all", "a", false, "show all images, including intermediate images from a build") flags.BoolVar(&opts.digests, "digests", false, "show digests") flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "filter output based on conditions provided") flags.StringVar(&opts.format, "format", "", "pretty-print images using a Go template") flags.BoolVar(&opts.json, "json", false, "output in JSON format") flags.BoolVarP(&opts.noHeading, "noheading", "n", false, "do not print column headings") // TODO needs alias here -- to `notruncate` flags.BoolVar(&opts.truncate, "no-trunc", false, "do not truncate output") flags.BoolVarP(&opts.quiet, "quiet", "q", false, "display only image IDs") flags.BoolVarP(&opts.history, "history", "", false, "display the image name history") rootCmd.AddCommand(imagesCommand) } func imagesCmd(c *cobra.Command, args []string, iopts *imageResults) error { if len(args) > 0 { if iopts.all { return errors.New("when using the --all switch, you may not pass any images names or IDs") } if err := buildahcli.VerifyFlagsArgsOrder(args); err != nil { return err } if len(args) > 1 { return errors.New("'buildah images' requires at most 1 argument") } } store, err := getStore(c) if err != nil { return err } systemContext, err := parse.SystemContextFromOptions(c) if err != nil { return fmt.Errorf("building system context: %w", err) } runtime, err := libimage.RuntimeFromStore(store, &libimage.RuntimeOptions{SystemContext: systemContext}) if err != nil { return err } ctx := context.Background() options := &libimage.ListImagesOptions{} if len(iopts.filter) > 0 { options.Filters = iopts.filter } if !iopts.all { options.Filters = append(options.Filters, "intermediate=false") } images, err := runtime.ListImages(ctx, options) if err != nil { return err } if len(args) > 0 { imagesMatchName, err := runtime.ListImagesByNames(args) if err != nil { return err } imagesIDs := map[string]struct{}{} for _, image := range imagesMatchName { imagesIDs[image.ID()] = struct{}{} } var imagesMatchNameAndFilter []*libimage.Image for _, image := range images { if _, ok := imagesIDs[image.ID()]; ok { imagesMatchNameAndFilter = append(imagesMatchNameAndFilter, image) } } images = imagesMatchNameAndFilter } if iopts.quiet && iopts.format != "" { return errors.New("quiet and format are mutually exclusive") } opts := imageOptions{ all: iopts.all, digests: iopts.digests, format: iopts.format, json: iopts.json, noHeading: iopts.noHeading, truncate: !iopts.truncate, quiet: iopts.quiet, history: iopts.history, } if opts.json { return formatImagesJSON(images, opts) } return formatImages(images, opts) } func outputHeader(opts imageOptions) string { if opts.format != "" { return strings.ReplaceAll(opts.format, `\t`, "\t") } if opts.quiet { return formats.IDString } format := "table {{.Name}}\t{{.Tag}}\t" if opts.noHeading { format = "{{.Name}}\t{{.Tag}}\t" } if opts.digests { format += "{{.Digest}}\t" } format += "{{.ID}}\t{{.CreatedAt}}\t{{.Size}}" if opts.readOnly { format += "\t{{.ReadOnly}}" } if opts.history { format += "\t{{.History}}" } return format } func formatImagesJSON(images []*libimage.Image, opts imageOptions) error { jsonImages := []jsonImage{} for _, image := range images { // Copy the base data over to the output param. size, err := image.Size() if err != nil { return err } created := image.Created() jsonImages = append(jsonImages, jsonImage{ CreatedAtRaw: created, Created: created.Unix(), CreatedAt: units.HumanDuration(time.Since(created)) + " ago", Digest: image.Digest().String(), ID: truncateID(image.ID(), opts.truncate), Names: image.Names(), ReadOnly: image.IsReadOnly(), Size: formattedSize(size), }) } data, err := json.MarshalIndent(jsonImages, "", " ") if err != nil { return err } fmt.Printf("%s\n", data) return nil } type imagesSorted []imageOutputParams func (a imagesSorted) Less(i, j int) bool { return a[i].CreatedAtRaw.After(a[j].CreatedAtRaw) } func (a imagesSorted) Len() int { return len(a) } func (a imagesSorted) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func formatImages(images []*libimage.Image, opts imageOptions) error { var outputData imagesSorted for _, image := range images { var outputParam imageOutputParams size, err := image.Size() if err != nil { return err } created := image.Created() outputParam.CreatedAtRaw = created outputParam.Created = created.Unix() outputParam.CreatedAt = units.HumanDuration(time.Since(created)) + " ago" outputParam.Digest = image.Digest().String() outputParam.ID = truncateID(image.ID(), opts.truncate) outputParam.Size = formattedSize(size) outputParam.ReadOnly = image.IsReadOnly() repoTags, err := image.NamedRepoTags() if err != nil { return err } nameTagPairs, err := libimage.ToNameTagPairs(repoTags) if err != nil { return err } for _, pair := range nameTagPairs { newParam := outputParam newParam.Name = pair.Name newParam.Tag = pair.Tag newParam.History = formatHistory(image.NamesHistory(), pair.Name, pair.Tag) outputData = append(outputData, newParam) // `images -q` should a given ID only once. if opts.quiet { break } } } sort.Sort(outputData) out := formats.StdoutTemplateArray{Output: imagesToGeneric(outputData), Template: outputHeader(opts), Fields: imagesHeader} return formats.Writer(out).Out() } func formatHistory(history []string, name, tag string) string { if len(history) == 0 { return none } // Skip the first history entry if already existing as name if fmt.Sprintf("%s:%s", name, tag) == history[0] { if len(history) == 1 { return none } return strings.Join(history[1:], ", ") } return strings.Join(history, ", ") } func truncateID(id string, truncate bool) string { if !truncate { return "sha256:" + id } idTruncLength := 12 if len(id) > idTruncLength { return id[:idTruncLength] } return id } func imagesToGeneric(templParams []imageOutputParams) (genericParams []any) { if len(templParams) > 0 { for _, v := range templParams { genericParams = append(genericParams, any(v)) } } return genericParams } func formattedSize(size int64) string { suffixes := [5]string{"B", "KB", "MB", "GB", "TB"} count := 0 formattedSize := float64(size) for formattedSize >= 1000 && count < 4 { formattedSize /= 1000 count++ } return fmt.Sprintf("%.3g %s", formattedSize, suffixes[count]) } func matchesID(imageID, argID string) bool { return strings.HasPrefix(imageID, argID) } func matchesReference(name, argName string) bool { if argName == "" { return true } splitName := strings.Split(name, ":") // If the arg contains a tag, we handle it differently than if it does not if strings.Contains(argName, ":") { splitArg := strings.Split(argName, ":") return strings.HasSuffix(splitName[0], splitArg[0]) && (splitName[1] == splitArg[1]) } return strings.HasSuffix(splitName[0], argName) } ================================================ FILE: cmd/buildah/images_test.go ================================================ package main import ( "testing" ) func TestSizeFormatting(t *testing.T) { t.Parallel() size := formattedSize(0) if size != "0 B" { t.Errorf("Error formatting size: expected '%s' got '%s'", "0 B", size) } size = formattedSize(1000) if size != "1 KB" { t.Errorf("Error formatting size: expected '%s' got '%s'", "1 KB", size) } size = formattedSize(1000 * 1000 * 1000 * 1000) if size != "1 TB" { t.Errorf("Error formatting size: expected '%s' got '%s'", "1 TB", size) } } func TestMatchWithTag(t *testing.T) { t.Parallel() isMatch := matchesReference("gcr.io/pause:latest", "pause:latest") if !isMatch { t.Error("expected match, got not match") } isMatch = matchesReference("gcr.io/pause:latest", "kubernetes/pause:latest") if isMatch { t.Error("expected not match, got match") } } func TestNoMatchesReferenceWithTag(t *testing.T) { t.Parallel() isMatch := matchesReference("gcr.io/pause:latest", "redis:latest") if isMatch { t.Error("expected no match, got match") } isMatch = matchesReference("gcr.io/pause:latest", "kubernetes/redis:latest") if isMatch { t.Error("expected no match, got match") } } func TestMatchesReferenceWithoutTag(t *testing.T) { t.Parallel() isMatch := matchesReference("gcr.io/pause:latest", "pause") if !isMatch { t.Error("expected match, got not match") } isMatch = matchesReference("gcr.io/pause:latest", "kubernetes/pause") if isMatch { t.Error("expected not match, got match") } } func TestNoMatchesReferenceWithoutTag(t *testing.T) { t.Parallel() isMatch := matchesReference("gcr.io/pause:latest", "redis") if isMatch { t.Error("expected no match, got match") } isMatch = matchesReference("gcr.io/pause:latest", "kubernetes/redis") if isMatch { t.Error("expected no match, got match") } } ================================================ FILE: cmd/buildah/info.go ================================================ package main import ( "encoding/json" "fmt" "os" "regexp" "runtime" "github.com/containers/buildah" "github.com/containers/buildah/define" "github.com/spf13/cobra" "go.podman.io/common/pkg/formats" "golang.org/x/term" ) type infoResults struct { debug bool format string } func infoInit() { var ( infoDescription = "\n Display information about the host and current storage statistics which are useful when reporting issues." opts infoResults ) infoCommand := &cobra.Command{ Use: "info", Short: "Display Buildah system information", Long: infoDescription, RunE: func(cmd *cobra.Command, _ []string) error { return infoCmd(cmd, opts) }, Args: cobra.NoArgs, Example: `buildah info`, } infoCommand.SetUsageTemplate(UsageTemplate()) flags := infoCommand.Flags() flags.BoolVarP(&opts.debug, "debug", "d", false, "display additional debug information") flags.StringVar(&opts.format, "format", "", "use `format` as a Go template to format the output") rootCmd.AddCommand(infoCommand) } func infoCmd(c *cobra.Command, iopts infoResults) error { info := map[string]any{} store, err := getStore(c) if err != nil { return err } infoArr, err := buildah.Info(store) if err != nil { return fmt.Errorf("getting info: %w", err) } if iopts.debug { debugInfo := debugInfo() infoArr = append(infoArr, buildah.InfoData{Type: "debug", Data: debugInfo}) } for _, currInfo := range infoArr { info[currInfo.Type] = currInfo.Data } if iopts.format != "" { format := iopts.format if matched, err := regexp.MatchString("{{.*}}", format); err != nil { return fmt.Errorf("validating format provided: %s: %w", format, err) } else if !matched { return fmt.Errorf("invalid format provided: %s", format) } t, err := formats.NewParse("info", format) if err != nil { return fmt.Errorf("template parsing error: %w", err) } if err = t.Execute(os.Stdout, info); err != nil { return err } if term.IsTerminal(int(os.Stdout.Fd())) { fmt.Println() } return nil } enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") if term.IsTerminal(int(os.Stdout.Fd())) { enc.SetEscapeHTML(false) } return enc.Encode(info) } // top-level "debug" info func debugInfo() map[string]any { info := map[string]any{} info["compiler"] = runtime.Compiler info["go version"] = runtime.Version() info["buildah version"] = define.Version info["git commit"] = GitCommit return info } ================================================ FILE: cmd/buildah/inspect.go ================================================ package main import ( "encoding/json" "errors" "fmt" "os" "regexp" "github.com/containers/buildah" buildahcli "github.com/containers/buildah/pkg/cli" "github.com/containers/buildah/pkg/parse" "github.com/spf13/cobra" "go.podman.io/common/pkg/formats" "golang.org/x/term" ) const ( inspectTypeContainer = "container" inspectTypeImage = "image" inspectTypeManifest = "manifest" ) type inspectResults struct { format string inspectType string } func inspectInit() { var ( opts inspectResults inspectDescription = "\n Inspects a build container's or built image's configuration." ) inspectCommand := &cobra.Command{ Use: "inspect", Short: "Inspect the configuration of a container or image", Long: inspectDescription, RunE: func(cmd *cobra.Command, args []string) error { return inspectCmd(cmd, args, opts) }, Example: `buildah inspect containerID buildah inspect --type image imageID buildah inspect --format '{{.OCIv1.Config.Env}}' alpine`, } inspectCommand.SetUsageTemplate(UsageTemplate()) flags := inspectCommand.Flags() flags.SetInterspersed(false) flags.StringVarP(&opts.format, "format", "f", "", "use `format` as a Go template to format the output") flags.StringVarP(&opts.inspectType, "type", "t", inspectTypeContainer, "look at the item of the specified `type` (container or image) and name") rootCmd.AddCommand(inspectCommand) } func inspectCmd(c *cobra.Command, args []string, iopts inspectResults) error { var builder *buildah.Builder if len(args) == 0 { return errors.New("container or image name must be specified") } if err := buildahcli.VerifyFlagsArgsOrder(args); err != nil { return err } if len(args) > 1 { return errors.New("too many arguments specified") } systemContext, err := parse.SystemContextFromOptions(c) if err != nil { return fmt.Errorf("building system context: %w", err) } name := args[0] store, err := getStore(c) if err != nil { return err } ctx := getContext() switch iopts.inspectType { case inspectTypeContainer: builder, err = openBuilder(ctx, store, name) if err != nil { if c.Flag("type").Changed { return fmt.Errorf("reading build container: %w", err) } builder, err = openImage(ctx, systemContext, store, name) if err != nil { if manifestErr := manifestInspect(ctx, store, systemContext, name); manifestErr == nil { return nil } return err } } case inspectTypeImage: builder, err = openImage(ctx, systemContext, store, name) if err != nil { return err } case inspectTypeManifest: return manifestInspect(ctx, store, systemContext, name) default: return fmt.Errorf("the only recognized types are %q and %q", inspectTypeContainer, inspectTypeImage) } out := buildah.GetBuildInfo(builder) if iopts.format != "" { format := iopts.format if matched, err := regexp.MatchString("{{.*}}", format); err != nil { return fmt.Errorf("validating format provided: %s: %w", format, err) } else if !matched { return fmt.Errorf("invalid format provided: %s", format) } t, err := formats.NewParse("inspect", format) if err != nil { return fmt.Errorf("template parsing error: %w", err) } if err = t.Execute(os.Stdout, out); err != nil { return err } if term.IsTerminal(int(os.Stdout.Fd())) { fmt.Println() } return nil } enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") if term.IsTerminal(int(os.Stdout.Fd())) { enc.SetEscapeHTML(false) } return enc.Encode(out) } ================================================ FILE: cmd/buildah/login.go ================================================ package main import ( "errors" "fmt" "os" "github.com/containers/buildah/pkg/parse" "github.com/spf13/cobra" "go.podman.io/common/pkg/auth" ) type loginReply struct { loginOpts auth.LoginOptions getLogin bool tlsVerify bool } func loginInit() { var ( opts = loginReply{ loginOpts: auth.LoginOptions{ Stdin: os.Stdin, Stdout: os.Stdout, AcceptRepositories: true, }, } loginDescription = "Login to a container registry on a specified server." ) loginCommand := &cobra.Command{ Use: "login", Short: "Login to a container registry", Long: loginDescription, RunE: func(cmd *cobra.Command, args []string) error { return loginCmd(cmd, args, &opts) }, Example: `buildah login quay.io`, } loginCommand.SetUsageTemplate(UsageTemplate()) flags := loginCommand.Flags() flags.BoolVar(&opts.tlsVerify, "tls-verify", true, "require HTTPS and verify certificates when accessing the registry. TLS verification cannot be used when talking to an insecure registry.") flags.BoolVar(&opts.getLogin, "get-login", true, "return the current login user for the registry") flags.AddFlagSet(auth.GetLoginFlags(&opts.loginOpts)) opts.loginOpts.Stdin = os.Stdin opts.loginOpts.Stdout = os.Stdout rootCmd.AddCommand(loginCommand) } func loginCmd(c *cobra.Command, args []string, iopts *loginReply) error { if len(args) > 1 { return errors.New("too many arguments, login takes only 1 argument") } if len(args) == 0 { return errors.New("please specify a registry to login to") } if err := setXDGRuntimeDir(); err != nil { return err } systemContext, err := parse.SystemContextFromOptions(c) if err != nil { return fmt.Errorf("building system context: %w", err) } // parse.SystemContextFromOptions may point this field to an auth.json or to a .docker/config.json; // that’s fair enough for reads, but incorrect for writes (the two files have incompatible formats), // and it interferes with the auth.Login’s own argument parsing. systemContext.AuthFilePath = "" ctx := getContext() iopts.loginOpts.GetLoginSet = c.Flag("get-login").Changed return auth.Login(ctx, systemContext, &iopts.loginOpts, args) } ================================================ FILE: cmd/buildah/logout.go ================================================ package main import ( "errors" "fmt" "os" "github.com/containers/buildah/pkg/parse" "github.com/spf13/cobra" "go.podman.io/common/pkg/auth" ) func logoutInit() { var ( opts = auth.LogoutOptions{ Stdout: os.Stdout, AcceptRepositories: true, } logoutDescription = "Remove the cached username and password for the registry." ) logoutCommand := &cobra.Command{ Use: "logout", Short: "Logout of a container registry", Long: logoutDescription, RunE: func(cmd *cobra.Command, args []string) error { return logoutCmd(cmd, args, &opts) }, Example: `buildah logout quay.io`, } logoutCommand.SetUsageTemplate(UsageTemplate()) flags := auth.GetLogoutFlags(&opts) flags.SetInterspersed(false) logoutCommand.Flags().AddFlagSet(flags) rootCmd.AddCommand(logoutCommand) } func logoutCmd(c *cobra.Command, args []string, iopts *auth.LogoutOptions) error { if len(args) > 1 { return errors.New("too many arguments, logout takes at most 1 argument") } if len(args) == 0 && !iopts.All { return errors.New("registry must be given") } if err := setXDGRuntimeDir(); err != nil { return err } systemContext, err := parse.SystemContextFromOptions(c) if err != nil { return fmt.Errorf("building system context: %w", err) } // parse.SystemContextFromOptions may point this field to an auth.json or to a .docker/config.json; // that’s fair enough for reads, but incorrect for writes (the two files have incompatible formats), // and it interferes with the auth.Logout’s own argument parsing. systemContext.AuthFilePath = "" return auth.Logout(systemContext, iopts, args) } ================================================ FILE: cmd/buildah/main.go ================================================ package main import ( "errors" "fmt" "os" "os/exec" "runtime" "runtime/pprof" "strings" "syscall" "github.com/containers/buildah" "github.com/containers/buildah/define" "github.com/containers/buildah/pkg/cli" "github.com/containers/buildah/pkg/parse" ispecs "github.com/opencontainers/image-spec/specs-go" rspecs "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "go.podman.io/common/pkg/config" "go.podman.io/storage" "go.podman.io/storage/pkg/unshare" ) type globalFlags struct { Debug bool LogLevel string Root string RunRoot string ImageStore string TransientStore bool StorageDriver string RegistriesConf string RegistriesConfDir string DefaultMountsFile string StorageOpts []string UserNSUID []string UserNSGID []string CPUProfile string cpuProfileFile *os.File MemoryProfile string UserShortNameAliasConfPath string CgroupManager string } var rootCmd = &cobra.Command{ Use: "buildah", Long: "A tool that facilitates building OCI images", RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Help() }, PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { return before(cmd) }, PersistentPostRunE: func(cmd *cobra.Command, _ []string) error { return after(cmd) }, SilenceUsage: true, SilenceErrors: true, } var ( globalFlagResults globalFlags exitCode int defaultContainerConfig *config.Config ) func mainInit() { var defaultStoreDriverOptions []string storageOptions, err := storage.DefaultStoreOptions() if err != nil { logrus.Error(err.Error()) os.Exit(1) } if len(storageOptions.GraphDriverOptions) > 0 { optionSlice := storageOptions.GraphDriverOptions[:] defaultStoreDriverOptions = optionSlice } defaultContainerConfig, err = config.Default() if err != nil { logrus.Error(err.Error()) os.Exit(1) } defaultContainerConfig.CheckCgroupsAndAdjustConfig() cobra.OnInitialize(initConfig) // Hide the implicit `completion` command in cobra. rootCmd.CompletionOptions.HiddenDefaultCmd = true // rootCmd.TraverseChildren = true rootCmd.Version = fmt.Sprintf("%s (image-spec %s, runtime-spec %s)", define.Version, ispecs.Version, rspecs.Version) rootCmd.PersistentFlags().BoolVar(&globalFlagResults.Debug, "debug", false, "print debugging information") // TODO Need to allow for environment variable rootCmd.PersistentFlags().StringVar(&globalFlagResults.RegistriesConf, "registries-conf", "", "path to registries.conf file (not usually used)") rootCmd.PersistentFlags().StringVar(&globalFlagResults.RegistriesConfDir, "registries-conf-dir", "", "path to registries.conf.d directory (not usually used)") rootCmd.PersistentFlags().StringVar(&globalFlagResults.UserShortNameAliasConfPath, "short-name-alias-conf", "", "path to short name alias cache file (not usually used)") rootCmd.PersistentFlags().StringVar(&globalFlagResults.Root, "root", storageOptions.GraphRoot, "storage root dir") rootCmd.PersistentFlags().StringVar(&globalFlagResults.RunRoot, "runroot", storageOptions.RunRoot, "storage state dir") rootCmd.PersistentFlags().StringVar(&globalFlagResults.CgroupManager, "cgroup-manager", defaultContainerConfig.Engine.CgroupManager, "cgroup manager") rootCmd.PersistentFlags().StringVar(&globalFlagResults.StorageDriver, "storage-driver", storageOptions.GraphDriverName, "storage-driver") rootCmd.PersistentFlags().StringVar(&globalFlagResults.ImageStore, "imagestore", storageOptions.ImageStore, "storage imagestore") rootCmd.PersistentFlags().BoolVar(&globalFlagResults.TransientStore, "transient-store", storageOptions.TransientStore, "store some information in transient storage") rootCmd.PersistentFlags().StringSliceVar(&globalFlagResults.StorageOpts, "storage-opt", defaultStoreDriverOptions, "storage driver option") rootCmd.PersistentFlags().StringSliceVar(&globalFlagResults.UserNSUID, "userns-uid-map", []string{}, "default `ctrID:hostID:length` UID mapping to use") rootCmd.PersistentFlags().StringSliceVar(&globalFlagResults.UserNSGID, "userns-gid-map", []string{}, "default `ctrID:hostID:length` GID mapping to use") rootCmd.PersistentFlags().StringVar(&globalFlagResults.DefaultMountsFile, "default-mounts-file", "", "path to default mounts file") rootCmd.PersistentFlags().StringVar(&globalFlagResults.LogLevel, logLevel, "warn", `the log level to be used, one of "trace", "debug", "info", "warn", "error", "fatal", or "panic"`) rootCmd.PersistentFlags().StringVar(&globalFlagResults.CPUProfile, "cpu-profile", "", "`file` to write CPU profile") rootCmd.PersistentFlags().StringVar(&globalFlagResults.MemoryProfile, "memory-profile", "", "`file` to write memory profile") if err := rootCmd.PersistentFlags().MarkHidden("cpu-profile"); err != nil { logrus.Fatalf("unable to mark cpu-profile flag as hidden: %v", err) } if err := rootCmd.PersistentFlags().MarkHidden("debug"); err != nil { logrus.Fatalf("unable to mark debug flag as hidden: %v", err) } if err := rootCmd.PersistentFlags().MarkHidden("default-mounts-file"); err != nil { logrus.Fatalf("unable to mark default-mounts-file flag as hidden: %v", err) } if err := rootCmd.PersistentFlags().MarkHidden("memory-profile"); err != nil { logrus.Fatalf("unable to mark memory-profile flag as hidden: %v", err) } } func initConfig() { // TODO Cobra allows us to do extra stuff here at init // time if we ever want to take advantage. } const logLevel = "log-level" func before(cmd *cobra.Command) error { strLvl, err := cmd.Flags().GetString(logLevel) if err != nil { return err } logrusLvl, err := logrus.ParseLevel(strLvl) if err != nil { return fmt.Errorf("unable to parse log level: %w", err) } logrus.SetLevel(logrusLvl) if globalFlagResults.Debug { logrus.SetLevel(logrus.DebugLevel) } switch cmd.Use { case "", "help", "version", "mount": return nil } debugCapabilities() unshare.MaybeReexecUsingUserNamespace(false) if globalFlagResults.CPUProfile != "" { globalFlagResults.cpuProfileFile, err = os.Create(globalFlagResults.CPUProfile) if err != nil { logrus.Fatalf("could not create CPU profile %s: %v", globalFlagResults.CPUProfile, err) } if err = pprof.StartCPUProfile(globalFlagResults.cpuProfileFile); err != nil { logrus.Fatalf("error starting CPU profiling: %v", err) } } defaultContainerConfig, err := config.Default() if err != nil { return err } for _, env := range defaultContainerConfig.Engine.Env.Get() { splitEnv := strings.SplitN(env, "=", 2) if len(splitEnv) != 2 { return fmt.Errorf("invalid environment variable %q from containers.conf, valid configuration is KEY=value pair", env) } // skip if the env is already defined if _, ok := os.LookupEnv(splitEnv[0]); ok { logrus.Debugf("environment variable %q is already defined, skip the settings from containers.conf", splitEnv[0]) continue } if err := os.Setenv(splitEnv[0], splitEnv[1]); err != nil { return err } } return nil } func shutdownStore(cmd *cobra.Command) error { if needToShutdownStore { store, err := getStore(cmd) if err != nil { return err } logrus.Debugf("shutting down the store") needToShutdownStore = false if _, err = store.Shutdown(false); err != nil { if errors.Is(err, storage.ErrLayerUsedByContainer) { logrus.Infof("failed to shutdown storage: %q", err) } else { logrus.Warnf("failed to shutdown storage: %q", err) } } } return nil } func after(cmd *cobra.Command) error { if err := shutdownStore(cmd); err != nil { return err } if globalFlagResults.CPUProfile != "" { pprof.StopCPUProfile() globalFlagResults.cpuProfileFile.Close() } if globalFlagResults.MemoryProfile != "" { memoryProfileFile, err := os.Create(globalFlagResults.MemoryProfile) if err != nil { logrus.Fatalf("could not create memory profile %s: %v", globalFlagResults.MemoryProfile, err) } defer memoryProfileFile.Close() runtime.GC() if err := pprof.Lookup("heap").WriteTo(memoryProfileFile, 1); err != nil { logrus.Fatalf("could not write memory profile %s: %v", globalFlagResults.MemoryProfile, err) } } return nil } func main() { if buildah.InitReexec() { return } mainInit() addcopyInit() buildInit() commitInit() configInit() containersInit() dumpboltInit() fromInit() imagesInit() infoInit() inspectInit() loginInit() logoutInit() manifestInit() mkcwInit() mountInit() pruneInit() pullInit() pushInit() renameInit() rmiInit() rmInit() rpcInit() runInit() sourceInit() tagInit() umountInit() unshareInit() versionInit() // Hard code TMPDIR functions to use $TMPDIR or /var/tmp os.Setenv("TMPDIR", parse.GetTempDir()) if err := rootCmd.Execute(); err != nil { if logrus.IsLevelEnabled(logrus.TraceLevel) { fmt.Fprintf(os.Stderr, "Error: %+v\n", err) } else { fmt.Fprintf(os.Stderr, "Error: %v\n", err) } exitCode = cli.ExecErrorCodeGeneric var ee *exec.ExitError if errors.As(err, &ee) { if w, ok := ee.Sys().(syscall.WaitStatus); ok { exitCode = w.ExitStatus() } } if err := shutdownStore(rootCmd); err != nil { logrus.Warnf("failed to shutdown storage: %q", err) } } os.Exit(exitCode) } ================================================ FILE: cmd/buildah/manifest.go ================================================ package main import ( "bytes" "context" "encoding/json" "errors" "fmt" "os" "strings" "time" "github.com/containers/buildah/pkg/cli" "github.com/containers/buildah/pkg/parse" "github.com/containers/buildah/util" "github.com/hashicorp/go-multierror" digest "github.com/opencontainers/go-digest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "go.podman.io/common/libimage" "go.podman.io/common/libimage/manifests" "go.podman.io/common/pkg/auth" cp "go.podman.io/image/v5/copy" "go.podman.io/image/v5/image" "go.podman.io/image/v5/manifest" "go.podman.io/image/v5/pkg/compression" "go.podman.io/image/v5/transports" "go.podman.io/image/v5/transports/alltransports" "go.podman.io/image/v5/types" "go.podman.io/storage" ) type manifestCreateOpts struct { os, arch string all, tlsVerify, insecure, amend bool annotations []string } type manifestAddOpts struct { authfile, certDir, creds, os, arch, variant, osVersion string features, osFeatures, annotations, artifactAnnotations []string tlsVerify, insecure, all bool artifact, artifactExcludeTitles bool artifactType, artifactLayerType string artifactConfigType, artifactConfigFile string artifactSubject string } type manifestRemoveOpts struct{} type manifestAnnotateOpts struct { os, arch, variant, osVersion string features, osFeatures, annotations []string index bool subject string } type manifestInspectOpts struct { authfile string tlsVerify bool } func manifestInit() { var ( manifestDescription = "\n Creates, modifies, and pushes manifest lists and image indexes." manifestCreateDescription = "\n Creates manifest lists and image indexes." manifestAddDescription = "\n Adds an image or artifact to a manifest list or image index." manifestRemoveDescription = "\n Removes an image or artifact from a manifest list or image index." manifestAnnotateDescription = "\n Adds or updates information about an image index or an entry in a manifest list or image index." manifestInspectDescription = "\n Display the contents of a manifest list or image index." manifestPushDescription = "\n Pushes manifest lists and image indexes to registries." manifestRmDescription = "\n Remove one or more manifest lists from local storage." manifestExistsDescription = "\n Check if a manifest list exists in local storage." manifestCreateOpts manifestCreateOpts manifestAddOpts manifestAddOpts manifestRemoveOpts manifestRemoveOpts manifestAnnotateOpts manifestAnnotateOpts manifestInspectOpts manifestInspectOpts manifestPushOpts pushOptions ) manifestCommand := &cobra.Command{ Use: "manifest", Short: "Manipulate manifest lists and image indexes", Long: manifestDescription, Example: `buildah manifest create localhost/list buildah manifest add localhost/list localhost/image buildah manifest annotate --annotation A=B localhost/list localhost/image buildah manifest annotate --annotation A=B localhost/list sha256:entryManifestDigest buildah manifest inspect localhost/list buildah manifest push localhost/list transport:destination buildah manifest remove localhost/list sha256:entryManifestDigest buildah manifest rm localhost/list`, } manifestCommand.SetUsageTemplate(UsageTemplate()) rootCmd.AddCommand(manifestCommand) manifestCreateCommand := &cobra.Command{ Use: "create", Short: "Create manifest list or image index", Long: manifestCreateDescription, RunE: func(cmd *cobra.Command, args []string) error { return manifestCreateCmd(cmd, args, manifestCreateOpts) }, Example: `buildah manifest create mylist:v1.11 buildah manifest create mylist:v1.11 arch-specific-image-to-add buildah manifest create --all mylist:v1.11 transport:tagged-image-to-add`, Args: cobra.MinimumNArgs(1), } manifestCreateCommand.SetUsageTemplate(UsageTemplate()) flags := manifestCreateCommand.Flags() flags.BoolVar(&manifestCreateOpts.all, "all", false, "add all of the lists' images if the images to add are lists") flags.BoolVar(&manifestCreateOpts.amend, "amend", false, "modify an existing list if one with the desired name already exists") flags.StringSliceVar(&manifestCreateOpts.annotations, "annotation", nil, "set an `annotation` for the image index") flags.StringVar(&manifestCreateOpts.os, "os", "", "if any of the specified images is a list, choose the one for `os`") if err := flags.MarkHidden("os"); err != nil { panic(fmt.Sprintf("error marking --os as hidden: %v", err)) } flags.StringVar(&manifestCreateOpts.arch, "arch", "", "if any of the specified images is a list, choose the one for `arch`") if err := flags.MarkHidden("arch"); err != nil { panic(fmt.Sprintf("error marking --arch as hidden: %v", err)) } flags.BoolVar(&manifestCreateOpts.insecure, "insecure", false, "neither require HTTPS nor verify certificates when accessing the registry. TLS verification cannot be used when talking to an insecure registry.") if err := flags.MarkHidden("insecure"); err != nil { panic(fmt.Sprintf("error marking insecure as hidden: %v", err)) } flags.BoolVar(&manifestCreateOpts.tlsVerify, "tls-verify", true, "require HTTPS and verify certificates when accessing the registry. TLS verification cannot be used when talking to an insecure registry.") flags.SetNormalizeFunc(cli.AliasFlags) manifestCommand.AddCommand(manifestCreateCommand) manifestAddCommand := &cobra.Command{ Use: "add", Short: "Add an image or artifact to a manifest list or image index", Long: manifestAddDescription, RunE: func(cmd *cobra.Command, args []string) error { return manifestAddCmd(cmd, args, manifestAddOpts) }, Example: `buildah manifest add mylist:v1.11 image:v1.11-amd64 buildah manifest add mylist:v1.11 transport:imageName buildah manifest add --artifact --artifact-type text/plain mylist:v1.11 ./somefile.txt ./somefile.png`, Args: cobra.MinimumNArgs(2), } manifestAddCommand.SetUsageTemplate(UsageTemplate()) flags = manifestAddCommand.Flags() flags.BoolVar(&manifestAddOpts.artifact, "artifact", false, "treat the argument as a filename and add it as an artifact") flags.StringVar(&manifestAddOpts.artifactType, "artifact-type", "", "artifact manifest media type") flags.StringVar(&manifestAddOpts.artifactConfigType, "artifact-config-type", imgspecv1.DescriptorEmptyJSON.MediaType, "artifact config media type") flags.StringVar(&manifestAddOpts.artifactConfigFile, "artifact-config", "", "artifact config file") flags.StringVar(&manifestAddOpts.artifactLayerType, "artifact-layer-type", "", "artifact layer media type") flags.BoolVar(&manifestAddOpts.artifactExcludeTitles, "artifact-exclude-titles", false, fmt.Sprintf(`refrain from setting %q annotations on "layers"`, imgspecv1.AnnotationTitle)) flags.StringVar(&manifestAddOpts.artifactSubject, "artifact-subject", "", "artifact subject reference") flags.StringSliceVar(&manifestAddOpts.artifactAnnotations, "artifact-annotation", nil, "artifact annotation") flags.StringVar(&manifestAddOpts.authfile, "authfile", auth.GetDefaultAuthFile(), "path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") flags.StringVar(&manifestAddOpts.certDir, "cert-dir", "", "use certificates at the specified path to access the registry") flags.StringVar(&manifestAddOpts.creds, "creds", "", "use `[username[:password]]` for accessing the registry") flags.StringVar(&manifestAddOpts.os, "os", "", "override the `OS` of the specified image") flags.StringVar(&manifestAddOpts.arch, "arch", "", "override the `architecture` of the specified image") flags.StringVar(&manifestAddOpts.variant, "variant", "", "override the `variant` of the specified image") flags.StringVar(&manifestAddOpts.osVersion, "os-version", "", "override the OS `version` of the specified image") flags.StringSliceVar(&manifestAddOpts.features, "features", nil, "override the `features` of the specified image") flags.StringSliceVar(&manifestAddOpts.osFeatures, "os-features", nil, "override the OS `features` of the specified image") flags.StringSliceVar(&manifestAddOpts.annotations, "annotation", nil, "set an `annotation` for the specified image or artifact") flags.BoolVar(&manifestAddOpts.insecure, "insecure", false, "neither require HTTPS nor verify certificates when accessing the registry. TLS verification cannot be used when talking to an insecure registry.") if err := flags.MarkHidden("insecure"); err != nil { panic(fmt.Sprintf("error marking insecure as hidden: %v", err)) } flags.BoolVar(&manifestAddOpts.tlsVerify, "tls-verify", true, "require HTTPS and verify certificates when accessing the registry. TLS verification cannot be used when talking to an insecure registry.") flags.BoolVar(&manifestAddOpts.all, "all", false, "add all of the list's images if the image is a list") flags.SetNormalizeFunc(cli.AliasFlags) manifestCommand.AddCommand(manifestAddCommand) manifestRemoveCommand := &cobra.Command{ Use: "remove", Short: "Remove an entry from a manifest list or image index", Long: manifestRemoveDescription, RunE: func(cmd *cobra.Command, args []string) error { return manifestRemoveCmd(cmd, args, manifestRemoveOpts) }, Example: `buildah manifest remove mylist:v1.11 sha256:15352d97781ffdf357bf3459c037be3efac4133dc9070c2dce7eca7c05c3e736`, Args: cobra.MinimumNArgs(2), } manifestRemoveCommand.SetUsageTemplate(UsageTemplate()) manifestCommand.AddCommand(manifestRemoveCommand) manifestExistsCommand := &cobra.Command{ Use: "exists", Short: "Check if a manifest list exists in local storage", Long: manifestExistsDescription, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { return manifestExistsCmd(cmd, args) }, Example: "buildah manifest exists mylist", } manifestExistsCommand.SetUsageTemplate(UsageTemplate()) manifestCommand.AddCommand(manifestExistsCommand) manifestAnnotateCommand := &cobra.Command{ Use: "annotate", Short: "Add or update information about an entry in a manifest list or image index", Long: manifestAnnotateDescription, RunE: func(cmd *cobra.Command, args []string) error { return manifestAnnotateCmd(cmd, args, manifestAnnotateOpts) }, Example: `buildah manifest annotate --annotation left=right mylist:v1.11 image:v1.11-amd64`, Args: cobra.RangeArgs(1, 2), } flags = manifestAnnotateCommand.Flags() flags.StringVar(&manifestAnnotateOpts.os, "os", "", "override the `OS` of the specified image") flags.StringVar(&manifestAnnotateOpts.arch, "arch", "", "override the `Architecture` of the specified image") flags.BoolVar(&manifestAnnotateOpts.index, "index", false, "set annotations or artifact type for the index itself instead of for an entry in the index") flags.StringVar(&manifestAnnotateOpts.variant, "variant", "", "override the `Variant` of the specified image") flags.StringVar(&manifestAnnotateOpts.osVersion, "os-version", "", "override the os `version` of the specified image") flags.StringSliceVar(&manifestAnnotateOpts.features, "features", nil, "override the `features` of the specified image") flags.StringSliceVar(&manifestAnnotateOpts.osFeatures, "os-features", nil, "override the os `features` of the specified image") flags.StringSliceVar(&manifestAnnotateOpts.annotations, "annotation", nil, "set an `annotation` for the specified image") flags.StringVar(&manifestAnnotateOpts.subject, "subject", "", "set a subject for the image index") manifestAnnotateCommand.SetUsageTemplate(UsageTemplate()) manifestCommand.AddCommand(manifestAnnotateCommand) manifestInspectCommand := &cobra.Command{ Use: "inspect", Short: "Display the contents of a manifest list or image index", Long: manifestInspectDescription, RunE: func(cmd *cobra.Command, args []string) error { return manifestInspectCmd(cmd, args, manifestInspectOpts) }, Example: `buildah manifest inspect mylist:v1.11`, Args: cobra.MinimumNArgs(1), } flags = manifestInspectCommand.Flags() flags.StringVar(&manifestInspectOpts.authfile, "authfile", auth.GetDefaultAuthFile(), "path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") flags.BoolVar(&manifestInspectOpts.tlsVerify, "tls-verify", true, "require HTTPS and verify certificates when accessing the registry. TLS verification cannot be used when talking to an insecure registry.") manifestInspectCommand.SetUsageTemplate(UsageTemplate()) manifestCommand.AddCommand(manifestInspectCommand) manifestPushCommand := &cobra.Command{ Use: "push", Short: "Push a manifest list or image index to a registry", Long: manifestPushDescription, RunE: func(cmd *cobra.Command, args []string) error { return manifestPushCmd(cmd, args, manifestPushOpts) }, Example: `buildah manifest push mylist:v1.11 transport:imageName`, Args: cobra.MinimumNArgs(1), } manifestPushCommand.SetUsageTemplate(UsageTemplate()) flags = manifestPushCommand.Flags() flags.BoolVar(&manifestPushOpts.rm, "rm", false, "remove the manifest list if push succeeds") flags.BoolVar(&manifestPushOpts.all, "all", true, "also push the images in the list") flags.StringVar(&manifestPushOpts.authfile, "authfile", auth.GetDefaultAuthFile(), "path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") flags.StringVar(&manifestPushOpts.certDir, "cert-dir", "", "use certificates at the specified path to access the registry") flags.StringVar(&manifestPushOpts.creds, "creds", "", "use `[username[:password]]` for accessing the registry") flags.StringVar(&manifestPushOpts.digestfile, "digestfile", "", "after copying the image, write the digest of the resulting digest to the file") flags.BoolVarP(&manifestPushOpts.forceCompressionFormat, "force-compression", "", false, "use the specified compression algorithm if the destination contains a differently-compressed variant already") flags.StringVar(&manifestPushOpts.compressionFormat, "compression-format", "", "compression format to use") flags.IntVar(&manifestPushOpts.compressionLevel, "compression-level", 0, "compression level to use") flags.StringVarP(&manifestPushOpts.format, "format", "f", "", "manifest type (oci or v2s2) to attempt to use when pushing the manifest list (default is manifest type of source)") flags.StringArrayVar(&manifestPushOpts.addCompression, "add-compression", defaultContainerConfig.Engine.AddCompression.Get(), "add instances with selected compression while pushing") flags.BoolVarP(&manifestPushOpts.removeSignatures, "remove-signatures", "", false, "don't copy signatures when pushing images") flags.StringVar(&manifestPushOpts.signBy, "sign-by", "", "sign the image using a GPG key with the specified `FINGERPRINT`") flags.StringVar(&manifestPushOpts.signaturePolicy, "signature-policy", "", "`pathname` of signature policy file (not usually used)") if err := flags.MarkHidden("signature-policy"); err != nil { panic(fmt.Sprintf("error marking signature-policy as hidden: %v", err)) } flags.BoolVar(&manifestPushOpts.insecure, "insecure", false, "neither require HTTPS nor verify certificates when accessing the registry. TLS verification cannot be used when talking to an insecure registry.") if err := flags.MarkHidden("insecure"); err != nil { panic(fmt.Sprintf("error marking insecure as hidden: %v", err)) } flags.BoolVar(&manifestPushOpts.tlsVerify, "tls-verify", true, "require HTTPS and verify certificates when accessing the registry. TLS verification cannot be used when talking to an insecure registry.") flags.BoolVarP(&manifestPushOpts.quiet, "quiet", "q", false, "don't output progress information when pushing lists") flags.IntVar(&manifestPushOpts.retry, "retry", int(defaultContainerConfig.Engine.Retry), "number of times to retry in case of failure when performing push") flags.StringVar(&manifestPushOpts.retryDelay, "retry-delay", defaultContainerConfig.Engine.RetryDelay, "delay between retries in case of push failures") flags.SetNormalizeFunc(cli.AliasFlags) manifestCommand.AddCommand(manifestPushCommand) manifestRmCommand := &cobra.Command{ Use: "rm", Short: "Remove manifest list or image index", Long: manifestRmDescription, RunE: func(cmd *cobra.Command, args []string) error { return manifestRmCmd(cmd, args) }, Example: `buildah manifest rm mylist:v1.11`, Args: cobra.MinimumNArgs(1), } manifestRmCommand.SetUsageTemplate(UsageTemplate()) manifestCommand.AddCommand(manifestRmCommand) } func manifestExistsCmd(c *cobra.Command, args []string) error { if len(args) == 0 { return errors.New("at least a name must be specified for the list") } name := args[0] store, err := getStore(c) if err != nil { return err } systemContext, err := parse.SystemContextFromOptions(c) if err != nil { return fmt.Errorf("building system context: %w", err) } runtime, err := libimage.RuntimeFromStore(store, &libimage.RuntimeOptions{SystemContext: systemContext}) if err != nil { return err } _, err = runtime.LookupManifestList(name) if err != nil { if errors.Is(err, storage.ErrImageUnknown) { exitCode = 1 } else { return err } } return nil } func manifestCreateCmd(c *cobra.Command, args []string, opts manifestCreateOpts) error { if len(args) == 0 { return errors.New("at least a name must be specified for the list") } listImageSpec := args[0] imageSpecs := args[1:] store, err := getStore(c) if err != nil { return err } systemContext, err := parse.SystemContextFromOptions(c) if err != nil { return fmt.Errorf("building system context: %w", err) } runtime, err := libimage.RuntimeFromStore(store, &libimage.RuntimeOptions{SystemContext: systemContext}) if err != nil { return err } list := manifests.Create() var manifestListID string names, err := util.ExpandNames([]string{listImageSpec}, systemContext, store) if err != nil { return fmt.Errorf("encountered while expanding image name %q: %w", listImageSpec, err) } if manifestListID, err = list.SaveToImage(store, "", names, ""); err != nil { if errors.Is(err, storage.ErrDuplicateName) && opts.amend { for _, name := range names { manifestList, err := runtime.LookupManifestList(listImageSpec) if err != nil { logrus.Debugf("no list named %q found: %v", listImageSpec, err) continue } if _, list, err = manifests.LoadFromImage(store, manifestList.ID()); err != nil { logrus.Debugf("no list found in %q", name) continue } manifestListID = manifestList.ID() break } if list == nil { return fmt.Errorf("--amend specified but no matching manifest list found with name %q", listImageSpec) } } else { return err } } locker, err := manifests.LockerForImage(store, manifestListID) if err != nil { return err } locker.Lock() defer locker.Unlock() if _, list, err = manifests.LoadFromImage(store, manifestListID); err != nil { return err } if len(opts.annotations) != 0 { annotations := make(map[string]string) for _, annotationSpec := range opts.annotations { k, v, ok := strings.Cut(annotationSpec, "=") if !ok { return fmt.Errorf(`no "=" found in annotation %q`, annotationSpec) } annotations[k] = v } if err := list.SetAnnotations(nil, annotations); err != nil { return err } } for _, imageSpec := range imageSpecs { ref, err := alltransports.ParseImageName(imageSpec) if err != nil { if ref, err = alltransports.ParseImageName(util.DefaultTransport + imageSpec); err != nil { // check if the local image exists if ref, _, err = util.FindImage(store, "", systemContext, imageSpec); err != nil { return err } } } refLocal, _, err := util.FindImage(store, "", systemContext, imageSpec) if err == nil { // Found local image so use that. ref = refLocal } if _, err = list.Add(getContext(), systemContext, ref, opts.all); err != nil { return err } } imageID, err := list.SaveToImage(store, manifestListID, names, "") if err == nil { fmt.Printf("%s\n", imageID) } return err } func manifestAddCmd(c *cobra.Command, args []string, opts manifestAddOpts) error { if err := auth.CheckAuthFile(opts.authfile); err != nil { return err } listImageSpec := "" imageSpec := "" artifactSpec := []string{} switch len(args) { case 0, 1: return errors.New("at least a list image and an image or artifact to add must be specified") default: listImageSpec = args[0] if listImageSpec == "" { return fmt.Errorf("invalid image name %q", args[0]) } if opts.artifact { artifactSpec = args[1:] } else { if len(args) > 2 { return errors.New("too many arguments: expected list and image add to list") } imageSpec = args[1] if imageSpec == "" { return fmt.Errorf("invalid image name %q", args[1]) } } } store, err := getStore(c) if err != nil { return err } systemContext, err := parse.SystemContextFromOptions(c) if err != nil { return fmt.Errorf("building system context: %w", err) } runtime, err := libimage.RuntimeFromStore(store, &libimage.RuntimeOptions{SystemContext: systemContext}) if err != nil { return err } manifestList, err := runtime.LookupManifestList(listImageSpec) if err != nil { return err } locker, err := manifests.LockerForImage(store, manifestList.ID()) if err != nil { return err } locker.Lock() defer locker.Unlock() _, list, err := manifests.LoadFromImage(store, manifestList.ID()) if err != nil { return err } var instanceDigest digest.Digest if opts.artifact { var subjectRef types.ImageReference if opts.artifactSubject != "" { if subjectRef, err = alltransports.ParseImageName(opts.artifactSubject); err != nil { if subjectRef, err = alltransports.ParseImageName(util.DefaultTransport + opts.artifactSubject); err != nil { if subjectRef, _, err = util.FindImage(store, "", systemContext, opts.artifactSubject); err != nil { logrus.Errorf("Error while trying to parse artifact subject %q: %v", opts.artifactSubject, err) return err } } } } var artifactType *string if c.Flags().Changed("artifact-type") { artifactType = &opts.artifactType } var artifactLayerType *string if c.Flags().Changed("artifact-layer-type") { artifactLayerType = &opts.artifactLayerType } options := manifests.AddArtifactOptions{ ManifestArtifactType: artifactType, LayerMediaType: artifactLayerType, SubjectReference: subjectRef, } if opts.artifactConfigType != "" { tmp := imgspecv1.DescriptorEmptyJSON tmp.MediaType = opts.artifactConfigType options.ConfigDescriptor = &tmp } if opts.artifactConfigFile != "" { if options.ConfigDescriptor == nil { tmp := imgspecv1.DescriptorEmptyJSON if opts.artifactConfigType == "" { tmp.MediaType = imgspecv1.MediaTypeImageConfig } options.ConfigDescriptor = &tmp } options.ConfigDescriptor.Size = -1 options.ConfigFile = opts.artifactConfigFile } if len(opts.artifactAnnotations) > 0 { options.Annotations = make(map[string]string, len(opts.artifactAnnotations)) for _, annotation := range opts.artifactAnnotations { k, v, _ := strings.Cut(annotation, "=") options.Annotations[k] = v } } options.ExcludeTitles = opts.artifactExcludeTitles instanceDigest, err = list.AddArtifact(getContext(), systemContext, options, artifactSpec...) if err != nil { logrus.Errorf("Error while trying to add artifact %q to image index: %v", artifactSpec, err) return err } } else { var changedArtifactFlags []string for _, artifactOption := range []string{"artifact-type", "artifact-config", "artifact-config-type", "artifact-layer-type", "artifact-subject", "artifact-exclude-titles", "artifact-annotation"} { if c.Flags().Changed(artifactOption) { changedArtifactFlags = append(changedArtifactFlags, "--"+artifactOption) } } switch { case len(changedArtifactFlags) == 1: return fmt.Errorf("%s requires --artifact", changedArtifactFlags[0]) case len(changedArtifactFlags) > 1: return fmt.Errorf("%s require --artifact", strings.Join(changedArtifactFlags, "/")) } var ref types.ImageReference if ref, err = alltransports.ParseImageName(imageSpec); err != nil { if ref, err = alltransports.ParseImageName(util.DefaultTransport + imageSpec); err != nil { if ref, _, err = util.FindImage(store, "", systemContext, imageSpec); err != nil { return err } } } instanceDigest, err = list.Add(getContext(), systemContext, ref, opts.all) if err != nil { var storeErr error // Retry without a custom system context. A user may want to add // a custom platform (see #3511). if ref, _, storeErr = util.FindImage(store, "", nil, imageSpec); storeErr != nil { logrus.Errorf("Error while trying to find image on local storage: %v", storeErr) return err } instanceDigest, storeErr = list.Add(getContext(), systemContext, ref, opts.all) if storeErr != nil { logrus.Errorf("Error while trying to add on manifest list: %v", storeErr) return err } } } if opts.os != "" { if err := list.SetOS(instanceDigest, opts.os); err != nil { return err } } if opts.osVersion != "" { if err := list.SetOSVersion(instanceDigest, opts.osVersion); err != nil { return err } } if len(opts.osFeatures) != 0 { if err := list.SetOSFeatures(instanceDigest, opts.osFeatures); err != nil { return err } } if opts.arch != "" { if err := list.SetArchitecture(instanceDigest, opts.arch); err != nil { return err } } if opts.variant != "" { if err := list.SetVariant(instanceDigest, opts.variant); err != nil { return err } } if len(opts.features) != 0 { if err := list.SetFeatures(instanceDigest, opts.features); err != nil { return err } } if len(opts.annotations) != 0 { annotations := make(map[string]string) for _, annotationSpec := range opts.annotations { k, v, ok := strings.Cut(annotationSpec, "=") if !ok { return fmt.Errorf(`no "=" found in annotation %q`, annotationSpec) } annotations[k] = v } if err := list.SetAnnotations(&instanceDigest, annotations); err != nil { return err } } updatedListID, err := list.SaveToImage(store, manifestList.ID(), nil, "") if err == nil { fmt.Printf("%s: %s\n", updatedListID, instanceDigest.String()) } return err } func manifestRemoveCmd(c *cobra.Command, args []string, _ manifestRemoveOpts) error { listImageSpec := "" var instanceDigest digest.Digest var instanceSpec string switch len(args) { case 0, 1: return errors.New("at least a list image and one or more instance digests must be specified") case 2: listImageSpec = args[0] if listImageSpec == "" { return fmt.Errorf(`invalid image name "%s"`, args[0]) } instanceSpec = args[1] if instanceSpec == "" { return fmt.Errorf(`invalid instance "%s"`, args[1]) } default: return errors.New("at least two arguments are necessary: list and digest of instance to remove from list") } store, err := getStore(c) if err != nil { return err } systemContext, err := parse.SystemContextFromOptions(c) if err != nil { return fmt.Errorf("building system context: %w", err) } runtime, err := libimage.RuntimeFromStore(store, &libimage.RuntimeOptions{SystemContext: systemContext}) if err != nil { return err } manifestList, err := runtime.LookupManifestList(listImageSpec) if err != nil { return err } _, list, err := manifests.LoadFromImage(store, manifestList.ID()) if err != nil { return err } d, err := list.InstanceByFile(instanceSpec) if err != nil { instanceRef, err := alltransports.ParseImageName(instanceSpec) if err != nil { if instanceRef, err = alltransports.ParseImageName(util.DefaultTransport + instanceSpec); err != nil { if instanceRef, _, err = util.FindImage(store, "", systemContext, instanceSpec); err != nil { return fmt.Errorf(`invalid instance "%s": %v`, instanceSpec, err) } } } ctx := getContext() instanceImg, err := instanceRef.NewImageSource(ctx, systemContext) if err != nil { return fmt.Errorf("reading image instance: %w", err) } defer instanceImg.Close() manifestBytes, _, err := image.UnparsedInstance(instanceImg, nil).Manifest(ctx) if err != nil { return fmt.Errorf("reading image instance manifest: %w", err) } d, err = manifest.Digest(manifestBytes) if err != nil { return fmt.Errorf("digesting image instance manifest: %w", err) } } instanceDigest = d if err := manifestList.RemoveInstance(instanceDigest); err != nil { return err } fmt.Printf("%s: %s\n", manifestList.ID(), instanceDigest.String()) return nil } func manifestRmCmd(c *cobra.Command, args []string) error { store, err := getStore(c) if err != nil { return err } systemContext, err := parse.SystemContextFromOptions(c) if err != nil { return fmt.Errorf("building system context: %w", err) } runtime, err := libimage.RuntimeFromStore(store, &libimage.RuntimeOptions{SystemContext: systemContext}) if err != nil { return err } options := &libimage.RemoveImagesOptions{ Filters: []string{"readonly=false"}, LookupManifest: true, } rmiReports, rmiErrors := runtime.RemoveImages(context.Background(), args, options) for _, r := range rmiReports { for _, u := range r.Untagged { fmt.Printf("untagged: %s\n", u) } } for _, r := range rmiReports { if r.Removed { fmt.Printf("%s\n", r.ID) } } var multiE *multierror.Error multiE = multierror.Append(multiE, rmiErrors...) return multiE.ErrorOrNil() } func manifestAnnotateCmd(c *cobra.Command, args []string, opts manifestAnnotateOpts) error { listImageSpec := "" instanceSpec := "" if opts.subject != "" { // this option is always only working at the index level opts.index = true } switch len(args) { case 0: return errors.New("at least a list image must be specified") case 1: listImageSpec = args[0] if listImageSpec == "" { return fmt.Errorf(`invalid image name "%s"`, args[0]) } if !opts.index { return errors.New(`expected an instance digest, image name, or artifact name`) } case 2: listImageSpec = args[0] if listImageSpec == "" { return fmt.Errorf(`invalid image name "%s"`, args[0]) } if opts.index { return fmt.Errorf(`did not expect image or artifact name "%s" when modifying the entire index`, args[1]) } instanceSpec = args[1] if instanceSpec == "" { return fmt.Errorf(`invalid instance digest, image name, or artifact name "%s"`, instanceSpec) } default: return errors.New("expected either a list name and --index or a list name and an image digest or image name or artifact name") } store, err := getStore(c) if err != nil { return err } systemContext, err := parse.SystemContextFromOptions(c) if err != nil { return fmt.Errorf("building system context: %w", err) } runtime, err := libimage.RuntimeFromStore(store, &libimage.RuntimeOptions{SystemContext: systemContext}) if err != nil { return err } manifestList, err := runtime.LookupManifestList(listImageSpec) if err != nil { return err } locker, err := manifests.LockerForImage(store, manifestList.ID()) if err != nil { return err } locker.Lock() defer locker.Unlock() _, list, err := manifests.LoadFromImage(store, manifestList.ID()) if err != nil { return err } var instance digest.Digest if !opts.index { d, err := list.InstanceByFile(instanceSpec) if err != nil { instanceRef, err := alltransports.ParseImageName(instanceSpec) if err != nil { if instanceRef, err = alltransports.ParseImageName(util.DefaultTransport + instanceSpec); err != nil { // check if the local image exists if instanceRef, _, err = util.FindImage(store, "", systemContext, instanceSpec); err != nil { return fmt.Errorf(`invalid instance "%s": %v`, instanceSpec, err) } } } ctx := getContext() instanceImg, err := instanceRef.NewImageSource(ctx, systemContext) if err != nil { return fmt.Errorf("reading image instance: %w", err) } defer instanceImg.Close() manifestBytes, _, err := image.UnparsedInstance(instanceImg, nil).Manifest(ctx) if err != nil { return fmt.Errorf("reading image instance manifest: %w", err) } d, err = manifest.Digest(manifestBytes) if err != nil { return fmt.Errorf("digesting image instance manifest: %w", err) } } instance = d } if opts.os != "" { if opts.index { return fmt.Errorf("--index is not compatible with --os") } if err := list.SetOS(instance, opts.os); err != nil { return err } } if opts.osVersion != "" { if opts.index { return fmt.Errorf("--index is not compatible with --os-version") } if err := list.SetOSVersion(instance, opts.osVersion); err != nil { return err } } if len(opts.osFeatures) != 0 { if opts.index { return fmt.Errorf("--index is not compatible with --os-features") } if err := list.SetOSFeatures(instance, opts.osFeatures); err != nil { return err } } if opts.arch != "" { if opts.index { return fmt.Errorf("--index is not compatible with --arch") } if err := list.SetArchitecture(instance, opts.arch); err != nil { return err } } if opts.variant != "" { if opts.index { return fmt.Errorf("--index is not compatible with --variant") } if err := list.SetVariant(instance, opts.variant); err != nil { return err } } if len(opts.features) != 0 { if opts.index { return fmt.Errorf("--index is not compatible with --features") } if err := list.SetFeatures(instance, opts.features); err != nil { return err } } if len(opts.annotations) != 0 { annotations := make(map[string]string) for _, annotationSpec := range opts.annotations { k, v, ok := strings.Cut(annotationSpec, "=") if !ok { return fmt.Errorf(`no "=" found in annotation %q`, annotationSpec) } annotations[k] = v } var instanceDigest *digest.Digest if !opts.index { instanceDigest = &instance } if err := list.SetAnnotations(instanceDigest, annotations); err != nil { return err } } if opts.subject != "" { subjectRef, err := alltransports.ParseImageName(opts.subject) if err != nil { if subjectRef, err = alltransports.ParseImageName(util.DefaultTransport + opts.subject); err != nil { // check if the local image exists if subjectRef, _, err = util.FindImage(store, "", systemContext, opts.subject); err != nil { logrus.Errorf("Error while trying to parse artifact subject: %v", err) return err } } } ctx := getContext() src, err := subjectRef.NewImageSource(ctx, systemContext) if err != nil { logrus.Errorf("Error while trying to read artifact subject: %v", err) return err } defer src.Close() manifestBytes, manifestType, err := image.UnparsedInstance(src, nil).Manifest(ctx) if err != nil { logrus.Errorf("Error while trying to read artifact subject manifest: %v", err) return err } manifestDigest, err := manifest.Digest(manifestBytes) if err != nil { logrus.Errorf("Error while trying to digest artifact subject manifest: %v", err) return err } descriptor := imgspecv1.Descriptor{ MediaType: manifestType, Size: int64(len(manifestBytes)), Digest: manifestDigest, } if err := list.SetSubject(&descriptor); err != nil { return err } } updatedListID, err := list.SaveToImage(store, manifestList.ID(), nil, "") if err == nil { if instance == "" { fmt.Printf("%s\n", updatedListID) } else { fmt.Printf("%s: %s\n", updatedListID, instance.String()) } } return nil } func manifestInspectCmd(c *cobra.Command, args []string, opts manifestInspectOpts) error { if c.Flag("authfile").Changed { if err := auth.CheckAuthFile(opts.authfile); err != nil { return err } } imageSpec := "" switch len(args) { case 0: return errors.New("at least a source list ID must be specified") case 1: imageSpec = args[0] if imageSpec == "" { return fmt.Errorf(`invalid image name "%s"`, imageSpec) } default: return errors.New("only one argument is necessary for inspect: an image name") } store, err := getStore(c) if err != nil { return err } systemContext, err := parse.SystemContextFromOptions(c) if err != nil { return fmt.Errorf("building system context: %w", err) } return manifestInspect(getContext(), store, systemContext, imageSpec) } func manifestInspect(ctx context.Context, store storage.Store, systemContext *types.SystemContext, imageSpec string) error { runtime, err := libimage.RuntimeFromStore(store, &libimage.RuntimeOptions{SystemContext: systemContext}) if err != nil { return err } printManifest := func(manifest []byte) error { var b bytes.Buffer err = json.Indent(&b, manifest, "", " ") if err != nil { return fmt.Errorf("rendering manifest for display: %w", err) } fmt.Printf("%s\n", b.String()) return nil } // Before doing a remote lookup, attempt to resolve the manifest list // locally. manifestList, err := runtime.LookupManifestList(imageSpec) if err == nil { schema2List, err := manifestList.Inspect() if err != nil { return err } rawSchema2List, err := json.Marshal(schema2List) if err != nil { return err } return printManifest(rawSchema2List) } if !errors.Is(err, storage.ErrImageUnknown) && !errors.Is(err, libimage.ErrNotAManifestList) { return err } // TODO: at some point `libimage` should support resolving manifests // like that. Similar to `libimage.Runtime.LookupImage` we could // implement a `*.LookupImageIndex`. refs, err := util.ResolveNameToReferences(store, systemContext, imageSpec) if err != nil { logrus.Debugf("error parsing reference to image %q: %v", imageSpec, err) } if ref, _, err := util.FindImage(store, "", systemContext, imageSpec); err == nil { refs = append(refs, ref) } else if ref, err := alltransports.ParseImageName(imageSpec); err == nil { refs = append(refs, ref) } if len(refs) == 0 { return fmt.Errorf("locating images with names %v", imageSpec) } var ( latestErr error result []byte ) appendErr := func(e error) { if latestErr == nil { latestErr = e } else { latestErr = fmt.Errorf("tried %v: %w", e, latestErr) } } for _, ref := range refs { logrus.Debugf("Testing reference %q for possible manifest", transports.ImageName(ref)) src, err := ref.NewImageSource(ctx, systemContext) if err != nil { appendErr(fmt.Errorf("reading image %q: %w", transports.ImageName(ref), err)) continue } defer src.Close() manifestBytes, manifestType, err := image.UnparsedInstance(src, nil).Manifest(ctx) if err != nil { appendErr(fmt.Errorf("loading manifest %q: %w", transports.ImageName(ref), err)) continue } if !manifest.MIMETypeIsMultiImage(manifestType) { appendErr(fmt.Errorf("manifest is of type %s (not a list type)", manifestType)) continue } result = manifestBytes break } if len(result) == 0 && latestErr != nil { return latestErr } return printManifest(result) } func manifestPushCmd(c *cobra.Command, args []string, opts pushOptions) error { if err := auth.CheckAuthFile(opts.authfile); err != nil { return err } listImageSpec := "" destSpec := "" switch len(args) { case 0: return errors.New("at least a source list ID must be specified") case 1: listImageSpec = args[0] destSpec = "docker://" + listImageSpec case 2: listImageSpec = args[0] destSpec = args[1] default: return errors.New("only two arguments are necessary to push: source and destination") } if listImageSpec == "" { return fmt.Errorf(`invalid image name "%s"`, listImageSpec) } if destSpec == "" { return fmt.Errorf(`invalid image name "%s"`, destSpec) } store, err := getStore(c) if err != nil { return err } systemContext, err := parse.SystemContextFromOptions(c) if err != nil { return fmt.Errorf("building system context: %w", err) } if opts.compressionFormat != "" { algo, err := compression.AlgorithmByName(opts.compressionFormat) if err != nil { return err } systemContext.CompressionFormat = &algo } if c.Flag("compression-level").Changed { systemContext.CompressionLevel = &opts.compressionLevel } if c.Flag("compression-format").Changed { if !c.Flag("force-compression").Changed { // If `compression-format` is set and no value for `--force-compression` // is selected then defaults to `true`. opts.forceCompressionFormat = true } } return manifestPush(systemContext, store, listImageSpec, destSpec, opts) } func manifestPush(systemContext *types.SystemContext, store storage.Store, listImageSpec, destSpec string, opts pushOptions) error { runtime, err := libimage.RuntimeFromStore(store, &libimage.RuntimeOptions{SystemContext: systemContext}) if err != nil { return err } manifestList, err := runtime.LookupManifestList(listImageSpec) if err != nil { return err } locker, err := manifests.LockerForImage(store, manifestList.ID()) if err != nil { return err } locker.Lock() defer locker.Unlock() _, list, err := manifests.LoadFromImage(store, manifestList.ID()) if err != nil { return err } dest, err := alltransports.ParseImageName(destSpec) if err != nil { return err } var manifestType string if opts.format != "" { switch opts.format { case "oci": manifestType = imgspecv1.MediaTypeImageManifest case "v2s2", "docker": manifestType = manifest.DockerV2Schema2MediaType default: return fmt.Errorf("unknown format %q. Choose on of the supported formats: 'oci' or 'v2s2'", opts.format) } } retry := uint(opts.retry) options := manifests.PushOptions{ Store: store, SystemContext: systemContext, ImageListSelection: cp.CopySpecificImages, Instances: nil, RemoveSignatures: opts.removeSignatures, SignBy: opts.signBy, ManifestType: manifestType, AddCompression: opts.addCompression, ForceCompressionFormat: opts.forceCompressionFormat, MaxRetries: &retry, } if opts.retryDelay != "" { retryDelay, err := time.ParseDuration(opts.retryDelay) if err != nil { return fmt.Errorf("unable to parse retryDelay %q: %w", opts.retryDelay, err) } options.RetryDelay = &retryDelay } if opts.all { options.ImageListSelection = cp.CopyAllImages } if !opts.quiet { options.ReportWriter = os.Stderr } _, digest, err := list.Push(getContext(), dest, options) if err == nil && opts.rm { _, err = store.DeleteImage(manifestList.ID(), true) } if opts.digestfile != "" { if err = os.WriteFile(opts.digestfile, []byte(digest.String()), 0o644); err != nil { return util.GetFailureCause(err, fmt.Errorf("failed to write digest to file %q: %w", opts.digestfile, err)) } } return err } ================================================ FILE: cmd/buildah/mkcw.go ================================================ package main import ( "fmt" "os" "strings" "github.com/containers/buildah" "github.com/containers/buildah/pkg/parse" "github.com/spf13/cobra" ) func mkcwCmd(c *cobra.Command, args []string, options buildah.CWConvertImageOptions) error { ctx := getContext() systemContext, err := parse.SystemContextFromOptions(c) if err != nil { return err } if options.AttestationURL == "" && options.DiskEncryptionPassphrase == "" { return fmt.Errorf("neither --attestation-url nor --passphrase flags provided, disk would not be decryptable") } store, err := getStore(c) if err != nil { return err } options.InputImage = args[0] options.Tag = args[1] options.ReportWriter = os.Stderr imageID, _, _, err := buildah.CWConvertImage(ctx, systemContext, store, options) if err == nil { fmt.Printf("%s\n", imageID) } return err } func mkcwInit() { var teeType string var addFile []string var options buildah.CWConvertImageOptions mkcwDescription := `Convert a conventional image to a confidential workload image.` mkcwCommand := &cobra.Command{ Use: "mkcw", Short: "Convert a conventional image to a confidential workload image", Long: mkcwDescription, RunE: func(cmd *cobra.Command, args []string) error { options.TeeType = parse.TeeType(teeType) if len(addFile) > 0 { options.ExtraImageContent = make(map[string]string) for _, spec := range addFile { source, dest, haveDest := strings.Cut(spec, ":") if !haveDest { dest = source } st, err := os.Stat(source) if err != nil { return fmt.Errorf("parsing add-file argument %q: source %q: %w", spec, source, err) } if st.IsDir() { return fmt.Errorf("parsing add-file argument %q: source %q is not a regular file", spec, source) } options.ExtraImageContent[dest] = source } } return mkcwCmd(cmd, args, options) }, Example: `buildah mkcw localhost/repository:typical localhost/repository:cw`, Args: cobra.ExactArgs(2), } mkcwCommand.SetUsageTemplate(UsageTemplate()) rootCmd.AddCommand(mkcwCommand) flags := mkcwCommand.Flags() flags.SetInterspersed(false) flags.StringVarP(&teeType, "type", "t", "", "TEE (trusted execution environment) type: SEV,SNP (default: SNP)") flags.StringArrayVar(&addFile, "add-file", nil, "add contents of a file to the image at a specified path (`source:destination`)") flags.StringVarP(&options.AttestationURL, "attestation-url", "u", "", "attestation server URL") flags.StringVarP(&options.BaseImage, "base-image", "b", "", "alternate base image (default: scratch)") flags.StringVarP(&options.DiskEncryptionPassphrase, "passphrase", "p", "", "disk encryption passphrase") flags.IntVarP(&options.CPUs, "cpus", "c", 0, "number of CPUs to expect") flags.IntVarP(&options.Memory, "memory", "m", 0, "amount of memory to expect (MB)") flags.StringVarP(&options.WorkloadID, "workload-id", "w", "", "workload ID") flags.StringVarP(&options.Slop, "slop", "s", "25%", "extra space needed for converting a container rootfs to a disk image") flags.StringVarP(&options.FirmwareLibrary, "firmware-library", "f", "", "location of libkrunfw-sev.so") flags.BoolVarP(&options.IgnoreAttestationErrors, "ignore-attestation-errors", "", false, "ignore attestation errors") if err := flags.MarkHidden("ignore-attestation-errors"); err != nil { panic(fmt.Sprintf("error marking ignore-attestation-errors as hidden: %v", err)) } flags.String("signature-policy", "", "`pathname` of signature policy file (not usually used)") if err := flags.MarkHidden("signature-policy"); err != nil { panic(fmt.Sprintf("error marking signature-policy as hidden: %v", err)) } } ================================================ FILE: cmd/buildah/mount.go ================================================ package main import ( "encoding/json" "fmt" "os" buildahcli "github.com/containers/buildah/pkg/cli" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) type jsonMount struct { Container string `json:"container,omitempty"` MountPoint string `json:"mountPoint"` } type mountOptions struct { json bool } func mountInit() { var ( mountDescription = `buildah mount mounts a working container's root filesystem for manipulation. Note: In rootless mode you need to first execute buildah unshare, to put you into the usernamespace. Afterwards you can buildah mount the container and view/modify the content in the containers root file system. ` opts mountOptions noTruncate bool ) mountCommand := &cobra.Command{ Use: "mount", Short: "Mount a working container's root filesystem", Long: mountDescription, RunE: func(cmd *cobra.Command, args []string) error { return mountCmd(cmd, args, opts) }, Example: `buildah mount buildah mount containerID buildah mount containerID1 containerID2 In rootless mode you must use buildah unshare first. buildah unshare buildah mount containerID `, } mountCommand.SetUsageTemplate(UsageTemplate()) flags := mountCommand.Flags() flags.SetInterspersed(false) flags.BoolVar(&opts.json, "json", false, "output in JSON format") flags.BoolVar(&noTruncate, "notruncate", false, "do not truncate output") rootCmd.AddCommand(mountCommand) if err := flags.MarkHidden("notruncate"); err != nil { logrus.Fatalf("error marking notruncate as hidden: %v", err) } } func mountCmd(c *cobra.Command, args []string, opts mountOptions) error { if err := buildahcli.VerifyFlagsArgsOrder(args); err != nil { return err } store, err := getStore(c) if err != nil { return err } var jsonMounts []jsonMount var lastError error if len(args) > 0 { // Do not allow to mount a graphdriver that is not vfs if we are creating the userns as part // of the mount command. // Differently, allow the mount if we are already in a userns, as the mount point will still // be accessible once "buildah mount" exits. if os.Geteuid() != 0 && store.GraphDriverName() != "vfs" { return fmt.Errorf("cannot mount using driver %s in rootless mode. You need to run it in a `buildah unshare` session", store.GraphDriverName()) } for _, name := range args { builder, err := openBuilder(getContext(), store, name) if err != nil { if lastError != nil { fmt.Fprintln(os.Stderr, lastError) } lastError = fmt.Errorf("reading build container %q: %w", name, err) continue } mountPoint, err := builder.Mount(builder.MountLabel) if err != nil { if lastError != nil { fmt.Fprintln(os.Stderr, lastError) } lastError = fmt.Errorf("mounting %q container %q: %w", name, builder.Container, err) continue } if len(args) > 1 { if opts.json { jsonMounts = append(jsonMounts, jsonMount{Container: name, MountPoint: mountPoint}) continue } fmt.Printf("%s %s\n", name, mountPoint) } else { if opts.json { jsonMounts = append(jsonMounts, jsonMount{MountPoint: mountPoint}) continue } fmt.Printf("%s\n", mountPoint) } } } else { builders, err := openBuilders(store) if err != nil { return fmt.Errorf("reading build containers: %w", err) } for _, builder := range builders { mounted, err := builder.Mounted() if err != nil { return err } if mounted { if opts.json { jsonMounts = append(jsonMounts, jsonMount{Container: builder.Container, MountPoint: builder.MountPoint}) continue } fmt.Printf("%s %s\n", builder.Container, builder.MountPoint) } } } if opts.json { data, err := json.MarshalIndent(jsonMounts, "", " ") if err != nil { return err } fmt.Printf("%s\n", data) } return lastError } ================================================ FILE: cmd/buildah/prune.go ================================================ package main import ( "context" "fmt" buildahcli "github.com/containers/buildah/pkg/cli" "github.com/containers/buildah/pkg/parse" "github.com/containers/buildah/pkg/volumes" "github.com/hashicorp/go-multierror" "github.com/spf13/cobra" "go.podman.io/common/libimage" ) type pruneOptions struct { force bool all bool } func pruneInit() { var ( pruneDescription = ` Cleanup intermediate images as well as build and mount cache.` opts pruneOptions ) pruneCommand := &cobra.Command{ Use: "prune", Short: "Cleanup intermediate images as well as build and mount cache", Long: pruneDescription, RunE: func(cmd *cobra.Command, args []string) error { return pruneCmd(cmd, args, opts) }, Example: `buildah prune buildah prune --force`, } pruneCommand.SetUsageTemplate(UsageTemplate()) flags := pruneCommand.Flags() flags.SetInterspersed(false) flags.BoolVarP(&opts.all, "all", "a", false, "remove all unused images") flags.BoolVarP(&opts.force, "force", "f", false, "force removal of the image and any containers using the image") rootCmd.AddCommand(pruneCommand) } func pruneCmd(c *cobra.Command, args []string, iopts pruneOptions) error { if err := buildahcli.VerifyFlagsArgsOrder(args); err != nil { return err } store, err := getStore(c) if err != nil { return err } systemContext, err := parse.SystemContextFromOptions(c) if err != nil { return err } runtime, err := libimage.RuntimeFromStore(store, &libimage.RuntimeOptions{SystemContext: systemContext}) if err != nil { return err } err = volumes.CleanCacheMount() if err != nil { return err } options := &libimage.RemoveImagesOptions{ Filters: []string{"readonly=false"}, } if !iopts.all { options.Filters = append(options.Filters, "dangling=true") options.Filters = append(options.Filters, "intermediate=true") } options.Force = iopts.force rmiReports, rmiErrors := runtime.RemoveImages(context.Background(), args, options) for _, r := range rmiReports { for _, u := range r.Untagged { fmt.Printf("untagged: %s\n", u) } } for _, r := range rmiReports { if r.Removed { fmt.Printf("%s\n", r.ID) } } var multiE *multierror.Error multiE = multierror.Append(multiE, rmiErrors...) return multiE.ErrorOrNil() } ================================================ FILE: cmd/buildah/pull.go ================================================ package main import ( "errors" "fmt" "os" "runtime" "time" "github.com/containers/buildah" "github.com/containers/buildah/define" "github.com/containers/buildah/pkg/cli" "github.com/containers/buildah/pkg/parse" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "go.podman.io/common/pkg/auth" ) type pullOptions struct { allTags bool authfile string blobCache string certDir string creds string signaturePolicy string quiet bool removeSignatures bool tlsVerify bool decryptionKeys []string pullPolicy string retry int retryDelay string } func pullInit() { var ( opts pullOptions pullDescription = ` Pulls an image from a registry and stores it locally. An image can be pulled using its tag or digest. If a tag is not specified, the image with the 'latest' tag (if it exists) is pulled.` ) pullCommand := &cobra.Command{ Use: "pull", Short: "Pull an image from the specified location", Long: pullDescription, RunE: func(cmd *cobra.Command, args []string) error { return pullCmd(cmd, args, opts) }, Example: `buildah pull imagename buildah pull docker-daemon:imagename:imagetag buildah pull myregistry/myrepository/imagename:imagetag`, } pullCommand.SetUsageTemplate(UsageTemplate()) flags := pullCommand.Flags() flags.SetInterspersed(false) flags.BoolVarP(&opts.allTags, "all-tags", "a", false, "download all tagged images in the repository") flags.StringVar(&opts.authfile, "authfile", auth.GetDefaultAuthFile(), "path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") flags.StringVar(&opts.blobCache, "blob-cache", "", "store copies of pulled image blobs in the specified directory") flags.StringVar(&opts.certDir, "cert-dir", "", "use certificates at the specified path to access the registry") flags.StringVar(&opts.creds, "creds", "", "use `[username[:password]]` for accessing the registry") flags.StringVar(&opts.pullPolicy, "policy", "missing", "missing, always, ifnewer, or never.") flags.BoolVarP(&opts.removeSignatures, "remove-signatures", "", false, "don't copy signatures when pulling image") flags.StringVar(&opts.signaturePolicy, "signature-policy", "", "`pathname` of signature policy file (not usually used)") flags.StringSliceVar(&opts.decryptionKeys, "decryption-key", nil, "key needed to decrypt the image") if err := flags.MarkHidden("signature-policy"); err != nil { panic(fmt.Sprintf("error marking signature-policy as hidden: %v", err)) } flags.BoolVarP(&opts.quiet, "quiet", "q", false, "don't output progress information when pulling images") flags.String("os", runtime.GOOS, "prefer `OS` instead of the running OS for choosing images") flags.String("arch", runtime.GOARCH, "prefer `ARCH` instead of the architecture of the machine for choosing images") flags.StringSlice("platform", []string{parse.DefaultPlatform()}, "prefer OS/ARCH instead of the current operating system and architecture for choosing images") flags.String("variant", "", "override the `variant` of the specified image") flags.BoolVar(&opts.tlsVerify, "tls-verify", true, "require HTTPS and verify certificates when accessing the registry. TLS verification cannot be used when talking to an insecure registry.") flags.IntVar(&opts.retry, "retry", int(defaultContainerConfig.Engine.Retry), "number of times to retry in case of failure when performing pull") flags.StringVar(&opts.retryDelay, "retry-delay", defaultContainerConfig.Engine.RetryDelay, "delay between retries in case of pull failures") if err := flags.MarkHidden("blob-cache"); err != nil { panic(fmt.Sprintf("error marking blob-cache as hidden: %v", err)) } rootCmd.AddCommand(pullCommand) } func pullCmd(c *cobra.Command, args []string, iopts pullOptions) error { if len(args) == 0 { return errors.New("an image name must be specified") } if err := cli.VerifyFlagsArgsOrder(args); err != nil { return err } if len(args) > 1 { return errors.New("too many arguments specified") } if err := auth.CheckAuthFile(iopts.authfile); err != nil { return err } systemContext, err := parse.SystemContextFromOptions(c) if err != nil { return fmt.Errorf("building system context: %w", err) } platforms, err := parse.PlatformsFromOptions(c) if err != nil { return err } if len(platforms) > 1 { logrus.Warnf("ignoring platforms other than %+v: %+v", platforms[0], platforms[1:]) } store, err := getStore(c) if err != nil { return err } decConfig, err := cli.DecryptConfig(iopts.decryptionKeys) if err != nil { return fmt.Errorf("unable to obtain decryption config: %w", err) } policy, ok := define.PolicyMap[iopts.pullPolicy] if !ok { return fmt.Errorf("unsupported pull policy %q", iopts.pullPolicy) } options := buildah.PullOptions{ SignaturePolicyPath: iopts.signaturePolicy, Store: store, SystemContext: systemContext, BlobDirectory: iopts.blobCache, AllTags: iopts.allTags, ReportWriter: os.Stderr, RemoveSignatures: iopts.removeSignatures, MaxRetries: iopts.retry, OciDecryptConfig: decConfig, PullPolicy: policy, } if iopts.retryDelay != "" { options.RetryDelay, err = time.ParseDuration(iopts.retryDelay) if err != nil { return fmt.Errorf("unable to parse value provided %q as --retry-delay: %w", iopts.retryDelay, err) } } if iopts.quiet { options.ReportWriter = nil // Turns off logging output } id, err := buildah.Pull(getContext(), args[0], options) if err != nil { return err } fmt.Printf("%s\n", id) return nil } ================================================ FILE: cmd/buildah/push.go ================================================ package main import ( "errors" "fmt" "os" "strings" "time" "github.com/containers/buildah" "github.com/containers/buildah/define" "github.com/containers/buildah/pkg/cli" "github.com/containers/buildah/pkg/parse" util "github.com/containers/buildah/util" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "go.podman.io/common/pkg/auth" "go.podman.io/image/v5/manifest" "go.podman.io/image/v5/pkg/compression" "go.podman.io/image/v5/transports" "go.podman.io/image/v5/transports/alltransports" "go.podman.io/storage" ) type pushOptions struct { all bool authfile string blobCache string certDir string creds string digestfile string disableCompression bool format string compressionFormat string compressionLevel int forceCompressionFormat bool retry int retryDelay string rm bool quiet bool removeSignatures bool signaturePolicy string signBy string tlsVerify bool encryptionKeys []string encryptLayers []int insecure bool addCompression []string } func pushInit() { var ( opts pushOptions pushDescription = fmt.Sprintf(` Pushes an image to a specified location. The Image "DESTINATION" uses a "transport":"details" format. If not specified, will reuse source IMAGE as DESTINATION. Supported transports: %s See buildah-push(1) section "DESTINATION" for the expected format `, getListOfTransports()) ) pushCommand := &cobra.Command{ Use: "push", Short: "Push an image to a specified destination", Long: pushDescription, RunE: func(cmd *cobra.Command, args []string) error { return pushCmd(cmd, args, opts) }, Example: `buildah push imageID docker://registry.example.com/repository:tag buildah push imageID docker-daemon:image:tagi buildah push imageID oci:/path/to/layout:image:tag`, } pushCommand.SetUsageTemplate(UsageTemplate()) flags := pushCommand.Flags() flags.SetInterspersed(false) flags.BoolVar(&opts.all, "all", false, "push all of the images referenced by the manifest list") flags.StringVar(&opts.authfile, "authfile", auth.GetDefaultAuthFile(), "path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") flags.StringVar(&opts.blobCache, "blob-cache", "", "assume image blobs in the specified directory will be available for pushing") flags.StringVar(&opts.certDir, "cert-dir", "", "use certificates at the specified path to access the registry") flags.StringVar(&opts.creds, "creds", "", "use `[username[:password]]` for accessing the registry") flags.StringVar(&opts.digestfile, "digestfile", "", "after copying the image, write the digest of the resulting image to the file") flags.BoolVarP(&opts.disableCompression, "disable-compression", "D", false, "don't compress layers") flags.BoolVarP(&opts.forceCompressionFormat, "force-compression", "", false, "use the specified compression algorithm if the destination contains a differently-compressed variant already") flags.StringVarP(&opts.format, "format", "f", "", "manifest type (oci, v2s1, or v2s2) to use in the destination (default is manifest type of source, with fallbacks)") flags.StringVar(&opts.compressionFormat, "compression-format", "", "compression format to use") flags.IntVar(&opts.compressionLevel, "compression-level", 0, "compression level to use") flags.BoolVarP(&opts.quiet, "quiet", "q", false, "don't output progress information when pushing images") flags.IntVar(&opts.retry, "retry", int(defaultContainerConfig.Engine.Retry), "number of times to retry in case of failure when performing push") flags.StringVar(&opts.retryDelay, "retry-delay", defaultContainerConfig.Engine.RetryDelay, "delay between retries in case of push failures") flags.BoolVar(&opts.rm, "rm", false, "remove the manifest list if push succeeds") flags.BoolVarP(&opts.removeSignatures, "remove-signatures", "", false, "don't copy signatures when pushing image") flags.StringVar(&opts.signBy, "sign-by", "", "sign the image using a GPG key with the specified `FINGERPRINT`") flags.StringVar(&opts.signaturePolicy, "signature-policy", "", "`pathname` of signature policy file (not usually used)") flags.StringSliceVar(&opts.encryptionKeys, "encryption-key", nil, "key with the encryption protocol to use needed to encrypt the image (e.g. jwe:/path/to/key.pem)") flags.IntSliceVar(&opts.encryptLayers, "encrypt-layer", nil, "layers to encrypt, 0-indexed layer indices with support for negative indexing (e.g. 0 is the first layer, -1 is the last layer). If not defined, will encrypt all layers if encryption-key flag is specified") if err := flags.MarkHidden("signature-policy"); err != nil { panic(fmt.Sprintf("error marking signature-policy as hidden: %v", err)) } flags.BoolVar(&opts.tlsVerify, "tls-verify", true, "require HTTPS and verify certificates when accessing the registry. TLS verification cannot be used when talking to an insecure registry.") if err := flags.MarkHidden("blob-cache"); err != nil { panic(fmt.Sprintf("error marking blob-cache as hidden: %v", err)) } rootCmd.AddCommand(pushCommand) } func pushCmd(c *cobra.Command, args []string, iopts pushOptions) error { var src, destSpec string if err := cli.VerifyFlagsArgsOrder(args); err != nil { return err } if err := auth.CheckAuthFile(iopts.authfile); err != nil { return err } switch len(args) { case 0: return errors.New("at least a source image ID must be specified") case 1: src = args[0] destSpec = src logrus.Debugf("Destination argument not specified, assuming the same as the source: %s", destSpec) case 2: src = args[0] destSpec = args[1] if src == "" { return fmt.Errorf(`invalid image name "%s"`, args[0]) } default: return errors.New("only two arguments are necessary to push: source and destination") } compress := define.Gzip if iopts.disableCompression { compress = define.Uncompressed } store, err := getStore(c) if err != nil { return err } dest, err := alltransports.ParseImageName(destSpec) // add the docker:// transport to see if they neglected it. if err != nil { destTransport := strings.Split(destSpec, ":")[0] if t := transports.Get(destTransport); t != nil { return err } if strings.Contains(destSpec, "://") { return err } destSpec = "docker://" + destSpec dest2, err2 := alltransports.ParseImageName(destSpec) if err2 != nil { return err } dest = dest2 logrus.Debugf("Assuming docker:// as the transport method for DESTINATION: %s", destSpec) } systemContext, err := parse.SystemContextFromOptions(c) if err != nil { return fmt.Errorf("building system context: %w", err) } var manifestType string if iopts.format != "" { switch iopts.format { case "oci": manifestType = imgspecv1.MediaTypeImageManifest case "v2s1": manifestType = manifest.DockerV2Schema1SignedMediaType case "v2s2", "docker": manifestType = manifest.DockerV2Schema2MediaType default: return fmt.Errorf("unknown format %q. Choose on of the supported formats: 'oci', 'v2s1', or 'v2s2'", iopts.format) } } encConfig, encLayers, err := cli.EncryptConfig(iopts.encryptionKeys, iopts.encryptLayers) if err != nil { return fmt.Errorf("unable to obtain encryption config: %w", err) } if c.Flag("compression-format").Changed { if !c.Flag("force-compression").Changed { // If `compression-format` is set and no value for `--force-compression` // is selected then defaults to `true`. iopts.forceCompressionFormat = true } } options := buildah.PushOptions{ Compression: compress, ManifestType: manifestType, SignaturePolicyPath: iopts.signaturePolicy, Store: store, SystemContext: systemContext, BlobDirectory: iopts.blobCache, RemoveSignatures: iopts.removeSignatures, SignBy: iopts.signBy, MaxRetries: iopts.retry, OciEncryptConfig: encConfig, OciEncryptLayers: encLayers, ForceCompressionFormat: iopts.forceCompressionFormat, } if iopts.retryDelay != "" { options.RetryDelay, err = time.ParseDuration(iopts.retryDelay) if err != nil { return fmt.Errorf("unable to parse value provided %q as --retry-delay: %w", iopts.retryDelay, err) } } if !iopts.quiet { options.ReportWriter = os.Stderr } if iopts.compressionFormat != "" { algo, err := compression.AlgorithmByName(iopts.compressionFormat) if err != nil { return err } options.CompressionFormat = &algo } if c.Flag("compression-level").Changed { options.CompressionLevel = &iopts.compressionLevel } ref, digest, err := buildah.Push(getContext(), src, dest, options) if err != nil { if !errors.Is(err, storage.ErrImageUnknown) { // Image might be a manifest so attempt a manifest push if manifestsErr := manifestPush(systemContext, store, src, destSpec, iopts); manifestsErr == nil { return nil } } return util.GetFailureCause(err, fmt.Errorf("pushing image %q to %q: %w", src, destSpec, err)) } if ref != nil { logrus.Debugf("pushed image %q with digest %s", ref, digest.String()) } else { logrus.Debugf("pushed image with digest %s", digest.String()) } logrus.Debugf("Successfully pushed %s with digest %s", transports.ImageName(dest), digest.String()) if iopts.digestfile != "" { if err = os.WriteFile(iopts.digestfile, []byte(digest.String()), 0o644); err != nil { return util.GetFailureCause(err, fmt.Errorf("failed to write digest to file %q: %w", iopts.digestfile, err)) } } return nil } // getListOfTransports gets the transports supported from the image library // and strips of the "tarball" transport from the string of transports returned func getListOfTransports() string { allTransports := strings.Join(transports.ListNames(), ",") return strings.Replace(allTransports, ",tarball", "", 1) } ================================================ FILE: cmd/buildah/rename.go ================================================ package main import ( "fmt" "github.com/containers/buildah" "github.com/spf13/cobra" ) var ( renameDescription = "\n Renames a local container." renameCommand = &cobra.Command{ Use: "rename", Short: "Rename a container", Long: renameDescription, RunE: renameCmd, Example: `buildah rename containerName NewName buildah rename containerID NewName`, Args: cobra.ExactArgs(2), } ) func renameInit() { renameCommand.SetUsageTemplate(UsageTemplate()) rootCmd.AddCommand(renameCommand) } func renameCmd(c *cobra.Command, args []string) error { var builder *buildah.Builder name := args[0] newName := args[1] store, err := getStore(c) if err != nil { return err } builder, err = openBuilder(getContext(), store, name) if err != nil { return fmt.Errorf("reading build container %q: %w", name, err) } oldName := builder.Container if oldName == newName { return fmt.Errorf("renaming a container with the same name as its current name") } if build, err := openBuilder(getContext(), store, newName); err == nil { return fmt.Errorf("the container name %q is already in use by container %q", newName, build.ContainerID) } err = store.SetNames(builder.ContainerID, []string{newName}) if err != nil { return fmt.Errorf("renaming container %q to the name %q: %w", oldName, newName, err) } builder.Container = newName return builder.Save() } ================================================ FILE: cmd/buildah/rm.go ================================================ package main import ( "errors" "fmt" "os" buildahcli "github.com/containers/buildah/pkg/cli" "github.com/containers/buildah/util" "github.com/spf13/cobra" ) type rmResults struct { all bool } func rmInit() { var ( rmDescription = "\n Removes one or more working containers, unmounting them if necessary." opts rmResults ) rmCommand := &cobra.Command{ Use: "rm", Aliases: []string{"delete"}, Short: "Remove one or more working containers", Long: rmDescription, RunE: func(cmd *cobra.Command, args []string) error { return rmCmd(cmd, args, opts) }, Example: `buildah rm containerID buildah rm containerID1 containerID2 containerID3 buildah rm --all`, } rmCommand.SetUsageTemplate(UsageTemplate()) flags := rmCommand.Flags() flags.SetInterspersed(false) flags.BoolVarP(&opts.all, "all", "a", false, "remove all containers") rootCmd.AddCommand(rmCommand) } func rmCmd(c *cobra.Command, args []string, iopts rmResults) error { delContainerErrStr := "removing container" if len(args) == 0 && !iopts.all { return errors.New("container ID must be specified") } if len(args) > 0 && iopts.all { return errors.New("when using the --all switch, you may not pass any containers names or IDs") } if err := buildahcli.VerifyFlagsArgsOrder(args); err != nil { return err } store, err := getStore(c) if err != nil { return err } var lastError error if iopts.all { builders, err := openBuilders(store) if err != nil { return fmt.Errorf("reading build containers: %w", err) } for _, builder := range builders { id := builder.ContainerID if err = builder.Delete(); err != nil { lastError = util.WriteError(os.Stderr, fmt.Errorf("%s %q: %w", delContainerErrStr, builder.Container, err), lastError) continue } fmt.Printf("%s\n", id) } } else { for _, name := range args { builder, err := openBuilder(getContext(), store, name) if err != nil { lastError = util.WriteError(os.Stderr, fmt.Errorf("%s %q: %w", delContainerErrStr, name, err), lastError) continue } id := builder.ContainerID if err = builder.Delete(); err != nil { lastError = util.WriteError(os.Stderr, fmt.Errorf("%s %q: %w", delContainerErrStr, name, err), lastError) continue } fmt.Printf("%s\n", id) } } return lastError } ================================================ FILE: cmd/buildah/rmi.go ================================================ package main import ( "context" "errors" "fmt" buildahcli "github.com/containers/buildah/pkg/cli" "github.com/containers/buildah/pkg/parse" "github.com/hashicorp/go-multierror" "github.com/spf13/cobra" "go.podman.io/common/libimage" ) type rmiOptions struct { all bool prune bool force bool } func rmiInit() { var ( rmiDescription = "\n Removes one or more locally stored images." opts rmiOptions ) rmiCommand := &cobra.Command{ Use: "rmi", Short: "Remove one or more images from local storage", Long: rmiDescription, RunE: func(cmd *cobra.Command, args []string) error { return rmiCmd(cmd, args, opts) }, Example: `buildah rmi imageID buildah rmi --all --force buildah rmi imageID1 imageID2 imageID3`, } rmiCommand.SetUsageTemplate(UsageTemplate()) flags := rmiCommand.Flags() flags.SetInterspersed(false) flags.BoolVarP(&opts.all, "all", "a", false, "remove all images") flags.BoolVarP(&opts.prune, "prune", "p", false, "prune dangling images") flags.BoolVarP(&opts.force, "force", "f", false, "force removal of the image and any containers using the image") rootCmd.AddCommand(rmiCommand) } func rmiCmd(c *cobra.Command, args []string, iopts rmiOptions) error { if len(args) == 0 && !iopts.all && !iopts.prune { return errors.New("image name or ID must be specified") } if len(args) > 0 && iopts.all { return errors.New("when using the --all switch, you may not pass any images names or IDs") } if iopts.all && iopts.prune { return errors.New("when using the --all switch, you may not use --prune switch") } if len(args) > 0 && iopts.prune { return errors.New("when using the --prune switch, you may not pass any images names or IDs") } if err := buildahcli.VerifyFlagsArgsOrder(args); err != nil { return err } store, err := getStore(c) if err != nil { return err } systemContext, err := parse.SystemContextFromOptions(c) if err != nil { return err } runtime, err := libimage.RuntimeFromStore(store, &libimage.RuntimeOptions{SystemContext: systemContext}) if err != nil { return err } options := &libimage.RemoveImagesOptions{ Filters: []string{"readonly=false"}, } if iopts.prune { options.Filters = append(options.Filters, "dangling=true") } else if !iopts.all { options.Filters = append(options.Filters, "intermediate=false") } options.Force = iopts.force rmiReports, rmiErrors := runtime.RemoveImages(context.Background(), args, options) for _, r := range rmiReports { for _, u := range r.Untagged { fmt.Printf("untagged: %s\n", u) } } for _, r := range rmiReports { if r.Removed { fmt.Printf("%s\n", r.ID) } } var multiE *multierror.Error multiE = multierror.Append(multiE, rmiErrors...) return multiE.ErrorOrNil() } ================================================ FILE: cmd/buildah/rpc.go ================================================ package main import ( "os" "os/exec" "path/filepath" "slices" "github.com/containers/buildah/internal/rpc/listen" "github.com/containers/buildah/internal/rpc/noop" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" "google.golang.org/grpc" "google.golang.org/grpc/reflection" ) var ( rpcDescription = "\n Runs a command with a rudimentary RPC server available." rpcCommand = &cobra.Command{ Use: "rpc", Short: "Run a command with a rudimentary RPC server available", Long: rpcDescription, RunE: rpcCmd, Hidden: true, Example: `buildah rpc [-e|--env NAME] [-l|--listen PATH] command ...`, Args: cobra.MinimumNArgs(1), } ) func rpcCmd(c *cobra.Command, args []string) error { store, err := getStore(c) if err != nil { return err } socketPath := c.Flag("listen").Value.String() if socketPath == "" { socketDir, err := os.MkdirTemp(store.RunRoot(), "buildah-socket") if err != nil { return err } defer func() { if err := os.RemoveAll(socketDir); err != nil { logrus.Errorf("removing %s: %v", socketDir, err) } }() socketPath = filepath.Join(socketDir, "socket") } listener, cleanup, err := listen.Listen(socketPath) if err != nil { return err } logrus.Debugf("listening for rpc requests at %q", socketPath) defer func() { if err := cleanup(); err != nil { logrus.Errorf("cleaning up: %v", err) } }() s := grpc.NewServer() noop.Register(s) reflection.Register(s) var errgroup errgroup.Group errgroup.Go(func() error { if err := s.Serve(listener); err != nil { return err } return nil }) defer func() { s.GracefulStop() // closes the listening socket if err := errgroup.Wait(); err != nil { logrus.Errorf("while waiting for rpc service to shut down: %v", err) } }() cmd := exec.Command(args[0], args[1:]...) cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr envVar := c.Flag("env").Value.String() if envVar != "" { cmd.Env = append(slices.Clone(cmd.Environ()), envVar+"="+"unix://"+socketPath) } return cmd.Run() } func rpcInit() { var rpcOptions struct { envVar string listenPath string } rpcCommand.SetUsageTemplate(UsageTemplate()) flags := rpcCommand.Flags() flags.SetInterspersed(false) flags.StringVarP(&rpcOptions.envVar, "env", "e", "", "set environment `variable` to point to listening socket path") flags.StringVarP(&rpcOptions.listenPath, "listen", "l", "", "listening socket `path`") rootCmd.AddCommand(rpcCommand) } ================================================ FILE: cmd/buildah/run.go ================================================ package main import ( "errors" "fmt" "os" "strings" "github.com/containers/buildah" "github.com/containers/buildah/internal/tmpdir" "github.com/containers/buildah/internal/volumes" buildahcli "github.com/containers/buildah/pkg/cli" "github.com/containers/buildah/pkg/overlay" "github.com/containers/buildah/pkg/parse" "github.com/containers/buildah/util" "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "go.podman.io/storage/pkg/mount" ) type runInputOptions struct { addHistory bool capAdd []string capDrop []string cdiConfigDir string contextDir string devices []string env []string hostname string isolation string mounts []string runtime string runtimeFlag []string noHostname bool noHosts bool noPivot bool terminal bool volumes []string workingDir string *buildahcli.NameSpaceResults } func runInit() { var ( runDescription = "\n Runs a specified command using the container's root filesystem as a root\n filesystem, using configuration settings inherited from the container's\n image or as specified using previous calls to the config command." opts runInputOptions ) namespaceResults := buildahcli.NameSpaceResults{} runCommand := &cobra.Command{ Use: "run", Short: "Run a command inside of the container", Long: runDescription, RunE: func(cmd *cobra.Command, args []string) error { opts.NameSpaceResults = &namespaceResults return runCmd(cmd, args, opts) }, Example: `buildah run containerID -- ps -auxw buildah run --terminal containerID /bin/bash buildah run --volume /path/on/host:/path/in/container:ro,z containerID /bin/sh`, } runCommand.SetUsageTemplate(UsageTemplate()) flags := runCommand.Flags() flags.SetInterspersed(false) flags.BoolVar(&opts.addHistory, "add-history", false, "add an entry for this operation to the image's history. Use BUILDAH_HISTORY environment variable to override. (default false)") flags.StringSliceVar(&opts.capAdd, "cap-add", []string{}, "add the specified capability (default [])") flags.StringSliceVar(&opts.capDrop, "cap-drop", []string{}, "drop the specified capability (default [])") flags.StringVar(&opts.cdiConfigDir, "cdi-config-dir", "", "`directory` of CDI configuration files") _ = flags.MarkHidden("cdi-config-dir") flags.StringVar(&opts.contextDir, "contextdir", "", "context directory path") flags.StringArrayVar(&opts.devices, "device", []string{}, "additional devices to provide") flags.StringArrayVarP(&opts.env, "env", "e", []string{}, "add environment variable to be set temporarily when running command (default [])") flags.StringVar(&opts.hostname, "hostname", "", "set the hostname inside of the container") flags.StringVar(&opts.isolation, "isolation", "", "`type` of process isolation to use. Use BUILDAH_ISOLATION environment variable to override.") // Do not set a default runtime here, we'll do that later in the processing. flags.StringVar(&opts.runtime, "runtime", util.Runtime(), "`path` to an alternate OCI runtime") flags.StringSliceVar(&opts.runtimeFlag, "runtime-flag", []string{}, "add global flags for the container runtime") flags.BoolVar(&opts.noHostname, "no-hostname", false, "do not override the /etc/hostname file within the container") flags.BoolVar(&opts.noHosts, "no-hosts", false, "do not override the /etc/hosts file within the container") flags.BoolVar(&opts.noPivot, "no-pivot", false, "do not use pivot root to jail process inside rootfs") flags.BoolVarP(&opts.terminal, "terminal", "t", false, "allocate a pseudo-TTY in the container") flags.StringArrayVarP(&opts.volumes, "volume", "v", []string{}, "bind mount a host location into the container while running the command") flags.StringArrayVar(&opts.mounts, "mount", []string{}, "attach a filesystem mount to the container (default [])") flags.StringVar(&opts.workingDir, "workingdir", "", "temporarily set working directory for command (default to container's workingdir)") userFlags := getUserFlags() namespaceFlags := buildahcli.GetNameSpaceFlags(&namespaceResults) flags.AddFlagSet(&userFlags) flags.AddFlagSet(&namespaceFlags) flags.SetNormalizeFunc(buildahcli.AliasFlags) rootCmd.AddCommand(runCommand) } func runCmd(c *cobra.Command, args []string, iopts runInputOptions) error { if len(args) == 0 { return errors.New("container ID must be specified") } name := args[0] args = Tail(args) if len(args) > 0 && args[0] == "--" { args = args[1:] } if len(args) == 0 { return errors.New("command must be specified") } tmpDir, err := os.MkdirTemp(tmpdir.GetTempDir(), "buildahvolume") if err != nil { return fmt.Errorf("creating temporary directory: %w", err) } defer func() { if err := os.Remove(tmpDir); err != nil { logrus.Debugf("removing should-be-empty temporary directory %q: %v", tmpDir, err) } }() store, err := getStore(c) if err != nil { return err } builder, err := openBuilder(getContext(), store, name) if err != nil { return fmt.Errorf("reading build container %q: %w", name, err) } isolation, err := parse.IsolationOption(c.Flag("isolation").Value.String()) if err != nil { return err } runtimeFlags := []string{} for _, arg := range iopts.runtimeFlag { runtimeFlags = append(runtimeFlags, "--"+arg) } noPivot := iopts.noPivot || (os.Getenv("BUILDAH_NOPIVOT") != "") namespaceOptions, networkPolicy, err := parse.NamespaceOptions(c) if err != nil { return err } if c.Flag("network").Changed && c.Flag("isolation").Changed { if isolation == buildah.IsolationChroot { if ns := namespaceOptions.Find(string(specs.NetworkNamespace)); ns != nil { if !ns.Host { return fmt.Errorf("cannot set --network other than host with --isolation %s", c.Flag("isolation").Value.String()) } } } } options := buildah.RunOptions{ Hostname: iopts.hostname, Runtime: iopts.runtime, Args: runtimeFlags, NoHostname: iopts.noHostname, NoHosts: iopts.noHosts, NoPivot: noPivot, User: c.Flag("user").Value.String(), Isolation: isolation, NamespaceOptions: namespaceOptions, ConfigureNetwork: networkPolicy, ContextDir: iopts.contextDir, AddCapabilities: iopts.capAdd, DropCapabilities: iopts.capDrop, WorkingDir: iopts.workingDir, DeviceSpecs: iopts.devices, CDIConfigDir: iopts.cdiConfigDir, } if c.Flag("terminal").Changed { if iopts.terminal { options.Terminal = buildah.WithTerminal } else { options.Terminal = buildah.WithoutTerminal } } options.Env = buildahcli.LookupEnvVarReferences(iopts.env, os.Environ()) systemContext, err := parse.SystemContextFromOptions(c) if err != nil { return fmt.Errorf("building system context: %w", err) } mounts, mountedImages, intermediateMounts, _, targetLocks, err := volumes.GetVolumes(systemContext, store, builder.MountLabel, iopts.volumes, iopts.mounts, iopts.contextDir, builder.IDMappingOptions, iopts.workingDir, tmpDir) if err != nil { return err } defer func() { if err := overlay.CleanupContent(tmpDir); err != nil { logrus.Debugf("unmounting overlay mounts under %q: %v", tmpDir, err) } for _, intermediateMount := range intermediateMounts { if err := mount.Unmount(intermediateMount); err != nil { logrus.Debugf("unmounting mount %q: %v", intermediateMount, err) } if err := os.Remove(intermediateMount); err != nil { logrus.Debugf("removing should-be-empty mount directory %q: %v", intermediateMount, err) } } for _, mountedImage := range mountedImages { if _, err := store.UnmountImage(mountedImage, false); err != nil { logrus.Debugf("unmounting image %q: %v", mountedImage, err) } } volumes.UnlockLockArray(targetLocks) }() options.Mounts = mounts options.CgroupManager = globalFlagResults.CgroupManager runerr := builder.Run(args, options) if runerr != nil { logrus.Debugf("error running %v in container %q: %v", args, builder.Container, runerr) } if runerr == nil { shell := "/bin/sh -c" if len(builder.Shell()) > 0 { shell = strings.Join(builder.Shell(), " ") } conditionallyAddHistory(builder, c, "%s %s", shell, strings.Join(args, " ")) return builder.Save() } return runerr } ================================================ FILE: cmd/buildah/source.go ================================================ package main import ( "context" "github.com/containers/buildah/internal/source" "github.com/spf13/cobra" ) var ( // buildah source sourceDescription = ` Create, push, pull and manage source images and associated source artifacts. A source image contains all source artifacts an ordinary OCI image has been built with. Those artifacts can be any kind of source artifact, such as source RPMs, an entire source tree or text files. Note that the buildah-source command and all its subcommands are experimental and may be subject to future changes. ` sourceCommand = &cobra.Command{ Use: "source", Short: "Manage source containers", Long: sourceDescription, RunE: func(_ *cobra.Command, _ []string) error { return nil }, } // buildah source create sourceCreateDescription = ` Create and initialize a source image. A source image is an OCI artifact; an OCI image with a custom config media type. Note that the buildah-source command and all its subcommands are experimental and may be subject to future changes. ` sourceCreateOptions = source.CreateOptions{} sourceCreateCommand = &cobra.Command{ Args: cobra.ExactArgs(1), Use: "create", Short: "Create a source image", Long: sourceCreateDescription, Example: "buildah source create /tmp/fedora:latest-source", RunE: func(_ *cobra.Command, args []string) error { return source.Create(context.Background(), args[0], sourceCreateOptions) }, } // buildah source add sourceAddOptions = source.AddOptions{} sourceAddDescription = ` Add add a source artifact to a source image. The artifact will be added as a gzip-compressed tar ball. Add attempts to auto-tar and auto-compress only if necessary. Note that the buildah-source command and all its subcommands are experimental and may be subject to future changes. ` sourceAddCommand = &cobra.Command{ Args: cobra.ExactArgs(2), Use: "add", Short: "Add a source artifact to a source image", Long: sourceAddDescription, Example: "buildah source add /tmp/fedora sources.tar.gz", RunE: func(_ *cobra.Command, args []string) error { return source.Add(context.Background(), args[0], args[1], sourceAddOptions) }, } // buildah source pull sourcePullOptions = source.PullOptions{} sourcePullDescription = ` Pull a source image from a registry to a specified path. The pull operation will fail if the image does not comply with a source-image OCI artifact. Note that the buildah-source command and all its subcommands are experimental and may be subject to future changes. ` sourcePullCommand = &cobra.Command{ Args: cobra.ExactArgs(2), Use: "pull", Short: "Pull a source image from a registry to a specified path", Long: sourcePullDescription, Example: "buildah source pull quay.io/sourceimage/example:latest /tmp/sourceimage:latest", RunE: func(_ *cobra.Command, args []string) error { return source.Pull(context.Background(), args[0], args[1], sourcePullOptions) }, } // buildah source push sourcePushOptions = source.PushOptions{} sourcePushDescription = ` Push a source image from a specified path to a registry. Note that the buildah-source command and all its subcommands are experimental and may be subject to future changes. ` sourcePushCommand = &cobra.Command{ Args: cobra.ExactArgs(2), Use: "push", Short: "Push a source image from a specified path to a registry", Long: sourcePushDescription, Example: "buildah source push /tmp/sourceimage:latest quay.io/sourceimage/example:latest", RunE: func(_ *cobra.Command, args []string) error { return source.Push(context.Background(), args[0], args[1], sourcePushOptions) }, } ) func sourceInit() { // buildah source sourceCommand.SetUsageTemplate(UsageTemplate()) rootCmd.AddCommand(sourceCommand) // buildah source create sourceCreateCommand.SetUsageTemplate(UsageTemplate()) sourceCommand.AddCommand(sourceCreateCommand) sourceCreateFlags := sourceCreateCommand.Flags() sourceCreateFlags.StringVar(&sourceCreateOptions.Author, "author", "", "set the author") sourceCreateFlags.BoolVar(&sourceCreateOptions.TimeStamp, "time-stamp", true, "set the \"created\" time stamp") // buildah source add sourceAddCommand.SetUsageTemplate(UsageTemplate()) sourceCommand.AddCommand(sourceAddCommand) sourceAddFlags := sourceAddCommand.Flags() sourceAddFlags.StringArrayVar(&sourceAddOptions.Annotations, "annotation", []string{}, "add an annotation (format: key=value)") // buildah source pull sourcePullCommand.SetUsageTemplate(UsageTemplate()) sourceCommand.AddCommand(sourcePullCommand) sourcePullFlags := sourcePullCommand.Flags() sourcePullFlags.StringVar(&sourcePullOptions.Credentials, "creds", "", "use `[username[:password]]` for accessing the registry") sourcePullFlags.BoolVar(&sourcePullOptions.TLSVerify, "tls-verify", true, "require HTTPS and verify certificates when accessing the registry") sourcePullFlags.BoolVarP(&sourcePullOptions.Quiet, "quiet", "q", false, "don't output pull progress information") // buildah source push sourcePushCommand.SetUsageTemplate(UsageTemplate()) sourceCommand.AddCommand(sourcePushCommand) sourcePushFlags := sourcePushCommand.Flags() sourcePushFlags.StringVar(&sourcePushOptions.Credentials, "creds", "", "use `[username[:password]]` for accessing the registry") sourcePushFlags.StringVar(&sourcePushOptions.DigestFile, "digestfile", "", "after copying the artifact, write the digest of the resulting image to the file") sourcePushFlags.BoolVar(&sourcePushOptions.TLSVerify, "tls-verify", true, "require HTTPS and verify certificates when accessing the registry") sourcePushFlags.BoolVarP(&sourcePushOptions.Quiet, "quiet", "q", false, "don't output push progress information") } ================================================ FILE: cmd/buildah/tag.go ================================================ package main import ( "fmt" "github.com/containers/buildah/pkg/parse" "github.com/spf13/cobra" "go.podman.io/common/libimage" ) var ( tagDescription = "\n Adds one or more additional names to locally-stored image." tagCommand = &cobra.Command{ Use: "tag", Short: "Add an additional name to a local image", Long: tagDescription, RunE: tagCmd, Example: `buildah tag imageName firstNewName buildah tag imageName firstNewName SecondNewName`, Args: cobra.MinimumNArgs(2), } ) func tagCmd(c *cobra.Command, args []string) error { store, err := getStore(c) if err != nil { return err } systemContext, err := parse.SystemContextFromOptions(c) if err != nil { return fmt.Errorf("building system context: %w", err) } runtime, err := libimage.RuntimeFromStore(store, &libimage.RuntimeOptions{SystemContext: systemContext}) if err != nil { return err } // Allow tagging manifest list instead of resolving instances from manifest lookupOptions := &libimage.LookupImageOptions{ManifestList: true} image, _, err := runtime.LookupImage(args[0], lookupOptions) if err != nil { return err } for _, tag := range args[1:] { if err := image.Tag(tag); err != nil { return err } } return nil } func tagInit() { tagCommand.SetUsageTemplate(UsageTemplate()) rootCmd.AddCommand(tagCommand) } ================================================ FILE: cmd/buildah/umount.go ================================================ package main import ( "errors" "fmt" "os" buildahcli "github.com/containers/buildah/pkg/cli" "github.com/spf13/cobra" ) func umountInit() { umountCommand := &cobra.Command{ Use: "umount", Aliases: []string{"unmount"}, Short: "Unmount the root file system of the specified working containers", Long: "Unmounts the root file system of the specified working containers.", RunE: umountCmd, Example: `buildah umount containerID buildah umount containerID1 containerID2 containerID3 buildah umount --all`, } umountCommand.SetUsageTemplate(UsageTemplate()) flags := umountCommand.Flags() flags.SetInterspersed(false) flags.BoolP("all", "a", false, "umount all of the currently mounted containers") rootCmd.AddCommand(umountCommand) } func umountCmd(c *cobra.Command, args []string) error { umountAll := c.Flag("all").Changed umountContainerErrStr := "error unmounting container" if len(args) == 0 && !umountAll { return errors.New("at least one container ID must be specified") } if len(args) > 0 && umountAll { return errors.New("when using the --all switch, you may not pass any container IDs") } if err := buildahcli.VerifyFlagsArgsOrder(args); err != nil { return err } store, err := getStore(c) if err != nil { return err } var lastError error if len(args) > 0 { for _, name := range args { builder, err := openBuilder(getContext(), store, name) if err != nil { if lastError != nil { fmt.Fprintln(os.Stderr, lastError) } lastError = fmt.Errorf("%s %s: %w", umountContainerErrStr, name, err) continue } if builder.MountPoint == "" { continue } if err = builder.Unmount(); err != nil { if lastError != nil { fmt.Fprintln(os.Stderr, lastError) } lastError = fmt.Errorf("%s %q: %w", umountContainerErrStr, builder.Container, err) continue } fmt.Printf("%s\n", builder.ContainerID) } } else { builders, err := openBuilders(store) if err != nil { return fmt.Errorf("reading build Containers: %w", err) } for _, builder := range builders { if builder.MountPoint == "" { continue } if err = builder.Unmount(); err != nil { if lastError != nil { fmt.Fprintln(os.Stderr, lastError) } lastError = fmt.Errorf("%s %q: %w", umountContainerErrStr, builder.Container, err) continue } fmt.Printf("%s\n", builder.ContainerID) } } return lastError } ================================================ FILE: cmd/buildah/unshare.go ================================================ //go:build linux package main import ( "fmt" "os" "os/exec" "sort" "strings" "github.com/moby/sys/capability" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "go.podman.io/storage" "go.podman.io/storage/pkg/unshare" ) var ( unshareDescription = "\n Runs a command in a modified user namespace." unshareCommand = &cobra.Command{ Use: "unshare", Short: "Run a command in a modified user namespace", Long: unshareDescription, RunE: unshareCmd, Example: `buildah unshare id buildah unshare cat /proc/self/uid_map buildah unshare buildah-script.sh`, } unshareMounts []string ) func unshareInit() { unshareCommand.SetUsageTemplate(UsageTemplate()) flags := unshareCommand.Flags() flags.SetInterspersed(false) flags.StringSliceVarP(&unshareMounts, "mount", "m", []string{}, "mount the specified containers (default [])") rootCmd.AddCommand(unshareCommand) } func unshareMount(c *cobra.Command, mounts []string) ([]string, func(), error) { var store storage.Store var mountedContainers, env []string if len(mounts) == 0 { return nil, nil, nil } unmount := func() { for _, mounted := range mountedContainers { builder, err := openBuilder(getContext(), store, mounted) if err != nil { fmt.Fprintln(os.Stderr, fmt.Errorf("loading information about build container %q: %w", mounted, err)) continue } err = builder.Unmount() if err != nil { fmt.Fprintln(os.Stderr, fmt.Errorf("unmounting build container %q: %w", mounted, err)) continue } } } store, err := getStore(c) if err != nil { return nil, nil, err } for _, mountSpec := range mounts { mount := strings.SplitN(mountSpec, "=", 2) container := mountSpec envVar := container if len(mount) == 2 { envVar = mount[0] container = mount[1] } builder, err := openBuilder(getContext(), store, container) if err != nil { unmount() return nil, nil, fmt.Errorf("loading information about build container %q: %w", container, err) } mountPoint, err := builder.Mount(builder.MountLabel) if err != nil { unmount() return nil, nil, fmt.Errorf("mounting build container %q: %w", container, err) } logrus.Debugf("mounted container %q at %q", container, mountPoint) mountedContainers = append(mountedContainers, container) if envVar != "" { envSpec := fmt.Sprintf("%s=%s", envVar, mountPoint) logrus.Debugf("adding %q to environment", envSpec) env = append(env, envSpec) } } return env, unmount, nil } // unshareCmd execs whatever using the ID mappings that we want to use for ourselves func unshareCmd(c *cobra.Command, args []string) error { // Set the default isolation type to use the "rootless" method. if _, present := os.LookupEnv("BUILDAH_ISOLATION"); !present { if err := os.Setenv("BUILDAH_ISOLATION", "rootless"); err != nil { logrus.Errorf("error setting BUILDAH_ISOLATION=rootless in environment: %v", err) os.Exit(1) } } // force reexec using the configured ID mappings unshare.MaybeReexecUsingUserNamespace(true) // exec the specified command, if there is one if len(args) < 1 { // try to exec the shell, if one's set shell, shellSet := os.LookupEnv("SHELL") if !shellSet { logrus.Errorf("no command specified") os.Exit(1) } args = []string{shell} } cmd := exec.Command(args[0], args[1:]...) cmd.Env = unshare.RootlessEnv() cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr mountEnvs, unmountMounts, err := unshareMount(c, unshareMounts) if err != nil { return err } cmd.Env = append(cmd.Env, mountEnvs...) unshare.ExecRunnable(cmd, unmountMounts) os.Exit(1) return nil } func debugCapabilities() { pid, err := capability.NewPid2(0) if err != nil { logrus.Errorf("error checking our capabilities: %v", err) return } if err := pid.Load(); err != nil { logrus.Errorf("error loading our current capabilities: %v", err) return } knownCaps := capability.ListKnown() effective := make([]string, 0, len(knownCaps)) for i := range knownCaps { have := pid.Get(capability.EFFECTIVE, knownCaps[i]) effective = append(effective, fmt.Sprintf("%s=%v", knownCaps[i].String(), have)) } sort.Strings(effective) logrus.Debugf("effective capabilities: %v", effective) } ================================================ FILE: cmd/buildah/unshare_unsupported.go ================================================ //go:build !linux package main import ( "github.com/spf13/cobra" ) func unshareInit() { unshareCommand := cobra.Command{ Use: "unshare", Hidden: true, RunE: func(cmd *cobra.Command, args []string) error { return nil }, } rootCmd.AddCommand(&unshareCommand) } func debugCapabilities() { } ================================================ FILE: cmd/buildah/version.go ================================================ package main import ( "encoding/json" "fmt" "runtime" "strconv" "time" "github.com/containerd/platforms" "github.com/containers/buildah/define" ispecs "github.com/opencontainers/image-spec/specs-go" rspecs "github.com/opencontainers/runtime-spec/specs-go" "github.com/spf13/cobra" iversion "go.podman.io/image/v5/version" ) // Overwritten at build time var ( GitCommit string buildInfo string ) type versionInfo struct { Version string `json:"version"` GoVersion string `json:"goVersion"` ImageSpec string `json:"imageSpec"` RuntimeSpec string `json:"runtimeSpec"` ImageVersion string `json:"imageVersion"` GitCommit string `json:"gitCommit"` Built string `json:"built"` OsArch string `json:"osArch"` BuildPlatform string `json:"buildPlatform"` } type versionOptions struct { json bool } func versionInit() { var opts versionOptions // cli command to print out the version info of buildah versionCommand := &cobra.Command{ Use: "version", Short: "Display the Buildah version information", Long: "Displays Buildah version information.", RunE: func(_ *cobra.Command, _ []string) error { return versionCmd(opts) }, Args: cobra.NoArgs, Example: `buildah version`, } versionCommand.SetUsageTemplate(UsageTemplate()) flags := versionCommand.Flags() flags.BoolVar(&opts.json, "json", false, "output in JSON format") rootCmd.AddCommand(versionCommand) } func versionCmd(opts versionOptions) error { var err error buildTime := int64(0) if buildInfo != "" { // converting unix time from string to int64 buildTime, err = strconv.ParseInt(buildInfo, 10, 64) if err != nil { return err } } version := versionInfo{ Version: define.Version, GoVersion: runtime.Version(), ImageSpec: ispecs.Version, RuntimeSpec: rspecs.Version, ImageVersion: iversion.Version, GitCommit: GitCommit, Built: time.Unix(buildTime, 0).Format(time.ANSIC), OsArch: runtime.GOOS + "/" + runtime.GOARCH, BuildPlatform: platforms.DefaultString(), } if opts.json { data, err := json.MarshalIndent(version, "", " ") if err != nil { return err } fmt.Printf("%s\n", data) return nil } fmt.Println("Version: ", version.Version) fmt.Println("Go Version: ", version.GoVersion) fmt.Println("Image Spec: ", version.ImageSpec) fmt.Println("Runtime Spec: ", version.RuntimeSpec) fmt.Println("image Version: ", version.ImageVersion) fmt.Println("Git Commit: ", version.GitCommit) // Prints out the build time in readable format fmt.Println("Built: ", version.Built) fmt.Println("OS/Arch: ", version.OsArch) fmt.Println("BuildPlatform: ", version.BuildPlatform) return nil } ================================================ FILE: commit.go ================================================ package buildah import ( "context" "encoding/json" "fmt" "io" "maps" "os" "strings" "time" "github.com/containers/buildah/internal/metadata" "github.com/containers/buildah/pkg/blobcache" "github.com/containers/buildah/util" encconfig "github.com/containers/ocicrypt/config" digest "github.com/opencontainers/go-digest" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" "go.podman.io/common/libimage" "go.podman.io/common/libimage/manifests" "go.podman.io/image/v5/docker" "go.podman.io/image/v5/docker/reference" "go.podman.io/image/v5/manifest" "go.podman.io/image/v5/signature" is "go.podman.io/image/v5/storage" "go.podman.io/image/v5/transports" "go.podman.io/image/v5/types" "go.podman.io/storage/pkg/archive" "go.podman.io/storage/pkg/stringid" ) const ( // BuilderIdentityAnnotation is the name of the label which will be set // to contain the name and version of the producer of the image at // commit-time. (N.B. yes, the constant's name includes "Annotation", // but it's added as a label.) BuilderIdentityAnnotation = "io.buildah.version" ) // CommitOptions can be used to alter how an image is committed. type CommitOptions struct { // PreferredManifestType is the preferred type of image manifest. The // image configuration format will be of a compatible type. PreferredManifestType string // Compression specifies the type of compression which is applied to // layer blobs. The default is to not use compression, but // archive.Gzip is recommended. Compression archive.Compression // SignaturePolicyPath specifies an override location for the signature // policy which should be used for verifying the new image as it is // being written. Except in specific circumstances, no value should be // specified, indicating that the shared, system-wide default policy // should be used. SignaturePolicyPath string // AdditionalTags is a list of additional names to add to the image, if // the transport to which we're writing the image gives us a way to add // them. AdditionalTags []string // ReportWriter is an io.Writer which will be used to log the writing // of the new image. ReportWriter io.Writer // HistoryTimestamp specifies a timestamp to use for the image's // created-on date, the corresponding field in new history entries, and // the timestamps to set on contents in new layer diffs. If left // unset, the current time is used for the configuration and manifest, // and timestamps of layer contents are used as-is. HistoryTimestamp *time.Time // SourceDateEpoch specifies a timestamp to use for the image's // created-on date and the corresponding field in new history entries. // If left unset, the current time is used for the configuration and // manifest. SourceDateEpoch *time.Time // RewriteTimestamp, if set, forces timestamps in generated layers to // not be later than the SourceDateEpoch, if it is set. RewriteTimestamp bool // github.com/containers/image/types SystemContext to hold credentials // and other authentication/authorization information. SystemContext *types.SystemContext // IIDFile tells the builder to write the image's ID, preceded by // "sha256:", to the specified file. IIDFile string // Squash tells the builder to produce an image with a single layer // instead of with possibly more than one layer. Squash bool // OmitHistory tells the builder to ignore the history of build layers and // base while preparing image-spec, setting this to true will ensure no history // is added to the image-spec. (default false) OmitHistory bool // BlobDirectory is the name of a directory in which we'll look for // prebuilt copies of layer blobs that we might otherwise need to // regenerate from on-disk layers. If blobs are available, the // manifest of the new image will reference the blobs rather than // on-disk layers. BlobDirectory string // EmptyLayer tells the builder to omit the diff for the working // container. EmptyLayer bool // OmitLayerHistoryEntry tells the builder to omit the diff for the // working container and to not add an entry in the commit history. By // default, the rest of the image's history is preserved, subject to // the OmitHistory setting. N.B.: setting this flag, without any // PrependedEmptyLayers, AppendedEmptyLayers, PrependedLinkedLayers, or // AppendedLinkedLayers will more or less produce a copy of the base // image. OmitLayerHistoryEntry bool // OmitTimestamp forces epoch 0 as created timestamp to allow for // deterministic, content-addressable builds. // Deprecated: use HistoryTimestamp or SourceDateEpoch (possibly with // RewriteTimestamp) instead. OmitTimestamp bool // SignBy is the fingerprint of a GPG key to use for signing the image. SignBy string // Manifest list to add the image to. Manifest string // MaxRetries is the maximum number of attempts we'll make to commit // the image to an external registry if the first attempt fails. MaxRetries int // RetryDelay is how long to wait before retrying a commit attempt to a // registry. RetryDelay time.Duration // OciEncryptConfig when non-nil indicates that an image should be encrypted. // The encryption options is derived from the construction of EncryptConfig object. OciEncryptConfig *encconfig.EncryptConfig // OciEncryptLayers represents the list of layers to encrypt. // If nil, don't encrypt any layers. // If non-nil and len==0, denotes encrypt all layers. // integers in the slice represent 0-indexed layer indices, with support for negative // indexing. i.e. 0 is the first layer, -1 is the last (top-most) layer. OciEncryptLayers *[]int // ConfidentialWorkloadOptions is used to force the output image's rootfs to contain a // LUKS-compatibly encrypted disk image (for use with krun) instead of the usual // contents of a rootfs. ConfidentialWorkloadOptions ConfidentialWorkloadOptions // UnsetEnvs is a list of environments to not add to final image. // Deprecated: use UnsetEnv() before committing, or set OverrideChanges // instead. UnsetEnvs []string // OverrideConfig is an optional Schema2Config which can override parts // of the working container's configuration for the image that is being // committed. OverrideConfig *manifest.Schema2Config // OverrideChanges is a slice of Dockerfile-style instructions to make // to the configuration of the image that is being committed, after // OverrideConfig is applied. OverrideChanges []string // ExtraImageContent is a map which describes additional content to add // to the new layer in the committed image. The map's keys are // filesystem paths in the image and the corresponding values are the // paths of files whose contents will be used in their place. The // contents will be owned by 0:0 and have mode 0o644. Currently only // accepts regular files. ExtraImageContent map[string]string // SBOMScanOptions encapsulates options which control whether or not we // run scanners on the rootfs that we're about to commit, and how. SBOMScanOptions []SBOMScanOptions // CompatSetParent causes the "parent" field to be set when committing // the image in Docker format. Newer BuildKit-based builds don't set // this field. CompatSetParent types.OptionalBool // CompatLayerOmissions causes the "/dev", "/proc", and "/sys" // directories to be omitted from the layer diff and related output, as // the classic builder did. Newer BuildKit-based builds include them // in the built image by default. CompatLayerOmissions types.OptionalBool // PrependedLinkedLayers and AppendedLinkedLayers are combinations of // history entries and locations of either directory trees (if // directories, per os.Stat()) or uncompressed layer blobs which should // be added to the image at commit-time. The order of these relative // to PrependedEmptyLayers and AppendedEmptyLayers, and relative to the // corresponding members in the Builder object, in the committed image // is not guaranteed. PrependedLinkedLayers, AppendedLinkedLayers []LinkedLayer // UnsetAnnotations is a list of annotations (names only) to withhold // from the image. UnsetAnnotations []string // Annotations is a list of annotations (in the form "key=value") to // add to the image. Annotations []string // CreatedAnnotation controls whether or not an "org.opencontainers.image.created" // annotation is present in the output image. CreatedAnnotation types.OptionalBool } // LinkedLayer combines a history entry with the location of either a directory // tree (if it's a directory, per os.Stat()) or an uncompressed layer blob // which should be added to the image at commit-time. The BlobPath and // History.EmptyLayer fields should be considered mutually-exclusive. type LinkedLayer struct { History v1.History // history entry to add BlobPath string // corresponding uncompressed blob file (layer as a tar archive), or directory tree to archive } // storageAllowedPolicyScopes overrides the policy for local storage // to ensure that we can read images from it. var storageAllowedPolicyScopes = signature.PolicyTransportScopes{ "": []signature.PolicyRequirement{ signature.NewPRInsecureAcceptAnything(), }, } // checkRegistrySourcesAllows checks the $BUILD_REGISTRY_SOURCES environment // variable, if it's set. The contents are expected to be a JSON-encoded // github.com/openshift/api/config/v1.Image, set by an OpenShift build // controller that arranged for us to be run in a container. func checkRegistrySourcesAllows(forWhat string, dest types.ImageReference) (insecure bool, err error) { transport := dest.Transport() if transport == nil { return false, nil } if transport.Name() != docker.Transport.Name() { return false, nil } dref := dest.DockerReference() if dref == nil || reference.Domain(dref) == "" { return false, nil } if registrySources, ok := os.LookupEnv("BUILD_REGISTRY_SOURCES"); ok && len(registrySources) > 0 { // Use local struct instead of github.com/openshift/api/config/v1 RegistrySources var sources struct { InsecureRegistries []string `json:"insecureRegistries,omitempty"` BlockedRegistries []string `json:"blockedRegistries,omitempty"` AllowedRegistries []string `json:"allowedRegistries,omitempty"` } if err := json.Unmarshal([]byte(registrySources), &sources); err != nil { return false, fmt.Errorf("parsing $BUILD_REGISTRY_SOURCES (%q) as JSON: %w", registrySources, err) } blocked := false if len(sources.BlockedRegistries) > 0 { for _, blockedDomain := range sources.BlockedRegistries { if blockedDomain == reference.Domain(dref) { blocked = true } } } if blocked { return false, fmt.Errorf("%s registry at %q denied by policy: it is in the blocked registries list", forWhat, reference.Domain(dref)) } allowed := true if len(sources.AllowedRegistries) > 0 { allowed = false for _, allowedDomain := range sources.AllowedRegistries { if allowedDomain == reference.Domain(dref) { allowed = true } } } if !allowed { return false, fmt.Errorf("%s registry at %q denied by policy: not in allowed registries list", forWhat, reference.Domain(dref)) } if len(sources.InsecureRegistries) > 0 { return true, nil } } return false, nil } func (b *Builder) addManifest(ctx context.Context, manifestName string, imageSpec string) (string, error) { var create bool systemContext := &types.SystemContext{} var list manifests.List runtime, err := libimage.RuntimeFromStore(b.store, &libimage.RuntimeOptions{SystemContext: systemContext}) if err != nil { return "", err } manifestList, err := runtime.LookupManifestList(manifestName) if err != nil { create = true list = manifests.Create() } else { locker, err := manifests.LockerForImage(b.store, manifestList.ID()) if err != nil { return "", err } locker.Lock() defer locker.Unlock() _, list, err = manifests.LoadFromImage(b.store, manifestList.ID()) if err != nil { return "", err } } names, err := util.ExpandNames([]string{manifestName}, systemContext, b.store) if err != nil { return "", fmt.Errorf("encountered while expanding manifest list name %q: %w", manifestName, err) } ref, err := util.VerifyTagName(imageSpec) if err != nil { // check if the local image exists if ref, _, err = util.FindImage(b.store, "", systemContext, imageSpec); err != nil { return "", err } } if _, err = list.Add(ctx, systemContext, ref, true); err != nil { return "", err } var imageID string if create { imageID, err = list.SaveToImage(b.store, "", names, manifest.DockerV2ListMediaType) } else { imageID, err = list.SaveToImage(b.store, manifestList.ID(), nil, "") } return imageID, err } // CommitResults is a structure returned when CommitResults() succeeds. type CommitResults struct { ImageID string // a local image ID, or part of the digest of the image's config blob Canonical reference.Canonical // set if destination included a DockerReference MediaType string // image manifest MIME type, always returned ImageManifest []byte // raw image manifest, always returned Digest digest.Digest // digest of the manifest, always returned Metadata map[string]any // always returned, format is flexible } // Commit writes the contents of the container, along with its updated // configuration, to a new image in the specified location, and if we know how, // add any additional tags that were specified. Returns the ID of the new image // if commit was successful and the image destination was local, a canonical // reference if the destination ImageReference include a DockerReference, and // the digest of the written image's manifest. // Commit() is implemented as a wrapper around CommitResults(). func (b *Builder) Commit(ctx context.Context, dest types.ImageReference, options CommitOptions) (string, reference.Canonical, digest.Digest, error) { results, err := b.CommitResults(ctx, dest, options) if err != nil { return "", nil, "", err } return results.ImageID, results.Canonical, results.Digest, nil } // CommitResults writes the contents of the container, along with its updated // configuration, to a new image in the specified location, and if we know how, // add any additional tags that were specified. Returns a CommitResults // structure. func (b *Builder) CommitResults(ctx context.Context, dest types.ImageReference, options CommitOptions) (*CommitResults, error) { var ( imgID string src types.ImageReference destinationTimestamp *time.Time ) // If we weren't given a name, build a destination reference using a // temporary name that we'll remove later. The correct thing to do // would be to read the manifest and configuration blob, and ask the // manifest for the ID that we'd give the image, but that computation // requires that we know the digests of the layer blobs, which we don't // want to compute here because we'll have to do it again when // cp.Image() instantiates a source image, and we don't want to do the // work twice. if options.OmitTimestamp { if options.HistoryTimestamp != nil { return nil, fmt.Errorf("OmitTimestamp and HistoryTimestamp can not be used together") } timestamp := time.Unix(0, 0).UTC() options.HistoryTimestamp = ×tamp } destinationTimestamp = options.HistoryTimestamp if options.SourceDateEpoch != nil { destinationTimestamp = options.SourceDateEpoch } nameToRemove := "" if dest == nil { nameToRemove = stringid.GenerateRandomID() + "-tmp" dest2, err := is.Transport.ParseStoreReference(b.store, nameToRemove) if err != nil { return nil, fmt.Errorf("creating temporary destination reference for image: %w", err) } dest = dest2 } systemContext := getSystemContext(b.store, options.SystemContext, options.SignaturePolicyPath) blocked, err := isReferenceBlocked(dest, systemContext) if err != nil { return nil, fmt.Errorf("checking if committing to registry for %q is blocked: %w", transports.ImageName(dest), err) } if blocked { return nil, fmt.Errorf("commit access to registry for %q is blocked by configuration", transports.ImageName(dest)) } // Load the system signing policy. commitPolicy, err := signature.DefaultPolicy(systemContext) if err != nil { return nil, fmt.Errorf("obtaining default signature policy: %w", err) } // Override the settings for local storage to make sure that we can always read the source "image". commitPolicy.Transports[is.Transport.Name()] = storageAllowedPolicyScopes policyContext, err := signature.NewPolicyContext(commitPolicy) if err != nil { return nil, fmt.Errorf("creating new signature policy context: %w", err) } defer func() { if err2 := policyContext.Destroy(); err2 != nil { logrus.Debugf("error destroying signature policy context: %v", err2) } }() // Check if the commit is blocked by $BUILDER_REGISTRY_SOURCES. insecure, err := checkRegistrySourcesAllows("commit to", dest) if err != nil { return nil, err } if insecure { if systemContext.DockerInsecureSkipTLSVerify == types.OptionalBoolFalse { return nil, fmt.Errorf("can't require tls verification on an insecured registry") } systemContext.DockerInsecureSkipTLSVerify = types.OptionalBoolTrue systemContext.OCIInsecureSkipTLSVerify = true systemContext.DockerDaemonInsecureSkipTLSVerify = true } logrus.Debugf("committing image with reference %q is allowed by policy", transports.ImageName(dest)) // If we need to scan the rootfs, do it now. options.ExtraImageContent = maps.Clone(options.ExtraImageContent) var extraImageContent, extraLocalContent map[string]string if len(options.SBOMScanOptions) != 0 { var scansDirectory string if extraImageContent, extraLocalContent, scansDirectory, err = b.sbomScan(ctx, options); err != nil { return nil, fmt.Errorf("scanning rootfs to generate SBOM for container %q: %w", b.ContainerID, err) } if scansDirectory != "" { defer func() { if err := os.RemoveAll(scansDirectory); err != nil { logrus.Warnf("removing temporary directory %q: %v", scansDirectory, err) } }() } if len(extraImageContent) > 0 { if options.ExtraImageContent == nil { options.ExtraImageContent = make(map[string]string, len(extraImageContent)) } // merge in the scanner-generated content for k, v := range extraImageContent { if _, set := options.ExtraImageContent[k]; !set { options.ExtraImageContent[k] = v } } } } // Build an image reference from which we can copy the finished image. src, err = b.makeContainerImageRef(options) if err != nil { return nil, fmt.Errorf("computing layer digests and building metadata for container %q: %w", b.ContainerID, err) } // In case we're using caching, decide how to handle compression for a cache. // If we're using blob caching, set it up for the source. maybeCachedSrc := src maybeCachedDest := dest if options.BlobDirectory != "" { compress := types.PreserveOriginal if options.Compression != archive.Uncompressed { compress = types.Compress } cache, err := blobcache.NewBlobCache(src, options.BlobDirectory, compress) if err != nil { return nil, fmt.Errorf("wrapping image reference %q in blob cache at %q: %w", transports.ImageName(src), options.BlobDirectory, err) } maybeCachedSrc = cache cache, err = blobcache.NewBlobCache(dest, options.BlobDirectory, compress) if err != nil { return nil, fmt.Errorf("wrapping image reference %q in blob cache at %q: %w", transports.ImageName(dest), options.BlobDirectory, err) } maybeCachedDest = cache } // "Copy" our image to where it needs to be. switch options.Compression { case archive.Uncompressed: systemContext.OCIAcceptUncompressedLayers = true case archive.Gzip: systemContext.DirForceCompress = true } if systemContext.ArchitectureChoice != b.Architecture() { systemContext.ArchitectureChoice = b.Architecture() } if systemContext.OSChoice != b.OS() { systemContext.OSChoice = b.OS() } var manifestBytes []byte if manifestBytes, err = retryCopyImage(ctx, policyContext, maybeCachedDest, maybeCachedSrc, dest, getCopyOptions(b.store, options.ReportWriter, nil, systemContext, "", false, options.SignBy, options.OciEncryptLayers, options.OciEncryptConfig, nil, destinationTimestamp), options.MaxRetries, options.RetryDelay); err != nil { return nil, fmt.Errorf("copying layers and metadata for container %q: %w", b.ContainerID, err) } // If we've got more names to attach, and we know how to do that for // the transport that we're writing the new image to, add them now. if len(options.AdditionalTags) > 0 { switch dest.Transport().Name() { case is.Transport.Name(): _, img, err := is.ResolveReference(dest) if err != nil { return nil, fmt.Errorf("locating just-written image %q: %w", transports.ImageName(dest), err) } if err = util.AddImageNames(b.store, "", systemContext, img, options.AdditionalTags); err != nil { return nil, fmt.Errorf("setting image names to %v: %w", append(img.Names, options.AdditionalTags...), err) } logrus.Debugf("assigned names %v to image %q", img.Names, img.ID) default: logrus.Warnf("don't know how to add tags to images stored in %q transport", dest.Transport().Name()) } } if dest.Transport().Name() == is.Transport.Name() { dest2, img, err := is.ResolveReference(dest) if err != nil { return nil, fmt.Errorf("locating image %q in local storage: %w", transports.ImageName(dest), err) } dest = dest2 imgID = img.ID toPruneNames := make([]string, 0, len(img.Names)) for _, name := range img.Names { if nameToRemove != "" && strings.Contains(name, nameToRemove) { toPruneNames = append(toPruneNames, name) } } if len(toPruneNames) > 0 { if err = b.store.RemoveNames(imgID, toPruneNames); err != nil { return nil, fmt.Errorf("failed to remove temporary name from image %q: %w", imgID, err) } logrus.Debugf("removing %v from assigned names to image %q", nameToRemove, img.ID) } if options.IIDFile != "" { if err = os.WriteFile(options.IIDFile, []byte("sha256:"+img.ID), 0o644); err != nil { return nil, err } } } // If we're supposed to store SBOM or PURL information in local files, write them now. for filename, content := range extraLocalContent { err := func() error { output, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644) if err != nil { return err } defer output.Close() input, err := os.Open(content) if err != nil { return err } defer input.Close() if _, err := io.Copy(output, input); err != nil { return fmt.Errorf("copying from %q to %q: %w", content, filename, err) } return nil }() if err != nil { return nil, err } } // Calculate the as-written digest of the image's manifest and build the digested // reference for the image. manifestDigest, err := manifest.Digest(manifestBytes) if err != nil { return nil, fmt.Errorf("computing digest of manifest of new image %q: %w", transports.ImageName(dest), err) } parsedManifest, err := manifest.FromBlob(manifestBytes, manifest.GuessMIMEType(manifestBytes)) if err != nil { return nil, fmt.Errorf("parsing written manifest to determine the image's ID: %w", err) } configInfo := parsedManifest.ConfigInfo() if configInfo.Size > 2 && configInfo.Digest.Validate() == nil { // don't be returning a digest of "" or "{}" if imgID == "" { imgID = configInfo.Digest.Encoded() } } var ref reference.Canonical if name := dest.DockerReference(); name != nil { ref, err = reference.WithDigest(name, manifestDigest) if err != nil { logrus.Warnf("error generating canonical reference with name %q and digest %s: %v", name, manifestDigest.String(), err) } } if options.Manifest != "" { manifestID, err := b.addManifest(ctx, options.Manifest, imgID) if err != nil { return nil, err } logrus.Debugf("added imgID %s to manifestID %s", imgID, manifestID) } descriptor := v1.Descriptor{ MediaType: manifest.GuessMIMEType(manifestBytes), Digest: manifestDigest, Size: int64(len(manifestBytes)), } metadata, err := metadata.Build(configInfo.Digest, descriptor) if err != nil { return nil, fmt.Errorf("building metadata map for image: %w", err) } results := CommitResults{ ImageID: imgID, Canonical: ref, MediaType: descriptor.MediaType, ImageManifest: manifestBytes, Digest: manifestDigest, Metadata: metadata, } return &results, nil } ================================================ FILE: commit_test.go ================================================ package buildah import ( "archive/tar" "context" "crypto/rand" "encoding/json" "fmt" "io" "os" "path/filepath" "testing" "time" digest "github.com/opencontainers/go-digest" v1 "github.com/opencontainers/image-spec/specs-go/v1" rspec "github.com/opencontainers/runtime-spec/specs-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.podman.io/image/v5/manifest" ociLayout "go.podman.io/image/v5/oci/layout" imageStorage "go.podman.io/image/v5/storage" "go.podman.io/image/v5/transports" "go.podman.io/image/v5/types" "go.podman.io/storage" "go.podman.io/storage/pkg/archive" storageTypes "go.podman.io/storage/types" ) func makeFile(t *testing.T, base string, size int64) string { t.Helper() fn := filepath.Join(t.TempDir(), base) f, err := os.Create(fn) require.NoError(t, err) defer f.Close() if size == 0 { size = 512 } _, err = io.CopyN(f, rand.Reader, size) require.NoErrorf(t, err, "writing payload file %d", base) return f.Name() } func TestCommitLinkedLayers(t *testing.T) { // This test cannot be parallelized as this uses NewBuilder() // which eventually and indirectly accesses a global variable // defined in `go-selinux`, this must be fixed at `go-selinux` // or builder must enable sometime of locking mechanism i.e if // routine is creating Builder other's must wait for it. // Tracked here: https://github.com/containers/buildah/issues/5967 ctx := context.TODO() now := time.Now() graphDriverName := os.Getenv("STORAGE_DRIVER") if graphDriverName == "" { graphDriverName = "vfs" } t.Logf("using storage driver %q", graphDriverName) store, err := storage.GetStore(storageTypes.StoreOptions{ RunRoot: t.TempDir(), GraphRoot: t.TempDir(), GraphDriverName: graphDriverName, }) require.NoError(t, err, "initializing storage") t.Cleanup(func() { _, err := store.Shutdown(true); assert.NoError(t, err) }) imageName := func(i int) string { return fmt.Sprintf("image%d", i) } makeFile := func(base string, size int64) string { return makeFile(t, base, size) } makeArchive := func(base string, size int64) string { t.Helper() file := makeFile(base, size) archiveDir := t.TempDir() st, err := os.Stat(file) require.NoError(t, err) archiveName := filepath.Join(archiveDir, filepath.Base(file)) f, err := os.Create(archiveName) require.NoError(t, err) defer f.Close() tw := tar.NewWriter(f) defer tw.Close() hdr, err := tar.FileInfoHeader(st, "") require.NoErrorf(t, err, "building tar header for %s", file) err = tw.WriteHeader(hdr) require.NoErrorf(t, err, "writing tar header for %s", file) f, err = os.Open(file) require.NoError(t, err) defer f.Close() _, err = io.Copy(tw, f) require.NoErrorf(t, err, "writing tar payload for %s", file) return archiveName } layerNumber := 0 // Build a from-scratch image with one layer. builderOptions := BuilderOptions{ FromImage: "scratch", NamespaceOptions: []NamespaceOption{{ Name: string(rspec.NetworkNamespace), Host: true, }}, SystemContext: &testSystemContext, } b, err := NewBuilder(ctx, store, builderOptions) require.NoError(t, err, "creating builder") b.SetCreatedBy(imageName(layerNumber)) firstFile := makeFile("file0", 0) err = b.Add("/", false, AddAndCopyOptions{}, firstFile) require.NoError(t, err, "adding", firstFile) commitOptions := CommitOptions{ SystemContext: &testSystemContext, } ref, err := imageStorage.Transport.ParseStoreReference(store, imageName(layerNumber)) require.NoError(t, err, "parsing reference for to-be-committed image", imageName(layerNumber)) _, _, _, err = b.Commit(ctx, ref, commitOptions) require.NoError(t, err, "committing", imageName(layerNumber)) // Build another image based on the first with not much in its layer. builderOptions.FromImage = imageName(layerNumber) layerNumber++ b, err = NewBuilder(ctx, store, builderOptions) require.NoError(t, err, "creating builder") b.SetCreatedBy(imageName(layerNumber)) secondFile := makeFile("file1", 0) err = b.Add("/", false, AddAndCopyOptions{}, secondFile) require.NoError(t, err, "adding", secondFile) commitOptions = CommitOptions{ SystemContext: &testSystemContext, } ref, err = imageStorage.Transport.ParseStoreReference(store, imageName(layerNumber)) require.NoError(t, err, "parsing reference for to-be-committed image", imageName(layerNumber)) _, _, _, err = b.Commit(ctx, ref, commitOptions) require.NoError(t, err, "committing", imageName(layerNumber)) // Build a third image with two layers on either side of its read-write layer. builderOptions.FromImage = imageName(layerNumber) layerNumber++ b, err = NewBuilder(ctx, store, builderOptions) require.NoError(t, err, "creating builder") thirdFile := makeFile("file2", 0) fourthArchiveFile := makeArchive("file3", 0) fifthFile := makeFile("file4", 0) sixthFile := makeFile("file5", 0) seventhArchiveFile := makeArchive("file6", 0) eighthFile := makeFile("file7", 0) ninthArchiveFile := makeArchive("file8", 0) err = b.Add("/", false, AddAndCopyOptions{}, sixthFile) require.NoError(t, err, "adding", sixthFile) b.SetCreatedBy(imageName(layerNumber + 3)) b.AddPrependedLinkedLayer(nil, imageName(layerNumber), "", "", filepath.Dir(thirdFile)) commitOptions = CommitOptions{ PrependedLinkedLayers: []LinkedLayer{ { BlobPath: fourthArchiveFile, History: v1.History{ Created: &now, CreatedBy: imageName(layerNumber + 1), }, }, { BlobPath: filepath.Dir(fifthFile), History: v1.History{ Created: &now, CreatedBy: imageName(layerNumber + 2), }, }, }, AppendedLinkedLayers: []LinkedLayer{ { BlobPath: seventhArchiveFile, History: v1.History{ Created: &now, CreatedBy: imageName(layerNumber + 4), }, }, { BlobPath: filepath.Dir(eighthFile), History: v1.History{ Created: &now, CreatedBy: imageName(layerNumber + 5), }, }, }, SystemContext: &testSystemContext, } b.AddAppendedLinkedLayer(nil, imageName(layerNumber+6), "", "", ninthArchiveFile) ref, err = imageStorage.Transport.ParseStoreReference(store, imageName(layerNumber)) require.NoErrorf(t, err, "parsing reference for to-be-committed image %q", imageName(layerNumber)) _, _, _, err = b.Commit(ctx, ref, commitOptions) require.NoErrorf(t, err, "committing %q", imageName(layerNumber)) // Build one last image based on the previous one. builderOptions.FromImage = imageName(layerNumber) layerNumber += 7 b, err = NewBuilder(ctx, store, builderOptions) require.NoError(t, err, "creating builder") b.SetCreatedBy(imageName(layerNumber)) tenthFile := makeFile("file9", 0) err = b.Add("/", false, AddAndCopyOptions{}, tenthFile) require.NoError(t, err, "adding", tenthFile) commitOptions = CommitOptions{ SystemContext: &testSystemContext, } ref, err = imageStorage.Transport.ParseStoreReference(store, imageName(layerNumber)) require.NoError(t, err, "parsing reference for to-be-committed image", imageName(layerNumber)) _, _, _, err = b.Commit(ctx, ref, commitOptions) require.NoError(t, err, "committing", imageName(layerNumber)) // Get set to examine this image. At this point, each history entry // should just have "image%d" as its CreatedBy field, and each layer // should have the corresponding file (and nothing else) in it. src, err := ref.NewImageSource(ctx, &testSystemContext) require.NoError(t, err, "opening image source") defer src.Close() img, err := ref.NewImage(ctx, &testSystemContext) require.NoError(t, err, "opening image") defer img.Close() config, err := img.OCIConfig(ctx) require.NoError(t, err, "reading config in OCI format") require.Len(t, config.History, 10, "history length") for i := range config.History { require.Equal(t, fmt.Sprintf("image%d", i), config.History[i].CreatedBy, "history createdBy is off") } require.Len(t, config.RootFS.DiffIDs, 10, "diffID list") layerContents := func(archive io.ReadCloser) []string { var contents []string defer archive.Close() tr := tar.NewReader(archive) entry, err := tr.Next() for entry != nil { contents = append(contents, entry.Name) if err != nil { break } entry, err = tr.Next() } require.ErrorIs(t, err, io.EOF) return contents } infos, err := img.LayerInfosForCopy(ctx) require.NoError(t, err, "getting layer infos") require.Len(t, infos, 10) for i, blobInfo := range infos { func() { t.Helper() rc, _, err := src.GetBlob(ctx, blobInfo, nil) require.NoError(t, err, "getting blob", i) defer rc.Close() contents := layerContents(rc) require.Len(t, contents, 1) require.Equal(t, fmt.Sprintf("file%d", i), contents[0]) }() } } func TestCommitCompression(t *testing.T) { // This test cannot be parallelized as this uses NewBuilder() // which eventually and indirectly accesses a global variable // defined in `go-selinux`, this must be fixed at `go-selinux` // or builder must enable sometime of locking mechanism i.e if // routine is creating Builder other's must wait for it. // Tracked here: https://github.com/containers/buildah/issues/5967 ctx := context.TODO() graphDriverName := os.Getenv("STORAGE_DRIVER") if graphDriverName == "" { graphDriverName = "vfs" } t.Logf("using storage driver %q", graphDriverName) store, err := storage.GetStore(storageTypes.StoreOptions{ RunRoot: t.TempDir(), GraphRoot: t.TempDir(), GraphDriverName: graphDriverName, }) require.NoError(t, err, "initializing storage") t.Cleanup(func() { _, err := store.Shutdown(true); assert.NoError(t, err) }) builderOptions := BuilderOptions{ FromImage: "scratch", NamespaceOptions: []NamespaceOption{{ Name: string(rspec.NetworkNamespace), Host: true, }}, SystemContext: &testSystemContext, } b, err := NewBuilder(ctx, store, builderOptions) require.NoError(t, err, "creating builder") payload := makeFile(t, "file0", 0) b.SetCreatedBy("ADD file0 in /") err = b.Add("/", false, AddAndCopyOptions{}, payload) require.NoError(t, err, "adding", payload) for _, compressor := range []struct { compression archive.Compression name string expectError bool layerMediaType string os string }{ {archive.Uncompressed, "uncompressed", false, v1.MediaTypeImageLayer, ""}, {archive.Uncompressed, "uncompressed-win", false, v1.MediaTypeImageLayer, "windows"}, {archive.Gzip, "gzip", false, v1.MediaTypeImageLayerGzip, ""}, {archive.Gzip, "gzip-win", false, v1.MediaTypeImageLayerGzip, "windows"}, {archive.Bzip2, "bz2", true, "", ""}, {archive.Bzip2, "bz2-win", true, "", "windows"}, {archive.Xz, "xz", true, "", ""}, {archive.Xz, "xz-win", true, "", "windows"}, {archive.Zstd, "zstd", false, v1.MediaTypeImageLayerZstd, ""}, {archive.Zstd, "zstd-win", false, v1.MediaTypeImageLayerZstd, "windows"}, } { t.Run(compressor.name, func(t *testing.T) { var ref types.ImageReference commitOptions := CommitOptions{ PreferredManifestType: v1.MediaTypeImageManifest, SystemContext: &testSystemContext, Compression: compressor.compression, } b.OCIv1.OS = compressor.os imageName := compressor.name ref, err := imageStorage.Transport.ParseStoreReference(store, imageName) require.NoErrorf(t, err, "parsing reference for to-be-committed local image %q", imageName) _, _, _, err = b.Commit(ctx, ref, commitOptions) if compressor.expectError { require.Errorf(t, err, "committing local image %q", imageName) } else { require.NoErrorf(t, err, "committing local image %q", imageName) } imageName = t.TempDir() ref, err = ociLayout.Transport.ParseReference(imageName) require.NoErrorf(t, err, "parsing reference for to-be-committed oci layout %q", imageName) _, _, _, err = b.Commit(ctx, ref, commitOptions) if compressor.expectError { require.Errorf(t, err, "committing oci layout %q", imageName) return } require.NoErrorf(t, err, "committing oci layout %q", imageName) src, err := ref.NewImageSource(ctx, &testSystemContext) require.NoErrorf(t, err, "reading oci layout %q", imageName) defer src.Close() manifestBytes, manifestType, err := src.GetManifest(ctx, nil) require.NoErrorf(t, err, "reading manifest from oci layout %q", imageName) require.Equalf(t, v1.MediaTypeImageManifest, manifestType, "manifest type from oci layout %q looked wrong", imageName) parsedManifest, err := manifest.OCI1FromManifest(manifestBytes) require.NoErrorf(t, err, "parsing manifest from oci layout %q", imageName) require.Lenf(t, parsedManifest.Layers, 1, "expected exactly one layer in oci layout %q", imageName) require.Equalf(t, compressor.layerMediaType, parsedManifest.Layers[0].MediaType, "expected the layer media type to reflect compression in oci layout %q", imageName) blobReadCloser, _, err := src.GetBlob(ctx, types.BlobInfo{ Digest: parsedManifest.Layers[0].Digest, MediaType: parsedManifest.Layers[0].MediaType, }, nil) require.NoErrorf(t, err, "reading the first layer from oci layout %q", imageName) defer blobReadCloser.Close() blob, err := io.ReadAll(blobReadCloser) require.NoErrorf(t, err, "consuming the first layer from oci layout %q", imageName) require.Equalf(t, compressor.compression, archive.DetectCompression(blob), "detected compression looks wrong for layer in oci layout %q") }) } } func TestCommitEmpty(t *testing.T) { // This test cannot be parallelized as this uses NewBuilder() // which eventually and indirectly accesses a global variable // defined in `go-selinux`, this must be fixed at `go-selinux` // or builder must enable sometime of locking mechanism i.e if // routine is creating Builder other's must wait for it. // Tracked here: https://github.com/containers/buildah/issues/5967 ctx := context.TODO() graphDriverName := os.Getenv("STORAGE_DRIVER") if graphDriverName == "" { graphDriverName = "vfs" } t.Logf("using storage driver %q", graphDriverName) store, err := storage.GetStore(storageTypes.StoreOptions{ RunRoot: t.TempDir(), GraphRoot: t.TempDir(), GraphDriverName: graphDriverName, }) require.NoError(t, err, "initializing storage") t.Cleanup(func() { _, err := store.Shutdown(true); assert.NoError(t, err) }) builderOptions := BuilderOptions{ FromImage: "scratch", NamespaceOptions: []NamespaceOption{{ Name: string(rspec.NetworkNamespace), Host: true, }}, SystemContext: &testSystemContext, } b, err := NewBuilder(ctx, store, builderOptions) require.NoError(t, err, "creating builder") committedLayoutDir := t.TempDir() committedRef, err := ociLayout.ParseReference(committedLayoutDir) require.NoError(t, err, "parsing reference to where we're committing a basic image") _, _, _, err = b.Commit(ctx, committedRef, CommitOptions{}) require.NoError(t, err, "committing with default settings") committedImg, err := committedRef.NewImageSource(ctx, &testSystemContext) require.NoError(t, err, "preparing to read committed image") defer committedImg.Close() committedManifestBytes, committedManifestType, err := committedImg.GetManifest(ctx, nil) require.NoError(t, err, "reading manifest from committed image") require.Equalf(t, v1.MediaTypeImageManifest, committedManifestType, "unexpected manifest type") committedManifest, err := manifest.FromBlob(committedManifestBytes, committedManifestType) require.NoError(t, err, "parsing manifest from committed image") require.Equalf(t, 1, len(committedManifest.LayerInfos()), "expected one layer in manifest") configReadCloser, _, err := committedImg.GetBlob(ctx, committedManifest.ConfigInfo(), nil) require.NoError(t, err, "reading config blob from committed image") defer configReadCloser.Close() var committedImage v1.Image err = json.NewDecoder(configReadCloser).Decode(&committedImage) require.NoError(t, err, "parsing config blob from committed image") require.Equalf(t, 1, len(committedImage.History), "expected one history entry") require.Falsef(t, committedImage.History[0].EmptyLayer, "expected lone history entry to not be marked as an empty layer") require.Equalf(t, 1, len(committedImage.RootFS.DiffIDs), "expected one rootfs layer") t.Run("emptylayer", func(t *testing.T) { options := CommitOptions{ EmptyLayer: true, } layoutDir := t.TempDir() ref, err := ociLayout.ParseReference(layoutDir) require.NoError(t, err, "parsing reference to image we're going to commit with EmptyLayer") _, _, _, err = b.Commit(ctx, ref, options) require.NoError(t, err, "committing with EmptyLayer = true") img, err := ref.NewImageSource(ctx, &testSystemContext) require.NoError(t, err, "preparing to read committed image") defer img.Close() manifestBytes, manifestType, err := img.GetManifest(ctx, nil) require.NoError(t, err, "reading manifest from committed image") require.Equalf(t, v1.MediaTypeImageManifest, manifestType, "unexpected manifest type") parsedManifest, err := manifest.FromBlob(manifestBytes, manifestType) require.NoError(t, err, "parsing manifest from committed image") require.Zerof(t, len(parsedManifest.LayerInfos()), "expected no layers in manifest") configReadCloser, _, err := img.GetBlob(ctx, parsedManifest.ConfigInfo(), nil) require.NoError(t, err, "reading config blob from committed image") defer configReadCloser.Close() var image v1.Image err = json.NewDecoder(configReadCloser).Decode(&image) require.NoError(t, err, "parsing config blob from committed image") require.Equalf(t, 1, len(image.History), "expected one history entry") require.Truef(t, image.History[0].EmptyLayer, "expected lone history entry to be marked as an empty layer") }) t.Run("omitlayerhistoryentry", func(t *testing.T) { options := CommitOptions{ OmitLayerHistoryEntry: true, } layoutDir := t.TempDir() ref, err := ociLayout.ParseReference(layoutDir) require.NoError(t, err, "parsing reference to image we're going to commit with OmitLayerHistoryEntry") _, _, _, err = b.Commit(ctx, ref, options) require.NoError(t, err, "committing with OmitLayerHistoryEntry = true") img, err := ref.NewImageSource(ctx, &testSystemContext) require.NoError(t, err, "preparing to read committed image") defer img.Close() manifestBytes, manifestType, err := img.GetManifest(ctx, nil) require.NoError(t, err, "reading manifest from committed image") require.Equalf(t, v1.MediaTypeImageManifest, manifestType, "unexpected manifest type") parsedManifest, err := manifest.FromBlob(manifestBytes, manifestType) require.NoError(t, err, "parsing manifest from committed image") require.Equalf(t, 0, len(parsedManifest.LayerInfos()), "expected no layers in manifest") configReadCloser, _, err := img.GetBlob(ctx, parsedManifest.ConfigInfo(), nil) require.NoError(t, err, "reading config blob from committed image") defer configReadCloser.Close() var image v1.Image err = json.NewDecoder(configReadCloser).Decode(&image) require.NoError(t, err, "parsing config blob from committed image") require.Equalf(t, 0, len(image.History), "expected no history entries") require.Equalf(t, 0, len(image.RootFS.DiffIDs), "expected no diff IDs") }) builderOptions.FromImage = transports.ImageName(committedRef) b, err = NewBuilder(ctx, store, builderOptions) require.NoError(t, err, "creating builder from committed base image") t.Run("derived-emptylayer", func(t *testing.T) { options := CommitOptions{ EmptyLayer: true, } layoutDir := t.TempDir() ref, err := ociLayout.ParseReference(layoutDir) require.NoError(t, err, "parsing reference to image we're going to commit with EmptyLayer") _, _, _, err = b.Commit(ctx, ref, options) require.NoError(t, err, "committing with EmptyLayer = true") img, err := ref.NewImageSource(ctx, &testSystemContext) require.NoError(t, err, "preparing to read committed image") defer img.Close() manifestBytes, manifestType, err := img.GetManifest(ctx, nil) require.NoError(t, err, "reading manifest from committed image") require.Equalf(t, v1.MediaTypeImageManifest, manifestType, "unexpected manifest type") parsedManifest, err := manifest.FromBlob(manifestBytes, manifestType) require.NoError(t, err, "parsing manifest from committed image") require.Equalf(t, len(committedManifest.LayerInfos()), len(parsedManifest.LayerInfos()), "expected no new layers in manifest") configReadCloser, _, err := img.GetBlob(ctx, parsedManifest.ConfigInfo(), nil) require.NoError(t, err, "reading config blob from committed image") defer configReadCloser.Close() var image v1.Image err = json.NewDecoder(configReadCloser).Decode(&image) require.NoError(t, err, "parsing config blob from committed image") require.Equalf(t, len(committedImage.History)+1, len(image.History), "expected one new history entry") require.Equalf(t, len(committedImage.RootFS.DiffIDs), len(image.RootFS.DiffIDs), "expected no new diff IDs") require.Truef(t, image.History[1].EmptyLayer, "expected new history entry to be marked as an empty layer") }) t.Run("derived-omitlayerhistoryentry", func(t *testing.T) { options := CommitOptions{ OmitLayerHistoryEntry: true, } layoutDir := t.TempDir() ref, err := ociLayout.ParseReference(layoutDir) require.NoError(t, err, "parsing reference to image we're going to commit with OmitLayerHistoryEntry") _, _, _, err = b.Commit(ctx, ref, options) require.NoError(t, err, "committing with OmitLayerHistoryEntry = true") img, err := ref.NewImageSource(ctx, &testSystemContext) require.NoError(t, err, "preparing to read committed image") defer img.Close() manifestBytes, manifestType, err := img.GetManifest(ctx, nil) require.NoError(t, err, "reading manifest from committed image") require.Equalf(t, v1.MediaTypeImageManifest, manifestType, "unexpected manifest type") parsedManifest, err := manifest.FromBlob(manifestBytes, manifestType) require.NoError(t, err, "parsing manifest from committed image") require.Equalf(t, len(committedManifest.LayerInfos()), len(parsedManifest.LayerInfos()), "expected no new layers in manifest") configReadCloser, _, err := img.GetBlob(ctx, parsedManifest.ConfigInfo(), nil) require.NoError(t, err, "reading config blob from committed image") defer configReadCloser.Close() var image v1.Image err = json.NewDecoder(configReadCloser).Decode(&image) require.NoError(t, err, "parsing config blob from committed image") require.Equalf(t, len(committedImage.History), len(image.History), "expected no new history entry") require.Equalf(t, len(committedImage.RootFS.DiffIDs), len(image.RootFS.DiffIDs), "expected no new diff IDs") }) t.Run("derived-synthetic", func(t *testing.T) { randomDir := t.TempDir() randomFile, err := os.CreateTemp(randomDir, "file") require.NoError(t, err, "creating a temporary file") layerDigest := digest.Canonical.Digester() _, err = io.CopyN(io.MultiWriter(layerDigest.Hash(), randomFile), rand.Reader, 512) require.NoError(t, err, "writing a temporary file") require.NoError(t, randomFile.Close(), "closing temporary file") options := CommitOptions{ OmitLayerHistoryEntry: true, AppendedLinkedLayers: []LinkedLayer{{ History: v1.History{ CreatedBy: "yolo", }, // history entry to add BlobPath: randomFile.Name(), }}, } layoutDir := t.TempDir() ref, err := ociLayout.ParseReference(layoutDir) require.NoErrorf(t, err, "parsing reference for to-be-committed image with externally-controlled changes") _, _, _, err = b.Commit(ctx, ref, options) require.NoError(t, err, "committing with OmitLayerHistoryEntry = true") img, err := ref.NewImageSource(ctx, &testSystemContext) require.NoError(t, err, "preparing to read committed image") defer img.Close() manifestBytes, manifestType, err := img.GetManifest(ctx, nil) require.NoError(t, err, "reading manifest from committed image") require.Equalf(t, v1.MediaTypeImageManifest, manifestType, "unexpected manifest type") parsedManifest, err := manifest.FromBlob(manifestBytes, manifestType) require.NoError(t, err, "parsing manifest from committed image") require.Equalf(t, len(committedManifest.LayerInfos())+1, len(parsedManifest.LayerInfos()), "expected one new layer in manifest") configReadCloser, _, err := img.GetBlob(ctx, parsedManifest.ConfigInfo(), nil) require.NoError(t, err, "reading config blob from committed image") defer configReadCloser.Close() var image v1.Image err = json.NewDecoder(configReadCloser).Decode(&image) require.NoError(t, err, "decoding image config") require.Equalf(t, len(committedImage.History)+1, len(image.History), "expected one new history entry") require.Equalf(t, len(committedImage.RootFS.DiffIDs)+1, len(image.RootFS.DiffIDs), "expected one new diff ID") require.Equalf(t, layerDigest.Digest(), image.RootFS.DiffIDs[len(image.RootFS.DiffIDs)-1], "expected new diff ID to match the randomly-generated layer") }) } ================================================ FILE: common.go ================================================ package buildah import ( "context" "errors" "io" "path/filepath" "time" "github.com/containers/buildah/define" encconfig "github.com/containers/ocicrypt/config" "go.podman.io/common/pkg/retry" cp "go.podman.io/image/v5/copy" "go.podman.io/image/v5/docker" "go.podman.io/image/v5/signature" is "go.podman.io/image/v5/storage" "go.podman.io/image/v5/types" "go.podman.io/storage" "go.podman.io/storage/pkg/fileutils" "go.podman.io/storage/pkg/unshare" ) const ( // OCI used to define the "oci" image format OCI = define.OCI // DOCKER used to define the "docker" image format DOCKER = define.DOCKER ) func getCopyOptions(store storage.Store, reportWriter io.Writer, sourceSystemContext *types.SystemContext, destinationSystemContext *types.SystemContext, manifestType string, removeSignatures bool, addSigner string, ociEncryptLayers *[]int, ociEncryptConfig *encconfig.EncryptConfig, ociDecryptConfig *encconfig.DecryptConfig, destinationTimestamp *time.Time) *cp.Options { sourceCtx := getSystemContext(store, nil, "") if sourceSystemContext != nil { *sourceCtx = *sourceSystemContext } destinationCtx := getSystemContext(store, nil, "") if destinationSystemContext != nil { *destinationCtx = *destinationSystemContext } return &cp.Options{ ReportWriter: reportWriter, SourceCtx: sourceCtx, DestinationCtx: destinationCtx, ForceManifestMIMEType: manifestType, RemoveSignatures: removeSignatures, SignBy: addSigner, OciEncryptConfig: ociEncryptConfig, OciDecryptConfig: ociDecryptConfig, OciEncryptLayers: ociEncryptLayers, DestinationTimestamp: destinationTimestamp, } } func getSystemContext(store storage.Store, defaults *types.SystemContext, signaturePolicyPath string) *types.SystemContext { sc := &types.SystemContext{} if defaults != nil { *sc = *defaults } if signaturePolicyPath != "" { sc.SignaturePolicyPath = signaturePolicyPath } if store != nil { if sc.SystemRegistriesConfPath == "" && unshare.IsRootless() { userRegistriesFile := filepath.Join(store.GraphRoot(), "registries.conf") if err := fileutils.Exists(userRegistriesFile); err == nil { sc.SystemRegistriesConfPath = userRegistriesFile } } } return sc } func retryCopyImage(ctx context.Context, policyContext *signature.PolicyContext, maybeWrappedDest, maybeWrappedSrc, directDest types.ImageReference, copyOptions *cp.Options, maxRetries int, retryDelay time.Duration) ([]byte, error) { return retryCopyImageWithOptions(ctx, policyContext, maybeWrappedDest, maybeWrappedSrc, directDest, copyOptions, maxRetries, retryDelay, true) } func retryCopyImageWithOptions(ctx context.Context, policyContext *signature.PolicyContext, maybeWrappedDest, maybeWrappedSrc, directDest types.ImageReference, copyOptions *cp.Options, maxRetries int, retryDelay time.Duration, retryOnLayerUnknown bool) ([]byte, error) { var ( manifestBytes []byte err error ) err = retry.IfNecessary(ctx, func() error { manifestBytes, err = cp.Image(ctx, policyContext, maybeWrappedDest, maybeWrappedSrc, copyOptions) return err }, &retry.RetryOptions{MaxRetry: maxRetries, Delay: retryDelay, IsErrorRetryable: func(err error) bool { if retryOnLayerUnknown && directDest.Transport().Name() == is.Transport.Name() && errors.Is(err, storage.ErrLayerUnknown) { // we were trying to reuse a layer that belonged to an // image that was deleted at just the right (worst // possible) time? yeah, try again return true } if directDest.Transport().Name() != docker.Transport.Name() { // if we're not talking to a registry, then nah return false } // hand it off to the default should-this-be-retried logic return retry.IsErrorRetryable(err) }}) return manifestBytes, err } ================================================ FILE: common_test.go ================================================ package buildah import ( "archive/tar" "bytes" "context" "encoding/json" "os" "path/filepath" "testing" "time" digest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" cp "go.podman.io/image/v5/copy" "go.podman.io/image/v5/signature" imageStorage "go.podman.io/image/v5/storage" "go.podman.io/image/v5/transports/alltransports" "go.podman.io/storage" storageTypes "go.podman.io/storage/types" ) type testRetryCopyImageWrappedStore struct { phantomImageID string storage.Store } func (ts *testRetryCopyImageWrappedStore) CreateImage(id string, names []string, layer, metadata string, options *storage.ImageOptions) (*storage.Image, error) { if id == ts.phantomImageID { if img, err := ts.Store.Image(id); img != nil && err == nil { // i'm another thread somewhere if _, err := ts.Store.DeleteImage(id, true); err != nil { return nil, err } } } return ts.Store.CreateImage(id, names, layer, metadata, options) } func TestRetryCopyImage(t *testing.T) { t.Parallel() ctx := context.TODO() graphDriverName := os.Getenv("STORAGE_DRIVER") if graphDriverName == "" { graphDriverName = "vfs" } store, err := storage.GetStore(storageTypes.StoreOptions{ RunRoot: t.TempDir(), GraphRoot: t.TempDir(), GraphDriverName: graphDriverName, }) require.NoError(t, err, "initializing storage") t.Cleanup(func() { _, err := store.Shutdown(true); assert.NoError(t, err) }) // construct an "image" that can be pulled into local storage var layerBuffer bytes.Buffer tw := tar.NewWriter(&layerBuffer) err = tw.WriteHeader(&tar.Header{ Name: "rootfile", Typeflag: tar.TypeReg, Size: 1234, }) require.NoError(t, err, "writing header for archive") _, err = tw.Write(make([]byte, 1234)) require.NoError(t, err, "writing empty file to archive") require.NoError(t, tw.Close(), "finishing layer") layerDigest := digest.Canonical.FromBytes(layerBuffer.Bytes()) imageConfig := v1.Image{ RootFS: v1.RootFS{ Type: "layers", DiffIDs: []digest.Digest{layerDigest}, }, } imageConfigBytes, err := json.Marshal(&imageConfig) require.NoError(t, err, "marshalling image configuration blob") imageConfigDigest := digest.Canonical.FromBytes(imageConfigBytes) imageManifest := v1.Manifest{ Versioned: ispec.Versioned{ SchemaVersion: 2, }, MediaType: v1.MediaTypeImageManifest, Config: v1.Descriptor{ MediaType: v1.MediaTypeImageConfig, Size: int64(len(imageConfigBytes)), Digest: digest.FromBytes(imageConfigBytes), }, Layers: []v1.Descriptor{ { MediaType: v1.MediaTypeImageLayer, Size: int64(layerBuffer.Len()), Digest: layerDigest, }, }, } imageManifestBytes, err := json.Marshal(&imageManifest) require.NoError(t, err, "marshalling image manifest") imageManifestDigest := digest.Canonical.FromBytes(imageManifestBytes) // write it to an oci layout ociDir := t.TempDir() blobbyDir := filepath.Join(ociDir, "blobs") require.NoError(t, os.Mkdir(blobbyDir, 0o700)) blobDir := filepath.Join(blobbyDir, layerDigest.Algorithm().String()) require.NoError(t, os.Mkdir(blobDir, 0o700)) require.NoError(t, os.WriteFile(filepath.Join(blobDir, layerDigest.Encoded()), layerBuffer.Bytes(), 0o600), "writing layer") require.NoError(t, os.WriteFile(filepath.Join(blobDir, imageConfigDigest.Encoded()), imageConfigBytes, 0o600), "writing image config") require.NoError(t, os.WriteFile(filepath.Join(blobDir, imageManifestDigest.Encoded()), imageManifestBytes, 0o600), "writing manifest") imageIndex := v1.Index{ Versioned: ispec.Versioned{ SchemaVersion: 2, }, MediaType: v1.MediaTypeImageIndex, Manifests: []v1.Descriptor{ { MediaType: v1.MediaTypeImageManifest, Digest: imageManifestDigest, Size: int64(len(imageManifestBytes)), }, }, } imageIndexBytes, err := json.Marshal(&imageIndex) require.NoError(t, err, "marshalling image index") require.NoError(t, os.WriteFile(filepath.Join(ociDir, v1.ImageIndexFile), imageIndexBytes, 0o600), "writing image index") imageLayout := v1.ImageLayout{ Version: v1.ImageLayoutVersion, } imageLayoutBytes, err := json.Marshal(&imageLayout) require.NoError(t, err, "marshalling image layout") require.NoError(t, os.WriteFile(filepath.Join(ociDir, v1.ImageLayoutFile), imageLayoutBytes, 0o600), "writing image layout") // pull the image, twice, just to make sure nothing weird happens srcRef, err := alltransports.ParseImageName("oci:" + ociDir) require.NoError(t, err, "building reference to image layout") destRef, err := imageStorage.Transport.NewStoreReference(store, nil, imageConfigDigest.Encoded()) require.NoError(t, err, "building reference to image in store") policy, err := signature.NewPolicyFromFile("tests/policy.json") require.NoError(t, err, "reading signature policy") policyContext, err := signature.NewPolicyContext(policy) require.NoError(t, err, "building policy context") t.Cleanup(func() { require.NoError(t, policyContext.Destroy(), "destroying policy context") }) _, err = retryCopyImage(ctx, policyContext, destRef, srcRef, destRef, &cp.Options{}, 3, 1*time.Second) require.NoError(t, err, "copying image") _, err = retryCopyImage(ctx, policyContext, destRef, srcRef, destRef, &cp.Options{}, 3, 1*time.Second) require.NoError(t, err, "copying image") // now make something weird happen wrappedStore := &testRetryCopyImageWrappedStore{ phantomImageID: imageConfigDigest.Encoded(), Store: store, } wrappedDestRef, err := imageStorage.Transport.NewStoreReference(wrappedStore, nil, imageConfigDigest.Encoded()) require.NoError(t, err, "building wrapped reference") // copy with retry-on-storage-layer-unknown = false: expect an error // (if it succeeds, either the test is broken, or we can remove this // case from the retry function) _, err = retryCopyImageWithOptions(ctx, policyContext, wrappedDestRef, srcRef, wrappedDestRef, &cp.Options{}, 3, 1*time.Second, false) require.ErrorIs(t, err, storage.ErrLayerUnknown, "copying image") // copy with retry-on-storage-layer-unknown = true: expect no error _, err = retryCopyImageWithOptions(ctx, policyContext, wrappedDestRef, srcRef, wrappedDestRef, &cp.Options{}, 3, 1*time.Second, true) require.NoError(t, err, "copying image") } ================================================ FILE: config.go ================================================ package buildah import ( "context" "encoding/json" "fmt" "maps" "os" "slices" "strings" "time" "github.com/containerd/platforms" "github.com/containers/buildah/define" "github.com/containers/buildah/docker" internalUtil "github.com/containers/buildah/internal/util" ociv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" "go.podman.io/image/v5/manifest" "go.podman.io/image/v5/pkg/compression" "go.podman.io/image/v5/transports" "go.podman.io/image/v5/types" "go.podman.io/storage/pkg/stringid" ) // unmarshalConvertedConfig obtains the config blob of img valid for the wantedManifestMIMEType format // (either as it exists, or converting the image if necessary), and unmarshals it into dest. // NOTE: The MIME type is of the _manifest_, not of the _config_ that is returned. func unmarshalConvertedConfig(ctx context.Context, dest any, img types.Image, wantedManifestMIMEType string) error { _, actualManifestMIMEType, err := img.Manifest(ctx) if err != nil { return fmt.Errorf("getting manifest MIME type for %q: %w", transports.ImageName(img.Reference()), err) } if wantedManifestMIMEType != actualManifestMIMEType { layerInfos := img.LayerInfos() for i := range layerInfos { // force the "compression" to gzip, which is supported by all of the formats we care about layerInfos[i].CompressionOperation = types.Compress layerInfos[i].CompressionAlgorithm = &compression.Gzip } updatedImg, err := img.UpdatedImage(ctx, types.ManifestUpdateOptions{ LayerInfos: layerInfos, }) if err != nil { return fmt.Errorf("resetting recorded compression for %q: %w", transports.ImageName(img.Reference()), err) } secondUpdatedImg, err := updatedImg.UpdatedImage(ctx, types.ManifestUpdateOptions{ ManifestMIMEType: wantedManifestMIMEType, }) if err != nil { return fmt.Errorf("converting image %q from %q to %q: %w", transports.ImageName(img.Reference()), actualManifestMIMEType, wantedManifestMIMEType, err) } img = secondUpdatedImg } config, err := img.ConfigBlob(ctx) if err != nil { return fmt.Errorf("reading %s config from %q: %w", wantedManifestMIMEType, transports.ImageName(img.Reference()), err) } if err := json.Unmarshal(config, dest); err != nil { return fmt.Errorf("parsing %s configuration %q from %q: %w", wantedManifestMIMEType, string(config), transports.ImageName(img.Reference()), err) } return nil } func (b *Builder) initConfig(ctx context.Context, sys *types.SystemContext, img types.Image, options *BuilderOptions) error { if img != nil { // A pre-existing image, as opposed to a "FROM scratch" new one. rawManifest, manifestMIMEType, err := img.Manifest(ctx) if err != nil { return fmt.Errorf("reading image manifest for %q: %w", transports.ImageName(img.Reference()), err) } rawConfig, err := img.ConfigBlob(ctx) if err != nil { return fmt.Errorf("reading image configuration for %q: %w", transports.ImageName(img.Reference()), err) } b.Manifest = rawManifest b.Config = rawConfig dimage := docker.V2Image{} if err := unmarshalConvertedConfig(ctx, &dimage, img, manifest.DockerV2Schema2MediaType); err != nil { return err } b.Docker = dimage oimage := ociv1.Image{} if err := unmarshalConvertedConfig(ctx, &oimage, img, ociv1.MediaTypeImageManifest); err != nil { return err } b.OCIv1 = oimage if manifestMIMEType == ociv1.MediaTypeImageManifest { // Attempt to recover format-specific data from the manifest. v1Manifest := ociv1.Manifest{} if err := json.Unmarshal(b.Manifest, &v1Manifest); err != nil { return fmt.Errorf("parsing OCI manifest %q: %w", string(b.Manifest), err) } if len(v1Manifest.Annotations) > 0 { if b.ImageAnnotations == nil { b.ImageAnnotations = make(map[string]string, len(v1Manifest.Annotations)) } maps.Copy(b.ImageAnnotations, v1Manifest.Annotations) } } } else { if options == nil || options.CompatScratchConfig != types.OptionalBoolTrue { b.Docker = docker.V2Image{ V1Image: docker.V1Image{ Config: &docker.Config{ WorkingDir: "/", }, }, } b.OCIv1 = ociv1.Image{ Config: ociv1.ImageConfig{ WorkingDir: "/", }, } } } b.setupLogger() b.fixupConfig(sys) return nil } func (b *Builder) fixupConfig(sys *types.SystemContext) { if b.Docker.Config != nil { // Prefer image-level settings over those from the container it was built from. b.Docker.ContainerConfig = *b.Docker.Config } b.Docker.Config = &b.Docker.ContainerConfig b.Docker.DockerVersion = "" now := time.Now().UTC() if b.Docker.Created.IsZero() { b.Docker.Created = now } if b.OCIv1.Created == nil || b.OCIv1.Created.IsZero() { b.OCIv1.Created = &now } currentPlatformSpecification := platforms.DefaultSpec() if b.OS() == "" { if sys != nil && sys.OSChoice != "" { b.SetOS(sys.OSChoice) } else { b.SetOS(currentPlatformSpecification.OS) } } if b.Architecture() == "" { if sys != nil && sys.ArchitectureChoice != "" { b.SetArchitecture(sys.ArchitectureChoice) b.SetVariant(sys.VariantChoice) } else { b.SetArchitecture(currentPlatformSpecification.Architecture) b.SetVariant(currentPlatformSpecification.Variant) } // in case the arch string we started with was shorthand for a known arch+variant pair, normalize it ps := internalUtil.NormalizePlatform(ociv1.Platform{OS: b.OS(), Architecture: b.Architecture(), Variant: b.Variant()}) b.SetArchitecture(ps.Architecture) b.SetVariant(ps.Variant) } if b.Format == define.Dockerv2ImageManifest && b.Hostname() == "" { b.SetHostname(stringid.TruncateID(stringid.GenerateRandomID())) } } func (b *Builder) setupLogger() { if b.Logger == nil { b.Logger = logrus.New() b.Logger.SetOutput(os.Stderr) b.Logger.SetLevel(logrus.GetLevel()) } } // Annotations returns a set of key-value pairs from the image's manifest. func (b *Builder) Annotations() map[string]string { return maps.Clone(b.ImageAnnotations) } // SetAnnotation adds or overwrites a key's value from the image's manifest. // Note: this setting is not present in the Docker v2 image format, so it is // discarded when writing images using Docker v2 formats. func (b *Builder) SetAnnotation(key, value string) { if b.ImageAnnotations == nil { b.ImageAnnotations = map[string]string{} } b.ImageAnnotations[key] = value } // UnsetAnnotation removes a key and its value from the image's manifest, if // it's present. func (b *Builder) UnsetAnnotation(key string) { delete(b.ImageAnnotations, key) } // ClearAnnotations removes all keys and their values from the image's // manifest. func (b *Builder) ClearAnnotations() { b.ImageAnnotations = nil } // CreatedBy returns a description of how this image was built. func (b *Builder) CreatedBy() string { return b.ImageCreatedBy } // SetCreatedBy sets the description of how this image was built. func (b *Builder) SetCreatedBy(how string) { b.ImageCreatedBy = how } // OS returns a name of the OS on which the container, or a container built // using an image built from this container, is intended to be run. func (b *Builder) OS() string { return b.OCIv1.OS } // SetOS sets the name of the OS on which the container, or a container built // using an image built from this container, is intended to be run. func (b *Builder) SetOS(os string) { b.OCIv1.OS = os b.Docker.OS = os } // OSVersion returns a version of the OS on which the container, or a container // built using an image built from this container, is intended to be run. func (b *Builder) OSVersion() string { return b.OCIv1.OSVersion } // SetOSVersion sets the version of the OS on which the container, or a // container built using an image built from this container, is intended to be // run. func (b *Builder) SetOSVersion(version string) { b.OCIv1.OSVersion = version b.Docker.OSVersion = version } // OSFeatures returns a list of OS features which the container, or a container // built using an image built from this container, depends on the OS supplying. func (b *Builder) OSFeatures() []string { return slices.Clone(b.OCIv1.OSFeatures) } // SetOSFeature adds a feature of the OS which the container, or a container // built using an image built from this container, depends on the OS supplying. func (b *Builder) SetOSFeature(feature string) { if !slices.Contains(b.OCIv1.OSFeatures, feature) { b.OCIv1.OSFeatures = append(b.OCIv1.OSFeatures, feature) } if !slices.Contains(b.Docker.OSFeatures, feature) { b.Docker.OSFeatures = append(b.Docker.OSFeatures, feature) } } // UnsetOSFeature removes a feature of the OS which the container, or a // container built using an image built from this container, depends on the OS // supplying. func (b *Builder) UnsetOSFeature(feature string) { if slices.Contains(b.OCIv1.OSFeatures, feature) { features := make([]string, 0, len(b.OCIv1.OSFeatures)) for _, f := range b.OCIv1.OSFeatures { if f != feature { features = append(features, f) } } b.OCIv1.OSFeatures = features } if slices.Contains(b.Docker.OSFeatures, feature) { features := make([]string, 0, len(b.Docker.OSFeatures)) for _, f := range b.Docker.OSFeatures { if f != feature { features = append(features, f) } } b.Docker.OSFeatures = features } } // ClearOSFeatures clears the list of features of the OS which the container, // or a container built using an image built from this container, depends on // the OS supplying. func (b *Builder) ClearOSFeatures() { b.OCIv1.OSFeatures = []string{} b.Docker.OSFeatures = []string{} } // Architecture returns a name of the architecture on which the container, or a // container built using an image built from this container, is intended to be // run. func (b *Builder) Architecture() string { return b.OCIv1.Architecture } // SetArchitecture sets the name of the architecture on which the container, or // a container built using an image built from this container, is intended to // be run. func (b *Builder) SetArchitecture(arch string) { b.OCIv1.Architecture = arch b.Docker.Architecture = arch } // Variant returns a name of the architecture variant on which the container, // or a container built using an image built from this container, is intended // to be run. func (b *Builder) Variant() string { return b.OCIv1.Variant } // SetVariant sets the name of the architecture variant on which the container, // or a container built using an image built from this container, is intended // to be run. func (b *Builder) SetVariant(variant string) { b.Docker.Variant = variant b.OCIv1.Variant = variant } // Maintainer returns contact information for the person who built the image. func (b *Builder) Maintainer() string { return b.OCIv1.Author } // SetMaintainer sets contact information for the person who built the image. func (b *Builder) SetMaintainer(who string) { b.OCIv1.Author = who b.Docker.Author = who } // User returns information about the user as whom the container, or a // container built using an image built from this container, should be run. func (b *Builder) User() string { return b.OCIv1.Config.User } // SetUser sets information about the user as whom the container, or a // container built using an image built from this container, should be run. // Acceptable forms are a user name or ID, optionally followed by a colon and a // group name or ID. func (b *Builder) SetUser(spec string) { b.OCIv1.Config.User = spec b.Docker.Config.User = spec } // OnBuild returns the OnBuild value from the container. func (b *Builder) OnBuild() []string { return slices.Clone(b.Docker.Config.OnBuild) } // ClearOnBuild removes all values from the OnBuild structure func (b *Builder) ClearOnBuild() { b.Docker.Config.OnBuild = []string{} } // SetOnBuild sets a trigger instruction to be executed when the image is used // as the base of another image. // Note: this setting is not present in the OCIv1 image format, so it is // discarded when writing images using OCIv1 formats. func (b *Builder) SetOnBuild(onBuild string) { if onBuild != "" && b.Format != define.Dockerv2ImageManifest { b.Logger.Warnf("ONBUILD is not supported for OCI image format, %s will be ignored. Must use `docker` format", onBuild) } b.Docker.Config.OnBuild = append(b.Docker.Config.OnBuild, onBuild) } // WorkDir returns the default working directory for running commands in the // container, or in a container built using an image built from this container. func (b *Builder) WorkDir() string { return b.OCIv1.Config.WorkingDir } // SetWorkDir sets the location of the default working directory for running // commands in the container, or in a container built using an image built from // this container. func (b *Builder) SetWorkDir(there string) { b.OCIv1.Config.WorkingDir = there b.Docker.Config.WorkingDir = there } // Shell returns the default shell for running commands in the // container, or in a container built using an image built from this container. func (b *Builder) Shell() []string { return slices.Clone(b.Docker.Config.Shell) } // SetShell sets the default shell for running // commands in the container, or in a container built using an image built from // this container. // Note: this setting is not present in the OCIv1 image format, so it is // discarded when writing images using OCIv1 formats, even though it is used // for subsequent RUN instructions while building this image. func (b *Builder) SetShell(shell []string) { if len(shell) > 0 && b.Format != define.Dockerv2ImageManifest { b.Logger.Warnf("SHELL is not persisted in the OCI image format, %s will be used for RUN subsequent instructions to build this image but will not be present in saved image which may affect any images that use this as a base. Must use `docker` format to persist in saved image", shell) } b.Docker.Config.Shell = slices.Clone(shell) } // Env returns a list of key-value pairs to be set when running commands in the // container, or in a container built using an image built from this container. func (b *Builder) Env() []string { return slices.Clone(b.OCIv1.Config.Env) } // SetEnv adds or overwrites a value to the set of environment strings which // should be set when running commands in the container, or in a container // built using an image built from this container. func (b *Builder) SetEnv(k string, v string) { reset := func(s *[]string) { n := []string{} for i := range *s { if !strings.HasPrefix((*s)[i], k+"=") { n = append(n, (*s)[i]) } } n = append(n, k+"="+v) *s = n } reset(&b.OCIv1.Config.Env) reset(&b.Docker.Config.Env) } // UnsetEnv removes a value from the set of environment strings which should be // set when running commands in this container, or in a container built using // an image built from this container. func (b *Builder) UnsetEnv(k string) { unset := func(s *[]string) { n := []string{} for i := range *s { if !strings.HasPrefix((*s)[i], k+"=") { n = append(n, (*s)[i]) } } *s = n } unset(&b.OCIv1.Config.Env) unset(&b.Docker.Config.Env) } // ClearEnv removes all values from the set of environment strings which should // be set when running commands in this container, or in a container built // using an image built from this container. func (b *Builder) ClearEnv() { b.OCIv1.Config.Env = []string{} b.Docker.Config.Env = []string{} } // Cmd returns the default command, or command parameters if an Entrypoint is // set, to use when running a container built from an image built from this // container. func (b *Builder) Cmd() []string { return slices.Clone(b.OCIv1.Config.Cmd) } // SetCmd sets the default command, or command parameters if an Entrypoint is // set, to use when running a container built from an image built from this // container. func (b *Builder) SetCmd(cmd []string) { b.OCIv1.Config.Cmd = slices.Clone(cmd) b.Docker.Config.Cmd = slices.Clone(cmd) } // Entrypoint returns the command to be run for containers built from images // built from this container. func (b *Builder) Entrypoint() []string { if len(b.OCIv1.Config.Entrypoint) > 0 { return slices.Clone(b.OCIv1.Config.Entrypoint) } return nil } // SetEntrypoint sets the command to be run for in containers built from images // built from this container. func (b *Builder) SetEntrypoint(ep []string) { b.OCIv1.Config.Entrypoint = slices.Clone(ep) b.Docker.Config.Entrypoint = slices.Clone(ep) } // Labels returns a set of key-value pairs from the image's runtime // configuration. func (b *Builder) Labels() map[string]string { return maps.Clone(b.OCIv1.Config.Labels) } // SetLabel adds or overwrites a key's value from the image's runtime // configuration. func (b *Builder) SetLabel(k string, v string) { if b.OCIv1.Config.Labels == nil { b.OCIv1.Config.Labels = map[string]string{} } b.OCIv1.Config.Labels[k] = v if b.Docker.Config.Labels == nil { b.Docker.Config.Labels = map[string]string{} } b.Docker.Config.Labels[k] = v } // UnsetLabel removes a key and its value from the image's runtime // configuration, if it's present. func (b *Builder) UnsetLabel(k string) { delete(b.OCIv1.Config.Labels, k) delete(b.Docker.Config.Labels, k) } // ClearLabels removes all keys and their values from the image's runtime // configuration. func (b *Builder) ClearLabels() { b.OCIv1.Config.Labels = map[string]string{} b.Docker.Config.Labels = map[string]string{} } // Ports returns the set of ports which should be exposed when a container // based on an image built from this container is run. func (b *Builder) Ports() []string { p := []string{} for k := range b.OCIv1.Config.ExposedPorts { p = append(p, k) } return p } // SetPort adds or overwrites an exported port in the set of ports which should // be exposed when a container based on an image built from this container is // run. func (b *Builder) SetPort(p string) { if b.OCIv1.Config.ExposedPorts == nil { b.OCIv1.Config.ExposedPorts = map[string]struct{}{} } b.OCIv1.Config.ExposedPorts[p] = struct{}{} if b.Docker.Config.ExposedPorts == nil { b.Docker.Config.ExposedPorts = make(docker.PortSet) } b.Docker.Config.ExposedPorts[docker.Port(p)] = struct{}{} } // UnsetPort removes an exposed port from the set of ports which should be // exposed when a container based on an image built from this container is run. func (b *Builder) UnsetPort(p string) { delete(b.OCIv1.Config.ExposedPorts, p) delete(b.Docker.Config.ExposedPorts, docker.Port(p)) } // ClearPorts empties the set of ports which should be exposed when a container // based on an image built from this container is run. func (b *Builder) ClearPorts() { b.OCIv1.Config.ExposedPorts = map[string]struct{}{} b.Docker.Config.ExposedPorts = docker.PortSet{} } // Volumes returns a list of filesystem locations which should be mounted from // outside of the container when a container built from an image built from // this container is run. func (b *Builder) Volumes() []string { v := []string{} for k := range b.OCIv1.Config.Volumes { v = append(v, k) } if len(v) > 0 { return v } return nil } // CheckVolume returns True if the location exists in the image's list of locations // which should be mounted from outside of the container when a container // based on an image built from this container is run func (b *Builder) CheckVolume(v string) bool { _, OCIv1Volume := b.OCIv1.Config.Volumes[v] _, DockerVolume := b.Docker.Config.Volumes[v] return OCIv1Volume || DockerVolume } // AddVolume adds a location to the image's list of locations which should be // mounted from outside of the container when a container based on an image // built from this container is run. func (b *Builder) AddVolume(v string) { if b.OCIv1.Config.Volumes == nil { b.OCIv1.Config.Volumes = map[string]struct{}{} } b.OCIv1.Config.Volumes[v] = struct{}{} if b.Docker.Config.Volumes == nil { b.Docker.Config.Volumes = map[string]struct{}{} } b.Docker.Config.Volumes[v] = struct{}{} } // RemoveVolume removes a location from the list of locations which should be // mounted from outside of the container when a container based on an image // built from this container is run. func (b *Builder) RemoveVolume(v string) { delete(b.OCIv1.Config.Volumes, v) delete(b.Docker.Config.Volumes, v) } // ClearVolumes removes all locations from the image's list of locations which // should be mounted from outside of the container when a container based on an // image built from this container is run. func (b *Builder) ClearVolumes() { b.OCIv1.Config.Volumes = map[string]struct{}{} b.Docker.Config.Volumes = map[string]struct{}{} } // Hostname returns the hostname which will be set in the container and in // containers built using images built from the container. func (b *Builder) Hostname() string { return b.Docker.Config.Hostname } // SetHostname sets the hostname which will be set in the container and in // containers built using images built from the container. // Note: this setting is not present in the OCIv1 image format, so it is // discarded when writing images using OCIv1 formats. func (b *Builder) SetHostname(name string) { b.Docker.Config.Hostname = name } // Domainname returns the domainname which will be set in the container and in // containers built using images built from the container. func (b *Builder) Domainname() string { return b.Docker.Config.Domainname } // SetDomainname sets the domainname which will be set in the container and in // containers built using images built from the container. // Note: this setting is not present in the OCIv1 image format, so it is // discarded when writing images using OCIv1 formats. func (b *Builder) SetDomainname(name string) { if name != "" && b.Format != define.Dockerv2ImageManifest { b.Logger.Warnf("DOMAINNAME is not supported for OCI image format, domainname %s will be ignored. Must use `docker` format", name) } b.Docker.Config.Domainname = name } // SetDefaultMountsFilePath sets the mounts file path for testing purposes func (b *Builder) SetDefaultMountsFilePath(path string) { b.DefaultMountsFilePath = path } // Comment returns the comment which will be set in the container and in // containers built using images built from the container func (b *Builder) Comment() string { return b.Docker.Comment } // SetComment sets the comment which will be set in the container and in // containers built using images built from the container. // Note: this setting is not present in the OCIv1 image format, so it is // discarded when writing images using OCIv1 formats. func (b *Builder) SetComment(comment string) { if comment != "" && b.Format != define.Dockerv2ImageManifest { logrus.Warnf("COMMENT is not supported for OCI image format, comment %s will be ignored. Must use `docker` format", comment) } b.Docker.Comment = comment } // HistoryComment returns the comment which will be used in the history item // which will describe the latest layer when we commit an image. func (b *Builder) HistoryComment() string { return b.ImageHistoryComment } // SetHistoryComment sets the comment which will be used in the history item // which will describe the latest layer when we commit an image. func (b *Builder) SetHistoryComment(comment string) { b.ImageHistoryComment = comment } // StopSignal returns the signal which will be set in the container and in // containers built using images built from the container func (b *Builder) StopSignal() string { return b.Docker.Config.StopSignal } // SetStopSignal sets the signal which will be set in the container and in // containers built using images built from the container. func (b *Builder) SetStopSignal(stopSignal string) { b.OCIv1.Config.StopSignal = stopSignal b.Docker.Config.StopSignal = stopSignal } // Healthcheck returns information that recommends how a container engine // should check if a running container is "healthy". func (b *Builder) Healthcheck() *docker.HealthConfig { if b.Docker.Config.Healthcheck == nil { return nil } return &docker.HealthConfig{ Test: slices.Clone(b.Docker.Config.Healthcheck.Test), Interval: b.Docker.Config.Healthcheck.Interval, Timeout: b.Docker.Config.Healthcheck.Timeout, StartPeriod: b.Docker.Config.Healthcheck.StartPeriod, StartInterval: b.Docker.Config.Healthcheck.StartInterval, Retries: b.Docker.Config.Healthcheck.Retries, } } // SetHealthcheck sets recommended commands to run in order to verify that a // running container based on this image is "healthy", along with information // specifying how often that test should be run, and how many times the test // should fail before the container should be considered unhealthy. // Note: this setting is not present in the OCIv1 image format, so it is // discarded when writing images using OCIv1 formats. func (b *Builder) SetHealthcheck(config *docker.HealthConfig) { b.Docker.Config.Healthcheck = nil if config != nil { if b.Format != define.Dockerv2ImageManifest { b.Logger.Warnf("HEALTHCHECK is not supported for OCI image format and will be ignored. Must use `docker` format") } b.Docker.Config.Healthcheck = &docker.HealthConfig{ Test: slices.Clone(config.Test), Interval: config.Interval, Timeout: config.Timeout, StartPeriod: config.StartPeriod, StartInterval: config.StartInterval, Retries: config.Retries, } } } // AddPrependedEmptyLayer adds an item to the history that we'll create when // committing the image, after any history we inherit from the base image, but // before the history item that we'll use to describe the new layer that we're // adding. func (b *Builder) AddPrependedEmptyLayer(created *time.Time, createdBy, author, comment string) { if created != nil { copiedTimestamp := *created created = &copiedTimestamp } b.PrependedEmptyLayers = append(b.PrependedEmptyLayers, ociv1.History{ Created: created, CreatedBy: createdBy, Author: author, Comment: comment, EmptyLayer: true, }) } // ClearPrependedEmptyLayers clears the list of history entries that we'll add // to the committed image before the entry for the layer that we're adding. func (b *Builder) ClearPrependedEmptyLayers() { b.PrependedEmptyLayers = nil } // AddAppendedEmptyLayer adds an item to the history that we'll create when // committing the image, after the history item that we'll use to describe the // new layer that we're adding. func (b *Builder) AddAppendedEmptyLayer(created *time.Time, createdBy, author, comment string) { if created != nil { copiedTimestamp := *created created = &copiedTimestamp } b.AppendedEmptyLayers = append(b.AppendedEmptyLayers, ociv1.History{ Created: created, CreatedBy: createdBy, Author: author, Comment: comment, EmptyLayer: true, }) } // ClearAppendedEmptyLayers clears the list of history entries that we'll add // to the committed image after the entry for the layer that we're adding. func (b *Builder) ClearAppendedEmptyLayers() { b.AppendedEmptyLayers = nil } // AddPrependedLinkedLayer adds an item to the history that we'll create when // committing the image, optionally with a layer, after any history we inherit // from the base image, but before the history item that we'll use to describe // the new layer that we're adding. // The blobPath can be either the location of an uncompressed archive, or a // directory whose contents will be archived to use as a layer blob. Leaving // blobPath empty is functionally similar to calling AddPrependedEmptyLayer(). func (b *Builder) AddPrependedLinkedLayer(created *time.Time, createdBy, author, comment, blobPath string) { if created != nil { copiedTimestamp := *created created = &copiedTimestamp } b.PrependedLinkedLayers = append(b.PrependedLinkedLayers, LinkedLayer{ BlobPath: blobPath, History: ociv1.History{ Created: created, CreatedBy: createdBy, Author: author, Comment: comment, EmptyLayer: blobPath == "", }, }) } // ClearPrependedLinkedLayers clears the list of history entries that we'll add // the committed image before the layer that we're adding (if we're adding it). func (b *Builder) ClearPrependedLinkedLayers() { b.PrependedLinkedLayers = nil } // AddAppendedLinkedLayer adds an item to the history that we'll create when // committing the image, optionally with a layer, after the history item that // we'll use to describe the new layer that we're adding. // The blobPath can be either the location of an uncompressed archive, or a // directory whose contents will be archived to use as a layer blob. Leaving // blobPath empty is functionally similar to calling AddAppendedEmptyLayer(). func (b *Builder) AddAppendedLinkedLayer(created *time.Time, createdBy, author, comment, blobPath string) { if created != nil { copiedTimestamp := *created created = &copiedTimestamp } b.AppendedLinkedLayers = append(b.AppendedLinkedLayers, LinkedLayer{ BlobPath: blobPath, History: ociv1.History{ Created: created, CreatedBy: createdBy, Author: author, Comment: comment, EmptyLayer: blobPath == "", }, }) } // ClearAppendedLinkedLayers clears the list of linked layers that we'll add to // the committed image after the layer that we're adding (if we're adding it). func (b *Builder) ClearAppendedLinkedLayers() { b.AppendedLinkedLayers = nil } ================================================ FILE: contrib/buildahimage/README.md ================================================ The buildah container image build context and automation have been moved to [https://github.com/containers/image_build/tree/main/buildah](https://github.com/containers/image_build/tree/main/buildah) ================================================ FILE: contrib/cirrus/build.sh ================================================ #!/usr/bin/env bash set -e source $(dirname $0)/lib.sh req_env_vars IN_PODMAN IN_PODMAN_NAME GOSRC remove_packaged_buildah_files go version && go env cd "$GOSRC" if [[ "$IN_PODMAN" == "true" ]] then in_podman --rm $IN_PODMAN_NAME $0 else echo "Compiling buildah (\$GOSRC=$GOSRC)" showrun make clean all EXTRA_BUILD_TAGS="$TEST_BUILD_TAGS" echo "Installing buildah" mkdir -p bin showrun make install PREFIX=/usr showrun ./bin/buildah info fi ================================================ FILE: contrib/cirrus/lib.sh ================================================ # Library of common, shared utility functions. This file is intended # to be sourced by other scripts, not called directly. # BEGIN Global export of all variables set -a # Due to differences across platforms and runtime execution environments, # handling of the (otherwise) default shell setup is non-uniform. Rather # than attempt to workaround differences, simply force-load/set required # items every time this library is utilized. USER="$(whoami)" HOME="$(getent passwd $USER | cut -d : -f 6)" # Some platforms set and make this read-only [[ -n "$UID" ]] || \ UID=$(getent passwd $USER | cut -d : -f 3) # Automation library installed at image-build time, # defining $AUTOMATION_LIB_PATH in this file. if [[ -r "/etc/automation_environment" ]]; then source /etc/automation_environment fi # shellcheck disable=SC2154 if [[ -n "$AUTOMATION_LIB_PATH" ]]; then # shellcheck source=/usr/share/automation/lib/common_lib.sh source $AUTOMATION_LIB_PATH/common_lib.sh else ( echo "WARNING: It does not appear that containers/automation was installed." echo " Functionality of most of this library will be negatively impacted" echo " This ${BASH_SOURCE[0]} was loaded by ${BASH_SOURCE[1]}" ) > /dev/stderr fi # Required for proper GPG functioning under automation GPG_TTY="${GPG_TTY:-/dev/null}" # Essential default paths, many are overridden when executing under Cirrus-CI # others are duplicated here, to assist in debugging. GOPATH="${GOPATH:-/var/tmp/go}" if type -P go &> /dev/null then # required for go 1.12+ GOCACHE="${GOCACHE:-$HOME/.cache/go-build}" eval "$(go env)" # Ensure compiled tooling is reachable PATH="$PATH:$GOPATH/bin" fi CIRRUS_WORKING_DIR="${CIRRUS_WORKING_DIR:-$GOPATH/src/github.com/containers/buildah}" GOSRC="${GOSRC:-$CIRRUS_WORKING_DIR}" PATH="$GOSRC/tests/tools/build:$HOME/bin:$GOPATH/bin:/usr/local/bin:/usr/lib/cri-o-runc/sbin:$PATH" SCRIPT_BASE=${SCRIPT_BASE:-./contrib/cirrus} cd $GOSRC if type -P git &> /dev/null then CIRRUS_CHANGE_IN_REPO=${CIRRUS_CHANGE_IN_REPO:-$(git show-ref --hash=8 HEAD || date +%s)} else # pick something unique and obviously not from Cirrus CIRRUS_CHANGE_IN_REPO=${CIRRUS_CHANGE_IN_REPO:-unknown$(date +%s)} fi export CI="${CI:-false}" CIRRUS_CI="${CIRRUS_CI:-false}" CONTINUOUS_INTEGRATION="${CONTINUOUS_INTEGRATION:-false}" CIRRUS_REPO_NAME=${CIRRUS_REPO_NAME:-buildah} CIRRUS_BASE_SHA=${CIRRUS_BASE_SHA:-unknown$(date +%d)} # difficult to reliably discover CIRRUS_BUILD_ID=${CIRRUS_BUILD_ID:-unknown$(date +%s)} # must be short and unique enough CIRRUS_TASK_ID=${CIRRUS_BUILD_ID:-unknown$(date +%d)} # to prevent state thrashing when # debugging with `hack/get_ci_vm.sh` # All CI jobs use a local registry export CI_USE_REGISTRY_CACHE=true # Regex defining all CI-related env. vars. necessary for all possible # testing operations on all platforms and versions. This is necessary # to avoid needlessly passing through global/system values across # contexts, such as host->container or root->rootless user # # List of envariables which must be EXACT matches # N/B: Don't include BUILDAH_ISOLATION, STORAGE_DRIVER, or CGROUP_MANAGER # here because they will negatively affect execution of the rootless # integration tests. PASSTHROUGH_ENV_EXACT='BUILDAH_RUNTIME|DEST_BRANCH|DISTRO_NV|GOPATH|GOSRC|ROOTLESS_USER|SCRIPT_BASE|IN_PODMAN_IMAGE' # List of envariable patterns which must match AT THE BEGINNING of the name. PASSTHROUGH_ENV_ATSTART='CI|TEST' # List of envariable patterns which can match ANYWHERE in the name PASSTHROUGH_ENV_ANYWHERE='_NAME|_FQIN' # Combine into one PASSTHROUGH_ENV_RE="(^($PASSTHROUGH_ENV_EXACT)\$)|(^($PASSTHROUGH_ENV_ATSTART))|($PASSTHROUGH_ENV_ANYWHERE)" # Unsafe env. vars for display SECRET_ENV_RE='ACCOUNT|GC[EP]..|SSH|PASSWORD|SECRET|TOKEN' # FQINs needed for testing REGISTRY_FQIN=${REGISTRY_FQIN:-quay.io/libpod/registry:2.8.2} ALPINE_FQIN=${ALPINE_FQIN:-quay.io/libpod/alpine} # for in-container testing IN_PODMAN_NAME="in_podman_$CIRRUS_TASK_ID" IN_PODMAN="${IN_PODMAN:-false}" # rootless_user ROOTLESS_USER="rootlessuser" # Downloaded, but not installed packages. PACKAGE_DOWNLOAD_DIR=/var/cache/download lilto() { err_retry 8 1000 "" "$@"; } # just over 4 minutes max bigto() { err_retry 7 5670 "" "$@"; } # 12 minutes max # Working with apt under automation is a PITA, make it easy # Avoid some ways of getting stuck waiting for user input export DEBIAN_FRONTEND=noninteractive # Short-cut for frequently used base command export APTGET='apt-get -qq --yes' # Short timeout for quick-running packaging command SHORT_APTGET="lilto $APTGET" SHORT_DNFY="lilto dnf -y" # Longer timeout for long-running packaging command LONG_APTGET="bigto $APTGET" LONG_DNFY="bigto dnf -y" # Allow easy substitution for debugging if needed CONTAINER_RUNTIME="showrun ${CONTAINER_RUNTIME:-podman}" # END Global export of all variables set +a bad_os_id_ver() { die "Unknown/Unsupported distro. $OS_RELEASE_ID and/or version $OS_RELEASE_VER for $(basename $0)" } # Remove all files provided by the distro version of buildah. # All VM cache-images used for testing include the distro buildah because it # simplifies installing necessary dependencies which can change over time. # For general CI testing however, calling this function makes sure the system # can only run the compiled source version. remove_packaged_buildah_files() { warn "Removing packaged buildah files to prevent conflicts with source build and testing." req_env_vars OS_RELEASE_ID if [[ "$OS_RELEASE_ID" =~ "debian" ]] then LISTING_CMD="dpkg-query -L buildah" else LISTING_CMD='rpm -ql buildah' fi # yum/dnf/dpkg may list system directories, only remove files $LISTING_CMD | while read fullpath do # Sub-directories may contain unrelated/valuable stuff if [[ -d "$fullpath" ]]; then continue; fi rm -vf "$fullpath" done if [[ -z "$CONTAINER" ]]; then # Be super extra sure and careful vs performant and completely safe sync && echo 3 > /proc/sys/vm/drop_caches fi } # Return a list of environment variables that should be passed through # to lower levels (tests in containers, or via ssh to rootless). # We return the variable names only, not their values. It is up to our # caller to reference values. passthrough_envars(){ warn "Will pass env. vars. matching the following regex: $PASSTHROUGH_ENV_RE" compgen -A variable | \ grep -Ev "$SECRET_ENV_RE" | \ grep -Ev "^PASSTHROUGH_" | \ grep -E "$PASSTHROUGH_ENV_RE" } in_podman() { req_env_vars IN_PODMAN_NAME GOSRC GOPATH SECRET_ENV_RE HOME [[ -n "$@" ]] || \ die "Must specify FQIN and command with arguments to execute" # Line-separated arguments which include shell-escaped special characters declare -a envargs while read -r var; do # Pass "-e VAR" on the command line, not "-e VAR=value". Podman can # do a much better job of transmitting the value than we can, # especially when value includes spaces. envargs+=("-e" "$var") done <<<"$(passthrough_envars)" showrun podman run -i --name="$IN_PODMAN_NAME" \ --net=host \ --privileged \ --cgroupns=host \ "${envargs[@]}" \ -e BUILDAH_ISOLATION \ -e STORAGE_DRIVER \ -e "IN_PODMAN=false" \ -e "CONTAINER=podman" \ -e "CGROUP_MANAGER=cgroupfs" \ -v "$HOME/auth:$HOME/auth:ro" \ -v /sys/fs/cgroup:/sys/fs/cgroup:rw \ --device /dev/fuse:rwm \ -v "$GOSRC:$GOSRC:z" \ --workdir "$GOSRC" \ "$@" } verify_local_registry(){ # On the unexpected/rare chance of a name-clash local CUSTOM_FQIN=localhost:5000/my-alpine-$RANDOM echo "Verifying local 'registry' container is operational" showrun podman version showrun podman info showrun podman ps --all showrun podman images showrun ls -alF $HOME/auth showrun podman pull $ALPINE_FQIN showrun podman login --tls-verify=false localhost:5000 --username testuser --password testpassword showrun podman tag $ALPINE_FQIN $CUSTOM_FQIN showrun podman push --tls-verify=false --creds=testuser:testpassword $CUSTOM_FQIN showrun podman ps --all showrun podman images showrun podman rmi $ALPINE_FQIN showrun podman rmi $CUSTOM_FQIN showrun podman pull --tls-verify=false --creds=testuser:testpassword $CUSTOM_FQIN showrun podman ps --all showrun podman images echo "Success, local registry is working, cleaning up." showrun podman rmi $CUSTOM_FQIN } execute_local_registry() { if nc -4 -z 127.0.0.1 5000 then warn "Found listener on localhost:5000, NOT starting up local registry server." verify_local_registry return 0 fi req_env_vars CONTAINER_RUNTIME GOSRC local authdirpath=$HOME/auth cd $GOSRC echo "Creating a self signed certificate and get it in the right places" mkdir -p $authdirpath openssl req \ -newkey rsa:4096 -nodes -sha256 -x509 -days 2 \ -subj "/C=US/ST=Foo/L=Bar/O=Red Hat, Inc./CN=registry host certificate" \ -addext subjectAltName=DNS:localhost \ -keyout $authdirpath/domain.key \ -out $authdirpath/domain.crt cp $authdirpath/domain.crt $authdirpath/domain.cert echo "Creating http credentials file" showrun htpasswd -Bbn testuser testpassword > $authdirpath/htpasswd echo "Starting up the local 'registry' container" showrun podman run -d -p 5000:5000 --name registry \ -v $authdirpath:$authdirpath:Z \ -e "REGISTRY_AUTH=htpasswd" \ -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \ -e REGISTRY_AUTH_HTPASSWD_PATH=$authdirpath/htpasswd \ -e REGISTRY_HTTP_TLS_CERTIFICATE=$authdirpath/domain.crt \ -e REGISTRY_HTTP_TLS_KEY=$authdirpath/domain.key \ $REGISTRY_FQIN verify_local_registry } setup_rootless() { req_env_vars GOPATH GOSRC SECRET_ENV_RE local rootless_uid local rootless_gid local env_var_val local akfilepath local sshcmd # Only do this once; established by setup_environment.sh # shellcheck disable=SC2154 if passwd --status $ROOTLESS_USER then if [[ $PRIV_NAME = "rootless" ]]; then msg "Updating $ROOTLESS_USER user permissions on possibly changed libpod code" chown -R $ROOTLESS_USER:$ROOTLESS_USER "$GOPATH" "$GOSRC" return 0 fi fi msg "************************************************************" msg "Setting up rootless user '$ROOTLESS_USER'" msg "************************************************************" cd $GOSRC || exit 1 # Guarantee independence from specific values rootless_uid=$((RANDOM+1000)) rootless_gid=$((RANDOM+1000)) rootless_supplemental_gid1=$((rootless_gid+1)) rootless_supplemental_gid2=$((rootless_supplemental_gid1+1)) rootless_supplemental_gid3=$((rootless_supplemental_gid2+1)) msg "creating $rootless_uid:$rootless_gid,$rootless_supplemental_gid1,$rootless_supplemental_gid2,$rootless_supplemental_gid3 $ROOTLESS_USER user" groupadd -g $rootless_gid $ROOTLESS_USER groupadd -g $rootless_supplemental_gid1 ${ROOTLESS_USER}sg1 groupadd -g $rootless_supplemental_gid2 ${ROOTLESS_USER}sg2 groupadd -g $rootless_supplemental_gid3 ${ROOTLESS_USER}sg3 useradd -g $rootless_gid -G ${ROOTLESS_USER}sg1,${ROOTLESS_USER}sg2,${ROOTLESS_USER}sg3 -u $rootless_uid --no-user-group --create-home $ROOTLESS_USER rootless_supplemental_gid4=$(awk 'BEGIN{FS=":"}/^rootlessuser:/{print $2+$3}' /etc/subgid) groupadd -g $rootless_supplemental_gid4 ${ROOTLESS_USER}sg4 usermod -G ${ROOTLESS_USER}sg1,${ROOTLESS_USER}sg2,${ROOTLESS_USER}sg3,${ROOTLESS_USER}sg4 $ROOTLESS_USER msg "running id for $ROOTLESS_USER" id $ROOTLESS_USER # We also set up rootless user for image-scp tests (running as root) if [[ $PRIV_NAME = "rootless" ]]; then chown -R $ROOTLESS_USER:$ROOTLESS_USER "$GOPATH" "$GOSRC" fi echo "$ROOTLESS_USER ALL=(root) NOPASSWD: ALL" > /etc/sudoers.d/ci-rootless mkdir -p "$HOME/.ssh" "/home/$ROOTLESS_USER/.ssh" msg "Creating ssh key pairs" [[ -r "$HOME/.ssh/id_rsa" ]] || \ ssh-keygen -t rsa -P "" -f "$HOME/.ssh/id_rsa" ssh-keygen -t ed25519 -P "" -f "/home/$ROOTLESS_USER/.ssh/id_ed25519" ssh-keygen -t rsa -P "" -f "/home/$ROOTLESS_USER/.ssh/id_rsa" msg "Setup authorized_keys" cat $HOME/.ssh/*.pub /home/$ROOTLESS_USER/.ssh/*.pub >> $HOME/.ssh/authorized_keys cat $HOME/.ssh/*.pub /home/$ROOTLESS_USER/.ssh/*.pub >> /home/$ROOTLESS_USER/.ssh/authorized_keys msg "Ensure the ssh daemon is up and running within 5 minutes" systemctl start sshd lilto systemctl is-active sshd msg "Configure ssh file permissions" chmod -R 700 "$HOME/.ssh" chmod -R 700 "/home/$ROOTLESS_USER/.ssh" chown -R $ROOTLESS_USER:$ROOTLESS_USER "/home/$ROOTLESS_USER/.ssh" msg " setup known_hosts for $USER" ssh-keyscan localhost > /root/.ssh/known_hosts msg " setup known_hosts for $ROOTLESS_USER" install -Z -m 700 -o $ROOTLESS_USER -g $ROOTLESS_USER \ /root/.ssh/known_hosts /home/$ROOTLESS_USER/.ssh/known_hosts msg "Setting up pass-through env. vars for $ROOTLESS_USER" while read -r env_var; do # N/B: Some values contain spaces and other potential nasty-bits # (i.e. $CIRRUS_COMMIT_MESSAGE). The %q conversion ensures proper # bash-style escaping. printf -- "export %s=%q\n" "${env_var}" "${!env_var}" | tee -a /home/$ROOTLESS_USER/ci_environment done <<<"$(passthrough_envars)" } ================================================ FILE: contrib/cirrus/logcollector.sh ================================================ #!/bin/bash set -e source $(dirname $0)/lib.sh req_env_vars CI GOSRC OS_RELEASE_ID case $1 in audit) case $OS_RELEASE_ID in debian) showrun cat /var/log/kern.log ;; fedora) showrun cat /var/log/audit/audit.log ;; *) bad_os_id_ver ;; esac ;; df) showrun df -lhTx tmpfs ;; journal) showrun journalctl -b ;; podman) showrun podman system info ;; buildah_version) showrun $GOSRC/bin/buildah version;; buildah_info) showrun $GOSRC/bin/buildah info;; golang) showrun go version;; packages) # These names are common to Fedora and Debian PKG_NAMES=(\ buildah conmon container-selinux containernetworking-plugins containers-common crun cri-o-runc libseccomp libseccomp2 podman runc skopeo slirp4netns ) case $OS_RELEASE_ID in fedora*) if [[ "$OS_RELEASE_VER" -ge 36 ]]; then PKG_NAMES+=(aardvark-dns netavark) fi PKG_LST_CMD='rpm -q --qf=%{N}-%{V}-%{R}-%{ARCH}\n' ;; debian*) PKG_LST_CMD='dpkg-query --show --showformat=${Package}-${Version}-${Architecture}\n' ;; *) bad_os_id_ver ;; esac # Any not-present packages will be listed as such $PKG_LST_CMD ${PKG_NAMES[@]} | sort -u ;; *) die "Warning, $(basename $0) doesn't know how to handle the parameter '$1'" esac ================================================ FILE: contrib/cirrus/setup.sh ================================================ #!/usr/bin/env bash set -e # N/B: In most (but not all) cases, these packages will already be installed # in the VM image at build-time (from libpod repo.). Running package install # again here, ensures that all cases are covered, and there is never any # expectation mismatch. source $(dirname $0)/lib.sh req_env_vars OS_RELEASE_ID OS_RELEASE_VER GOSRC IN_PODMAN_IMAGE CIRRUS_CHANGE_TITLE msg "Running df." df -hT msg "Disabling git repository owner-check system-wide." # Newer versions of git bark if repo. files are unexpectedly owned. # This mainly affects rootless and containerized testing. But # the testing environment is disposable, so we don't care.= git config --system --add safe.directory $GOSRC # Support optional/draft testing using latest/greatest # podman-next COPR packages. This requires a draft PR # to ensure changes also pass CI w/o package updates. if [[ "$OS_RELEASE_ID" =~ "fedora" ]] && \ [[ "$CIRRUS_CHANGE_TITLE" =~ CI:NEXT ]] then # shellcheck disable=SC2154 if [[ "$CIRRUS_PR_DRAFT" != "true" ]]; then die "Magic 'CI:NEXT' string can only be used on DRAFT PRs" fi showrun dnf copr enable rhcontainerbot/podman-next -y showrun dnf upgrade -y fi msg "Setting up $OS_RELEASE_ID $OS_RELEASE_VER" cd $GOSRC case "$OS_RELEASE_ID" in fedora) warn "Hard-coding podman to use crun" cat > /etc/containers/containers.conf < /sys/block/sda/queue/scheduler warn "I/O scheduler: $(cat /sys/block/sda/queue/scheduler)" fi execute_local_registry # checks for existing port 5000 listener if [[ "$IN_PODMAN" == "true" ]] then req_env_vars IN_PODMAN_IMAGE IN_PODMAN_NAME echo "Setting up image to use for \$IN_PODMAN=true testing" cd $GOSRC in_podman $IN_PODMAN_IMAGE $0 showrun podman commit $IN_PODMAN_NAME $IN_PODMAN_NAME showrun podman rm -f $IN_PODMAN_NAME fi ================================================ FILE: contrib/cirrus/test.sh ================================================ #!/usr/bin/env bash set -e source $(dirname $0)/lib.sh req_env_vars IN_PODMAN IN_PODMAN_NAME GOSRC 1 # shellcheck disable=SC2154 if [[ "$PRIV_NAME" == "rootless" ]] && [[ "$UID" -eq 0 ]]; then # Remove /var/lib/cni, it is not required for rootless cni. # We have to test that it works without this directory. # https://github.com/containers/podman/issues/10857 rm -rf /var/lib/cni # change permission of go src and cache directory # so rootless user can access it chown -R $ROOTLESS_USER:root /var/tmp/go chmod -R g+rwx /var/tmp/go req_env_vars ROOTLESS_USER msg "Re-executing test through ssh as user '$ROOTLESS_USER'" msg "************************************************************" set -x exec ssh $ROOTLESS_USER@localhost \ -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no \ -o CheckHostIP=no $GOSRC/$SCRIPT_BASE/test.sh $1 # Does not return! elif [[ "$UID" -ne 0 ]]; then # Load important env. vars written during setup.sh (run as root) # call to setup_rootless() source /home/$ROOTLESS_USER/ci_environment fi # else: not running rootless, do nothing special msg "Test-time env. var. definitions (filtered):" show_env_vars if [[ "$IN_PODMAN" == "true" ]] then cd $GOSRC # Host build environment != container environment showrun make clean in_podman --rm $IN_PODMAN_NAME:latest $0 $1 else cd $GOSRC showrun make showrun make install.tools case $1 in validate) showrun ooe.sh git remote add upstream "$CIRRUS_REPO_CLONE_URL" showrun ooe.sh git remote update if [[ -n "$CIRRUS_PR" ]]; then echo "Validating a PR" export GITVALIDATE_EPOCH="$CIRRUS_BASE_SHA" elif [[ -n "$CIRRUS_TAG" ]]; then echo "Refusing to validating a Tag" return 0 else echo "Validating a Branch" export GITVALIDATE_EPOCH="$CIRRUS_LAST_GREEN_CHANGE" fi echo "Linting & Validating from ${GITVALIDATE_EPOCH:-default EPOCH}" showrun make lint LINTFLAGS="--timeout=20m --color=always -j1" showrun make validate ;; unit) race= if [[ -z "$CIRRUS_PR" ]]; then # If not running on a PR then run unit tests # with appropriate `-race` flags. race="-race" fi showrun make test-unit RACEFLAGS=$race ;; conformance) # Typically it's undesirable to install packages at runtime. # This test compares images built with the "latest" version # of docker, against images built with buildah. Runtime installs # are required to ensure the latest docker version is used. [[ "$OS_RELEASE_ID" == "debian" ]] || \ bad_os_id_ver systemctl enable --now docker showrun make test-conformance ;; integration) showrun make test-integration ;; *) die "First parameter to $(basename $0) not supported: '$1'" ;; esac fi ================================================ FILE: contrib/cirrus/timestamp.awk ================================================ # This script is intended to be piped into by automation, in order to # mark output lines with timing information. For example: # /path/to/command |& awk --file timestamp.awk BEGIN { STARTTIME=systime() printf "[%s] START", strftime("%T") printf " - All [+xxxx] lines that follow are relative to right now.\n" } { printf "[%+05ds] %s\n", systime()-STARTTIME, $0 } END { printf "[%s] END", strftime("%T") printf " - [%+05ds] total duration since START\n", systime()-STARTTIME } ================================================ FILE: contrib/completions/bash/buildah ================================================ # bash completion file for buildah command # # This script provides completion of: # - commands and their options # - filepaths # # To enable the completions either: # - place this file in /usr/share/bash-completion/completions # or # - copy this file to e.g. ~/.buildah-completion.sh and add the line # below to your .bashrc after bash completion features are loaded # . ~/.buildah-completion.sh # # Configuration: # # __buildah_to_alternatives transforms a multiline list of strings into a single line # string with the words separated by `|`. # This is used to prepare arguments to __buildah_pos_first_nonflag(). __buildah_to_alternatives() { local parts=( $1 ) local IFS='|' echo "${parts[*]}" } # __buildah_to_extglob transforms a multiline list of options into an extglob pattern # suitable for use in case statements. __buildah_to_extglob() { local extglob=$( __buildah_to_alternatives "$1" ) echo "@($extglob)" } # __buildah_pos_first_nonflag finds the position of the first word that is neither # option nor an option's argument. If there are options that require arguments, # you should pass a glob describing those options, e.g. "--option1|-o|--option2" # Use this function to restrict completions to exact positions after the argument list. __buildah_pos_first_nonflag() { local argument_flags=$1 local counter=$((${subcommand_pos:-${command_pos}} + 1)) while [ $counter -le $cword ]; do if [ -n "$argument_flags" ] && eval "case '${words[$counter]}' in $argument_flags) true ;; *) false ;; esac"; then (( counter++ )) # eat "=" in case of --option=arg syntax [ "${words[$counter]}" = "=" ] && (( counter++ )) else case "${words[$counter]}" in -*) ;; *) break ;; esac fi # Bash splits words at "=", retaining "=" as a word, examples: # "--log-level=error" => 3 words, "--log-opt syslog-facility=daemon" => 4 words while [ "${words[$counter + 1]}" = "=" ] ; do counter=$(( counter + 2)) done (( counter++ )) done echo $counter } # Note for developers: # Please arrange options sorted alphabetically by long name with the short # options immediately following their corresponding long form. # This order should be applied to lists, alternatives and code blocks. __buildah_previous_extglob_setting=$(shopt -p extglob) shopt -s extglob # __buildah_list_mounted __buildah_list_mounted() { COMPREPLY=($(compgen -W "$(buildah mount | awk '{print $1}')" -- $cur)) } __buildah_list_containers() { COMPREPLY=($(compgen -W "$(buildah containers --format '{{.ContainerName}} {{.ContainerID}}' )" -- $cur)) } __buildah_list_images() { COMPREPLY=($(compgen -W "$(buildah images --format '{{.ID}} {{.Name}}' )" -- $cur)) } __buildah_list_images_scratch() { COMPREPLY=($(compgen -W "$(buildah images --format '{{.ID}} {{.Name}}' ) scratch" -- $cur)) } __buildah_list_containers_images() { COMPREPLY=($(compgen -W "$(buildah containers --format '{{.ContainerName}} {{.ContainerID}}') $(buildah images --format '{{.ID}} {{.Name}}')" -- $cur)) } __buildah_pos_first_nonflag() { local argument_flags=$1 local counter=$((${subcommand_pos:-${command_pos}} + 1)) while [ $counter -le $cword ]; do if [ -n "$argument_flags" ] && eval "case '${words[$counter]}' in $argument_flags) true ;; *) false ;; esac"; then ((counter++)) else case "${words[$counter]}" in -*) ;; *) break ;; esac fi ((counter++)) done echo $counter } # Transforms a multiline list of strings into a single line string # with the words separated by "|". # This is used to prepare arguments to __buildah_pos_first_nonflag(). __buildah_to_alternatives() { local parts=($1) local IFS='|' echo "${parts[*]}" } # Transforms a multiline list of options into an extglob pattern # suitable for use in case statements. __buildah_to_extglob() { local extglob=$(__buildah_to_alternatives "$1") echo "@($extglob)" } # Subcommand processing. # Locates the first occurrence of any of the subcommands contained in the # first argument. In case of a match, calls the corresponding completion # function and returns 0. # If no match is found, 1 is returned. The calling function can then # continue processing its completion. # # TODO if the preceding command has options that accept arguments and an # argument is equal or one of the subcommands, this is falsely detected as # a match. __buildah_subcommands() { local subcommands="$1" local counter=$(($command_pos + 1)) while [ $counter -lt $cword ]; do case "${words[$counter]}" in $(__buildah_to_extglob "$subcommands") ) subcommand_pos=$counter local subcommand=${words[$counter]} local completions_func=_buildah_${command}_${subcommand} declare -F $completions_func >/dev/null && $completions_func return 0 ;; esac (( counter++ )) done return 1 } # suppress trailing whitespace __buildah_nospace() { # compopt is not available in ancient bash versions type compopt &>/dev/null && compopt -o nospace } # global options that may appear after the buildah command _buildah_buildah() { local boolean_options=" --help -h --version -v " local options_with_args=" --cgroup-manager --registries-conf --registries-conf-dir --root --runroot --storage-driver --storage-opt --userns-uid-map --userns-gid-map " case "$prev" in --root | --runroot) case "$cur" in *:*) ;; # TODO somehow do _filedir for stuff inside the image, if it's already specified (which is also somewhat difficult to determine) '') COMPREPLY=($(compgen -W '/' -- "$cur")) __buildah_nospace ;; *) _filedir __buildah_nospace ;; esac return ;; --storage-driver) COMPREPLY=($(compgen -W 'overlay' 'vfs' -- "$cur")) return ;; --cgroup-manager) COMPREPLY=($(compgen -W 'cgroupfs systemd' -- "$cur")) return ;; $(__buildah_to_extglob "$options_with_args")) return ;; esac case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; *) local counter=$(__buildah_pos_first_nonflag $(__buildah_to_extglob "$options_with_args")) if [ $cword -eq $counter ]; then COMPREPLY=($(compgen -W "${commands[*]} help" -- "$cur")) fi ;; esac } _buildah_rmi() { local boolean_options=" --all -a --prune -p --force -f --help -h " case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; *) __buildah_list_images ;; esac } _buildah_rm() { local boolean_options=" --all -a --help -h " case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; *) __buildah_list_containers ;; esac } _buildah_help() { local counter=$(__buildah_pos_first_nonflag) if [ $cword -eq $counter ]; then COMPREPLY=($(compgen -W "${commands[*]}" -- "$cur")) fi } _buildah_config() { local boolean_options=" --add-history --help -h " local options_with_args=" --annotation -a --arch --author --cmd --comment --created-by --domainname --entrypoint --env -e --healthcheck --healthcheck-interval --healthcheck-retries --healthcheck-start-period --healthcheck-timeout --history-comment --hostname --label -l --onbuild --os --port -p --shell --stop-signal --user -u --variant --volume -v --workingdir " local all_options="$options_with_args $boolean_options" case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; *) __buildah_list_containers ;; esac } _buildah_commit() { local boolean_options=" --help -h --disable-compression -D --manifest --quiet -q --rm --squash --tls-verify --omit-timestamp " local options_with_args=" --authfile --cert-dir --creds --format -f --iidfile --sign-by " local all_options="$options_with_args $boolean_options" case "$prev" in --signature-policy) case "$cur" in *:*) ;; # TODO somehow do _filedir for stuff inside the image, if it's already specified (which is also somewhat difficult to determine) '') COMPREPLY=($(compgen -W '/' -- "$cur")) __buildah_nospace ;; *) _filedir __buildah_nospace ;; esac return ;; $(__buildah_to_extglob "$options_with_args")) return ;; esac case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; *) __buildah_list_containers ;; esac } _buildah_bud() { local boolean_options=" --all-platforms --help -h --layers --no-cache --omit-timestamp --pull --pull-always --pull-never --quiet -q --squash --tls-verify " local options_with_args=" --arch --add-host --annotation --authfile --build-arg --cap-add --cap-drop --cert-dir --cgroup-parent --cpu-period --cpu-quota --cpu-shares --cpuset-cpus --cpuset-mems --creds --decryption-key --device --dns-search --dns --dns-option -f --file --format --http-proxy --ignorefile --iidfile --isolation --ipc --label --manifest -m --memory --memory-swap --net --network --no-pivot --os --pid --platform --runtime --runtime-flag --security-opt --shm-size --sign-by -t --tag --target --ulimit --userns --userns-uid-map --userns-gid-map --userns-uid-map-user --userns-gid-map-group --uts --variant --volume -v " local all_options="$options_with_args $boolean_options" case "$prev" in --runtime) COMPREPLY=($(compgen -W 'runc runv' -- "$cur")) ;; $(__buildah_to_extglob "$options_with_args")) return ;; esac case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; esac } _buildah_build_using_dockerfile() { _buildah_bud "$@" } _buildah_run() { local boolean_options=" --add-history --help -t --terminal -h " local options_with_args=" --cap-add --cap-drop --hostname --ipc --isolation --mount --net --network --pid --runtime --runtime-flag --user --uts --volume -v " local all_options="$options_with_args $boolean_options" case "$prev" in --runtime) COMPREPLY=($(compgen -W 'runc runv' -- "$cur")) ;; $(__buildah_to_extglob "$options_with_args")) return ;; esac case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; *) __buildah_list_containers ;; esac } _buildah_copy() { local boolean_options=" --add-history --help -h --quiet -q --tls-verify --remove-signatures " local options_with_args=" --chown --chmod --contextdir --ignorefile --from --authfile --cert-dir --creds --decryption-key " local all_options="$options_with_args $boolean_options" case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; esac } _buildah_add() { local boolean_options=" --add-history --help -h --quiet -q --tls-verify --remove-signatures " local options_with_args=" --chown --chmod --contextdir --ignorefile --from --authfile --cert-dir --creds --decryption-key " local all_options="$options_with_args $boolean_options" case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; esac } _buildah_unmount() { _buildah_umount $@ } _buildah_umount() { local boolean_options=" --all -a --help -h " local options_with_args=" " local all_options="$options_with_args $boolean_options" case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; *) __buildah_list_mounted ;; esac } _buildah_pull() { local boolean_options=" --all-tags -a --help -h --quiet -q --tls-verify --remove-signatures " local options_with_args=" --authfile --cert-dir --creds --decryption-key --policy " local all_options="$options_with_args $boolean_options" case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; esac } _buildah_push() { local boolean_options=" --all --help -h --disable-compression -D --quiet -q --rm --tls-verify --remove-signatures " local options_with_args=" --authfile --cert-dir --creds --encrypt-layer --encryption-key --format -f --sign-by " local all_options="$options_with_args $boolean_options" case "$prev" in --signature-policy) case "$cur" in *:*) ;; # TODO somehow do _filedir for stuff inside the image, if it's already specified (which is also somewhat difficult to determine) '') COMPREPLY=($(compgen -W '/' -- "$cur")) __buildah_nospace ;; *) _filedir __buildah_nospace ;; esac return ;; $(__buildah_to_extglob "$options_with_args")) return ;; esac case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; *) __buildah_list_images ;; esac } _buildah_logout() { local boolean_options=" --help -h --all -a " local options_with_args=" --authfile " local all_options="$options_with_args $boolean_options" case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; esac } _buildah_login() { local boolean_options=" --help -h --get-login --tls-verify " local options_with_args=" --authfile --cert-dir --password string -p --password-stdin --username -u " local all_options="$options_with_args $boolean_options" case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; esac } _buildah_manifest() { local boolean_options=" --help -h --all " subcommands=" add annotate create inspect push remove rm " __buildah_subcommands "$subcommands" && return case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options " -- "$cur")) ;; *) COMPREPLY=( $( compgen -W "$subcommands" -- "$cur" ) ) ;; esac } _buildah_manifest_add() { local boolean_options=" --help -h --all --tls-verify " local options_with_args=" --authfile --annotation --arch --cert-dir --creds --features --os --os-features --os-version --variant " local all_options="$options_with_args $boolean_options" case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; esac } _buildah_manifest_annotate() { local boolean_options=" --help -h " local options_with_args=" --annotation --arch --features --os --os-features --os-version --variant " local all_options="$options_with_args $boolean_options" case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; esac } _buildah_manifest_create() { local boolean_options=" --help -h --all " local options_with_args=" " local all_options="$options_with_args $boolean_options" case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; esac } _buildah_manifest_inspect() { local boolean_options=" --help -h " local options_with_args=" " local all_options="$options_with_args $boolean_options" case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; esac } _buildah_manifest_push() { local boolean_options=" --help -h --all --remove-signatures --tls-verify " local options_with_args=" --authfile --cert-dir --creds --digestfile --format -f --rm --sign-by " local all_options="$options_with_args $boolean_options" case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; esac } _buildah_manifest_remove() { local boolean_options=" --help -h " local options_with_args=" " local all_options="$options_with_args $boolean_options" case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; esac } _buildah_manifest_rm() { local boolean_options=" --help -h " local options_with_args=" " local all_options="$options_with_args $boolean_options" case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; esac } _buildah_mount() { local boolean_options=" --help -h --notruncate " local options_with_args=" " local all_options="$options_with_args $boolean_options" case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; *) __buildah_list_containers ;; esac } _buildah_ps() { _buildah_containers } _buildah_list() { _buildah_containers } _buildah_ls() { _buildah_containers } _buildah_containers() { local boolean_options=" --help -h --json --quiet -q --noheading -n --notruncate -a --all " local options_with_args=" --filter -f --format " local all_options="$options_with_args $boolean_options" case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; esac } _buildah_images() { local boolean_options=" --all -a --digests --help -h --history --json --quiet -q --noheading -n --no-trunc --notruncate " local options_with_args=" --filter -f --format " local all_options="$options_with_args $boolean_options" case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; esac } _buildah_info() { local options_with_args=" --log-level --D --format " local all_options="$options_with_args" case "$cur" in -*) COMPREPLY=($(compgen -W "$options_with_args" -- "$cur")) ;; esac } _buildah_inspect() { local options_with_args=" --format -f --type -t " local all_options="$options_with_args" case "$cur" in -*) COMPREPLY=($(compgen -W "$options_with_args" -- "$cur")) ;; *) __buildah_list_containers_images ;; esac } _buildah_tag() { local options_with_args=" " local all_options="$options_with_args" case "$cur" in -*) COMPREPLY=($(compgen -W "$options_with_args" -- "$cur")) ;; *) __buildah_list_images ;; esac } _buildah_from() { local boolean_options=" --help -h --pull --pull-always --pull-never --quiet -q --tls-verify " local options_with_args=" --add-host --arch --authfile --cap-add --cap-drop --cert-dir --cgroup-parent --cidfile --cpu-period --cpu-quota --cpu-shares --cpuset-cpus --cpuset-mems --creds --device --http-proxy --ipc --isolation -m --memory --memory-swap --name --net --network --os --pid --platform --security-opt --shm-size --ulimit --userns --userns-uid-map --userns-gid-map --userns-uid-map-user --userns-gid-map-group --uts --variant --volume " case "$cur" in -*) COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) ;; *) __buildah_list_images_scratch ;; esac } _buildah_unshare() { local boolean_options=" --help -h " local options_with_args=" --mount " } _buildah_rename() { local boolean_options=" --help -h " local options_with_args=" " } _buildah_version() { local boolean_options=" --help -h " local options_with_args=" " } _buildah() { local previous_extglob_setting=$(shopt -p extglob) shopt -s extglob local commands=( add bud build commit config containers copy delete from images info inspect list ls manifest mount pull push ps rename rm rmi run tag umount unmount unshare version ) COMPREPLY=() local cur prev words cword _get_comp_words_by_ref -n : cur prev words cword local command='buildah' command_pos=0 subcommand_pos local counter=1 while [ $counter -lt $cword ]; do case "${words[$counter]}" in $(__buildah_to_extglob "$global_options_with_args") ) (( counter++ )) ;; -*) ;; =) (( counter++ )) ;; *) command="${words[$counter]}" command_pos=$counter break ;; esac (( counter++ )) done local binary="${words[0]}" local completions_func=_buildah_${command/-/_} declare -F $completions_func >/dev/null && $completions_func eval "$previous_extglob_setting" return 0 } eval "$__buildah_previous_extglob_setting" unset __buildah_previous_extglob_setting complete -F _buildah buildah ================================================ FILE: convertcw.go ================================================ package buildah import ( "context" "fmt" "io" "time" "github.com/containers/buildah/define" "github.com/containers/buildah/internal/mkcw" encconfig "github.com/containers/ocicrypt/config" "github.com/opencontainers/go-digest" "github.com/sirupsen/logrus" "go.podman.io/image/v5/docker/reference" "go.podman.io/image/v5/types" "go.podman.io/storage" "go.podman.io/storage/pkg/archive" ) // CWConvertImageOptions provides both required and optional bits of // configuration for CWConvertImage(). type CWConvertImageOptions struct { // Required parameters. InputImage string // If supplied, we'll tag the resulting image with the specified name. Tag string OutputImage types.ImageReference // If supplied, we'll register the workload with this server. // Practically necessary if DiskEncryptionPassphrase is not set, in // which case we'll generate one and throw it away after. AttestationURL string // Used to measure the environment. If left unset (0), defaults will be applied. CPUs int Memory int // Can be manually set. If left unset ("", false, nil), reasonable values will be used. TeeType define.TeeType IgnoreAttestationErrors bool WorkloadID string DiskEncryptionPassphrase string Slop string FirmwareLibrary string BaseImage string Logger *logrus.Logger ExtraImageContent map[string]string // Passed through to BuilderOptions. Most settings won't make // sense to be made available here because we don't launch a process. ContainerSuffix string PullPolicy PullPolicy BlobDirectory string SignaturePolicyPath string ReportWriter io.Writer IDMappingOptions *IDMappingOptions Format string MaxPullRetries int PullRetryDelay time.Duration OciDecryptConfig *encconfig.DecryptConfig MountLabel string } // CWConvertImage takes the rootfs and configuration from one image, generates a // LUKS-encrypted disk image that more or less includes them both, and puts the // result into a new container image. // Returns the new image's ID and digest on success, along with a canonical // reference for it if a repository name was specified. func CWConvertImage(ctx context.Context, systemContext *types.SystemContext, store storage.Store, options CWConvertImageOptions) (string, reference.Canonical, digest.Digest, error) { // Apply our defaults if some options aren't set. logger := options.Logger if logger == nil { logger = logrus.StandardLogger() } // Now create the target working container, pulling the base image if // there is one and it isn't present. builderOptions := BuilderOptions{ FromImage: options.BaseImage, SystemContext: systemContext, Logger: logger, ContainerSuffix: options.ContainerSuffix, PullPolicy: options.PullPolicy, BlobDirectory: options.BlobDirectory, SignaturePolicyPath: options.SignaturePolicyPath, ReportWriter: options.ReportWriter, IDMappingOptions: options.IDMappingOptions, Format: options.Format, MaxPullRetries: options.MaxPullRetries, PullRetryDelay: options.PullRetryDelay, OciDecryptConfig: options.OciDecryptConfig, MountLabel: options.MountLabel, } target, err := NewBuilder(ctx, store, builderOptions) if err != nil { return "", nil, "", fmt.Errorf("creating container from target image: %w", err) } defer func() { if err := target.Delete(); err != nil { logrus.Warnf("deleting target container: %v", err) } }() targetDir, err := target.Mount("") if err != nil { return "", nil, "", fmt.Errorf("mounting target container: %w", err) } defer func() { if err := target.Unmount(); err != nil { logrus.Warnf("unmounting target container: %v", err) } }() // Mount the source image, pulling it first if necessary. builderOptions = BuilderOptions{ FromImage: options.InputImage, SystemContext: systemContext, Logger: logger, ContainerSuffix: options.ContainerSuffix, PullPolicy: options.PullPolicy, BlobDirectory: options.BlobDirectory, SignaturePolicyPath: options.SignaturePolicyPath, ReportWriter: options.ReportWriter, IDMappingOptions: options.IDMappingOptions, Format: options.Format, MaxPullRetries: options.MaxPullRetries, PullRetryDelay: options.PullRetryDelay, OciDecryptConfig: options.OciDecryptConfig, MountLabel: options.MountLabel, } source, err := NewBuilder(ctx, store, builderOptions) if err != nil { return "", nil, "", fmt.Errorf("creating container from source image: %w", err) } defer func() { if err := source.Delete(); err != nil { logrus.Warnf("deleting source container: %v", err) } }() sourceInfo := GetBuildInfo(source) if err != nil { return "", nil, "", fmt.Errorf("retrieving info about source image: %w", err) } sourceImageID := sourceInfo.FromImageID sourceSize, err := store.ImageSize(sourceImageID) if err != nil { return "", nil, "", fmt.Errorf("computing size of source image: %w", err) } sourceDir, err := source.Mount("") if err != nil { return "", nil, "", fmt.Errorf("mounting source container: %w", err) } defer func() { if err := source.Unmount(); err != nil { logrus.Warnf("unmounting source container: %v", err) } }() // Generate the image contents. archiveOptions := mkcw.ArchiveOptions{ AttestationURL: options.AttestationURL, CPUs: options.CPUs, Memory: options.Memory, TempDir: targetDir, TeeType: options.TeeType, IgnoreAttestationErrors: options.IgnoreAttestationErrors, ImageSize: sourceSize, WorkloadID: options.WorkloadID, DiskEncryptionPassphrase: options.DiskEncryptionPassphrase, Slop: options.Slop, FirmwareLibrary: options.FirmwareLibrary, Logger: logger, GraphOptions: store.GraphOptions(), ExtraImageContent: options.ExtraImageContent, } rc, workloadConfig, err := mkcw.Archive(sourceDir, &source.OCIv1, archiveOptions) if err != nil { return "", nil, "", fmt.Errorf("generating encrypted image content: %w", err) } if err = archive.Untar(rc, targetDir, &archive.TarOptions{}); err != nil { if err = rc.Close(); err != nil { logger.Warnf("cleaning up: %v", err) } return "", nil, "", fmt.Errorf("saving encrypted image content: %w", err) } if err = rc.Close(); err != nil { return "", nil, "", fmt.Errorf("cleaning up: %w", err) } // Commit the image. Clear out most of the configuration (if there is any — we default // to scratch as a base) so that an engine that doesn't or can't set up a TEE will just // run the static entrypoint. The rest of the configuration which the runtime consults // is in the .krun_config.json file in the encrypted filesystem. logger.Log(logrus.DebugLevel, "committing disk image") target.ClearAnnotations() target.ClearEnv() target.ClearLabels() target.ClearOnBuild() target.ClearPorts() target.ClearVolumes() target.SetCmd(nil) target.SetCreatedBy(fmt.Sprintf(": convert %q for use with %q", sourceImageID, workloadConfig.Type)) target.SetDomainname("") target.SetEntrypoint([]string{"/entrypoint"}) target.SetHealthcheck(nil) target.SetHostname("") target.SetMaintainer("") target.SetShell(nil) target.SetUser("") target.SetWorkDir("") commitOptions := CommitOptions{ SystemContext: systemContext, } if options.Tag != "" { commitOptions.AdditionalTags = append(commitOptions.AdditionalTags, options.Tag) } return target.Commit(ctx, options.OutputImage, commitOptions) } ================================================ FILE: convertcw_test.go ================================================ package buildah import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net" "net/http" "os" "path/filepath" "sync" "testing" "github.com/containers/buildah/internal/mkcw" mkcwtypes "github.com/containers/buildah/internal/mkcw/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.podman.io/storage" ) // dummyAttestationHandler replies with a fixed response code to requests to // the right path, and caches passphrases indexed by workload ID type dummyAttestationHandler struct { t *testing.T status int passphrases map[string]string passphrasesLock sync.Mutex } func (d *dummyAttestationHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { var body bytes.Buffer if req.Body != nil { if _, err := io.Copy(&body, req.Body); err != nil { d.t.Logf("reading request body: %v", err) return } req.Body.Close() } if req.URL != nil && req.URL.Path == "/kbs/v0/register_workload" { var registrationRequest mkcwtypes.RegistrationRequest // if we can't decode the client request, bail if err := json.Unmarshal(body.Bytes(), ®istrationRequest); err != nil { rw.WriteHeader(http.StatusInternalServerError) return } // cache the passphrase d.passphrasesLock.Lock() if d.passphrases == nil { d.passphrases = make(map[string]string) } d.passphrases[registrationRequest.WorkloadID] = registrationRequest.Passphrase d.passphrasesLock.Unlock() // return the predetermined status status := d.status if status == 0 { status = http.StatusOK } rw.WriteHeader(status) return } // no such handler rw.WriteHeader(http.StatusInternalServerError) } func TestCWConvertImage(t *testing.T) { // This test cannot be parallelized as this uses NewBuilder() // which eventually and indirectly accesses a global variable // defined in `go-selinux`, this must be fixed at `go-selinux` // or builder must enable sometime of locking mechanism i.e if // routine is creating Builder other's must wait for it. // Tracked here: https://github.com/containers/buildah/issues/5967 ctx := context.TODO() for _, status := range []int{http.StatusOK, http.StatusInternalServerError} { for _, ignoreChainRetrievalErrors := range []bool{false, true} { for _, ignoreAttestationErrors := range []bool{false, true} { t.Run(fmt.Sprintf("status~%d~ignoreChainRetrievalErrors~%v~ignoreAttestationErrors~%v", status, ignoreChainRetrievalErrors, ignoreAttestationErrors), func(t *testing.T) { // create a per-test Store object storeOptions := storage.StoreOptions{ GraphRoot: t.TempDir(), RunRoot: t.TempDir(), GraphDriverName: "vfs", } store, err := storage.GetStore(storeOptions) require.NoError(t, err) t.Cleanup(func() { if _, err := store.Shutdown(true); err != nil { t.Logf("store.Shutdown(%q): %v", t.Name(), err) } }) // listen on a system-assigned port listener, err := net.Listen("tcp", ":0") require.NoError(t, err) // keep track of our listener address addr := listener.Addr() // serve requests on that listener handler := &dummyAttestationHandler{t: t, status: status} server := http.Server{ Handler: handler, } go func() { if err := server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { t.Logf("serve: %v", err) } }() // clean up at the end of this test t.Cleanup(func() { assert.NoError(t, server.Close()) }) // convert an image options := CWConvertImageOptions{ InputImage: "docker.io/library/busybox", Tag: "localhost/busybox:encrypted", AttestationURL: "http://" + addr.String(), IgnoreAttestationErrors: ignoreAttestationErrors, Slop: "16MB", SignaturePolicyPath: testSystemContext.SignaturePolicyPath, } id, _, _, err := CWConvertImage(ctx, &testSystemContext, store, options) if status != http.StatusOK && !ignoreAttestationErrors { assert.Error(t, err) return } if ignoreChainRetrievalErrors && ignoreAttestationErrors { assert.NoError(t, err) } if err != nil { t.Skipf("%s: %v", t.Name(), err) return } // mount the image path, err := store.MountImage(id, nil, "") require.NoError(t, err) t.Cleanup(func() { if _, err := store.UnmountImage(id, true); err != nil { t.Logf("store.UnmountImage(%q): %v", t.Name(), err) } }) // check that the image's contents look like what we expect: disk disk := filepath.Join(path, "disk.img") require.FileExists(t, disk) workloadConfig, err := mkcw.ReadWorkloadConfigFromImage(disk) require.NoError(t, err) handler.passphrasesLock.Lock() decryptionPassphrase := handler.passphrases[workloadConfig.WorkloadID] handler.passphrasesLock.Unlock() err = mkcw.CheckLUKSPassphrase(disk, decryptionPassphrase) assert.NoError(t, err) // check that the image's contents look like what we expect: config file config := filepath.Join(path, "krun-sev.json") require.FileExists(t, config) workloadConfigBytes, err := os.ReadFile(config) require.NoError(t, err) var workloadConfigTwo mkcwtypes.WorkloadConfig err = json.Unmarshal(workloadConfigBytes, &workloadConfigTwo) require.NoError(t, err) assert.Equal(t, workloadConfig, workloadConfigTwo) // check that the image's contents look like what we expect: an executable entry point entrypoint := filepath.Join(path, "entrypoint") require.FileExists(t, entrypoint) st, err := os.Stat(entrypoint) require.NoError(t, err) assert.Equal(t, st.Mode().Type(), os.FileMode(0)) // regular file }) } } } } ================================================ FILE: copier/copier.go ================================================ package copier import ( "archive/tar" "bytes" "encoding/json" "errors" "fmt" "io" "io/fs" "net" "os" "os/user" "path" "path/filepath" "slices" "sort" "strconv" "strings" "sync" "syscall" "time" "unicode" "github.com/sirupsen/logrus" "go.podman.io/image/v5/pkg/compression" "go.podman.io/storage/pkg/archive" "go.podman.io/storage/pkg/fileutils" "go.podman.io/storage/pkg/idtools" "go.podman.io/storage/pkg/reexec" ) const ( copierCommand = "buildah-copier" maxLoopsFollowed = 64 // See http://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html#tag_20_92_13_06, from archive/tar cISUID = 0o4000 // Set uid, from archive/tar cISGID = 0o2000 // Set gid, from archive/tar cISVTX = 0o1000 // Save text (sticky bit), from archive/tar // xattrs in the PAXRecords map are namespaced with this prefix xattrPAXRecordNamespace = "SCHILY.xattr." ) func init() { reexec.Register(copierCommand, copierMain) } // extendedGlob calls filepath.Glob() on the passed-in patterns. If there is a // "**" component in the pattern, filepath.Glob() will be called with the "**" // replaced with all of the subdirectories under that point, and the results // will be concatenated. // The matched paths are returned in lexical order, which makes the output deterministic. func extendedGlob(pattern string) (matches []string, err error) { subdirs := func(dir string) []string { var subdirectories []string if err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { if err != nil { return nil } if d.IsDir() { if rel, err := filepath.Rel(dir, path); err == nil { subdirectories = append(subdirectories, rel) } } return nil }); err != nil { subdirectories = []string{"."} } return subdirectories } expandPatterns := func(pattern string) []string { components := []string{} dir := pattern file := "" for dir != filepath.VolumeName(dir) && dir != string(os.PathSeparator) { dir, file = filepath.Split(dir) if file != "" { components = append([]string{file}, components...) } dir = strings.TrimSuffix(dir, string(os.PathSeparator)) } patterns := []string{filepath.VolumeName(dir) + string(os.PathSeparator)} for i := range components { var nextPatterns []string if components[i] == "**" { for _, parent := range patterns { nextSubdirs := subdirs(parent) for _, nextSubdir := range nextSubdirs { nextPatterns = append(nextPatterns, filepath.Join(parent, nextSubdir)) } } } else { for _, parent := range patterns { nextPattern := filepath.Join(parent, components[i]) nextPatterns = append(nextPatterns, nextPattern) } } patterns = []string{} seen := map[string]struct{}{} for _, nextPattern := range nextPatterns { if _, seen := seen[nextPattern]; seen { continue } patterns = append(patterns, nextPattern) seen[nextPattern] = struct{}{} } } return patterns } patterns := expandPatterns(pattern) for _, pattern := range patterns { theseMatches, err := filepath.Glob(pattern) if err != nil { return nil, err } matches = append(matches, theseMatches...) } sort.Strings(matches) return matches, nil } // isArchivePath returns true if the specified path can be read like a (possibly // compressed) tarball. func isArchivePath(path string) bool { f, err := os.Open(path) if err != nil { return false } defer f.Close() rc, _, err := compression.AutoDecompress(f) if err != nil { return false } defer rc.Close() tr := tar.NewReader(rc) _, err = tr.Next() return err == nil } // requestType encodes exactly what kind of request this is. type requestType string const ( requestEval requestType = "EVAL" requestStat requestType = "STAT" requestGet requestType = "GET" requestPut requestType = "PUT" requestMkdir requestType = "MKDIR" requestRemove requestType = "REMOVE" requestQuit requestType = "QUIT" requestEnsure requestType = "ENSURE" requestConditionalRemove requestType = "CONDRM" ) // Request encodes a single request. type request struct { Request requestType Root string // used by all requests preservedRoot string rootPrefix string // used to reconstruct paths being handed back to the caller Directory string // used by all requests preservedDirectory string Globs []string `json:",omitempty"` // used by stat, get preservedGlobs []string StatOptions StatOptions GetOptions GetOptions PutOptions PutOptions MkdirOptions MkdirOptions RemoveOptions RemoveOptions EnsureOptions EnsureOptions ConditionalRemoveOptions ConditionalRemoveOptions } func (req *request) Excludes() []string { switch req.Request { case requestEval: return nil case requestStat: return req.StatOptions.Excludes case requestGet: return req.GetOptions.Excludes case requestPut: return nil case requestMkdir: return nil case requestRemove: return nil case requestQuit: return nil case requestEnsure: return nil case requestConditionalRemove: return nil default: panic(fmt.Sprintf("not an implemented request type: %q", req.Request)) } } func (req *request) UIDMap() []idtools.IDMap { switch req.Request { case requestEval: return nil case requestStat: return req.StatOptions.UIDMap case requestGet: return req.GetOptions.UIDMap case requestPut: return req.PutOptions.UIDMap case requestMkdir: return req.MkdirOptions.UIDMap case requestRemove: return nil case requestQuit: return nil case requestEnsure: return req.EnsureOptions.UIDMap case requestConditionalRemove: return req.ConditionalRemoveOptions.UIDMap default: panic(fmt.Sprintf("not an implemented request type: %q", req.Request)) } } func (req *request) GIDMap() []idtools.IDMap { switch req.Request { case requestEval: return nil case requestStat: return req.StatOptions.GIDMap case requestGet: return req.GetOptions.GIDMap case requestPut: return req.PutOptions.GIDMap case requestMkdir: return req.MkdirOptions.GIDMap case requestRemove: return nil case requestQuit: return nil case requestEnsure: return req.EnsureOptions.GIDMap case requestConditionalRemove: return req.ConditionalRemoveOptions.GIDMap default: panic(fmt.Sprintf("not an implemented request type: %q", req.Request)) } } // Response encodes a single response. type response struct { Error string `json:",omitempty"` Stat statResponse Eval evalResponse Get getResponse Put putResponse Mkdir mkdirResponse Remove removeResponse Ensure ensureResponse ConditionalRemove conditionalRemoveResponse } // statResponse encodes a response for a single Stat request. type statResponse struct { Globs []*StatsForGlob } // evalResponse encodes a response for a single Eval request. type evalResponse struct { Evaluated string } // StatsForGlob encode results for a single glob pattern passed to Stat(). type StatsForGlob struct { Error string `json:",omitempty"` // error if the Glob pattern was malformed Glob string // input pattern to which this result corresponds Globbed []string // a slice of zero or more names that match the glob Results map[string]*StatForItem // one for each Globbed value if there are any, or for Glob } // StatForItem encode results for a single filesystem item, as returned by Stat(). type StatForItem struct { Error string `json:",omitempty"` Name string Size int64 // dereferenced value for symlinks Mode os.FileMode // dereferenced value for symlinks ModTime time.Time // dereferenced value for symlinks UID, GID int64 // usually in the uint32 range, set to -1 if unknown IsSymlink bool IsDir bool // dereferenced value for symlinks IsRegular bool // dereferenced value for symlinks IsArchive bool // dereferenced value for symlinks ImmediateTarget string `json:",omitempty"` // raw link content } // getResponse encodes a response for a single Get request. type getResponse struct{} // putResponse encodes a response for a single Put request. type putResponse struct{} // mkdirResponse encodes a response for a single Mkdir request. type mkdirResponse struct{} // removeResponse encodes a response for a single Remove request. type removeResponse struct{} // ensureResponse encodes a response to an Ensure request. type ensureResponse struct { Created []string // paths that were created because they weren't already present Noted []EnsureParentPath // preexisting paths that are parents of created items } // conditionalRemoveResponse encodes a response to a conditionalRemove request. type conditionalRemoveResponse struct { Removed []string // paths that were removed } // EvalOptions controls parts of Eval()'s behavior. type EvalOptions struct{} // Eval evaluates the directory's path, including any intermediate symbolic // links. // If root is specified and the current OS supports it, and the calling process // has the necessary privileges, evaluation is performed in a chrooted context. // If the directory is specified as an absolute path, it should either be the // root directory or a subdirectory of the root directory. Otherwise, the // directory is treated as a path relative to the root directory. func Eval(root string, directory string, _ EvalOptions) (string, error) { req := request{ Request: requestEval, Root: root, Directory: directory, } resp, err := copier(nil, nil, req) if err != nil { return "", err } if resp.Error != "" { return "", errors.New(resp.Error) } return resp.Eval.Evaluated, nil } // StatOptions controls parts of Stat()'s behavior. type StatOptions struct { UIDMap, GIDMap []idtools.IDMap // map from hostIDs to containerIDs when returning results CheckForArchives bool // check for and populate the IsArchive bit in returned values Excludes []string // contents to pretend don't exist, using the OS-specific path separator } // Stat globs the specified pattern in the specified directory and returns its // results. // If root and directory are both not specified, the current root directory is // used, and relative names in the globs list are treated as being relative to // the current working directory. // If root is specified and the current OS supports it, and the calling process // has the necessary privileges, the stat() is performed in a chrooted context. // If the directory is specified as an absolute path, it should either be the // root directory or a subdirectory of the root directory. Otherwise, the // directory is treated as a path relative to the root directory. // Relative names in the glob list are treated as being relative to the // directory. func Stat(root string, directory string, options StatOptions, globs []string) ([]*StatsForGlob, error) { req := request{ Request: requestStat, Root: root, Directory: directory, Globs: slices.Clone(globs), StatOptions: options, } resp, err := copier(nil, nil, req) if err != nil { return nil, err } if resp.Error != "" { return nil, errors.New(resp.Error) } return resp.Stat.Globs, nil } // GetOptions controls parts of Get()'s behavior. type GetOptions struct { UIDMap, GIDMap []idtools.IDMap // map from hostIDs to containerIDs in the output archive Excludes []string // contents to pretend don't exist, using the OS-specific path separator ExpandArchives bool // extract the contents of named items that are archives ChownDirs *idtools.IDPair // set ownership on directories. no effect on archives being extracted ChmodDirs *os.FileMode // set permissions on directories. no effect on archives being extracted ChownFiles *idtools.IDPair // set ownership of files. no effect on archives being extracted ChmodFiles *os.FileMode // set permissions on files. no effect on archives being extracted Parents bool // maintain the sources parent directory in the destination StripSetuidBit bool // strip the setuid bit off of items being copied. no effect on archives being extracted StripSetgidBit bool // strip the setgid bit off of items being copied. no effect on archives being extracted StripStickyBit bool // strip the sticky bit off of items being copied. no effect on archives being extracted StripXattrs bool // don't record extended attributes of items being copied. no effect on archives being extracted KeepDirectoryNames bool // don't strip the top directory's basename from the paths of items in subdirectories Rename map[string]string // rename items with the specified names, or under the specified names NoDerefSymlinks bool // don't follow symlinks when globs match them IgnoreUnreadable bool // ignore errors reading items, instead of returning an error NoCrossDevice bool // if a subdirectory is a mountpoint with a different device number, include it but skip its contents Timestamp *time.Time // timestamp to force on all contents } // Get produces an archive containing items that match the specified glob // patterns and writes it to bulkWriter. // If root and directory are both not specified, the current root directory is // used, and relative names in the globs list are treated as being relative to // the current working directory. // If root is specified and the current OS supports it, and the calling process // has the necessary privileges, the contents are read in a chrooted context. // If the directory is specified as an absolute path, it should either be the // root directory or a subdirectory of the root directory. Otherwise, the // directory is treated as a path relative to the root directory. // Relative names in the glob list are treated as being relative to the // directory. func Get(root string, directory string, options GetOptions, globs []string, bulkWriter io.Writer) error { req := request{ Request: requestGet, Root: root, Directory: directory, Globs: slices.Clone(globs), StatOptions: StatOptions{ CheckForArchives: options.ExpandArchives, }, GetOptions: options, } resp, err := copier(nil, bulkWriter, req) if err != nil { return err } if resp.Error != "" { return errors.New(resp.Error) } return nil } // PutOptions controls parts of Put()'s behavior. type PutOptions struct { UIDMap, GIDMap []idtools.IDMap // map from containerIDs to hostIDs when writing contents to disk DefaultDirOwner *idtools.IDPair // set ownership of implicitly-created directories, default is ChownDirs, or 0:0 if ChownDirs not set DefaultDirMode *os.FileMode // set permissions on implicitly-created directories, default is ChmodDirs, or 0755 if ChmodDirs not set ChownDirs *idtools.IDPair // set ownership of newly-created directories ChmodDirs *os.FileMode // set permissions on newly-created directories ChownFiles *idtools.IDPair // set ownership of newly-created files ChmodFiles *os.FileMode // set permissions on newly-created files StripSetuidBit bool // strip the setuid bit off of items being written StripSetgidBit bool // strip the setgid bit off of items being written StripStickyBit bool // strip the sticky bit off of items being written StripXattrs bool // don't bother trying to set extended attributes of items being copied IgnoreXattrErrors bool // ignore any errors encountered when attempting to set extended attributes IgnoreDevices bool // ignore items which are character or block devices NoOverwriteDirNonDir bool // instead of quietly overwriting directories with non-directories, return an error NoOverwriteNonDirDir bool // instead of quietly overwriting non-directories with directories, return an error Rename map[string]string // rename items with the specified names, or under the specified names } // Put extracts an archive from the bulkReader at the specified directory. // If root and directory are both not specified, the current root directory is // used. // If root is specified and the current OS supports it, and the calling process // has the necessary privileges, the contents are written in a chrooted // context. If the directory is specified as an absolute path, it should // either be the root directory or a subdirectory of the root directory. // Otherwise, the directory is treated as a path relative to the root // directory. func Put(root string, directory string, options PutOptions, bulkReader io.Reader) error { req := request{ Request: requestPut, Root: root, Directory: directory, PutOptions: options, } resp, err := copier(bulkReader, nil, req) if err != nil { return err } if resp.Error != "" { return errors.New(resp.Error) } return nil } // MkdirOptions controls parts of Mkdir()'s behavior. type MkdirOptions struct { UIDMap, GIDMap []idtools.IDMap // map from containerIDs to hostIDs when creating directories ModTimeNew *time.Time // set mtime and atime of newly-created directories ChownNew *idtools.IDPair // set ownership of newly-created directories ChmodNew *os.FileMode // set permissions on newly-created directories } // Mkdir ensures that the specified directory exists. Any directories which // need to be created will be given the specified ownership and permissions. // If root and directory are both not specified, the current root directory is // used. // If root is specified and the current OS supports it, and the calling process // has the necessary privileges, the directory is created in a chrooted // context. If the directory is specified as an absolute path, it should // either be the root directory or a subdirectory of the root directory. // Otherwise, the directory is treated as a path relative to the root // directory. func Mkdir(root string, directory string, options MkdirOptions) error { req := request{ Request: requestMkdir, Root: root, Directory: directory, MkdirOptions: options, } resp, err := copier(nil, nil, req) if err != nil { return err } if resp.Error != "" { return errors.New(resp.Error) } return nil } // RemoveOptions controls parts of Remove()'s behavior. type RemoveOptions struct { All bool // if Directory is a directory, remove its contents as well } // Remove removes the specified directory or item, traversing any intermediate // symbolic links. // If the root directory is not specified, the current root directory is used. // If root is specified and the current OS supports it, and the calling process // has the necessary privileges, the remove() is performed in a chrooted context. // If the item to remove is specified as an absolute path, it should either be // in the root directory or in a subdirectory of the root directory. Otherwise, // the directory is treated as a path relative to the root directory. func Remove(root string, item string, options RemoveOptions) error { req := request{ Request: requestRemove, Root: root, Directory: item, RemoveOptions: options, } resp, err := copier(nil, nil, req) if err != nil { return err } if resp.Error != "" { return errors.New(resp.Error) } return nil } // cleanerReldirectory resolves relative path candidate lexically, attempting // to ensure that when joined as a subdirectory of another directory, it does // not reference anything outside of that other directory. func cleanerReldirectory(candidate string) string { cleaned := strings.TrimPrefix(filepath.Clean(string(os.PathSeparator)+candidate), string(os.PathSeparator)) if cleaned == "" { return "." } return cleaned } // convertToRelSubdirectory returns the path of directory, bound and relative to // root, as a relative path, or an error if that path can't be computed or if // the two directories are on different volumes func convertToRelSubdirectory(root, directory string) (relative string, err error) { if root == "" || !filepath.IsAbs(root) { return "", fmt.Errorf("expected root directory to be an absolute path, got %q", root) } if directory == "" || !filepath.IsAbs(directory) { return "", fmt.Errorf("expected directory to be an absolute path, got %q", root) } if filepath.VolumeName(root) != filepath.VolumeName(directory) { return "", fmt.Errorf("%q and %q are on different volumes", root, directory) } rel, err := filepath.Rel(root, directory) if err != nil { return "", fmt.Errorf("computing path of %q relative to %q: %w", directory, root, err) } return cleanerReldirectory(rel), nil } func currentVolumeRoot() (string, error) { cwd, err := os.Getwd() if err != nil { return "", fmt.Errorf("getting current working directory: %w", err) } return filepath.VolumeName(cwd) + string(os.PathSeparator), nil } func isVolumeRoot(candidate string) (bool, error) { abs, err := filepath.Abs(candidate) if err != nil { return false, fmt.Errorf("converting %q to an absolute path: %w", candidate, err) } return abs == filepath.VolumeName(abs)+string(os.PathSeparator), nil } func looksLikeAbs(candidate string) bool { return candidate[0] == os.PathSeparator && (len(candidate) == 1 || candidate[1] != os.PathSeparator) } func copier(bulkReader io.Reader, bulkWriter io.Writer, req request) (*response, error) { if req.Directory == "" { if req.Root == "" { wd, err := os.Getwd() if err != nil { return nil, fmt.Errorf("getting current working directory: %w", err) } req.Directory = wd } else { req.Directory = req.Root } } if req.Root == "" { root, err := currentVolumeRoot() if err != nil { return nil, fmt.Errorf("determining root of current volume: %w", err) } req.Root = root } if filepath.IsAbs(req.Directory) { _, err := convertToRelSubdirectory(req.Root, req.Directory) if err != nil { return nil, fmt.Errorf("rewriting %q to be relative to %q: %w", req.Directory, req.Root, err) } } isAlreadyRoot, err := isVolumeRoot(req.Root) if err != nil { return nil, fmt.Errorf("checking if %q is a root directory: %w", req.Root, err) } if !isAlreadyRoot && canChroot { return copierWithSubprocess(bulkReader, bulkWriter, req) } return copierWithoutSubprocess(bulkReader, bulkWriter, req) } func copierWithoutSubprocess(bulkReader io.Reader, bulkWriter io.Writer, req request) (*response, error) { req.preservedRoot = req.Root req.rootPrefix = string(os.PathSeparator) req.preservedDirectory = req.Directory req.preservedGlobs = slices.Clone(req.Globs) if !filepath.IsAbs(req.Directory) { req.Directory = filepath.Join(req.Root, cleanerReldirectory(req.Directory)) } absoluteGlobs := make([]string, 0, len(req.Globs)) for _, glob := range req.preservedGlobs { if filepath.IsAbs(glob) { relativeGlob, err := convertToRelSubdirectory(req.preservedRoot, glob) if err != nil { fmt.Fprintf(os.Stderr, "error rewriting %q to be relative to %q: %v", glob, req.preservedRoot, err) os.Exit(1) } absoluteGlobs = append(absoluteGlobs, filepath.Join(req.Root, string(os.PathSeparator)+relativeGlob)) } else { absoluteGlobs = append(absoluteGlobs, filepath.Join(req.Directory, cleanerReldirectory(glob))) } } req.Globs = absoluteGlobs resp, cb, err := copierHandler(bulkReader, bulkWriter, req) if err != nil { return nil, err } if cb != nil { if err = cb(); err != nil { return nil, err } } return resp, nil } func closeIfNotNilYet(f **os.File, what string) { if f != nil && *f != nil { err := (*f).Close() *f = nil if err != nil { logrus.Debugf("error closing %s: %v", what, err) } } } func copierWithSubprocess(bulkReader io.Reader, bulkWriter io.Writer, req request) (resp *response, err error) { if bulkReader == nil { bulkReader = bytes.NewReader([]byte{}) } if bulkWriter == nil { bulkWriter = io.Discard } cmd := reexec.Command(copierCommand) stdinRead, stdinWrite, err := os.Pipe() if err != nil { return nil, fmt.Errorf("pipe: %w", err) } defer closeIfNotNilYet(&stdinRead, "stdin pipe reader") defer closeIfNotNilYet(&stdinWrite, "stdin pipe writer") encoder := json.NewEncoder(stdinWrite) stdoutRead, stdoutWrite, err := os.Pipe() if err != nil { return nil, fmt.Errorf("pipe: %w", err) } defer closeIfNotNilYet(&stdoutRead, "stdout pipe reader") defer closeIfNotNilYet(&stdoutWrite, "stdout pipe writer") decoder := json.NewDecoder(stdoutRead) bulkReaderRead, bulkReaderWrite, err := os.Pipe() if err != nil { return nil, fmt.Errorf("pipe: %w", err) } defer closeIfNotNilYet(&bulkReaderRead, "child bulk content reader pipe, read end") defer closeIfNotNilYet(&bulkReaderWrite, "child bulk content reader pipe, write end") bulkWriterRead, bulkWriterWrite, err := os.Pipe() if err != nil { return nil, fmt.Errorf("pipe: %w", err) } defer closeIfNotNilYet(&bulkWriterRead, "child bulk content writer pipe, read end") defer closeIfNotNilYet(&bulkWriterWrite, "child bulk content writer pipe, write end") cmd.Dir = "/" cmd.Env = append([]string{fmt.Sprintf("LOGLEVEL=%d", logrus.GetLevel())}, os.Environ()...) errorBuffer := bytes.Buffer{} cmd.Stdin = stdinRead cmd.Stdout = stdoutWrite cmd.Stderr = &errorBuffer cmd.ExtraFiles = []*os.File{bulkReaderRead, bulkWriterWrite} if err = cmd.Start(); err != nil { return nil, fmt.Errorf("starting subprocess: %w", err) } cmdToWaitFor := cmd defer func() { if cmdToWaitFor != nil { if err := cmdToWaitFor.Wait(); err != nil { if errorBuffer.String() != "" { logrus.Debug(errorBuffer.String()) } } } }() stdinRead.Close() stdinRead = nil stdoutWrite.Close() stdoutWrite = nil bulkReaderRead.Close() bulkReaderRead = nil bulkWriterWrite.Close() bulkWriterWrite = nil killAndReturn := func(err error, step string) error { if err2 := cmd.Process.Kill(); err2 != nil { return fmt.Errorf("killing subprocess: %v; %s: %w", err2, step, err) } if errors.Is(err, io.ErrClosedPipe) || errors.Is(err, syscall.EPIPE) { err2 := cmd.Wait() if errorText := strings.TrimFunc(errorBuffer.String(), unicode.IsSpace); errorText != "" { err = fmt.Errorf("%s: %w", errorText, err) } if err2 != nil { return fmt.Errorf("waiting on subprocess: %v; %s: %w", err2, step, err) } } return fmt.Errorf("%v: %w", step, err) } if err = encoder.Encode(req); err != nil { return nil, killAndReturn(err, "error encoding work request for copier subprocess") } if err = decoder.Decode(&resp); err != nil { if errors.Is(err, io.EOF) && errorBuffer.Len() > 0 { return nil, killAndReturn(errors.New(errorBuffer.String()), "error in copier subprocess") } return nil, killAndReturn(err, "error decoding response from copier subprocess") } if err = encoder.Encode(&request{Request: requestQuit}); err != nil { return nil, killAndReturn(err, "error encoding quit request for copier subprocess") } stdinWrite.Close() stdinWrite = nil stdoutRead.Close() stdoutRead = nil var wg sync.WaitGroup var readError, writeError error wg.Add(1) go func() { _, writeError = io.Copy(bulkWriter, bulkWriterRead) bulkWriterRead.Close() bulkWriterRead = nil wg.Done() }() wg.Add(1) go func() { _, readError = io.Copy(bulkReaderWrite, bulkReader) bulkReaderWrite.Close() bulkReaderWrite = nil wg.Done() }() wg.Wait() cmdToWaitFor = nil if err = cmd.Wait(); err != nil { if errorBuffer.String() != "" { err = fmt.Errorf("%s", errorBuffer.String()) } return nil, err } if cmd.ProcessState.Exited() && !cmd.ProcessState.Success() { err = fmt.Errorf("subprocess exited with error") if errorBuffer.String() != "" { err = fmt.Errorf("%s", errorBuffer.String()) } return nil, err } loggedOutput := strings.TrimSuffix(errorBuffer.String(), "\n") if len(loggedOutput) > 0 { for output := range strings.SplitSeq(loggedOutput, "\n") { logrus.Debug(output) } } if readError != nil { return nil, fmt.Errorf("passing bulk input to subprocess: %w", readError) } if writeError != nil { return nil, fmt.Errorf("passing bulk output from subprocess: %w", writeError) } return resp, nil } func copierMain() { var chrooted bool decoder := json.NewDecoder(os.Stdin) encoder := json.NewEncoder(os.Stdout) previousRequestRoot := "" // Attempt a user and host lookup to force libc (glibc, and possibly others that use dynamic // modules to handle looking up user and host information) to load modules that match the libc // our binary is currently using. Hopefully they're loaded on first use, so that they won't // need to be loaded after we've chrooted into the rootfs, which could include modules that // don't match our libc and which can't be loaded, or modules which we don't want to execute // because we don't trust their code. _, _ = user.Lookup("buildah") _, _ = net.LookupHost("localhost") // Set logging. if level := os.Getenv("LOGLEVEL"); level != "" { if ll, err := strconv.Atoi(level); err == nil { logrus.SetLevel(logrus.Level(ll)) } } // Set up descriptors for receiving and sending tarstreams. bulkReader := os.NewFile(3, "bulk-reader") bulkWriter := os.NewFile(4, "bulk-writer") for { // Read a request. req := new(request) if err := decoder.Decode(req); err != nil { fmt.Fprintf(os.Stderr, "error decoding request from copier parent process: %v", err) os.Exit(1) } if req.Request == requestQuit { // Making Quit a specific request means that we could // run Stat() at a caller's behest before using the // same process for Get() or Put(). Maybe later. break } // Multiple requests should list the same root, because we // can't un-chroot to chroot to some other location. if previousRequestRoot != "" { // Check that we got the same input value for // where-to-chroot-to. if req.Root != previousRequestRoot { fmt.Fprintf(os.Stderr, "error: can't change location of chroot from %q to %q", previousRequestRoot, req.Root) os.Exit(1) } previousRequestRoot = req.Root } else { // Figure out where to chroot to, if we weren't told. if req.Root == "" { root, err := currentVolumeRoot() if err != nil { fmt.Fprintf(os.Stderr, "error determining root of current volume: %v", err) os.Exit(1) } req.Root = root } // Change to the specified root directory. var err error chrooted, err = chroot(req.Root) if err != nil { fmt.Fprintf(os.Stderr, "%v", err) os.Exit(1) } } req.preservedRoot = req.Root req.rootPrefix = string(os.PathSeparator) req.preservedDirectory = req.Directory req.preservedGlobs = slices.Clone(req.Globs) if chrooted { // We'll need to adjust some things now that the root // directory isn't what it was. Make the directory and // globs absolute paths for simplicity's sake. absoluteDirectory := req.Directory if !filepath.IsAbs(req.Directory) { absoluteDirectory = filepath.Join(req.Root, cleanerReldirectory(req.Directory)) } relativeDirectory, err := convertToRelSubdirectory(req.preservedRoot, absoluteDirectory) if err != nil { fmt.Fprintf(os.Stderr, "error rewriting %q to be relative to %q: %v", absoluteDirectory, req.preservedRoot, err) os.Exit(1) } req.Directory = filepath.Clean(string(os.PathSeparator) + relativeDirectory) absoluteGlobs := make([]string, 0, len(req.Globs)) for i, glob := range req.preservedGlobs { if filepath.IsAbs(glob) { relativeGlob, err := convertToRelSubdirectory(req.preservedRoot, glob) if err != nil { fmt.Fprintf(os.Stderr, "error rewriting %q to be relative to %q: %v", glob, req.preservedRoot, err) os.Exit(1) } absoluteGlobs = append(absoluteGlobs, filepath.Clean(string(os.PathSeparator)+relativeGlob)) } else { absoluteGlobs = append(absoluteGlobs, filepath.Join(req.Directory, cleanerReldirectory(req.Globs[i]))) } } req.Globs = absoluteGlobs req.rootPrefix = req.Root req.Root = string(os.PathSeparator) } else { // Make the directory and globs absolute paths for // simplicity's sake. if !filepath.IsAbs(req.Directory) { req.Directory = filepath.Join(req.Root, cleanerReldirectory(req.Directory)) } absoluteGlobs := make([]string, 0, len(req.Globs)) for i, glob := range req.preservedGlobs { if filepath.IsAbs(glob) { absoluteGlobs = append(absoluteGlobs, req.Globs[i]) } else { absoluteGlobs = append(absoluteGlobs, filepath.Join(req.Directory, cleanerReldirectory(req.Globs[i]))) } } req.Globs = absoluteGlobs } resp, cb, err := copierHandler(bulkReader, bulkWriter, *req) if err != nil { fmt.Fprintf(os.Stderr, "error handling request %#v from copier parent process: %v", *req, err) os.Exit(1) } // Encode the response. if err := encoder.Encode(resp); err != nil { fmt.Fprintf(os.Stderr, "error encoding response %#v for copier parent process: %v", *req, err) os.Exit(1) } // If there's bulk data to transfer, run the callback to either // read or write it. if cb != nil { if err = cb(); err != nil { fmt.Fprintf(os.Stderr, "error during bulk transfer for %#v: %v", *req, err) os.Exit(1) } } } } func copierHandler(bulkReader io.Reader, bulkWriter io.Writer, req request) (*response, func() error, error) { // NewPatternMatcher splits patterns into components using // os.PathSeparator, implying that it expects OS-specific naming // conventions. excludes := req.Excludes() pm, err := fileutils.NewPatternMatcher(excludes) if err != nil { return nil, nil, fmt.Errorf("processing excludes list %v: %w", excludes, err) } var idMappings *idtools.IDMappings uidMap, gidMap := req.UIDMap(), req.GIDMap() if len(uidMap) > 0 && len(gidMap) > 0 { idMappings = idtools.NewIDMappingsFromMaps(uidMap, gidMap) } switch req.Request { default: return nil, nil, fmt.Errorf("not an implemented request type: %q", req.Request) case requestEval: resp := copierHandlerEval(req) return resp, nil, nil case requestStat: resp := copierHandlerStat(req, pm, idMappings) return resp, nil, nil case requestGet: return copierHandlerGet(bulkWriter, req, pm, idMappings) case requestPut: return copierHandlerPut(bulkReader, req, idMappings) case requestMkdir: return copierHandlerMkdir(req, idMappings) case requestRemove: resp := copierHandlerRemove(req) return resp, nil, nil case requestEnsure: resp := copierHandlerEnsure(req, idMappings) return resp, nil, nil case requestConditionalRemove: resp := copierHandlerConditionalRemove(req, idMappings) return resp, nil, nil case requestQuit: return nil, nil, nil } } // pathIsExcluded computes path relative to root, then asks the pattern matcher // if the result is excluded. Returns the relative path and the matcher's // results. func pathIsExcluded(root, path string, pm *fileutils.PatternMatcher) (string, bool, error) { rel, err := convertToRelSubdirectory(root, path) if err != nil { return "", false, fmt.Errorf("copier: error computing path of %q relative to root %q: %w", path, root, err) } if pm == nil { return rel, false, nil } if rel == "." { // special case return rel, false, nil } // Matches uses filepath.FromSlash() to convert candidates before // checking if they match the patterns it's been given, implying that // it expects Unix-style paths. matches, err := pm.Matches(filepath.ToSlash(rel)) //nolint:staticcheck if err != nil { return rel, false, fmt.Errorf("copier: error checking if %q is excluded: %w", rel, err) } if matches { return rel, true, nil } return rel, false, nil } // resolvePath resolves symbolic links in paths, treating the specified // directory as the root. // Resolving the path this way, and using the result, is in no way secure // against another process manipulating the content that we're looking at, and // it is not expected to be. // This helps us approximate chrooted behavior on systems and in test cases // where chroot isn't available. func resolvePath(root, path string, evaluateFinalComponent bool, pm *fileutils.PatternMatcher) (string, error) { rel, err := convertToRelSubdirectory(root, path) if err != nil { return "", fmt.Errorf("making path %q relative to %q", path, root) } workingPath := root followed := 0 components := strings.Split(rel, string(os.PathSeparator)) excluded := false for len(components) > 0 { // if anything we try to examine is excluded, then resolution has to "break" _, thisExcluded, err := pathIsExcluded(root, filepath.Join(workingPath, components[0]), pm) if err != nil { return "", err } excluded = excluded || thisExcluded if !excluded { if target, err := os.Readlink(filepath.Join(workingPath, components[0])); err == nil && (len(components) != 1 || evaluateFinalComponent) { followed++ if followed > maxLoopsFollowed { return "", &os.PathError{ Op: "open", Path: path, Err: syscall.ELOOP, } } if filepath.IsAbs(target) || looksLikeAbs(target) { // symlink to an absolute path - prepend the // root directory to that absolute path to // replace the current location, and resolve // the remaining components workingPath = root components = append(strings.Split(target, string(os.PathSeparator)), components[1:]...) continue } // symlink to a relative path - add the link target to // the current location to get the next location, and // resolve the remaining components rel, err := convertToRelSubdirectory(root, filepath.Join(workingPath, target)) if err != nil { return "", fmt.Errorf("making path %q relative to %q", filepath.Join(workingPath, target), root) } workingPath = root components = append(strings.Split(filepath.Clean(string(os.PathSeparator)+rel), string(os.PathSeparator)), components[1:]...) continue } } // append the current component's name to get the next location workingPath = filepath.Join(workingPath, components[0]) if workingPath == filepath.Join(root, "..") { // attempted to go above the root using a relative path .., scope it workingPath = root } // ready to handle the next component components = components[1:] } return workingPath, nil } func copierHandlerEval(req request) *response { errorResponse := func(fmtspec string, args ...any) *response { return &response{Error: fmt.Sprintf(fmtspec, args...), Eval: evalResponse{}} } resolvedTarget, err := resolvePath(req.Root, req.Directory, true, nil) if err != nil { return errorResponse("copier: eval: error resolving %q: %v", req.Directory, err) } return &response{Eval: evalResponse{Evaluated: filepath.Join(req.rootPrefix, resolvedTarget)}} } func copierHandlerStat(req request, pm *fileutils.PatternMatcher, idMappings *idtools.IDMappings) *response { errorResponse := func(fmtspec string, args ...any) *response { return &response{Error: fmt.Sprintf(fmtspec, args...), Stat: statResponse{}} } if len(req.Globs) == 0 { return errorResponse("copier: stat: expected at least one glob pattern, got none") } var stats []*StatsForGlob for i, glob := range req.Globs { s := StatsForGlob{ Glob: req.preservedGlobs[i], } // glob this pattern globMatched, err := extendedGlob(glob) if err != nil { s.Error = fmt.Sprintf("copier: stat: %q while matching glob pattern %q", err.Error(), glob) } if len(globMatched) == 0 && strings.ContainsAny(glob, "*?[") { continue } // collect the matches s.Globbed = make([]string, 0, len(globMatched)) s.Results = make(map[string]*StatForItem) for _, globbed := range globMatched { rel, excluded, err := pathIsExcluded(req.Root, globbed, pm) if err != nil { return errorResponse("copier: stat: %v", err) } if excluded { continue } // if the glob was an absolute path, reconstruct the // path that we should hand back for the match var resultName string if filepath.IsAbs(req.preservedGlobs[i]) { resultName = filepath.Join(req.rootPrefix, globbed) } else { relResult := rel if req.Directory != req.Root { relResult, err = convertToRelSubdirectory(req.Directory, globbed) if err != nil { return errorResponse("copier: stat: error making %q relative to %q: %v", globbed, req.Directory, err) } } resultName = relResult } result := StatForItem{Name: resultName} s.Globbed = append(s.Globbed, resultName) s.Results[resultName] = &result // lstat the matched value linfo, err := os.Lstat(globbed) if err != nil { result.Error = err.Error() continue } result.Size = linfo.Size() result.Mode = linfo.Mode() result.UID, result.GID = -1, -1 if uid, gid, err := owner(linfo); err == nil { if idMappings != nil && !idMappings.Empty() { hostPair := idtools.IDPair{UID: uid, GID: gid} uid, gid, err = idMappings.ToContainer(hostPair) if err != nil { return errorResponse("copier: stat: mapping host filesystem owners %#v to container filesystem owners: %v", hostPair, err) } } result.UID, result.GID = int64(uid), int64(gid) } result.ModTime = linfo.ModTime() result.IsDir = linfo.IsDir() result.IsRegular = result.Mode.IsRegular() result.IsSymlink = (linfo.Mode() & os.ModeType) == os.ModeSymlink checkForArchive := req.StatOptions.CheckForArchives if result.IsSymlink { // if the match was a symbolic link, read it immediateTarget, err := os.Readlink(globbed) if err != nil { result.Error = err.Error() continue } // record where it points, both by itself (it // could be a relative link) and in the context // of the chroot result.ImmediateTarget = immediateTarget resolvedTarget, err := resolvePath(req.Root, globbed, true, pm) if err != nil { return errorResponse("copier: stat: error resolving %q: %v", globbed, err) } // lstat the thing that we point to info, err := os.Lstat(resolvedTarget) if err != nil { result.Error = err.Error() continue } // replace IsArchive/IsDir/IsRegular with info about the target if info.Mode().IsRegular() && req.StatOptions.CheckForArchives { result.IsArchive = isArchivePath(resolvedTarget) checkForArchive = false } result.IsDir = info.IsDir() result.IsRegular = info.Mode().IsRegular() } if result.IsRegular && checkForArchive { // we were asked to check on this, and it // wasn't a symlink, in which case we'd have // already checked what the link points to result.IsArchive = isArchivePath(globbed) } } // no unskipped matches -> error if len(s.Globbed) == 0 { s.Globbed = nil s.Results = nil s.Error = fmt.Sprintf("copier: stat: %q: %v", glob, syscall.ENOENT) } stats = append(stats, &s) } // no matches -> error if len(stats) == 0 { s := StatsForGlob{ Error: fmt.Sprintf("copier: stat: %q: %v", req.Globs, syscall.ENOENT), } stats = append(stats, &s) } return &response{Stat: statResponse{Globs: stats}} } func errorIsPermission(err error) bool { if err == nil { return false } return errors.Is(err, os.ErrPermission) || strings.Contains(err.Error(), "permission denied") } func getParents(path string, stopPath string) []string { out := []string{} for path != "/" && path != "." && path != stopPath { path = filepath.Dir(path) if path == stopPath { continue } out = append(out, path) } slices.Reverse(out) return out } func checkLinks(item string, req request, info os.FileInfo) (string, os.FileInfo, error) { // chase links. if we hit a dead end, we should just fail oldItem := item followedLinks := 0 const maxFollowedLinks = 16 for !req.GetOptions.NoDerefSymlinks && info.Mode()&os.ModeType == os.ModeSymlink && followedLinks < maxFollowedLinks { path, err := os.Readlink(item) if err != nil { continue } if filepath.IsAbs(path) || looksLikeAbs(path) { path = filepath.Join(req.Root, path) } else { path = filepath.Join(filepath.Dir(item), path) } item = path if _, err = convertToRelSubdirectory(req.Root, item); err != nil { return "", nil, fmt.Errorf("copier: get: computing path of %q(%q) relative to %q: %w", oldItem, item, req.Root, err) } if info, err = os.Lstat(item); err != nil { return "", nil, fmt.Errorf("copier: get: lstat %q(%q): %w", oldItem, item, err) } followedLinks++ } if followedLinks >= maxFollowedLinks { return "", nil, fmt.Errorf("copier: get: resolving symlink %q(%q): %w", oldItem, item, syscall.ELOOP) } return item, info, nil } func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMatcher, idMappings *idtools.IDMappings) (*response, func() error, error) { statRequest := req statRequest.Request = requestStat statResponse := copierHandlerStat(req, pm, idMappings) errorResponse := func(fmtspec string, args ...any) (*response, func() error, error) { return &response{Error: fmt.Sprintf(fmtspec, args...), Stat: statResponse.Stat, Get: getResponse{}}, nil, nil } if statResponse.Error != "" { return errorResponse("%s", statResponse.Error) } if len(req.Globs) == 0 { return errorResponse("copier: get: expected at least one glob pattern, got 0") } // build a queue of items by globbing type queueItem struct { glob string parents []string } var queue []queueItem globMatchedCount := 0 for _, glob := range req.Globs { globMatched, err := extendedGlob(glob) if err != nil { return errorResponse("copier: get: glob %q: %v", glob, err) } for _, path := range globMatched { var parents []string if req.GetOptions.Parents { parents = getParents(path, req.Directory) } globMatchedCount++ queue = append(queue, queueItem{glob: path, parents: parents}) } } // no matches -> error if len(queue) == 0 { return errorResponse("copier: get: globs %v matched nothing (%d filtered out): %v", req.Globs, globMatchedCount, syscall.ENOENT) } topInfo, err := os.Stat(req.Directory) if err != nil { return errorResponse("copier: get: error reading info about directory %q: %v", req.Directory, err) } cb := func() error { tw := tar.NewWriter(bulkWriter) defer tw.Close() hardlinkChecker := new(hardlinkChecker) itemsCopied := 0 addedParents := map[string]struct{}{} for i, qItem := range queue { item := qItem.glob // if we're not discarding the names of individual directories, keep track of this one relNamePrefix := "" if req.GetOptions.KeepDirectoryNames { relNamePrefix = filepath.Base(item) } // if the named thing-to-read is a symlink, dereference it info, err := os.Lstat(item) if err != nil { return fmt.Errorf("copier: get: lstat %q: %w", item, err) } if req.GetOptions.Parents && info.Mode().IsDir() { if !slices.Contains(qItem.parents, item) { qItem.parents = append(qItem.parents, item) } } // Copy parents in to tarball first if exists for _, parent := range qItem.parents { oldParent := parent parentInfo, err := os.Lstat(parent) if err != nil { return fmt.Errorf("copier: get: lstat %q: %w", parent, err) } parent, parentInfo, err = checkLinks(parent, req, parentInfo) if err != nil { return err } parentName, err := convertToRelSubdirectory(req.Directory, oldParent) if err != nil { return fmt.Errorf("copier: get: error computing path of %q relative to %q: %w", parent, req.Directory, err) } if parentName == "" || parentName == "." { // skip the "." entry continue } if _, ok := addedParents[parentName]; ok { continue } addedParents[parentName] = struct{}{} if err := copierHandlerGetOne(parentInfo, "", parentName, parent, req.GetOptions, tw, hardlinkChecker, idMappings); err != nil { if req.GetOptions.IgnoreUnreadable && errorIsPermission(err) { continue } else if errors.Is(err, os.ErrNotExist) { logrus.Warningf("copier: file disappeared while reading: %q", parent) return nil } return fmt.Errorf("copier: get: %q: %w", queue[i].glob, err) } itemsCopied++ } item, info, err = checkLinks(item, req, info) if err != nil { return err } // evaluate excludes relative to the root directory if info.Mode().IsDir() { // we don't expand any of the contents that are archives options := req.GetOptions options.ExpandArchives = false walkfn := func(path string, d fs.DirEntry, err error) error { if err != nil { if options.IgnoreUnreadable && errorIsPermission(err) { if info != nil && d.IsDir() { return filepath.SkipDir } return nil } else if errors.Is(err, os.ErrNotExist) { logrus.Warningf("copier: file disappeared while reading: %q", path) return nil } return fmt.Errorf("copier: get: error reading %q: %w", path, err) } if d.Type() == os.ModeSocket { logrus.Warningf("copier: skipping socket %q", d.Name()) return nil } // compute the path of this item // relative to the top-level directory, // for the tar header rel, relErr := convertToRelSubdirectory(item, path) if relErr != nil { return fmt.Errorf("copier: get: error computing path of %q relative to top directory %q: %w", path, item, relErr) } // prefix the original item's name if we're keeping it if relNamePrefix != "" { rel = filepath.Join(relNamePrefix, rel) } if rel == "" || rel == "." { // skip the "." entry return nil } skippedPath, skip, err := pathIsExcluded(req.Root, path, pm) if err != nil { return err } if skip { if d.IsDir() { // if there are no "include // this anyway" patterns at // all, we don't need to // descend into this particular // directory if it's a directory if !pm.Exclusions() { return filepath.SkipDir } // if there are exclusion // patterns for which this // path is a prefix, we // need to keep descending for _, pattern := range pm.Patterns() { if !pattern.Exclusion() { continue } spec := strings.Trim(pattern.String(), string(os.PathSeparator)) trimmedPath := strings.Trim(skippedPath, string(os.PathSeparator)) if strings.HasPrefix(spec+string(os.PathSeparator), trimmedPath) { // we can't just skip over // this directory return nil } } // there are exclusions, but // none of them apply here return filepath.SkipDir } // skip this item, but if we're // a directory, a more specific // but-include-this for // something under it might // also be in the excludes list return nil } // if it's a symlink, read its target symlinkTarget := "" if d.Type() == os.ModeSymlink { target, err := os.Readlink(path) if err != nil { return fmt.Errorf("copier: get: readlink(%q(%q)): %w", rel, path, err) } symlinkTarget = target } info, err := d.Info() if err != nil { return err } // if it's a directory and we're staying on one device, and it's on a // different device than the one we started from, skip its contents var ok error if d.IsDir() && req.GetOptions.NoCrossDevice { if !sameDevice(topInfo, info) { ok = filepath.SkipDir } } if req.GetOptions.Parents { rel, err = convertToRelSubdirectory(req.Directory, path) if err != nil { return fmt.Errorf("copier: get: error computing path of %q relative to %q: %w", path, req.Root, err) } } // add the item to the outgoing tar stream if err := copierHandlerGetOne(info, symlinkTarget, rel, path, options, tw, hardlinkChecker, idMappings); err != nil { if req.GetOptions.IgnoreUnreadable && errorIsPermission(err) { return ok } else if errors.Is(err, os.ErrNotExist) { logrus.Warningf("copier: file disappeared while reading: %q", path) return nil } return err } return ok } // walk the directory tree, checking/adding items individually if err := filepath.WalkDir(item, walkfn); err != nil { return fmt.Errorf("copier: get: %q(%q): %w", queue[i].glob, item, err) } itemsCopied++ } else { _, skip, err := pathIsExcluded(req.Root, item, pm) if err != nil { return err } if skip { continue } name := filepath.Base(queue[i].glob) if req.GetOptions.Parents { name, err = convertToRelSubdirectory(req.Directory, queue[i].glob) if err != nil { return fmt.Errorf("copier: get: error computing path of %q relative to %q: %w", item, req.Root, err) } if name == "" || name == "." { // skip the "." entry continue } } if err := copierHandlerGetOne(info, "", name, item, req.GetOptions, tw, hardlinkChecker, idMappings); err != nil { if req.GetOptions.IgnoreUnreadable && errorIsPermission(err) { continue } return fmt.Errorf("copier: get: %q: %w", queue[i].glob, err) } itemsCopied++ } } if itemsCopied == 0 { return fmt.Errorf("copier: get: copied no items: %w", syscall.ENOENT) } return nil } return &response{Stat: statResponse.Stat, Get: getResponse{}}, cb, nil } func handleRename(rename map[string]string, name string) string { if rename == nil { return name } // header names always use '/', so use path instead of filepath to manipulate it if directMapping, ok := rename[name]; ok { return directMapping } prefix, remainder := path.Split(name) for prefix != "" { if mappedPrefix, ok := rename[prefix]; ok { return path.Join(mappedPrefix, remainder) } if prefix[len(prefix)-1] == '/' { prefix = prefix[:len(prefix)-1] if mappedPrefix, ok := rename[prefix]; ok { return path.Join(mappedPrefix, remainder) } } newPrefix, middlePart := path.Split(prefix) if newPrefix == prefix { return name } prefix = newPrefix remainder = path.Join(middlePart, remainder) } return name } // mapWithPrefixedKeysWithoutKeyPrefix returns a map containing every element // of m that had p as a prefix in its (string) key, with that prefix stripped // from its key. items are shallow-copied using assignment. if m is nil, the // returned map will be nil, otherwise it will at least have been allocated func mapWithPrefixedKeysWithoutKeyPrefix[K any](m map[string]K, p string) map[string]K { if m == nil { return m } cloned := make(map[string]K, len(m)) for k, v := range m { if after, ok := strings.CutPrefix(k, p); ok { cloned[after] = v } } return cloned } func copierHandlerGetOne(srcfi os.FileInfo, symlinkTarget, name, contentPath string, options GetOptions, tw *tar.Writer, hardlinkChecker *hardlinkChecker, idMappings *idtools.IDMappings) error { // build the header using the name provided hdr, err := tar.FileInfoHeader(srcfi, symlinkTarget) if err != nil { return fmt.Errorf("generating tar header for %s (%s): %w", contentPath, symlinkTarget, err) } if name != "" { hdr.Name = filepath.ToSlash(name) } hdr.Uname, hdr.Gname = "", "" if options.Rename != nil { hdr.Name = handleRename(options.Rename, hdr.Name) } if options.StripSetuidBit { hdr.Mode &^= cISUID } if options.StripSetgidBit { hdr.Mode &^= cISGID } if options.StripStickyBit { hdr.Mode &^= cISVTX } // read extended attributes var xattrs map[string]string if !options.StripXattrs { xattrs, err = Lgetxattrs(contentPath) if err != nil { return fmt.Errorf("getting extended attributes for %q: %w", contentPath, err) } if len(xattrs) > 0 && hdr.PAXRecords == nil { hdr.PAXRecords = make(map[string]string, len(xattrs)) } } for k, v := range xattrs { hdr.PAXRecords[xattrPAXRecordNamespace+k] = v } if hdr.Typeflag == tar.TypeReg { // if it's an archive and we're extracting archives, read the // file and spool out its contents in-line. (if we just // inlined the whole file, we'd also be inlining the EOF marker // it contains) if options.ExpandArchives && isArchivePath(contentPath) { f, err := os.Open(contentPath) if err != nil { return fmt.Errorf("opening file for reading archive contents: %w", err) } defer f.Close() rc, _, err := compression.AutoDecompress(f) if err != nil { return fmt.Errorf("decompressing %s: %w", contentPath, err) } defer rc.Close() tr := tar.NewReader(rc) hdr, err := tr.Next() for err == nil { if options.Rename != nil { hdr.Name = handleRename(options.Rename, hdr.Name) } if options.Timestamp != nil { timestamp := options.Timestamp.UTC() hdr.ModTime = timestamp if !hdr.AccessTime.IsZero() { hdr.AccessTime = timestamp } if !hdr.ChangeTime.IsZero() { hdr.ChangeTime = timestamp } } if err = tw.WriteHeader(hdr); err != nil { return fmt.Errorf("writing tar header from %q to pipe: %w", contentPath, err) } if hdr.Size != 0 { n, err := io.Copy(tw, tr) if err != nil { return fmt.Errorf("extracting content from archive %s: %s: %w", contentPath, hdr.Name, err) } if n != hdr.Size { return fmt.Errorf("extracting contents of archive %s: incorrect length for %q", contentPath, hdr.Name) } tw.Flush() } hdr, err = tr.Next() } if err != io.EOF { return fmt.Errorf("extracting contents of archive %s: %w", contentPath, err) } return nil } // if this regular file is hard linked to something else we've // already added, set up to output a TypeLink entry instead of // a TypeReg entry target := hardlinkChecker.Check(srcfi) if target != "" { hdr.Typeflag = tar.TypeLink hdr.Linkname = filepath.ToSlash(target) hdr.Size = 0 } else { // note the device/inode pair for this file hardlinkChecker.Add(srcfi, name) } } // map the ownership for the archive if idMappings != nil && !idMappings.Empty() { hostPair := idtools.IDPair{UID: hdr.Uid, GID: hdr.Gid} hdr.Uid, hdr.Gid, err = idMappings.ToContainer(hostPair) if err != nil { return fmt.Errorf("mapping host filesystem owners %#v to container filesystem owners: %w", hostPair, err) } } // force ownership and/or permissions, if requested if hdr.Typeflag == tar.TypeDir { if options.ChownDirs != nil { hdr.Uid, hdr.Gid = options.ChownDirs.UID, options.ChownDirs.GID } if options.ChmodDirs != nil { hdr.Mode = int64(*options.ChmodDirs) } if !strings.HasSuffix(hdr.Name, "/") { hdr.Name += "/" } } else { if options.ChownFiles != nil { hdr.Uid, hdr.Gid = options.ChownFiles.UID, options.ChownFiles.GID } if options.ChmodFiles != nil { hdr.Mode = int64(*options.ChmodFiles) } } // read fflags, if any if err := archive.ReadFileFlagsToTarHeader(contentPath, hdr); err != nil { return fmt.Errorf("getting fflags: %w", err) } var f *os.File switch hdr.Typeflag { case tar.TypeReg: // open the file first so that we don't write a header for it if we can't actually read it f, err = os.Open(contentPath) if err != nil { return fmt.Errorf("opening file for adding its contents to archive: %w", err) } defer f.Close() case tar.TypeDir: // open the directory file first to make sure we can access it. f, err = os.Open(contentPath) if err != nil { return fmt.Errorf("opening directory for adding its contents to archive: %w", err) } defer f.Close() } if options.Timestamp != nil { timestamp := options.Timestamp.UTC() hdr.ModTime = timestamp if !hdr.AccessTime.IsZero() { hdr.AccessTime = timestamp } if !hdr.ChangeTime.IsZero() { hdr.ChangeTime = timestamp } } // output the header if err = tw.WriteHeader(hdr); err != nil { return fmt.Errorf("writing header for %s (%s): %w", contentPath, hdr.Name, err) } if hdr.Typeflag == tar.TypeReg { // output the content n, err := io.Copy(tw, f) if err != nil { return fmt.Errorf("copying %s: %w", contentPath, err) } if n != hdr.Size { return fmt.Errorf("copying %s: incorrect size (expected %d bytes, read %d bytes)", contentPath, n, hdr.Size) } tw.Flush() } return nil } func copierHandlerPut(bulkReader io.Reader, req request, idMappings *idtools.IDMappings) (*response, func() error, error) { errorResponse := func(fmtspec string, args ...any) (*response, func() error, error) { return &response{Error: fmt.Sprintf(fmtspec, args...), Put: putResponse{}}, nil, nil } dirUID, dirGID, defaultDirUID, defaultDirGID := 0, 0, 0, 0 if req.PutOptions.ChownDirs != nil { dirUID, dirGID = req.PutOptions.ChownDirs.UID, req.PutOptions.ChownDirs.GID defaultDirUID, defaultDirGID = dirUID, dirGID } defaultDirMode := os.FileMode(0o755) if req.PutOptions.ChmodDirs != nil { defaultDirMode = *req.PutOptions.ChmodDirs } if req.PutOptions.DefaultDirOwner != nil { defaultDirUID, defaultDirGID = req.PutOptions.DefaultDirOwner.UID, req.PutOptions.DefaultDirOwner.GID } if req.PutOptions.DefaultDirMode != nil { defaultDirMode = *req.PutOptions.DefaultDirMode } var fileUID, fileGID *int if req.PutOptions.ChownFiles != nil { fileUID, fileGID = &req.PutOptions.ChownFiles.UID, &req.PutOptions.ChownFiles.GID } if idMappings != nil && !idMappings.Empty() { containerDirPair := idtools.IDPair{UID: dirUID, GID: dirGID} hostDirPair, err := idMappings.ToHost(containerDirPair) if err != nil { return errorResponse("copier: put: error mapping container filesystem owner %d:%d to host filesystem owners: %v", dirUID, dirGID, err) } dirUID, dirGID = hostDirPair.UID, hostDirPair.GID defaultDirUID, defaultDirGID = hostDirPair.UID, hostDirPair.GID if req.PutOptions.ChownFiles != nil { containerFilePair := idtools.IDPair{UID: *fileUID, GID: *fileGID} hostFilePair, err := idMappings.ToHost(containerFilePair) if err != nil { return errorResponse("copier: put: error mapping container filesystem owner %d:%d to host filesystem owners: %v", fileUID, fileGID, err) } fileUID, fileGID = &hostFilePair.UID, &hostFilePair.GID } } directoryModes := make(map[string]os.FileMode) ensureDirectoryUnderRoot := func(directory string) error { rel, err := convertToRelSubdirectory(req.Root, directory) if err != nil { return fmt.Errorf("%q is not a subdirectory of %q: %w", directory, req.Root, err) } subdir := "" for component := range strings.SplitSeq(rel, string(os.PathSeparator)) { subdir = filepath.Join(subdir, component) path := filepath.Join(req.Root, subdir) if err := os.Mkdir(path, 0o700); err == nil { if err = lchown(path, defaultDirUID, defaultDirGID); err != nil { return fmt.Errorf("copier: put: error setting owner of %q to %d:%d: %w", path, defaultDirUID, defaultDirGID, err) } // make a conditional note to set this directory's permissions // later, but not if we already had an explicitly-provided mode if _, ok := directoryModes[path]; !ok { directoryModes[path] = defaultDirMode } } else { // FreeBSD can return EISDIR for "mkdir /": // https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=59739. if !errors.Is(err, os.ErrExist) && !errors.Is(err, syscall.EISDIR) { return fmt.Errorf("copier: put: error checking directory %q: %w", path, err) } } } return nil } makeDirectoryWriteable := func(directory string) error { if _, ok := directoryModes[directory]; !ok { st, err := os.Lstat(directory) if err != nil { return fmt.Errorf("copier: put: error reading permissions of directory %q: %w", directory, err) } mode := st.Mode() directoryModes[directory] = mode } if err := os.Chmod(directory, 0o700); err != nil { return fmt.Errorf("copier: put: error making directory %q writable: %w", directory, err) } return nil } createFile := func(path string, tr *tar.Reader) (int64, error) { f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC|os.O_EXCL, 0o600) if err != nil && errors.Is(err, os.ErrExist) { if req.PutOptions.NoOverwriteDirNonDir { if st, err2 := os.Lstat(path); err2 == nil && st.IsDir() { return 0, fmt.Errorf("copier: put: error creating file at %q: %w", path, err) } } if err = os.RemoveAll(path); err != nil { if os.IsPermission(err) { if err := makeDirectoryWriteable(filepath.Dir(path)); err != nil { return 0, err } err = os.RemoveAll(path) } if err != nil { return 0, fmt.Errorf("copier: put: error removing item to be overwritten %q: %w", path, err) } } f, err = os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC|os.O_EXCL, 0o600) } if err != nil && os.IsPermission(err) { if err = makeDirectoryWriteable(filepath.Dir(path)); err != nil { return 0, err } f, err = os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC|os.O_EXCL, 0o600) } if err != nil { return 0, fmt.Errorf("copier: put: error opening file %q for writing: %w", path, err) } defer f.Close() n, err := io.Copy(f, tr) if err != nil { return n, fmt.Errorf("copier: put: error writing file %q: %w", path, err) } return n, nil } targetDirectory, err := resolvePath(req.Root, req.Directory, true, nil) if err != nil { return errorResponse("copier: put: error resolving %q: %v", req.Directory, err) } info, err := os.Lstat(targetDirectory) if err == nil { if !info.IsDir() { return errorResponse("copier: put: %s (%s): exists but is not a directory", req.Directory, targetDirectory) } } else { if !errors.Is(err, os.ErrNotExist) { return errorResponse("copier: put: %s: %v", req.Directory, err) } if err := ensureDirectoryUnderRoot(req.Directory); err != nil { return errorResponse("copier: put: %v", err) } } cb := func() error { type directoryAndTimes struct { directory string atime, mtime time.Time } var directoriesAndTimes []directoryAndTimes defer func() { for i := range directoriesAndTimes { directoryAndTimes := directoriesAndTimes[len(directoriesAndTimes)-i-1] if err := lutimes(false, directoryAndTimes.directory, directoryAndTimes.atime, directoryAndTimes.mtime); err != nil { logrus.Debugf("error setting access and modify timestamps on %q to %s and %s: %v", directoryAndTimes.directory, directoryAndTimes.atime, directoryAndTimes.mtime, err) } } for directory, mode := range directoryModes { if err := os.Chmod(directory, mode); err != nil { logrus.Debugf("error setting permissions of %q to 0%o: %v", directory, uint32(mode), err) } } }() ignoredItems := make(map[string]struct{}) tr := tar.NewReader(bulkReader) hdr, err := tr.Next() for err == nil { nameBeforeRenaming := hdr.Name if len(hdr.Name) == 0 { // no name -> ignore the entry ignoredItems[nameBeforeRenaming] = struct{}{} hdr, err = tr.Next() continue } if req.PutOptions.Rename != nil { hdr.Name = handleRename(req.PutOptions.Rename, hdr.Name) } // figure out who should own this new item if idMappings != nil && !idMappings.Empty() { containerPair := idtools.IDPair{UID: hdr.Uid, GID: hdr.Gid} hostPair, err := idMappings.ToHost(containerPair) if err != nil { return fmt.Errorf("mapping container filesystem owner 0,0 to host filesystem owners: %w", err) } hdr.Uid, hdr.Gid = hostPair.UID, hostPair.GID } if hdr.Typeflag == tar.TypeDir { if req.PutOptions.ChownDirs != nil { hdr.Uid, hdr.Gid = dirUID, dirGID } } else { if req.PutOptions.ChownFiles != nil { hdr.Uid, hdr.Gid = *fileUID, *fileGID } } // make sure the parent directory exists, including for tar.TypeXGlobalHeader entries // that we otherwise ignore, because that's what docker build does with them path := filepath.Join(targetDirectory, cleanerReldirectory(filepath.FromSlash(hdr.Name))) if err := ensureDirectoryUnderRoot(filepath.Dir(path)); err != nil { return err } // figure out what the permissions should be if req.PutOptions.StripSetuidBit && hdr.Mode&cISUID == cISUID { hdr.Mode &^= cISUID } if req.PutOptions.StripSetgidBit && hdr.Mode&cISGID == cISGID { hdr.Mode &^= cISGID } if req.PutOptions.StripStickyBit && hdr.Mode&cISVTX == cISVTX { hdr.Mode &^= cISVTX } if hdr.Typeflag == tar.TypeDir { if req.PutOptions.ChmodDirs != nil { hdr.Mode = int64(*req.PutOptions.ChmodDirs) } } else { if req.PutOptions.ChmodFiles != nil { hdr.Mode = int64(*req.PutOptions.ChmodFiles) } } // create the new item devMajor := uint32(hdr.Devmajor) devMinor := uint32(hdr.Devminor) mode := os.FileMode(hdr.Mode) & os.ModePerm switch hdr.Typeflag { // no type flag for sockets default: return fmt.Errorf("unrecognized Typeflag %c", hdr.Typeflag) case tar.TypeReg: var written int64 written, err = createFile(path, tr) // only check the length if there wasn't an error, which we'll // check along with errors for other types of entries if err == nil && written != hdr.Size { return fmt.Errorf("copier: put: error creating regular file %q: incorrect length (%d != %d)", path, written, hdr.Size) } case tar.TypeLink: var linkTarget string if _, ignoredTarget := ignoredItems[hdr.Linkname]; ignoredTarget { // hard link to an ignored item: skip this, too ignoredItems[nameBeforeRenaming] = struct{}{} goto nextHeader } if req.PutOptions.Rename != nil { hdr.Linkname = handleRename(req.PutOptions.Rename, hdr.Linkname) } if linkTarget, err = resolvePath(targetDirectory, filepath.Join(req.Root, filepath.FromSlash(hdr.Linkname)), true, nil); err != nil { return fmt.Errorf("resolving hardlink target path %q under root %q", hdr.Linkname, req.Root) } if err = os.Link(linkTarget, path); err != nil && errors.Is(err, os.ErrExist) { if req.PutOptions.NoOverwriteDirNonDir { if st, err := os.Lstat(path); err == nil && st.IsDir() { break } } if err = os.RemoveAll(path); err == nil { err = os.Link(linkTarget, path) } } case tar.TypeSymlink: // if req.PutOptions.Rename != nil { // todo: the general solution requires resolving to an absolute path, handling // renaming, and then possibly converting back to a relative symlink // } if err = os.Symlink(filepath.FromSlash(hdr.Linkname), filepath.FromSlash(path)); err != nil && errors.Is(err, os.ErrExist) { if req.PutOptions.NoOverwriteDirNonDir { if st, err := os.Lstat(path); err == nil && st.IsDir() { break } } if err = os.RemoveAll(path); err == nil { err = os.Symlink(filepath.FromSlash(hdr.Linkname), filepath.FromSlash(path)) } } case tar.TypeChar: if req.PutOptions.IgnoreDevices { ignoredItems[nameBeforeRenaming] = struct{}{} goto nextHeader } if err = mknod(path, chrMode(0o600), int(mkdev(devMajor, devMinor))); err != nil && errors.Is(err, os.ErrExist) { if req.PutOptions.NoOverwriteDirNonDir { if st, err := os.Lstat(path); err == nil && st.IsDir() { break } } if err = os.RemoveAll(path); err == nil { err = mknod(path, chrMode(0o600), int(mkdev(devMajor, devMinor))) } } case tar.TypeBlock: if req.PutOptions.IgnoreDevices { ignoredItems[nameBeforeRenaming] = struct{}{} goto nextHeader } if err = mknod(path, blkMode(0o600), int(mkdev(devMajor, devMinor))); err != nil && errors.Is(err, os.ErrExist) { if req.PutOptions.NoOverwriteDirNonDir { if st, err := os.Lstat(path); err == nil && st.IsDir() { break } } if err = os.RemoveAll(path); err == nil { err = mknod(path, blkMode(0o600), int(mkdev(devMajor, devMinor))) } } case tar.TypeDir: // FreeBSD can return EISDIR for "mkdir /": // https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=59739. if err = os.Mkdir(path, 0o700); err != nil && (errors.Is(err, os.ErrExist) || errors.Is(err, syscall.EISDIR)) { if st, stErr := os.Lstat(path); stErr == nil && !st.IsDir() { if req.PutOptions.NoOverwriteNonDirDir { break } if err = os.Remove(path); err == nil { err = os.Mkdir(path, 0o700) } } else { err = stErr } // either we removed it and retried, or it was a directory, // in which case we want to just add the new stuff under it } // make a note of the directory's times. we // might create items under it, which will // cause the mtime to change after we correct // it, so we'll need to correct it again later directoriesAndTimes = append(directoriesAndTimes, directoryAndTimes{ directory: path, atime: hdr.AccessTime, mtime: hdr.ModTime, }) // set the mode here unconditionally, in case the directory is in // the archive more than once for whatever reason directoryModes[path] = mode case tar.TypeFifo: if err = mkfifo(path, 0o600); err != nil && errors.Is(err, os.ErrExist) { if req.PutOptions.NoOverwriteDirNonDir { if st, err := os.Lstat(path); err == nil && st.IsDir() { break } } if err = os.RemoveAll(path); err == nil { err = mkfifo(path, 0o600) } } case tar.TypeXGlobalHeader: // Per archive/tar, PAX uses these to specify key=value information // applies to all subsequent entries. The one in reported in #2717, // https://www.openssl.org/source/openssl-1.1.1g.tar.gz, includes a // comment=(40 byte hex string) at the start, possibly a digest. // Don't try to create whatever path was used for the header. goto nextHeader } // check for errors if err != nil { return fmt.Errorf("copier: put: error creating %q: %w", path, err) } // set ownership if err = lchown(path, hdr.Uid, hdr.Gid); err != nil { return fmt.Errorf("copier: put: error setting ownership of %q to %d:%d: %w", path, hdr.Uid, hdr.Gid, err) } // set permissions, except for symlinks, since we don't // have an lchmod, and directories, which we'll fix up // on our way out so that we don't get tripped up by // directories which we're not supposed to be able to // write to, but which we'll need to create content in if hdr.Typeflag != tar.TypeSymlink && hdr.Typeflag != tar.TypeDir { if err = os.Chmod(path, mode); err != nil { return fmt.Errorf("copier: put: error setting permissions on %q to 0%o: %w", path, mode, err) } } // set other bits that might have been reset by chown() if hdr.Typeflag != tar.TypeSymlink { if hdr.Mode&cISUID == cISUID { mode |= os.ModeSetuid } if hdr.Mode&cISGID == cISGID { mode |= os.ModeSetgid } if hdr.Mode&cISVTX == cISVTX { mode |= os.ModeSticky } if hdr.Typeflag == tar.TypeDir { // if/when we do the final setting of permissions on this // directory, make sure to incorporate these bits, too directoryModes[path] = mode } if err = os.Chmod(path, mode); err != nil { return fmt.Errorf("copier: put: setting additional permissions on %q to 0%o: %w", path, mode, err) } } // set xattrs, including some that might have been reset by chown() if !req.PutOptions.StripXattrs { xattrs := mapWithPrefixedKeysWithoutKeyPrefix(hdr.PAXRecords, xattrPAXRecordNamespace) if err = Lsetxattrs(path, xattrs); err != nil { if !req.PutOptions.IgnoreXattrErrors { return fmt.Errorf("copier: put: error setting extended attributes on %q: %w", path, err) } } } // set time if hdr.AccessTime.IsZero() || hdr.AccessTime.Before(hdr.ModTime) { hdr.AccessTime = hdr.ModTime } if err = lutimes(hdr.Typeflag == tar.TypeSymlink, path, hdr.AccessTime, hdr.ModTime); err != nil { return fmt.Errorf("setting access and modify timestamps on %q to %s and %s: %w", path, hdr.AccessTime, hdr.ModTime, err) } // set fflags if supported if err := archive.WriteFileFlagsFromTarHeader(path, hdr); err != nil { return fmt.Errorf("copier: put: error setting fflags on %q: %w", path, err) } nextHeader: hdr, err = tr.Next() } if err != io.EOF { return fmt.Errorf("reading tar stream: expected EOF: %w", err) } // Drain any remaining data from bulkReader to prevent broken pipe errors. // tar.Reader returns EOF after reading the standard tar EOF marker // (two 512-byte blocks of nulls), but the tar file may have additional // trailing null bytes. If we don't read them, the subprocess exits before // the sender finishes writing to the pipe, causing EPIPE/broken pipe. // See: https://github.com/containers/buildah/issues/6573 if _, err := io.Copy(io.Discard, bulkReader); err != nil { logrus.Debugf("error draining remaining data from tar stream: %v", err) } return nil } return &response{Error: "", Put: putResponse{}}, cb, nil } func copierHandlerMkdir(req request, idMappings *idtools.IDMappings) (*response, func() error, error) { errorResponse := func(fmtspec string, args ...any) (*response, func() error, error) { //nolint:unparam return &response{Error: fmt.Sprintf(fmtspec, args...), Mkdir: mkdirResponse{}}, nil, nil } dirUID, dirGID := 0, 0 if req.MkdirOptions.ChownNew != nil { dirUID, dirGID = req.MkdirOptions.ChownNew.UID, req.MkdirOptions.ChownNew.GID } dirMode := os.FileMode(0o755) if req.MkdirOptions.ChmodNew != nil { dirMode = *req.MkdirOptions.ChmodNew } if idMappings != nil && !idMappings.Empty() { containerDirPair := idtools.IDPair{UID: dirUID, GID: dirGID} hostDirPair, err := idMappings.ToHost(containerDirPair) if err != nil { return errorResponse("copier: mkdir: error mapping container filesystem owner %d:%d to host filesystem owners: %v", dirUID, dirGID, err) } dirUID, dirGID = hostDirPair.UID, hostDirPair.GID } directory, err := resolvePath(req.Root, req.Directory, true, nil) if err != nil { return errorResponse("copier: mkdir: error resolving %q: %v", req.Directory, err) } rel, err := convertToRelSubdirectory(req.Root, directory) if err != nil { return errorResponse("copier: mkdir: error computing path of %q relative to %q: %v", directory, req.Root, err) } subdir := "" var created []string for component := range strings.SplitSeq(rel, string(os.PathSeparator)) { subdir = filepath.Join(subdir, component) path := filepath.Join(req.Root, subdir) if err := os.Mkdir(path, 0o700); err == nil { if err = chown(path, dirUID, dirGID); err != nil { return errorResponse("copier: mkdir: error setting owner of %q to %d:%d: %v", path, dirUID, dirGID, err) } if err = chmod(path, dirMode); err != nil { return errorResponse("copier: mkdir: error setting permissions on %q to 0%o: %v", path, dirMode, err) } created = append(created, path) } else { // FreeBSD can return EISDIR for "mkdir /": // https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=59739. if !errors.Is(err, os.ErrExist) && !errors.Is(err, syscall.EISDIR) { return errorResponse("copier: mkdir: error checking directory %q: %v", path, err) } } } // set timestamps last, in case we needed to create some nested directories, which would // update the timestamps on directories that we'd just set timestamps on, if we had done // that immediately if req.MkdirOptions.ModTimeNew != nil { when := *req.MkdirOptions.ModTimeNew for _, newDirectory := range created { if err = lutimes(false, newDirectory, when, when); err != nil { return errorResponse("copier: mkdir: error setting datestamp on %q: %v", newDirectory, err) } } } return &response{Error: "", Mkdir: mkdirResponse{}}, nil, nil } func copierHandlerRemove(req request) *response { errorResponse := func(fmtspec string, args ...any) *response { return &response{Error: fmt.Sprintf(fmtspec, args...), Remove: removeResponse{}} } resolvedTarget, err := resolvePath(req.Root, req.Directory, false, nil) if err != nil { return errorResponse("copier: remove: %v", err) } if req.RemoveOptions.All { err = os.RemoveAll(resolvedTarget) } else { err = os.Remove(resolvedTarget) } if err != nil { return errorResponse("copier: remove %q: %v", req.Directory, err) } return &response{Error: "", Remove: removeResponse{}} } // EnsurePath is a single item being passed to an Ensure() call. type EnsurePath struct { Path string // a pathname, relative to the Directory, possibly relative to the root Typeflag byte // can be either TypeReg or TypeDir, everything else is currently ignored ModTime *time.Time // mtime to set on newly-created items, default is to leave them be Chmod *os.FileMode // mode, defaults to 000 for files and 700 for directories Chown *idtools.IDPair // owner settings to set on newly-created items, defaults to 0:0 } // EnsureOptions controls parts of Ensure()'s behavior. type EnsureOptions struct { UIDMap, GIDMap []idtools.IDMap // map from containerIDs to hostIDs in the chroot Paths []EnsurePath } // EnsureParentPath is a parent (or grandparent, or...) directory of an item // created by Ensure(), along with information about it, from before the item // in question was created. If the information about this directory hasn't // changed when commit-time rolls around, it's most likely that this directory // is only being considered for inclusion in the layer because it was pulled // up, and it was not actually changed. type EnsureParentPath = ConditionalRemovePath // Ensure ensures that the specified mount point targets exist under the root. // If the root directory is not specified, the current root directory is used. // If root is specified and the current OS supports it, and the calling process // has the necessary privileges, the operation is performed in a chrooted // context. // Returns a slice with the pathnames of items that needed to be created and a // slice of affected parent directories and information about them. func Ensure(root, directory string, options EnsureOptions) ([]string, []EnsureParentPath, error) { req := request{ Request: requestEnsure, Root: root, Directory: directory, EnsureOptions: options, } resp, err := copier(nil, nil, req) if err != nil { return nil, nil, err } if resp.Error != "" { return nil, nil, errors.New(resp.Error) } return resp.Ensure.Created, resp.Ensure.Noted, nil } func copierHandlerEnsure(req request, idMappings *idtools.IDMappings) *response { errorResponse := func(fmtspec string, args ...any) *response { return &response{Error: fmt.Sprintf(fmtspec, args...), Ensure: ensureResponse{}} } slices.SortFunc(req.EnsureOptions.Paths, func(a, b EnsurePath) int { return strings.Compare(a.Path, b.Path) }) var created []string notedByName := map[string]EnsureParentPath{} for _, item := range req.EnsureOptions.Paths { uid, gid := 0, 0 if item.Chown != nil { uid, gid = item.Chown.UID, item.Chown.GID } var mode os.FileMode switch item.Typeflag { case tar.TypeReg: mode = 0o000 case tar.TypeDir: mode = 0o700 default: continue } if item.Chmod != nil { mode = *item.Chmod } if idMappings != nil && !idMappings.Empty() { containerDirPair := idtools.IDPair{UID: uid, GID: gid} hostDirPair, err := idMappings.ToHost(containerDirPair) if err != nil { return errorResponse("copier: ensure: error mapping container filesystem owner %d:%d to host filesystem owners: %v", uid, gid, err) } uid, gid = hostDirPair.UID, hostDirPair.GID } directory, err := resolvePath(req.Root, req.Directory, true, nil) if err != nil { return errorResponse("copier: ensure: error resolving %q: %v", req.Directory, err) } rel, err := convertToRelSubdirectory(req.Root, directory) if err != nil { return errorResponse("copier: ensure: error computing path of %q relative to %q: %v", directory, req.Root, err) } subdir := "" components := strings.Split(filepath.Join(rel, item.Path), string(os.PathSeparator)) components = slices.DeleteFunc(components, func(s string) bool { return s == "" || s == "." }) for i, component := range components { parentPath := subdir if parentPath == "" { parentPath = "." } leaf := filepath.Join(parentPath, component) parentInfo, err := os.Stat(filepath.Join(req.Root, parentPath)) if err != nil { return errorResponse("copier: ensure: checking datestamps on %q (%d: %v): %v", parentPath, i, components, err) } if parentPath != "." { parentModTime := parentInfo.ModTime().UTC() parentMode := parentInfo.Mode() uid, gid, err := owner(parentInfo) if err != nil { return errorResponse("copier: ensure: error reading owner of %q: %v", parentPath, err) } notedByName[parentPath] = EnsureParentPath{ Path: parentPath, ModTime: &parentModTime, Mode: &parentMode, Owner: &idtools.IDPair{UID: uid, GID: gid}, } } if i < len(components)-1 || item.Typeflag == tar.TypeDir { err = os.Mkdir(filepath.Join(req.Root, leaf), mode) subdir = leaf } else if item.Typeflag == tar.TypeReg { var f *os.File if f, err = os.OpenFile(filepath.Join(req.Root, leaf), os.O_CREATE|os.O_EXCL|os.O_RDWR, mode); err == nil { f.Close() } } else { continue } if err == nil { createdLeaf := leaf if len(createdLeaf) > 1 { createdLeaf = strings.TrimPrefix(createdLeaf, string(os.PathSeparator)) } created = append(created, createdLeaf) if err = chown(filepath.Join(req.Root, leaf), uid, gid); err != nil { return errorResponse("copier: ensure: error setting owner of %q to %d:%d: %v", leaf, uid, gid, err) } if err = chmod(filepath.Join(req.Root, leaf), mode); err != nil { return errorResponse("copier: ensure: error setting permissions on %q to 0%o: %v", leaf, mode, err) } if item.ModTime != nil { if err := os.Chtimes(filepath.Join(req.Root, leaf), *item.ModTime, *item.ModTime); err != nil { return errorResponse("copier: ensure: resetting datestamp on %q: %v", leaf, err) } } } else { // FreeBSD can return EISDIR for "mkdir /": // https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=59739. if !errors.Is(err, os.ErrExist) && !errors.Is(err, syscall.EISDIR) { return errorResponse("copier: ensure: error checking item %q: %v", leaf, err) } } if err := os.Chtimes(filepath.Join(req.Root, parentPath), parentInfo.ModTime(), parentInfo.ModTime()); err != nil { return errorResponse("copier: ensure: resetting datestamp on %q: %v", parentPath, err) } } } slices.Sort(created) noted := make([]EnsureParentPath, 0, len(notedByName)) for _, n := range notedByName { if slices.Contains(created, n.Path) { continue } noted = append(noted, n) } slices.SortFunc(noted, func(a, b EnsureParentPath) int { return strings.Compare(a.Path, b.Path) }) return &response{Error: "", Ensure: ensureResponse{Created: created, Noted: noted}} } // ConditionalRemovePath is a single item being passed to an ConditionalRemove() call. type ConditionalRemovePath struct { Path string // a pathname, relative to the Directory, possibly relative to the root ModTime *time.Time // mtime to expect this item to have, if it's a condition Mode *os.FileMode // mode to expect this item to have, if it's a condition Owner *idtools.IDPair // owner to expect this item to have, if it's a condition } // ConditionalRemoveOptions controls parts of ConditionalRemove()'s behavior. type ConditionalRemoveOptions struct { UIDMap, GIDMap []idtools.IDMap // map from containerIDs to hostIDs in the chroot Paths []ConditionalRemovePath } // ConditionalRemove removes the set of named items if they're present and // currently match the additional conditions, returning the list of items it // removed. Directories will also only be removed if they have no contents, // and will be left in place otherwise. func ConditionalRemove(root, directory string, options ConditionalRemoveOptions) ([]string, error) { req := request{ Request: requestConditionalRemove, Root: root, Directory: directory, ConditionalRemoveOptions: options, } resp, err := copier(nil, nil, req) if err != nil { return nil, err } if resp.Error != "" { return nil, errors.New(resp.Error) } return resp.ConditionalRemove.Removed, nil } func copierHandlerConditionalRemove(req request, idMappings *idtools.IDMappings) *response { errorResponse := func(fmtspec string, args ...any) *response { return &response{Error: fmt.Sprintf(fmtspec, args...), ConditionalRemove: conditionalRemoveResponse{}} } slices.SortFunc(req.ConditionalRemoveOptions.Paths, func(a, b ConditionalRemovePath) int { return strings.Compare(b.Path, a.Path) }) var removed []string for _, item := range req.ConditionalRemoveOptions.Paths { uid, gid := 0, 0 if item.Owner != nil { uid, gid = item.Owner.UID, item.Owner.GID } if idMappings != nil && !idMappings.Empty() { containerDirPair := idtools.IDPair{UID: uid, GID: gid} hostDirPair, err := idMappings.ToHost(containerDirPair) if err != nil { return errorResponse("copier: conditionalRemove: error mapping container filesystem owner %d:%d to host filesystem owners: %v", uid, gid, err) } uid, gid = hostDirPair.UID, hostDirPair.GID } directory, err := resolvePath(req.Root, req.Directory, true, nil) if err != nil { return errorResponse("copier: conditionalRemove: error resolving %q: %v", req.Directory, err) } rel, err := convertToRelSubdirectory(req.Root, directory) if err != nil { return errorResponse("copier: conditionalRemove: error computing path of %q relative to %q: %v", directory, req.Root, err) } components := strings.Split(filepath.Join(rel, item.Path), string(os.PathSeparator)) components = slices.DeleteFunc(components, func(s string) bool { return s == "" || s == "." }) if len(components) == 0 { continue } itemPath := filepath.Join(append([]string{req.Root}, components...)...) itemInfo, err := os.Lstat(itemPath) if err != nil { if !errors.Is(err, os.ErrNotExist) { return errorResponse("copier: conditionalRemove: checking on candidate %q: %v", itemPath, err) } // okay? removed = append(removed, item.Path) continue } parentPath := filepath.Dir(itemPath) parentInfo, err := os.Stat(parentPath) if err != nil { return errorResponse("copier: conditionalRemove: checking on parent directory %q: %v", parentPath, err) } if item.Mode != nil && itemInfo.Mode().Perm()&fs.ModePerm != *item.Mode&fs.ModePerm { // mismatch, modified? ignore continue } if item.ModTime != nil && !item.ModTime.Equal(itemInfo.ModTime()) { // mismatch, modified? ignore continue } if item.Owner != nil { ownerUID, ownerGID, err := owner(itemInfo) if err != nil { return errorResponse("copier: conditionalRemove: checking ownership of %q: %v", itemPath, err) } if uid != ownerUID || gid != ownerGID { // mismatch, modified? ignore continue } } if err := os.Remove(itemPath); err != nil && !errors.Is(err, os.ErrNotExist) { if !errors.Is(err, syscall.EEXIST) && !errors.Is(err, syscall.ENOTEMPTY) { return errorResponse("copier: conditionalRemove: removing %q: %v", itemPath, err) } // okay? not removed, but it wasn't empty, so okay? continue } removed = append(removed, item.Path) if err := os.Chtimes(parentPath, parentInfo.ModTime(), parentInfo.ModTime()); err != nil { return errorResponse("copier: conditionalRemove: resetting datestamp on %q: %v", parentPath, err) } } slices.Sort(removed) return &response{Error: "", ConditionalRemove: conditionalRemoveResponse{Removed: removed}} } ================================================ FILE: copier/copier_linux_test.go ================================================ package copier import ( "archive/tar" "bytes" "encoding/json" "errors" "fmt" "io" "os" "path/filepath" "testing" "github.com/moby/sys/capability" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.podman.io/storage/pkg/mount" "go.podman.io/storage/pkg/reexec" "golang.org/x/sys/unix" ) func init() { reexec.Register("get", getWrappedMain) } type getWrappedOptions struct { Root, Directory string GetOptions GetOptions Globs []string DropCaps []capability.Cap } func getWrapped(root string, directory string, getOptions GetOptions, globs []string, dropCaps []capability.Cap, bulkWriter io.Writer) error { options := getWrappedOptions{ Root: root, Directory: directory, GetOptions: getOptions, Globs: globs, DropCaps: dropCaps, } encoded, err := json.Marshal(&options) if err != nil { return fmt.Errorf("marshalling options: %w", err) } cmd := reexec.Command("get") cmd.Env = append(cmd.Env, "OPTIONS="+string(encoded)) cmd.Stdout = bulkWriter stderrBuf := bytes.Buffer{} cmd.Stderr = &stderrBuf err = cmd.Run() if stderrBuf.Len() > 0 { if err != nil { return fmt.Errorf("%v: %s", err, stderrBuf.String()) } return fmt.Errorf("%s", stderrBuf.String()) } return err } func getWrappedMain() { var options getWrappedOptions if err := json.Unmarshal([]byte(os.Getenv("OPTIONS")), &options); err != nil { fmt.Fprintf(os.Stderr, "%v", err) os.Exit(1) } if len(options.DropCaps) > 0 { caps, err := capability.NewPid2(0) if err != nil { fmt.Fprintf(os.Stderr, "%v", err) os.Exit(1) } if err := caps.Load(); err != nil { fmt.Fprintf(os.Stderr, "%v", err) os.Exit(1) } for _, capType := range []capability.CapType{ capability.AMBIENT, capability.BOUNDING, capability.INHERITABLE, capability.PERMITTED, capability.EFFECTIVE, } { for _, cap := range options.DropCaps { if caps.Get(capType, cap) { caps.Unset(capType, cap) } } if err := caps.Apply(capType); err != nil { fmt.Fprintf(os.Stderr, "error dropping capability %+v: %v", options.DropCaps, err) os.Exit(1) } } } if err := Get(options.Root, options.Directory, options.GetOptions, options.Globs, os.Stdout); err != nil { fmt.Fprintf(os.Stderr, "%v", err) os.Exit(1) } } func TestGetPermissionErrorNoChroot(t *testing.T) { couldChroot := canChroot canChroot = false testGetPermissionError(t) canChroot = couldChroot } func TestGetPermissionErrorChroot(t *testing.T) { if uid != 0 { t.Skipf("chroot() requires root privileges, skipping") } couldChroot := canChroot canChroot = true testGetPermissionError(t) canChroot = couldChroot } func testGetPermissionError(t *testing.T) { dropCaps := []capability.Cap{capability.CAP_DAC_OVERRIDE, capability.CAP_DAC_READ_SEARCH} tmp := t.TempDir() err := os.Mkdir(filepath.Join(tmp, "unreadable-directory"), 0o000) require.NoError(t, err, "error creating an unreadable directory") err = os.Mkdir(filepath.Join(tmp, "readable-directory"), 0o755) require.NoError(t, err, "error creating a readable directory") err = os.Mkdir(filepath.Join(tmp, "readable-directory", "unreadable-subdirectory"), 0o000) require.NoError(t, err, "error creating an unreadable subdirectory") err = os.WriteFile(filepath.Join(tmp, "unreadable-file"), []byte("hi, i'm a file that you can't read"), 0o000) require.NoError(t, err, "error creating an unreadable file") err = os.WriteFile(filepath.Join(tmp, "readable-file"), []byte("hi, i'm also a file, and you can read me"), 0o644) require.NoError(t, err, "error creating a readable file") err = os.WriteFile(filepath.Join(tmp, "readable-directory", "unreadable-file"), []byte("hi, i'm also a file that you can't read"), 0o000) require.NoError(t, err, "error creating an unreadable file in a readable directory") for _, ignore := range []bool{false, true} { t.Run(fmt.Sprintf("ignore=%v", ignore), func(t *testing.T) { var buf bytes.Buffer err = getWrapped(tmp, tmp, GetOptions{IgnoreUnreadable: ignore}, []string{"."}, dropCaps, &buf) if ignore { assert.NoError(t, err, "expected no errors") tr := tar.NewReader(&buf) items := 0 _, err := tr.Next() for err == nil { items++ _, err = tr.Next() } assert.True(t, errors.Is(err, io.EOF), "expected EOF to finish read contents") assert.Equalf(t, 2, items, "expected two readable items, got %d", items) } else { assert.Error(t, err, "expected an error") assert.Truef(t, errorIsPermission(err), "expected the error (%v) to be a permission error", err) } }) } } func TestGetNoCrossDevice(t *testing.T) { if uid != 0 { t.Skip("test requires root privileges, skipping") } tmpdir := t.TempDir() err := unix.Unshare(unix.CLONE_NEWNS) require.NoError(t, err, "error creating new mount namespace") subdir := filepath.Join(tmpdir, "subdir") err = os.Mkdir(subdir, 0o755) require.NoErrorf(t, err, "error creating %q", subdir) err = mount.Mount("tmpfs", subdir, "tmpfs", "rw") require.NoErrorf(t, err, "error mounting tmpfs at %q", subdir) defer func() { err := mount.Unmount(subdir) assert.NoErrorf(t, err, "error unmounting %q", subdir) }() skipped := filepath.Join(subdir, "skipped.txt") err = os.WriteFile(skipped, []byte("this file should have been skipped\n"), 0o644) require.NoErrorf(t, err, "error writing file at %q", skipped) var buf bytes.Buffer err = Get(tmpdir, tmpdir, GetOptions{NoCrossDevice: true}, []string{"/"}, &buf) // grab contents of tmpdir require.NoErrorf(t, err, "error reading contents at %q", tmpdir) tr := tar.NewReader(&buf) th, err := tr.Next() // should be the "subdir" directory require.NoError(t, err, "error reading first entry archived") assert.Equal(t, "subdir/", th.Name, `first entry in archive was not named "subdir/"`) th, err = tr.Next() assert.Error(t, err, "should not have gotten a second entry in archive") assert.True(t, errors.Is(err, io.EOF), "expected an EOF trying to read a second entry in archive") if err == nil { t.Logf("got unexpected entry for %q", th.Name) } } ================================================ FILE: copier/copier_test.go ================================================ package copier import ( "archive/tar" "bufio" "bytes" "errors" "flag" "fmt" "io" "io/fs" "os" "path" "path/filepath" "reflect" "slices" "sort" "strconv" "strings" "sync" "syscall" "testing" "time" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.podman.io/storage/pkg/idtools" "go.podman.io/storage/pkg/reexec" ) func TestMain(m *testing.M) { if reexec.Init() { return } flag.Parse() if testing.Verbose() { logrus.SetLevel(logrus.DebugLevel) } os.Exit(m.Run()) } // makeFileContents creates contents for a file of a specified size func makeContents(length int64) io.ReadCloser { pipeReader, pipeWriter := io.Pipe() buffered := bufio.NewWriter(pipeWriter) go func() { count := int64(0) for count < length { if _, err := buffered.Write([]byte{"0123456789abcdef"[count%16]}); err != nil { buffered.Flush() pipeWriter.CloseWithError(err) return } count++ } buffered.Flush() pipeWriter.Close() }() return pipeReader } // makeArchiveSlice creates an archive from the set of headers and returns a byte slice. func makeArchiveSlice(headers []tar.Header) []byte { rc := makeArchive(headers, nil) defer rc.Close() buf := new(bytes.Buffer) if _, err := io.Copy(buf, rc); err != nil { panic("error creating in-memory archive") } return buf.Bytes() } // makeArchive creates an archive from the set of headers. func makeArchive(headers []tar.Header, contents map[string][]byte) io.ReadCloser { if contents == nil { contents = make(map[string][]byte) } pipeReader, pipeWriter := io.Pipe() go func() { var err error buffered := bufio.NewWriter(pipeWriter) tw := tar.NewWriter(buffered) for _, header := range headers { var fileContent []byte switch header.Typeflag { case tar.TypeLink, tar.TypeSymlink: header.Size = 0 case tar.TypeReg: fileContent = contents[header.Name] if len(fileContent) != 0 { header.Size = int64(len(fileContent)) } } if err = tw.WriteHeader(&header); err != nil { break } if header.Typeflag == tar.TypeReg && header.Size > 0 { var fileContents io.Reader if len(fileContent) > 0 { fileContents = bytes.NewReader(fileContent) } else { rc := makeContents(header.Size) defer rc.Close() fileContents = rc } if _, err = io.Copy(tw, fileContents); err != nil { break } } } tw.Close() buffered.Flush() if err != nil { pipeWriter.CloseWithError(err) } else { pipeWriter.Close() } }() return pipeReader } // makeContextFromArchive creates a temporary directory, and a subdirectory // inside of it, from an archive and returns its location. It can be removed // once it's no longer needed. func makeContextFromArchive(t *testing.T, archive io.ReadCloser, subdir string) (string, error) { tmp := t.TempDir() uidMap := []idtools.IDMap{{HostID: os.Getuid(), ContainerID: 0, Size: 1}} gidMap := []idtools.IDMap{{HostID: os.Getgid(), ContainerID: 0, Size: 1}} err := Put(tmp, path.Join(tmp, subdir), PutOptions{UIDMap: uidMap, GIDMap: gidMap}, archive) archive.Close() if err != nil { return "", err } return tmp, err } // enumerateFiles walks a directory, returning the items it contains as a slice // of names relative to that directory. func enumerateFiles(directory string) ([]enumeratedFile, error) { var results []enumeratedFile err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error { if info == nil || err != nil { return err } rel, err := filepath.Rel(directory, path) if err != nil { return err } if rel != "" && rel != "." { results = append(results, enumeratedFile{ name: rel, mode: info.Mode() & os.ModePerm, isSymlink: info.Mode()&os.ModeSymlink == os.ModeSymlink, date: info.ModTime().UTC().String(), }) } return nil }) if err != nil { return nil, err } return results, nil } type expectedError struct { inSubdir bool name string err error } type enumeratedFile struct { name string mode os.FileMode isSymlink bool date string } var ( testDate = time.Unix(1485449953, 0) secondTestDate = time.Unix(1485449953*2, 0) uid = os.Getuid() testArchiveSlice = makeArchiveSlice([]tar.Header{ {Name: "item-0", Typeflag: tar.TypeReg, Size: 123, Mode: 0o600, ModTime: testDate}, {Name: "item-1", Typeflag: tar.TypeReg, Size: 456, Mode: 0o600, ModTime: testDate}, {Name: "item-2", Typeflag: tar.TypeReg, Size: 789, Mode: 0o600, ModTime: testDate}, }) testArchives = []struct { name string rootOnly bool headers []tar.Header contents map[string][]byte excludes []string expectedGetErrors []expectedError subdirContents map[string][]string renames []struct { name string renames map[string]string expected []string } }{ { name: "regular", rootOnly: false, headers: []tar.Header{ {Name: "file-0", Typeflag: tar.TypeReg, Size: 123456789, Mode: 0o600, ModTime: testDate}, {Name: "file-a", Typeflag: tar.TypeReg, Size: 23, Mode: 0o600, ModTime: testDate}, {Name: "file-b", Typeflag: tar.TypeReg, Size: 23, Mode: 0o600, ModTime: testDate}, {Name: "file-c", Typeflag: tar.TypeLink, Linkname: "file-a", Mode: 0o600, ModTime: testDate}, {Name: "file-u", Typeflag: tar.TypeReg, Size: 23, Mode: cISUID | 0o755, ModTime: testDate}, {Name: "file-g", Typeflag: tar.TypeReg, Size: 23, Mode: cISGID | 0o755, ModTime: testDate}, {Name: "file-t", Typeflag: tar.TypeReg, Size: 23, Mode: cISVTX | 0o755, ModTime: testDate}, {Name: "link-0", Typeflag: tar.TypeSymlink, Linkname: "../file-0", Size: 123456789, Mode: 0o777, ModTime: testDate}, {Name: "link-a", Typeflag: tar.TypeSymlink, Linkname: "file-a", Size: 23, Mode: 0o777, ModTime: testDate}, {Name: "link-b", Typeflag: tar.TypeSymlink, Linkname: "../file-a", Size: 23, Mode: 0o777, ModTime: testDate}, {Name: "hlink-0", Typeflag: tar.TypeLink, Linkname: "file-0", Size: 123456789, Mode: 0o600, ModTime: testDate}, {Name: "hlink-a", Typeflag: tar.TypeLink, Linkname: "/file-a", Size: 23, Mode: 0o600, ModTime: testDate}, {Name: "hlink-b", Typeflag: tar.TypeLink, Linkname: "../file-b", Size: 23, Mode: 0o600, ModTime: testDate}, {Name: "subdir-a", Typeflag: tar.TypeDir, Mode: 0o700, ModTime: testDate}, {Name: "subdir-a/file-n", Typeflag: tar.TypeReg, Size: 108, Mode: 0o660, ModTime: testDate}, {Name: "subdir-a/file-o", Typeflag: tar.TypeReg, Size: 34, Mode: 0o660, ModTime: testDate}, {Name: "subdir-a/file-a", Typeflag: tar.TypeSymlink, Linkname: "../file-a", Size: 23, Mode: 0o777, ModTime: testDate}, {Name: "subdir-a/file-b", Typeflag: tar.TypeSymlink, Linkname: "../../file-b", Size: 23, Mode: 0o777, ModTime: testDate}, {Name: "subdir-a/file-c", Typeflag: tar.TypeSymlink, Linkname: "/file-c", Size: 23, Mode: 0o777, ModTime: testDate}, {Name: "subdir-b", Typeflag: tar.TypeDir, Mode: 0o700, ModTime: testDate}, {Name: "subdir-b/file-n", Typeflag: tar.TypeReg, Size: 216, Mode: 0o660, ModTime: testDate}, {Name: "subdir-b/file-o", Typeflag: tar.TypeReg, Size: 45, Mode: 0o660, ModTime: testDate}, {Name: "subdir-c", Typeflag: tar.TypeDir, Mode: 0o700, ModTime: testDate}, {Name: "subdir-c/file-n", Typeflag: tar.TypeReg, Size: 432, Mode: 0o666, ModTime: testDate}, {Name: "subdir-c/file-o", Typeflag: tar.TypeReg, Size: 56, Mode: 0o666, ModTime: testDate}, {Name: "subdir-d", Typeflag: tar.TypeDir, Mode: 0o700, ModTime: testDate}, {Name: "subdir-d/hlink-0", Typeflag: tar.TypeLink, Linkname: "../file-0", Size: 123456789, Mode: 0o600, ModTime: testDate}, {Name: "subdir-d/hlink-a", Typeflag: tar.TypeLink, Linkname: "/file-a", Size: 23, Mode: 0o600, ModTime: testDate}, {Name: "subdir-d/hlink-b", Typeflag: tar.TypeLink, Linkname: "../../file-b", Size: 23, Mode: 0o600, ModTime: testDate}, {Name: "archive-a", Typeflag: tar.TypeReg, Size: 0, Mode: 0o600, ModTime: testDate}, {Name: "subdir-e", Typeflag: tar.TypeDir, Mode: 0o500, ModTime: testDate}, {Name: "subdir-e/file-p", Typeflag: tar.TypeReg, Size: 890, Mode: 0o600, ModTime: testDate}, }, contents: map[string][]byte{ "archive-a": testArchiveSlice, }, expectedGetErrors: []expectedError{ {inSubdir: false, name: "link-0", err: syscall.ENOENT}, {inSubdir: false, name: "link-b", err: syscall.ENOENT}, {inSubdir: false, name: "subdir-a/file-b", err: syscall.ENOENT}, {inSubdir: true, name: "link-0", err: syscall.ENOENT}, {inSubdir: true, name: "link-b", err: syscall.ENOENT}, {inSubdir: true, name: "subdir-a/file-b", err: syscall.ENOENT}, {inSubdir: true, name: "subdir-a/file-c", err: syscall.ENOENT}, }, renames: []struct { name string renames map[string]string expected []string }{ { name: "no-match-dir", renames: map[string]string{"subdir-z": "subdir-y"}, expected: []string{ "file-0", "file-a", "file-b", "file-c", "file-u", "file-g", "file-t", "link-0", "link-a", "link-b", "hlink-0", "hlink-a", "hlink-b", "subdir-a", "subdir-a/file-n", "subdir-a/file-o", "subdir-a/file-a", "subdir-a/file-b", "subdir-a/file-c", "subdir-b", "subdir-b/file-n", "subdir-b/file-o", "subdir-c", "subdir-c/file-n", "subdir-c/file-o", "subdir-d", "subdir-d/hlink-0", "subdir-d/hlink-a", "subdir-d/hlink-b", "subdir-e", "subdir-e/file-p", "archive-a", }, }, { name: "no-match-file", renames: map[string]string{"file-n": "file-z"}, expected: []string{ "file-0", "file-a", "file-b", "file-c", "file-u", "file-g", "file-t", "link-0", "link-a", "link-b", "hlink-0", "hlink-a", "hlink-b", "subdir-a", "subdir-a/file-n", "subdir-a/file-o", "subdir-a/file-a", "subdir-a/file-b", "subdir-a/file-c", "subdir-b", "subdir-b/file-n", "subdir-b/file-o", "subdir-c", "subdir-c/file-n", "subdir-c/file-o", "subdir-d", "subdir-d/hlink-0", "subdir-d/hlink-a", "subdir-d/hlink-b", "subdir-e", "subdir-e/file-p", "archive-a", }, }, { name: "directory", renames: map[string]string{"subdir-a": "subdir-z"}, expected: []string{ "file-0", "file-a", "file-b", "file-c", "file-u", "file-g", "file-t", "link-0", "link-a", "link-b", "hlink-0", "hlink-a", "hlink-b", "subdir-z", "subdir-z/file-n", "subdir-z/file-o", "subdir-z/file-a", "subdir-z/file-b", "subdir-z/file-c", "subdir-b", "subdir-b/file-n", "subdir-b/file-o", "subdir-c", "subdir-c/file-n", "subdir-c/file-o", "subdir-d", "subdir-d/hlink-0", "subdir-d/hlink-a", "subdir-d/hlink-b", "subdir-e", "subdir-e/file-p", "archive-a", }, }, { name: "file-in-directory", renames: map[string]string{"subdir-a/file-n": "subdir-a/file-z"}, expected: []string{ "file-0", "file-a", "file-b", "file-c", "file-u", "file-g", "file-t", "link-0", "link-a", "link-b", "hlink-0", "hlink-a", "hlink-b", "subdir-a", "subdir-a/file-z", "subdir-a/file-o", "subdir-a/file-a", "subdir-a/file-b", "subdir-a/file-c", "subdir-b", "subdir-b/file-n", "subdir-b/file-o", "subdir-c", "subdir-c/file-n", "subdir-c/file-o", "subdir-d", "subdir-d/hlink-0", "subdir-d/hlink-a", "subdir-d/hlink-b", "subdir-e", "subdir-e/file-p", "archive-a", }, }, }, }, { name: "devices", rootOnly: true, headers: []tar.Header{ {Name: "char-dev", Typeflag: tar.TypeChar, Devmajor: 0, Devminor: 0, Mode: 0o600, ModTime: testDate}, {Name: "blk-dev", Typeflag: tar.TypeBlock, Devmajor: 0, Devminor: 0, Mode: 0o600, ModTime: testDate}, }, }, } ) func TestPutNoChroot(t *testing.T) { couldChroot := canChroot canChroot = false testPut(t) canChroot = couldChroot } func testPut(t *testing.T) { uidMap := []idtools.IDMap{{HostID: os.Getuid(), ContainerID: 0, Size: 1}} gidMap := []idtools.IDMap{{HostID: os.Getgid(), ContainerID: 0, Size: 1}} for i := range testArchives { for _, topdir := range []string{"", ".", "top"} { t.Run(fmt.Sprintf("archive=%s,topdir=%s", testArchives[i].name, topdir), func(t *testing.T) { if uid != 0 && testArchives[i].rootOnly { t.Skipf("test archive %q can only be tested with root privileges, skipping", testArchives[i].name) } dir, err := makeContextFromArchive(t, makeArchive(testArchives[i].headers, testArchives[i].contents), topdir) require.NoErrorf(t, err, "error creating context from archive %q, topdir=%q", testArchives[i].name, topdir) // enumerate what we expect to have created expected := make([]enumeratedFile, 0, len(testArchives[i].headers)+1) if topdir != "" && topdir != "." { info, err := os.Stat(filepath.Join(dir, topdir)) require.NoErrorf(t, err, "error statting directory %q", filepath.Join(dir, topdir)) expected = append(expected, enumeratedFile{ name: filepath.FromSlash(topdir), mode: info.Mode() & os.ModePerm, isSymlink: info.Mode()&os.ModeSymlink == os.ModeSymlink, date: info.ModTime().UTC().String(), }) } for _, hdr := range testArchives[i].headers { expected = append(expected, enumeratedFile{ name: filepath.Join(filepath.FromSlash(topdir), filepath.FromSlash(hdr.Name)), mode: os.FileMode(hdr.Mode) & os.ModePerm, isSymlink: hdr.Typeflag == tar.TypeSymlink, date: hdr.ModTime.UTC().String(), }) } sort.Slice(expected, func(i, j int) bool { return strings.Compare(expected[i].name, expected[j].name) < 0 }) // enumerate what we actually created fileList, err := enumerateFiles(dir) require.NoErrorf(t, err, "error walking context directory for archive %q, topdir=%q", testArchives[i].name, topdir) sort.Slice(fileList, func(i, j int) bool { return strings.Compare(fileList[i].name, fileList[j].name) < 0 }) // make sure they're the same moddedEnumeratedFiles := func(enumerated []enumeratedFile) []enumeratedFile { m := make([]enumeratedFile, 0, len(enumerated)) for i := range enumerated { e := enumeratedFile{ name: enumerated[i].name, mode: os.FileMode(int64(enumerated[i].mode) & testModeMask), isSymlink: enumerated[i].isSymlink, date: enumerated[i].date, } if testIgnoreSymlinkDates && e.isSymlink { e.date = "" } m = append(m, e) } return m } if !reflect.DeepEqual(expected, fileList) && reflect.DeepEqual(moddedEnumeratedFiles(expected), moddedEnumeratedFiles(fileList)) { logrus.Warn("chmod() lost some bits and possibly timestamps on symlinks, otherwise we match the source archive") } else { require.Equal(t, expected, fileList, "list of files in context directory for archive %q under topdir %q should match the archived used to populate it", testArchives[i].name, topdir) } }) } for _, renames := range testArchives[i].renames { t.Run(fmt.Sprintf("archive=%s,rename=%s", testArchives[i].name, renames.name), func(t *testing.T) { if uid != 0 && testArchives[i].rootOnly { t.Skipf("test archive %q can only be tested with root privileges, skipping", testArchives[i].name) } tmp := t.TempDir() archive := makeArchive(testArchives[i].headers, testArchives[i].contents) err := Put(tmp, tmp, PutOptions{UIDMap: uidMap, GIDMap: gidMap, Rename: renames.renames}, archive) require.NoErrorf(t, err, "error extracting archive %q to directory %q", testArchives[i].name, tmp) var found []string err = filepath.WalkDir(tmp, func(path string, _ fs.DirEntry, err error) error { if err != nil { return err } rel, err := filepath.Rel(tmp, path) if err != nil { return err } if rel == "." { return nil } found = append(found, rel) return nil }) require.NoErrorf(t, err, "error walking context directory for archive %q under %q", testArchives[i].name, tmp) sort.Strings(found) expected := renames.expected sort.Strings(expected) assert.Equal(t, expected, found, "renaming did not work as expected") }) } } // Overwrite directory for _, overwrite := range []bool{false, true} { for _, typeFlag := range []byte{tar.TypeReg, tar.TypeLink, tar.TypeSymlink, tar.TypeChar, tar.TypeBlock, tar.TypeFifo} { t.Run(fmt.Sprintf("overwrite (dir)=%v,type=%c", overwrite, typeFlag), func(t *testing.T) { archive := makeArchiveSlice([]tar.Header{ {Name: "target", Typeflag: tar.TypeSymlink, Mode: 0o755, Linkname: "target", ModTime: testDate}, {Name: "target", Typeflag: tar.TypeDir, Mode: 0o755, ModTime: testDate}, {Name: "target", Typeflag: tar.TypeSymlink, Mode: 0o755, Linkname: "target", ModTime: testDate}, {Name: "target", Typeflag: tar.TypeReg, Size: 123, Mode: 0o755, ModTime: testDate}, {Name: "test", Typeflag: tar.TypeDir, Size: 0, Mode: 0o755, ModTime: testDate}, {Name: "test/content", Typeflag: tar.TypeReg, Size: 0, Mode: 0o755, ModTime: testDate}, {Name: "test", Typeflag: typeFlag, Size: 0, Mode: 0o755, Linkname: "target", ModTime: testDate}, }) tmp := t.TempDir() err := Put(tmp, tmp, PutOptions{UIDMap: uidMap, GIDMap: gidMap, NoOverwriteDirNonDir: !overwrite}, bytes.NewReader(archive)) if overwrite { if !errors.Is(err, syscall.EPERM) { assert.Nilf(t, err, "expected to overwrite directory with type %c: %v", typeFlag, err) } } else { assert.Errorf(t, err, "expected an error trying to overwrite directory with type %c", typeFlag) } }) } } // Overwrite non-directory for _, overwrite := range []bool{false, true} { for _, typeFlag := range []byte{tar.TypeReg, tar.TypeLink, tar.TypeSymlink, tar.TypeChar, tar.TypeBlock, tar.TypeFifo} { t.Run(fmt.Sprintf("overwrite (non-dir)=%v,type=%c", overwrite, typeFlag), func(t *testing.T) { archive := makeArchiveSlice([]tar.Header{ {Name: "target", Typeflag: tar.TypeSymlink, Mode: 0o755, Linkname: "target", ModTime: testDate}, {Name: "target", Typeflag: tar.TypeReg, Mode: 0o755, ModTime: testDate}, {Name: "target", Typeflag: tar.TypeSymlink, Mode: 0o755, Linkname: "target", ModTime: testDate}, {Name: "target", Typeflag: tar.TypeReg, Size: 123, Mode: 0o755, ModTime: testDate}, {Name: "test", Typeflag: typeFlag, Size: 0, Mode: 0o755, Linkname: "target", ModTime: testDate}, {Name: "test", Typeflag: tar.TypeDir, Size: 0, Mode: 0o755, ModTime: testDate}, {Name: "test/content", Typeflag: tar.TypeReg, Size: 0, Mode: 0o755, ModTime: testDate}, }) tmp := t.TempDir() err := Put(tmp, tmp, PutOptions{UIDMap: uidMap, GIDMap: gidMap, NoOverwriteNonDirDir: !overwrite}, bytes.NewReader(archive)) if overwrite { if !errors.Is(err, syscall.EPERM) { assert.Nilf(t, err, "expected to overwrite file with type %c: %v", typeFlag, err) } } else { assert.Errorf(t, err, "expected an error trying to overwrite file of type %c", typeFlag) } }) } } for _, ignoreDevices := range []bool{false, true} { for _, typeFlag := range []byte{tar.TypeChar, tar.TypeBlock} { t.Run(fmt.Sprintf("ignoreDevices=%v,type=%c", ignoreDevices, typeFlag), func(t *testing.T) { if uid != 0 && !ignoreDevices { t.Skip("can only test !IgnoreDevices with root privileges, skipping") } archive := makeArchiveSlice([]tar.Header{ {Name: "test", Typeflag: typeFlag, Size: 0, Mode: 0o600, ModTime: testDate, Devmajor: 0, Devminor: 0}, {Name: "link", Typeflag: tar.TypeLink, Size: 0, Mode: 0o600, ModTime: testDate, Linkname: "test"}, {Name: "unrelated", Typeflag: tar.TypeReg, Size: 0, Mode: 0o600, ModTime: testDate}, }) tmp := t.TempDir() err := Put(tmp, tmp, PutOptions{UIDMap: uidMap, GIDMap: gidMap, IgnoreDevices: ignoreDevices}, bytes.NewReader(archive)) require.Nilf(t, err, "expected to extract content with typeflag %c without an error: %v", typeFlag, err) fileList, err := enumerateFiles(tmp) require.Nilf(t, err, "unexpected error scanning the contents of extraction directory for typeflag %c: %v", typeFlag, err) expectedItems := 3 if ignoreDevices { expectedItems = 1 } require.Equalf(t, expectedItems, len(fileList), "didn't extract as many things as expected for typeflag %c", typeFlag) }) } } for _, stripSetuidBit := range []bool{false, true} { for _, stripSetgidBit := range []bool{false, true} { for _, stripStickyBit := range []bool{false, true} { t.Run(fmt.Sprintf("stripSetuidBit=%v,stripSetgidBit=%v,stripStickyBit=%v", stripSetuidBit, stripSetgidBit, stripStickyBit), func(t *testing.T) { mode := int64(0o700) | cISUID | cISGID | cISVTX archive := makeArchiveSlice([]tar.Header{ {Name: "test", Typeflag: tar.TypeReg, Size: 0, Mode: mode, ModTime: testDate}, }) tmp := t.TempDir() putOptions := PutOptions{ UIDMap: uidMap, GIDMap: gidMap, StripSetuidBit: stripSetuidBit, StripSetgidBit: stripSetgidBit, StripStickyBit: stripStickyBit, } err := Put(tmp, tmp, putOptions, bytes.NewReader(archive)) require.Nilf(t, err, "unexpected error writing sample file", err) st, err := os.Stat(filepath.Join(tmp, "test")) require.Nilf(t, err, "unexpected error checking permissions of file", err) assert.Equalf(t, stripSetuidBit, st.Mode()&os.ModeSetuid == 0, "setuid bit was not set/stripped correctly") assert.Equalf(t, stripSetgidBit, st.Mode()&os.ModeSetgid == 0, "setgid bit was not set/stripped correctly") assert.Equalf(t, stripStickyBit, st.Mode()&os.ModeSticky == 0, "sticky bit was not set/stripped correctly") }) } } } } func isExpectedError(err error, inSubdir bool, name string, expectedErrors []expectedError) bool { // if we couldn't read that content, check if it's one of the expected failures for _, expectedError := range expectedErrors { if expectedError.inSubdir != inSubdir { continue } if expectedError.name != name { continue } if !strings.Contains(err.Error(), expectedError.err.Error()) { // not expecting this specific error continue } // it's an expected failure return true } return false } func TestStatNoChroot(t *testing.T) { couldChroot := canChroot canChroot = false testStat(t) canChroot = couldChroot } func testStat(t *testing.T) { for _, absolute := range []bool{false, true} { for _, topdir := range []string{"", ".", "top"} { for _, testArchive := range testArchives { if uid != 0 && testArchive.rootOnly { t.Logf("test archive %q can only be tested with root privileges, skipping", testArchive.name) continue } dir, err := makeContextFromArchive(t, makeArchive(testArchive.headers, testArchive.contents), topdir) require.NoErrorf(t, err, "error creating context from archive %q", testArchive.name) root := dir for _, testItem := range testArchive.headers { name := filepath.FromSlash(testItem.Name) if absolute { name = filepath.Join(root, topdir, name) } t.Run(fmt.Sprintf("absolute=%t,topdir=%s,archive=%s,item=%s", absolute, topdir, testArchive.name, name), func(t *testing.T) { // read stats about this item var excludes []string for _, exclude := range testArchive.excludes { excludes = append(excludes, filepath.FromSlash(exclude)) } options := StatOptions{ CheckForArchives: false, Excludes: excludes, } stats, err := Stat(root, topdir, options, []string{name}) require.NoErrorf(t, err, "error statting %q: %v", name, err) for _, st := range stats { // should not have gotten an error require.Emptyf(t, st.Error, "expected no error from stat %q", st.Glob) // no matching characters -> should have matched one item require.NotEmptyf(t, st.Globbed, "expected at least one match on glob %q", st.Glob) matches := 0 for _, glob := range st.Globbed { matches++ require.Equal(t, st.Glob, glob, "expected entry for %q", st.Glob) require.NotNil(t, st.Results[glob], "%q globbed %q, but there are no results for it", st.Glob, glob) toStat := glob if !absolute { toStat = filepath.Join(root, topdir, name) } _, err = os.Lstat(toStat) require.NoErrorf(t, err, "got error on lstat() of returned value %q(%q(%q)): %v", toStat, glob, name, err) result := st.Results[glob] switch testItem.Typeflag { case tar.TypeReg: if actualContent, ok := testArchive.contents[testItem.Name]; ok { testItem.Size = int64(len(actualContent)) } checkStatInfoOwnership(t, result) require.Equal(t, testItem.Size, result.Size, "unexpected size difference for %q", name) require.True(t, result.IsRegular, "expected %q.IsRegular to be true", glob) require.False(t, result.IsDir, "expected %q.IsDir to be false", glob) require.False(t, result.IsSymlink, "expected %q.IsSymlink to be false", glob) case tar.TypeDir: require.False(t, result.IsRegular, "expected %q.IsRegular to be false", glob) require.True(t, result.IsDir, "expected %q.IsDir to be true", glob) require.False(t, result.IsSymlink, "expected %q.IsSymlink to be false", glob) case tar.TypeSymlink: require.True(t, result.IsSymlink, "%q is supposed to be a symbolic link, but is not", name) require.Equal(t, filepath.FromSlash(testItem.Linkname), result.ImmediateTarget, "%q is supposed to point to %q, but points to %q", glob, testItem.Linkname, result.ImmediateTarget) case tar.TypeBlock, tar.TypeChar: require.False(t, result.IsRegular, "%q is a regular file, but is not supposed to be", name) require.False(t, result.IsDir, "%q is a directory, but is not supposed to be", name) require.False(t, result.IsSymlink, "%q is not supposed to be a symbolic link, but appears to be one", name) } } require.Equal(t, 1, matches, "non-glob %q matched %d items, not exactly one", name, matches) } }) } } } } } func TestGetSingleNoChroot(t *testing.T) { couldChroot := canChroot canChroot = false testGetSingle(t) canChroot = couldChroot } func testGetSingle(t *testing.T) { for _, absolute := range []bool{false, true} { for _, topdir := range []string{"", ".", "top"} { for _, testArchive := range testArchives { var excludes []string for _, exclude := range testArchive.excludes { excludes = append(excludes, filepath.FromSlash(exclude)) } getOptions := GetOptions{ Excludes: excludes, ExpandArchives: false, } if uid != 0 && testArchive.rootOnly { t.Logf("test archive %q can only be tested with root privileges, skipping", testArchive.name) continue } dir, err := makeContextFromArchive(t, makeArchive(testArchive.headers, testArchive.contents), topdir) require.NoErrorf(t, err, "error creating context from archive %q", testArchive.name) root := dir for _, testItem := range testArchive.headers { name := filepath.FromSlash(testItem.Name) if absolute { name = filepath.Join(root, topdir, name) } t.Run(fmt.Sprintf("absolute=%t,topdir=%s,archive=%s,item=%s", absolute, topdir, testArchive.name, name), func(t *testing.T) { // check if we can get this one item err := Get(root, topdir, getOptions, []string{name}, io.Discard) // if we couldn't read that content, check if it's one of the expected failures if err != nil && isExpectedError(err, topdir != "" && topdir != ".", testItem.Name, testArchive.expectedGetErrors) { return } require.NoErrorf(t, err, "error getting %q under %q", name, filepath.Join(root, topdir)) // we'll check subdirectories later if testItem.Typeflag == tar.TypeDir { return } // check what we get when we get this one item pipeReader, pipeWriter := io.Pipe() var getErr error var wg sync.WaitGroup wg.Add(1) go func() { getErr = Get(root, topdir, getOptions, []string{name}, pipeWriter) pipeWriter.Close() wg.Done() }() tr := tar.NewReader(pipeReader) hdr, err := tr.Next() for err == nil { assert.Equal(t, filepath.Base(name), filepath.FromSlash(hdr.Name), "expected item named %q, got %q", filepath.Base(name), filepath.FromSlash(hdr.Name)) hdr, err = tr.Next() } assert.Equal(t, io.EOF.Error(), err.Error(), "expected EOF at end of archive, got %q", err.Error()) if !t.Failed() && testItem.Typeflag == tar.TypeReg && testItem.Mode&(cISUID|cISGID|cISVTX) != 0 { for _, stripSetuidBit := range []bool{false, true} { for _, stripSetgidBit := range []bool{false, true} { for _, stripStickyBit := range []bool{false, true} { t.Run(fmt.Sprintf("absolute=%t,topdir=%s,archive=%s,item=%s,strip_setuid=%t,strip_setgid=%t,strip_sticky=%t", absolute, topdir, testArchive.name, name, stripSetuidBit, stripSetgidBit, stripStickyBit), func(t *testing.T) { var getErr error var wg sync.WaitGroup getOptions := getOptions getOptions.StripSetuidBit = stripSetuidBit getOptions.StripSetgidBit = stripSetgidBit getOptions.StripStickyBit = stripStickyBit pipeReader, pipeWriter := io.Pipe() wg.Add(1) go func() { getErr = Get(root, topdir, getOptions, []string{name}, pipeWriter) pipeWriter.Close() wg.Done() }() tr := tar.NewReader(pipeReader) hdr, err := tr.Next() for err == nil { expectedMode := testItem.Mode modifier := "" if stripSetuidBit { expectedMode &^= cISUID modifier += "(with setuid bit stripped) " } if stripSetgidBit { expectedMode &^= cISGID modifier += "(with setgid bit stripped) " } if stripStickyBit { expectedMode &^= cISVTX modifier += "(with sticky bit stripped) " } if expectedMode != hdr.Mode && expectedMode&testModeMask == hdr.Mode&testModeMask { logrus.Warnf("chmod() lost some bits: expected 0%o, got 0%o", expectedMode, hdr.Mode) } else { assert.Equal(t, expectedMode, hdr.Mode, "expected item named %q %sto have mode 0%o, got 0%o", hdr.Name, modifier, expectedMode, hdr.Mode) } hdr, err = tr.Next() } assert.Equal(t, io.EOF.Error(), err.Error(), "expected EOF at end of archive, got %q", err.Error()) wg.Wait() assert.NoErrorf(t, getErr, "unexpected error from Get(%q): %v", name, getErr) pipeReader.Close() }) } } } } wg.Wait() assert.NoErrorf(t, getErr, "unexpected error from Get(%q): %v", name, getErr) pipeReader.Close() }) } } } } } func TestGetMultipleNoChroot(t *testing.T) { couldChroot := canChroot canChroot = false testGetMultiple(t) canChroot = couldChroot } func testGetMultiple(t *testing.T) { type getTestArchiveCase struct { name string pattern string exclude []string items []string expandArchives bool stripSetuidBit bool stripSetgidBit bool stripStickyBit bool stripXattrs bool keepDirectoryNames bool renames map[string]string noDerefSymlinks bool parents bool timestamp *time.Time } getTestArchives := []struct { name string headers []tar.Header contents map[string][]byte cases []getTestArchiveCase expectedGetErrors []expectedError }{ { name: "regular", headers: []tar.Header{ {Name: "file-0", Typeflag: tar.TypeReg, Size: 123456789, Mode: 0o600}, {Name: "file-a", Typeflag: tar.TypeReg, Size: 23, Mode: 0o600}, {Name: "file-b", Typeflag: tar.TypeReg, Size: 23, Mode: 0o600}, {Name: "link-a", Typeflag: tar.TypeSymlink, Linkname: "file-a", Size: 23, Mode: 0o600}, {Name: "link-c", Typeflag: tar.TypeSymlink, Linkname: "subdir-c", Mode: 0o700, ModTime: testDate}, {Name: "archive-a", Typeflag: tar.TypeReg, Size: 0, Mode: 0o600}, {Name: "non-archive-a", Typeflag: tar.TypeReg, Size: 1199, Mode: 0o600}, {Name: "hlink-0", Typeflag: tar.TypeLink, Linkname: "file-0", Size: 123456789, Mode: 0o600}, {Name: "something-a", Typeflag: tar.TypeReg, Size: 34, Mode: 0o600}, {Name: "subdir-a/", Typeflag: tar.TypeDir, Mode: 0o700}, {Name: "subdir-a/file-n", Typeflag: tar.TypeReg, Size: 108, Mode: 0o660}, {Name: "subdir-a/file-o", Typeflag: tar.TypeReg, Size: 45, Mode: 0o660}, {Name: "subdir-a/file-a", Typeflag: tar.TypeSymlink, Linkname: "../file-a", Size: 23, Mode: 0o600}, {Name: "subdir-a/file-b", Typeflag: tar.TypeSymlink, Linkname: "../../file-b", Size: 23, Mode: 0o600}, {Name: "subdir-a/file-c", Typeflag: tar.TypeReg, Size: 56, Mode: 0o600}, {Name: "subdir-b/", Typeflag: tar.TypeDir, Mode: 0o700}, {Name: "subdir-b/file-n", Typeflag: tar.TypeReg, Size: 216, Mode: 0o660}, {Name: "subdir-b/file-o", Typeflag: tar.TypeReg, Size: 67, Mode: 0o660}, {Name: "subdir-c/", Typeflag: tar.TypeDir, Mode: 0o700}, {Name: "subdir-c/file-p", Typeflag: tar.TypeReg, Size: 432, Mode: 0o666}, {Name: "subdir-c/file-q", Typeflag: tar.TypeReg, Size: 78, Mode: 0o666}, {Name: "subdir-d/", Typeflag: tar.TypeDir, Mode: 0o700}, {Name: "subdir-d/hlink-0", Typeflag: tar.TypeLink, Linkname: "../file-0", Size: 123456789, Mode: 0o600}, {Name: "subdir-e/", Typeflag: tar.TypeDir, Mode: 0o700}, {Name: "subdir-e/subdir-f/", Typeflag: tar.TypeDir, Mode: 0o700}, {Name: "subdir-e/subdir-f/hlink-b", Typeflag: tar.TypeLink, Linkname: "../../file-b", Size: 23, Mode: 0o600}, }, contents: map[string][]byte{ "archive-a": testArchiveSlice, }, expectedGetErrors: []expectedError{ {inSubdir: true, name: ".", err: syscall.ENOENT}, {inSubdir: true, name: "/subdir-b/*", err: syscall.ENOENT}, {inSubdir: true, name: "../../subdir-b/*", err: syscall.ENOENT}, }, cases: []getTestArchiveCase{ { name: "everything", pattern: ".", items: []string{ "file-0", "file-a", "file-b", "link-a", "link-c", "hlink-0", "something-a", "archive-a", "non-archive-a", "subdir-a/", "subdir-a/file-n", "subdir-a/file-o", "subdir-a/file-a", "subdir-a/file-b", "subdir-a/file-c", "subdir-b/", "subdir-b/file-n", "subdir-b/file-o", "subdir-c/", "subdir-c/file-p", "subdir-c/file-q", "subdir-d/", "subdir-d/hlink-0", "subdir-e/", "subdir-e/subdir-f/", "subdir-e/subdir-f/hlink-b", }, }, { name: "wildcard", pattern: "*", items: []string{ "file-0", "file-a", "file-b", "link-a", "hlink-0", "something-a", "archive-a", "non-archive-a", "file-n", // from subdir-a "file-o", // from subdir-a "file-a", // from subdir-a "file-b", // from subdir-a "file-c", // from subdir-a "file-n", // from subdir-b "file-o", // from subdir-b "file-p", // from subdir-c "file-p", // from link-c -> subdir-c "file-q", // from subdir-c "file-q", // from link-c -> subdir-c "hlink-0", // from subdir-d "subdir-f/", // from subdir-e "subdir-f/hlink-b", // from subdir-e }, }, { name: "timestamped", pattern: "file*", items: []string{ "file-0", "file-a", "file-b", }, timestamp: &secondTestDate, }, { name: "dot-with-wildcard-includes-and-excludes", pattern: ".", exclude: []string{"**/*-a", "!**/*-c"}, items: []string{ "file-0", "file-b", "link-c", "hlink-0", // "subdir-a/file-c", // strings.HasPrefix("**/*-c", "subdir-a/") is false "subdir-b/", "subdir-b/file-n", "subdir-b/file-o", "subdir-c/", "subdir-c/file-p", "subdir-c/file-q", "subdir-d/", "subdir-d/hlink-0", "subdir-e/", "subdir-e/subdir-f/", "subdir-e/subdir-f/hlink-b", }, }, { name: "everything-with-wildcard-includes-and-excludes", pattern: "*", exclude: []string{"**/*-a", "!**/*-c"}, items: []string{ "file-0", "file-b", "file-c", "file-n", "file-o", "file-p", // from subdir-c "file-p", // from link-c -> subdir-c "file-q", // from subdir-c "file-q", // from link-c -> subdir-c "hlink-0", "hlink-0", "subdir-f/", "subdir-f/hlink-b", }, }, { name: "dot-with-dot-exclude", pattern: ".", exclude: []string{".", "!**/*-c"}, items: []string{ "file-0", "file-a", "file-b", "link-a", "link-c", "hlink-0", "something-a", "archive-a", "non-archive-a", "subdir-a/", "subdir-a/file-a", "subdir-a/file-b", "subdir-a/file-c", "subdir-a/file-n", "subdir-a/file-o", "subdir-b/", "subdir-b/file-n", "subdir-b/file-o", "subdir-c/", "subdir-c/file-p", "subdir-c/file-q", "subdir-d/", "subdir-d/hlink-0", "subdir-e/", "subdir-e/subdir-f/", "subdir-e/subdir-f/hlink-b", }, }, { name: "everything-with-dot-exclude", pattern: "*", exclude: []string{".", "!**/*-c"}, items: []string{ "file-0", "file-a", "file-a", "file-b", "file-b", "file-c", "file-n", "file-n", "file-o", "file-o", "file-p", "file-p", "file-q", "file-q", "hlink-0", "hlink-0", "link-a", "something-a", "archive-a", "non-archive-a", "subdir-f/", "subdir-f/hlink-b", }, }, { name: "all-with-all-exclude", pattern: "*", exclude: []string{"*", "!**/*-c"}, items: []string{ "file-c", "file-p", "file-p", "file-q", "file-q", }, }, { name: "everything-with-all-exclude", pattern: ".", exclude: []string{"*", "!**/*-c"}, items: []string{ // "subdir-a/file-c", // strings.HasPrefix("**/*-c", "subdir-a/") is false "link-c", "subdir-c/", "subdir-c/file-p", "subdir-c/file-q", }, }, { name: "file-wildcard", pattern: "file-*", items: []string{ "file-0", "file-a", "file-b", }, }, { name: "file-and-dir-wildcard", pattern: "*-a", items: []string{ "file-a", "link-a", "something-a", "archive-a", "non-archive-a", "file-n", // from subdir-a "file-o", // from subdir-a "file-a", // from subdir-a "file-b", // from subdir-a "file-c", // from subdir-a }, }, { name: "file-and-dir-wildcard-with-exclude", pattern: "*-a", exclude: []string{"subdir-a", "top/subdir-a"}, items: []string{ "file-a", "link-a", "something-a", "archive-a", "non-archive-a", }, }, { name: "file-and-dir-wildcard-with-wildcard-exclude", pattern: "*-a", exclude: []string{"subdir*", "top/subdir*"}, items: []string{ "file-a", "link-a", "something-a", "archive-a", "non-archive-a", }, }, { name: "file-and-dir-wildcard-with-deep-exclude", pattern: "*-a", exclude: []string{"**/subdir-a"}, items: []string{ "file-a", "link-a", "something-a", "archive-a", "non-archive-a", }, }, { name: "file-and-dir-wildcard-with-wildcard-deep-exclude", pattern: "*-a", exclude: []string{"**/subdir*"}, items: []string{ "file-a", "link-a", "something-a", "archive-a", "non-archive-a", }, }, { name: "file-and-dir-wildcard-with-deep-include", pattern: "*-a", exclude: []string{"**/subdir-a", "!**/file-c"}, items: []string{ "file-a", "link-a", "something-a", "archive-a", "non-archive-a", "file-c", }, }, { name: "file-and-dir-wildcard-with-wildcard-deep-include", pattern: "*-a", exclude: []string{"**/subdir*", "!**/file-c"}, items: []string{ "file-a", "link-a", "something-a", "archive-a", "non-archive-a", "file-c", }, }, { name: "subdirectory", pattern: "subdir-e", items: []string{ "subdir-f/", "subdir-f/hlink-b", }, }, { name: "subdirectory-wildcard", pattern: "*/subdir-*", items: []string{ "hlink-b", // from subdir-e/subdir-f }, }, { name: "not-expanded-archive", pattern: "*archive-a", items: []string{ "archive-a", "non-archive-a", }, }, { name: "expanded-archive", pattern: "*archive-a", expandArchives: true, items: []string{ "non-archive-a", "item-0", "item-1", "item-2", }, }, { name: "subdir-without-name", pattern: "subdir-e", items: []string{ "subdir-f/", "subdir-f/hlink-b", }, }, { name: "subdir-with-name", pattern: "subdir-e", keepDirectoryNames: true, items: []string{ "subdir-e/", "subdir-e/subdir-f/", "subdir-e/subdir-f/hlink-b", }, }, { name: "root-wildcard", pattern: "/subdir-b/*", keepDirectoryNames: false, items: []string{ "file-n", "file-o", }, }, { name: "dotdot-wildcard", pattern: "../../subdir-b/*", keepDirectoryNames: false, items: []string{ "file-n", "file-o", }, }, { name: "wildcard-with-rename", pattern: "*-a", keepDirectoryNames: false, renames: map[string]string{"file-a": "renamed"}, items: []string{ "renamed", // from file-a "link-a", "archive-a", "non-archive-a", "something-a", "file-n", // from subdir-a "file-o", // from subdir-a "renamed", // from subdir-a/file-a -> file-a -> renamed "file-b", // from subdir-a "file-c", // from subdir-a }, }, { name: "wildcard-with-rename-keep", pattern: "*-a", keepDirectoryNames: true, renames: map[string]string{"subdir-a": "subdir-b"}, items: []string{ "file-a", "link-a", "archive-a", "non-archive-a", "something-a", "subdir-b/", "subdir-b/file-n", "subdir-b/file-o", "subdir-b/file-a", "subdir-b/file-b", "subdir-b/file-c", }, }, { name: "no-deref-symlinks-baseline", pattern: "*-a", noDerefSymlinks: true, items: []string{ "file-a", "link-a", "archive-a", "non-archive-a", "something-a", "file-n", // from subdir-a "file-o", // from subdir-a "file-a", // from subdir-a "file-b", // from subdir-a "file-c", // from subdir-a }, }, { name: "no-deref-symlinks-directory", pattern: "link-c", noDerefSymlinks: true, items: []string{ "link-c", }, }, { name: "deref-symlinks-directory", pattern: "link-c", noDerefSymlinks: false, items: []string{ "file-p", // from link-c -> subdir-c "file-q", // from link-c -> subdir-c }, }, { name: "wildcard and parents", pattern: "*", parents: true, items: []string{ "file-0", "file-a", "file-b", "link-a", "hlink-0", "something-a", "archive-a", "non-archive-a", "subdir-a/", "subdir-b/", "subdir-c/", "subdir-d/", "subdir-e/", "subdir-a/file-n", "subdir-a/file-o", "subdir-a/file-a", "subdir-a/file-b", "subdir-a/file-c", "subdir-b/file-n", "subdir-b/file-o", "subdir-c/file-p", "subdir-c/file-p", "subdir-c/file-q", "subdir-c/file-q", "subdir-d/hlink-0", "subdir-e/subdir-f/", "subdir-e/subdir-f/hlink-b", }, }, { name: "everything-with-wildcard-includes-and-excludes-parents", pattern: "*", parents: true, exclude: []string{"**/*-a", "!**/*-c"}, items: []string{ "file-0", "file-b", "subdir-a/", "subdir-b/", "subdir-c/", "subdir-d/", "subdir-e/", "subdir-a/file-c", "subdir-b/file-n", "subdir-b/file-o", "subdir-c/file-p", "subdir-c/file-p", "subdir-c/file-q", "subdir-c/file-q", "hlink-0", "subdir-d/hlink-0", "subdir-e/subdir-f/", "subdir-e/subdir-f/hlink-b", }, }, { name: "file-and-dir-wildcard-parents", pattern: "*-a", parents: true, items: []string{ "file-a", "link-a", "something-a", "archive-a", "non-archive-a", "subdir-a/", "subdir-a/file-n", "subdir-a/file-o", "subdir-a/file-a", "subdir-a/file-b", "subdir-a/file-c", }, }, { name: "root-wildcard-parents", pattern: "/subdir-b/*", parents: true, items: []string{ "subdir-b/", "subdir-b/file-n", "subdir-b/file-o", }, }, { name: "dotdot-wildcard-parents", pattern: "../../subdir-b/*", parents: true, items: []string{ "subdir-b/", "subdir-b/file-n", "subdir-b/file-o", }, }, { name: "dir-with-parents", pattern: "subdir-e/subdir-f/", parents: true, items: []string{ "subdir-e/", "subdir-e/subdir-f/", "subdir-e/subdir-f/hlink-b", }, }, { name: "hlink-with-parents", pattern: "subdir-e/subdir-f/hlink-b", parents: true, items: []string{ "subdir-e/", "subdir-e/subdir-f/", "subdir-e/subdir-f/hlink-b", }, }, }, }, } for _, topdir := range []string{"", ".", "top"} { for _, testArchive := range getTestArchives { dir, err := makeContextFromArchive(t, makeArchive(testArchive.headers, testArchive.contents), topdir) require.NoErrorf(t, err, "error creating context from archive %q", testArchive.name) root := dir cases := make(map[string]struct{}) for _, testCase := range testArchive.cases { if _, ok := cases[testCase.name]; ok { t.Fatalf("duplicate case %q", testCase.name) } cases[testCase.name] = struct{}{} } for _, testCase := range testArchive.cases { var excludes []string for _, exclude := range testCase.exclude { excludes = append(excludes, filepath.FromSlash(exclude)) } getOptions := GetOptions{ Excludes: excludes, ExpandArchives: testCase.expandArchives, StripSetuidBit: testCase.stripSetuidBit, StripSetgidBit: testCase.stripSetgidBit, StripStickyBit: testCase.stripStickyBit, StripXattrs: testCase.stripXattrs, KeepDirectoryNames: testCase.keepDirectoryNames, Rename: testCase.renames, NoDerefSymlinks: testCase.noDerefSymlinks, Parents: testCase.parents, Timestamp: testCase.timestamp, } t.Run(fmt.Sprintf("topdir=%s,archive=%s,case=%s,pattern=%s", topdir, testArchive.name, testCase.name, testCase.pattern), func(t *testing.T) { // ensure that we can get stuff using this spec err := Get(root, topdir, getOptions, []string{testCase.pattern}, io.Discard) if err != nil && isExpectedError(err, topdir != "" && topdir != ".", testCase.pattern, testArchive.expectedGetErrors) { return } require.NoErrorf(t, err, "error getting %q under %q", testCase.pattern, filepath.Join(root, topdir)) // see what we get when we get this pattern pipeReader, pipeWriter := io.Pipe() var getErr error var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() getErr = Get(root, topdir, getOptions, []string{testCase.pattern}, pipeWriter) pipeWriter.Close() }() tr := tar.NewReader(pipeReader) hdr, err := tr.Next() actualContents := []string{} for hdr != nil { actualContents = append(actualContents, filepath.FromSlash(hdr.Name)) assert.Equal(t, "", hdr.Uname, "expected user name field to be cleared") assert.Equal(t, "", hdr.Gname, "expected group name field to be cleared") if testCase.timestamp != nil { assert.Truef(t, testCase.timestamp.Equal(hdr.ModTime), "timestamp was supposed to be forced for %q", hdr.Name) } if err != nil { break } hdr, err = tr.Next() } pipeReader.Close() sort.Strings(actualContents) // compare it to what we were supposed to get expectedContents := make([]string, 0, len(testCase.items)) for _, item := range testCase.items { expectedContents = append(expectedContents, filepath.FromSlash(item)) } sort.Strings(expectedContents) assert.Equal(t, io.EOF.Error(), err.Error(), "expected EOF at end of archive, got %q", err.Error()) wg.Wait() assert.NoErrorf(t, getErr, "unexpected error from Get(%q)", testCase.pattern) assert.Equal(t, expectedContents, actualContents, "Get(%q,excludes=%v) didn't produce the right set of items", testCase.pattern, excludes) }) } } } } func TestEvalNoChroot(t *testing.T) { couldChroot := canChroot canChroot = false testEval(t) canChroot = couldChroot } func testEval(t *testing.T) { tmp := t.TempDir() options := EvalOptions{} linkname := filepath.Join(tmp, "link") vectors := []struct { id, linkTarget, inputPath, evaluatedPath string }{ {"0a", "target", "link/foo", "target/foo"}, {"1a", "/target", "link/foo", "target/foo"}, {"2a", "../target", "link/foo", "target/foo"}, {"3a", "/../target", "link/foo", "target/foo"}, {"4a", "../../target", "link/foo", "target/foo"}, {"5a", "target/subdirectory", "link/foo", "target/subdirectory/foo"}, {"6a", "/target/subdirectory", "link/foo", "target/subdirectory/foo"}, {"7a", "../target/subdirectory", "link/foo", "target/subdirectory/foo"}, {"8a", "/../target/subdirectory", "link/foo", "target/subdirectory/foo"}, {"9a", "../../target/subdirectory", "link/foo", "target/subdirectory/foo"}, // inputPath is lexically cleaned to "foo" early, so callers // won't get values consistent with the kernel, but we use the // result for ADD and COPY, where docker build seems to have // the same limitation {"0b", "target", "link/../foo", "foo"}, {"1b", "/target", "link/../foo", "foo"}, {"2b", "../target", "link/../foo", "foo"}, {"3b", "/../target", "link/../foo", "foo"}, {"4b", "../../target", "link/../foo", "foo"}, {"5b", "target/subdirectory", "link/../foo", "foo"}, {"6b", "/target/subdirectory", "link/../foo", "foo"}, {"7b", "../target/subdirectory", "link/../foo", "foo"}, {"8b", "/../target/subdirectory", "link/../foo", "foo"}, {"9b", "../../target/subdirectory", "link/../foo", "foo"}, } for _, vector := range vectors { t.Run(fmt.Sprintf("id=%s", vector.id), func(t *testing.T) { err := os.Symlink(vector.linkTarget, linkname) if err != nil && errors.Is(err, os.ErrExist) { os.Remove(linkname) err = os.Symlink(vector.linkTarget, linkname) } require.NoErrorf(t, err, "error creating link from %q to %q", linkname, vector.linkTarget) evaluated, err := Eval(tmp, filepath.Join(tmp, vector.inputPath), options) require.NoErrorf(t, err, "error evaluating %q: %v", vector.inputPath, err) require.Equalf(t, filepath.Join(tmp, vector.evaluatedPath), evaluated, "evaluation of %q with %q pointing to %q failed", vector.inputPath, linkname, vector.linkTarget) }) } } func TestMkdirNoChroot(t *testing.T) { couldChroot := canChroot canChroot = false testMkdir(t) canChroot = couldChroot } func testMkdir(t *testing.T) { type testCase struct { name string create string expect []string } testArchives := []struct { name string headers []tar.Header testCases []testCase }{ { name: "regular", headers: []tar.Header{ {Name: "subdir-a", Typeflag: tar.TypeDir, Mode: 0o755, ModTime: testDate}, {Name: "subdir-a/subdir-b", Typeflag: tar.TypeDir, Mode: 0o755, ModTime: testDate}, {Name: "subdir-a/subdir-b/subdir-c", Typeflag: tar.TypeDir, Mode: 0o755, ModTime: testDate}, {Name: "subdir-a/subdir-b/dangle1", Typeflag: tar.TypeSymlink, Linkname: "dangle1-target", ModTime: testDate}, {Name: "subdir-a/subdir-b/dangle2", Typeflag: tar.TypeSymlink, Linkname: "../dangle2-target", ModTime: testDate}, {Name: "subdir-a/subdir-b/dangle3", Typeflag: tar.TypeSymlink, Linkname: "../../dangle3-target", ModTime: testDate}, {Name: "subdir-a/subdir-b/dangle4", Typeflag: tar.TypeSymlink, Linkname: "../../../dangle4-target", ModTime: testDate}, {Name: "subdir-a/subdir-b/dangle5", Typeflag: tar.TypeSymlink, Linkname: "../../../../dangle5-target", ModTime: testDate}, {Name: "subdir-a/subdir-b/dangle6", Typeflag: tar.TypeSymlink, Linkname: "/dangle6-target", ModTime: testDate}, {Name: "subdir-a/subdir-b/dangle7", Typeflag: tar.TypeSymlink, Linkname: "/../dangle7-target", ModTime: testDate}, }, testCases: []testCase{ { name: "basic", create: "subdir-d", expect: []string{"subdir-d"}, }, { name: "subdir", create: "subdir-d/subdir-e/subdir-f", expect: []string{"subdir-d", "subdir-d/subdir-e", "subdir-d/subdir-e/subdir-f"}, }, { name: "dangling-link-itself", create: "subdir-a/subdir-b/dangle1", expect: []string{"subdir-a/subdir-b/dangle1-target"}, }, { name: "dangling-link-as-intermediate-parent", create: "subdir-a/subdir-b/dangle2/final", expect: []string{"subdir-a/dangle2-target", "subdir-a/dangle2-target/final"}, }, { name: "dangling-link-as-intermediate-grandparent", create: "subdir-a/subdir-b/dangle3/final", expect: []string{"dangle3-target", "dangle3-target/final"}, }, { name: "dangling-link-as-intermediate-attempted-relative-breakout", create: "subdir-a/subdir-b/dangle4/final", expect: []string{"dangle4-target", "dangle4-target/final"}, }, { name: "dangling-link-as-intermediate-attempted-relative-breakout-again", create: "subdir-a/subdir-b/dangle5/final", expect: []string{"dangle5-target", "dangle5-target/final"}, }, { name: "dangling-link-itself-absolute", create: "subdir-a/subdir-b/dangle6", expect: []string{"dangle6-target"}, }, { name: "dangling-link-as-intermediate-absolute", create: "subdir-a/subdir-b/dangle6/final", expect: []string{"dangle6-target", "dangle6-target/final"}, }, { name: "dangling-link-as-intermediate-absolute-relative-breakout", create: "subdir-a/subdir-b/dangle7/final", expect: []string{"dangle7-target", "dangle7-target/final"}, }, { name: "parent-parent-final", create: "../../final", expect: []string{"final"}, }, { name: "root-parent-final", create: "/../final", expect: []string{"final"}, }, { name: "root-parent-intermediate-parent-final", create: "/../intermediate/../final", expect: []string{"final"}, }, }, }, } for i := range testArchives { t.Run(testArchives[i].name, func(t *testing.T) { for _, testCase := range testArchives[i].testCases { t.Run(testCase.name, func(t *testing.T) { dir, err := makeContextFromArchive(t, makeArchive(testArchives[i].headers, nil), "") require.NoErrorf(t, err, "error creating context from archive %q, topdir=%q", testArchives[i].name, "") root := dir options := MkdirOptions{ChownNew: &idtools.IDPair{UID: os.Getuid(), GID: os.Getgid()}} var beforeNames, afterNames []string err = filepath.WalkDir(dir, func(path string, _ fs.DirEntry, err error) error { if err != nil { return err } rel, err := filepath.Rel(dir, path) if err != nil { return err } beforeNames = append(beforeNames, rel) return nil }) require.NoErrorf(t, err, "error walking directory to catalog pre-Mkdir contents: %v", err) err = Mkdir(root, testCase.create, options) require.NoErrorf(t, err, "error creating directory %q under %q with Mkdir: %v", testCase.create, root, err) err = filepath.WalkDir(dir, func(path string, _ fs.DirEntry, err error) error { if err != nil { return err } rel, err := filepath.Rel(dir, path) if err != nil { return err } afterNames = append(afterNames, rel) return nil }) require.NoErrorf(t, err, "error walking directory to catalog post-Mkdir contents: %v", err) expected := slices.Clone(beforeNames) for _, expect := range testCase.expect { expected = append(expected, filepath.FromSlash(expect)) } sort.Strings(expected) sort.Strings(afterNames) assert.Equal(t, expected, afterNames, "expected different paths") }) } }) } } func TestCleanerSubdirectory(t *testing.T) { testCases := [][2]string{ {".", "."}, {"..", "."}, {"/", "."}, {"directory/subdirectory/..", "directory"}, {"directory/../..", "."}, {"../../directory", "directory"}, {"../directory/subdirectory", "directory/subdirectory"}, {"/directory/../..", "."}, {"/directory/../../directory", "directory"}, } for _, testCase := range testCases { t.Run(testCase[0], func(t *testing.T) { cleaner := cleanerReldirectory(filepath.FromSlash(testCase[0])) assert.Equal(t, testCase[1], filepath.ToSlash(cleaner), "expected to get %q, got %q", testCase[1], cleaner) }) } } func TestHandleRename(t *testing.T) { renames := map[string]string{ "a": "b", "c": "d", "a/1": "a/2", } testCases := [][2]string{ {"a", "b"}, {"a/1", "a/2"}, {"a/1/2", "a/2/2"}, {"a/1/2/3", "a/2/2/3"}, {"a/2/3/4", "b/2/3/4"}, {"a/2/3", "b/2/3"}, {"a/2", "b/2"}, {"c/2", "d/2"}, } for i, testCase := range testCases { t.Run(strconv.Itoa(i), func(t *testing.T) { renamed := handleRename(renames, testCase[0]) assert.Equal(t, testCase[1], renamed, "expected to get %q, got %q", testCase[1], renamed) }) } } func TestRemoveNoChroot(t *testing.T) { couldChroot := canChroot canChroot = false testRemove(t) canChroot = couldChroot } func testRemove(t *testing.T) { type testCase struct { name string remove string all bool fail bool removed []string } testArchives := []struct { name string headers []tar.Header testCases []testCase }{ { name: "regular", headers: []tar.Header{ {Name: "subdir-a", Typeflag: tar.TypeDir, Mode: 0o755, ModTime: testDate}, {Name: "subdir-a/file-a", Typeflag: tar.TypeReg, Mode: 0o755, ModTime: testDate}, {Name: "subdir-a/file-b", Typeflag: tar.TypeReg, Mode: 0o755, ModTime: testDate}, {Name: "subdir-a/subdir-b", Typeflag: tar.TypeDir, Mode: 0o755, ModTime: testDate}, {Name: "subdir-a/subdir-b/subdir-c", Typeflag: tar.TypeDir, Mode: 0o755, ModTime: testDate}, {Name: "subdir-a/subdir-b/subdir-c/parent", Typeflag: tar.TypeSymlink, Linkname: "..", ModTime: testDate}, {Name: "subdir-a/subdir-b/subdir-c/link-b", Typeflag: tar.TypeSymlink, Linkname: "../../file-b", ModTime: testDate}, {Name: "subdir-a/subdir-b/subdir-c/root", Typeflag: tar.TypeSymlink, Linkname: "/", ModTime: testDate}, {Name: "subdir-a/subdir-d", Typeflag: tar.TypeDir, Mode: 0o755, ModTime: testDate}, {Name: "subdir-a/subdir-e", Typeflag: tar.TypeDir, Mode: 0o755, ModTime: testDate}, {Name: "subdir-a/subdir-e/subdir-f", Typeflag: tar.TypeDir, Mode: 0o755, ModTime: testDate}, }, testCases: []testCase{ { name: "file", remove: "subdir-a/file-a", removed: []string{"subdir-a/file-a"}, }, { name: "file-all", remove: "subdir-a/file-a", all: true, removed: []string{"subdir-a/file-a"}, }, { name: "subdir", remove: "subdir-a/subdir-b", all: false, fail: true, }, { name: "subdir-all", remove: "subdir-a/subdir-b/subdir-c", all: true, removed: []string{ "subdir-a/subdir-b/subdir-c", "subdir-a/subdir-b/subdir-c/parent", "subdir-a/subdir-b/subdir-c/link-b", "subdir-a/subdir-b/subdir-c/root", }, }, { name: "file-link", remove: "subdir-a/subdir-b/subdir-c/link-b", removed: []string{"subdir-a/subdir-b/subdir-c/link-b"}, }, { name: "file-link-all", remove: "subdir-a/subdir-b/subdir-c/link-b", all: true, removed: []string{"subdir-a/subdir-b/subdir-c/link-b"}, }, { name: "file-link-indirect", remove: "subdir-a/subdir-b/subdir-c/parent/subdir-c/link-b", removed: []string{"subdir-a/subdir-b/subdir-c/link-b"}, }, { name: "file-link-indirect-all", remove: "subdir-a/subdir-b/subdir-c/parent/subdir-c/link-b", all: true, removed: []string{"subdir-a/subdir-b/subdir-c/link-b"}, }, { name: "dir-link", remove: "subdir-a/subdir-b/subdir-c/root", removed: []string{"subdir-a/subdir-b/subdir-c/root"}, }, { name: "dir-link-all", remove: "subdir-a/subdir-b/subdir-c/root", all: true, removed: []string{"subdir-a/subdir-b/subdir-c/root"}, }, { name: "dir-through-link", remove: "subdir-a/subdir-b/subdir-c/root/subdir-a/subdir-d", removed: []string{"subdir-a/subdir-d"}, }, { name: "dir-through-link-all", remove: "subdir-a/subdir-b/subdir-c/root/subdir-a/subdir-d", all: true, removed: []string{"subdir-a/subdir-d"}, }, { name: "tree-through-link", remove: "subdir-a/subdir-b/subdir-c/root/subdir-a/subdir-e", all: false, fail: true, }, { name: "tree-through-link-all", remove: "subdir-a/subdir-b/subdir-c/root/subdir-a/subdir-e", all: true, removed: []string{"subdir-a/subdir-e", "subdir-a/subdir-e/subdir-f"}, }, }, }, } for i := range testArchives { t.Run(testArchives[i].name, func(t *testing.T) { for _, testCase := range testArchives[i].testCases { t.Run(testCase.name, func(t *testing.T) { dir, err := makeContextFromArchive(t, makeArchive(testArchives[i].headers, nil), "") require.NoErrorf(t, err, "error creating context from archive %q, topdir=%q", testArchives[i].name, "") root := dir options := RemoveOptions{All: testCase.all} beforeNames := make(map[string]struct{}) err = filepath.WalkDir(dir, func(path string, _ fs.DirEntry, err error) error { if err != nil { return err } rel, err := filepath.Rel(dir, path) if err != nil { return err } beforeNames[rel] = struct{}{} return nil }) require.NoErrorf(t, err, "error walking directory to catalog pre-Remove contents: %v", err) err = Remove(root, testCase.remove, options) if testCase.fail { require.Errorf(t, err, "did not expect to succeed removing item %q under %q with Remove", testCase.remove, root) return } require.NoErrorf(t, err, "error removing item %q under %q with Remove: %v", testCase.remove, root, err) afterNames := make(map[string]struct{}) err = filepath.WalkDir(dir, func(path string, _ fs.DirEntry, err error) error { if err != nil { return err } rel, err := filepath.Rel(dir, path) if err != nil { return err } afterNames[rel] = struct{}{} return nil }) require.NoErrorf(t, err, "error walking directory to catalog post-Remove contents: %v", err) var removed []string for beforeName := range beforeNames { if _, stillPresent := afterNames[beforeName]; !stillPresent { removed = append(removed, beforeName) } } var expected []string for _, expect := range testCase.removed { expected = append(expected, filepath.FromSlash(expect)) } sort.Strings(expected) sort.Strings(removed) assert.Equal(t, expected, removed, "expected different paths to be missing") }) } }) } } func TestExtendedGlob(t *testing.T) { tmpdir := t.TempDir() buf := []byte("buffer") var expected1, expected2 []string require.NoError(t, os.Mkdir(filepath.Join(tmpdir, "a"), 0o700)) require.NoError(t, os.Mkdir(filepath.Join(tmpdir, "a", "b"), 0o700)) require.NoError(t, os.WriteFile(filepath.Join(tmpdir, "a", "b", "a.dat"), buf, 0o600)) expected1 = append(expected1, filepath.Join(tmpdir, "a", "b", "a.dat")) require.NoError(t, os.Mkdir(filepath.Join(tmpdir, "b"), 0o700)) require.NoError(t, os.Mkdir(filepath.Join(tmpdir, "b", "c"), 0o700)) require.NoError(t, os.Mkdir(filepath.Join(tmpdir, "c"), 0o700)) require.NoError(t, os.Mkdir(filepath.Join(tmpdir, "c", "d"), 0o700)) require.NoError(t, os.WriteFile(filepath.Join(tmpdir, "c", "d", "c.dat"), buf, 0o600)) expected1 = append(expected1, filepath.Join(tmpdir, "c", "d", "c.dat")) expected2 = append(expected2, filepath.Join(tmpdir, "c", "d", "c.dat")) require.NoError(t, os.Mkdir(filepath.Join(tmpdir, "d"), 0o700)) require.NoError(t, os.WriteFile(filepath.Join(tmpdir, "d", "d.dat"), buf, 0o600)) expected1 = append(expected1, filepath.Join(tmpdir, "d", "d.dat")) expected2 = append(expected2, filepath.Join(tmpdir, "d", "d.dat")) matched, err := extendedGlob(filepath.Join(tmpdir, "**", "*.dat")) require.NoError(t, err, "globbing") require.ElementsMatchf(t, expected1, matched, "**/*.dat") matched, err = extendedGlob(filepath.Join(tmpdir, "**", "d", "*.dat")) require.NoError(t, err, "globbing") require.ElementsMatch(t, expected2, matched, "**/d/*.dat") matched, err = extendedGlob(filepath.Join(tmpdir, "**", "**", "d", "*.dat")) require.NoError(t, err, "globbing") require.ElementsMatch(t, expected2, matched, "**/**/d/*.dat") matched, err = extendedGlob(filepath.Join(tmpdir, "**", "d", "**", "*.dat")) require.NoError(t, err, "globbing") require.ElementsMatch(t, expected2, matched, "**/d/**/*.dat") } func testEnsure(t *testing.T) { zero := time.Unix(0, 0) worldReadable := os.FileMode(0o644) ugReadable := os.FileMode(0o750) testCases := []struct { description string subdir string mkdirs []string options EnsureOptions expectCreated []string expectNoted []EnsureParentPath }{ { description: "base", subdir: "subdir", options: EnsureOptions{ Paths: []EnsurePath{ { Path: filepath.Join(string(os.PathSeparator), "a", "b", "a"), Typeflag: tar.TypeReg, Chmod: &worldReadable, }, { Path: filepath.Join("a", "b", "b"), Typeflag: tar.TypeReg, ModTime: &zero, }, { Path: filepath.Join(string(os.PathSeparator), "a", "b", "c"), Typeflag: tar.TypeDir, ModTime: &zero, }, { Path: filepath.Join("a", "b", "d"), Typeflag: tar.TypeDir, }, }, }, expectCreated: []string{ "subdir", "subdir/a", "subdir/a/b", "subdir/a/b/a", "subdir/a/b/b", "subdir/a/b/c", "subdir/a/b/d", }, expectNoted: []EnsureParentPath{}, }, { description: "nosubdir", options: EnsureOptions{ Paths: []EnsurePath{ { Path: filepath.Join(string(os.PathSeparator), "a", "b", "c"), Typeflag: tar.TypeDir, ModTime: &zero, }, { Path: filepath.Join("a", "b", "d"), Typeflag: tar.TypeDir, }, }, }, expectCreated: []string{ "a", "a/b", "a/b/c", "a/b/d", }, expectNoted: []EnsureParentPath{}, }, { description: "mkdir-first", subdir: "dir/subdir", mkdirs: []string{"dir", "dir/subdir"}, options: EnsureOptions{ Paths: []EnsurePath{ { Path: filepath.Join(string(os.PathSeparator), "a", "b", "a"), Typeflag: tar.TypeReg, Chmod: &worldReadable, }, { Path: filepath.Join("a", "b", "b"), Typeflag: tar.TypeReg, ModTime: &zero, }, { Path: filepath.Join(string(os.PathSeparator), "a", "b", "c"), Typeflag: tar.TypeDir, ModTime: &zero, }, { Path: filepath.Join("a", "b", "d"), Typeflag: tar.TypeDir, }, }, }, expectCreated: []string{ "dir/subdir/a", "dir/subdir/a/b", "dir/subdir/a/b/a", "dir/subdir/a/b/b", "dir/subdir/a/b/c", "dir/subdir/a/b/d", }, expectNoted: []EnsureParentPath{ { Path: "dir", Mode: &ugReadable, Owner: &idtools.IDPair{UID: 1, GID: 1}, // ModTime gets updated when we create dir/subdir, can't check it }, { Path: "dir/subdir", Mode: &ugReadable, Owner: &idtools.IDPair{UID: 1, GID: 1}, ModTime: &zero, }, }, }, } for i := range testCases { t.Run(testCases[i].description, func(t *testing.T) { testStarted := time.Now() tmpdir := t.TempDir() for _, mkdir := range testCases[i].mkdirs { err := Mkdir(tmpdir, mkdir, MkdirOptions{ ModTimeNew: &zero, ChmodNew: &ugReadable, ChownNew: &idtools.IDPair{UID: 1, GID: 1}, }) require.NoError(t, err, "unexpected error ensuring") } created, noted, err := Ensure(tmpdir, testCases[i].subdir, testCases[i].options) require.NoError(t, err, "unexpected error ensuring") require.EqualValues(t, testCases[i].expectCreated, created, "did not expect to create these") require.Equal(t, len(testCases[i].expectNoted), len(noted), "noticed the wrong number of things") for n := range noted { require.Equalf(t, testCases[i].expectNoted[n].Path, noted[n].Path, "noticed item %d path", n) if testCases[i].expectNoted[n].Mode != nil { require.Equalf(t, testCases[i].expectNoted[n].Mode.Perm(), noted[n].Mode.Perm(), "noticed item %q mode", noted[n].Path) } if testCases[i].expectNoted[n].Owner != nil { require.Equalf(t, *testCases[i].expectNoted[n].Owner, *noted[n].Owner, "noticed item %q owner", noted[n].Path) } if testCases[i].expectNoted[n].ModTime != nil { require.Equalf(t, testCases[i].expectNoted[n].ModTime.UnixNano(), noted[n].ModTime.UnixNano(), "noticed item %q mtime", noted[n].Path) } } for _, item := range testCases[i].options.Paths { target := filepath.Join(tmpdir, testCases[i].subdir, item.Path) st, err := os.Stat(target) require.NoError(t, err, "we supposedly created %q", item.Path) if item.Chmod != nil { assert.Equalf(t, *item.Chmod, st.Mode().Perm(), "permissions look wrong on %q", item.Path) } if item.Chown != nil { uid, gid, err := owner(st) require.NoErrorf(t, err, "expected to be able to read uid:gid for %q", item.Path) assert.Equalf(t, item.Chown.UID, uid, "user looks wrong on %q", item.Path) assert.Equalf(t, item.Chown.GID, gid, "group looks wrong on %q", item.Path) } if item.ModTime != nil { assert.Equalf(t, item.ModTime.Unix(), st.ModTime().Unix(), "datestamp looks wrong on %q", item.Path) } else { assert.True(t, !testStarted.After(st.ModTime()), "datestamp is too old on %q: %v < %v", st.ModTime(), testStarted) } } }) } } func TestEnsureNoChroot(t *testing.T) { couldChroot := canChroot canChroot = false testEnsure(t) canChroot = couldChroot } func testConditionalRemove(t *testing.T) { mode, mismatchedMode := os.FileMode(0o751), os.FileMode(0o755) now := time.Now() then := time.Unix(now.Unix()/2, 0) type create struct { path string typeFlag byte mtime *time.Time mode *os.FileMode } testCases := []struct { description string subdir string create []create remove ConditionalRemoveOptions expectedRemoved []string expectedRemain []string }{ { description: "withoutsubdir", create: []create{ {path: "/a", typeFlag: tar.TypeDir}, {path: "b", typeFlag: tar.TypeReg}, {path: "c/d", typeFlag: tar.TypeReg}, {path: "c/e", typeFlag: tar.TypeReg}, }, remove: ConditionalRemoveOptions{ Paths: []ConditionalRemovePath{ {Path: "a"}, {Path: "b"}, {Path: "c"}, {Path: "c/e"}, }, }, expectedRemoved: []string{"a", "b", "c/e"}, expectedRemain: []string{"c/d", "c"}, }, { description: "withsubdir", subdir: "subdir", create: []create{ {path: "/a", typeFlag: tar.TypeDir}, {path: "b", typeFlag: tar.TypeReg}, {path: "c/d", typeFlag: tar.TypeReg}, {path: "c/e", typeFlag: tar.TypeReg}, }, remove: ConditionalRemoveOptions{ Paths: []ConditionalRemovePath{ {Path: "a"}, {Path: "b"}, {Path: "c"}, {Path: "c/e"}, }, }, expectedRemoved: []string{"a", "b", "c/e"}, expectedRemain: []string{"c/d", "c"}, }, { description: "withsubdir", subdir: "subdir", create: []create{ {path: "/a", typeFlag: tar.TypeDir}, {path: "b", typeFlag: tar.TypeReg}, {path: "c/d", typeFlag: tar.TypeReg}, {path: "c/e", typeFlag: tar.TypeReg}, }, remove: ConditionalRemoveOptions{ Paths: []ConditionalRemovePath{ {Path: "a"}, {Path: "b"}, {Path: "c"}, {Path: "c/e"}, }, }, expectedRemoved: []string{"a", "b", "c/e"}, expectedRemain: []string{"c/d", "c"}, }, { description: "unconditional", create: []create{ {path: "/a", typeFlag: tar.TypeDir, mtime: &then, mode: &mode}, {path: "b", typeFlag: tar.TypeReg, mtime: &then, mode: &mode}, {path: "c/d", typeFlag: tar.TypeReg, mtime: &then, mode: &mode}, {path: "c/e", typeFlag: tar.TypeReg, mtime: &then, mode: &mode}, }, remove: ConditionalRemoveOptions{ Paths: []ConditionalRemovePath{ {Path: "a"}, {Path: "b"}, {Path: "c"}, {Path: "c/e"}, }, }, expectedRemoved: []string{"a", "b", "c/e"}, expectedRemain: []string{"c/d", "c"}, }, { description: "conditions-not-met", create: []create{ {path: "/a", typeFlag: tar.TypeDir, mtime: &then, mode: &mode}, {path: "b", typeFlag: tar.TypeReg, mtime: &then, mode: &mode}, {path: "c/d", typeFlag: tar.TypeReg, mtime: &then, mode: &mode}, {path: "c/e", typeFlag: tar.TypeReg, mtime: &then, mode: &mode}, }, remove: ConditionalRemoveOptions{ Paths: []ConditionalRemovePath{ {Path: "a", Mode: &mismatchedMode}, {Path: "b", Mode: &mismatchedMode}, {Path: "c", Mode: &mismatchedMode}, {Path: "c/e", Mode: &mismatchedMode}, {Path: "a", ModTime: &now}, {Path: "b", ModTime: &now}, {Path: "c", ModTime: &now}, {Path: "c/e", ModTime: &now}, }, }, expectedRemain: []string{"a", "b", "c/e", "c/d", "c"}, }, { description: "conditions-met", create: []create{ {path: "/a", typeFlag: tar.TypeDir, mtime: &then, mode: &mode}, {path: "b", typeFlag: tar.TypeReg, mtime: &then, mode: &mode}, {path: "c/d", typeFlag: tar.TypeReg, mtime: &then, mode: &mode}, {path: "c/e", typeFlag: tar.TypeReg, mtime: &then, mode: &mode}, }, remove: ConditionalRemoveOptions{ Paths: []ConditionalRemovePath{ {Path: "a", ModTime: &then, Mode: &mode}, {Path: "b", ModTime: &then, Mode: &mode}, {Path: "c"}, {Path: "c/d", ModTime: &then, Mode: &mode}, }, }, expectedRemoved: []string{"a", "b", "c/d"}, expectedRemain: []string{"c", "c/e"}, }, } for i := range testCases { t.Run(testCases[i].description, func(t *testing.T) { tmpdir := t.TempDir() var create EnsureOptions for _, what := range testCases[i].create { create.Paths = append(create.Paths, EnsurePath{ Path: what.path, Typeflag: what.typeFlag, ModTime: what.mtime, Chmod: what.mode, }) } created, _, err := Ensure(tmpdir, testCases[i].subdir, create) require.NoErrorf(t, err, "unexpected error creating %#v", create) remove := testCases[i].remove for _, what := range created { remove.Paths = append(remove.Paths, ConditionalRemovePath{ Path: what, }) } removed, err := ConditionalRemove(tmpdir, testCases[i].subdir, testCases[i].remove) require.NoError(t, err, "unexpected error removing") expectedRemoved := slices.Clone(testCases[i].expectedRemoved) slices.Sort(expectedRemoved) require.EqualValues(t, expectedRemoved, removed, "did not expect these to be removed") var remain []string err = filepath.Walk(filepath.Join(tmpdir, testCases[i].subdir), func(path string, _ fs.FileInfo, err error) error { if err != nil { return err } rel, err := filepath.Rel(filepath.Join(tmpdir, testCases[i].subdir), path) if err != nil { return fmt.Errorf("computing path of %q relative to %q: %w", path, filepath.Join(tmpdir, testCases[i].subdir), err) } if rel != "" && rel == "." { return nil } remain = append(remain, rel) return nil }) slices.Sort(remain) expectedRemain := slices.Clone(testCases[i].expectedRemain) slices.Sort(expectedRemain) require.NoError(t, err, "unexpected error checking what's left") require.EqualValues(t, expectedRemain, remain, "did not expect these to be left behind") }) } } func TestConditionalRemoveNoChroot(t *testing.T) { couldChroot := canChroot canChroot = false testConditionalRemove(t) canChroot = couldChroot } func TestSortedExtendedGlob(t *testing.T) { tmpdir := t.TempDir() buf := []byte("buffer") expect := []string{} for _, name := range []string{"z", "y", "x", "a", "b", "c", "d", "e", "f"} { require.NoError(t, os.WriteFile(filepath.Join(tmpdir, name), buf, 0o600)) expect = append(expect, filepath.Join(tmpdir, name)) } sort.Strings(expect) matched, err := extendedGlob(filepath.Join(tmpdir, "*")) require.NoError(t, err, "globbing") require.ElementsMatch(t, expect, matched, "sorted globbing") } func TestTarPut(t *testing.T) { testCases := []struct { name string trailingNullsBytes int }{ { name: "without-trailing-nulls", trailingNullsBytes: 0, }, { name: "with-trailing-nulls", trailingNullsBytes: 8192, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { testDir := t.TempDir() var tarBuf bytes.Buffer tw := tar.NewWriter(&tarBuf) hdr := &tar.Header{ Name: "testfile.txt", Mode: 0o644, Size: 11, ModTime: time.Now(), } err := tw.WriteHeader(hdr) require.NoError(t, err, "failed to write tar header") _, err = tw.Write([]byte("hello world")) require.NoError(t, err, "failed to write tar content") err = tw.Close() require.NoError(t, err, "failed to close tar writer") // Add extra trailing null bytes to simulate what many tar implementations do. // This reproduces issue https://github.com/containers/buildah/issues/6573 where // tar.Reader returns EOF after the standard EOF marker but before all trailing nulls // are consumed, causing broken pipe. if tc.trailingNullsBytes > 0 { extraNulls := make([]byte, tc.trailingNullsBytes) tarBuf.Write(extraNulls) } pipeReader, pipeWriter := io.Pipe() writeErrChan := make(chan error, 1) go func() { defer pipeWriter.Close() _, err := io.Copy(pipeWriter, &tarBuf) writeErrChan <- err }() req := request{ Root: testDir, Directory: "/", Request: requestPut, } resp, cb, err := copierHandlerPut(pipeReader, req, nil) require.NoError(t, err, "copierHandlerPut returned error") require.Empty(t, resp.Error, "copierHandlerPut returned error response") require.NotNil(t, cb, "expected callback to be returned") require.NoError(t, cb(), "callback returned error") select { case writeErr := <-writeErrChan: require.NoError(t, writeErr, "write to pipe failed (broken pipe)") case <-time.After(5 * time.Second): t.Fatal("timeout waiting for write to complete") } extractedFile := filepath.Join(testDir, "testfile.txt") content, err := os.ReadFile(extractedFile) require.NoError(t, err, "failed to read extracted file") require.Equal(t, "hello world", string(content), "extracted file content mismatch") }) } } ================================================ FILE: copier/copier_unix_test.go ================================================ //go:build !windows package copier import ( "os" "testing" "github.com/stretchr/testify/require" ) const ( testModeMask = int64(os.ModePerm) testIgnoreSymlinkDates = false ) func TestPutChroot(t *testing.T) { if uid != 0 { t.Skip("chroot() requires root privileges, skipping") } couldChroot := canChroot canChroot = true testPut(t) canChroot = couldChroot } func TestStatChroot(t *testing.T) { if uid != 0 { t.Skip("chroot() requires root privileges, skipping") } couldChroot := canChroot canChroot = true testStat(t) canChroot = couldChroot } func TestGetSingleChroot(t *testing.T) { if uid != 0 { t.Skip("chroot() requires root privileges, skipping") } couldChroot := canChroot canChroot = true testGetSingle(t) canChroot = couldChroot } func TestGetMultipleChroot(t *testing.T) { if uid != 0 { t.Skip("chroot() requires root privileges, skipping") } couldChroot := canChroot canChroot = true testGetMultiple(t) canChroot = couldChroot } func TestEvalChroot(t *testing.T) { if uid != 0 { t.Skip("chroot() requires root privileges, skipping") } couldChroot := canChroot canChroot = true testEval(t) canChroot = couldChroot } func TestMkdirChroot(t *testing.T) { if uid != 0 { t.Skip("chroot() requires root privileges, skipping") } couldChroot := canChroot canChroot = true testMkdir(t) canChroot = couldChroot } func TestRemoveChroot(t *testing.T) { if uid != 0 { t.Skip("chroot() requires root privileges, skipping") } couldChroot := canChroot canChroot = true testRemove(t) canChroot = couldChroot } func TestEnsureChroot(t *testing.T) { if uid != 0 { t.Skip("chroot() requires root privileges, skipping") } couldChroot := canChroot canChroot = true testEnsure(t) canChroot = couldChroot } func TestConditionalRemoveChroot(t *testing.T) { if uid != 0 { t.Skip("chroot() requires root privileges, skipping") } couldChroot := canChroot canChroot = true testConditionalRemove(t) canChroot = couldChroot } func checkStatInfoOwnership(t *testing.T, result *StatForItem) { t.Helper() require.EqualValues(t, 0, result.UID, "expected the owning user to be reported") require.EqualValues(t, 0, result.GID, "expected the owning group to be reported") } ================================================ FILE: copier/copier_windows_test.go ================================================ //go:build windows package copier import ( "testing" "github.com/stretchr/testify/require" ) func checkStatInfoOwnership(t *testing.T, result *StatForItem) { t.Helper() require.EqualValues(t, -1, result.UID, "expected the owning user to not be supported") require.EqualValues(t, -1, result.GID, "expected the owning group to not be supported") } ================================================ FILE: copier/hardlink_not_uint64.go ================================================ //go:build darwin || (linux && mips) || (linux && mipsle) || (linux && mips64) || (linux && mips64le) package copier import ( "syscall" ) func makeHardlinkDeviceAndInode(st *syscall.Stat_t) hardlinkDeviceAndInode { return hardlinkDeviceAndInode{ device: uint64(st.Dev), inode: uint64(st.Ino), } } ================================================ FILE: copier/hardlink_uint64.go ================================================ //go:build (linux && !mips && !mipsle && !mips64 && !mips64le) || freebsd || netbsd package copier import ( "syscall" ) func makeHardlinkDeviceAndInode(st *syscall.Stat_t) hardlinkDeviceAndInode { return hardlinkDeviceAndInode{ device: st.Dev, inode: st.Ino, } } ================================================ FILE: copier/hardlink_unix.go ================================================ //go:build !windows package copier import ( "os" "sync" "syscall" ) type hardlinkDeviceAndInode struct { device, inode uint64 } type hardlinkChecker struct { hardlinks sync.Map } func (h *hardlinkChecker) Check(fi os.FileInfo) string { if st, ok := fi.Sys().(*syscall.Stat_t); ok && fi.Mode().IsRegular() && st.Nlink > 1 { if name, ok := h.hardlinks.Load(makeHardlinkDeviceAndInode(st)); ok && name.(string) != "" { return name.(string) } } return "" } func (h *hardlinkChecker) Add(fi os.FileInfo, name string) { if st, ok := fi.Sys().(*syscall.Stat_t); ok && fi.Mode().IsRegular() && st.Nlink > 1 { h.hardlinks.Store(makeHardlinkDeviceAndInode(st), name) } } ================================================ FILE: copier/hardlink_windows.go ================================================ //go:build !linux && !darwin package copier import ( "os" ) type hardlinkChecker struct{} func (h *hardlinkChecker) Check(fi os.FileInfo) string { return "" } func (h *hardlinkChecker) Add(fi os.FileInfo, name string) { } ================================================ FILE: copier/mknod_int.go ================================================ //go:build !windows && !freebsd package copier import ( "golang.org/x/sys/unix" ) func mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, dev) } ================================================ FILE: copier/mknod_uint64.go ================================================ //go:build freebsd package copier import ( "golang.org/x/sys/unix" ) func mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, uint64(dev)) } ================================================ FILE: copier/syscall_unix.go ================================================ //go:build !windows package copier import ( "fmt" "os" "syscall" "time" "golang.org/x/sys/unix" ) var canChroot = os.Getuid() == 0 func chroot(root string) (bool, error) { if canChroot { if err := os.Chdir(root); err != nil { return false, fmt.Errorf("changing to intended-new-root directory %q: %w", root, err) } if err := unix.Chroot(root); err != nil { return false, fmt.Errorf("chrooting to directory %q: %w", root, err) } if err := os.Chdir(string(os.PathSeparator)); err != nil { return false, fmt.Errorf("changing to just-became-root directory %q: %w", root, err) } return true, nil } return false, nil } func chrMode(mode os.FileMode) uint32 { return uint32(unix.S_IFCHR | mode) } func blkMode(mode os.FileMode) uint32 { return uint32(unix.S_IFBLK | mode) } func mkdev(major, minor uint32) uint64 { return unix.Mkdev(major, minor) } func mkfifo(path string, mode uint32) error { return unix.Mkfifo(path, mode) } func chmod(path string, mode os.FileMode) error { return os.Chmod(path, mode) } func chown(path string, uid, gid int) error { return os.Chown(path, uid, gid) } func lchown(path string, uid, gid int) error { return os.Lchown(path, uid, gid) } func lutimes(_ bool, path string, atime, mtime time.Time) error { if atime.IsZero() || mtime.IsZero() { now := time.Now() if atime.IsZero() { atime = now } if mtime.IsZero() { mtime = now } } return unix.Lutimes(path, []unix.Timeval{unix.NsecToTimeval(atime.UnixNano()), unix.NsecToTimeval(mtime.UnixNano())}) } func owner(info os.FileInfo) (int, int, error) { if st, ok := info.Sys().(*syscall.Stat_t); ok { return int(st.Uid), int(st.Gid), nil } return -1, -1, syscall.ENOSYS } // sameDevice returns true unless we're sure that they're not on the same device func sameDevice(a, b os.FileInfo) bool { aSys := a.Sys() bSys := b.Sys() if aSys == nil || bSys == nil { return true } uA, okA := aSys.(*syscall.Stat_t) uB, okB := bSys.(*syscall.Stat_t) if !okA || !okB { return true } return uA.Dev == uB.Dev } ================================================ FILE: copier/syscall_windows.go ================================================ //go:build windows package copier import ( "errors" "os" "syscall" "time" "golang.org/x/sys/windows" ) var canChroot = false func chroot(path string) (bool, error) { return false, nil } func chrMode(mode os.FileMode) uint32 { return windows.S_IFCHR | uint32(mode) } func blkMode(mode os.FileMode) uint32 { return windows.S_IFBLK | uint32(mode) } func mkdev(major, minor uint32) uint64 { return 0 } func mkfifo(path string, mode uint32) error { return syscall.ENOSYS } func mknod(path string, mode uint32, dev int) error { return syscall.ENOSYS } func chmod(path string, mode os.FileMode) error { err := os.Chmod(path, mode) if err != nil && errors.Is(err, syscall.EWINDOWS) { return nil } return err } func chown(path string, uid, gid int) error { err := os.Chown(path, uid, gid) if err != nil && errors.Is(err, syscall.EWINDOWS) { return nil } return err } func lchown(path string, uid, gid int) error { err := os.Lchown(path, uid, gid) if err != nil && errors.Is(err, syscall.EWINDOWS) { return nil } return err } func lutimes(isSymlink bool, path string, atime, mtime time.Time) error { if isSymlink { return nil } if atime.IsZero() || mtime.IsZero() { now := time.Now() if atime.IsZero() { atime = now } if mtime.IsZero() { mtime = now } } return windows.UtimesNano(path, []windows.Timespec{windows.NsecToTimespec(atime.UnixNano()), windows.NsecToTimespec(mtime.UnixNano())}) } func owner(info os.FileInfo) (int, int, error) { return -1, -1, syscall.ENOSYS } // sameDevice returns true since we can't be sure that they're not on the same device func sameDevice(a, b os.FileInfo) bool { return true } ================================================ FILE: copier/xattrs.go ================================================ //go:build linux || netbsd || freebsd || darwin package copier import ( "errors" "fmt" "path/filepath" "strings" "syscall" "github.com/sirupsen/logrus" "go.podman.io/storage/pkg/unshare" "golang.org/x/sys/unix" ) const ( xattrsSupported = true imaXattr = "security.ima" ) var ( relevantAttributes = []string{"security.capability", imaXattr, "user.*"} // the attributes that we preserve - we discard others irrelevantAttributes = []string{"user.overlay.*"} // the attributes that we discard, even from the relevantAttributes list initialXattrListSize = 64 * 1024 initialXattrValueSize = 64 * 1024 ) // isRelevantXattr checks if "attribute" matches one of the attribute patterns // listed in the "relevantAttributes" list. func isRelevantXattr(attribute string) bool { for _, relevant := range relevantAttributes { matched, err := filepath.Match(relevant, attribute) if err != nil || !matched { continue } for _, irrelevant := range irrelevantAttributes { matched, err := filepath.Match(irrelevant, attribute) if err != nil || !matched { continue } return false } return true } return false } // Lgetxattrs returns a map of the relevant extended attributes set on the given file. func Lgetxattrs(path string) (map[string]string, error) { maxSize := 64 * 1024 * 1024 listSize := initialXattrListSize var list []byte for listSize < maxSize { list = make([]byte, listSize) size, err := unix.Llistxattr(path, list) if err != nil { if errors.Is(err, syscall.ERANGE) { listSize *= 2 continue } if errors.Is(err, syscall.ENOTSUP) || errors.Is(err, syscall.ENOSYS) { // treat these errors listing xattrs as equivalent to "no xattrs" list = list[:0] break } return nil, fmt.Errorf("listing extended attributes of %q: %w", path, err) } list = list[:size] break } if listSize >= maxSize { return nil, fmt.Errorf("unable to read list of attributes for %q: size would have been too big", path) } m := make(map[string]string) for attribute := range strings.SplitSeq(string(list), string('\000')) { if isRelevantXattr(attribute) { attributeSize := initialXattrValueSize var attributeValue []byte for attributeSize < maxSize { attributeValue = make([]byte, attributeSize) size, err := unix.Lgetxattr(path, attribute, attributeValue) if err != nil { if errors.Is(err, syscall.ERANGE) { attributeSize *= 2 continue } return nil, fmt.Errorf("getting value of extended attribute %q on %q: %w", attribute, path, err) } m[attribute] = string(attributeValue[:size]) break } if attributeSize >= maxSize { return nil, fmt.Errorf("unable to read attribute %q of %q: size would have been too big", attribute, path) } } } return m, nil } // Lsetxattrs sets the relevant members of the specified extended attributes on the given file. func Lsetxattrs(path string, xattrs map[string]string) error { for attribute, value := range xattrs { if isRelevantXattr(attribute) { if err := unix.Lsetxattr(path, attribute, []byte(value), 0); err != nil { if unshare.IsRootless() && attribute == imaXattr { logrus.Warnf("Unable to set %q xattr on %q: %v", attribute, path, err) } else { return fmt.Errorf("setting value of extended attribute %q on %q: %w", attribute, path, err) } } } } return nil } ================================================ FILE: copier/xattrs_test.go ================================================ //go:build linux || netbsd || freebsd || darwin package copier import ( "errors" "fmt" "os" "syscall" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func init() { // exercise the ERANGE-handling logic initialXattrListSize = 1 initialXattrValueSize = 1 } func TestXattrIsRelevant(t *testing.T) { cases := []struct { xattrName string relevant bool }{ {"user.a", true}, {"user.b", true}, {"security.foo", false}, {imaXattr, true}, {"security.capability", true}, {"user.overlay.base", false}, } for _, c := range cases { t.Run(c.xattrName, func(t *testing.T) { relevant := isRelevantXattr(c.xattrName) if c.relevant { require.True(t, relevant, "should be considered relevant and kept") } else { require.False(t, relevant, "should be considered irrelevant and discarded") } }) } } func TestXattrs(t *testing.T) { t.Parallel() if !xattrsSupported { t.Skipf("xattrs are not supported on this platform, skipping") } testValues := map[string]string{ "user.a": "attribute value a", "user.b": "attribute value b", } tmp := t.TempDir() for attribute, value := range testValues { t.Run(fmt.Sprintf("attribute=%s", attribute), func(t *testing.T) { f, err := os.CreateTemp(tmp, "copier-xattr-test-") if !assert.Nil(t, err, "error creating test file: %v", err) { t.FailNow() } defer os.Remove(f.Name()) err = Lsetxattrs(f.Name(), map[string]string{attribute: value}) if errors.Is(err, syscall.ENOTSUP) { t.Skipf("extended attributes not supported on %q, skipping", tmp) } if !assert.Nil(t, err, "error setting attribute on file: %v", err) { t.FailNow() } xattrs, err := Lgetxattrs(f.Name()) if !assert.Nil(t, err, "error reading attributes of file: %v", err) { t.FailNow() } xvalue, ok := xattrs[attribute] if !assert.True(t, ok, "did not read back attribute %q for file", attribute) { t.FailNow() } if !assert.Equal(t, value, xvalue, "read back different value for attribute %q", attribute) { t.FailNow() } }) } } ================================================ FILE: copier/xattrs_unsupported.go ================================================ //go:build !linux && !netbsd && !freebsd && !darwin package copier const ( xattrsSupported = false ) func Lgetxattrs(path string) (map[string]string, error) { return nil, nil } func Lsetxattrs(path string, xattrs map[string]string) error { return nil } ================================================ FILE: define/build.go ================================================ package define import ( "io" "time" encconfig "github.com/containers/ocicrypt/config" "go.podman.io/common/libimage/manifests" nettypes "go.podman.io/common/libnetwork/types" "go.podman.io/image/v5/docker/reference" "go.podman.io/image/v5/types" "go.podman.io/storage/pkg/archive" "golang.org/x/sync/semaphore" ) // AdditionalBuildContext contains verbose details about a parsed build context from --build-context type AdditionalBuildContext struct { // Value is the URL of an external tar archive. IsURL bool // Value is the name of an image which may or may not have already been pulled. IsImage bool // Value holds a URL (if IsURL), an image name (if IsImage), or an absolute filesystem path. Value string // Absolute filesystem path to a downloaded and exported build context // from an external tar archive. This will be populated only if the // build context was a URL and its contents have been downloaded. DownloadedCache string } // CommonBuildOptions are resources that can be defined by flags for both buildah from and build type CommonBuildOptions struct { // AddHost is the list of hostnames to add to the build container's /etc/hosts. AddHost []string // OmitHistory tells the builder to ignore the history of build layers and // base while preparing image-spec, setting this to true will ensure no history // is added to the image-spec. (default false) OmitHistory bool // CgroupParent is the path to cgroups under which the cgroup for the container will be created. CgroupParent string // CPUPeriod limits the CPU CFS (Completely Fair Scheduler) period CPUPeriod uint64 // CPUQuota limits the CPU CFS (Completely Fair Scheduler) quota CPUQuota int64 // CPUShares (relative weight CPUShares uint64 // CPUSetCPUs in which to allow execution (0-3, 0,1) CPUSetCPUs string // CPUSetMems memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems. CPUSetMems string // HTTPProxy determines whether *_proxy env vars from the build host are passed into the container. HTTPProxy bool // IdentityLabel if set controls whether or not a `io.buildah.version` label is added to the built image. // Setting this to false does not clear the label if it would be inherited from the base image. IdentityLabel types.OptionalBool // Memory is the upper limit (in bytes) on how much memory running containers can use. Memory int64 // DNSSearch is the list of DNS search domains to add to the build container's /etc/resolv.conf DNSSearch []string // DNSServers is the list of DNS servers to add to the build container's /etc/resolv.conf DNSServers []string // DNSOptions is the list of DNS DNSOptions []string // LabelOpts is a slice of the fields of an SELinux context, given in "field:pair" format, or "disable". // Recognized field names are "role", "type", and "level". LabelOpts []string // Paths to mask Masks []string // MemorySwap limits the amount of memory and swap together. MemorySwap int64 // NoHostname tells the builder not to create /etc/hostname content when running // containers. NoHostname bool // NoHosts tells the builder not to create /etc/hosts content when running // containers. NoHosts bool // NoNewPrivileges removes the ability for the container to gain privileges NoNewPrivileges bool // OmitTimestamp forces epoch 0 as created timestamp to allow for // deterministic, content-addressable builds. OmitTimestamp bool // SeccompProfilePath is the pathname of a seccomp profile. SeccompProfilePath string // ApparmorProfile is the name of an apparmor profile. ApparmorProfile string // ShmSize is the "size" value to use when mounting an shmfs on the container's /dev/shm directory. ShmSize string // Ulimit specifies resource limit options, in the form type:softlimit[:hardlimit]. // These types are recognized: // "core": maximum core dump size (ulimit -c) // "cpu": maximum CPU time (ulimit -t) // "data": maximum size of a process's data segment (ulimit -d) // "fsize": maximum size of new files (ulimit -f) // "locks": maximum number of file locks (ulimit -x) // "memlock": maximum amount of locked memory (ulimit -l) // "msgqueue": maximum amount of data in message queues (ulimit -q) // "nice": niceness adjustment (nice -n, ulimit -e) // "nofile": maximum number of open files (ulimit -n) // "nproc": maximum number of processes (ulimit -u) // "rss": maximum size of a process's (ulimit -m) // "rtprio": maximum real-time scheduling priority (ulimit -r) // "rttime": maximum amount of real-time execution between blocking syscalls // "sigpending": maximum number of pending signals (ulimit -i) // "stack": maximum stack size (ulimit -s) Ulimit []string // Volumes to bind mount into the container Volumes []string // Secrets are the available secrets to use in a build. Each item in the // slice takes the form "id=foo,src=bar", where both "id" and "src" are // required, in that order, and "bar" is the name of a file. Secrets []string // SSHSources is the available ssh agent connections to forward in the build SSHSources []string // OCIHooksDir is the location of OCI hooks for the build containers OCIHooksDir []string // Paths to unmask Unmasks []string } // BuildOptions can be used to alter how an image is built. type BuildOptions struct { // ContainerSuffix it the name to suffix containers with ContainerSuffix string // ContextDirectory is the default source location for COPY and ADD // commands. ContextDirectory string // PullPolicy controls whether or not we pull images. It should be one // of PullIfMissing, PullAlways, PullIfNewer, or PullNever. PullPolicy PullPolicy // Registry is a value which is prepended to the image's name, if it // needs to be pulled and the image name alone can not be resolved to a // reference to a source image. No separator is implicitly added. Registry string // IgnoreUnrecognizedInstructions tells us to just log instructions we // don't recognize, and try to keep going. IgnoreUnrecognizedInstructions bool // Manifest Name to which the image will be added. Manifest string // Quiet tells us whether or not to announce steps as we go through them. Quiet bool // Isolation controls how Run() runs things. Isolation Isolation // Runtime is the name of the command to run for RUN instructions when // Isolation is either IsolationDefault or IsolationOCI. It should // accept the same arguments and flags that runc does. Runtime string // RuntimeArgs adds global arguments for the runtime. RuntimeArgs []string // TransientMounts is a list of unparsed src:dest volume instructions that will be provided to // RUN instructions. TransientMounts []string // TransientRunMounts is a list of unparsed mounts (e.g. type=secret etc) that will be provided to // RUN instructions. TransientRunMounts []string // CacheFrom specifies any remote repository which can be treated as // potential cache source. CacheFrom []reference.Named // CacheTo specifies any remote repository which can be treated as // potential cache destination. CacheTo []reference.Named // CacheTTL specifies duration, if specified using `--cache-ttl` then // cache intermediate images under this duration will be considered as // valid cache sources and images outside this duration will be ignored. CacheTTL time.Duration // Compression specifies the type of compression which is applied to // layer blobs. The default is to not use compression, but // archive.Gzip is recommended. Compression archive.Compression // Arguments which can be interpolated into Dockerfiles Args map[string]string // Map of external additional build contexts AdditionalBuildContexts map[string]*AdditionalBuildContext // Name of the image to write to. Output string // BuildOutputs specifies if any custom build output is selected for // following build. It allows the end user to export the image's // rootfs to a directory or a tar archive. See the documentation of // 'buildah build --output' for the details of the syntax. BuildOutputs []string // Deprecated: use BuildOutputs instead. BuildOutput string // ConfidentialWorkload controls whether or not, and if so, how, we produce an // image that's meant to be run using krun as a VM instead of a conventional // process-type container. ConfidentialWorkload ConfidentialWorkloadOptions // Additional tags to add to the image that we write, if we know of a // way to add them. AdditionalTags []string // Logfile specifies if log output is redirected to an external file // instead of stdout, stderr. LogFile string // LogByPlatform tells imagebuildah to split log to different log files // for each platform if logging to external file was selected. LogSplitByPlatform bool // Log is a callback that will print a progress message. If no value // is supplied, the message will be sent to Err (or os.Stderr, if Err // is nil) by default. Log func(format string, args ...any) // In is connected to stdin for RUN instructions. In io.Reader // Out is a place where non-error log messages are sent. Out io.Writer // Err is a place where error log messages should be sent. Err io.Writer // SignaturePolicyPath specifies an override location for the signature // policy which should be used for verifying the new image as it is // being written. Except in specific circumstances, no value should be // specified, indicating that the shared, system-wide default policy // should be used. SignaturePolicyPath string // SourcePolicyFile specifies the path to a BuildKit-compatible source // policy JSON file. When specified, source references (e.g., base images // in FROM instructions) are evaluated against the policy rules. Rules // can DENY specific sources or CONVERT them to different references // (e.g., pinning tags to digests). SourcePolicyFile string // SkipUnusedStages allows users to skip stages in a multi-stage builds // which do not contribute anything to the target stage. Expected default // value is true. SkipUnusedStages types.OptionalBool // ReportWriter is an io.Writer which will be used to report the // progress of the (possible) pulling of the source image and the // writing of the new image. ReportWriter io.Writer // OutputFormat is the format of the output image's manifest and // configuration data. // Accepted values are buildah.OCIv1ImageManifest and buildah.Dockerv2ImageManifest. OutputFormat string // SystemContext holds parameters used for authentication. SystemContext *types.SystemContext // NamespaceOptions controls how we set up namespaces processes that we // might need when handling RUN instructions. NamespaceOptions []NamespaceOption // ConfigureNetwork controls whether or not network interfaces and // routing are configured for a new network namespace (i.e., when not // joining another's namespace and not just using the host's // namespace), effectively deciding whether or not the process has a // usable network. ConfigureNetwork NetworkConfigurationPolicy // Deprecated: CNIPluginPath was the location of CNI plugin helpers. // It is no longer used and is expected to be empty. CNIPluginPath string // Deprecated: CNIConfigDir was the location of CNI configuration files. // It is no longer used and is expected to be empty. CNIConfigDir string // NetworkInterface is the libnetwork network interface used to setup netavark networks. NetworkInterface nettypes.ContainerNetwork `json:"-"` // ID mapping options to use if we're setting up our own user namespace // when handling RUN instructions. IDMappingOptions *IDMappingOptions // InheritLabels controls whether or not built images will retain the labels // which were set in their base images InheritLabels types.OptionalBool // InheritAnnotations controls whether or not built images will retain the annotations // which were set in their base images InheritAnnotations types.OptionalBool // AddCapabilities is a list of capabilities to add to the default set when // handling RUN instructions. AddCapabilities []string // DropCapabilities is a list of capabilities to remove from the default set // when handling RUN instructions. If a capability appears in both lists, it // will be dropped. DropCapabilities []string // CommonBuildOpts is *required*. CommonBuildOpts *CommonBuildOptions // CPPFlags are additional arguments to pass to the C Preprocessor (cpp). CPPFlags []string // DefaultMountsFilePath is the file path holding the mounts to be mounted for RUN // instructions in "host-path:container-path" format DefaultMountsFilePath string // IIDFile tells the builder to write the image ID to the specified file IIDFile string // IIDFileRaw tells the builder to write the image ID to the specified file without the algorithm prefix IIDFileRaw string // Squash tells the builder to produce an image with a single layer instead of with // possibly more than one layer, by only committing a new layer after processing the // final instruction. Squash bool // Labels to set in a committed image. Labels []string // LayerLabels metadata for an intermediate image LayerLabels []string // Annotations to set in a committed image, in OCI format. Annotations []string // OnBuild commands to be run by builds that use the image we'll commit as a base image. OnBuild []string // Layers tells the builder to commit an image for each step in the Dockerfile. Layers bool // SaveStages tells the builder to save intermediate stage images instead of removing them. SaveStages bool // StageLabels tells the builder to add metadata labels to all stage images (including the final image). // These labels include stage name (stage alias or position) and base image (pullspec or image ID if // stage uses another stage as base). This option requires SaveStages to be enabled. StageLabels bool // NoCache tells the builder to build the image from scratch without checking for a cache. // It creates a new set of cached images for the build. NoCache bool // RemoveIntermediateCtrs tells the builder whether to remove intermediate containers used // during the build process. Default is true. RemoveIntermediateCtrs bool // ForceRmIntermediateCtrs tells the builder to remove all intermediate containers even if // the build was unsuccessful. ForceRmIntermediateCtrs bool // BlobDirectory is a directory which we'll use for caching layer blobs. // // This option will be overridden for cache pulls if // CachePullDestinationLookupReferenceFunc is set, and overridden for cache pushes if // CachePushSourceLookupReferenceFunc is set. BlobDirectory string // Target the targeted FROM in the Dockerfile to build. Target string // Devices are unparsed devices to provide to RUN instructions. Devices []string // SignBy is the fingerprint of a GPG key to use for signing images. SignBy string // Architecture specifies the target architecture of the image to be built. Architecture string // Timestamp specifies a timestamp to use for the image's created-on // date, the corresponding field in new history entries, the timestamps // to set on contents in new layer diffs, and the timestamps to set on // contents written as specified in the BuildOutput field. If left // unset, the current time is used for the configuration and manifest, // and layer contents are recorded as-is. Timestamp *time.Time // SourceDateEpoch specifies a timestamp to use for the image's // created-on date and the corresponding field in new history entries, // and any content written as specified in the BuildOutput field. If // left unset, the current time is used for the configuration and // manifest, and layer and BuildOutput contents retain their original // timestamps. SourceDateEpoch *time.Time // RewriteTimestamp, if set, forces timestamps in generated layers to // not be later than the SourceDateEpoch, if it is also set. RewriteTimestamp bool // OS is the specifies the operating system of the image to be built. OS string // MaxPullPushRetries is the maximum number of attempts we'll make to pull or push any one // image from or to an external registry if the first attempt fails. MaxPullPushRetries int // PullPushRetryDelay is how long to wait before retrying a pull or push attempt. PullPushRetryDelay time.Duration // OciDecryptConfig contains the config that can be used to decrypt an image if it is // encrypted if non-nil. If nil, it does not attempt to decrypt an image. OciDecryptConfig *encconfig.DecryptConfig // Jobs is the number of stages to run in parallel. If not specified it defaults to 1. // Ignored if a JobSemaphore is provided. Jobs *int // JobSemaphore, for when you want Jobs to be shared with more than just this build. JobSemaphore *semaphore.Weighted // LogRusage logs resource usage for each step. LogRusage bool // File to which the Rusage logs will be saved to instead of stdout. RusageLogFile string // Excludes is a list of excludes to be used instead of the .dockerignore file. Excludes []string // IgnoreFile is a name of the .containerignore file IgnoreFile string // From is the image name to use to replace the value specified in the first // FROM instruction in the Containerfile. From string // GroupAdd is a list of groups to add to the primary process when handling RUN // instructions. The magic 'keep-groups' value indicates that the process should // be allowed to inherit the current set of supplementary groups. GroupAdd []string // Platforms is the list of parsed OS/Arch/Variant triples that we want // to build the image for. If this slice has items in it, the OS and // Architecture fields above are ignored. Platforms []struct{ OS, Arch, Variant string } // AllPlatforms tells the builder to set the list of target platforms // to match the set of platforms for which all of the build's base // images are available. If this field is set, Platforms is ignored. AllPlatforms bool // UnsetEnvs is a list of environments to not add to final image. UnsetEnvs []string // UnsetLabels is a list of labels to not add to final image from base image. UnsetLabels []string // UnsetAnnotations is a list of annotations to not add to final image from base image. UnsetAnnotations []string // Envs is a list of environment variables to set in the final image. Envs []string // OSFeatures specifies operating system features the image requires. // It is typically only set when the OS is "windows". OSFeatures []string // OSVersion specifies the exact operating system version the image // requires. It is typically only set when the OS is "windows". Any // value set in a base image will be preserved, so this does not // frequently need to be set. OSVersion string // SBOMScanOptions encapsulates options which control whether or not we // run scanners on the rootfs that we're about to commit, and how. SBOMScanOptions []SBOMScanOptions // CDIConfigDir is the location of CDI configuration files, if the files in // the default configuration locations shouldn't be used. CDIConfigDir string // CachePullSourceLookupReferenceFunc is an optional LookupReferenceFunc // used to look up source references for cache pulls. CachePullSourceLookupReferenceFunc manifests.LookupReferenceFunc // CachePullDestinationLookupReferenceFunc is an optional generator // function which provides a LookupReferenceFunc used to look up // destination references for cache pulls. // // BlobDirectory will be ignored for cache pulls if this option is set. CachePullDestinationLookupReferenceFunc func(srcRef types.ImageReference) manifests.LookupReferenceFunc // CachePushSourceLookupReferenceFunc is an optional generator function // which provides a LookupReferenceFunc used to look up source // references for cache pushes. // // BlobDirectory will be ignored for cache pushes if this option is set. CachePushSourceLookupReferenceFunc func(dest types.ImageReference) manifests.LookupReferenceFunc // CachePushDestinationLookupReferenceFunc is an optional // LookupReferenceFunc used to look up destination references for cache // pushes CachePushDestinationLookupReferenceFunc manifests.LookupReferenceFunc // CompatSetParent causes the "parent" field to be set in the image's // configuration when committing in Docker format. Newer // BuildKit-based docker build doesn't set this field. CompatSetParent types.OptionalBool // CompatVolumes causes the contents of locations marked as volumes in // base images or by a VOLUME instruction to be preserved during RUN // instructions. Newer BuildKit-based docker build doesn't bother. CompatVolumes types.OptionalBool // CompatScratchConfig causes the image, if it does not have a base // image, to begin with a truly empty default configuration instead of // a minimal default configuration. Newer BuildKit-based docker build // provides a minimal initial configuration with a working directory // set in it. CompatScratchConfig types.OptionalBool // CompatLayerOmissions causes the "/dev", "/proc", and "/sys" // directories to be omitted from the image and related output. Newer // BuildKit-based builds include them in the built image by default. CompatLayerOmissions types.OptionalBool // NoPivotRoot inhibits the usage of pivot_root when setting up the rootfs NoPivotRoot bool // CreatedAnnotation controls whether or not an "org.opencontainers.image.created" // annotation is present in the output image. CreatedAnnotation types.OptionalBool // MetadataFile is the name of a file to which the builder should write a JSON map // containing metadata about the built image. MetadataFile string } ================================================ FILE: define/build_test.go ================================================ package define import ( "go.podman.io/common/libimage" "go.podman.io/common/libimage/manifests" ) // We changed a field of the latter type to the former, so make sure they're // still type aliases so that doing so doesn't break API. var _ libimage.LookupReferenceFunc = manifests.LookupReferenceFunc(nil) ================================================ FILE: define/isolation.go ================================================ package define import ( "fmt" ) type Isolation int const ( // IsolationDefault is whatever we think will work best. IsolationDefault Isolation = iota // IsolationOCI is a proper OCI runtime. IsolationOCI // IsolationChroot is a more chroot-like environment: less isolation, // but with fewer requirements. IsolationChroot // IsolationOCIRootless is a proper OCI runtime in rootless mode. IsolationOCIRootless ) // String converts a Isolation into a string. func (i Isolation) String() string { switch i { case IsolationDefault, IsolationOCI: return "oci" case IsolationChroot: return "chroot" case IsolationOCIRootless: return "rootless" } return fmt.Sprintf("unrecognized isolation type %d", i) } ================================================ FILE: define/mount_freebsd.go ================================================ //go:build freebsd package define const ( // TypeBind is the type for mounting host dir TypeBind = "nullfs" // TempDir is the default for storing temporary files TempDir = "/var/tmp" ) // Mount potions for bind var BindOptions = []string{} ================================================ FILE: define/mount_linux.go ================================================ //go:build linux package define const ( // TypeBind is the type for mounting host dir TypeBind = "bind" // TempDir is the default for storing temporary files TempDir = "/dev/shm" ) // Mount potions for bind var BindOptions = []string{"bind"} ================================================ FILE: define/mount_unsupported.go ================================================ //go:build darwin || windows || netbsd package define const ( // TypeBind is the type for mounting host dir TypeBind = "bind" // TempDir is the default for storing temporary files TempDir = "/var/tmp" ) // Mount potions for bind var BindOptions = []string{""} ================================================ FILE: define/namespace.go ================================================ package define import ( "fmt" ) // NamespaceOption controls how we set up a namespace when launching processes. type NamespaceOption struct { // Name specifies the type of namespace, typically matching one of the // ...Namespace constants defined in // github.com/opencontainers/runtime-spec/specs-go. Name string // Host is used to force our processes to use the host's namespace of // this type. Host bool // Path is the path of the namespace to attach our process to, if Host // is not set. If Host is not set and Path is also empty, a new // namespace will be created for the process that we're starting. // If Name is specs.NetworkNamespace, if Path doesn't look like an // absolute path, it is treated as a comma-separated list of network // configuration names which will be selected from among all of the // network configurations which we find. Path string } // NamespaceOptions provides some helper methods for a slice of NamespaceOption // structs. type NamespaceOptions []NamespaceOption // Find the configuration for the namespace of the given type. If there are // duplicates, find the _last_ one of the type, since we assume it was appended // more recently. func (n *NamespaceOptions) Find(namespace string) *NamespaceOption { for i := range *n { j := len(*n) - 1 - i if (*n)[j].Name == namespace { return &((*n)[j]) } } return nil } // AddOrReplace either adds or replaces the configuration for a given namespace. func (n *NamespaceOptions) AddOrReplace(options ...NamespaceOption) { nextOption: for _, option := range options { for i := range *n { j := len(*n) - 1 - i if (*n)[j].Name == option.Name { (*n)[j] = option continue nextOption } } *n = append(*n, option) } } // NetworkConfigurationPolicy takes the value NetworkDefault, NetworkDisabled, // or NetworkEnabled. type NetworkConfigurationPolicy int const ( // NetworkDefault is one of the values that BuilderOptions.ConfigureNetwork // can take, signalling that the default behavior should be used. NetworkDefault NetworkConfigurationPolicy = iota // NetworkDisabled is one of the values that BuilderOptions.ConfigureNetwork // can take, signalling that network interfaces should NOT be configured for // newly-created network namespaces. NetworkDisabled // NetworkEnabled is one of the values that BuilderOptions.ConfigureNetwork // can take, signalling that network interfaces should be configured for // newly-created network namespaces. NetworkEnabled ) // String formats a NetworkConfigurationPolicy as a string. func (p NetworkConfigurationPolicy) String() string { switch p { case NetworkDefault: return "NetworkDefault" case NetworkDisabled: return "NetworkDisabled" case NetworkEnabled: return "NetworkEnabled" } return fmt.Sprintf("unknown NetworkConfigurationPolicy %d", p) } ================================================ FILE: define/pull.go ================================================ package define import ( "fmt" ) // PullPolicy takes the value PullIfMissing, PullAlways, PullIfNewer, or PullNever. // N.B.: the enumeration values for this type differ from those used by // github.com/containers/common/pkg/config.PullPolicy (their zero values // indicate different policies), so they are not interchangeable. type PullPolicy int const ( // PullIfMissing is one of the values that BuilderOptions.PullPolicy // can take, signalling that the source image should be pulled from a // registry if a local copy of it is not already present. PullIfMissing PullPolicy = iota // PullAlways is one of the values that BuilderOptions.PullPolicy can // take, signalling that a fresh, possibly updated, copy of the image // should be pulled from a registry before the build proceeds. PullAlways // PullIfNewer is one of the values that BuilderOptions.PullPolicy // can take, signalling that the source image should only be pulled // from a registry if a local copy is not already present or if a // newer version the image is present on the repository. PullIfNewer // PullNever is one of the values that BuilderOptions.PullPolicy can // take, signalling that the source image should not be pulled from a // registry. PullNever ) // String converts a PullPolicy into a string. func (p PullPolicy) String() string { switch p { case PullIfMissing: return "missing" case PullAlways: return "always" case PullIfNewer: return "ifnewer" case PullNever: return "never" } return fmt.Sprintf("unrecognized policy %d", p) } var PolicyMap = map[string]PullPolicy{ "missing": PullIfMissing, "always": PullAlways, "never": PullNever, "ifnewer": PullIfNewer, } ================================================ FILE: define/pull_test.go ================================================ package define import ( "testing" "github.com/stretchr/testify/assert" ) func TestPullPolicy(t *testing.T) { t.Parallel() for name, val := range PolicyMap { assert.Equal(t, name, val.String()) } } ================================================ FILE: define/types.go ================================================ package define import ( "bufio" "bytes" "errors" "fmt" "io" "net/http" urlpkg "net/url" "os" "os/exec" "path" "path/filepath" "strings" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" "go.podman.io/image/v5/manifest" "go.podman.io/storage/pkg/archive" "go.podman.io/storage/pkg/chrootarchive" "go.podman.io/storage/pkg/ioutils" "go.podman.io/storage/types" ) const ( // Package is the name of this package, used in help output and to // identify working containers. Package = "buildah" // Version for the Package. Also used by .packit.sh for Packit builds. Version = "1.43.0-dev" // DefaultRuntime if containers.conf fails. DefaultRuntime = "runc" // OCIv1ImageManifest is the MIME type of an OCIv1 image manifest, // suitable for specifying as a value of the PreferredManifestType // member of a CommitOptions structure. It is also the default. OCIv1ImageManifest = v1.MediaTypeImageManifest // Dockerv2ImageManifest is the MIME type of a Docker v2s2 image // manifest, suitable for specifying as a value of the // PreferredManifestType member of a CommitOptions structure. Dockerv2ImageManifest = manifest.DockerV2Schema2MediaType // OCI used to define the "oci" image format OCI = "oci" // DOCKER used to define the "docker" image format DOCKER = "docker" // SEV is a known trusted execution environment type: AMD-SEV (secure encrypted virtualization using encrypted state, requires epyc 1000 "naples") SEV TeeType = "sev" // SNP is a known trusted execution environment type: AMD-SNP (SEV secure nested pages) (requires epyc 3000 "milan") SNP TeeType = "snp" ) // DefaultRlimitValue is the value set by default for nofile and nproc const RLimitDefaultValue = uint64(1048576) // TeeType is a supported trusted execution environment type. type TeeType string var ( // Deprecated: DefaultCapabilities values should be retrieved from // github.com/containers/common/pkg/config DefaultCapabilities = []string{ "CAP_AUDIT_WRITE", "CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_FSETID", "CAP_KILL", "CAP_MKNOD", "CAP_NET_BIND_SERVICE", "CAP_SETFCAP", "CAP_SETGID", "CAP_SETPCAP", "CAP_SETUID", "CAP_SYS_CHROOT", } // Deprecated: DefaultNetworkSysctl values should be retrieved from // github.com/containers/common/pkg/config DefaultNetworkSysctl = map[string]string{ "net.ipv4.ping_group_range": "0 0", } Gzip = archive.Gzip Bzip2 = archive.Bzip2 Xz = archive.Xz Zstd = archive.Zstd Uncompressed = archive.Uncompressed ) // IDMappingOptions controls how we set up UID/GID mapping when we set up a // user namespace. type IDMappingOptions struct { HostUIDMapping bool HostGIDMapping bool UIDMap []specs.LinuxIDMapping GIDMap []specs.LinuxIDMapping AutoUserNs bool AutoUserNsOpts types.AutoUserNsOptions } // Secret is a secret source that can be used in a RUN type Secret struct { ID string Source string SourceType string } func (s Secret) ResolveValue() ([]byte, error) { switch s.SourceType { case "env": return []byte(os.Getenv(s.Source)), nil case "file": rv, err := os.ReadFile(s.Source) if err != nil { return nil, fmt.Errorf("reading file for secret ID %s: %w", s.ID, err) } return rv, nil default: return nil, fmt.Errorf("invalid secret type: %s for secret ID: %s", s.SourceType, s.ID) } } // BuildOutputOptions contains the the outcome of parsing the value of a build --output flag // Deprecated: This structure is now internal type BuildOutputOption struct { Path string // Only valid if !IsStdout IsDir bool IsStdout bool } // ConfidentialWorkloadOptions encapsulates options which control whether or not // we output an image whose rootfs contains a LUKS-compatibly-encrypted disk image // instead of the usual rootfs contents. type ConfidentialWorkloadOptions struct { Convert bool AttestationURL string CPUs int Memory int TempDir string // used for the temporary plaintext copy of the disk image TeeType TeeType IgnoreAttestationErrors bool WorkloadID string DiskEncryptionPassphrase string Slop string FirmwareLibrary string } // SBOMMergeStrategy tells us how to merge multiple SBOM documents into one. type SBOMMergeStrategy string const ( // SBOMMergeStrategyCat literally concatenates the documents. SBOMMergeStrategyCat SBOMMergeStrategy = "cat" // SBOMMergeStrategyCycloneDXByComponentNameAndVersion adds components // from the second document to the first, so long as they have a // name+version combination which is not already present in the // components array. SBOMMergeStrategyCycloneDXByComponentNameAndVersion SBOMMergeStrategy = "merge-cyclonedx-by-component-name-and-version" // SBOMMergeStrategySPDXByPackageNameAndVersionInfo adds packages from // the second document to the first, so long as they have a // name+versionInfo combination which is not already present in the // first document's packages array, and adds hasExtractedLicensingInfos // items from the second document to the first, so long as they include // a licenseId value which is not already present in the first // document's hasExtractedLicensingInfos array. SBOMMergeStrategySPDXByPackageNameAndVersionInfo SBOMMergeStrategy = "merge-spdx-by-package-name-and-versioninfo" ) // SBOMScanOptions encapsulates options which control whether or not we run a // scanner on the rootfs that we're about to commit, and how. type SBOMScanOptions struct { Type []string // a shorthand name for a defined group of these options Image string // the scanner image to use PullPolicy PullPolicy // how to get the scanner image Commands []string // one or more commands to invoke for the image rootfs or ContextDir locations ContextDir []string // one or more "source" directory locations SBOMOutput string // where to save SBOM scanner output outside of the image (i.e., the local filesystem) PURLOutput string // where to save PURL list outside of the image (i.e., the local filesystem) ImageSBOMOutput string // where to save SBOM scanner output in the image ImagePURLOutput string // where to save PURL list in the image MergeStrategy SBOMMergeStrategy // how to merge the outputs of multiple scans } // TempDirForURL checks if the passed-in string looks like a URL or "-". If it // is, TempDirForURL creates a temporary directory, arranges for its contents // to be the contents of that URL, and returns the temporary directory's path, // along with the relative name of a subdirectory which should be used as the // build context (which may be empty or "."). Removal of the temporary // directory is the responsibility of the caller. If the string doesn't look // like a URL or "-", TempDirForURL returns empty strings and a nil error code. func TempDirForURL(dir, prefix, url string) (name string, subdir string, err error) { if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") && !strings.HasPrefix(url, "git://") && !strings.HasPrefix(url, "github.com/") && url != "-" { return "", "", nil } name, err = os.MkdirTemp(dir, prefix) if err != nil { return "", "", fmt.Errorf("creating temporary directory for %q: %w", url, err) } downloadDir := filepath.Join(name, "download") if err = os.MkdirAll(downloadDir, 0o700); err != nil { return "", "", fmt.Errorf("creating directory %q for %q: %w", downloadDir, url, err) } urlParsed, err := urlpkg.Parse(url) if err != nil { return "", "", fmt.Errorf("parsing url %q: %w", url, err) } if strings.HasPrefix(url, "git://") || strings.HasSuffix(urlParsed.Path, ".git") { combinedOutput, gitSubDir, err := cloneToDirectory(url, downloadDir) if err != nil { if err2 := os.RemoveAll(name); err2 != nil { logrus.Debugf("error removing temporary directory %q: %v", name, err2) } return "", "", fmt.Errorf("cloning %q to %q:\n%s: %w", url, name, string(combinedOutput), err) } logrus.Debugf("Build context is at %q", filepath.Join(downloadDir, gitSubDir)) return name, filepath.Join(filepath.Base(downloadDir), gitSubDir), nil } if strings.HasPrefix(url, "github.com/") { ghurl := url url = fmt.Sprintf("https://%s/archive/master.tar.gz", ghurl) logrus.Debugf("resolving url %q to %q", ghurl, url) subdir = path.Base(ghurl) + "-master" } if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") { err = downloadToDirectory(url, downloadDir) if err != nil { if err2 := os.RemoveAll(name); err2 != nil { logrus.Debugf("error removing temporary directory %q: %v", name, err2) } return "", "", err } logrus.Debugf("Build context is at %q", filepath.Join(downloadDir, subdir)) return name, filepath.Join(filepath.Base(downloadDir), subdir), nil } if url == "-" { err = stdinToDirectory(downloadDir) if err != nil { if err2 := os.RemoveAll(name); err2 != nil { logrus.Debugf("error removing temporary directory %q: %v", name, err2) } return "", "", err } logrus.Debugf("Build context is at %q", filepath.Join(downloadDir, subdir)) return name, filepath.Join(filepath.Base(downloadDir), subdir), nil } logrus.Debugf("don't know how to retrieve %q", url) if err2 := os.RemoveAll(name); err2 != nil { logrus.Debugf("error removing temporary directory %q: %v", name, err2) } return "", "", errors.New("unreachable code reached") } // parseGitBuildContext parses git build context to `repo`, `sub-dir` // `branch/commit`, accepts GitBuildContext in the format of // `repourl.git[#[branch-or-commit]:subdir]`. func parseGitBuildContext(url string) (string, string, string) { gitSubdir := "" gitBranch := "" gitBranchPart := strings.Split(url, "#") if len(gitBranchPart) > 1 { // check if string contains path to a subdir gitSubDirPart := strings.Split(gitBranchPart[1], ":") if len(gitSubDirPart) > 1 { gitSubdir = gitSubDirPart[1] } gitBranch = gitSubDirPart[0] } return gitBranchPart[0], gitSubdir, gitBranch } func cloneToDirectory(url, dir string) ([]byte, string, error) { var cmd *exec.Cmd gitRepo, gitSubdir, gitRef := parseGitBuildContext(url) // init repo cmd = exec.Command("git", "init", dir) combinedOutput, err := cmd.CombinedOutput() if err != nil { // Return err.Error() instead of err as we want buildah to override error code with more predictable // value. return combinedOutput, gitSubdir, fmt.Errorf("failed while performing `git init`: %s", err.Error()) } // add origin cmd = exec.Command("git", "remote", "add", "origin", gitRepo) cmd.Dir = dir combinedOutput, err = cmd.CombinedOutput() if err != nil { // Return err.Error() instead of err as we want buildah to override error code with more predictable // value. return combinedOutput, gitSubdir, fmt.Errorf("failed while performing `git remote add`: %s", err.Error()) } logrus.Debugf("fetching repo %q and branch (or commit ID) %q to %q", gitRepo, gitRef, dir) args := []string{"fetch", "-u", "--depth=1", "origin", "--", gitRef} cmd = exec.Command("git", args...) cmd.Dir = dir combinedOutput, err = cmd.CombinedOutput() if err != nil { // Return err.Error() instead of err as we want buildah to override error code with more predictable // value. return combinedOutput, gitSubdir, fmt.Errorf("failed while performing `git fetch`: %s", err.Error()) } cmd = exec.Command("git", "checkout", "FETCH_HEAD") cmd.Dir = dir combinedOutput, err = cmd.CombinedOutput() if err != nil { // Return err.Error() instead of err as we want buildah to override error code with more predictable // value. return combinedOutput, gitSubdir, fmt.Errorf("failed while performing `git checkout`: %s", err.Error()) } return combinedOutput, gitSubdir, nil } func downloadToDirectory(url, dir string) error { logrus.Debugf("extracting %q to %q", url, dir) resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest { return fmt.Errorf("invalid response status %d", resp.StatusCode) } if resp.ContentLength == 0 { return fmt.Errorf("no contents in %q", url) } if err := chrootarchive.Untar(resp.Body, dir, nil); err != nil { resp1, err := http.Get(url) if err != nil { return err } defer resp1.Body.Close() body, err := io.ReadAll(resp1.Body) if err != nil { return err } dockerfile := filepath.Join(dir, "Dockerfile") // Assume this is a Dockerfile if err := ioutils.AtomicWriteFile(dockerfile, body, 0o600); err != nil { return fmt.Errorf("failed to write %q to %q: %w", url, dockerfile, err) } } return nil } func stdinToDirectory(dir string) error { logrus.Debugf("extracting stdin to %q", dir) r := bufio.NewReader(os.Stdin) b, err := io.ReadAll(r) if err != nil { return fmt.Errorf("failed to read from stdin: %w", err) } reader := bytes.NewReader(b) if err := chrootarchive.Untar(reader, dir, nil); err != nil { dockerfile := filepath.Join(dir, "Dockerfile") // Assume this is a Dockerfile if err := ioutils.AtomicWriteFile(dockerfile, b, 0o600); err != nil { return fmt.Errorf("failed to write bytes to %q: %w", dockerfile, err) } } return nil } ================================================ FILE: define/types_test.go ================================================ package define import ( "testing" "github.com/stretchr/testify/assert" ) func TestParseGitBuildContext(t *testing.T) { t.Parallel() // Tests with only repo repo, subdir, branch := parseGitBuildContext("https://github.com/containers/repo.git") assert.Equal(t, repo, "https://github.com/containers/repo.git") assert.Equal(t, subdir, "") assert.Equal(t, branch, "") // Tests url with branch repo, subdir, branch = parseGitBuildContext("https://github.com/containers/repo.git#main") assert.Equal(t, repo, "https://github.com/containers/repo.git") assert.Equal(t, subdir, "") assert.Equal(t, branch, "main") // Tests url with no branch and subdir repo, subdir, branch = parseGitBuildContext("https://github.com/containers/repo.git#:mydir") assert.Equal(t, repo, "https://github.com/containers/repo.git") assert.Equal(t, subdir, "mydir") assert.Equal(t, branch, "") // Tests url with branch and subdir repo, subdir, branch = parseGitBuildContext("https://github.com/containers/repo.git#main:mydir") assert.Equal(t, repo, "https://github.com/containers/repo.git") assert.Equal(t, subdir, "mydir") assert.Equal(t, branch, "main") } ================================================ FILE: define/types_unix.go ================================================ //go:build darwin || linux package define import ( "github.com/opencontainers/runc/libcontainer/devices" ) // BuildahDevice is a wrapper around devices.Device // with additional support for renaming a device // using bind-mount in rootless environments. type BuildahDevice struct { devices.Device Source string Destination string } type ContainerDevices = []BuildahDevice ================================================ FILE: define/types_unsupported.go ================================================ //go:build !linux && !darwin package define // ContainerDevices is currently not implemented. type ContainerDevices = []struct{} ================================================ FILE: delete.go ================================================ package buildah import "fmt" // Delete removes the working container. The buildah.Builder object should not // be used after this method is called. func (b *Builder) Delete() error { if err := b.store.DeleteContainer(b.ContainerID); err != nil { return fmt.Errorf("deleting build container %q: %w", b.ContainerID, err) } b.MountPoint = "" b.Container = "" b.ContainerID = "" return nil } ================================================ FILE: demos/README.md ================================================ ![buildah logo](https://cdn.rawgit.com/containers/buildah/main/logos/buildah-logo_large.png) # Buildah Demos The purpose of these demonstrations is twofold: 1. To help automate some of the tutorial material so that Buildah newcomers can walk through some of the concepts. 2. For Buildah enthusiasts and practitioners to use for demos at educational presentations - college classes, Meetups etc. It is assumed that you have installed Buildah and Podman on your machine. $ sudo yum -y install podman buildah For the Docker compatibility demo you will also need to install Docker. $ sudo yum -y install docker Replace `yum` with `dnf` if required. ## Building from scratch demo filename: [`buildah-scratch-demo.sh`](buildah-scratch-demo.sh) This demo builds a container image from scratch. The container is going to inject a bash shell script and therefore requires the installation of coreutils and bash. Please make sure you have installed Buildah and Podman. Also this demo uses Quay.io to push the image to that registry when it is completed. If you are not logged in then it will fail at that step and finish. If you wish to login to Quay.io before running the demo, then it will push to your repository successfully. ```bash # Rootful session $ sudo buildah login quay.io # # or # # Rootless session $ buildah login quay.io ``` There are several variables you will want to set that are listed at the top of the script. The name for the container image, your Quay.io username, your name, and the Fedora release number: demoimg=myshdemo quayuser=UserNameHere myname=YourNameHere distrorelease=30 pkgmgr=dnf # switch to yum if using yum ## Buildah and Docker compatibility demo filename: [`docker-compatibility-demo.sh`](docker-compatibility-demo.sh) This demo builds an nginx container image using Buildah. It modifies the homepage and commits the image. The container is tested using `podman run` and then stopped. The Docker daemon is then started and the image is pushed to the Docker repository. The container is started using `docker run` and tested. There are several variables you will want to set that are listed at the top of the script. The name for the container image, your Quay.io username, your name, and the Fedora release number: demoimg=dockercompatibilitydemo quayuser=UsernameHere myname=YourNameHere distro=fedora distrorelease=30 pkgmgr=dnf # switch to yum if using yum ## Buildah build using Docker demo filename: [`docker-bud-demo.sh`](buildah-bud-demo.sh) This demo builds an nginx container image using Buildah with. Buildah's `buildah-using-docker`, or `bud` option, provides a mechanism for using existing Dockerfiles to build the container image. This image is the same as the image in the Docker compatibility demo (at time of creating this README). The container is tested using `podman run` and then stopped. The Docker daemon is then started and the image is pushed to the Docker repository. The container is started using `docker run` and tested. There are several variables you will want to set that are listed at the top of the script. The name for the container image, your Quay.io username, your name, and the Fedora release number: demoimg=buildahbuddemo quayuser=UsernameHere myname=YourNameHere distro=fedora distrorelease=30 pkgmgr=dnf # switch to yum if using yum ================================================ FILE: demos/buildah-bud-demo.sh ================================================ #!/usr/bin/env bash # buildah-bud-demo.sh # author : ipbabble # Assumptions install buildah, podman & docker # Do NOT start the docker daemon # Set some of the variables below demoimg=buildahbuddemo quayuser=ipbabble myname="William Henry" distro=fedora distrorelease=30 pkgmgr=dnf # switch to yum if using yum #Setting up some colors for helping read the demo output red=$(tput setaf 1) green=$(tput setaf 2) yellow=$(tput setaf 3) blue=$(tput setaf 4) cyan=$(tput setaf 6) reset=$(tput sgr0) echo -e "Using ${green}GREEN${reset} to introduce Buildah steps" echo -e "Using ${yellow}YELLOW${reset} to introduce code" echo -e "Using ${blue}BLUE${reset} to introduce Podman steps" echo -e "Using ${cyan}CYAN${reset} to introduce bash commands" echo -e "Using ${red}RED${reset} to introduce Docker commands" echo -e "Building an image called ${demoimg}" read -p "${green}Start of the script${reset}" set -x DOCKERFILE=./Dockerfile /bin/cat <$DOCKERFILE FROM docker://docker.io/fedora:latest MAINTAINER ${myname} RUN dnf -y update; dnf -y clean all RUN dnf -y install nginx --setopt install_weak_deps=false; dnf -y clean all RUN echo "daemon off;" >> /etc/nginx/nginx.conf RUN echo "nginx on Fedora" > /usr/share/nginx/html/index.html EXPOSE 80 CMD [ "/usr/sbin/nginx" ] EOM read -p "${cyan}Display the Dockerfile:${reset}" cat $DOCKERFILE read -p "${green}Create a new container image from Dockerfile${reset}" buildah bud -t $demoimg . read -p "${green}List the images we have.${reset}" buildah images read -p "${green}Inspect the container image meta data${yellow}" buildah inspect --type image $demoimg read -p "${blue}Run the container using Podman.${reset}" containernum=$(podman run -d -p 80:80 $demoimg) read -p "${cyan}Check that nginx is up and running with our new page${reset}" curl localhost read -p "${blue}Stop the container and rm it${reset}" podman ps podman stop $containernum podman rm $containernum read -p "${cyan}Check that nginx is down${reset}" curl localhost read -p "${cyan}Start the Docker daemon. Using restart in case it is already started${reset}" systemctl restart docker read -p "${red}List the Docker images in the repository - should be empty${reset}" docker images read -p "${blue}Push the image to the local Docker repository using docker-daemon${reset}" podman push $demoimg docker-daemon:$quayuser/${demoimg}:latest read -p "${red}List the Docker images in the repository${reset}" docker images read -p "${red}Start the container from the new Docker repo image${reset}" dockercontainer=$(docker run -d -p 80:80 $quayuser/$demoimg) read -p "${cyan}Check that nginx is up and running with our new page${reset}" curl localhost read -p "${red}Stop the container and remove it and the image${reset}" docker stop $dockercontainer docker rm $dockercontainer docker rmi $demoimg read -p "${cyan}Stop Docker${reset}" systemctl stop docker echo -e "${red}We are done!${reset}" ================================================ FILE: demos/buildah-scratch-demo.sh ================================================ #!/usr/bin/env bash # author : ipbabble # Assumptions install buildah and podman # login to Quay.io using buildah if you want to see the image push # otherwise it will just fail the last step and no biggy. # buildah login quay.io # Set some of the variables below ################# # is_rootless # Check if we run as normal user ################# function is_rootless() { [ "$(id -u)" -ne 0 ] } ## Steps in this demo use pkg-managers like dnf and yum which ## must be invoked as root. Similarly `buildah mount` only work ## as root. The `buildah unshare` command switches your user ## session to root within the user namespace. if is_rootless; then exec buildah unshare $0 fi demoimg=myshdemo quayuser=ipbabble myname=WilliamHenry distrorelease=42 pkgmgr=dnf # switch to yum if using yum #Setting up some colors for helping read the demo output bold=$(tput bold) red=$(tput setaf 1) green=$(tput setaf 2) yellow=$(tput setaf 3) blue=$(tput setaf 4) cyan=$(tput setaf 6) reset=$(tput sgr0) echo -e "Using ${green}GREEN${reset} to introduce Buildah steps" echo -e "Using ${yellow}YELLOW${reset} to introduce code" echo -e "Using ${blue}BLUE${reset} to introduce Podman steps" echo -e "Using ${cyan}CYAN${reset} to introduce bash commands" echo -e "Building an image called ${demoimg}" read -p "${green}Start of the script${reset}" set -x read -p "${green}Create a new container on disk from scratch${reset}" newcontainer=$(buildah from scratch) read -p "${green}Mount the root directory of the new scratch container${reset}" scratchmnt=$(buildah mount $newcontainer) read -p "${cyan}Lets see what is in scratchmnt${reset}" ls $scratchmnt echo -e "${red}Note that the root of the scratch container is EMPTY!${reset}" read -p "${cyan}Time to install some basic bash capabilities: coreutils and bash packages${reset}" if [ "$pkgmgr" == "dnf" ]; then $pkgmgr install --installroot $scratchmnt --releasever ${distrorelease} bash coreutils --use-host-config --setopt "*.countme=false" --setopt install_weak_deps=false -y elif [ "$pkgmgr" == "yum" ]; then $pkgmgr install --installroot $scratchmnt --releasever ${distrorelease} bash coreutils ---use-host-config --setopt "*.countme=false" y else echo -e "${red}[Error] Unknown package manager ${pkgmgr}${reset}" fi read -p "${cyan}Clean up the packages${reset}" $pkgmgr clean --installroot $scratchmnt all read -p "${green}Run the shell and see what is inside. When your done, type ${red}exit${green} and return.${reset}" buildah run $newcontainer bash read -p "${cyan}Let's look at the program${yellow}" FILE=./runecho.sh /bin/cat <$FILE #!/usr/bin/env bash for i in {1..9}; do echo "This is a new cloud native container using Buildah [" \$i "]" done EOM chmod +x $FILE cat $FILE read -p "${green}Copy program into the container and run ls to see it is there${reset}" buildah copy $newcontainer $FILE /usr/bin ls -al $scratchmnt/usr/bin/*.sh read -p "${green}Run the container using Buildah${reset}" buildah run $newcontainer /usr/bin/runecho.sh read -p "${green}Make the container run the program by default when container is run${reset}" buildah config --entrypoint /usr/bin/runecho.sh $newcontainer read -p "${green}Set some config information for the container image${reset}" buildah config --author "${myname}" --created-by "${quayuser}" --label name=${demoimg} $newcontainer read -p "${green}Inspect the meta data${yellow}" buildah inspect $newcontainer read -p "${green}Unmount the container and commit to an image called ${demoimg}.${reset}" buildah unmount $newcontainer buildah commit $newcontainer $demoimg read -p "${green}List the images we have.${reset}" buildah images read -p "${blue}Run the container using Podman.${reset}" podman run -t $demoimg read -p "${green}Make sure you are already logged into your account on Quay.io. Or use Quay creds.${reset}" buildah push $demoimg docker://quay.io/$quayuser/$demoimg echo -e "${red}We are done!${reset}" ================================================ FILE: demos/buildah_multi_stage.sh ================================================ #!/usr/bin/env bash # author : tsweeney (based on ipbabble's other demos) # Based on Alex Ellis blog (https://blog.alexellis.io/mutli-stage-docker-builds - note multi is misspelled) # Assumptions install buildah and podman # Set some of the variables below ################# # is_rootless # Check if we run as normal user ################# function is_rootless() { [ "$(id -u)" -ne 0 ] } ## The `buildah mount` only work as root so use ## `buildah unshare` command which switches your ## user session to root within the user namespace. if is_rootless; then buildah unshare $0 exit fi demoimg=mymultidemo quayuser=myquayuser myname=MyName distrorelease=30 pkgmgr=dnf # switch to yum if using yum #Setting up some colors for helping read the demo output bold=$(tput bold) red=$(tput setaf 1) green=$(tput setaf 2) yellow=$(tput setaf 3) blue=$(tput setaf 4) cyan=$(tput setaf 6) reset=$(tput sgr0) echo -e "Using ${green}GREEN${reset} to introduce Buildah steps" echo -e "Using ${yellow}YELLOW${reset} to introduce code" echo -e "Using ${blue}BLUE${reset} to introduce Podman steps" echo -e "Using ${cyan}CYAN${reset} to introduce bash commands" echo -e "Building an image called ${demoimg}" read -p "${green}Start of the script${reset}" set -x read -p "${yellow}Create Dockerfile.multi${reset}" FILE=./Dockerfile.multi /bin/cat <$FILE FROM golang:1.7.3 as builder WORKDIR /go/src/github.com/alexellis/href-counter/ RUN go get -d -v golang.org/x/net/html COPY app.go . RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY --from=builder /go/src/github.com/alexellis/href-counter/app . CMD ["./app"] EOM chmod +x $FILE read -p "${yellow}Let's look at our Dockerfile.multi${reset}" cat ./Dockerfile.multi read -p "${yellow}Pull app.go from GitHub${reset}" curl https://raw.githubusercontent.com/alexellis/href-counter/master/app.go > app.go read -p "${green}Create a new image on disk from Dockerfile.multi${reset}" newcontainer=$(buildah bud -t multifromfile:latest -f ./Dockerfile.multi .) read -p "${blue}Run the multifromfile container${reset}" podman run --network=host -e url=https://www.alexellis.io/ multifromfile:latest podman run --network=host -e url=https://www.alexellis.io/ multifromfile:latest read -p "${green}Let's check the size of the images${reset}" buildah images read -p "${green}Let's clear out our containers${reset}" buildah rm -a read -p "${green}Let's build the container with Buildah, first GoLang${reset}" buildcntr=$(buildah from golang:1.7.3) read -p "${green}Let's mount the container getting the root directory${reset}" buildmnt=$(buildah mount $buildcntr) read -p "${green}Let's get x/net/html into the container${reset}" buildah run $buildcntr go get -d -v golang.org/x/net/html read -p "${yellow}Copy app.go into the container${reset}" cp app.go $buildmnt/go read -p "${green}Build app.go inside the container${reset}" buildah run $buildcntr /bin/sh -c "CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app ." read -p "${green}Build new image to run application in production${reset}" rtcntr=$(buildah from alpine:latest) read -p "${green}Mount the new images root fs${reset}" rtmnt=$(buildah mount $rtcntr) read -p "${green}Install required packages${reset}" buildah run $rtcntr apk --no-cache add ca-certificates read -p "${yellow}Copy the app from the previous container${reset}" cp $buildmnt/go/app $rtmnt read -p "${yellow}Set the CMD for the container${reset}" buildah config --cmd ./app $rtcntr read -p "${yellow}Unmount and commit the rtimg${reset}" buildah unmount $rtcntr buildah commit $rtcntr multifrombuildah:latest read -p "${blue}Run the multifrombuildah container${reset}" podman run --network=host -e url=https://www.alexellis.io/ multifrombuildah:latest podman run --network=host -e url=https://www.alexellis.io/ multifrombuildah:latest read -p "${green}Let's check the size of the images${reset}" buildah images read -p "${green}Let's clear out our containers${reset}" buildah rm -a read -p "${green}Let's clear out our images${reset}" buildah rmi -a -f read -p "${green}Let's remove app.go and Dockerfile.multi${reset}" rm ./app.go ./Dockerfile.multi echo -e "${red}We are done!${reset}" ================================================ FILE: demos/docker-compatibility-demo.sh ================================================ #!/usr/bin/env bash # docker-compatibility-demo.sh # author : ipbabble # Assumptions install buildah, podman & docker # Do NOT start the docker daemon # Set some of the variables below demoimg=dockercompatibilitydemo quayuser=ipbabble myname="William Henry" distro=fedora distrorelease=30 pkgmgr=dnf # switch to yum if using yum #Setting up some colors for helping read the demo output bold=$(tput bold) red=$(tput setaf 1) green=$(tput setaf 2) yellow=$(tput setaf 3) blue=$(tput setaf 4) cyan=$(tput setaf 6) reset=$(tput sgr0) echo -e "Using ${green}GREEN${reset} to introduce Buildah steps" echo -e "Using ${yellow}YELLOW${reset} to introduce code" echo -e "Using ${blue}BLUE${reset} to introduce Podman steps" echo -e "Using ${cyan}CYAN${reset} to introduce bash commands" echo -e "Using ${red}RED${reset} to introduce Docker commands" echo -e "Building an image called ${demoimg}" read -p "${green}Start of the script${reset}" set -x read -p "${green}Create a new container on disk from ${distro}${reset}" newcontainer=$(buildah from ${distro}) read -p "${green}Update packages and clean all ${reset}" buildah run $newcontainer -- ${pkgmgr} -y update && ${pkgmgr} -y clean all read -p "${green}Install nginx${reset}" buildah run $newcontainer -- ${pkgmgr} -y install nginx && ${pkgmgr} -y clean all read -p "${green}Make some nginx config and home page changes ${reset}" buildah run $newcontainer bash -c 'echo "daemon off;" >> /etc/nginx/nginx.conf' buildah run $newcontainer bash -c 'echo "nginx on OCI Fedora image, built using Buildah" > /usr/share/nginx/html/index.html' read -p "${green}Use buildah config to expose the port and set the entrypoint${reset}" buildah config --port 80 --entrypoint /usr/sbin/nginx $newcontainer read -p "${green}Set other meta data using buildah config${reset}" buildah config --created-by "${quayuser}" $newcontainer buildah config --author "${myname}" --label name=$demoimg $newcontainer read -p "${green}Inspect the container image meta data${yellow}" buildah inspect $newcontainer read -p "${green}Commit the container to an OCI image called ${demoimg}.${reset}" buildah commit $newcontainer $demoimg read -p "${green}List the images we have.${reset}" buildah images read -p "${blue}Run the container using Podman.${reset}" containernum=$(podman run -d -p 80:80 $demoimg) read -p "${cyan}Check that nginx is up and running with our new page${reset}" curl localhost read -p "${blue}Stop the container and rm it${reset}" podman ps podman stop $containernum podman rm $containernum read -p "${cyan}Check that nginx is down${reset}" curl localhost read -p "${cyan}Start the Docker daemon. Using restart in case it is already started${reset}" systemctl restart docker read -p "${red}List the Docker images in the repository - should be empty${reset}" docker images read -p "${blue}Push the image to the local Docker repository using docker-daemon${reset}" podman push $demoimg docker-daemon:$quayuser/dockercompatibilitydemo:latest read -p "${red}List the Docker images in the repository${reset}" docker images read -p "${red}Start the container from the new Docker repo image${reset}" dockercontainer=$(docker run -d -p 80:80 $quayuser/$demoimg) read -p "${cyan}Check that nginx is up and running with our new page${reset}" curl localhost read -p "${red}Stop the container and rm it${reset}" docker stop $dockercontainer docker rm $dockercontainer docker rmi $demoimg read -p "${cyan}Stop Docker${reset}" systemctl stop docker echo -e "${red}We are done!${reset}" ================================================ FILE: developmentplan.md ================================================ ![buildah logo](https://cdn.rawgit.com/containers/buildah/main/logos/buildah-logo_large.png) # Development Plan ## Development goals for Buildah * Integration into Kubernetes and potentially other tools. The biggest requirement for this is to be able run Buildah within a standard linux container without SYS_ADMIN privileges. This would allow Buildah to run non-privileged containers inside of Kubernetes, so you could distribute your container workloads. * Integration with User Namespace, Podman has this already and the goal is to get `buildah build` and `buildah run` to be able to run its containers in a usernamespace to give the builder better security isolation from the host. * Buildah `buildah build` command's goal is to have feature parity with other OCI image and container build systems. * Addressing issues from the community as reported in the [Issues](https://github.com/containers/buildah/issues) page. ================================================ FILE: digester.go ================================================ package buildah import ( "archive/tar" "errors" "fmt" "hash" "io" "sync" "time" digest "github.com/opencontainers/go-digest" ) type digester interface { io.WriteCloser ContentType() string Digest() digest.Digest } // A simple digester just digests its content as-is. type simpleDigester struct { digester digest.Digester hasher hash.Hash contentType string } func newSimpleDigester(contentType string) digester { finalDigester := digest.Canonical.Digester() return &simpleDigester{ digester: finalDigester, hasher: finalDigester.Hash(), contentType: contentType, } } func (s *simpleDigester) ContentType() string { return s.contentType } func (s *simpleDigester) Write(p []byte) (int, error) { return s.hasher.Write(p) } func (s *simpleDigester) Close() error { return nil } func (s *simpleDigester) Digest() digest.Digest { return s.digester.Digest() } // A tarFilterer passes a tarball through to an io.WriteCloser, potentially // modifying headers as it goes. type tarFilterer struct { wg sync.WaitGroup pipeWriter *io.PipeWriter closedLock sync.Mutex closed bool err error } func (t *tarFilterer) Write(p []byte) (int, error) { n, err := t.pipeWriter.Write(p) if err != nil { t.closedLock.Lock() closed := t.closed t.closedLock.Unlock() err = fmt.Errorf("writing to tar filter pipe (closed=%v,err=%v): %w", closed, t.err, err) } return n, err } func (t *tarFilterer) Close() error { t.closedLock.Lock() if t.closed { t.closedLock.Unlock() return errors.New("tar filter is already closed") } t.closed = true t.closedLock.Unlock() err := t.pipeWriter.Close() t.wg.Wait() if err != nil { return fmt.Errorf("closing filter pipe: %w", err) } return t.err } // newTarFilterer passes one or more tar archives through to an io.WriteCloser // as a single archive, potentially calling filter to modify headers and // contents as it goes. // // Note: if "filter" indicates that a given item should be skipped, there is no // guarantee that there will not be a subsequent item of type TypeLink, which // is a hard link, which points to the skipped item as the link target. func newTarFilterer(writeCloser io.WriteCloser, filter func(hdr *tar.Header) (skip, replaceContents bool, replacementContents io.Reader)) io.WriteCloser { pipeReader, pipeWriter := io.Pipe() tarWriter := tar.NewWriter(writeCloser) filterer := &tarFilterer{ pipeWriter: pipeWriter, } filterer.wg.Add(1) go func() { filterer.closedLock.Lock() closed := filterer.closed filterer.closedLock.Unlock() for !closed { tarReader := tar.NewReader(pipeReader) hdr, err := tarReader.Next() for err == nil { var skip, replaceContents bool var replacementContents io.Reader if filter != nil { skip, replaceContents, replacementContents = filter(hdr) } if !skip { if err = tarWriter.WriteHeader(hdr); err != nil { err = fmt.Errorf("writing tar header for %q: %w", hdr.Name, err) break } if hdr.Size != 0 { var n int64 var copyErr error if replaceContents { n, copyErr = io.CopyN(tarWriter, replacementContents, hdr.Size) } else { n, copyErr = io.Copy(tarWriter, tarReader) } if copyErr != nil { err = fmt.Errorf("copying content for %q: %w", hdr.Name, copyErr) break } if n != hdr.Size { err = fmt.Errorf("filtering content for %q: expected %d bytes, got %d bytes", hdr.Name, hdr.Size, n) break } } if err = tarWriter.Flush(); err != nil { err = fmt.Errorf("flushing tar item padding for %q: %w", hdr.Name, err) break } } hdr, err = tarReader.Next() } if !errors.Is(err, io.EOF) { filterer.err = fmt.Errorf("reading tar archive: %w", err) break } filterer.closedLock.Lock() closed = filterer.closed filterer.closedLock.Unlock() } err1 := tarWriter.Close() err := writeCloser.Close() if err == nil { err = err1 } if err != nil { pipeReader.CloseWithError(err) } else { pipeReader.Close() } filterer.wg.Done() }() return filterer } // A tar digester digests an archive, modifying the headers it digests by // calling a specified function to potentially modify the header that it's // about to write. type tarDigester struct { isOpen bool nested digester tarFilterer io.WriteCloser } func modifyTarHeaderForDigesting(hdr *tar.Header) (skip, replaceContents bool, replacementContents io.Reader) { zeroTime := time.Time{} hdr.ModTime = zeroTime hdr.AccessTime = zeroTime hdr.ChangeTime = zeroTime return false, false, nil } func newTarDigester(contentType string) digester { nested := newSimpleDigester(contentType) digester := &tarDigester{ isOpen: true, nested: nested, tarFilterer: newTarFilterer(nested, modifyTarHeaderForDigesting), } return digester } func (t *tarDigester) ContentType() string { return t.nested.ContentType() } func (t *tarDigester) Digest() digest.Digest { return t.nested.Digest() } func (t *tarDigester) Write(p []byte) (int, error) { return t.tarFilterer.Write(p) } func (t *tarDigester) Close() error { if t.isOpen { t.isOpen = false return t.tarFilterer.Close() } return nil } // CompositeDigester can compute a digest over multiple items. type CompositeDigester struct { digesters []digester closer io.Closer } // closeOpenDigester closes an open sub-digester, if we have one. func (c *CompositeDigester) closeOpenDigester() { if c.closer != nil { c.closer.Close() c.closer = nil } } // Restart clears all state, so that the composite digester can start over. func (c *CompositeDigester) Restart() { c.closeOpenDigester() c.digesters = nil } // Start starts recording the digest for a new item ("", "file", or "dir"). // The caller should call Hash() immediately after to retrieve the new // io.WriteCloser. func (c *CompositeDigester) Start(contentType string) { c.closeOpenDigester() switch contentType { case "": c.digesters = append(c.digesters, newSimpleDigester("")) case "file", "dir": digester := newTarDigester(contentType) c.closer = digester c.digesters = append(c.digesters, digester) default: panic(fmt.Sprintf(`unrecognized content type: expected "", "file", or "dir", got %q`, contentType)) } } // Hash returns the hasher for the current item. func (c *CompositeDigester) Hash() io.WriteCloser { num := len(c.digesters) if num == 0 { return nil } return c.digesters[num-1] } // Digest returns the content type and a composite digest over everything // that's been digested. func (c *CompositeDigester) Digest() (string, digest.Digest) { c.closeOpenDigester() num := len(c.digesters) switch num { case 0: return "", "" case 1: return c.digesters[0].ContentType(), c.digesters[0].Digest() default: content := "" for i, digester := range c.digesters { if i > 0 { content += "," } contentType := digester.ContentType() if contentType != "" { contentType += ":" } content += contentType + digester.Digest().Encoded() } return "multi", digest.Canonical.FromString(content) } } ================================================ FILE: digester_test.go ================================================ package buildah import ( "archive/tar" "bytes" "io" "strings" "sync" "testing" "time" digest "github.com/opencontainers/go-digest" "github.com/stretchr/testify/require" ) func (c *CompositeDigester) isOpen() bool { for _, digester := range c.digesters { if tarDigester, ok := digester.(*tarDigester); ok { if tarDigester.isOpen { return true } } } return false } func TestCompositeDigester(t *testing.T) { t.Parallel() tests := []struct { name string itemTypes []string resultType string }{ { name: "download", itemTypes: []string{""}, resultType: "", }, { name: "file", itemTypes: []string{"file"}, resultType: "file", }, { name: "dir", itemTypes: []string{"dir"}, resultType: "dir", }, { name: "multiple-1", itemTypes: []string{"file", "dir"}, resultType: "multi", }, { name: "multiple-2", itemTypes: []string{"dir", "file"}, resultType: "multi", }, { name: "multiple-3", itemTypes: []string{"", "dir"}, resultType: "multi", }, { name: "multiple-4", itemTypes: []string{"", "file"}, resultType: "multi", }, { name: "multiple-5", itemTypes: []string{"dir", ""}, resultType: "multi", }, { name: "multiple-6", itemTypes: []string{"file", ""}, resultType: "multi", }, } var digester CompositeDigester var i int var buf bytes.Buffer zero := time.Unix(0, 0) for _, test := range tests { t.Run(test.name, func(t *testing.T) { for _, filtered := range []bool{false, true} { desc := "unfiltered" if filtered { desc = "filter" } t.Run(desc, func(t *testing.T) { if i > 0 { // restart only after it's been used some, to make sure it's not necessary otherwise digester.Restart() } i++ size := int64(i * 32) // items for this archive will be bigger than the last one for _, itemType := range test.itemTypes { for int64(buf.Len()) < size { err := buf.WriteByte(byte(buf.Len() % 256)) require.Nil(t, err, "error padding content buffer: %v", err) } // feed it content that it will treat either as raw data ("") or expect to // look like a tarball ("file"/"dir") digester.Start(itemType) hasher := digester.Hash() // returns an io.WriteCloser require.NotNil(t, hasher, "digester returned a null hasher?") if itemType == "" { // write something that isn't an archive n, err := io.Copy(hasher, &buf) require.Nil(t, err, "error writing tar content to digester: %v", err) require.Equal(t, size, n, "short write writing tar content to digester") continue } // write an archive var written bytes.Buffer // a copy of the archive we're generating and digesting hasher = &struct { io.Writer io.Closer }{ Writer: io.MultiWriter(hasher, &written), // splice into the writer Closer: hasher, } if filtered { // wrap the WriteCloser in another WriteCloser hasher = newTarFilterer(hasher, func(hdr *tar.Header) (bool, bool, io.Reader) { hdr.ModTime = zero return false, false, nil }) require.NotNil(t, hasher, "newTarFilterer returned a null WriteCloser?") } // write this item as an archive tw := tar.NewWriter(hasher) hdr := &tar.Header{ Name: "content", Size: size, Mode: 0o640, ModTime: time.Now(), Typeflag: tar.TypeReg, } err := tw.WriteHeader(hdr) require.Nil(t, err, "error writing tar header to digester: %v", err) n, err := io.Copy(tw, &buf) require.Nil(t, err, "error writing tar content to digester: %v", err) require.Equal(t, size, n, "short write writing tar content to digester") err = tw.Flush() require.Nil(t, err, "error flushing tar content to digester: %v", err) err = tw.Close() require.Nil(t, err, "error closing tar archive being written digester: %v", err) if filtered { // the ContentDigester can close its own if we don't explicitly ask it to, // but if we wrapped it in a filter, we have to close the filter to clean // up the filter, so we can't skip it to exercise that logic; we have to // leave that for the corresponding unfiltered case to try hasher.Close() } // now read the archive back tr := tar.NewReader(&written) require.NotNil(t, tr, "unable to read byte buffer?") hdr, err = tr.Next() for err == nil { var n int64 if filtered { // the filter should have set the modtime to unix 0 require.Equal(t, zero, hdr.ModTime, "timestamp for entry should have been zero") } else { // the filter should have left modtime to "roughly now" require.NotEqual(t, zero, hdr.ModTime, "timestamp for entry should not have been zero") } n, err = io.Copy(io.Discard, tr) require.Nil(t, err, "error reading tar content from buffer: %v", err) require.Equal(t, hdr.Size, n, "short read reading tar content") hdr, err = tr.Next() } require.Equal(t, io.EOF, err, "finished reading archive with %v, not EOF", err) } // check the composite digest type matches expectations and the value is not just the // digest of zero-length data, which is absolutely not what we wrote digestType, digestValue := digester.Digest() require.Equal(t, test.resultType, digestType, "expected to get a %q digest back for %v, got %q", test.resultType, test.itemTypes, digestType) require.NotEqual(t, digest.Canonical.FromBytes([]byte{}), digestValue, "digester wasn't fed any data") require.False(t, digester.isOpen(), "expected digester to have been closed with this usage pattern") }) } }) } } func TestTarFilterer(t *testing.T) { t.Parallel() tests := []struct { name string input, output map[string]string breakAfter int filter func(*tar.Header) (bool, bool, io.Reader) }{ { name: "none", input: map[string]string{ "file a": "content a", "file b": "content b", }, output: map[string]string{ "file a": "content a", "file b": "content b", }, filter: nil, }, { name: "plain", input: map[string]string{ "file a": "content a", "file b": "content b", }, output: map[string]string{ "file a": "content a", "file b": "content b", }, filter: func(*tar.Header) (bool, bool, io.Reader) { return false, false, nil }, }, { name: "skip", input: map[string]string{ "file a": "content a", "file b": "content b", }, output: map[string]string{ "file a": "content a", }, filter: func(hdr *tar.Header) (bool, bool, io.Reader) { return hdr.Name == "file b", false, nil }, }, { name: "replace", input: map[string]string{ "file a": "content a", "file b": "content b", "file c": "content c", }, output: map[string]string{ "file a": "content a", "file b": "content b+c", "file c": "content c", }, breakAfter: 2, filter: func(hdr *tar.Header) (bool, bool, io.Reader) { if hdr.Name == "file b" { content := "content b+c" hdr.Size = int64(len(content)) return false, true, strings.NewReader(content) } return false, false, nil }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { var buffer bytes.Buffer tw := tar.NewWriter(&buffer) files := 0 for filename, contents := range test.input { hdr := tar.Header{ Name: filename, Size: int64(len(contents)), Typeflag: tar.TypeReg, } err := tw.WriteHeader(&hdr) require.Nil(t, err, "unexpected error from TarWriter.WriteHeader") n, err := io.CopyN(tw, strings.NewReader(contents), int64(len(contents))) require.Nil(t, err, "unexpected error copying to tar writer") require.Equal(t, int64(len(contents)), n, "unexpected write length") files++ if test.breakAfter != 0 && files%test.breakAfter == 0 { // this test may have us writing multiple archives to the buffer // they should still read back as a single archive tw.Close() tw = tar.NewWriter(&buffer) } } tw.Close() output := make(map[string]string) pipeReader, pipeWriter := io.Pipe() var wg sync.WaitGroup wg.Add(1) go func() { tr := tar.NewReader(pipeReader) hdr, err := tr.Next() for err == nil { var buffer bytes.Buffer var n int64 n, err = io.Copy(&buffer, tr) require.Nil(t, err, "unexpected error copying from tar reader") require.Equal(t, hdr.Size, n, "unexpected read length") output[hdr.Name] = buffer.String() hdr, err = tr.Next() } require.Equal(t, io.EOF, err, "unexpected error ended our tarstream read") pipeReader.Close() wg.Done() }() filterer := newTarFilterer(pipeWriter, test.filter) _, err := io.Copy(filterer, &buffer) require.Nil(t, err, "unexpected error copying archive through filter to reader") filterer.Close() wg.Wait() require.Equal(t, test.output, output, "got unexpected results") }) } } ================================================ FILE: docker/AUTHORS ================================================ # This file lists all individuals having contributed content to the repository. # For how it is generated, see `hack/generate-authors.sh`. Aanand Prasad Aaron Davidson Aaron Feng Aaron Huslage Aaron Lehmann Aaron Welch Aaron.L.Xu Abel Muiño Abhijeet Kasurde Abhinav Ajgaonkar Abhishek Chanda Abin Shahab Adam Avilla Adam Eijdenberg Adam Kunk Adam Miller Adam Mills Adam Singer Adam Walz Addam Hardy Aditi Rajagopal Aditya Adolfo Ochagavía Adria Casas Adrian Moisey Adrian Mouat Adrian Oprea Adrien Folie Adrien Gallouët Ahmed Kamal Ahmet Alp Balkan Aidan Feldman Aidan Hobson Sayers AJ Bowen Ajey Charantimath ajneu Akihiro Suda Akira Koyasu Akshay Karle Al Tobey alambike Alan Scherger Alan Thompson Albert Callarisa Albert Zhang Aleksa Sarai Aleksandrs Fadins Alena Prokharchyk Alessandro Boch Alessio Biancalana Alex Chan Alex Chen Alex Coventry Alex Crawford Alex Ellis Alex Gaynor Alex Olshansky Alex Samorukov Alex Warhawk Alexander Artemenko Alexander Boyd Alexander Larsson Alexander Morozov Alexander Shopov Alexandre Beslic Alexandre González Alexandru Sfirlogea Alexey Guskov Alexey Kotlyarov Alexey Shamrin Alexis THOMAS Alfred Landrum Ali Dehghani Alicia Lauerman Alihan Demir Allen Madsen Allen Sun almoehi Alvaro Saurin Alvin Richards amangoel Amen Belayneh Amir Goldstein Amit Bakshi Amit Krishnan Amit Shukla Amy Lindburg Anand Patil AnandkumarPatel Anatoly Borodin Anchal Agrawal Anders Janmyr Andre Dublin <81dublin@gmail.com> Andre Granovsky Andrea Luzzardi Andrea Turli Andreas Köhler Andreas Savvides Andreas Tiefenthaler Andrei Gherzan Andrew C. Bodine Andrew Clay Shafer Andrew Duckworth Andrew France Andrew Gerrand Andrew Guenther Andrew Hsu Andrew Kuklewicz Andrew Macgregor Andrew Macpherson Andrew Martin Andrew McDonnell Andrew Munsell Andrew Po Andrew Weiss Andrew Williams Andrews Medina Andrey Petrov Andrey Stolbovsky André Martins andy Andy Chambers andy diller Andy Goldstein Andy Kipp Andy Rothfusz Andy Smith Andy Wilson Anes Hasicic Anil Belur Anil Madhavapeddy Ankush Agarwal Anonmily Anran Qiao Anthon van der Neut Anthony Baire Anthony Bishopric Anthony Dahanne Anthony Sottile Anton Löfgren Anton Nikitin Anton Polonskiy Anton Tiurin Antonio Murdaca Antonis Kalipetis Antony Messerli Anuj Bahuguna Anusha Ragunathan apocas Arash Deshmeh ArikaChen Arnaud Lefebvre Arnaud Porterie Arthur Barr Arthur Gautier Artur Meyster Arun Gupta Asbjørn Enge averagehuman Avi Das Avi Miller Avi Vaid ayoshitake Azat Khuyiyakhmetov Bardia Keyoumarsi Barnaby Gray Barry Allard Bartłomiej Piotrowski Bastiaan Bakker bdevloed Ben Bonnefoy Ben Firshman Ben Golub Ben Hall Ben Sargent Ben Severson Ben Toews Ben Wiklund Benjamin Atkin Benoit Chesneau Bernerd Schaefer Bert Goethals Bharath Thiruveedula Bhiraj Butala Bhumika Bayani Bilal Amarni Bill W bin liu Bingshen Wang Blake Geno Boaz Shuster bobby abbott Boshi Lian boucher Bouke Haarsma Boyd Hemphill boynux Bradley Cicenas Bradley Wright Brandon Liu Brandon Philips Brandon Rhodes Brendan Dixon Brent Salisbury Brett Higgins Brett Kochendorfer Brian (bex) Exelbierd Brian Bland Brian DeHamer Brian Dorsey Brian Flad Brian Goff Brian McCallister Brian Olsen Brian Shumate Brian Torres-Gil Brian Trump Brice Jaglin Briehan Lombaard Bruno Bigras Bruno Binet Bruno Gazzera Bruno Renié Bruno Tavares Bryan Bess Bryan Boreham Bryan Matsuo Bryan Murphy buddhamagnet Burke Libbey Byung Kang Caleb Spare Calen Pennington Cameron Boehmer Cameron Spear Campbell Allen Candid Dauth Cao Weiwei Carl Henrik Lunde Carl Loa Odin Carl X. Su Carlos Alexandro Becker Carlos Sanchez Carol Fager-Higgins Cary Casey Bisson Ce Gao Cedric Davies Cezar Sa Espinola Chad Swenson Chance Zibolski Chander G Charles Chan Charles Hooper Charles Law Charles Lindsay Charles Merriam Charles Sarrazin Charles Smith Charlie Drage Charlie Lewis Chase Bolt ChaYoung You Chen Chao Chen Chuanliang Chen Hanxiao Chen Mingjie cheney90 Chewey Chia-liang Kao chli Cholerae Hu Chris Alfonso Chris Armstrong Chris Dituri Chris Fordham Chris Gavin Chris Khoo Chris McKinnel Chris Seto Chris Snow Chris St. Pierre Chris Stivers Chris Swan Chris Wahl Chris Weyl chrismckinnel Christian Berendt Christian Böhme Christian Persson Christian Rotzoll Christian Simon Christian Stefanescu ChristoperBiscardi Christophe Mehay Christophe Troestler Christopher Currie Christopher Jones Christopher Latham Christopher Rigor Christy Perez Chun Chen Ciro S. Costa Clayton Coleman Clinton Kitson Coenraad Loubser Colin Dunklau Colin Rice Colin Walters Collin Guarino Colm Hally companycy Cory Forsyth cressie176 CrimsonGlory Cristian Staretu cristiano balducci Cruceru Calin-Cristian CUI Wei Cyprian Gracz Cyril F Daan van Berkel Daehyeok Mun Dafydd Crosby dalanlan Damian Smyth Damien Nadé Damien Nozay Damjan Georgievski Dan Anolik Dan Buch Dan Cotora Dan Feldman Dan Griffin Dan Hirsch Dan Keder Dan Levy Dan McPherson Dan Stine Dan Walsh Dan Williams Daniel Antlinger Daniel Exner Daniel Farrell Daniel Garcia Daniel Gasienica Daniel Hiltgen Daniel Menet Daniel Mizyrycki Daniel Nephin Daniel Norberg Daniel Nordberg Daniel Robinson Daniel S Daniel Von Fange Daniel X Moore Daniel YC Lin Daniel Zhang Daniel, Dao Quang Minh Danny Berger Danny Yates Darren Coxall Darren Shepherd Darren Stahl Dattatraya Kumbhar Davanum Srinivas Dave Barboza Dave Henderson Dave MacDonald Dave Tucker David Anderson David Calavera David Corking David Cramer David Currie David Davis David Dooling David Gageot David Gebler David Lawrence David Lechner David M. Karr David Mackey David Mat David Mcanulty David Pelaez David R. Jenni David Röthlisberger David Sheets David Sissitka David Trott David Williamson David Xia David Young Davide Ceretti Dawn Chen dbdd dcylabs decadent deed02392 Deng Guangxing Deni Bertovic Denis Gladkikh Denis Ollier Dennis Chen Dennis Docter Derek Derek Derek Ch Derek McGowan Deric Crago Deshi Xiao devmeyster Devvyn Murphy Dharmit Shah Diego Romero Diego Siqueira Dieter Reuter Dillon Dixon Dima Stopel Dimitri John Ledkov Dimitris Rozakis Dimitry Andric Dinesh Subhraveti Ding Fei Diogo Monica DiuDiugirl Djibril Koné dkumor Dmitri Logvinenko Dmitri Shuralyov Dmitry Demeshchuk Dmitry Gusev Dmitry Kononenko Dmitry Shyshkin Dmitry Smirnov Dmitry V. Krivenok Dmitry Vorobev Dolph Mathews Dominik Dingel Dominik Finkbeiner Dominik Honnef Don Kirkby Don Kjer Don Spaulding Donald Huang Dong Chen Donovan Jones Doron Podoleanu Doug Davis Doug MacEachern Doug Tangren Dr Nic Williams dragon788 Dražen Lučanin Drew Erny Dustin Sallings Ed Costello Edmund Wagner Eiichi Tsukata Eike Herzbach Eivin Giske Skaaren Eivind Uggedal Elan Ruusamäe Elena Morozova Elias Probst Elijah Zupancic eluck Elvir Kuric Emil Hernvall Emily Maier Emily Rose Emir Ozer Enguerran Eohyung Lee epeterso Eric Barch Eric Curtin Eric Hanchrow Eric Lee Eric Myhre Eric Paris Eric Rafaloff Eric Rosenberg Eric Sage Erica Windisch Eric Yang Eric-Olivier Lamey Erik Bray Erik Dubbelboer Erik Hollensbe Erik Inge Bolsø Erik Kristensen Erik St. Martin Erik Weathers Erno Hopearuoho Erwin van der Koogh Euan Eugene Yakubovich eugenkrizo evalle Evan Allrich Evan Carmi Evan Hazlett Evan Hazlett Evan Krall Evan Phoenix Evan Wies Evelyn Xu Everett Toews Evgeny Vereshchagin Ewa Czechowska Eystein Måløy Stenberg ezbercih Ezra Silvera Fabiano Rosas Fabio Falci Fabio Rapposelli Fabio Rehm Fabrizio Regini Fabrizio Soppelsa Faiz Khan falmp Fangyuan Gao <21551127@zju.edu.cn> Fareed Dudhia Fathi Boudra Federico Gimenez Felipe Oliveira Felix Abecassis Felix Geisendörfer Felix Hupfeld Felix Rabe Felix Ruess Felix Schindler Ferenc Szabo Fernando Fero Volar Ferran Rodenas Filipe Brandenburger Filipe Oliveira fl0yd Flavio Castelli FLGMwt Florian Florian Klein Florian Maier Florian Weingarten Florin Asavoaie fonglh fortinux Francesc Campoy Francis Chuang Francisco Carriedo Francisco Souza Frank Groeneveld Frank Herrmann Frank Macreery Frank Rosquin Fred Lifton Frederick F. Kautz IV Frederik Loeffert Frederik Nordahl Jul Sabroe Freek Kalter frosforever fy2462 Félix Baylac-Jacqué Félix Cantournet Gabe Rosenhouse Gabor Nagy Gabriel Linder Gabriel Monroy Gabriel Nicolas Avellaneda Gaetan de Villele Galen Sampson Gang Qiao Gareth Rushgrove Garrett Barboza Gaurav gautam, prasanna Gaël PORTAY GennadySpb Geoffrey Bachelet George MacRorie George Xie Georgi Hristozov Gereon Frey German DZ Gert van Valkenhoef Gerwim Gianluca Borello Gildas Cuisinier gissehel Giuseppe Mazzotta Gleb Fotengauer-Malinovskiy Gleb M Borisov Glyn Normington GoBella Goffert van Gool Gosuke Miyashita Gou Rao Govinda Fichtner Grant Reaber Graydon Hoare Greg Fausak Greg Thornton grossws grunny gs11 Guilhem Lettron Guilherme Salgado Guillaume Dufour Guillaume J. Charmes guoxiuyan Gurjeet Singh Guruprasad gwx296173 Günter Zöchbauer Hans Kristian Flaatten Hans Rødtang Hao Shu Wei Hao Zhang <21521210@zju.edu.cn> Harald Albers Harley Laue Harold Cooper Harry Zhang Harshal Patil He Simei He Xin heartlock <21521209@zju.edu.cn> Hector Castro Helen Xie Henning Sprang Hobofan Hollie Teal Hong Xu Hongbin Lu hsinko <21551195@zju.edu.cn> Hu Keping Hu Tao Huanzhong Zhang Huayi Zhang Hugo Duncan Hugo Marisco <0x6875676f@gmail.com> Hunter Blanks huqun Huu Nguyen hyeongkyu.lee hyp3rdino Hyzhou <1187766782@qq.com> Ian Babrou Ian Bishop Ian Bull Ian Calvert Ian Campbell Ian Lee Ian Main Ian Truslove Iavael Icaro Seara Igor Dolzhikov Iliana Weller Ilkka Laukkanen Ilya Dmitrichenko Ilya Gusev ILYA Khlopotov imre Fitos inglesp Ingo Gottwald Isaac Dupree Isabel Jimenez Isao Jonas Ivan Babrou Ivan Fraixedes Ivan Grcic J Bruni J. Nunn Jack Danger Canty Jacob Atzen Jacob Edelman Jacob Tomlinson Jake Champlin Jake Moshenko Jake Sanders jakedt James Allen James Carey James Carr James DeFelice James Harrison Fisher James Kyburz James Kyle James Lal James Mills James Nugent James Turnbull Jamie Hannaford Jamshid Afshar Jan Keromnes Jan Koprowski Jan Pazdziora Jan Toebes Jan-Gerd Tenberge Jan-Jaap Driessen Jana Radhakrishnan Jannick Fahlbusch Janonymous Januar Wayong Jared Biel Jared Hocutt Jaroslaw Zabiello jaseg Jasmine Hegman Jason Divock Jason Giedymin Jason Green Jason Hall Jason Heiss Jason Livesay Jason McVetta Jason Plum Jason Shepherd Jason Smith Jason Sommer Jason Stangroome jaxgeller Jay Jay Jay Kamat Jean-Baptiste Barth Jean-Baptiste Dalido Jean-Christophe Berthon Jean-Paul Calderone Jean-Pierre Huynh Jean-Tiare Le Bigot Jeff Anderson Jeff Johnston Jeff Lindsay Jeff Mickey Jeff Minard Jeff Nickoloff Jeff Silberman Jeff Welch Jeffrey Bolle Jeffrey Morgan Jeffrey van Gogh Jenny Gebske Jeremy Grosser Jeremy Price Jeremy Qian Jeremy Unruh Jeroen Jacobs Jesse Dearing Jesse Dubay Jessica Frazelle Jezeniel Zapanta jgeiger Jhon Honce Ji.Zhilong Jian Zhang jianbosun Jie Luo Jilles Oldenbeuving Jim Alateras Jim Minter Jim Perrin Jimmy Cuadra Jimmy Puckett jimmyxian Jinsoo Park Jiri Popelka Jiuyue Ma Jiří Župka jjy jmzwcn Joao Fernandes Joe Beda Joe Doliner Joe Ferguson Joe Gordon Joe Shaw Joe Van Dyk Joel Friedly Joel Handwell Joel Hansson Joel Wurtz Joey Geiger Joey Gibson Joffrey F Johan Euphrosine Johan Rydberg Johanan Lieberman Johannes 'fish' Ziemke John Costa John Feminella John Gardiner Myers John Gossman John Howard (VM) John Mulhausen John OBrien III John Starks John Stephens John Tims John Warwick John Willis johnharris85 Jon Wedaman Jonas Pfenniger Jonathan A. Sternberg Jonathan Boulle Jonathan Camp Jonathan Dowland Jonathan Lebon Jonathan Lomas Jonathan McCrohan Jonathan Mueller Jonathan Pares Jonathan Rudenberg Jonathan Stoppani Jonh Wendell Joost Cassee Jordan Jordan Arentsen Jordan Sissel Jorge Marin Jose Diaz-Gonzalez Joseph Anthony Pasquale Holsten Joseph Hager Joseph Kern Josh Josh Bodah Josh Chorlton Josh Eveleth Josh Hawn Josh Horwitz Josh Poimboeuf Josh Wilson Josiah Kiehl José Tomás Albornoz JP jrabbit jroenf Julian Taylor Julien Barbier Julien Bisconti Julien Bordellier Julien Dubois Julien Pervillé Julio Montes Jun-Ru Chang Jussi Nummelin Justas Brazauskas Justin Cormack Justin Force Justin Plock Justin Simonelis Justin Terry Justyn Temme Jyrki Puttonen Jérôme Petazzoni Jörg Thalheim Kai Blin Kai Qiang Wu(Kennan) Kamil Domański kamjar gerami Kanstantsin Shautsou Kara Alexandra Karan Lyons Kareem Khazem kargakis Karl Grzeszczak Karol Duleba Katie McLaughlin Kato Kazuyoshi Katrina Owen Kawsar Saiyeed Kay Yan kayrus Ke Li Ke Xu Kei Ohmura Keith Hudgins Keli Hu Ken Cochrane Ken Herner Ken ICHIKAWA Kenfe-Mickaël Laventure Kenjiro Nakayama Kent Johnson Kevin "qwazerty" Houdebert Kevin Burke Kevin Clark Kevin J. Lynagh Kevin Jing Qiu Kevin Kern Kevin Menard Kevin P. Kucharczyk Kevin Richardson Kevin Shi Kevin Wallace Kevin Yap kevinmeredith Keyvan Fatehi kies Kim BKC Carlbacker Kim Eik Kimbro Staken Kir Kolyshkin Kiran Gangadharan Kirill Kolyshkin Kirill SIbirev knappe Kohei Tsuruta Koichi Shiraishi Konrad Kleine Konstantin L Konstantin Pelykh Krasi Georgiev Krasimir Georgiev Kris-Mikael Krister Kristian Haugene Kristina Zabunova krrg Kun Zhang Kunal Kushwaha Kyle Conroy Kyle Linden kyu Lachlan Coote Lai Jiangshan Lajos Papp Lakshan Perera Lalatendu Mohanty Lance Chen Lance Kinley Lars Butler Lars Kellogg-Stedman Lars R. Damerow Lars-Magnus Skog Laszlo Meszaros Laura Frank Laurent Erignoux Laurie Voss Leandro Siqueira Lee Chao <932819864@qq.com> Lee, Meng-Han leeplay Lei Jitang Len Weincier Lennie Leo Gallucci Leszek Kowalski Levi Blackstone Levi Gross Lewis Daly Lewis Marshall Lewis Peckover Liam Macgillavry Liana Lo Liang Mingqiang Liang-Chi Hsieh liaoqingwei Lily Guo limsy Lin Lu LingFaKe Linus Heckemann Liran Tal Liron Levin Liu Bo Liu Hua liwenqi lixiaobing10051267 Liz Zhang LIZAO LI Lizzie Dixon <_@lizzie.io> Lloyd Dewolf Lokesh Mandvekar longliqiang88 <394564827@qq.com> Lorenz Leutgeb Lorenzo Fontana Louis Opter Luca Favatella Luca Marturana Luca Orlandi Luca-Bogdan Grigorescu Lucas Chan Lucas Chi Luciano Mores Luis Martínez de Bartolomé Izquierdo Luiz Svoboda Lukas Waslowski lukaspustina Lukasz Zajaczkowski lukemarsden Lyn Lynda O'Leary Lénaïc Huard Ma Müller Ma Shimiao Mabin Madhav Puri Madhu Venugopal Mageee <21521230.zju.edu.cn> Mahesh Tiyyagura malnick Malte Janduda manchoz Manfred Touron Manfred Zabarauskas Mansi Nahar mansinahar Manuel Meurer Manuel Woelker mapk0y Marc Abramowitz Marc Kuo Marc Tamsky Marcelo Salazar Marco Hennings Marcus Cobden Marcus Farkas Marcus Linke Marcus Ramberg Marek Goldmann Marian Marinov Marianna Tessel Mario Loriedo Marius Gundersen Marius Sturm Marius Voila Mark Allen Mark McGranaghan Mark McKinstry Mark Milstein Mark Parker Mark West Marko Mikulicic Marko Tibold Markus Fix Martijn Dwars Martijn van Oosterhout Martin Honermeyer Martin Kelly Martin Mosegaard Amdisen Martin Redmond Mary Anthony Masahito Zembutsu Masayuki Morita Mason Malone Mateusz Sulima Mathias Monnerville Mathieu Le Marec - Pasquet Mathieu Parent Matt Apperson Matt Bachmann Matt Bentley Matt Haggard Matt Hoyle Matt McCormick Matt Moore Matt Richardson Matt Robenolt Matthew Heon Matthew Lapworth Matthew Mayer Matthew Mueller Matthew Riley Matthias Klumpp Matthias Kühnle Matthias Rampke Matthieu Hauglustaine mattymo mattyw Mauricio Garavaglia mauriyouth Max Shytikov Maxim Fedchyshyn Maxim Ivanov Maxim Kulkin Maxim Treskin Maxime Petazzoni Meaglith Ma meejah Megan Kostick Mehul Kar Mei ChunTao Mengdi Gao Mert Yazıcıoğlu mgniu Micah Zoltu Michael A. Smith Michael Bridgen Michael Brown Michael Chiang Michael Crosby Michael Currie Michael Friis Michael Gorsuch Michael Grauer Michael Holzheu Michael Hudson-Doyle Michael Huettermann Michael Irwin Michael Käufl Michael Neale Michael Prokop Michael Scharf Michael Stapelberg Michael Steinert Michael Thies Michael West Michal Fojtik Michal Gebauer Michal Jemala Michal Minář Michal Wieczorek Michaël Pailloncy Michał Czeraszkiewicz Michiel@unhosted Mickaël FORTUNATO Miguel Angel Fernández Miguel Morales Mihai Borobocea Mihuleacc Sergiu Mike Brown Mike Chelen Mike Danese Mike Dillon Mike Dougherty Mike Gaffney Mike Goelzer Mike Leone Mike MacCana Mike Naberezny Mike Snitzer mikelinjie <294893458@qq.com> Mikhail Sobolev Milind Chawre Miloslav Trmač mingqing Mingzhen Feng Misty Stanley-Jones Mitch Capper mlarcher Mohammad Banikazemi Mohammed Aaqib Ansari Mohit Soni Morgan Bauer Morgante Pell Morgy93 Morten Siebuhr Morton Fox Moysés Borges mqliang Mrunal Patel msabansal mschurenko Muayyad Alsadi muge Mustafa Akın Muthukumar R Máximo Cuadros Médi-Rémi Hashim Nahum Shalman Nakul Pathak Nalin Dahyabhai Nan Monnand Deng Naoki Orii Natalie Parker Natanael Copa Nate Brennand Nate Eagleson Nate Jones Nathan Hsieh Nathan Kleyn Nathan LeClaire Nathan McCauley Nathan Williams Neal McBurnett Neil Peterson Nelson Chen Neyazul Haque Nghia Tran Niall O'Higgins Nicholas E. Rabenau nick Nick DeCoursin Nick Irvine Nick Parker Nick Payne Nick Stenning Nick Stinemates NickrenREN Nicola Kabar Nicolas Borboën Nicolas De loof Nicolas Dudebout Nicolas Goy Nicolas Kaiser Nicolás Hock Isaza Nigel Poulton NikolaMandic nikolas Nikolay Milovanov Nirmal Mehta Nishant Totla NIWA Hideyuki Noah Treuhaft noducks Nolan Darilek nponeccop Nuutti Kotivuori nzwsch O.S. Tezer objectified OddBloke odk- Oguz Bilgic Oh Jinkyun Ohad Schneider ohmystack Ole Reifschneider Oliver Neal Olivier Gambier Olle Jonsson Oriol Francès orkaa Oskar Niburski Otto Kekäläinen Ovidio Mallo oyld ozlerhakan paetling pandrew panticz Paolo G. Giarrusso Pascal Borreli Pascal Hartig Patrick Böänziger Patrick Devine Patrick Hemmer Patrick Stapleton pattichen Paul paul Paul Annesley Paul Bellamy Paul Bowsher Paul Furtado Paul Hammond Paul Jimenez Paul Kehrer Paul Lietar Paul Liljenberg Paul Morie Paul Nasrat Paul Weaver Paulo Ribeiro Pavel Lobashov Pavel Pospisil Pavel Sutyrin Pavel Tikhomirov Pavlos Ratis Pavol Vargovcik Peeyush Gupta Peggy Li Pei Su Penghan Wang perhapszzy@sina.com pestophagous Peter Bourgon Peter Braden Peter Choi Peter Dave Hello Peter Edge Peter Ericson Peter Esbensen Peter Malmgren Peter Salvatore Peter Volpe Peter Waller Petr Švihlík Phil Phil Estes Phil Spitler Philip Monroe Philipp Wahala Philipp Weissensteiner Phillip Alexander pidster Piergiuliano Bossi Pierre Pierre Carrier Pierre Dal-Pra Pierre Wacrenier Pierre-Alain RIVIERE Piotr Bogdan pixelistik Porjo Poul Kjeldager Sørensen Pradeep Chhetri Prasanna Gautam Prayag Verma Przemek Hejman pysqz qhuang Qiang Huang Qinglan Peng qudongfang Quentin Brossard Quentin Perez Quentin Tayssier r0n22 Rafal Jeczalik Rafe Colton Raghavendra K T Raghuram Devarakonda Rajat Pandit Rajdeep Dua Ralf Sippl Ralle Ralph Bean Ramkumar Ramachandra Ramon Brooker Ramon van Alteren Ray Tsang ReadmeCritic Recursive Madman Reficul Regan McCooey Remi Rampin Renato Riccieri Santos Zannon resouer rgstephens Rhys Hiltner Rich Moyse Rich Seymour Richard Richard Burnison Richard Harvey Richard Mathie Richard Metzler Richard Scothern Richo Healey Rick Bradley Rick van de Loo Rick Wieman Rik Nijessen Riku Voipio Riley Guerin Ritesh H Shukla Riyaz Faizullabhoy Rob Vesse Robert Bachmann Robert Bittle Robert Obryk Robert Stern Robert Terhaar Robert Wallis Roberto G. Hashioka Roberto Muñoz Fernández Robin Naundorf Robin Schneider Robin Speekenbrink robpc Rodolfo Carvalho Rodrigo Vaz Roel Van Nyen Roger Peppe Rohit Jnagal Rohit Kadam Rojin George Roland Huß Roland Kammerer Roland Moriz Roma Sokolov Roman Strashkin Ron Smits Ron Williams root root root root root Rory Hunter Rory McCune Ross Boucher Rovanion Luckey Rozhnov Alexandr rsmoorthy Rudolph Gottesheim Rui Lopes Runshen Zhu Ryan Abrams Ryan Anderson Ryan Aslett Ryan Belgrave Ryan Detzel Ryan Fowler Ryan McLaughlin Ryan O'Donnell Ryan Seto Ryan Thomas Ryan Trauntvein Ryan Wallner Ryan Zhang RyanDeng Rémy Greinhofer s. rannou s00318865 Sabin Basyal Sachin Joshi Sagar Hani Sainath Grandhi sakeven Sally O'Malley Sam Abed Sam Alba Sam Bailey Sam J Sharpe Sam Neirinck Sam Reis Sam Rijs Sambuddha Basu Sami Wagiaalla Samuel Andaya Samuel Dion-Girardeau Samuel Karp Samuel PHAN Sandeep Bansal Sankar சங்கர் Sanket Saurav Santhosh Manohar sapphiredev Satnam Singh satoru Satoshi Amemiya Satoshi Tagomori scaleoutsean Scott Bessler Scott Collier Scott Johnston Scott Stamp Scott Walls sdreyesg Sean Christopherson Sean Cronin Sean McIntyre Sean OMeara Sean P. Kane Sean Rodman Sebastiaan van Steenis Sebastiaan van Stijn Senthil Kumar Selvaraj Senthil Kumaran SeongJae Park Seongyeol Lim Serge Hallyn Sergey Alekseev Sergey Evstifeev Sergii Kabashniuk Serhat Gülçiçek Sevki Hasirci Shane Canon Shane da Silva shaunol Shawn Landden Shawn Siefkas shawnhe Shayne Wang Shekhar Gulati Sheng Yang Shengbo Song Shev Yan Shih-Yuan Lee Shijiang Wei Shishir Mahajan Shoubhik Bose Shourya Sarcar shuai-z Shukui Yang Shuwei Hao Sian Lerk Lau sidharthamani Silas Sewell Silvan Jegen Simei He Simon Eskildsen Simon Leinen Simon Taranto Sindhu S Sjoerd Langkemper skaasten Solganik Alexander Solomon Hykes Song Gao Soshi Katsuta Soulou Spencer Brown Spencer Smith Sridatta Thatipamala Sridhar Ratnakumar Srini Brahmaroutu srinsriv Steeve Morin Stefan Berger Stefan J. Wernli Stefan Praszalowicz Stefan S. Stefan Scherer Stefan Staudenmeyer Stefan Weil Stephen Crosby Stephen Day Stephen Drake Stephen Rust Steve Dougherty Steve Durrheimer Steve Francia Steve Koch Steven Burgess Steven Erenst Steven Hartland Steven Iveson Steven Merrill Steven Richards Steven Taylor Subhajit Ghosh Sujith Haridasan Sun Gengze <690388648@qq.com> Suryakumar Sudar Sven Dowideit Swapnil Daingade Sylvain Baubeau Sylvain Bellemare Sébastien Sébastien Luttringer Sébastien Stormacq Tadej Janež TAGOMORI Satoshi tang0th Tangi COLIN Tatsuki Sugiura Tatsushi Inagaki Taylor Jones tbonza Ted M. Young Tehmasp Chaudhri Tejesh Mehta terryding77 <550147740@qq.com> tgic Thatcher Peskens theadactyl Thell 'Bo' Fowler Thermionix Thijs Terlouw Thomas Bikeev Thomas Frössman Thomas Gazagnaire Thomas Grainger Thomas Hansen Thomas Leonard Thomas LEVEIL Thomas Orozco Thomas Riccardi Thomas Schroeter Thomas Sjögren Thomas Swift Thomas Tanaka Thomas Texier Tianon Gravi Tianyi Wang Tibor Vass Tiffany Jernigan Tiffany Low Tim Bosse Tim Dettrick Tim Düsterhus Tim Hockin Tim Ruffles Tim Smith Tim Terhorst Tim Wang Tim Waugh Tim Wraight Tim Zju <21651152@zju.edu.cn> timfeirg Timothy Hobbs tjwebb123 tobe Tobias Bieniek Tobias Bradtke Tobias Gesellchen Tobias Klauser Tobias Munk Tobias Schmidt Tobias Schwab Todd Crane Todd Lunter Todd Whiteman Toli Kuznets Tom Barlow Tom Booth Tom Denham Tom Fotherby Tom Howe Tom Hulihan Tom Maaswinkel Tom Wilkie Tom X. Tobin Tomas Tomecek Tomasz Kopczynski Tomasz Lipinski Tomasz Nurkiewicz Tommaso Visconti Tomáš Hrčka Tonny Xu Tony Abboud Tony Daws Tony Miller toogley Torstein Husebø Tõnis Tiigi tpng tracylihui <793912329@qq.com> Trapier Marshall Travis Cline Travis Thieman Trent Ogren Trevor Trevor Pounds Trevor Sullivan trishnaguha Tristan Carel Troy Denton Tyler Brock Tzu-Jung Lee Ulysse Carion unknown vagrant Vaidas Jablonskis Veres Lajos vgeta Victor Algaze Victor Coisne Victor Costan Victor I. Wood Victor Lyuboslavsky Victor Marmol Victor Palma Victor Vieux Victoria Bialas Vijaya Kumar K Viktor Stanchev Viktor Vojnovski VinayRaghavanKS Vincent Batts Vincent Bernat Vincent Bernat Vincent Demeester Vincent Giersch Vincent Mayers Vincent Woo Vinod Kulkarni Vishal Doshi Vishnu Kannan Vitor Monteiro Vivek Agarwal Vivek Dasgupta Vivek Goyal Vladimir Bulyga Vladimir Kirillov Vladimir Pouzanov Vladimir Rutsky Vladimir Varankin VladimirAus Vojtech Vitek (V-Teq) waitingkuo Walter Leibbrandt Walter Stanish WANG Chao Wang Long Wang Ping Wang Xing Wang Yuexiao Ward Vandewege WarheadsSE Wayne Chang Wei Wu Wei-Ting Kuo weiyan Weiyang Zhu Wen Cheng Ma Wendel Fleming Wenkai Yin Wentao Zhang Wenxuan Zhao Wenyu You <21551128@zju.edu.cn> Wenzhi Liang Wes Morgan Wewang Xiaorenfine Will Dietz Will Rouesnel Will Weaver willhf William Delanoue William Henry William Hubbs William Martin William Riancho William Thurston WiseTrem wlan0 Wolfgang Powisch wonderflow Wonjun Kim xamyzhao Xianglin Gao Xianlu Bird XiaoBing Jiang Xiaoxu Chen xiekeyang Xinbo Weng Xinzi Zhou Xiuming Chen xlgao-zju xuzhaokui Yahya YAMADA Tsuyoshi Yan Feng Yang Bai yangshukui Yanqiang Miao Yasunori Mahata Yestin Sun Yi EungJun Yibai Zhang Yihang Ho Ying Li Yohei Ueda Yong Tang Yongzhi Pan yorkie You-Sheng Yang (楊有勝) Youcef YEKHLEF Yu Peng Yuan Sun yuchangchun yuchengxia Yunxiang Huang Yurii Rashkovskii yuzou Zac Dover Zach Borboa Zachary Jaffee Zain Memon Zaiste! Zane DeGraffenried Zefan Li Zen Lin(Zhinan Lin) Zhang Kun Zhang Wei Zhang Wentao zhangxianwei Zhenan Ye <21551168@zju.edu.cn> zhenghenghuo Zhenkun Bi zhouhao Zhu Guihua Zhu Kunjia Zhuoyun Wei Zilin Du zimbatm Ziming Dong ZJUshuaizhou <21551191@zju.edu.cn> zmarouf Zoltan Tombol zqh Zuhayr Elahi Zunayed Ali Álex González Álvaro Lázaro Átila Camurça Alves 尹吉峰 搏通 ================================================ FILE: docker/types.go ================================================ package docker // // Types extracted from Docker // import ( "time" digest "github.com/opencontainers/go-digest" "go.podman.io/image/v5/pkg/strslice" ) // github.com/moby/moby/image/rootfs.go const TypeLayers = "layers" // github.com/docker/distribution/manifest/schema2/manifest.go const V2S2MediaTypeUncompressedLayer = "application/vnd.docker.image.rootfs.diff.tar" // github.com/moby/moby/image/rootfs.go // V2S2RootFS describes images root filesystem // This is currently a placeholder that only supports layers. In the future // this can be made into an interface that supports different implementations. type V2S2RootFS struct { Type string `json:"type"` DiffIDs []digest.Digest `json:"diff_ids,omitempty"` } // github.com/moby/moby/image/image.go // V2S2History stores build commands that were used to create an image type V2S2History struct { // Created is the timestamp at which the image was created Created time.Time `json:"created"` // Author is the name of the author that was specified when committing the image Author string `json:"author,omitempty"` // CreatedBy keeps the Dockerfile command used while building the image CreatedBy string `json:"created_by,omitempty"` // Comment is the commit message that was set when committing the image Comment string `json:"comment,omitempty"` // EmptyLayer is set to true if this history item did not generate a // layer. Otherwise, the history item is associated with the next // layer in the RootFS section. EmptyLayer bool `json:"empty_layer,omitempty"` } // github.com/moby/moby/image/image.go // ID is the content-addressable ID of an image. type ID digest.Digest // github.com/moby/moby/api/types/container/config.go // HealthConfig holds configuration settings for the HEALTHCHECK feature. type HealthConfig struct { // Test is the test to perform to check that the container is healthy. // An empty slice means to inherit the default. // The options are: // {} : inherit healthcheck // {"NONE"} : disable healthcheck // {"CMD", args...} : exec arguments directly // {"CMD-SHELL", command} : run command with system's default shell Test []string `json:",omitempty"` // Zero means to inherit. Durations are expressed as integer nanoseconds. Interval time.Duration `json:",omitempty"` // Interval is the time to wait between checks. Timeout time.Duration `json:",omitempty"` // Timeout is the time to wait before considering the check to have hung. StartPeriod time.Duration `json:",omitempty"` // Time to wait after the container starts before running the first check. StartInterval time.Duration `json:",omitempty"` // Time to wait between checks during the StartPeriod. // Retries is the number of consecutive failures needed to consider a container as unhealthy. // Zero means inherit. Retries int `json:",omitempty"` } // github.com/docker/go-connections/nat/nat.go // PortSet is a collection of structs indexed by Port type PortSet map[Port]struct{} // github.com/docker/go-connections/nat/nat.go // Port is a string containing port number and protocol in the format "80/tcp" type Port string // github.com/moby/moby/api/types/container/config.go // Config contains the configuration data about a container. // It should hold only portable information about the container. // Here, "portable" means "independent from the host we are running on". // Non-portable information *should* appear in HostConfig. // All fields added to this struct must be marked `omitempty` to keep getting // predictable hashes from the old `v1Compatibility` configuration. type Config struct { Hostname string // Hostname Domainname string // Domainname User string // User that will run the command(s) inside the container, also support user:group AttachStdin bool // Attach the standard input, makes possible user interaction AttachStdout bool // Attach the standard output AttachStderr bool // Attach the standard error ExposedPorts PortSet `json:",omitempty"` // List of exposed ports Tty bool // Attach standard streams to a tty, including stdin if it is not closed. OpenStdin bool // Open stdin StdinOnce bool // If true, close stdin after the 1 attached client disconnects. Env []string // List of environment variable to set in the container Cmd strslice.StrSlice // Command to run when starting the container Healthcheck *HealthConfig `json:",omitempty"` // Healthcheck describes how to check the container is healthy ArgsEscaped bool `json:",omitempty"` // True if command is already escaped (Windows specific) Image string // Name of the image as it was passed by the operator (e.g. could be symbolic) Volumes map[string]struct{} // List of volumes (mounts) used for the container WorkingDir string // Current directory (PWD) in the command will be launched Entrypoint strslice.StrSlice // Entrypoint to run when starting the container NetworkDisabled bool `json:",omitempty"` // Is network disabled MacAddress string `json:",omitempty"` // Mac Address of the container OnBuild []string // ONBUILD metadata that were defined on the image Dockerfile Labels map[string]string // List of labels set to this container StopSignal string `json:",omitempty"` // Signal to stop a container StopTimeout *int `json:",omitempty"` // Timeout (in seconds) to stop a container Shell strslice.StrSlice `json:",omitempty"` // Shell for shell-form of RUN, CMD, ENTRYPOINT } // github.com/docker/distribution/manifest/schema1/config_builder.go // For non-top-level layers, create fake V1Compatibility strings that // fit the format and don't collide with anything else, but don't // result in runnable images on their own. type V1Compatibility struct { ID string `json:"id"` Parent string `json:"parent,omitempty"` Comment string `json:"comment,omitempty"` Created time.Time `json:"created"` ContainerConfig struct { Cmd []string } `json:"container_config"` Author string `json:"author,omitempty"` ThrowAway bool `json:"throwaway,omitempty"` } // github.com/moby/moby/image/image.go // V1Image stores the V1 image configuration. type V1Image struct { // ID is a unique 64 character identifier of the image ID string `json:"id,omitempty"` // Parent is the ID of the parent image Parent string `json:"parent,omitempty"` // Comment is the commit message that was set when committing the image Comment string `json:"comment,omitempty"` // Created is the timestamp at which the image was created Created time.Time `json:"created"` // Container is the id of the container used to commit Container string `json:"container,omitempty"` // ContainerConfig is the configuration of the container that is committed into the image ContainerConfig Config `json:"container_config"` // DockerVersion specifies the version of Docker that was used to build the image DockerVersion string `json:"docker_version,omitempty"` // Author is the name of the author that was specified when committing the image Author string `json:"author,omitempty"` // Config is the configuration of the container received from the client Config *Config `json:"config,omitempty"` // Architecture is the hardware that the image is build and runs on Architecture string `json:"architecture,omitempty"` // Variant is a variant of the CPU that the image is built and runs on Variant string `json:"variant,omitempty"` // OS is the operating system used to build and run the image OS string `json:"os,omitempty"` // Size is the total size of the image including all layers it is composed of Size int64 `json:",omitempty"` } // github.com/moby/moby/image/image.go // V2Image stores the image configuration type V2Image struct { V1Image Parent ID `json:"parent,omitempty"` RootFS *V2S2RootFS `json:"rootfs,omitempty"` History []V2S2History `json:"history,omitempty"` OSVersion string `json:"os.version,omitempty"` OSFeatures []string `json:"os.features,omitempty"` } // github.com/docker/distribution/manifest/versioned.go // V2Versioned provides a struct with the manifest schemaVersion and mediaType. // Incoming content with unknown schema version can be decoded against this // struct to check the version. type V2Versioned struct { // SchemaVersion is the image manifest schema that this image follows SchemaVersion int `json:"schemaVersion"` // MediaType is the media type of this schema. MediaType string `json:"mediaType,omitempty"` } // github.com/docker/distribution/manifest/schema1/manifest.go // V2S1FSLayer is a container struct for BlobSums defined in an image manifest type V2S1FSLayer struct { // BlobSum is the tarsum of the referenced filesystem image layer BlobSum digest.Digest `json:"blobSum"` } // github.com/docker/distribution/manifest/schema1/manifest.go // V2S1History stores unstructured v1 compatibility information type V2S1History struct { // V1Compatibility is the raw v1 compatibility information V1Compatibility string `json:"v1Compatibility"` } // github.com/docker/distribution/manifest/schema1/manifest.go // V2S1Manifest provides the base accessible fields for working with V2 image // format in the registry. type V2S1Manifest struct { V2Versioned // Name is the name of the image's repository Name string `json:"name"` // Tag is the tag of the image specified by this manifest Tag string `json:"tag"` // Architecture is the host architecture on which this image is intended to // run Architecture string `json:"architecture"` // FSLayers is a list of filesystem layer blobSums contained in this image FSLayers []V2S1FSLayer `json:"fsLayers"` // History is a list of unstructured historical data for v1 compatibility History []V2S1History `json:"history"` } // github.com/docker/distribution/blobs.go // V2S2Descriptor describes targeted content. Used in conjunction with a blob // store, a descriptor can be used to fetch, store and target any kind of // blob. The struct also describes the wire protocol format. Fields should // only be added but never changed. type V2S2Descriptor struct { // MediaType describe the type of the content. All text based formats are // encoded as utf-8. MediaType string `json:"mediaType,omitempty"` // Size in bytes of content. Size int64 `json:"size,omitempty"` // Digest uniquely identifies the content. A byte stream can be verified // against against this digest. Digest digest.Digest `json:"digest,omitempty"` // URLs contains the source URLs of this content. URLs []string `json:"urls,omitempty"` // NOTE: Before adding a field here, please ensure that all // other options have been exhausted. Much of the type relationships // depend on the simplicity of this type. } // github.com/docker/distribution/manifest/schema2/manifest.go // V2S2Manifest defines a schema2 manifest. type V2S2Manifest struct { V2Versioned // Config references the image configuration as a blob. Config V2S2Descriptor `json:"config"` // Layers lists descriptors for the layers referenced by the // configuration. Layers []V2S2Descriptor `json:"layers"` } const ( // github.com/moby/buildkit/exporter/containerimage/exptypes/types.go ExporterImageDigestKey = "containerimage.digest" ExporterImageConfigDigestKey = "containerimage.config.digest" ExporterImageDescriptorKey = "containerimage.descriptor" ) ================================================ FILE: docs/Makefile ================================================ PREFIX := /usr/local DATADIR := ${PREFIX}/share MANDIR := $(DATADIR)/man # Following go-md2man is guaranteed on host GOMD2MAN ?= ../tests/tools/build/go-md2man ifeq ($(shell uname -s),FreeBSD) SED=gsed else SED=sed endif docs: $(patsubst %.md,%,$(wildcard *.md)) %.1: %.1.md ### sed is used to filter http/s links as well as relative links ### replaces "\" at the end of a line with two spaces ### this ensures that manpages are rendered correctly @$(SED) -e 's/\((buildah[^)]*\.md\(#.*\)\?)\)//g' \ -e 's/\[\(buildah[^]]*\)\]/\1/g' \ -e 's/\[\([^]]*\)](http[^)]\+)/\1/g' \ -e 's;<\(/\)\?\(a\|a\s\+[^>]*\|sup\)>;;g' \ -e 's/\\$$/ /g' $< | \ $(GOMD2MAN) -in /dev/stdin -out $@ .PHONY: install install: install -d ${DESTDIR}/${MANDIR}/man1 install -m 0644 buildah*.1 ${DESTDIR}/${MANDIR}/man1 install -m 0644 links/buildah*.1 ${DESTDIR}/${MANDIR}/man1 .PHONY: clean clean: $(RM) buildah*.1 ================================================ FILE: docs/buildah-add.1.md ================================================ # buildah-add "1" "April 2021" "buildah" ## NAME buildah\-add - Add the contents of a file, URL, or a directory to a container. ## SYNOPSIS **buildah add** [*options*] *container* *src* [[*src* ...] *dest*] ## DESCRIPTION Adds the contents of a file, URL, or a directory to a container's working directory or a specified location in the container. If a local source file appears to be an archive, its contents are extracted and added instead of the archive file itself. If a local directory is specified as a source, its *contents* are copied to the destination. ## OPTIONS **--add-history** Add an entry to the history which will note the digest of the added content. Defaults to false. Note: You can also override the default value of --add-history by setting the BUILDAH\_HISTORY environment variable. `export BUILDAH_HISTORY=true` **--cert-dir** *path* Use certificates at *path* (\*.crt, \*.cert, \*.key) when connecting to registries for pulling images named with the **--from** flag, and when connecting to HTTPS servers when fetching sources from locations specified with HTTPS URLs. The default certificates directory is _/etc/containers/certs.d_. **--checksum** *checksum* Checksum the source content. The value of *checksum* must be a standard container digest string. Only supported for HTTP sources. **--chmod** *permissions* Sets the access permissions of the destination content. Accepts the numerical format. **--chown** *owner*:*group* Sets the user and group ownership of the destination content. **--contextdir** *directory* Build context directory. Specifying a context directory causes Buildah to chroot into that context directory. This means copying files pointed at by symbolic links outside of the chroot will fail. **--exclude** *pattern* Exclude copying files matching the specified pattern. Option can be specified multiple times. See containerignore(5) for supported formats. **--from** *containerOrImage* Use the root directory of the specified working container or image as the root directory when resolving absolute source paths and the path of the context directory. If an image needs to be pulled, options recognized by `buildah pull` can be used. **--ignorefile** *file* Path to an alternative .containerignore (.dockerignore) file. Requires \-\-contextdir be specified. **--link** Create an independent image layer for the added files instead of modifying the working container's filesystem. If `buildah run` creates a file and `buildah add --link` adds a file to the same path, the file from `buildah add --link` will be present in the committed image. The --link layer is applied after all container filesystem changes at commit time. **--quiet**, **-q** Refrain from printing a digest of the added content. **--retry** *attempts* Number of times to retry in case of failure when pulling images from registries or retrieving content from HTTPS URLs. Defaults to `3`. **--retry-delay** *duration* Duration of delay between retry attempts in case of failure when pulling images from registries or retrieving content from HTTPS URLs. Defaults to `2s`. **--timestamp** *seconds* Set the timestamp ("mtime") for added content to exactly this number of seconds since the epoch (Unix time 0, i.e., 00:00:00 UTC on 1 January 1970) to help allow for deterministic builds. The destination directory into which the content is being copied will most likely reflect the time at which the content was added to it. **--tls-verify** *bool-value* Require verification of certificates when retrieving sources from HTTPS locations, or when pulling images referred to with the **--from*** flag (defaults to true). TLS verification cannot be used when talking to an insecure registry. ## EXAMPLE buildah add containerID '/myapp/app.conf' '/myapp/app.conf' buildah add --chown myuser:mygroup containerID '/myapp/app.conf' '/myapp/app.conf' buildah add --chmod 660 containerID '/myapp/app.conf' '/myapp/app.conf' buildah add containerID '/home/myuser/myproject.go' buildah add containerID '/home/myuser/myfiles.tar' '/tmp' buildah add containerID '/tmp/workingdir' '/tmp/workingdir' buildah add containerID 'https://github.com/containers/buildah/blob/main/README.md' '/tmp' buildah add containerID 'passwd' 'certs.d' /etc ## FILES ### .containerignore or .dockerignore If a .containerignore or .dockerignore file exists in the context directory, `buildah add` reads its contents. If both exist, then .containerignore is used. When the `--ignorefile` option is specified Buildah reads it and uses it to decide which content to exclude when copying content into the working container. Users can specify a series of Unix shell glob patterns in an ignore file to identify files/directories to exclude. Buildah supports a special wildcard string `**` which matches any number of directories (including zero). For example, **/*.go will exclude all files that end with .go that are found in all directories. Example .containerignore/.dockerignore file: ``` # here are files we want to exclude */*.c **/output* src ``` `*/*.c` Excludes files and directories whose names end with .c in any top level subdirectory. For example, the source file include/rootless.c. `**/output*` Excludes files and directories starting with `output` from any directory. `src` Excludes files named src and the directory src as well as any content in it. Lines starting with ! (exclamation mark) can be used to make exceptions to exclusions. The following is an example .containerignore file that uses this mechanism: ``` *.doc !Help.doc ``` Exclude all doc files except Help.doc when copying content into the container. This functionality is compatible with the handling of .containerignore files described here: https://github.com/containers/common/blob/main/docs/containerignore.5.md ## SEE ALSO buildah(1), containerignore(5) ================================================ FILE: docs/buildah-build.1.md ================================================ # buildah-build "1" "April 2017" "buildah" ## NAME buildah\-build - Build an image using instructions from Containerfiles ## SYNOPSIS **buildah build** [*options*] [*context*] **buildah bud** [*options*] [*context*] ## DESCRIPTION Builds an image using instructions from one or more Containerfiles or Dockerfiles and a specified build context directory. A Containerfile uses the same syntax as a Dockerfile internally. For this document, a file referred to as a Containerfile can be a file named either 'Containerfile' or 'Dockerfile'. The build context directory can be specified as the http(s) URL of an archive, git repository or Containerfile. If no context directory is specified, then Buildah will assume the current working directory as build context, which should contain a Containerfile. Containerfiles ending with a ".in" suffix will be preprocessed via cpp(1). This can be useful to decompose Containerfiles into several reusable parts that can be used via CPP's **#include** directive. Notice, a Containerfile.in file can still be used by other tools when manually preprocessing them via `cpp -E`. Any comments ( Lines beginning with `#` ) in included Containerfile(s) that are not preprocess commands, will be printed as warnings during builds. When the URL is an archive, the contents of the URL is downloaded to a temporary location and extracted before execution. When the URL is a Containerfile, the file is downloaded to a temporary location. When a Git repository is set as the URL, the repository is cloned locally and then used as the build context. A non-default branch (or commit ID) and subdirectory of the cloned git repository can be used by including their names at the end of the URL in the form `myrepo.git#mybranch:subdir`, `myrepo.git#mycommit:subdir`, or `myrepo.git#:subdir` if the subdirectory should be used from the default branch. ## OPTIONS **--add-host**=[] Add a custom host-to-IP mapping (host:ip) Add a line to /etc/hosts. The format is hostname:ip. The **--add-host** option can be set multiple times. Conflicts with the --no-hosts option. Instead of an IP address, the special flag host-gateway can be given. This resolves to an IP address the container can use to connect to the host. The IP address chosen depends on your network setup, thus there's no guarantee that Buildah can determine the host-gateway address automatically, which will then cause Buildah to fail with an error message. You can overwrite this IP address using the host_containers_internal_ip option in containers.conf. The host-gateway address is also used by Buildah to automatically add the host.containers.internal and host.docker.internal hostnames to /etc/hosts. You can prevent that by either giving the --no-hosts option, or by setting host_containers_internal_ip="none" in containers.conf. If no host-gateway address was configured manually and Buildah fails to determine the IP address automatically, Buildah will silently skip adding these internal hostnames to /etc/hosts. If Buildah is running in a virtual machine using podman machine (this includes Mac and Windows hosts), Buildah will silently skip adding the internal hostnames to /etc/hosts, unless an IP address was configured manually; the internal hostnames are resolved by the gvproxy DNS resolver instead. Buildah will use the /etc/hosts file of the host as a basis by default, i.e. any hostname present in this file will also be present in the /etc/hosts file of the container. A different base file can be configured using the base_hosts_file config in containers.conf **--all-platforms** Instead of building for a set of platforms specified using the **--platform** option, inspect the build's base images, and build for all of the platforms for which they are all available. Stages that use *scratch* as a starting point can not be inspected, so at least one non-*scratch* stage must be present for detection to work usefully. **--annotation** *annotation[=value]* Add an image *annotation* (e.g. annotation=*value*) to the image metadata. Can be used multiple times. If *annotation* is named, but neither `=` nor a `value` is provided, then the *annotation* is set to an empty value. Note: this information is not present in Docker image formats, so it is discarded when writing images in Docker formats. **--arch**="ARCH" Set the ARCH of the image to be built, and that of the base image to be pulled, if the build uses one, to the provided value instead of using the architecture of the host. (Examples: arm, arm64, 386, amd64, ppc64le, s390x) **--authfile** *path* Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json. See containers-auth.json(5) for more information. This file is created using `buildah login`. If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using `docker login`. Note: You can also override the default path of the authentication file by setting the REGISTRY\_AUTH\_FILE environment variable. `export REGISTRY_AUTH_FILE=path` **--build-arg** *arg=value* Specifies a build argument and its value, which will be interpolated in instructions read from the Containerfiles in the same way that environment variables are, but which will not be added to environment variable list in the resulting image's configuration. Please refer to the [BUILD TIME VARIABLES](#build-time-variables) section for the list of variables that can be overridden within the Containerfile at run time. **--build-arg-file** *path* Specifies a file containing lines of build arguments of the form arg=value. The suggested file name is argfile.conf. Comment lines beginning with `#` are ignored, along with blank lines. All others should be of the `arg=value` format passed to `--build-arg`. If several arguments are provided via the `--build-arg-file` and `--build-arg` options, the build arguments will be merged across all of the provided files and command line arguments. Any file provided in a `--build-arg-file` option will be read before the arguments supplied via the `--build-arg` option. When a given argument name is specified several times, the last instance is the one that is passed to the resulting builds. This means `--build-arg` values always override those in a `--build-arg-file`. **--build-context** *name=value* Specify an additional build context using its short name and its location. Additional build contexts can be referenced in the same manner as we access different stages in `COPY` instruction. Valid values could be: * Local directory – e.g. --build-context project2=../path/to/project2/src * HTTP URL to a tarball – e.g. --build-context src=https://example.org/releases/src.tar * Container image – specified with a container-image:// prefix, e.g. --build-context alpine=container-image://alpine:3.15, (also accepts docker://, docker-image://) On the Containerfile side, you can reference the build context on all commands that accept the “from” parameter. Here’s how that might look: ```Dockerfile FROM [name] COPY --from=[name] ... RUN --mount=from=[name] … ``` The value of `[name]` is matched with the following priority order: * Named build context defined with --build-context [name]=.. * Stage defined with AS [name] inside Containerfile * Image [name], either local or in a remote registry **--cache-from** Repository to utilize as a potential list of cache sources. When specified, Buildah will try to look for cache images in the specified repositories and will attempt to pull cache images instead of actually executing the build steps locally. Buildah will only attempt to pull previously cached images if they are considered as valid cache hits. Use the `--cache-to` option to populate a remote repository or repositories with cache content. Example ```bash # populate a cache and also consult it buildah build -t test --layers --cache-to registry/myrepo/cache --cache-from registry/myrepo/cache . ``` Note: `--cache-from` option is ignored unless `--layers` is specified. Note: Buildah's `--cache-from` option is designed differently than Docker and BuildKit's `--cache-from` option. Buildah's distributed cache mechanism pulls intermediate images from the remote registry itself, unlike Docker and BuildKit where the intermediate image is stored in the image itself. Buildah's approach is similar to kaniko, which does not inflate the size of the original image with intermediate images. Also, intermediate images can truly be kept distributed across one or more remote registries using Buildah's caching mechanism. **--cache-to** Set this flag to specify list of remote repositories that will be used to store cache images. Buildah will attempt to push newly built cache image to the remote repositories. Note: Use the `--cache-from` option in order to use cache content in a remote repository. Example ```bash # populate a cache and also consult it buildah build -t test --layers --cache-to registry/myrepo/cache --cache-from registry/myrepo/cache . ``` Note: `--cache-to` option is ignored unless `--layers` is specified. Note: Buildah's `--cache-to` option is designed differently than Docker and BuildKit's `--cache-to` option. Buildah's distributed cache mechanism push intermediate images to the remote registry itself, unlike Docker and BuildKit where the intermediate image is stored in the image itself. Buildah's approach is similar to kaniko, which does not inflate the size of the original image with intermediate images. Also, intermediate images can truly be kept distributed across one or more remote registries using Buildah's caching mechanism. **--cache-ttl** *duration* Limit the use of cached images to only consider images with created timestamps less than *duration* ago. For example if `--cache-ttl=1h` is specified, Buildah will only consider intermediate cache images which are created under the duration of one hour, and intermediate cache images outside this duration will be ignored. Note: Setting `--cache-ttl=0` manually is equivalent to using `--no-cache` in the implementation since this would effectively mean that user is not willing to use cache at all. **--cap-add**=*CAP\_xxx* When executing RUN instructions, run the command specified in the instruction with the specified capability added to its capability set. Certain capabilities are granted by default; this option can be used to add more. **--cap-drop**=*CAP\_xxx* When executing RUN instructions, run the command specified in the instruction with the specified capability removed from its capability set. The CAP\_CHOWN, CAP\_DAC\_OVERRIDE, CAP\_FOWNER, CAP\_FSETID, CAP\_KILL, CAP\_NET\_BIND\_SERVICE, CAP\_SETFCAP, CAP\_SETGID, CAP\_SETPCAP, and CAP\_SETUID capabilities are granted by default; this option can be used to remove them. The list of default capabilities is managed in containers.conf(5). If a capability is specified to both the **--cap-add** and **--cap-drop** options, it will be dropped, regardless of the order in which the options were given. **--cert-dir** *path* Use certificates at *path* (\*.crt, \*.cert, \*.key) to connect to the registry and retrieve contents from HTTPS locations for ADD instructions. The default certificates directory is _/etc/containers/certs.d_. **--cgroup-parent**="" Path to cgroups under which the cgroup for RUN instructions will be created. If the path is not absolute, the path is considered to be relative to the cgroups path of the init process. Cgroups will be created if they do not already exist. **--cgroupns** *how* Sets the configuration for cgroup namespaces when handling `RUN` instructions. The configured value can be "" (the empty string) or "private" to indicate that a new cgroup namespace should be created, or it can be "host" to indicate that the cgroup namespace in which `buildah` itself is being run should be reused. **--compat-volumes** Handle directories marked using the VOLUME instruction (both in this build, and those inherited from base images) such that their contents can only be modified by ADD and COPY instructions. Any changes made in those locations by RUN instructions will be reverted. Before the introduction of this option, this behavior was the default, but it is now disabled by default. **--compress** This option is added to be aligned with other containers CLIs. Buildah doesn't send a copy of the context directory to a daemon or a remote server. Thus, compressing the data before sending it is irrelevant to Buildah. **--cpp-flag**="" Set additional flags to pass to the C Preprocessor cpp(1). Containerfiles ending with a ".in" suffix will be preprocessed via cpp(1). This option can be used to pass additional flags to cpp. Note: You can also set default CPPFLAGS by setting the BUILDAH\_CPPFLAGS environment variable (e.g., `export BUILDAH_CPPFLAGS="-DDEBUG"`). **--cpu-period**=*0* Set the CPU period for the Completely Fair Scheduler (CFS), which is a duration in microseconds. Once the container's CPU quota is used up, it will not be scheduled to run until the current period ends. Defaults to 100000 microseconds. On some systems, changing the CPU limits may not be allowed for non-root users. For more details, see https://github.com/containers/podman/blob/main/troubleshooting.md#26-running-containers-with-cpu-limits-fails-with-a-permissions-error **--cpu-quota**=*0* Limit the CPU CFS (Completely Fair Scheduler) quota Limit the container's CPU usage. By default, containers run with the full CPU resource. This flag tells the kernel to restrict the container's CPU usage to the quota you specify. On some systems, changing the CPU limits may not be allowed for non-root users. For more details, see https://github.com/containers/podman/blob/main/troubleshooting.md#26-running-containers-with-cpu-limits-fails-with-a-permissions-error **--cpu-shares**, **-c**=*0* CPU shares (relative weight) By default, all containers get the same proportion of CPU cycles. This proportion can be modified by changing the container's CPU share weighting relative to the weighting of all other running containers. To modify the proportion from the default of 1024, use the **--cpu-shares** flag to set the weighting to 2 or higher. The proportion will only apply when CPU-intensive processes are running. When tasks in one container are idle, other containers can use the left-over CPU time. The actual amount of CPU time will vary depending on the number of containers running on the system. For example, consider three containers, one has a cpu-share of 1024 and two others have a cpu-share setting of 512. When processes in all three containers attempt to use 100% of CPU, the first container would receive 50% of the total CPU time. If you add a fourth container with a cpu-share of 1024, the first container only gets 33% of the CPU. The remaining containers receive 16.5%, 16.5% and 33% of the CPU. On a multi-core system, the shares of CPU time are distributed over all CPU cores. Even if a container is limited to less than 100% of CPU time, it can use 100% of each individual CPU core. For example, consider a system with more than three cores. If you start one container **{C0}** with **-c=512** running one process, and another container **{C1}** with **-c=1024** running two processes, this can result in the following division of CPU shares: PID container CPU CPU share 100 {C0} 0 100% of CPU0 101 {C1} 1 100% of CPU1 102 {C1} 2 100% of CPU2 **--cpuset-cpus**="" CPUs in which to allow execution (0-3, 0,1) **--cpuset-mems**="" Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems. If you have four memory nodes on your system (0-3), use `--cpuset-mems=0,1` then processes in your container will only use memory from the first two memory nodes. **--created-annotation** Add an image *annotation* (see also **--annotation**) to the image metadata setting "org.opencontainers.image.created" to the current time, or to the datestamp specified to the **--source-date-epoch** or **--timestamp** flag, if either was used. If *false*, no such annotation will be present in the written image. Note: this information is not present in Docker image formats, so it is discarded when writing images in Docker formats. **--creds** *creds* The [username[:password]] to use to authenticate with the registry if required. If one or both values are not supplied, a command line prompt will appear and the value can be entered. The password is entered without echo. **--cw** *options* Produce an image suitable for use as a confidential workload running in a trusted execution environment (TEE) using krun (i.e., *crun* built with the libkrun feature enabled and invoked as *krun*). Instead of the conventional contents, the root filesystem of the image will contain an encrypted disk image and configuration information for krun. The value for *options* is a comma-separated list of key=value pairs, supplying configuration information which is needed for producing the additional data which will be included in the container image. Recognized _keys_ are: *attestation_url*: The location of a key broker / attestation server. If a value is specified, the new image's workload ID, along with the passphrase used to encrypt the disk image, will be registered with the server, and the server's location will be stored in the container image. At run-time, krun is expected to contact the server to retrieve the passphrase using the workload ID, which is also stored in the container image. If no value is specified, a *passphrase* value *must* be specified. *cpus*: The number of virtual CPUs which the image expects to be run with at run-time. If not specified, a default value will be supplied. *firmware_library*: The location of the libkrunfw-sev shared library. If not specified, `buildah` checks for its presence in a number of hard-coded locations. *memory*: The amount of memory which the image expects to be run with at run-time, as a number of megabytes. If not specified, a default value will be supplied. *passphrase*: The passphrase to use to encrypt the disk image which will be included in the container image. If no value is specified, but an *attestation_url* value is specified, a randomly-generated passphrase will be used. The authors recommend setting an *attestation_url* but not a *passphrase*. *slop*: Extra space to allocate for the disk image compared to the size of the container image's contents, expressed either as a percentage (..%) or a size value (bytes, or larger units if suffixes like KB or MB are present), or a sum of two or more such specifications. If not specified, `buildah` guesses that 25% more space than the contents will be enough, but this option is provided in case its guess is wrong. *type*: The type of trusted execution environment (TEE) which the image should be marked for use with. Accepted values are "SEV" (AMD Secure Encrypted Virtualization - Encrypted State) and "SNP" (AMD Secure Encrypted Virtualization - Secure Nested Paging). If not specified, defaults to "SNP". *workload_id*: A workload identifier which will be recorded in the container image, to be used at run-time for retrieving the passphrase which was used to encrypt the disk image. If not specified, a semi-random value will be derived from the base image's image ID. **--decryption-key** *key[:passphrase]* The [key[:passphrase]] to be used for decryption of images. Key can point to keys and/or certificates. Decryption will be tried with all keys. If the key is protected by a passphrase, it is required to be passed in the argument and omitted otherwise. **--device**=*device* Add a host device, or devices under a directory, to the environment of any **RUN** instructions run during the build. The optional *permissions* parameter can be used to specify device permissions, using any one or more of **r** for read, **w** for write, and **m** for **mknod**(2). Example: **--device=/dev/sdc:/dev/xvdc:rwm**. Note: if _host-device_ is a symbolic link then it will be resolved first. The container will only store the major and minor numbers of the host device. The device to share can also be specified using a Container Device Interface (CDI) specification (https://github.com/cncf-tags/container-device-interface). Note: if the user only has access rights via a group, accessing the device from inside a rootless container will fail. The **crun**(1) runtime offers a workaround for this by adding the option **--annotation run.oci.keep_original_groups=1**. **--disable-compression**, **-D** Don't compress filesystem layers when building the image unless it is required by the location where the image is being written. This is the default setting, because image layers are compressed automatically when they are pushed to registries, and images being written to local storage would only need to be decompressed again to be stored. Compression can be forced in all cases by specifying **--disable-compression=false**. **--disable-content-trust** This is a Docker specific option to disable image verification to a Container registry and is not supported by Buildah. This flag is a NOOP and provided solely for scripting compatibility. **--dns**=[] Set custom DNS servers. Invalid if using **--dns** with **--network=none**. This option can be used to override the DNS configuration passed to the container. Typically this is necessary when the host DNS configuration is invalid for the container (e.g., 127.0.0.1). When this is the case the `--dns` flag is necessary for every run. The special value **none** can be specified to disable creation of /etc/resolv.conf in the container by Buildah. The /etc/resolv.conf file in the image will be used without changes. **--dns-option**=[] Set custom DNS options. Invalid if using **--dns-option** with **--network=none**. **--dns-search**=[] Set custom DNS search domains. Invalid if using **--dns-search** with **--network=none**. **--env** *env[=value]* Add a value (e.g. env=*value*) to the built image. Can be used multiple times. If neither `=` nor a `*value*` are specified, but *env* is set in the current environment, the value from the current environment will be added to the image. The value of *env* can be overridden by ENV instructions in the Containerfile. To remove an environment variable from the built image, use the `--unsetenv` option. **--file**, **-f** *Containerfile* Specifies a Containerfile which contains instructions for building the image, either a local file or an **http** or **https** URL. If more than one Containerfile is specified, *FROM* instructions will only be accepted from the last specified file. If a local file is specified as the Containerfile and it does not exist, the context directory will be prepended to the local file value. If you specify `-f -`, the Containerfile contents will be read from stdin. **--force-rm** *bool-value* Always remove intermediate containers after a build, even if the build fails (default false). **--format** Control the format for the built image's manifest and configuration data. Recognized formats include *oci* (OCI image-spec v1.0, the default) and *docker* (version 2, using schema format 2 for the manifest). Note: You can also override the default format by setting the BUILDAH\_FORMAT environment variable. `export BUILDAH_FORMAT=docker` **--from** Overrides the first `FROM` instruction within the Containerfile. If there are multiple FROM instructions in a Containerfile, only the first is changed. **--group-add**=*group* | *keep-groups* Assign additional groups to the primary user running within the container process. - `keep-groups` is a special flag that tells Buildah to keep the supplementary group access. Allows container to use the user's supplementary group access. If file systems or devices are only accessible by the rootless user's group, this flag tells the OCI runtime to pass the group access into the container. Currently only available with the `crun` OCI runtime. Note: `keep-groups` is exclusive, other groups cannot be specified with this flag. **--help**, **-h** Print usage statement **--hooks-dir** *path* Each `*.json` file in the path configures a hook for buildah build containers. For more details on the syntax of the JSON files and the semantics of hook injection, see oci-hooks(5). Buildah currently support both the 1.0.0 and 0.1.0 hook schemas, although the 0.1.0 schema is deprecated. This option may be set multiple times; paths from later options have higher precedence (oci-hooks(5) discusses directory precedence). For the annotation conditions, buildah uses any annotations set in the generated OCI configuration. For the bind-mount conditions, only mounts explicitly requested by the caller via --volume are considered. Bind mounts that buildah inserts by default (e.g. /dev/shm) are not considered. If --hooks-dir is unset for root callers, Buildah will currently default to /usr/share/containers/oci/hooks.d and /etc/containers/oci/hooks.d in order of increasing precedence. Using these defaults is deprecated, and callers should migrate to explicitly setting --hooks-dir. **--http-proxy**=true By default proxy environment variables are passed into the container if set for the buildah process. This can be disabled by setting the `--http-proxy` option to `false`. The environment variables passed in include `http_proxy`, `https_proxy`, `ftp_proxy`, `no_proxy`, and also the upper case versions of those. **--identity-label** *bool-value* Adds a label `io.buildah.version` with its value set to the version of buildah which built the image (default true unless `--timestamp` or `--source-date-epoch` is used). **--ignorefile** *file* Path to an alternative .containerignore (.dockerignore) file. **--iidfile** *ImageIDfile* Write the built image's ID to the file. When `--platform` is specified more than once, attempting to use this option will trigger an error. **--iidfile-raw** *ImageIDfile* Write the built image's ID to the file without the algorithm prefix (e.g., `sha256:`). When `--platform` is specified more than once, attempting to use this option will trigger an error. An alias `--raw-iidfile` is also available. **--inherit-annotations** *bool-value* Inherit the annotations from the base image or base stages. (default true). Use cases which set this flag to *false* may need to do the same for the **--created-annotation** flag. **--inherit-labels** *bool-value* Inherit the labels from the base image or base stages. (default true). **--ipc** *how* Sets the configuration for IPC namespaces when handling `RUN` instructions. The configured value can be "" (the empty string) or "container" to indicate that a new IPC namespace should be created, or it can be "host" to indicate that the IPC namespace in which `buildah` itself is being run should be reused, or it can be the path to an IPC namespace which is already in use by another process. **--isolation** *type* Controls what type of isolation is used for running processes as part of `RUN` instructions. Recognized types include *oci* (OCI-compatible runtime, the default), *rootless* (OCI-compatible runtime invoked using a modified configuration, with *--no-new-keyring* added to its *create* invocation, reusing the host's network and UTS namespaces, and creating private IPC, PID, mount, and user namespaces; the default for unprivileged users), and *chroot* (an internal wrapper that leans more toward chroot(1) than container technology, reusing the host's control group, network, IPC, and PID namespaces, and creating private mount and UTS namespaces, and creating user namespaces only when they're required for ID mapping). Note: You can also override the default isolation type by setting the BUILDAH\_ISOLATION environment variable. `export BUILDAH_ISOLATION=oci` **--jobs** *N* Run up to N concurrent stages in parallel. If the number of jobs is greater than 1, stdin will be read from /dev/null. If 0 is specified, then there is no limit on the number of jobs that run in parallel. **--label** *label[=value]* Add an image *label* (e.g. label=*value*) to the image metadata. Can be used multiple times. If *label* is named, but neither `=` nor a `value` is provided, then the *label* is set to an empty value. Users can set a special LABEL **io.containers.capabilities=CAP1,CAP2,CAP3** in a Containerfile that specifies the list of Linux capabilities required for the container to run properly. This label specified in a container image tells container engines, like Podman, which recognize this label to run the container with just these capabilities. The container engine launches the container with just the specified capabilities, as long as this list of capabilities is a subset of the default list. If the specified capabilities are not in the default set, container engines should print an error message and will run the container with the default capabilities. **--layer-label** *label[=value]* Add an intermediate image *label* (e.g. label=*value*) to the metadata in intermediate images, i.e., any images built for non-final stages and for non-final instructions in stages when **--layers** is **true**. It can be used multiple times. If *label* is named, but neither `=` nor a `value` is provided, then the *label* is set to an empty value. **--layers** *bool-value* Cache intermediate images during the build process (Default is `false`). Note: You can also override the default value of layers by setting the BUILDAH\_LAYERS environment variable. `export BUILDAH_LAYERS=true` **--logfile** *filename* Log output which would be sent to standard output and standard error to the specified file instead of to standard output and standard error. **--logsplit** *bool-value* If --logfile and --platform is specified following flag allows end-users to split log file for each platform into different files with naming convention as `${logfile}_${platform-os}_${platform-arch}`. **--manifest** *listName* Name of the manifest list to which the built image will be added. Creates the manifest list if it does not exist. This option is useful for building multi architecture images. If _listName_ does not include a registry name component, the registry name *localhost* will be prepended to the list name. **--memory**, **-m**="" Memory limit (format: [], where unit = b, k, m or g) Allows you to constrain the memory available to a container. If the host supports swap memory, then the **-m** memory setting can be larger than physical RAM. If a limit of 0 is specified (not using **-m**), the container's memory is not limited. The actual limit may be rounded up to a multiple of the operating system's page size (the value would be very large, that's millions of trillions). **--memory-swap**="LIMIT" A limit value equal to memory plus swap. Must be used with the **-m** (**--memory**) flag. The swap `LIMIT` should always be larger than **-m** (**--memory**) value. By default, the swap `LIMIT` will be set to double the value of --memory. The format of `LIMIT` is `[]`. Unit can be `b` (bytes), `k` (kilobytes), `m` (megabytes), or `g` (gigabytes). If you don't specify a unit, `b` is used. Set LIMIT to `-1` to enable unlimited swap. **--metadata-file** *MetadataFile* Write information about the built image to the named file. When `--platform` is specified more than once, attempting to use this option will trigger an error. **--mount** *mount-instruction* Adds this mount to each `RUN` command in a Containerfile before executing. For example: `buildah build --mount type=secret,id=mysecret ...` and a Containerfile entry of: `RUN cat /run/secrets/mysecret` Has the same effect as: `RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret` **--network**, **--net**=*mode* Sets the configuration for network namespaces when handling `RUN` instructions. Valid _mode_ values are: - **none**: no networking. Invalid if using **--dns**, **--dns-opt**, or **--dns-search**; - **host**: use the host network stack. Note: the host mode gives the container full access to local system services such as D-bus and is therefore considered insecure; - **ns:**_path_: path to a network namespace to join; - **private**: create a new namespace for the container (default) - **\**: Join the network with the given name or ID, e.g. use `--network mynet` to join the network with the name mynet. Only supported for rootful users. - **slirp4netns[:OPTIONS,...]**: use **slirp4netns**(1) to create a user network stack. This is the default for rootless containers. It is possible to specify these additional options, they can also be set with `network_cmd_options` in containers.conf: - **allow_host_loopback=true|false**: Allow slirp4netns to reach the host loopback IP (default is 10.0.2.2 or the second IP from slirp4netns cidr subnet when changed, see the cidr option below). The default is false. - **mtu=MTU**: Specify the MTU to use for this network. (Default is `65520`). - **cidr=CIDR**: Specify ip range to use for this network. (Default is `10.0.2.0/24`). - **enable_ipv6=true|false**: Enable IPv6. Default is true. (Required for `outbound_addr6`). - **outbound_addr=INTERFACE**: Specify the outbound interface slirp binds to (ipv4 traffic only). - **outbound_addr=IPv4**: Specify the outbound ipv4 address slirp binds to. - **outbound_addr6=INTERFACE**: Specify the outbound interface slirp binds to (ipv6 traffic only). - **outbound_addr6=IPv6**: Specify the outbound ipv6 address slirp binds to. - **pasta[:OPTIONS,...]**: use **pasta**(1) to create a user-mode networking stack. \ This is only supported in rootless mode. \ By default, IPv4 and IPv6 addresses and routes, as well as the pod interface name, are copied from the host. If port forwarding isn't configured, ports are forwarded dynamically as services are bound on either side (init namespace or container namespace). Port forwarding preserves the original source IP address. Options described in pasta(1) can be specified as comma-separated arguments. \ In terms of pasta(1) options, **--config-net** is given by default, in order to configure networking when the container is started, and **--no-map-gw** is also assumed by default, to avoid direct access from container to host using the gateway address. The latter can be overridden by passing **--map-gw** in the pasta-specific options (despite not being an actual pasta(1) option). \ Also, **-t none** and **-u none** are passed to disable automatic port forwarding based on bound ports. Similarly, **-T none** and **-U none** are given to disable the same functionality from container to host. \ Some examples: - **pasta:--map-gw**: Allow the container to directly reach the host using the gateway address. - **pasta:--mtu,1500**: Specify a 1500 bytes MTU for the _tap_ interface in the container. - **pasta:--ipv4-only,-a,10.0.2.0,-n,24,-g,10.0.2.2,--dns-forward,10.0.2.3,-m,1500,--no-ndp,--no-dhcpv6,--no-dhcp**, equivalent to default slirp4netns(1) options: disable IPv6, assign `10.0.2.0/24` to the `tap0` interface in the container, with gateway `10.0.2.3`, enable DNS forwarder reachable at `10.0.2.3`, set MTU to 1500 bytes, disable NDP, DHCPv6 and DHCP support. - **pasta:-I,tap0,--ipv4-only,-a,10.0.2.0,-n,24,-g,10.0.2.2,--dns-forward,10.0.2.3,--no-ndp,--no-dhcpv6,--no-dhcp**, equivalent to default slirp4netns(1) options with Podman overrides: same as above, but leave the MTU to 65520 bytes - **pasta:-t,auto,-u,auto,-T,auto,-U,auto**: enable automatic port forwarding based on observed bound ports from both host and container sides - **pasta:-T,5201**: enable forwarding of TCP port 5201 from container to host, using the loopback interface instead of the tap interface for improved performance **--no-cache** Do not use existing cached images for the container build. Build from the start with a new set of cached layers. **--no-hostname** Do not create the _/etc/hostname_ file in the container for RUN instructions. By default, Buildah manages the _/etc/hostname_ file, adding the container's own hostname. When the **--no-hostname** option is set, the image's _/etc/hostname_ will be preserved unmodified if it exists. **--no-hosts** Do not create the _/etc/hosts_ file in the container for RUN instructions. By default, Buildah manages _/etc/hosts_, adding the container's own IP address. **--no-hosts** disables this, and the image's _/etc/hosts_ will be preserved unmodified. Conflicts with the --add-host option. **--omit-history** *bool-value* Omit build history information in the built image. (default false). This option is useful for the cases where end users explicitly want to set `--omit-history` to omit the optional `History` from built images or when working with images built using build tools that do not include `History` information in their images. **--os**="OS" Set the OS of the image to be built, and that of the base image to be pulled, if the build uses one, instead of using the current operating system of the host. **--os-feature** *feature* Set the name of a required operating system *feature* for the image which will be built. By default, if the image is not based on *scratch*, the base image's required OS feature list is kept, if the base image specified any. This option is typically only meaningful when the image's OS is Windows. If *feature* has a trailing `-`, then the *feature* is removed from the set of required features which will be listed in the image. **--os-version** *version* Set the exact required operating system *version* for the image which will be built. By default, if the image is not based on *scratch*, the base image's required OS version is kept, if the base image specified one. This option is typically only meaningful when the image's OS is Windows, and is typically set in Windows base images, so using this option is usually unnecessary. **--output**, **-o**="" Additional output (format: type=local,dest=path) The --output (or -o) option supplements the default behavior of building a container image by allowing users to export the image's contents as files on the local filesystem, which can be useful for generating local binaries, code generation, etc. The value for --output is a comma-separated sequence of key=value pairs, defining the output type and options. Supported _keys_ are: **dest**: Destination for exported output. Can be set to `-` to indicate standard output, or to an absolute or relative path. **type**: Defines the type of output to be written. Must be one of the values listed below. Valid _type_ values are: **local**: write the resulting build files to a directory on the client-side. **tar**: write the resulting files as a single tarball (.tar). Alternatively, instead of a comma-separated sequence, the value of **--output** can be just the destination (in the `**dest**` format) (e.g. `--output some-path`, `--output -`), and the **type** will be inferred to be **tar** if the output destination is `-`, and **local** otherwise. Timestamps on the output contents will be set to exactly match the value specified using the **--timestamp** flag, or to exactly match the value specified for the **--source-date-epoch** flag, if either are specified. Note that the **--tag** option can also be used to write the image to any location described by `containers-transports(5)`. **--pid** *how* Sets the configuration for PID namespaces when handling `RUN` instructions. The configured value can be "" (the empty string) or "private" to indicate that a new PID namespace should be created, or it can be "host" to indicate that the PID namespace in which `buildah` itself is being run should be reused, or it can be the path to a PID namespace which is already in use by another process. **--platform**="OS/ARCH[/VARIANT]" Set the OS/ARCH of the built image (and its base image, if your build uses one) to the provided value instead of using the current operating system and architecture of the host (for example `linux/arm`, `linux/arm64`, `linux/amd64`). The `--platform` flag can be specified more than once, or given a comma-separated list of values as its argument. When more than one platform is specified, the `--manifest` option should be used instead of the `--tag` option. OS/ARCH pairs are those used by the Go Programming Language. In several cases the ARCH value for a platform differs from one produced by other tools such as the `arch` command. Valid OS and architecture name combinations are listed as values for $GOOS and $GOARCH at https://golang.org/doc/install/source#environment, and can also be found by running `go tool dist list`. The `buildah build` command allows building images for all Linux architectures, even non-native architectures. When building images for a different architecture, the `RUN` instructions require emulation software installed on the host provided by packages like `qemu-user-static`. Note: it is always preferred to build images on the native architecture if possible. **NOTE:** The `--platform` option may not be used in combination with the `--arch`, `--os`, or `--variant` options. **--pull** Pull image policy. If not specified, the default is **missing**. If an explicit **--pull** argument is provided without any value, use the **always** behavior. - **always**: Pull base and SBOM scanner images from the registries listed in registries.conf. Raise an error if a base or SBOM scanner image is not found in the registries, even if an image with the same name is present locally. - **missing**: SBOM scanner images only if they could not be found in the local containers storage. Raise an error if no image could be found and the pull fails. - **never**: Do not pull base and SBOM scanner images from registries, use only the local versions. Raise an error if the image is not present locally. - **newer**: Pull base and SBOM scanner images from the registries listed in registries.conf if newer. Raise an error if a base or SBOM scanner image is not found in the registries when image with the same name is not present locally. **--quiet**, **-q** Suppress output messages which indicate which instruction is being processed, and of progress when pulling images from a registry, and when writing the output image. **--retry** *attempts* Number of times to retry in case of failure when performing push/pull of images to/from registry. Defaults to `3`. **--retry-delay** *duration* Duration of delay between retry attempts in case of failure when performing push/pull of images to/from registry. Defaults to `2s`. **--rewrite-timestamp** When generating new layers for the image, ensure that no newly added content bears a timestamp later than the value used by the **--source-date-epoch** flag, if one was provided, by replacing any timestamps which are later than that value, with that value. **--rm** *bool-value* Remove intermediate containers after a successful build (default true). **--runtime** *path* The *path* to an alternate OCI-compatible runtime, which will be used to run commands specified by the **RUN** instruction. Default is `runc`, or `crun` when machine is configured to use cgroups V2. Note: You can also override the default runtime by setting the BUILDAH\_RUNTIME environment variable. `export BUILDAH_RUNTIME=/usr/bin/crun` **--runtime-flag** *flag* Adds global flags for the container runtime. To list the supported flags, please consult the manpages of the selected container runtime. Note: Do not pass the leading `--` to the flag. To pass the runc flag `--log-format json` to buildah build, the option given would be `--runtime-flag log-format=json`. **--save-stages** *bool-value* Preserve intermediate stage images instead of removing them after the build completes (Default is `false`). By default, Buildah removes intermediate stage images to save space. This option keeps those images, which can be useful for debugging multi-stage builds or for reusing intermediate stages in subsequent builds. `--save-stages` can be used with `--layers` and subsequent builds with `--layers` can use the preserved intermediate layers as cache. When combined with `--stage-labels`, all stage images (including the final image) will include metadata labels for easier identification and management. **--sbom** *preset* Generate SBOMs (Software Bills Of Materials) for the output image by scanning the working container and build contexts using the named combination of scanner image, scanner commands, and merge strategy. Must be specified with one or more of **--sbom-image-output**, **--sbom-image-purl-output**, **--sbom-output**, and **--sbom-purl-output**. Recognized presets, and the set of options which they equate to: - "syft", "syft-cyclonedx": --sbom-scanner-image=ghcr.io/anchore/syft --sbom-scanner-command="/syft scan -q dir:{ROOTFS} --output cyclonedx-json={OUTPUT}" --sbom-scanner-command="/syft scan -q dir:{CONTEXT} --output cyclonedx-json={OUTPUT}" --sbom-merge-strategy=merge-cyclonedx-by-component-name-and-version - "syft-spdx": --sbom-scanner-image=ghcr.io/anchore/syft --sbom-scanner-command="/syft scan -q dir:{ROOTFS} --output spdx-json={OUTPUT}" --sbom-scanner-command="/syft scan -q dir:{CONTEXT} --output spdx-json={OUTPUT}" --sbom-merge-strategy=merge-spdx-by-package-name-and-versioninfo - "trivy", "trivy-cyclonedx": --sbom-scanner-image=ghcr.io/aquasecurity/trivy --sbom-scanner-command="trivy filesystem -q {ROOTFS} --format cyclonedx --output {OUTPUT}" --sbom-scanner-command="trivy filesystem -q {CONTEXT} --format cyclonedx --output {OUTPUT}" --sbom-merge-strategy=merge-cyclonedx-by-component-name-and-version - "trivy-spdx": --sbom-scanner-image=ghcr.io/aquasecurity/trivy --sbom-scanner-command="trivy filesystem -q {ROOTFS} --format spdx-json --output {OUTPUT}" --sbom-scanner-command="trivy filesystem -q {CONTEXT} --format spdx-json --output {OUTPUT}" --sbom-merge-strategy=merge-spdx-by-package-name-and-versioninfo **--sbom-image-output** *path* When generating SBOMs, store the generated SBOM in the specified path in the output image. There is no default. **--sbom-image-purl-output** *path* When generating SBOMs, scan them for PURL ([package URL](https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst)) information, and save a list of found PURLs to the specified path in the output image. There is no default. **--sbom-merge-strategy** *method* If more than one **--sbom-scanner-command** value is being used, use the specified method to merge the output from later commands with output from earlier commands. Recognized values include: - cat Concatenate the files. - merge-cyclonedx-by-component-name-and-version Merge the "component" fields of JSON documents, ignoring values from documents when the combination of their "name" and "version" values is already present. Documents are processed in the order in which they are generated, which is the order in which the commands that generate them were specified. - merge-spdx-by-package-name-and-versioninfo Merge the "package" fields of JSON documents, ignoring values from documents when the combination of their "name" and "versionInfo" values is already present. Documents are processed in the order in which they are generated, which is the order in which the commands that generate them were specified. **--sbom-output** *file* When generating SBOMs, store the generated SBOM in the named file on the local filesystem. There is no default. **--sbom-purl-output** *file* When generating SBOMs, scan them for PURL ([package URL](https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst)) information, and save a list of found PURLs to the named file in the local filesystem. There is no default. **--sbom-scanner-command** *image* Generate SBOMs by running the specified command from the scanner image. If multiple commands are specified, they are run in the order in which they are specified. These text substitutions are performed: - {ROOTFS} The root of the built image's filesystem, bind mounted. - {CONTEXT} The build context and additional build contexts, bind mounted. - {OUTPUT} The name of a temporary output file, to be read and merged with others or copied elsewhere. **--sbom-scanner-image** *image* Generate SBOMs using the specified scanner image. **--secret**=**id=id[,src=*envOrFile*][,env=ENV][,type=file|env]** Pass secret information to be used in the Containerfile for building images in a safe way that will not end up stored in the final image, or be seen in other stages. The value of the secret will be read from an environment variable or file named by the "id" option, or named by the "src" option if it is specified, or from an environment variable specified by the "env" option. The secret will be mounted in the container at `/run/secrets/` by default. To later use the secret, use the --mount flag in a `RUN` instruction within a `Containerfile`: `RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret` The location of the secret in the container can be overridden using the "target", "dst", or "destination" option of the `RUN --mount` flag. `RUN --mount=type=secret,id=mysecret,target=/run/secrets/myothersecret cat /run/secrets/myothersecret` The secret may alternatively be exposed as an environment variable to the process: `RUN --mount=type=secret,id=mysecret,env=FOO sh -c 'echo "Hello $FOO"'` Note: changing the contents of secret files will not trigger a rebuild of layers that use said secrets. **--security-opt**=[] Security Options "apparmor=unconfined" : Turn off apparmor confinement for the container "apparmor=your-profile" : Set the apparmor confinement profile for the container "label=user:USER" : Set the label user for the container "label=role:ROLE" : Set the label role for the container "label=type:TYPE" : Set the label type for the container "label=level:LEVEL" : Set the label level for the container "label=disable" : Turn off label confinement for the container "mask=_/path/1:/path/2_": The paths to mask separated by a colon. A masked path cannot be accessed inside the container. "no-new-privileges" : Disable container processes from gaining additional privileges "seccomp=unconfined" : Turn off seccomp confinement for the container "seccomp=profile.json : JSON configuration for a seccomp filter "unmask=_ALL_ or _/path/1:/path/2_, or shell expanded paths (/proc/*): Paths to unmask separated by a colon. If set to **ALL**, it unmasks all the paths that are masked or made read-only by default. The default masked paths are **/proc/acpi, /proc/interrupts, /proc/kcore, /proc/keys, /proc/latency_stats, /proc/sched_debug, /proc/scsi, /proc/timer_list, /proc/timer_stats, /sys/devices/virtual/powercap, /sys/firmware**, and **/sys/fs/selinux**. The default paths that are read-only are **/proc/asound**, **/proc/bus**, **/proc/fs**, **/proc/irq**, **/proc/sys**, and **/proc/sysrq-trigger**. **--shm-size**="" Size of `/dev/shm`. The format is ``. `number` must be greater than `0`. Unit is optional and can be `b` (bytes), `k` (kilobytes), `m`(megabytes), or `g` (gigabytes). If you omit the unit, the system uses bytes. If you omit the size entirely, the system uses `64m`. **--sign-by** *fingerprint* Sign the built image using the GPG key that matches the specified fingerprint. **--skip-unused-stages** *bool-value* Skip stages in multi-stage builds which don't affect the target stage. (Default is `true`). **--source-date-epoch** *seconds* Set the "created" timestamp for the built image to this number of seconds since the epoch (Unix time 0, i.e., 00:00:00 UTC on 1 January 1970) (default is to use the value set in the `SOURCE_DATE_EPOCH` environment variable, or the current time if it is not set). The "created" timestamp is written into the image's configuration and manifest when the image is committed, so running the same build two different times will ordinarily produce images with different sha256 hashes, even if no other changes were made to the Containerfile and build context. When this flag is set, a `SOURCE_DATE_EPOCH` build arg will provide its value for a stage in which it is declared. When this flag is set, the image configuration's "created" timestamp is always set to the time specified, which should allow for identical images to be built at different times using the same set of inputs. When this flag is set, output written as specified to the **--output** flag will bear exactly the specified timestamp. Conflicts with the similar **--timestamp** flag, which also sets its specified time on the contents of new layers. **--source-policy-file** *pathname* Specifies the path to a BuildKit-compatible source policy JSON file. When specified, source references (e.g., base images in FROM instructions) are evaluated against the policy rules before being used. Source policies allow controlling which images can be used as base images and optionally converting image references (e.g., pinning tags to specific digests) without modifying Containerfiles. This is useful for enforcing organizational policies and ensuring build reproducibility. The policy file is a JSON document containing an array of rules. Each rule has: - **action**: The action to take when the rule matches. Valid actions are: - **ALLOW**: Explicitly allow the source (no transformation). - **DENY**: Block the source and fail the build. - **CONVERT**: Transform the source to a different reference specified in `updates`. - **selector**: Specifies which sources the rule applies to. - **identifier**: The source identifier to match (e.g., `docker-image://docker.io/library/alpine:latest`). - **matchType**: How to match the identifier. Valid types are `EXACT` and `WILDCARD` (supports `*` and `?` glob patterns). Defaults to `WILDCARD` if not specified. - **updates**: For `CONVERT` actions, specifies the replacement identifier. Rules are evaluated in order; the first matching rule wins. If no rule matches, the source is allowed by default. Note: Source policy CONVERT rules are processed after **--build-context** substitutions but before any substitutions specified in **containers-registries.conf(5)**. This provides multiple ways to override which base image is used for a particular stage, in order of precedence: `--build-context`, then source policy, then registries.conf. Example policy file that pins alpine:latest to a specific digest: ```json { "rules": [ { "action": "CONVERT", "selector": { "identifier": "docker-image://docker.io/library/alpine:latest" }, "updates": { "identifier": "docker-image://docker.io/library/alpine@sha256:..." } } ] } ``` Example policy file that denies all ubuntu images: ```json { "rules": [ { "action": "DENY", "selector": { "identifier": "docker-image://docker.io/library/ubuntu:*", "matchType": "WILDCARD" } } ] } ``` **--squash** Squash all layers, including those from base image(s), into one single layer. (Default is false). By default, Buildah preserves existing base-image layers and adds only one new layer on a build. The --layers option can be used to preserve intermediate build layers. **--ssh**=**default**|*id[=socket>|[,]* SSH agent socket or keys to expose to the build. The socket path can be left empty to use the value of `default=$SSH_AUTH_SOCK` To later use the ssh agent, use the --mount flag in a `RUN` instruction within a `Containerfile`: `RUN --mount=type=secret,id=id mycmd` **--stage-labels** *bool-value* Add metadata labels to all intermediate stage images of the multistage build, including the final image (Default is `false`). This option requires `--save-stages` to be enabled. When enabled, all intermediate stage images and final image will be labeled with: - `io.buildah.stage.name`: The stage alias (from `FROM ... AS alias`), or stage position if no alias is specified - `io.buildah.stage.base`: The base image used by this stage (pullspec or image ID when stage uses another stage as base) These labels make it easier to identify, query, and manage images from multi-stage builds. **--stdin** Pass stdin into the RUN containers. Sometimes commands being RUN within a Containerfile want to request information from the user. For example apt asking for a confirmation for install. Use --stdin to be able to interact from the terminal during the build. **--tag**, **-t** *imageName* Specifies the name which will be assigned to the resulting image if the build process completes successfully. If _imageName_ does not include a registry name component, the registry name *localhost* will be prepended to the image name. The **--tag** option supports all transports from `containers-transports(5)`. If no transport is specified, the `containers-storage` (i.e., local storage) transport is used. __buildah build --tag=oci-archive:./foo.ociarchive .__ __buildah build -t quay.io/username/foo .__ **--target** *stageName* Set the target build stage to build. When building a Containerfile with multiple build stages, --target can be used to specify an intermediate build stage by name as the final stage for the resulting image. Commands after the target stage will be skipped. **--timestamp** *seconds* Set the "created" timestamp for the built image to this number of seconds since the epoch (Unix time 0, i.e., 00:00:00 UTC on 1 January 1970) (defaults to current time). The "created" timestamp is written into the image's configuration and manifest when the image is committed, so running the same build two different times will ordinarily produce images with different sha256 hashes, even if no other changes were made to the Containerfile and build context. When --timestamp is set, the "created" timestamp is always set to the time specified, which should allow for identical images to be built at different times using the same set of inputs. When --timestamp is set, all content in layers created as part of the build, and output written as specified to the **--output** flag, will also bear this same timestamp. Conflicts with the similar **--source-date-epoch** flag, which by default does not affect the timestamps of layer contents. **--tls-verify** *bool-value* Require HTTPS and verification of certificates when talking to container registries (defaults to true) and retrieving content from HTTPS locations for ADD instructions. TLS verification cannot be used when talking to an insecure registry. **--ulimit** *type*=*soft-limit*[:*hard-limit*] Specifies resource limits to apply to processes launched when processing `RUN` instructions. This option can be specified multiple times. Recognized resource types include: "core": maximum core dump size (ulimit -c) "cpu": maximum CPU time (ulimit -t) "data": maximum size of a process's data segment (ulimit -d) "fsize": maximum size of new files (ulimit -f) "locks": maximum number of file locks (ulimit -x) "memlock": maximum amount of locked memory (ulimit -l) "msgqueue": maximum amount of data in message queues (ulimit -q) "nice": niceness adjustment (nice -n, ulimit -e) "nofile": maximum number of open files (ulimit -n) "nofile": maximum number of open files (1048576); when run by root "nproc": maximum number of processes (ulimit -u) "nproc": maximum number of processes (1048576); when run by root "rss": maximum size of a process's (ulimit -m) "rtprio": maximum real-time scheduling priority (ulimit -r) "rttime": maximum amount of real-time execution between blocking syscalls "sigpending": maximum number of pending signals (ulimit -i) "stack": maximum stack size (ulimit -s) **--unsetannotation** *annotation* Unset the image annotation, causing the annotation not to be inherited from the base image. **--unsetenv** *env* Unset environment variables from the final image. **--unsetlabel** *label* Unset the image label, causing the label not to be inherited from the base image. **--userns** *how* Sets the configuration for user namespaces when handling `RUN` instructions. The configured value can be "" (the empty string) , "private" or "auto" to indicate that a new user namespace should be created, it can be "host" to indicate that the user namespace in which `buildah` itself is being run should be reused, or it can be the path to an user namespace which is already in use by another process. auto: automatically create a unique user namespace. The --userns=auto flag, requires that the user name containers and a range of subordinate user ids that the build container is allowed to use be specified in the /etc/subuid and /etc/subgid files. Example: `containers:2147483647:2147483648`. Buildah allocates unique ranges of UIDs and GIDs from the containers subordinate user ids. The size of the ranges is based on the number of UIDs required in the image. The number of UIDs and GIDs can be overridden with the size option. Valid `auto` options: * gidmapping=CONTAINER_GID:HOST_GID:SIZE: to force a GID mapping to be present in the user namespace. * size=SIZE: to specify an explicit size for the automatic user namespace. e.g. --userns=auto:size=8192. If size is not specified, auto will estimate a size for the user namespace. * uidmapping=CONTAINER_UID:HOST_UID:SIZE: to force a UID mapping to be present in the user namespace. **--userns-gid-map** *mapping* Directly specifies a GID mapping which should be used to set ownership, at the filesystem level, on the working container's contents. Commands run when handling `RUN` instructions will default to being run in their own user namespaces, configured using the UID and GID maps. Entries in this map take the form of one or more colon-separated triples of a starting in-container GID, a corresponding starting host-level GID, and the number of consecutive IDs which the map entry represents. This option overrides the *remap-gids* setting in the *options* section of /etc/containers/storage.conf. If this option is not specified, but a global --userns-gid-map setting is supplied, settings from the global option will be used. **--userns-gid-map-group** *group* Specifies that a GID mapping which should be used to set ownership, at the filesystem level, on the working container's contents, can be found in entries in the `/etc/subgid` file which correspond to the specified group. Commands run when handling `RUN` instructions will default to being run in their own user namespaces, configured using the UID and GID maps. If --userns-uid-map-user is specified, but --userns-gid-map-group is not specified, `buildah` will assume that the specified user name is also a suitable group name to use as the default setting for this option. Users can specify the maps directly using `--userns-gid-map` described in the buildah(1) man page. **NOTE:** When this option is specified by a rootless user, the specified mappings are relative to the rootless usernamespace in the container, rather than being relative to the host as it would be when run rootful. **--userns-uid-map** *mapping* Directly specifies a UID mapping which should be used to set ownership, at the filesystem level, on the working container's contents. Commands run when handling `RUN` instructions will default to being run in their own user namespaces, configured using the UID and GID maps. Entries in this map take the form of one or more colon-separated triples of a starting in-container UID, a corresponding starting host-level UID, and the number of consecutive IDs which the map entry represents. This option overrides the *remap-uids* setting in the *options* section of /etc/containers/storage.conf. If this option is not specified, but a global --userns-uid-map setting is supplied, settings from the global option will be used. **--userns-uid-map-user** *user* Specifies that a UID mapping which should be used to set ownership, at the filesystem level, on the working container's contents, can be found in entries in the `/etc/subuid` file which correspond to the specified user. Commands run when handling `RUN` instructions will default to being run in their own user namespaces, configured using the UID and GID maps. If --userns-gid-map-group is specified, but --userns-uid-map-user is not specified, `buildah` will assume that the specified group name is also a suitable user name to use as the default setting for this option. **NOTE:** When this option is specified by a rootless user, the specified mappings are relative to the rootless usernamespace in the container, rather than being relative to the host as it would be when run rootful. **--uts** *how* Sets the configuration for UTS namespaces when handling `RUN` instructions. The configured value can be "" (the empty string) or "container" to indicate that a new UTS namespace should be created, or it can be "host" to indicate that the UTS namespace in which `buildah` itself is being run should be reused, or it can be the path to a UTS namespace which is already in use by another process. **--variant**="" Set the architecture variant of the image to be pulled. **--volume**, **-v**[=*[HOST-DIR:CONTAINER-DIR[:OPTIONS]]*] Mount a host directory into containers when executing *RUN* instructions during the build. The `OPTIONS` are a comma delimited list and can be: * [rw|ro] * [U] * [z|Z|O] * [`[r]shared`|`[r]slave`|`[r]private`] [[1]](#Footnote1) The `CONTAINER-DIR` must be an absolute path such as `/src/docs`. The `HOST-DIR` must be an absolute path as well. Buildah bind-mounts the `HOST-DIR` to the path you specify. For example, if you supply `/foo` as the host path, Buildah copies the contents of `/foo` to the container filesystem on the host and bind mounts that into the container. You can specify multiple **-v** options to mount one or more mounts to a container. `Write Protected Volume Mounts` You can add the `:ro` or `:rw` suffix to a volume to mount it read-only or read-write mode, respectively. By default, the volumes are mounted read-write. See examples. `Chowning Volume Mounts` By default, Buildah does not change the owner and group of source volume directories mounted into containers. If a container is created in a new user namespace, the UID and GID in the container may correspond to another UID and GID on the host. The `:U` suffix tells Buildah to use the correct host UID and GID based on the UID and GID within the container, to change the owner and group of the source volume. `Labeling Volume Mounts` Labeling systems like SELinux require that proper labels are placed on volume content mounted into a container. Without a label, the security system might prevent the processes running inside the container from using the content. By default, Buildah does not change the labels set by the OS. To change a label in the container context, you can add either of two suffixes `:z` or `:Z` to the volume mount. These suffixes tell Buildah to relabel file objects on the shared volumes. The `z` option tells Buildah that two containers share the volume content. As a result, Buildah labels the content with a shared content label. Shared volume labels allow all containers to read/write content. The `Z` option tells Buildah to label the content with a private unshared label. Only the current container can use a private volume. `Overlay Volume Mounts` The `:O` flag tells Buildah to mount the directory from the host as a temporary storage using the Overlay file system. The `RUN` command containers are allowed to modify contents within the mountpoint and are stored in the container storage in a separate directory. In Overlay FS terms the source directory will be the lower, and the container storage directory will be the upper. Modifications to the mount point are destroyed when the `RUN` command finishes executing, similar to a tmpfs mount point. Any subsequent execution of `RUN` commands sees the original source directory content, any changes from previous RUN commands no longer exist. One use case of the `overlay` mount is sharing the package cache from the host into the container to allow speeding up builds. Note: - The `O` flag is not allowed to be specified with the `Z` or `z` flags. Content mounted into the container is labeled with the private label. On SELinux systems, labels in the source directory must be readable by the container label. If not, SELinux container separation must be disabled for the container to work. - Modification of the directory volume mounted into the container with an overlay mount can cause unexpected failures. It is recommended that you do not modify the directory until the container finishes running. By default bind mounted volumes are `private`. That means any mounts done inside container will not be visible on the host and vice versa. This behavior can be changed by specifying a volume mount propagation property. When the mount propagation policy is set to `shared`, any mounts completed inside the container on that volume will be visible to both the host and container. When the mount propagation policy is set to `slave`, one way mount propagation is enabled and any mounts completed on the host for that volume will be visible only inside of the container. To control the mount propagation property of the volume use the `:[r]shared`, `:[r]slave` or `:[r]private` propagation flag. The propagation property can be specified only for bind mounted volumes and not for internal volumes or named volumes. For mount propagation to work on the source mount point (the mount point where source dir is mounted on) it has to have the right propagation properties. For shared volumes, the source mount point has to be shared. And for slave volumes, the source mount has to be either shared or slave. [[1]](#Footnote1) Use `df ` to determine the source mount and then use `findmnt -o TARGET,PROPAGATION ` to determine propagation properties of source mount, if `findmnt` utility is not available, the source mount point can be determined by looking at the mount entry in `/proc/self/mountinfo`. Look at `optional fields` and see if any propagation properties are specified. `shared:X` means the mount is `shared`, `master:X` means the mount is `slave` and if nothing is there that means the mount is `private`. [[1]](#Footnote1) To change propagation properties of a mount point use the `mount` command. For example, to bind mount the source directory `/foo` do `mount --bind /foo /foo` and `mount --make-private --make-shared /foo`. This will convert /foo into a `shared` mount point. The propagation properties of the source mount can be changed directly. For instance if `/` is the source mount for `/foo`, then use `mount --make-shared /` to convert `/` into a `shared` mount. ## BUILD TIME VARIABLES The ENV instruction in a Containerfile can be used to define variable values. When the image is built, the values will persist in the container image. At times it is more convenient to change the values in the Containerfile via a command-line option rather than changing the values within the Containerfile itself. The following variables can be used in conjunction with the `--build-arg` option to override the corresponding values set in the Containerfile using the `ENV` instruction. * HTTP_PROXY * HTTPS_PROXY * FTP_PROXY * NO_PROXY Please refer to the [Using Build Time Variables](#using-build-time-variables) section of the Examples. ## EXAMPLE ### Build an image using local Containerfiles buildah build . buildah build -f Containerfile . cat ~/Containerfile | buildah build -f - . buildah build -f Containerfile.simple -f Containerfile.notsosimple . buildah build --timestamp=$(date '+%s') -t imageName . buildah build -t imageName . buildah build --tls-verify=true -t imageName -f Containerfile.simple . buildah build --tls-verify=false -t imageName . buildah build --runtime-flag log-format=json . buildah build -f Containerfile --runtime-flag debug . buildah build --authfile /tmp/auths/myauths.json --cert-dir ~/auth --tls-verify=true --creds=username:password -t imageName -f Containerfile.simple . buildah build --memory 40m --cpu-period 10000 --cpu-quota 50000 --ulimit nofile=1024:1028 -t imageName . buildah build --security-opt label=level:s0:c100,c200 --cgroup-parent /path/to/cgroup/parent -t imageName . buildah build --arch=arm --variant v7 -t imageName . buildah build --volume /home/test:/myvol:ro,Z -t imageName . buildah build -v /home/test:/myvol:z,U -t imageName . buildah build -v /var/lib/dnf:/var/lib/dnf:O -t imageName . buildah build --layers -t imageName . buildah build --save-stages -t imageName . buildah build --save-stages --layers -t imageName . buildah build --save-stages --stage-labels -t imageName . buildah build --save-stages --stage-labels --layers -t imageName . buildah build --no-cache -t imageName . buildah build -f Containerfile --layers --force-rm -t imageName . buildah build --no-cache --rm=false -t imageName . buildah build --dns-search=example.com --dns=223.5.5.5 --dns-option=use-vc . buildah build -f Containerfile.in --cpp-flag="-DDEBUG" -t imageName . buildah build --network mynet . buildah build --env LANG=en_US.UTF-8 -t imageName . buildah build --env EDITOR -t imageName . buildah build --unsetenv LANG -t imageName . buildah build --os-version 10.0.19042.1645 -t imageName . buildah build --os-feature win32k -t imageName . buildah build --os-feature win32k- -t imageName . buildah build --secret=id=mysecret . buildah build --secret=id=mysecret,env=MYSECRET . buildah build --secret=id=mysecret,src=MYSECRET,type=env . buildah build --secret=id=mysecret,src=.mysecret,type=file . buildah build --secret=id=mysecret,src=.mysecret . ### Building an image with a source policy buildah build --source-policy-file /etc/buildah/source-policy.json -t imageName . ### Using FROM --after for explicit stage dependencies When using local transports like `FROM oci-archive:file.ociarchive` where the file is produced by an earlier stage, Buildah cannot automatically detect the dependency. Use the `--after` flag on the FROM instruction to declare explicit stage dependencies: ```Dockerfile FROM quay.io/skopeo/stable AS builder RUN --mount=type=bind,target=/src,rw skopeo copy docker://quay.io/fedora/fedora-minimal oci-archive:/src/fedora.ociarchive FROM --after=builder oci-archive:fedora.ociarchive # This stage will wait for builder to complete before evaluating FROM ``` ### Building an multi-architecture image using the --manifest option (requires emulation software) buildah build --arch arm --manifest myimage /tmp/mysrc buildah build --arch amd64 --manifest myimage /tmp/mysrc buildah build --arch s390x --manifest myimage /tmp/mysrc buildah bud --platform linux/s390x,linux/ppc64le,linux/amd64 --manifest myimage /tmp/mysrc buildah build --platform linux/arm64 --platform linux/amd64 --manifest myimage /tmp/mysrc buildah bud --all-platforms --manifest myimage /tmp/mysrc ### Building an image using (--output) custom build output buildah build -o out . buildah build --output type=local,dest=out . buildah build --output type=tar,dest=out.tar . buildah build -o - . > out.tar ### Preserving and querying intermediate stage images Build a multi-stage image while preserving intermediate stages with metadata labels: buildah build --save-stages --stage-labels -t myapp . Find an intermediate image for a specific stage name: buildah images --filter "label=io.buildah.stage.name=builder" Find an intermediate image for a specific stage base image: buildah images --filter "label=io.buildah.stage.base=golang:1.21" ### Building an image using a URL This will clone the specified GitHub repository from the URL and use it as context. The Containerfile or Dockerfile at the root of the repository is used as the context of the build. This only works if the GitHub repository is a dedicated repository. buildah build https://github.com/containers/PodmanHello.git Note: Github does not support using `git://` for performing `clone` operation due to recent changes in their security guidance (https://github.blog/2021-09-01-improving-git-protocol-security-github/). Use an `https://` URL if the source repository is hosted on Github. ### Building an image using a URL to a tarball'ed context Buildah will fetch the tarball archive, decompress it and use its contents as the build context. The Containerfile or Dockerfile at the root of the archive and the rest of the archive will get used as the context of the build. If you pass an -f PATH/Containerfile option as well, the system will look for that file inside the contents of the tarball. buildah build -f dev/Containerfile https://10.10.10.1/buildah/context.tar.gz Note: supported compression formats are 'xz', 'bzip2', 'gzip' and 'identity' (no compression). ### Using Build Time Variables #### Replace the value set for the HTTP_PROXY environment variable within the Containerfile. buildah build --build-arg=HTTP_PROXY="http://127.0.0.1:8321" ## ENVIRONMENT **BUILD\_REGISTRY\_SOURCES** BUILD\_REGISTRY\_SOURCES, if set, is treated as a JSON object which contains lists of registry names under the keys `insecureRegistries`, `blockedRegistries`, and `allowedRegistries`. When pulling an image from a registry, if the name of the registry matches any of the items in the `blockedRegistries` list, the image pull attempt is denied. If there are registries in the `allowedRegistries` list, and the registry's name is not in the list, the pull attempt is denied. **TMPDIR** The TMPDIR environment variable allows the user to specify where temporary files are stored while pulling and pushing images. Defaults to '/var/tmp'. ## Files ### `.containerignore`/`.dockerignore` If the .containerignore/.dockerignore file exists in the context directory, `buildah build` reads its contents. If both exist, then .containerignore is used. Use the `--ignorefile` flag to override the ignore file path location. Buildah uses the content to exclude files and directories from the context directory, when executing COPY and ADD directives in the Containerfile/Dockerfile Users can specify a series of Unix shell globals in a .containerignore/.dockerignore file to identify files/directories to exclude. Buildah supports a special wildcard string `**` which matches any number of directories (including zero). For example, `**/*.go` will exclude all files that end with .go that are found in all directories. Example .containerignore file: ``` # exclude this content for image */*.c **/output* src ``` `*/*.c` Excludes files and directories whose names end with .c in any top level subdirectory. For example, the source file include/rootless.c. `**/output*` Excludes files and directories starting with `output` from any directory. `src` Excludes files named src and the directory src as well as any content in it. Lines starting with ! (exclamation mark) can be used to make exceptions to exclusions. The following is an example .containerignore/.dockerignore file that uses this mechanism: ``` *.doc !Help.doc ``` Exclude all doc files except Help.doc from the image. This functionality is compatible with the handling of .containerignore files described here: https://github.com/containers/common/blob/main/docs/containerignore.5.md **registries.conf** (`/etc/containers/registries.conf`) registries.conf is the configuration file which specifies which container registries should be consulted when completing image names which do not include a registry or domain portion. **policy.json** (`/etc/containers/policy.json`) Signature policy file. This defines the trust policy for container images. Controls which container registries can be used for image, and whether or not the tool should trust the images. ## SEE ALSO buildah(1), cpp(1), buildah-login(1), docker-login(1), namespaces(7), pid\_namespaces(7), containers-policy.json(5), containers-registries.conf(5), user\_namespaces(7), crun(1), runc(8), containers.conf(5), oci-hooks(5), containers-transports(5), containers-auth.json(5) ## FOOTNOTES 1: The Buildah project is committed to inclusivity, a core value of open source. The `master` and `slave` mount propagation terminology used here is problematic and divisive, and should be changed. However, these terms are currently used within the Linux kernel and must be used as-is at this time. When the kernel maintainers rectify this usage, Buildah will follow suit immediately. ================================================ FILE: docs/buildah-commit.1.md ================================================ # buildah-commit "1" "March 2017" "buildah" ## NAME buildah\-commit - Create an image from a working container. ## SYNOPSIS **buildah commit** [*options*] *container* [*image*] ## DESCRIPTION Writes a new image using the specified container's read-write layer and if it is based on an image, the layers of that image. If *image* does not begin with a registry name component, `localhost` will be added to the name. If *image* is not provided, the image will have no name. When an image has no name, the `buildah images` command will display `` in the `REPOSITORY` and `TAG` columns. The *image* value supports all transports from `containers-transports(5)`. If no transport is specified, the `containers-storage` (i.e., local storage) transport is used. ## RETURN VALUE The image ID of the image that was created. On error, 1 is returned and errno is returned. ## OPTIONS **--add-file** *source[:destination]* Read the contents of the file `source` and add it to the committed image as a file at `destination`. If `destination` is not specified, the path of `source` will be used. The new file will be owned by UID 0, GID 0, have 0644 permissions, and be given the timestamp specified to the **--timestamp** option if it is specified. This option can be specified multiple times. **--annotation** *annotation[=value]* Add an image *annotation* (e.g. annotation=*value*) to the image metadata. Can be used multiple times. If *annotation* is named, but neither `=` nor a `value` is provided, then the *annotation* is set to an empty value. Note: this information is not present in Docker image formats, so it is discarded when writing images in Docker formats. **--authfile** *path* Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json. See containers-auth.json(5) for more information. This file is created using `buildah login`. If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using `docker login`. Note: You can also override the default path of the authentication file by setting the REGISTRY\_AUTH\_FILE environment variable. `export REGISTRY_AUTH_FILE=path` **--cert-dir** *path* Use certificates at *path* (\*.crt, \*.cert, \*.key) to connect to the registry. The default certificates directory is _/etc/containers/certs.d_. **--change**, **-c** *"INSTRUCTION"* Apply the change to the committed image that would have been made if it had been built using a Containerfile which included the specified instruction. This option can be specified multiple times. **--config** *filename* Read a JSON-encoded version of an image configuration object from the specified file, and merge the values from it with the configuration of the image being committed. **--created-annotation** Add an image *annotation* (see also **--annotation**) to the image metadata setting "org.opencontainers.image.created" to the current time, or to the datestamp specified to the **--source-date-epoch** or **--timestamp** flag, if either was used. If *false*, no such annotation will be present in the written image. Note: this information is not present in Docker image formats, so it is discarded when writing images in Docker formats. **--creds** *creds* The [username[:password]] to use to authenticate with the registry if required. If one or both values are not supplied, a command line prompt will appear and the value can be entered. The password is entered without echo. **--cw** *options* Produce an image suitable for use as a confidential workload running in a trusted execution environment (TEE) using krun (i.e., *crun* built with the libkrun feature enabled and invoked as *krun*). Instead of the conventional contents, the root filesystem of the image will contain an encrypted disk image and configuration information for krun. The value for *options* is a comma-separated list of key=value pairs, supplying configuration information which is needed for producing the additional data which will be included in the container image. Recognized _keys_ are: *attestation_url*: The location of a key broker / attestation server. If a value is specified, the new image's workload ID, along with the passphrase used to encrypt the disk image, will be registered with the server, and the server's location will be stored in the container image. At run-time, krun is expected to contact the server to retrieve the passphrase using the workload ID, which is also stored in the container image. If no value is specified, a *passphrase* value *must* be specified. *cpus*: The number of virtual CPUs which the image expects to be run with at run-time. If not specified, a default value will be supplied. *firmware_library*: The location of the libkrunfw-sev shared library. If not specified, `buildah` checks for its presence in a number of hard-coded locations. *memory*: The amount of memory which the image expects to be run with at run-time, as a number of megabytes. If not specified, a default value will be supplied. *passphrase*: The passphrase to use to encrypt the disk image which will be included in the container image. If no value is specified, but an *attestation_url* value is specified, a randomly-generated passphrase will be used. The authors recommend setting an *attestation_url* but not a *passphrase*. *slop*: Extra space to allocate for the disk image compared to the size of the container image's contents, expressed either as a percentage (..%) or a size value (bytes, or larger units if suffixes like KB or MB are present), or a sum of two or more such specifications separated by "+". If not specified, `buildah` guesses that 25% more space than the contents will be enough, but this option is provided in case its guess is wrong. If the specified or computed size is less than 10 megabytes, it will be increased to 10 megabytes. *type*: The type of trusted execution environment (TEE) which the image should be marked for use with. Accepted values are "SEV" (AMD Secure Encrypted Virtualization - Encrypted State) and "SNP" (AMD Secure Encrypted Virtualization - Secure Nested Paging). If not specified, defaults to "SNP". *workload_id*: A workload identifier which will be recorded in the container image, to be used at run-time for retrieving the passphrase which was used to encrypt the disk image. If not specified, a semi-random value will be derived from the base image's image ID. **--disable-compression**, **-D** Don't compress filesystem layers when building the image unless it is required by the location where the image is being written. This is the default setting, because image layers are compressed automatically when they are pushed to registries, and images being written to local storage would only need to be decompressed again to be stored. Compression can be forced in all cases by specifying **--disable-compression=false**. **--encrypt-layer** *layer(s)* Layer(s) to encrypt: 0-indexed layer indices with support for negative indexing (e.g. 0 is the first layer, -1 is the last layer). If not defined, will encrypt all layers if encryption-key flag is specified. **--encryption-key** *key* The [protocol:keyfile] specifies the encryption protocol, which can be JWE (RFC7516), PGP (RFC4880), and PKCS7 (RFC2315) and the key material required for image encryption. For instance, jwe:/path/to/key.pem or pgp:admin@example.com or pkcs7:/path/to/x509-file. **--format**, **-f** *[oci | docker]* Control the format for the image manifest and configuration data. Recognized formats include *oci* (OCI image-spec v1.0, the default) and *docker* (version 2, using schema format 2 for the manifest). Note: You can also override the default format by setting the BUILDAH_FORMAT environment variable. `export BUILDAH_FORMAT=docker` **--identity-label** *bool-value* Adds a label `io.buildah.version` with its value set to the version of buildah which committed the image (default true unless `--timestamp` or `--source-date-epoch` is used). **--iidfile** *ImageIDfile* Write the image ID to the file. **--manifest** "listName" Name of the manifest list to which the built image will be added. Creates the manifest list if it does not exist. This option is useful for building multi architecture images. **--metadata-file** *MetadataFile* Write information about the committed image to the named file. **--omit-history** *bool-value* Omit build history information in the built image. (default false). This option is useful for the cases where end users explicitly want to set `--omit-history` to omit the optional `History` from built images or when working with images built using build tools that do not include `History` information in their images. **--pull** When the *--pull* flag is enabled or set explicitly to `true` (with *--pull=true*), attempt to pull the latest versions of SBOM scanner images from the registries listed in registries.conf if a local SBOM scanner image does not exist or the image in the registry is newer than the one in local storage. Raise an error if the SBOM scanner image is not in any listed registry and is not present locally. If the flag is disabled (with *--pull=false*), do not pull SBOM scanner images from registries, use only local versions. Raise an error if a SBOM scanner image is not present locally. If the pull flag is set to `always` (with *--pull=always*), pull SBOM scanner images from the registries listed in registries.conf. Raise an error if a SBOM scanner image is not found in the registries, even if an image with the same name is present locally. If the pull flag is set to `missing` (with *--pull=missing*), pull SBOM scanner images only if they could not be found in the local containers storage. Raise an error if no image could be found and the pull fails. If the pull flag is set to `never` (with *--pull=never*), do not pull SBOM scanner images from registries, use only the local versions. Raise an error if the image is not present locally. **--quiet**, **-q** When writing the output image, suppress progress output. **--rewrite-timestamp** When generating the new layer for the image, ensure that no newly added content bears a timestamp later than the value used by the **--source-date-epoch** flag, if one was provided, by replacing any timestamps which are later than that value, with that value. **--rm** Remove the working container and its contents after creating the image. Default leaves the container and its content in place. **--sbom** *preset* Generate SBOMs (Software Bills Of Materials) for the output image by scanning the working container and build contexts using the named combination of scanner image, scanner commands, and merge strategy. Must be specified with one or more of **--sbom-image-output**, **--sbom-image-purl-output**, **--sbom-output**, and **--sbom-purl-output**. Recognized presets, and the set of options which they equate to: - "syft", "syft-cyclonedx": --sbom-scanner-image=ghcr.io/anchore/syft --sbom-scanner-command="/syft scan -q dir:{ROOTFS} --output cyclonedx-json={OUTPUT}" --sbom-scanner-command="/syft scan -q dir:{CONTEXT} --output cyclonedx-json={OUTPUT}" --sbom-merge-strategy=merge-cyclonedx-by-component-name-and-version - "syft-spdx": --sbom-scanner-image=ghcr.io/anchore/syft --sbom-scanner-command="/syft scan -q dir:{ROOTFS} --output spdx-json={OUTPUT}" --sbom-scanner-command="/syft scan -q dir:{CONTEXT} --output spdx-json={OUTPUT}" --sbom-merge-strategy=merge-spdx-by-package-name-and-versioninfo - "trivy", "trivy-cyclonedx": --sbom-scanner-image=ghcr.io/aquasecurity/trivy --sbom-scanner-command="trivy filesystem -q {ROOTFS} --format cyclonedx --output {OUTPUT}" --sbom-scanner-command="trivy filesystem -q {CONTEXT} --format cyclonedx --output {OUTPUT}" --sbom-merge-strategy=merge-cyclonedx-by-component-name-and-version - "trivy-spdx": --sbom-scanner-image=ghcr.io/aquasecurity/trivy --sbom-scanner-command="trivy filesystem -q {ROOTFS} --format spdx-json --output {OUTPUT}" --sbom-scanner-command="trivy filesystem -q {CONTEXT} --format spdx-json --output {OUTPUT}" --sbom-merge-strategy=merge-spdx-by-package-name-and-versioninfo **--sbom-image-output** *path* When generating SBOMs, store the generated SBOM in the specified path in the output image. There is no default. **--sbom-image-purl-output** *path* When generating SBOMs, scan them for PURL ([package URL](https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst)) information, and save a list of found PURLs to the named file in the local filesystem. There is no default. **--sbom-merge-strategy** *method* If more than one **--sbom-scanner-command** value is being used, use the specified method to merge the output from later commands with output from earlier commands. Recognized values include: - cat Concatenate the files. - merge-cyclonedx-by-component-name-and-version Merge the "component" fields of JSON documents, ignoring values from documents when the combination of their "name" and "version" values is already present. Documents are processed in the order in which they are generated, which is the order in which the commands that generate them were specified. - merge-spdx-by-package-name-and-versioninfo Merge the "package" fields of JSON documents, ignoring values from documents when the combination of their "name" and "versionInfo" values is already present. Documents are processed in the order in which they are generated, which is the order in which the commands that generate them were specified. **--sbom-output** *file* When generating SBOMs, store the generated SBOM in the named file on the local filesystem. There is no default. **--sbom-purl-output** *file* When generating SBOMs, scan them for PURL ([package URL](https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst)) information, and save a list of found PURLs to the named file in the local filesystem. There is no default. **--sbom-scanner-command** *image* Generate SBOMs by running the specified command from the scanner image. If multiple commands are specified, they are run in the order in which they are specified. These text substitutions are performed: - {ROOTFS} The root of the built image's filesystem, bind mounted. - {CONTEXT} The build context and additional build contexts, bind mounted. - {OUTPUT} The name of a temporary output file, to be read and merged with others or copied elsewhere. **--sbom-scanner-image** *image* Generate SBOMs using the specified scanner image. **--sign-by** *fingerprint* Sign the new image using the GPG key that matches the specified fingerprint. **--source-date-epoch** *seconds* Set the "created" timestamp for the image to this number of seconds since the epoch (Unix time 0, i.e., 00:00:00 UTC on 1 January 1970) to make it easier to create deterministic builds (defaults to $SOURCE_DATE_EPOCH if set, otherwise the current time will be used). The "created" timestamp is written into the image's configuration and manifest when the image is committed, so committing the same working container at two different times will produce images with different sha256 hashes, even if no other changes were made to the working container in between. When --source-date-epoch is set, the "created" timestamp is always set to the time specified, which should allow for identical images to be committed at different times. Conflicts with the similar **--timestamp** flag, which also sets its specified time on layer contents. **--squash** Squash all of the new image's layers (including those inherited from a base image) into a single new layer. **--timestamp** *seconds* Set the "created" timestamp for the image to this number of seconds since the epoch (Unix time 0, i.e., 00:00:00 UTC on 1 January 1970) to make it easier to create deterministic builds (defaults to current time). The "created" timestamp is written into the image's configuration and manifest when the image is committed, so committing the same working container at two different times will produce images with different sha256 hashes, even if no other changes were made to the working container in between. When --timestamp is set, the "created" timestamp is always set to the time specified, which should allow for identical images to be committed at different times. All content in the new layer added as part of the image will also bear this timestamp. Conflicts with the similar **--source-date-epoch** flag, which by default does not affect the timestamps of layer contents. **--tls-verify** *bool-value* Require HTTPS and verification of certificates when talking to container registries (defaults to true). TLS verification cannot be used when talking to an insecure registry. **--unsetannotation** *annotation* Unset the image annotation, causing the annotation not to be inherited from the base image. **--unsetenv** *env* Unset environment variables from the final image. ## EXAMPLE This example saves an image based on the container. `buildah commit containerID newImageName` This example saves an image named newImageName based on the container and removes the working container. `buildah commit --rm containerID newImageName` This example commits to an OCI archive file named /tmp/newImageName based on the container. `buildah commit containerID oci-archive:/tmp/newImageName` This example saves an image with no name, removes the working container, and creates a new container using the image's ID. `buildah from $(buildah commit --rm containerID)` This example saves an image based on the container disabling compression. `buildah commit --disable-compression containerID` This example saves an image named newImageName based on the container disabling compression. `buildah commit --disable-compression containerID newImageName` This example commits the container to the image on the local registry while turning off tls verification. `buildah commit --tls-verify=false containerID docker://localhost:5000/imageId` This example commits the container to the image on the local registry using credentials and certificates for authentication. `buildah commit --cert-dir ~/auth --tls-verify=true --creds=username:password containerID docker://localhost:5000/imageId` This example commits the container to the image on the local registry using credentials from the /tmp/auths/myauths.json file and certificates for authentication. `buildah commit --authfile /tmp/auths/myauths.json --cert-dir ~/auth --tls-verify=true --creds=username:password containerID docker://localhost:5000/imageName` This example saves an image based on the container, but stores dates based on epoch time. `buildah commit --timestamp=0 containerID newImageName` ### Building an multi-architecture image using the --manifest option (requires emulation software) ``` #!/bin/sh build() { ctr=$(./bin/buildah from --arch $1 ubi8) ./bin/buildah run $ctr dnf install -y iputils ./bin/buildah commit --manifest ubi8ping $ctr } build arm build amd64 build s390x ``` ## ENVIRONMENT **BUILD\_REGISTRY\_SOURCES** BUILD\_REGISTRY\_SOURCES, if set, is treated as a JSON object which contains lists of registry names under the keys `insecureRegistries`, `blockedRegistries`, and `allowedRegistries`. When committing an image, if the image is to be given a name, the portion of the name that corresponds to a registry is compared to the items in the `blockedRegistries` list, and if it matches any of them, the commit attempt is denied. If there are registries in the `allowedRegistries` list, and the portion of the name that corresponds to the registry is not in the list, the commit attempt is denied. **TMPDIR** The TMPDIR environment variable allows the user to specify where temporary files are stored while pulling and pushing images. Defaults to '/var/tmp'. ## FILES **registries.conf** (`/etc/containers/registries.conf`) registries.conf is the configuration file which specifies which container registries should be consulted when completing image names which do not include a registry or domain portion. **policy.json** (`/etc/containers/policy.json`) Signature policy file. This defines the trust policy for container images. Controls which container registries can be used for image, and whether or not the tool should trust the images. ## SEE ALSO buildah(1), buildah-images(1), containers-policy.json(5), containers-registries.conf(5), containers-transports(5), containers-auth.json(5) ================================================ FILE: docs/buildah-config.1.md ================================================ # buildah-config "1" "March 2017" "buildah" ## NAME buildah\-config - Update image configuration settings. ## SYNOPSIS **buildah config** [*options*] *container* ## DESCRIPTION Updates one or more of the settings kept for a container. ## OPTIONS **--add-history** Add an entry to the image's history which will note changes to the settings for **--cmd**, **--entrypoint**, **--env**, **--healthcheck**, **--label**, **--onbuild**, **--port**, **--shell**, **--stop-signal**, **--user**, **--volume**, and **--workingdir**. Defaults to false. Note: You can also override the default value of --add-history by setting the BUILDAH\_HISTORY environment variable. `export BUILDAH_HISTORY=true` **--annotation**, **-a** *annotation*=*annotation* Add an image *annotation* (e.g. annotation=*annotation*) to the image manifest of any images which will be built using the specified container. Can be used multiple times. If *annotation* has a trailing `-`, then the *annotation* is removed from the config. If the *annotation* is set to "-" then all annotations are removed from the config. **--arch** *architecture* Set the target *architecture* for any images which will be built using the specified container. By default, if the container was based on an image, that image's target architecture is kept, otherwise the host's architecture is recorded. **--author** *author* Set contact information for the *author* for any images which will be built using the specified container. **--cmd** *command* Set the default *command* to run for containers based on any images which will be built using the specified container. When used in combination with an *entry point*, this specifies the default parameters for the *entry point*. **--comment** *comment* Set the image-level comment for any images which will be built using the specified container. Note: this setting is not present in the OCIv1 image format, so it is discarded when writing images using OCIv1 formats. **--created-by** *created* Set the description of how the topmost layer was *created* for any images which will be created using the specified container. **--domainname** *domain* Set the domainname to set when running containers based on any images built using the specified container. Note: this setting is not present in the OCIv1 image format, so it is discarded when writing images using OCIv1 formats. **--entrypoint** *"command"* | *'["command", "arg1", ...]'* Set the *entry point* for containers based on any images which will be built using the specified container. buildah supports two formats for entrypoint. It can be specified as a simple string, or as an array of commands. Note: When the entrypoint is specified as a string, container runtimes will ignore the `cmd` value of the container image. However if you use the array form, then the cmd will be appended onto the end of the entrypoint cmd and be executed together. Note: The string form is appended to the `sh -c` command as the entrypoint. The array form replaces entrypoint entirely. String Format: ``` $ buildah from scratch $ buildah config --entrypoint "/usr/bin/notashell" working-container $ buildah inspect --format '{{ .OCIv1.Config.Entrypoint }}' working-container [/bin/sh -c /usr/bin/notshell] $ buildah inspect --format '{{ .Docker.Config.Entrypoint }}' working-container [/bin/sh -c /usr/bin/notshell] ``` Array Format: ``` $ buildah config --entrypoint '["/usr/bin/notashell"]' working-container $ buildah inspect --format '{{ .OCIv1.Config.Entrypoint }}' working-container [/usr/bin/notashell] $ buildah inspect --format '{{ .Docker.Config.Entrypoint }}' working-container [/usr/bin/notashell] ``` **--env**, **-e** *env[=value]* Add a value (e.g. env=*value*) to the environment for containers based on any images which will be built using the specified container. Can be used multiple times. If *env* is named but neither `=` nor a `value` is specified, then the value will be taken from the current process environment. If *env* has a trailing `-`, then the *env* is removed from the config. If the *env* is set to "-" then all environment variables are removed from the config. **--healthcheck** *command* Specify a command which should be run to check if a container is running correctly. Values can be *NONE*, "*CMD* ..." (run the specified command directly), or "*CMD-SHELL* ..." (run the specified command using the system's shell), or the empty value (remove a previously-set value and related settings). Note: this setting is not present in the OCIv1 image format, so it is discarded when writing images using OCIv1 formats. **--healthcheck-interval** *interval* Specify how often the command specified using the *--healthcheck* option should be run. Note: this setting is not present in the OCIv1 image format, so it is discarded when writing images using OCIv1 formats. **--healthcheck-retries** *count* Specify how many times the command specified using the *--healthcheck* option can fail before the container is considered to be unhealthy. Note: this setting is not present in the OCIv1 image format, so it is discarded when writing images using OCIv1 formats. **--healthcheck-start-interval** *interval* Specify the time between health checks during the start period. Note: this setting is not present in the OCIv1 image format, so it is discarded when writing images using OCIv1 formats. **--healthcheck-start-period** *interval* Specify how much time can elapse after a container has started before a failure to run the command specified using the *--healthcheck* option should be treated as an indication that the container is failing. During this time period, failures will be attributed to the container not yet having fully started, and will not be counted as errors. After the command succeeds, or the time period has elapsed, failures will be counted as errors. Note: this setting is not present in the OCIv1 image format, so it is discarded when writing images using OCIv1 formats. **--healthcheck-timeout** *interval* Specify how long to wait after starting the command specified using the *--healthcheck* option to wait for the command to return its exit status. If the command has not returned within this time, it should be considered to have failed. Note: this setting is not present in the OCIv1 image format, so it is discarded when writing images using OCIv1 formats. **--history-comment** *comment* Sets a comment on the topmost layer in any images which will be created using the specified container. **--hostname** *host* Set the hostname to set when running containers based on any images built using the specified container. Note: this setting is not present in the OCIv1 image format, so it is discarded when writing images using OCIv1 formats. **--label**, **-l** *label*=*value* Add an image *label* (e.g. label=*value*) to the image configuration of any images which will be built using the specified container. Can be used multiple times. If *label* has a trailing `-`, then the *label* is removed from the config. If the *label* is set to "-" then all labels are removed from the config. **--onbuild** *onbuild command* Add an ONBUILD command to the image. ONBUILD commands are automatically run when images are built based on the image you are creating. Note: this setting is not present in the OCIv1 image format, so it is discarded when writing images using OCIv1 formats. **--os** *operating system* Set the target *operating system* for any images which will be built using the specified container. By default, if the container was based on an image, its OS is kept, otherwise the host's OS's name is recorded. **--os-feature** *feature* Set the name of a required operating system *feature* for any images which will be built using the specified container. By default, if the container was based on an image, the base image's required OS feature list is kept, if it specified one. This option is typically only meaningful when the image's OS is Windows. If *feature* has a trailing `-`, then the *feature* is removed from the set of required features which will be listed in the image. If the *feature* is set to "-" then the entire features list is removed from the config. **--os-version** *version* Set the exact required operating system *version* for any images which will be built using the specified container. By default, if the container was based on an image, the base image's required OS version is kept, if it specified one. This option is typically only meaningful when the image's OS is Windows, and is typically set in Windows base images, so using this option is usually unnecessary. **--port**, **-p** *port/protocol* Add a *port* to expose when running containers based on any images which will be built using the specified container. Can be used multiple times. To specify whether the port listens on TCP or UDP, use "port/protocol". The default is TCP if the protocol is not specified. To expose the port on both TCP and UDP, specify the port option multiple times. If *port* has a trailing `-` and is already set, then the *port* is removed from the configuration. If the port is set to `-` then all exposed ports settings are removed from the configuration. **--shell** *shell* Set the default *shell* to run inside of the container image. The shell instruction allows the default shell used for the shell form of commands to be overridden. The default shell for Linux containers is "/bin/sh -c". Note: this setting is not present in the OCIv1 image format, so it is discarded when writing images using OCIv1 formats. **--stop-signal** *signal* Set default *stop signal* for container. This signal will be sent when container is stopped, default is SIGINT. **--unsetannotation** *annotation* Unset the image annotation, causing the annotation not to be inherited from the base image. **--unsetlabel** *label* Unset the image label, causing the label not to be inherited from the base image. **--user**, **-u** *user*[:*group*] Set the default *user* to be used when running containers based on this image. The user can be specified as a user name or UID, optionally followed by a group name or GID, separated by a colon (':'). If names are used, the container should include entries for those names in its */etc/passwd* and */etc/group* files. **--variant** *variant* Set the target architecture *variant* for any images which will be built using the specified container. By default, if the container was based on an image, that image's target architecture and variant information is kept, otherwise the host's architecture and variant are recorded. **--volume**, **-v** *volume* Add a location in the directory tree which should be marked as a *volume* in any images which will be built using the specified container. Can be used multiple times. If *volume* has a trailing `-`, and is already set, then the *volume* is removed from the config. If the *volume* is set to "-" then all volumes are removed from the config. **--workingdir** *directory* Set the initial working *directory* for containers based on images which will be built using the specified container. ## EXAMPLE buildah config --author='Jane Austen' --workingdir='/etc/mycontainers' containerID buildah config --entrypoint /entrypoint.sh containerID buildah config --entrypoint '[ "/entrypoint.sh", "dev" ]' containerID buildah config --env foo=bar --env PATH=$PATH containerID buildah config --env foo- containerID buildah config --label Name=Mycontainer --label Version=1.0 containerID buildah config --label Name- containerID buildah config --annotation note=myNote containerID buildah config --annotation note- buildah config --volume /usr/myvol containerID buildah config --volume /usr/myvol- containerID buildah config --port 1234 --port 8080 containerID buildah config --port 514/tcp --port 514/udp containerID buildah config --env 1234=5678 containerID buildah config --env 1234- containerID buildah config --os-version 10.0.19042.1645 containerID buildah config --os-feature win32k containerID buildah config --os-feature win32k- containerID ## SEE ALSO buildah(1) ================================================ FILE: docs/buildah-containers.1.md ================================================ # buildah-containers "1" "March 2017" "buildah" ## NAME buildah\-containers - List the working containers and their base images. ## SYNOPSIS **buildah containers** [*options*] ## DESCRIPTION Lists containers which appear to be Buildah working containers, their names and IDs, and the names and IDs of the images from which they were initialized. ## OPTIONS **--all**, **-a** List information about all containers, including those which were not created by and are not being used by Buildah. Containers created by Buildah are denoted with an '*' in the 'BUILDER' column. **--filter**, **-f** Filter output based on conditions provided. Valid filters are listed below: | **Filter** | **Description** | | --------------- | ------------------------------------------------------------------- | | id | [ID] Container's ID | | name | [Name] Container's name | | ancestor | [ImageName] Image or descendant used to create container | **--format** Pretty-print containers using a Go template. Valid placeholders for the Go template are listed below: | **Placeholder** | **Description** | | --------------- | -----------------------------------------| | .ContainerID | Container ID | | .Builder | Whether container was created by buildah | | .ImageID | Image ID | | .ImageName | Image name | | .ContainerName | Container name | **--json** Output in JSON format. **--noheading**, **-n** Omit the table headings from the listing of containers. **--notruncate** Do not truncate IDs and image names in the output. **--quiet**, **-q** Displays only the container IDs. ## EXAMPLE buildah containers ``` CONTAINER ID BUILDER IMAGE ID IMAGE NAME CONTAINER NAME ccf84de04b80 * 53ce4390f2ad registry.access.redhat.com/ub... ubi8-working-container 45be1d806fc5 * 16ea53ea7c65 docker.io/library/busybox:latest busybox-working-container ``` buildah containers --quiet ``` ccf84de04b80c309ce6586997c79a769033dc4129db903c1882bc24a058438b8 45be1d806fc533fcfc2beee77e424d87e5990d3ce9214d6b374677d6630bba07 ``` buildah containers -q --noheading --notruncate ``` ccf84de04b80c309ce6586997c79a769033dc4129db903c1882bc24a058438b8 45be1d806fc533fcfc2beee77e424d87e5990d3ce9214d6b374677d6630bba07 ``` buildah containers --json ``` [ { "id": "ccf84de04b80c309ce6586997c79a769033dc4129db903c1882bc24a058438b8", "builder": true, "imageid": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e", "imagename": "registry.access.redhat.com/ubi8:latest", "containername": "ubi8-working-container" }, { "id": "45be1d806fc533fcfc2beee77e424d87e5990d3ce9214d6b374677d6630bba07", "builder": true, "imageid": "16ea53ea7c652456803632d67517b78a4f9075a10bfdc4fc6b7b4cbf2bc98497", "imagename": "docker.io/library/busybox:latest", "containername": "busybox-working-container" } ] ``` buildah containers --format "{{.ContainerID}} {{.ContainerName}}" ``` ccf84de04b80c309ce6586997c79a769033dc4129db903c1882bc24a058438b8 ubi8-working-container 45be1d806fc533fcfc2beee77e424d87e5990d3ce9214d6b374677d6630bba07 busybox-working-container ``` buildah containers --format "Container ID: {{.ContainerID}}" ``` Container ID: ccf84de04b80c309ce6586997c79a769033dc4129db903c1882bc24a058438b8 Container ID: 45be1d806fc533fcfc2beee77e424d87e5990d3ce9214d6b374677d6630bba07 ``` buildah containers --filter ancestor=ubuntu ``` CONTAINER ID BUILDER IMAGE ID IMAGE NAME CONTAINER NAME fbfd3505376e * 0ff04b2e7b63 docker.io/library/ubuntu:latest ubuntu-working-container ``` ## SEE ALSO buildah(1) ================================================ FILE: docs/buildah-copy.1.md ================================================ # buildah-copy "1" "April 2021" "buildah" ## NAME buildah\-copy - Copies the contents of a file, URL, or directory into a container's working directory. ## SYNOPSIS **buildah copy** *container* *src* [[*src* ...] *dest*] ## DESCRIPTION Copies the contents of a file, URL, or a directory to a container's working directory or a specified location in the container. If a local directory is specified as a source, its *contents* are copied to the destination. ## OPTIONS **--add-history** Add an entry to the history which will note the digest of the added content. Defaults to false. Note: You can also override the default value of --add-history by setting the BUILDAH\_HISTORY environment variable. `export BUILDAH_HISTORY=true` **--cert-dir** *path* Use certificates at *path* (\*.crt, \*.cert, \*.key) when connecting to registries for pulling images named with the **--from** flag. The default certificates directory is _/etc/containers/certs.d_. **--checksum** *checksum* Checksum the source content. The value of *checksum* must be a standard container digest string. Only supported for HTTP sources. **--chmod** *permissions* Sets the access permissions of the destination content. Accepts the numerical format. If `--from` is not used, defaults to `0755`. **--chown** *owner*:*group* Sets the user and group ownership of the destination content. If `--from` is not used, defaults to `0:0`. **--contextdir** *directory* Build context directory. Specifying a context directory causes Buildah to chroot into the context directory. This means copying files pointed at by symbolic links outside of the chroot will fail. **--exclude** *pattern* Exclude copying files matching the specified pattern. Option can be specified multiple times. See containerignore(5) for supported formats. **--from** *containerOrImage* Use the root directory of the specified working container or image as the root directory when resolving absolute source paths and the path of the context directory. If an image needs to be pulled, options recognized by `buildah pull` can be used. If `--chown` or `--chmod` are not used, permissions and ownership is preserved. **--ignorefile** *file* Path to an alternative .containerignore (.dockerignore) file. Requires \-\-contextdir be specified. **--link** Create an independent image layer for the added files instead of modifying the working container's filesystem. If `buildah run` creates a file and `buildah copy --link` adds a file to the same path, the file from `buildah copy --link` will be present in the committed image. The --link layer is applied after all container filesystem changes at commit time. **--parents** Preserve leading directories in the paths of items being copied, relative to either the top of the build context, or to the "pivot point", a location in the source path marked by a path component named "." (i.e., where "/./" occurs in the path). **--quiet**, **-q** Refrain from printing a digest of the copied content. **--retry** *attempts* Number of times to retry in case of failure when performing pull of images from registry. Defaults to `3`. **--retry-delay** *duration* Duration of delay between retry attempts in case of failure when performing pull of images from registry. Defaults to `2s`. **--timestamp** *seconds* Set the timestamp ("mtime") for added content to exactly this number of seconds since the epoch (Unix time 0, i.e., 00:00:00 UTC on 1 January 1970) to help allow for deterministic builds. The destination directory into which the content is being copied will most likely reflect the time at which the content was added to it. **--tls-verify** *bool-value* Require verification of certificates when pulling images referred to with the **--from*** flag (defaults to true). TLS verification cannot be used when talking to an insecure registry. ## EXAMPLE buildah copy containerID '/myapp/app.conf' '/myapp/app.conf' buildah copy --exclude=**/*.md docs containerID 'docs' '/docs' buildah copy --parents containerID './x/a.txt' './y/a.txt' '/parents' buildah copy --chown myuser:mygroup containerID '/myapp/app.conf' '/myapp/app.conf' buildah copy --chmod 660 containerID '/myapp/app.conf' '/myapp/app.conf' buildah copy containerID '/home/myuser/myproject.go' buildah copy containerID '/home/myuser/myfiles.tar' '/tmp' buildah copy containerID '/tmp/workingdir' '/tmp/workingdir' buildah copy containerID 'https://github.com/containers/buildah' '/tmp' buildah copy containerID 'passwd' 'certs.d' /etc ## FILES ### .containerignore/.dockerignore If the .containerignore/.dockerignore file exists in the context directory, `buildah copy` reads its contents. If both exist, then .containerignore is used. When the `--ignorefile` option is specified Buildah reads it and uses it to decide which content to exclude when copying content into the working container. Users can specify a series of Unix shell glob patterns in an ignore file to identify files/directories to exclude. Buildah supports a special wildcard string `**` which matches any number of directories (including zero). For example, `**/*.go` will exclude all files that end with .go that are found in all directories. Example .containerignore/.dockerignore file: ``` # here are files we want to exclude */*.c **/output* src ``` `*/*.c` Excludes files and directories whose names end with .c in any top level subdirectory. For example, the source file include/rootless.c. `**/output*` Excludes files and directories starting with `output` from any directory. `src` Excludes files named src and the directory src as well as any content in it. Lines starting with ! (exclamation mark) can be used to make exceptions to exclusions. The following is an example .containerignore/.dockerignore file that uses this mechanism: ``` *.doc !Help.doc ``` Exclude all doc files except Help.doc when copying content into the container. This functionality is compatible with the handling of .containerignore files described here: https://github.com/containers/common/blob/main/docs/containerignore.5.md ## SEE ALSO buildah(1), containerignore(5) ================================================ FILE: docs/buildah-from.1.md ================================================ # buildah-from "1" "March 2017" "buildah" ## NAME buildah\-from - Creates a new working container, either from scratch or using a specified image as a starting point. ## SYNOPSIS **buildah from** [*options*] *image* ## DESCRIPTION Creates a working container based upon the specified image name. If the supplied image name is "scratch" a new empty container is created. Image names use a "transport":"details" format. Multiple transports are supported: **dir:**_path_ An existing local directory _path_ containing the manifest, layer tarballs, and signatures in individual files. This is a non-standardized format, primarily useful for debugging or noninvasive image inspection. **docker://**_docker-reference_ (Default) An image in a registry implementing the "Docker Registry HTTP API V2". By default, uses the authorization state in `$XDG_RUNTIME_DIR/containers/auth.json`, which is set using `(buildah login)`. See containers-auth.json(5) for more information. If the authorization state is not found there, `$HOME/.docker/config.json` is checked, which is set using `(docker login)`. If _docker-reference_ does not include a registry name, *localhost* will be consulted first, followed by any registries named in the registries configuration. **docker-archive:**_path_ An image is retrieved as a `podman load` formatted file. **docker-daemon:**_docker-reference_ An image _docker-reference_ stored in the docker daemon's internal storage. _docker-reference_ must include either a tag or a digest. Alternatively, when reading images, the format can also be docker-daemon:algo:digest (an image ID). **oci:**_path_**:**_tag_** An image tag in a directory compliant with "Open Container Image Layout Specification" at _path_. **oci-archive:**_path_**:**_tag_ An image _tag_ in a directory compliant with "Open Container Image Layout Specification" at _path_. ### DEPENDENCIES Buildah resolves the path to the registry to pull from by using the /etc/containers/registries.conf file, containers-registries.conf(5). If the `buildah from` command fails with an "image not known" error, first verify that the registries.conf file is installed and configured appropriately. ## RETURN VALUE The container ID of the container that was created. On error 1 is returned. ## OPTIONS **--add-host**=[] Add a custom host-to-IP mapping (host:ip) Add a line to /etc/hosts. The format is hostname:ip. The **--add-host** option can be set multiple times. **--arch**="ARCH" Set the ARCH of the image to be pulled to the provided value instead of using the architecture of the host. (Examples: arm, arm64, 386, amd64, ppc64le, s390x) **--authfile** *path* Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json. See containers-auth.json(5) for more information. This file is created using `buildah login`. If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using `docker login`. Note: You can also override the default path of the authentication file by setting the REGISTRY\_AUTH\_FILE environment variable. `export REGISTRY_AUTH_FILE=path` **--cap-add**=*CAP\_xxx* Add the specified capability to the default set of capabilities which will be supplied for subsequent *buildah run* invocations which use this container. Certain capabilities are granted by default; this option can be used to add more. **--cap-drop**=*CAP\_xxx* Remove the specified capability from the default set of capabilities which will be supplied for subsequent *buildah run* invocations which use this container. The CAP\_CHOWN, CAP\_DAC\_OVERRIDE, CAP\_FOWNER, CAP\_FSETID, CAP\_KILL, CAP\_NET\_BIND\_SERVICE, CAP\_SETFCAP, CAP\_SETGID, CAP\_SETPCAP, and CAP\_SETUID capabilities are granted by default; this option can be used to remove them. The list of default capabilities is managed in containers.conf(5). If a capability is specified to both the **--cap-add** and **--cap-drop** options, it will be dropped, regardless of the order in which the options were given. **--cert-dir** *path* Use certificates at *path* (\*.crt, \*.cert, \*.key) to connect to the registry. The default certificates directory is _/etc/containers/certs.d_. **--cgroup-parent**="" Path to cgroups under which the cgroup for the container will be created. If the path is not absolute, the path is considered to be relative to the cgroups path of the init process. Cgroups will be created if they do not already exist. **--cgroupns** *how* Sets the configuration for IPC namespaces when the container is subsequently used for `buildah run`. The configured value can be "" (the empty string) or "private" to indicate that a new cgroup namespace should be created, or it can be "host" to indicate that the cgroup namespace in which `buildah` itself is being run should be reused. **--cidfile** *ContainerIDFile* Write the container ID to the file. **--cpu-period**=*0* Limit the CPU CFS (Completely Fair Scheduler) period Limit the container's CPU usage. This flag tells the kernel to restrict the container's CPU usage to the period you specify. **--cpu-quota**=*0* Limit the CPU CFS (Completely Fair Scheduler) quota Limit the container's CPU usage. By default, containers run with the full CPU resource. This flag tells the kernel to restrict the container's CPU usage to the quota you specify. **--cpu-shares**, **-c**=*0* CPU shares (relative weight) By default, all containers get the same proportion of CPU cycles. This proportion can be modified by changing the container's CPU share weighting relative to the weighting of all other running containers. To modify the proportion from the default of 1024, use the **--cpu-shares** flag to set the weighting to 2 or higher. The proportion will only apply when CPU-intensive processes are running. When tasks in one container are idle, other containers can use the left-over CPU time. The actual amount of CPU time will vary depending on the number of containers running on the system. For example, consider three containers, one has a cpu-share of 1024 and two others have a cpu-share setting of 512. When processes in all three containers attempt to use 100% of CPU, the first container would receive 50% of the total CPU time. If you add a fourth container with a cpu-share of 1024, the first container only gets 33% of the CPU. The remaining containers receive 16.5%, 16.5% and 33% of the CPU. On a multi-core system, the shares of CPU time are distributed over all CPU cores. Even if a container is limited to less than 100% of CPU time, it can use 100% of each individual CPU core. For example, consider a system with more than three cores. If you start one container **{C0}** with **-c=512** running one process, and another container **{C1}** with **-c=1024** running two processes, this can result in the following division of CPU shares: PID container CPU CPU share 100 {C0} 0 100% of CPU0 101 {C1} 1 100% of CPU1 102 {C1} 2 100% of CPU2 **--cpuset-cpus**="" CPUs in which to allow execution (0-3, 0,1) **--cpuset-mems**="" Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems. If you have four memory nodes on your system (0-3), use `--cpuset-mems=0,1` then processes in your container will only use memory from the first two memory nodes. **--creds** *creds* The [username[:password]] to use to authenticate with the registry if required. If one or both values are not supplied, a command line prompt will appear and the value can be entered. The password is entered without echo. **--decryption-key** *key[:passphrase]* The [key[:passphrase]] to be used for decryption of images. Key can point to keys and/or certificates. Decryption will be tried with all keys. If the key is protected by a passphrase, it is required to be passed in the argument and omitted otherwise. **--device**=*device* Add a host device, or devices under a directory, to the environment of subsequent **buildah run** invocations for the new working container. The optional *permissions* parameter can be used to specify device permissions, using any one or more of **r** for read, **w** for write, and **m** for **mknod**(2). Example: **--device=/dev/sdc:/dev/xvdc:rwm**. Note: if _host-device_ is a symbolic link then it will be resolved first. The container will only store the major and minor numbers of the host device. The device to share can also be specified using a Container Device Interface (CDI) specification (https://github.com/cncf-tags/container-device-interface). Note: if the user only has access rights via a group, accessing the device from inside a rootless container will fail. The **crun**(1) runtime offers a workaround for this by adding the option **--annotation run.oci.keep_original_groups=1**. **--dns**=[] Set custom DNS servers This option can be used to override the DNS configuration passed to the container. Typically this is necessary when the host DNS configuration is invalid for the container (e.g., 127.0.0.1). When this is the case the `--dns` flag is necessary for every run. The special value **none** can be specified to disable creation of /etc/resolv.conf in the container by Buildah. The /etc/resolv.conf file in the image will be used without changes. **--dns-option**=[] Set custom DNS options **--dns-search**=[] Set custom DNS search domains **--format**, **-f** *oci* | *docker* Control the format for the built image's manifest and configuration data. Recognized formats include *oci* (OCI image-spec v1.0, the default) and *docker* (version 2, using schema format 2 for the manifest). Note: You can also override the default format by setting the BUILDAH\_FORMAT environment variable. `export BUILDAH_FORMAT=docker` **--group-add**=*group* | *keep-groups* Assign additional groups to the primary user running within the container process. - `keep-groups` is a special flag that tells Buildah to keep the supplementary group access. Allows container to use the user's supplementary group access. If file systems or devices are only accessible by the rootless user's group, this flag tells the OCI runtime to pass the group access into the container. Currently only available with the `crun` OCI runtime. Note: `keep-groups` is exclusive, other groups cannot be specified with this flag. **--http-proxy** By default proxy environment variables are passed into the container if set for the Buildah process. This can be disabled by setting the `--http-proxy` option to `false`. The environment variables passed in include `http_proxy`, `https_proxy`, `ftp_proxy`, `no_proxy`, and also the upper case versions of those. Defaults to `true` **--ipc** *how* Sets the configuration for IPC namespaces when the container is subsequently used for `buildah run`. The configured value can be "" (the empty string) or "container" to indicate that a new IPC namespace should be created, or it can be "host" to indicate that the IPC namespace in which `Buildah` itself is being run should be reused, or it can be the path to an IPC namespace which is already in use by another process. **--isolation** *type* Controls what type of isolation is used for running processes under `buildah run`. Recognized types include *oci* (OCI-compatible runtime, the default), *rootless* (OCI-compatible runtime invoked using a modified configuration, with *--no-new-keyring* added to its *create* invocation, reusing the host's network and UTS namespaces, and creating private IPC, PID, mount, and user namespaces; the default for unprivileged users), and *chroot* (an internal wrapper that leans more toward chroot(1) than container technology, reusing the host's control group, network, IPC, and PID namespaces, and creating private mount and UTS namespaces, and creating user namespaces only when they're required for ID mapping). Note: You can also override the default isolation type by setting the BUILDAH\_ISOLATION environment variable. `export BUILDAH_ISOLATION=oci` **--memory**, **-m**="" Memory limit (format: [], where unit = b, k, m or g) Allows you to constrain the memory available to a container. If the host supports swap memory, then the **-m** memory setting can be larger than physical RAM. If a limit of 0 is specified (not using **-m**), the container's memory is not limited. The actual limit may be rounded up to a multiple of the operating system's page size (the value would be very large, that's millions of trillions). **--memory-swap**="LIMIT" A limit value equal to memory plus swap. Must be used with the **-m** (**--memory**) flag. The swap `LIMIT` should always be larger than **-m** (**--memory**) value. By default, the swap `LIMIT` will be set to double the value of --memory. The format of `LIMIT` is `[]`. Unit can be `b` (bytes), `k` (kilobytes), `m` (megabytes), or `g` (gigabytes). If you don't specify a unit, `b` is used. Set LIMIT to `-1` to enable unlimited swap. **--name** *name* A *name* for the working container **--network**=*mode*, **--net**=*mode* Sets the configuration for network namespaces when the container is subsequently used for `buildah run`. Valid _mode_ values are: - **none**: no networking. Invalid if using **--dns**, **--dns-opt**, or **--dns-search**; - **host**: use the host network stack. Note: the host mode gives the container full access to local system services such as D-bus and is therefore considered insecure; - **ns:**_path_: path to a network namespace to join; - **private**: create a new namespace for the container (default) - **\**: Join the network with the given name or ID, e.g. use `--network mynet` to join the network with the name mynet. Only supported for rootful users. - **slirp4netns[:OPTIONS,...]**: use **slirp4netns**(1) to create a user network stack. This is the default for rootless containers. It is possible to specify these additional options, they can also be set with `network_cmd_options` in containers.conf: - **allow_host_loopback=true|false**: Allow slirp4netns to reach the host loopback IP (default is 10.0.2.2 or the second IP from slirp4netns cidr subnet when changed, see the cidr option below). The default is false. - **mtu=MTU**: Specify the MTU to use for this network. (Default is `65520`). - **cidr=CIDR**: Specify ip range to use for this network. (Default is `10.0.2.0/24`). - **enable_ipv6=true|false**: Enable IPv6. Default is true. (Required for `outbound_addr6`). - **outbound_addr=INTERFACE**: Specify the outbound interface slirp binds to (ipv4 traffic only). - **outbound_addr=IPv4**: Specify the outbound ipv4 address slirp binds to. - **outbound_addr6=INTERFACE**: Specify the outbound interface slirp binds to (ipv6 traffic only). - **outbound_addr6=IPv6**: Specify the outbound ipv6 address slirp binds to. - **pasta[:OPTIONS,...]**: use **pasta**(1) to create a user-mode networking stack. \ This is only supported in rootless mode. \ By default, IPv4 and IPv6 addresses and routes, as well as the pod interface name, are copied from the host. If port forwarding isn't configured, ports are forwarded dynamically as services are bound on either side (init namespace or container namespace). Port forwarding preserves the original source IP address. Options described in pasta(1) can be specified as comma-separated arguments. \ In terms of pasta(1) options, **--config-net** is given by default, in order to configure networking when the container is started, and **--no-map-gw** is also assumed by default, to avoid direct access from container to host using the gateway address. The latter can be overridden by passing **--map-gw** in the pasta-specific options (despite not being an actual pasta(1) option). \ Also, **-t none** and **-u none** are passed to disable automatic port forwarding based on bound ports. Similarly, **-T none** and **-U none** are given to disable the same functionality from container to host. \ Some examples: - **pasta:--map-gw**: Allow the container to directly reach the host using the gateway address. - **pasta:--mtu,1500**: Specify a 1500 bytes MTU for the _tap_ interface in the container. - **pasta:--ipv4-only,-a,10.0.2.0,-n,24,-g,10.0.2.2,--dns-forward,10.0.2.3,-m,1500,--no-ndp,--no-dhcpv6,--no-dhcp**, equivalent to default slirp4netns(1) options: disable IPv6, assign `10.0.2.0/24` to the `tap0` interface in the container, with gateway `10.0.2.3`, enable DNS forwarder reachable at `10.0.2.3`, set MTU to 1500 bytes, disable NDP, DHCPv6 and DHCP support. - **pasta:-I,tap0,--ipv4-only,-a,10.0.2.0,-n,24,-g,10.0.2.2,--dns-forward,10.0.2.3,--no-ndp,--no-dhcpv6,--no-dhcp**, equivalent to default slirp4netns(1) options with Podman overrides: same as above, but leave the MTU to 65520 bytes - **pasta:-t,auto,-u,auto,-T,auto,-U,auto**: enable automatic port forwarding based on observed bound ports from both host and container sides - **pasta:-T,5201**: enable forwarding of TCP port 5201 from container to host, using the loopback interface instead of the tap interface for improved performance **--os**="OS" Set the OS of the image to be pulled to the provided value instead of using the current operating system of the host. **--pid** *how* Sets the configuration for PID namespaces when the container is subsequently used for `buildah run`. The configured value can be "" (the empty string) or "container" to indicate that a new PID namespace should be created, or it can be "host" to indicate that the PID namespace in which `Buildah` itself is being run should be reused, or it can be the path to a PID namespace which is already in use by another process. **--platform**="OS/ARCH[/VARIANT]" Set the OS/ARCH of the image to be pulled to the provided value instead of using the current operating system and architecture of the host (for example `linux/arm`). OS/ARCH pairs are those used by the Go Programming Language. In several cases the ARCH value for a platform differs from one produced by other tools such as the `arch` command. Valid OS and architecture name combinations are listed as values for $GOOS and $GOARCH at https://golang.org/doc/install/source#environment, and can also be found by running `go tool dist list`. While `buildah from` is happy to pull an image for any platform that exists, `buildah run` will not be able to run binaries provided by that image without the help of emulation provided by packages like `qemu-user-static`. **NOTE:** The `--platform` option may not be used in combination with the `--arch`, `--os`, or `--variant` options. **--pull** Pull image policy. If not specified, the default is **missing**. If an explicit **--pull** argument is provided without any value, use the **always** behavior. - **always**: Pull base and SBOM scanner images from the registries listed in registries.conf. Raise an error if a base or SBOM scanner image is not found in the registries, even if an image with the same name is present locally. - **missing**: SBOM scanner images only if they could not be found in the local containers storage. Raise an error if no image could be found and the pull fails. - **never**: Do not pull base and SBOM scanner images from registries, use only the local versions. Raise an error if the image is not present locally. - **newer**: Pull base and SBOM scanner images from the registries listed in registries.conf if newer. Raise an error if a base or SBOM scanner image is not found in the registries when image with the same name is not present locally. **--quiet**, **-q** If an image needs to be pulled from the registry, suppress progress output. **--retry** *attempts* Number of times to retry in case of failure when performing pull of images from registry. Defaults to `3`. **--retry-delay** *duration* Duration of delay between retry attempts in case of failure when performing pull of images from registry. Defaults to `2s`. **--security-opt**=[] Security Options "label=user:USER" : Set the label user for the container "label=role:ROLE" : Set the label role for the container "label=type:TYPE" : Set the label type for the container "label=level:LEVEL" : Set the label level for the container "label=disable" : Turn off label confinement for the container "no-new-privileges" : Not supported "seccomp=unconfined" : Turn off seccomp confinement for the container "seccomp=profile.json : White listed syscalls seccomp Json file to be used as a seccomp filter "apparmor=unconfined" : Turn off apparmor confinement for the container "apparmor=your-profile" : Set the apparmor confinement profile for the container **--shm-size**="" Size of `/dev/shm`. The format is ``. `number` must be greater than `0`. Unit is optional and can be `b` (bytes), `k` (kilobytes), `m`(megabytes), or `g` (gigabytes). If you omit the unit, the system uses bytes. If you omit the size entirely, the system uses `64m`. **--tls-verify** *bool-value* Require HTTPS and verification of certificates when talking to container registries (defaults to true). TLS verification cannot be used when talking to an insecure registry. **--ulimit** *type*=*soft-limit*[:*hard-limit*] Specifies resource limits to apply to processes launched during `buildah run`. This option can be specified multiple times. Recognized resource types include: "core": maximum core dump size (ulimit -c) "cpu": maximum CPU time (ulimit -t) "data": maximum size of a process's data segment (ulimit -d) "fsize": maximum size of new files (ulimit -f) "locks": maximum number of file locks (ulimit -x) "memlock": maximum amount of locked memory (ulimit -l) "msgqueue": maximum amount of data in message queues (ulimit -q) "nice": niceness adjustment (nice -n, ulimit -e) "nofile": maximum number of open files (ulimit -n) "nofile": maximum number of open files (1048576); when run by root "nproc": maximum number of processes (ulimit -u) "nproc": maximum number of processes (1048576); when run by root "rss": maximum size of a process's (ulimit -m) "rtprio": maximum real-time scheduling priority (ulimit -r) "rttime": maximum amount of real-time execution between blocking syscalls "sigpending": maximum number of pending signals (ulimit -i) "stack": maximum stack size (ulimit -s) **--userns** *how* Sets the configuration for user namespaces when the container is subsequently used for `buildah run`. The configured value can be "" (the empty string) or "container" to indicate that a new user namespace should be created, it can be "host" to indicate that the user namespace in which `Buildah` itself is being run should be reused, or it can be the path to an user namespace which is already in use by another process. **--userns-gid-map** *mapping* Directly specifies a GID mapping which should be used to set ownership, at the filesystem level, on the working container's contents. Commands run when handling `RUN` instructions will default to being run in their own user namespaces, configured using the UID and GID maps. Entries in this map take the form of one or more colon-separated triples of a starting in-container GID, a corresponding starting host-level GID, and the number of consecutive IDs which the map entry represents. This option overrides the *remap-gids* setting in the *options* section of /etc/containers/storage.conf. If this option is not specified, but a global --userns-gid-map setting is supplied, settings from the global option will be used. **--userns-gid-map-group** *mapping* Directly specifies a GID mapping which should be used to set ownership, at the filesystem level, on the container's contents. Commands run using `buildah run` will default to being run in their own user namespaces, configured using the UID and GID maps. Entries in this map take the form of one or more triples of a starting in-container GID, a corresponding starting host-level GID, and the number of consecutive IDs which the map entry represents. This option overrides the *remap-gids* setting in the *options* section of /etc/containers/storage.conf. If this option is not specified, but a global --userns-gid-map setting is supplied, settings from the global option will be used. If none of --userns-uid-map-user, --userns-gid-map-group, or --userns-gid-map are specified, but --userns-uid-map is specified, the GID map will be set to use the same numeric values as the UID map. **NOTE:** When this option is specified by a rootless user, the specified mappings are relative to the rootless usernamespace in the container, rather than being relative to the host as it would be when run rootful. **--userns-gid-map-group** *group* Specifies that a GID mapping which should be used to set ownership, at the filesystem level, on the container's contents, can be found in entries in the `/etc/subgid` file which correspond to the specified group. Commands run using `buildah run` will default to being run in their own user namespaces, configured using the UID and GID maps. If --userns-uid-map-user is specified, but --userns-gid-map-group is not specified, `Buildah` will assume that the specified user name is also a suitable group name to use as the default setting for this option. **--userns-uid-map** *mapping* Directly specifies a UID mapping which should be used to set ownership, at the filesystem level, on the working container's contents. Commands run when handling `RUN` instructions will default to being run in their own user namespaces, configured using the UID and GID maps. Entries in this map take the form of one or more colon-separated triples of a starting in-container UID, a corresponding starting host-level UID, and the number of consecutive IDs which the map entry represents. This option overrides the *remap-uids* setting in the *options* section of /etc/containers/storage.conf. If this option is not specified, but a global --userns-uid-map setting is supplied, settings from the global option will be used. **--userns-uid-map-user** *mapping* Directly specifies a UID mapping which should be used to set ownership, at the filesystem level, on the container's contents. Commands run using `buildah run` will default to being run in their own user namespaces, configured using the UID and GID maps. Entries in this map take the form of one or more triples of a starting in-container UID, a corresponding starting host-level UID, and the number of consecutive IDs which the map entry represents. This option overrides the *remap-uids* setting in the *options* section of /etc/containers/storage.conf. If this option is not specified, but a global --userns-uid-map setting is supplied, settings from the global option will be used. If none of --userns-uid-map-user, --userns-gid-map-group, or --userns-uid-map are specified, but --userns-gid-map is specified, the UID map will be set to use the same numeric values as the GID map. **NOTE:** When this option is specified by a rootless user, the specified mappings are relative to the rootless usernamespace in the container, rather than being relative to the host as it would be when run rootful. **--userns-uid-map-user** *user* Specifies that a UID mapping which should be used to set ownership, at the filesystem level, on the container's contents, can be found in entries in the `/etc/subuid` file which correspond to the specified user. Commands run using `buildah run` will default to being run in their own user namespaces, configured using the UID and GID maps. If --userns-gid-map-group is specified, but --userns-uid-map-user is not specified, `Buildah` will assume that the specified group name is also a suitable user name to use as the default setting for this option. **--uts** *how* Sets the configuration for UTS namespaces when the container is subsequently used for `buildah run`. The configured value can be "" (the empty string) or "container" to indicate that a new UTS namespace should be created, or it can be "host" to indicate that the UTS namespace in which `Buildah` itself is being run should be reused, or it can be the path to a UTS namespace which is already in use by another process. **--variant**="" Set the architecture variant of the image to be pulled. **--volume**, **-v**[=*[HOST-DIR:CONTAINER-DIR[:OPTIONS]]*] Create a bind mount. If you specify, ` -v /HOST-DIR:/CONTAINER-DIR`, Buildah bind mounts `/HOST-DIR` in the host to `/CONTAINER-DIR` in the Buildah container. The `OPTIONS` are a comma delimited list and can be: * [rw|ro] * [U] * [z|Z|O] * [`[r]shared`|`[r]slave`|`[r]private`|`[r]unbindable`] [[1]](#Footnote1) The `CONTAINER-DIR` must be an absolute path such as `/src/docs`. The `HOST-DIR` must be an absolute path as well. Buildah bind-mounts the `HOST-DIR` to the path you specify. For example, if you supply `/foo` as the host path, Buildah copies the contents of `/foo` to the container filesystem on the host and bind mounts that into the container. You can specify multiple **-v** options to mount one or more mounts to a container. `Write Protected Volume Mounts` You can add the `:ro` or `:rw` suffix to a volume to mount it read-only or read-write mode, respectively. By default, the volumes are mounted read-write. See examples. `Chowning Volume Mounts` By default, Buildah does not change the owner and group of source volume directories mounted into containers. If a container is created in a new user namespace, the UID and GID in the container may correspond to another UID and GID on the host. The `:U` suffix tells Buildah to use the correct host UID and GID based on the UID and GID within the container, to change the owner and group of the source volume. `Labeling Volume Mounts` Labeling systems like SELinux require that proper labels are placed on volume content mounted into a container. Without a label, the security system might prevent the processes running inside the container from using the content. By default, Buildah does not change the labels set by the OS. To change a label in the container context, you can add either of two suffixes `:z` or `:Z` to the volume mount. These suffixes tell Buildah to relabel file objects on the shared volumes. The `z` option tells Buildah that two containers share the volume content. As a result, Buildah labels the content with a shared content label. Shared volume labels allow all containers to read/write content. The `Z` option tells Buildah to label the content with a private unshared label. Only the current container can use a private volume. `Overlay Volume Mounts` The `:O` flag tells Buildah to mount the directory from the host as a temporary storage using the Overlay file system. The `RUN` command containers are allowed to modify contents within the mountpoint and are stored in the container storage in a separate directory. In Overlay FS terms the source directory will be the lower, and the container storage directory will be the upper. Modifications to the mount point are destroyed when the `RUN` command finishes executing, similar to a tmpfs mount point. Any subsequent execution of `RUN` commands sees the original source directory content, any changes from previous RUN commands no longer exist. One use case of the `overlay` mount is sharing the package cache from the host into the container to allow speeding up builds. Note: - The `O` flag is not allowed to be specified with the `Z` or `z` flags. Content mounted into the container is labeled with the private label. On SELinux systems, labels in the source directory need to be readable by the container label. If not, SELinux container separation must be disabled for the container to work. - Modification of the directory volume mounted into the container with an overlay mount can cause unexpected failures. It is recommended that you do not modify the directory until the container finishes running. By default bind mounted volumes are `private`. That means any mounts done inside container will not be visible on the host and vice versa. This behavior can be changed by specifying a volume mount propagation property. When the mount propagation policy is set to `shared`, any mounts completed inside the container on that volume will be visible to both the host and container. When the mount propagation policy is set to `slave`, one way mount propagation is enabled and any mounts completed on the host for that volume will be visible only inside of the container. To control the mount propagation property of the volume use the `:[r]shared`, `:[r]slave`, `[r]private` or `[r]unbindable`propagation flag. The propagation property can be specified only for bind mounted volumes and not for internal volumes or named volumes. For mount propagation to work on the source mount point (the mount point where source dir is mounted on) it has to have the right propagation properties. For shared volumes, the source mount point has to be shared. And for slave volumes, the source mount has to be either shared or slave. [[1]](#Footnote1) Use `df ` to determine the source mount and then use `findmnt -o TARGET,PROPAGATION ` to determine propagation properties of source mount, if `findmnt` utility is not available, the source mount point can be determined by looking at the mount entry in `/proc/self/mountinfo`. Look at `optional fields` and see if any propagation properties are specified. `shared:X` means the mount is `shared`, `master:X` means the mount is `slave` and if nothing is there that means the mount is `private`. [[1]](#Footnote1) To change propagation properties of a mount point use the `mount` command. For example, to bind mount the source directory `/foo` do `mount --bind /foo /foo` and `mount --make-private --make-shared /foo`. This will convert /foo into a `shared` mount point. The propagation properties of the source mount can be changed directly. For instance if `/` is the source mount for `/foo`, then use `mount --make-shared /` to convert `/` into a `shared` mount. ## EXAMPLE buildah from --pull imagename buildah from --pull docker://myregistry.example.com/imagename buildah from docker-daemon:imagename:imagetag buildah from --name mycontainer docker-archive:filename buildah from oci-archive:filename buildah from --name mycontainer dir:directoryname buildah from --pull-always --name "mycontainer" myregistry.example.com/imagename buildah from --tls-verify=false myregistry/myrepository/imagename:imagetag buildah from --creds=myusername:mypassword --cert-dir ~/auth myregistry/myrepository/imagename:imagetag buildah from --authfile=/tmp/auths/myauths.json myregistry/myrepository/imagename:imagetag buildah from --memory 40m --cpu-shares 2 --cpuset-cpus 0,2 --security-opt label=level:s0:c100,c200 myregistry/myrepository/imagename:imagetag buildah from --ulimit nofile=1024:1028 --cgroup-parent /path/to/cgroup/parent myregistry/myrepository/imagename:imagetag buildah from --volume /home/test:/myvol:ro,Z myregistry/myrepository/imagename:imagetag buildah from -v /home/test:/myvol:z,U myregistry/myrepository/imagename:imagetag buildah from -v /var/lib/yum:/var/lib/yum:O myregistry/myrepository/imagename:imagetag buildah from --arch=arm --variant v7 myregistry/myrepository/imagename:imagetag ## ENVIRONMENT **BUILD\_REGISTRY\_SOURCES** BUILD\_REGISTRY\_SOURCES, if set, is treated as a JSON object which contains lists of registry names under the keys `insecureRegistries`, `blockedRegistries`, and `allowedRegistries`. When pulling an image from a registry, if the name of the registry matches any of the items in the `blockedRegistries` list, the image pull attempt is denied. If there are registries in the `allowedRegistries` list, and the registry's name is not in the list, the pull attempt is denied. **TMPDIR** The TMPDIR environment variable allows the user to specify where temporary files are stored while pulling and pushing images. Defaults to '/var/tmp'. ## FILES **registries.conf** (`/etc/containers/registries.conf`) registries.conf is the configuration file which specifies which container registries should be consulted when completing image names which do not include a registry or domain portion. **policy.json** (`/etc/containers/policy.json`) Signature policy file. This defines the trust policy for container images. Controls which container registries can be used for image, and whether or not the tool should trust the images. ## SEE ALSO buildah(1), buildah-pull(1), buildah-login(1), docker-login(1), namespaces(7), pid\_namespaces(7), containers-policy.json(5), containers-registries.conf(5), user\_namespaces(7), containers.conf(5), containers-auth.json(5) ## FOOTNOTES 1: The Buildah project is committed to inclusivity, a core value of open source. The `master` and `slave` mount propagation terminology used here is problematic and divisive, and should be changed. However, these terms are currently used within the Linux kernel and must be used as-is at this time. When the kernel maintainers rectify this usage, Buildah will follow suit immediately. ================================================ FILE: docs/buildah-images.1.md ================================================ # buildah-images "1" "March 2017" "buildah" ## NAME buildah\-images - List images in local storage. ## SYNOPSIS **buildah images** [*options*] [*image*] ## DESCRIPTION Displays locally stored images, their names, sizes, created date and their IDs. The created date is displayed in the time locale of the local machine. ## OPTIONS **--all**, **-a** Show all images, including intermediate images from a build. **--digests** Show the image digests. **--filter**, **-f**=[] Filter output based on conditions provided (default []). Filters: **after,since=image** Filter on images created since the given image. **before=image** Filter on images created before the given image. **dangling=true|false** Show dangling images. An images is considered to be dangling if it has no associated names and tags. **id=id** Show image with this specific ID. **intermediate=true|false** Show intermediate images. An images is considered to be an indermediate image if it is dangling and has no children. **label=key[=value]** Filter by images labels key and/or value. **readonly=true|false** Show only read only images or Read/Write images. The default is to show both. Read/Only images can be configured by modifying the "additionalimagestores" in the /etc/containers/storage.conf file. **reference=reference** Show images matching the specified reference. Wildcards are supported (e.g., "reference=*fedora:3*"). **--format**="TEMPLATE" Pretty-print images using a Go template. Valid placeholders for the Go template are listed below: | **Placeholder** | **Description** | | --------------- | -----------------------------------------| | .Created | Creation date in epoch time | | .CreatedAt | Creation date Pretty Formatted | | .CreatedAtRaw | Creation date in raw format | | .Digest | Image Digest | | .ID | Image ID | | .Name | Image Name | | .ReadOnly | Indicates if image came from a R/O store | | .Size | Image Size | | .Tag | Image Tag | **--history** Display the image name history. **--json** Display the output in JSON format. **--no-trunc** Do not truncate output. **--noheading**, **-n** Omit the table headings from the listing of images. **--quiet**, **-q** Displays only the image IDs. ## EXAMPLE buildah images buildah images fedora:latest buildah images --json buildah images --quiet buildah images -q --noheading --no-trunc buildah images --quiet fedora:latest buildah images --filter dangling=true buildah images --format "ImageID: {{.ID}}" ``` $ buildah images REPOSITORY TAG IMAGE ID CREATED SIZE registry.access.redhat.com/ubi8 latest 53ce4390f2ad 3 weeks ago 233 MB docker.io/library/busybox latest 16ea53ea7c65 3 weeks ago 1.46 MB quay.io/libpod/testimage 20210610 9f9ec7f2fdef 4 months ago 7.99 MB ``` ``` # buildah images -a IMAGE NAME IMAGE TAG IMAGE ID CREATED AT SIZE registry.access.redhat.com/ubi8 latest 53ce4390f2ad 3 weeks ago 233 MB 8c6e16890c2b Jun 13, 2018 15:52 4.42 MB localhost/test latest c0cfe75da054 Jun 13, 2018 15:52 4.42 MB ``` ``` # buildah images --format '{{.ID}} {{.CreatedAtRaw}}' 3f53bb00af943dfdf815650be70c0fa7b426e56a66f5e3362b47a129d57d5991 2018-12-20 19:21:30.122610396 -0500 EST 8e09da8f6701d7cde1526d79e3123b0f1109b78d925dfe9f9bac6d59d702a390 2019-01-08 09:22:52.330623532 -0500 EST ``` ``` # buildah images --format '{{.ID}} {{.Name}} {{.Digest}} {{.CreatedAt}} {{.Size}} {{.CreatedAtRaw}}' 3f53bb00af943dfdf815650be70c0fa7b426e56a66f5e3362b47a129d57d5991 docker.io/library/alpine sha256:3d2e482b82608d153a374df3357c0291589a61cc194ec4a9ca2381073a17f58e Dec 20, 2018 19:21 4.67 MB 2018-12-20 19:21:30.122610396 -0500 EST 8e09da8f6701d7cde1526d79e3123b0f1109b78d925dfe9f9bac6d59d702a390 sha256:894532ec56e0205ce68ca7230b00c18aa3c8ee39fcdb310615c60e813057229c Jan 8, 2019 09:22 4.67 MB 2019-01-08 09:22:52.330623532 -0500 EST ``` ## SEE ALSO buildah(1), containers-storage.conf(5) ================================================ FILE: docs/buildah-info.1.md ================================================ # buildah-info "1" "November 2018" "Buildah" ## NAME buildah\-info - Display Buildah system information. ## SYNOPSIS **buildah info** [*options*] ## DESCRIPTION The information displayed pertains to the host and current storage statistics which is useful when reporting issues. ## OPTIONS **--debug**, **-d** Show additional information. **--format** *template* Use *template* as a Go template when formatting the output. ## EXAMPLE Run buildah info response: ``` $ buildah info { "host": { "Distribution": { "distribution": "ubuntu", "version": "18.04" }, "MemTotal": 16702980096, "MemFree": 309428224, "SwapFree": 2146693120, "SwapTotal": 2147479552, "arch": "amd64", "cpus": 4, "hostname": "localhost.localdomain", "kernel": "4.15.0-36-generic", "os": "linux", "rootless": false, "uptime": "91h 30m 59.9s (Approximately 3.79 days)" }, "store": { "ContainerStore": { "number": 2 }, "GraphDriverName": "overlay", "GraphOptions": [ "overlay.override_kernel_check=true" ], "GraphRoot": "/var/lib/containers/storage", "GraphStatus": { "Backing Filesystem": "extfs", "Native Overlay Diff": "true", "Supports d_type": "true" }, "ImageStore": { "number": 1 }, "RunRoot": "/run/containers/storage" } } ``` Run buildah info and retrieve only the store information: ``` $ buildah info --format={{".store"}} map[GraphOptions:[overlay.override_kernel_check=true] GraphStatus:map[Backing Filesystem:extfs Supports d_type:true Native Overlay Diff:true] ImageStore:map[number:1] ContainerStore:map[number:2] GraphRoot:/var/lib/containers/storage RunRoot:/run/containers/storage GraphDriverName:overlay] ``` ## SEE ALSO buildah(1) ================================================ FILE: docs/buildah-inspect.1.md ================================================ # buildah-inspect "1" "May 2017" "buildah" ## NAME buildah\-inspect - Display information about working containers or images or manifest lists. ## SYNOPSIS **buildah inspect** [*options*] [**--**] *object* ## DESCRIPTION Prints the low-level information on Buildah object(s) (e.g. container, images, manifest lists) identified by name or ID. By default, this will render all results in a JSON array. If the container, image, or manifest lists have the same name, this will return container JSON for an unspecified type. If a format is specified, the given template will be executed for each result. ## OPTIONS **--format**, **-f** *template* Use *template* as a Go template when formatting the output. Users of this option should be familiar with the [*text/template* package](https://golang.org/pkg/text/template/) in the Go standard library, and of internals of Buildah's implementation. **--type**, **-t** **container** | **image** | **manifest** Specify whether *object* is a container, image or a manifest list. ## EXAMPLE buildah inspect containerID buildah inspect --type container containerID buildah inspect --type image imageID buildah inspect --format '{{.OCIv1.Config.Env}}' alpine ## SEE ALSO buildah(1) ================================================ FILE: docs/buildah-login.1.md ================================================ # buildah-login "1" "Apr 2019" "buildah" ## NAME buildah\-login - Login to a container registry ## SYNOPSIS **buildah login** [*options*] *registry* ## DESCRIPTION **buildah login** logs into a specified registry server with the correct username and password. **buildah login** reads in the username and password from STDIN. The username and password can also be set using the **username** and **password** flags. The path of the authentication file can be specified by the user by setting the **authfile** flag. The default path used is **${XDG\_RUNTIME_DIR}/containers/auth.json**. If XDG_RUNTIME_DIR is not set, the default is /run/user/$UID/containers/auth.json. **buildah [GLOBAL OPTIONS]** **buildah login [GLOBAL OPTIONS]** **buildah login [OPTIONS] REGISTRY [GLOBAL OPTIONS]** ## OPTIONS **--authfile** Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json. See containers-auth.json(5) for more information. This file is created using `buildah login`. Note: You can also override the default path of the authentication file by setting the REGISTRY\_AUTH\_FILE environment variable. `export REGISTRY_AUTH_FILE=path` **--cert-dir** *path* Use certificates at *path* (\*.crt, \*.cert, \*.key) to connect to the registry. The default certificates directory is _/etc/containers/certs.d_. **--compat-auth-file**=*path* Instead of updating the default credentials file, update the one at *path*, and use a Docker-compatible format. **--get-login** Return the logged-in user for the registry. Return error if no login is found. **--help**, **-h** Print usage statement **--password**, **-p** Password for registry **--password-stdin** Take the password from stdin **--tls-verify** Require HTTPS and verification of certificates when talking to container registries (default: true). If explicitly set to true, then TLS verification will be used. If set to false, then TLS verification will not be used. If not specified, TLS verification will be used unless the target registry is listed as an insecure registry in registries.conf. TLS verification cannot be used when talking to an insecure registry. **--username**, **-u** Username for registry **--verbose**, **-v** print detailed information about credential store ## EXAMPLES ``` $ buildah login quay.io Username: qiwanredhat Password: Login Succeeded! ``` ``` $ buildah login -u testuser -p testpassword localhost:5000 Login Succeeded! ``` ``` $ buildah login --authfile ./auth.json quay.io Username: qiwanredhat Password: Login Succeeded! ``` ``` $ buildah login --tls-verify=false -u test -p test localhost:5000 Login Succeeded! ``` ``` $ buildah login --cert-dir /etc/containers/certs.d/ -u foo -p bar localhost:5000 Login Succeeded! ``` ``` $ buildah login -u testuser --password-stdin < pw.txt quay.io Login Succeeded! ``` ``` $ echo $testpassword | buildah login -u testuser --password-stdin quay.io Login Succeeded! ``` ## SEE ALSO buildah(1), buildah-logout(1), containers-auth.json(5) ================================================ FILE: docs/buildah-logout.1.md ================================================ # buildah-logout "1" "Apr 2019" "buildah" ## NAME buildah\-logout - Logout of a container registry ## SYNOPSIS **buildah logout** [*options*] *registry* ## DESCRIPTION **buildah logout** logs out of a specified registry server by deleting the cached credentials stored in the **auth.json** file. The path of the authentication file can be overridden by the user by setting the **authfile** flag. The default path used is **${XDG\_RUNTIME_DIR}/containers/auth.json**. See containers-auth.json(5) for more information. All the cached credentials can be removed by setting the **all** flag. **buildah [GLOBAL OPTIONS]** **buildah logout [GLOBAL OPTIONS]** **buildah logout [OPTIONS] REGISTRY [GLOBAL OPTIONS]** ## OPTIONS **--all**, **-a** Remove the cached credentials for all registries in the auth file **--authfile** Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json. See containers-auth.json(5) for more information. Note: You can also override the default path of the authentication file by setting the REGISTRY\_AUTH\_FILE environment variable. `export REGISTRY_AUTH_FILE=path` **--compat-auth-file**=*path* Instead of updating the default credentials file, update the one at *path*, and use a Docker-compatible format. **--help**, **-h** Print usage statement ## EXAMPLES ``` $ buildah logout quay.io Removed login credentials for quay.io ``` ``` $ buildah logout --authfile authdir/myauths.json quay.io Removed login credentials for quay.io ``` ``` $ buildah logout --all Remove login credentials for all registries ``` ## SEE ALSO buildah(1), buildah-login(1), containers-auth.json(5) ================================================ FILE: docs/buildah-manifest-add.1.md ================================================ # buildah-manifest-add "1" "September 2019" "buildah" ## NAME buildah\-manifest\-add - Add an image or artifact to a manifest list or image index. ## SYNOPSIS **buildah manifest add** [options...] *listNameOrIndexName* *imageOrArtifactName* [...] ## DESCRIPTION Adds the specified image to the specified manifest list or image index, or creates an artifact manifest and adds it to the specified image index. ## RETURN VALUE The list image's ID and the digest of the image's manifest. ## OPTIONS **--all** If the image which should be added to the list or index is itself a list or index, add all of the contents to the local list. By default, only one image from such a list or index will be added to the list or index. Combining *--all* with any of the other options described below is NOT recommended. **--annotation** *annotation=value* Set an annotation on the entry for the newly-added image or artifact manifest. **--arch** Override the architecture which the list or index records as a requirement for the image. If *imageName* refers to a manifest list or image index, the architecture information will be retrieved from it. Otherwise, it will be retrieved from the image's configuration information. **--artifact** Create an artifact manifest and add it to the image index. Arguments after the index name will be interpreted as file names rather than as image references. In most scenarios, the **--artifact-type** option should also be specified. **--artifact-annotation** *annotation=value* When creating an artifact manifest and adding it to the image index, set an annotation in the artifact manifest. **--artifact-config** *filename* When creating an artifact manifest and adding it to the image index, use the specified file's contents as the configuration blob in the artifact manifest. In most scenarios, leaving the default value, which signifies an empty configuration, unchanged, is the preferred option. **--artifact-config-type** *type* When creating an artifact manifest and adding it to the image index, use the specified MIME type as the `mediaType` associated with the configuration blob in the artifact manifest. In most scenarios, leaving the default value, which signifies either an empty configuration or the standard OCI configuration type, unchanged, is the preferred option. **--artifact-exclude-titles** When creating an artifact manifest and adding it to the image index, do not set "org.opencontainers.image.title" annotations equal to the file's basename for each file added to the artifact manifest. Tools which retrieve artifacts from a registry may use these values to choose names for files when saving artifacts to disk, so this option is not recommended unless it is required for interoperability with a particular registry. **--artifact-layer-type** *type* When creating an artifact manifest and adding it to the image index, use the specified MIME type as the `mediaType` associated with the files' contents. If not specified, guesses based on either the files names or their contents will be made and used, but the option should be specified if certainty is needed. **--artifact-subject** *imageName* When creating an artifact manifest and adding it to the image index, set the *subject* field in the artifact manifest to mark the artifact manifest as being associated with the specified image in some way. An artifact manifest can only be associated with, at most, one subject. **--artifact-type** *type* When creating an artifact manifest, use the specified MIME type as the manifest's `artifactType` value instead of the less informative default value. **--authfile** *path* Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json. See containers-auth.json(5) for more information. This file is created using `buildah login`. If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using `docker login`. Note: You can also override the default path of the authentication file by setting the REGISTRY\_AUTH\_FILE environment variable. `export REGISTRY_AUTH_FILE=path` **--cert-dir** *path* Use certificates at *path* (\*.crt, \*.cert, \*.key) to connect to the registry. The default certificates directory is _/etc/containers/certs.d_. **--creds** *creds* The [username[:password]] to use to authenticate with the registry if required. If one or both values are not supplied, a command line prompt will appear and the value can be entered. The password is entered without echo. **--features** Specify the features list which the list or index records as requirements for the image. This option is rarely used. **--os** Override the OS which the list or index records as a requirement for the image. If *imageName* refers to a manifest list or image index, the OS information will be retrieved from it. Otherwise, it will be retrieved from the image's configuration information. **--os-features** Specify the OS features list which the list or index records as requirements for the image. This option is rarely used. **--os-version** Specify the OS version which the list or index records as a requirement for the image. This option is rarely used. **--tls-verify** *bool-value* Require HTTPS and verification of certificates when talking to container registries (defaults to true). TLS verification cannot be used when talking to an insecure registry. **--variant** Specify the variant which the list or index records for the image. This option is typically used to distinguish between multiple entries which share the same architecture value, but which expect different versions of its instruction set. ## EXAMPLE ``` buildah manifest add mylist:v1.11 docker://fedora 506d8f4bb54931ea03a7e70173a0ed6302e3fb92dfadb3955ba5c17812e95c51: sha256:f81f09918379d5442d20dff82a298f29698197035e737f76e511d5af422cabd7 ``` ``` buildah manifest add --all mylist:v1.11 docker://fedora 506d8f4bb54931ea03a7e70173a0ed6302e3fb92dfadb3955ba5c17812e95c51: sha256:f81f09918379d5442d20dff82a298f29698197035e737f76e511d5af422cabd7 ``` ``` buildah manifest add --arch arm64 --variant v8 mylist:v1.11 docker://fedora@sha256:c829b1810d2dbb456e74a695fd3847530c8319e5a95dca623e9f1b1b89020d8b 506d8f4bb54931ea03a7e70173a0ed6302e3fb92dfadb3955ba5c17812e95c51: sha256:c829b1810d2dbb456e74a695fd3847530c8319e5a95dca623e9f1b1b89020d8b ``` ``` buildah manifest add --artifact --artifact-type application/x-cd-image mylist:v1.11 ./imagefile.iso 506d8f4bb54931ea03a7e70173a0ed6302e3fb92dfadb3955ba5c17812e95c51: sha256:1768fae728f6f8ff3d0f8c7df409d7f4f0ca5c89b070810bd4aa4a2ed2eca8bb ``` ## SEE ALSO buildah(1), buildah-login(1), buildah-manifest(1), buildah-manifest-create(1), buildah-manifest-remove(1), buildah-manifest-annotate(1), buildah-manifest-inspect(1), buildah-manifest-push(1), buildah-rmi(1), docker-login(1), containers-auth.json(5) ================================================ FILE: docs/buildah-manifest-annotate.1.md ================================================ # buildah-manifest-annotate "1" "September 2019" "buildah" ## NAME buildah\-manifest\-annotate - Add and update information about an image or artifact to a manifest list or image index. ## SYNOPSIS **buildah manifest annotate** [options...] *listNameOrIndexName* *imageManifestDigestOrImageOrArtifactName* ## DESCRIPTION Adds or updates information about an image or artifact included in a manifest list or image index. ## RETURN VALUE The list image's ID and the digest of the image's manifest. ## OPTIONS **--annotation** *annotation=value* Set an annotation on the entry for the specified image or artifact. If **--index** is also specified, sets the annotation on the entire image index. **--arch** Override the architecture which the list or index records as a requirement for the image. This is usually automatically retrieved from the image's configuration information, so it is rarely necessary to use this option. **--features** Specify the features list which the list or index records as requirements for the image. This option is rarely used. **--index** Treats arguments to the **--annotation** option as annotation values to be set on the image index itself rather than on an entry in the image index. Implied for **--subject**. **--os** Override the OS which the list or index records as a requirement for the image. This is usually automatically retrieved from the image's configuration information, so it is rarely necessary to use this option. **--os-features** Specify the OS features list which the list or index records as requirements for the image. This option is rarely used. **--os-version** Specify the OS version which the list or index records as a requirement for the image. This option is rarely used. **--subject** *imageName* Set the *subject* field in the image index to mark the image index as being associated with the specified image in some way. An image index can only be associated with, at most, one subject. **--variant** Specify the variant which the list or index records for the image. This option is typically used to distinguish between multiple entries which share the same architecture value, but which expect different versions of its instruction set. ## EXAMPLE ``` buildah manifest annotate --arch arm64 --variant v8 mylist:v1.11 sha256:c829b1810d2dbb456e74a695fd3847530c8319e5a95dca623e9f1b1b89020d8b 506d8f4bb54931ea03a7e70173a0ed6302e3fb92dfadb3955ba5c17812e95c51: sha256:c829b1810d2dbb456e74a695fd3847530c8319e5a95dca623e9f1b1b89020d8b ``` ``` buildah manifest annotate --index --annotation food=yummy mylist:v1.11 506d8f4bb54931ea03a7e70173a0ed6302e3fb92dfadb3955ba5c17812e95c51: sha256:c829b1810d2dbb456e74a695fd3847530c8319e5a95dca623e9f1b1b89020d8b ``` ## SEE ALSO buildah(1), buildah-manifest(1), buildah-manifest-create(1), buildah-manifest-add(1), buildah-manifest-remove(1), buildah-manifest-inspect(1), buildah-manifest-push(1), buildah-rmi(1) ================================================ FILE: docs/buildah-manifest-create.1.md ================================================ # buildah-manifest-create "1" "August 2022" "buildah" ## NAME buildah\-manifest\-create - Create a manifest list or image index. ## SYNOPSIS **buildah manifest create** [options...] *listNameOrIndexName* [*imageName* ...] ## DESCRIPTION Creates a new manifest list and stores it as an image in local storage using the specified name. If additional images are specified, they are added to the newly-created list or index. ## RETURN VALUE The randomly-generated image ID of the newly-created list or index. The image can be deleted using the *buildah rmi* command. ## OPTIONS **--all** If any of the images which should be added to the new list or index are themselves lists or indexes, add all of their contents. By default, only one image from such a list will be added to the newly-created list or index. **--amend** If a manifest list named *listNameOrIndexName* already exists, modify the preexisting list instead of exiting with an error. The contents of *listNameOrIndexName* are not modified if no *imageName*s are given. **--annotation** *annotation=value* Set an annotation on the newly-created image index. **--tls-verify** *bool-value* Require HTTPS and verification of certificates when talking to container registries (defaults to true). TLS verification cannot be used when talking to an insecure registry. ## EXAMPLE ``` buildah manifest create mylist:v1.11 941c1259e4b85bebf23580a044e4838aa3c1e627528422c9bf9262ff1661fca9 buildah manifest create --amend mylist:v1.11 941c1259e4b85bebf23580a044e4838aa3c1e627528422c9bf9262ff1661fca9 ``` ``` buildah manifest create mylist:v1.11 docker://fedora 941c1259e4b85bebf23580a044e4838aa3c1e627528422c9bf9262ff1661fca9 ``` ``` buildah manifest create --all mylist:v1.11 docker://fedora 941c1259e4b85bebf23580a044e4838aa3c1e627528422c9bf9262ff1661fca9 ``` ## SEE ALSO buildah(1), buildah-manifest(1), buildah-manifest-add(1), buildah-manifest-remove(1), buildah-manifest-annotate(1), buildah-manifest-inspect(1), buildah-manifest-push(1), buildah-rmi(1) ================================================ FILE: docs/buildah-manifest-exists.1.md ================================================ % buildah-manifest-exists(1) ## NAME buildah\-manifest\-exists - Check if the given manifest list exists in local storage ## SYNOPSIS **buildah manifest exists** *manifest* ## DESCRIPTION **buildah manifest exists** checks if a manifest list exists in local storage. Buildah will return an exit code of `0` when the manifest list is found. A `1` will be returned otherwise. An exit code of `125` indicates there was another issue. ## OPTIONS #### **--help**, **-h** Print usage statement. ## EXAMPLE Check if a manifest list called `list1` exists (the manifest list does actually exist). ``` $ buildah manifest exists list1 $ echo $? 0 $ ``` Check if an manifest called `mylist` exists (the manifest list does not actually exist). ``` $ buildah manifest exists mylist $ echo $? 1 $ ``` ## SEE ALSO **[buildah(1)](buildah.1.md)**, **[buildah-manifest(1)](buildah-manifest.1.md)** ================================================ FILE: docs/buildah-manifest-inspect.1.md ================================================ # buildah-manifest-inspect "1" "September 2019" "buildah" ## NAME buildah\-manifest\-inspect - Display a manifest list or image index. ## SYNOPSIS **buildah manifest inspect** *listNameOrIndexName* ## DESCRIPTION Displays the manifest list or image index stored using the specified image name. ## RETURN VALUE A formatted JSON representation of the manifest list or image index. ## OPTIONS **--authfile** *path* Path of the authentication file. Default is ${XDG\_RUNTIME\_DIR}/containers/auth.json, which is set using `buildah login`. If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using `docker login`. **--tls-verify** *bool-value* Require HTTPS and verification of certificates when talking to container registries (defaults to true). TLS verification cannot be used when talking to an insecure registry. ## EXAMPLE ``` buildah manifest inspect mylist:v1.11 ``` ## SEE ALSO buildah(1), buildah-manifest(1), buildah-manifest-create(1), buildah-manifest-add(1), buildah-manifest-remove(1), buildah-manifest-annotate(1), buildah-manifest-push(1), buildah-rmi(1) ================================================ FILE: docs/buildah-manifest-push.1.md ================================================ # buildah-manifest-push "1" "September 2019" "buildah" ## NAME buildah\-manifest\-push - Push a manifest list or image index to a registry. ## SYNOPSIS **buildah manifest push** [options...] *listNameOrIndexName* *transport:details* ## DESCRIPTION Pushes a manifest list or image index to a registry. ## RETURN VALUE The list image's ID and the digest of the image's manifest. ## OPTIONS **--add-compression** *compression* Makes sure that requested compression variant for each platform is added to the manifest list keeping original instance intact in the same manifest list. Supported values are (`gzip`, `zstd` and `zstd:chunked`) Note: This is different than `--compression` which replaces the instance with requested with specified compression while `--add-compression` makes sure than each instance has it variant added to manifest list without modifying the original instance. **--all** Push the images mentioned in the manifest list or image index, in addition to the list or index itself. (Default true) **--authfile** *path* Path of the authentication file. Default is ${XDG\_RUNTIME\_DIR}/containers/auth.json, which is set using `buildah login`. If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using `docker login`. **--cert-dir** *path* Use certificates at *path* (\*.crt, \*.cert, \*.key) to connect to the registry. The default certificates directory is _/etc/containers/certs.d_. **--compression-format** *format* Specifies the compression format to use. Supported values are: `gzip`, `zstd` and `zstd:chunked`. **--compression-level** *level* Specify the compression level used with the compression. Specifies the compression level to use. The value is specific to the compression algorithm used, e.g. for zstd the accepted values are in the range 1-20 (inclusive), while for gzip it is 1-9 (inclusive). **--creds** *creds* The [username[:password]] to use to authenticate with the registry if required. If one or both values are not supplied, a command line prompt will appear and the value can be entered. The password is entered without echo. **--digestfile** *Digestfile* After copying the image, write the digest of the resulting image to the file. **--force-compression** If set, push uses the specified compression algorithm even if the destination contains a differently-compressed variant already. Defaults to `true` if `--compression-format` is explicitly specified on the command-line, `false` otherwise. **--format**, **-f** Manifest list type (oci or v2s2) to use when pushing the list (default is oci). **--quiet**, **-q** Don't output progress information when pushing lists. **--remove-signatures** Don't copy signatures when pushing images. **--retry** *attempts* Number of times to retry in case of failure when performing push of images to registry. Defaults to `3`. **--retry-delay** *duration* Duration of delay between retry attempts in case of failure when performing push of images to registry. Defaults to `2s`. **--rm** Delete the manifest list or image index from local storage if pushing succeeds. **--sign-by** *fingerprint* Sign the pushed images using the GPG key that matches the specified fingerprint. **--tls-verify** *bool-value* Require HTTPS and verification of certificates when talking to container registries (defaults to true). TLS verification cannot be used when talking to an insecure registry. ## EXAMPLE ``` buildah manifest push mylist:v1.11 registry.example.org/mylist:v1.11 ``` ## SEE ALSO buildah(1), buildah-login(1), buildah-manifest(1), buildah-manifest-create(1), buildah-manifest-add(1), buildah-manifest-remove(1), buildah-manifest-annotate(1), buildah-manifest-inspect(1), buildah-rmi(1), docker-login(1) ================================================ FILE: docs/buildah-manifest-remove.1.md ================================================ # buildah-manifest-remove "1" "September 2019" "buildah" ## NAME buildah\-manifest\-remove - Remove an image from a manifest list or image index. ## SYNOPSIS **buildah manifest remove** *listNameOrIndexName* *imageNameOrManifestDigestOrArtifactName* ## DESCRIPTION Removes the image with the specified name or digest from the specified manifest list or image index, or the specified artifact from the specified image index. ## RETURN VALUE The list image's ID and the digest of the removed image's manifest. ## EXAMPLE ``` buildah manifest remove mylist:v1.11 sha256:f81f09918379d5442d20dff82a298f29698197035e737f76e511d5af422cabd7 506d8f4bb54931ea03a7e70173a0ed6302e3fb92dfadb3955ba5c17812e95c51: sha256:f81f09918379d5442d20dff82a298f29698197035e737f76e511d5af422cabd7 ``` ## SEE ALSO buildah(1), buildah-manifest(1), buildah-manifest-create(1), buildah-manifest-add(1), buildah-manifest-annotate(1), buildah-manifest-inspect(1), buildah-manifest-push(1), buildah-rmi(1) ================================================ FILE: docs/buildah-manifest-rm.1.md ================================================ # buildah-manifest-rm "1" "April 2021" "buildah" ## NAME buildah\-manifest\-rm - Removes one or more manifest lists. ## SYNOPSIS **buildah manifest rm** [*listNameOrIndexName* ...] ## DESCRIPTION Removes one or more locally stored manifest lists. ## EXAMPLE buildah manifest rm buildah manifest-rm listID1 listID2 **storage.conf** (`/etc/containers/storage.conf`) storage.conf is the storage configuration file for all tools using containers/storage The storage configuration file specifies all of the available container storage options for tools using shared container storage. ## SEE ALSO buildah(1), containers-storage.conf(5), buildah-manifest(1) ================================================ FILE: docs/buildah-manifest.1.md ================================================ # buildah-manifest "1" "September 2019" "buildah" ## NAME buildah-manifest - Create and manipulate manifest lists and image indexes. ## SYNOPSIS buildah manifest COMMAND [OPTIONS] [ARG...] ## DESCRIPTION The `buildah manifest` command provides subcommands which can be used to: * Create a working Docker manifest list or OCI image index. * Add an entry to a manifest list or image index for a specified image. * Add an entry to an image index for an artifact manifest referring to a file. * Add or update information about an entry in a manifest list or image index. * Delete a working container or an image. * Push a manifest list or image index to a registry or other location. ## SUBCOMMANDS | Command | Man Page | Description | | ------- | -------------------------------------------------------------- | --------------------------------------------------------------------------- | | add | [buildah-manifest-add(1)](buildah-manifest-add.1.md) | Add an image or artifact to a manifest list or image index. | | annotate | [buildah-manifest-annotate(1)](buildah-manifest-annotate.1.md) | Add or update information about an image or artifact in a manifest list or image index. | | create | [buildah-manifest-create(1)](buildah-manifest-create.1.md) | Create a manifest list or image index. | | exists | [buildah-manifest-exists(1)](buildah-manifest-exists.1.md) | Check if a manifest list exists in local storage. | | inspect | [buildah-manifest-inspect(1)](buildah-manifest-inspect.1.md) | Display the contents of a manifest list or image index. | | push | [buildah-manifest-push(1)](buildah-manifest-push.1.md) | Push a manifest list or image index to a registry or other location. | | remove | [buildah-manifest-remove(1)](buildah-manifest-remove.1.md) | Remove an image from a manifest list or image index. | | rm | [buildah-manifest-rm(1)](buildah-manifest-rm.1.md) | Remove manifest list from local storage. | ## EXAMPLES ### Building a multi-arch manifest list from a Containerfile Assuming the `Containerfile` uses `RUN` instructions, the host needs a way to execute non-native binaries. Configuring this is beyond the scope of this example. Building a multi-arch manifest list `shazam` in parallel across 4-threads can be done like this: $ platarch=linux/amd64,linux/ppc64le,linux/arm64,linux/s390x $ buildah build --jobs=4 --platform=$platarch --manifest shazam . **Note:** The `--jobs` argument is optional, and the `--manifest` option should be used instead of the`-t` or `--tag` options. ### Assembling a multi-arch manifest from separately built images Assuming `example.com/example/shazam:$arch` images are built separately on other hosts and pushed to the `example.com` registry. They may be combined into a manifest list, and pushed using a simple loop: $ REPO=example.com/example/shazam $ buildah manifest create $REPO:latest $ for IMGTAG in amd64 s390x ppc64le arm64; do \ buildah manifest add $REPO:latest docker://$REPO:IMGTAG; \ done $ buildah manifest push --all $REPO:latest **Note:** The `add` instruction argument order is `` then ``. Also, the `--all` push option is required to ensure all contents are pushed, not just the native platform/arch. ### Removing and tagging a manifest list before pushing Special care is needed when removing and pushing manifest lists, as opposed to the contents. You almost always want to use the `manifest rm` and `manifest push --all` subcommands. For example, a rename and push could be performed like this: $ buildah tag localhost/shazam example.com/example/shazam $ buildah manifest rm localhost/shazam $ buildah manifest push --all example.com/example/shazam ## SEE ALSO buildah(1), buildah-manifest-create(1), buildah-manifest-add(1), buildah-manifest-remove(1), buildah-manifest-annotate(1), buildah-manifest-inspect(1), buildah-manifest-push(1), buildah-manifest-rm(1) ================================================ FILE: docs/buildah-mkcw.1.md ================================================ # buildah-mkcw "1" "July 2023" "buildah" ## NAME buildah\-mkcw - Convert a conventional container image into a confidential workload image. ## SYNOPSIS **buildah mkcw** [*options*] *source* *destination* ## DESCRIPTION Converts the contents of a container image into a new container image which is suitable for use in a trusted execution environment (TEE), typically run using krun (i.e., crun built with the libkrun feature enabled and invoked as *krun*). Instead of the conventional contents, the root filesystem of the created image will contain an encrypted disk image and configuration information for krun. ## source A container image, stored locally or in a registry ## destination A container image, stored locally or in a registry ## OPTIONS **--add-file** *source[:destination]* Read the contents of the file `source` and add it to the committed image as a file at `destination`. If `destination` is not specified, the path of `source` will be used. The new file will be owned by UID 0, GID 0, have 0644 permissions, and be given a current timestamp. This option can be specified multiple times. **--attestation-url**, **-u** *url* The location of a key broker / attestation server. If a value is specified, the new image's workload ID, along with the passphrase used to encrypt the disk image, will be registered with the server, and the server's location will be stored in the container image. At run-time, krun is expected to contact the server to retrieve the passphrase using the workload ID, which is also stored in the container image. If no value is specified, a *passphrase* value *must* be specified. **--base-image**, **-b** *image* An alternate image to use as the base for the output image. By default, the *scratch* non-image is used. **--cpus**, **-c** *number* The number of virtual CPUs which the image expects to be run with at run-time. If not specified, a default value will be supplied. **--firmware-library**, **-f** *file* The location of the libkrunfw-sev shared library. If not specified, `buildah` checks for its presence in a number of hard-coded locations. **--memory**, **-m** *number* The amount of memory which the image expects to be run with at run-time, as a number of megabytes. If not specified, a default value will be supplied. **--passphrase**, **-p** *text* The passphrase to use to encrypt the disk image which will be included in the container image. If no value is specified, but an *--attestation-url* value is specified, a randomly-generated passphrase will be used. The authors recommend setting an *--attestation-url* but not a *--passphrase*. **--slop**, **-s** *{percentage%|sizeKB|sizeMB|sizeGB}* Extra space to allocate for the disk image compared to the size of the container image's contents, expressed either as a percentage (..%) or a size value (bytes, or larger units if suffixes like KB or MB are present), or a sum of two or more such specifications. If not specified, `buildah` guesses that 25% more space than the contents will be enough, but this option is provided in case its guess is wrong. If the specified or computed size is less than 10 megabytes, it will be increased to 10 megabytes. **--type**, **-t** {SEV|SNP} The type of trusted execution environment (TEE) which the image should be marked for use with. Accepted values are "SEV" (AMD Secure Encrypted Virtualization - Encrypted State) and "SNP" (AMD Secure Encrypted Virtualization - Secure Nested Paging). If not specified, defaults to "SNP". **--workload-id**, **-w** *id* A workload identifier which will be recorded in the container image, to be used at run-time for retrieving the passphrase which was used to encrypt the disk image. If not specified, a semi-random value will be derived from the base image's image ID. ## SEE ALSO buildah(1) ================================================ FILE: docs/buildah-mount.1.md ================================================ # buildah-mount "1" "March 2017" "buildah" ## NAME buildah\-mount - Mount a working container's root filesystem. ## SYNOPSIS **buildah mount** [*container* ...] ## DESCRIPTION Mounts the specified container's root file system in a location which can be accessed from the host, and returns its location. If the mount command is invoked without any arguments, the tool will list all of the currently mounted containers. When running in rootless mode, mount runs in a different namespace so that the mounted volume might not be accessible from the host when using a driver different than `vfs`. To be able to access the file system mounted, you might need to create the mount namespace separately as part of `buildah unshare`. In the environment created with `buildah unshare` you can then use `buildah mount` and have access to the mounted file system. ## RETURN VALUE The location of the mounted file system. On error an empty string and errno is returned. ## OPTIONS **--json** Output in JSON format. ## EXAMPLE ``` buildah mount working-container /var/lib/containers/storage/overlay2/f3ac502d97b5681989dff84dfedc8354239bcecbdc2692f9a639f4e080a02364/merged ``` ``` buildah mount working-container /var/lib/containers/storage/overlay2/f3ac502d97b5681989dff84dfedc8354239bcecbdc2692f9a639f4e080a02364/merged fedora-working-container /var/lib/containers/storage/overlay2/0ff7d7ca68bed1ace424f9df154d2dd7b5a125c19d887f17653cbcd5b6e30ba1/merged ``` ``` buildah mount working-container fedora-working-container ubi8-working-container working-container /var/lib/containers/storage/overlay/f8cac5cce73e5102ab321cc5b57c0824035b5cb82b6822e3c86ebaff69fefa9c/merged fedora-working-container /var/lib/containers/storage/overlay/c3ec418be5bda5b72dca74c4d397e05829fe62ecd577dd7518b5f7fc1ca5f491/merged ubi8-working-container /var/lib/containers/storage/overlay/03a071f206f70f4fcae5379bd5126be86b5352dc2a0c3449cd6fca01b77ea868/merged ``` If running in rootless mode, you need to do a buildah unshare first to use the mount point. ``` $ buildah unshare # buildah mount working-container /var/lib/containers/storage/overlay/f8cac5cce73e5102ab321cc5b57c0824035b5cb82b6822e3c86ebaff69fefa9c/merged # cp foobar /var/lib/containers/storage/overlay/f8cac5cce73e5102ab321cc5b57c0824035b5cb82b6822e3c86ebaff69fefa9c/merged # buildah unmount working-container # exit $ buildah commit working-container newimage ``` ## SEE ALSO buildah(1), buildah-unshare(1), buildah-umount(1) ================================================ FILE: docs/buildah-prune.1.md ================================================ # buildah-rmi "1" "Jan 2023" "buildah" ## NAME buildah\-prune - Cleanup intermediate images as well as build and mount cache. ## SYNOPSIS **buildah prune** ## DESCRIPTION Cleanup intermediate images as well as build and mount cache. ## OPTIONS **--all**, **-a** All local images will be removed from the system that do not have containers using the image as a reference image. **--force**, **-f** This option will cause Buildah to remove all containers that are using the image before removing the image from the system. ## EXAMPLE buildah prune buildah prune --force ## SEE ALSO buildah(1), containers-registries.conf(5), containers-storage.conf(5) ================================================ FILE: docs/buildah-pull.1.md ================================================ # buildah-pull "1" "July 2018" "buildah" ## NAME buildah\-pull - Pull an image from a registry. ## SYNOPSIS **buildah pull** [*options*] *image* ## DESCRIPTION Pulls an image based upon the specified input. It supports all transports from `containers-transports(5)` (see examples below). If no transport is specified, the input is subject to short-name resolution (see `containers-registries.conf(5)`) and the `docker` (i.e., container registry) transport is used. ### DEPENDENCIES Buildah resolves the path to the registry to pull from by using the /etc/containers/registries.conf file, containers-registries.conf(5). If the `buildah pull` command fails with an "image not known" error, first verify that the registries.conf file is installed and configured appropriately. ## RETURN VALUE The image ID of the image that was pulled. On error 1 is returned. ## OPTIONS **--all-tags**, **-a** All tagged images in the repository will be pulled. **--arch**="ARCH" Set the ARCH of the image to be pulled to the provided value instead of using the architecture of the host. (Examples: arm, arm64, 386, amd64, ppc64le, s390x) **--authfile** *path* Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json. See containers-auth.json(5) for more information. This file is created using `buildah login`. If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using `docker login`. Note: You can also override the default path of the authentication file by setting the REGISTRY\_AUTH\_FILE environment variable. `export REGISTRY_AUTH_FILE=path` **--cert-dir** *path* Use certificates at *path* (\*.crt, \*.cert, \*.key) to connect to the registry. The default certificates directory is _/etc/containers/certs.d_. **--creds** *creds* The [username[:password]] to use to authenticate with the registry if required. If one or both values are not supplied, a command line prompt will appear and the value can be entered. The password is entered without echo. **--decryption-key** *key[:passphrase]* The [key[:passphrase]] to be used for decryption of images. Key can point to keys and/or certificates. Decryption will be tried with all keys. If the key is protected by a passphrase, it is required to be passed in the argument and omitted otherwise. **--os**="OS" Set the OS of the image to be pulled instead of using the current operating system of the host. **--platform**="OS/ARCH[/VARIANT]" Set the OS/ARCH of the image to be pulled to the provided value instead of using the current operating system and architecture of the host (for example `linux/arm`). OS/ARCH pairs are those used by the Go Programming Language. In several cases the ARCH value for a platform differs from one produced by other tools such as the `arch` command. Valid OS and architecture name combinations are listed as values for $GOOS and $GOARCH at https://golang.org/doc/install/source#environment, and can also be found by running `go tool dist list`. **NOTE:** The `--platform` option may not be used in combination with the `--arch`, `--os`, or `--variant` options. **--policy**=**always**|**missing**|**never**|**newer** Pull image policy. The default is **missing**. - **always**: Always pull the image and throw an error if the pull fails. - **missing**: Pull the image only if it could not be found in the local containers storage. Throw an error if no image could be found and the pull fails. - **never**: Never pull the image but use the one from the local containers storage. Throw an error if no image could be found. - **newer**: Pull if the image on the registry is newer than the one in the local containers storage. An image is considered to be newer when the digests are different. Comparing the time stamps is prone to errors. Pull errors are suppressed if a local image was found. **--quiet**, **-q** If an image needs to be pulled from the registry, suppress progress output. **--remove-signatures** Don't copy signatures when pulling images. **--retry** *attempts* Number of times to retry in case of failure when performing pull of images from registry. Defaults to `3`. **--retry-delay** *duration* Duration of delay between retry attempts in case of failure when performing pull of images from registry. Defaults to `2s`. **--tls-verify** *bool-value* Require HTTPS and verification of certificates when talking to container registries (defaults to true). TLS verification cannot be used when talking to an insecure registry. **--variant**="" Set the architecture variant of the image to be pulled. ## EXAMPLE buildah pull imagename buildah pull docker://myregistry.example.com/imagename buildah pull docker-daemon:imagename:imagetag buildah pull docker-archive:filename buildah pull oci-archive:filename buildah pull dir:directoryname buildah pull --tls-verify=false myregistry/myrepository/imagename:imagetag buildah pull --creds=myusername:mypassword --cert-dir ~/auth myregistry/myrepository/imagename:imagetag buildah pull --authfile=/tmp/auths/myauths.json myregistry/myrepository/imagename:imagetag buildah pull --arch=aarch64 myregistry/myrepository/imagename:imagetag buildah pull --arch=arm --variant=v7 myregistry/myrepository/imagename:imagetag ## ENVIRONMENT **BUILD\_REGISTRY\_SOURCES** BUILD\_REGISTRY\_SOURCES, if set, is treated as a JSON object which contains lists of registry names under the keys `insecureRegistries`, `blockedRegistries`, and `allowedRegistries`. When pulling an image from a registry, if the name of the registry matches any of the items in the `blockedRegistries` list, the image pull attempt is denied. If there are registries in the `allowedRegistries` list, and the registry's name is not in the list, the pull attempt is denied. **TMPDIR** The TMPDIR environment variable allows the user to specify where temporary files are stored while pulling and pushing images. Defaults to '/var/tmp'. ## FILES **registries.conf** (`/etc/containers/registries.conf`) registries.conf is the configuration file which specifies which container registries should be consulted when completing image names which do not include a registry or domain portion. **policy.json** (`/etc/containers/policy.json`) Signature policy file. This defines the trust policy for container images. Controls which container registries can be used for image, and whether or not the tool should trust the images. ## SEE ALSO buildah(1), buildah-from(1), buildah-login(1), docker-login(1), containers-policy.json(5), containers-registries.conf(5), containers-transports(5), containers-auth.json(5) ================================================ FILE: docs/buildah-push.1.md ================================================ # buildah-push "1" "June 2017" "buildah" ## NAME buildah\-push - Push an image, manifest list or image index from local storage to elsewhere. ## SYNOPSIS **buildah push** [*options*] *image* [*destination*] ## DESCRIPTION Pushes an image from local storage to a specified destination, decompressing and recompessing layers as needed. ## imageID Image stored in local container/storage ## DESTINATION DESTINATION is the location the container image is pushed to. It supports all transports from `containers-transports(5)` (see examples below). If no transport is specified, the `docker` (i.e., container registry) transport is used. ## OPTIONS **--all** If specified image is a manifest list or image index, push the images in addition to the list or index itself. **--authfile** *path* Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json. See containers-auth.json(5) for more information. This file is created using `buildah login`. If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using `docker login`. Note: You can also override the default path of the authentication file by setting the REGISTRY\_AUTH\_FILE environment variable. `export REGISTRY_AUTH_FILE=path` **--cert-dir** *path* Use certificates at *path* (\*.crt, \*.cert, \*.key) to connect to the registry. The default certificates directory is _/etc/containers/certs.d_. **--compression-format** *format* Specifies the compression format to use. Supported values are: `gzip`, `zstd` and `zstd:chunked`. `zstd:chunked` is incompatible with encrypting images, and will be treated as `zstd` with a warning in that case. **--compression-level** *level* Specify the compression level used with the compression. Specifies the compression level to use. The value is specific to the compression algorithm used, e.g. for zstd the accepted values are in the range 1-20 (inclusive), while for gzip it is 1-9 (inclusive). **--creds** *creds* The [username[:password]] to use to authenticate with the registry if required. If one or both values are not supplied, a command line prompt will appear and the value can be entered. The password is entered without echo. **--digestfile** *Digestfile* After copying the image, write the digest of the resulting image to the file. **--disable-compression**, **-D** Don't compress copies of filesystem layers which will be pushed. **--encrypt-layer** *layer(s)* Layer(s) to encrypt: 0-indexed layer indices with support for negative indexing (e.g. 0 is the first layer, -1 is the last layer). If not defined, will encrypt all layers if encryption-key flag is specified. **--encryption-key** *key* The [protocol:keyfile] specifies the encryption protocol, which can be JWE (RFC7516), PGP (RFC4880), and PKCS7 (RFC2315) and the key material required for image encryption. For instance, jwe:/path/to/key.pem or pgp:admin@example.com or pkcs7:/path/to/x509-file. **--force-compression** If set, push uses the specified compression algorithm even if the destination contains a differently-compressed variant already. Defaults to `true` if `--compression-format` is explicitly specified on the command-line, `false` otherwise. **--format**, **-f** Manifest Type (oci, v2s2, or v2s1) to use when pushing an image. (default is manifest type of the source image, with fallbacks) **--quiet**, **-q** When writing the output image, suppress progress output. **--remove-signatures** Don't copy signatures when pushing images. **--retry** *attempts* Number of times to retry in case of failure when performing push of images to registry. Defaults to `3`. **--retry-delay** *duration* Duration of delay between retry attempts in case of failure when performing push of images to registry. Defaults to `2s`. **--rm** When pushing a manifest list or image index, delete them from local storage if pushing succeeds. **--sign-by** *fingerprint* Sign the pushed image using the GPG key that matches the specified fingerprint. **--tls-verify** *bool-value* Require HTTPS and verification of certificates when talking to container registries (defaults to true). TLS verification cannot be used when talking to an insecure registry. ## EXAMPLE This example pushes the image specified by the imageID to a local directory in docker format. `# buildah push imageID dir:/path/to/image` This example pushes the image specified by the imageID to a local directory in oci format. `# buildah push imageID oci:/path/to/layout:image:tag` This example pushes the image specified by the imageID to a tar archive in oci format. `# buildah push imageID oci-archive:/path/to/archive:image:tag` This example pushes the image specified by the imageID to a container registry named registry.example.com. `# buildah push imageID docker://registry.example.com/repository:tag` This example pushes the image specified by the imageID to a container registry named registry.example.com and saves the digest in the specified digestfile. `# buildah push --digestfile=/tmp/mydigest imageID docker://registry.example.com/repository:tag` This example works like **docker push**, assuming *registry.example.com/my_image* is a local image. `# buildah push registry.example.com/my_image` This example pushes the image specified by the imageID to a private container registry named registry.example.com with authentication from /tmp/auths/myauths.json. `# buildah push --authfile /tmp/auths/myauths.json imageID docker://registry.example.com/repository:tag` This example pushes the image specified by the imageID and puts it into the local docker container store. `# buildah push imageID docker-daemon:image:tag` This example pushes the image specified by the imageID and puts it into the registry on the localhost while turning off tls verification. `# buildah push --tls-verify=false imageID localhost:5000/my-imageID` This example pushes the image specified by the imageID and puts it into the registry on the localhost using credentials and certificates for authentication. `# buildah push --cert-dir ~/auth --tls-verify=true --creds=username:password imageID localhost:5000/my-imageID` ## ENVIRONMENT **BUILD\_REGISTRY\_SOURCES** BUILD\_REGISTRY\_SOURCES, if set, is treated as a JSON object which contains lists of registry names under the keys `insecureRegistries`, `blockedRegistries`, and `allowedRegistries`. When pushing an image to a registry, if the portion of the destination image name that corresponds to a registry is compared to the items in the `blockedRegistries` list, and if it matches any of them, the push attempt is denied. If there are registries in the `allowedRegistries` list, and the portion of the name that corresponds to the registry is not in the list, the push attempt is denied. **TMPDIR** The TMPDIR environment variable allows the user to specify where temporary files are stored while pulling and pushing images. Defaults to '/var/tmp'. ## FILES **registries.conf** (`/etc/containers/registries.conf`) registries.conf is the configuration file which specifies which container registries should be consulted when completing image names which do not include a registry or domain portion. **policy.json** (`/etc/containers/policy.json`) Signature policy file. This defines the trust policy for container images. Controls which container registries can be used for image, and whether or not the tool should trust the images. ## SEE ALSO buildah(1), buildah-login(1), containers-policy.json(5), docker-login(1), containers-registries.conf(5), buildah-manifest(1), containers-transports(5), containers-auth.json(5) ================================================ FILE: docs/buildah-rename.1.md ================================================ # buildah-rename "1" "July 2018" "buildah" ## NAME buildah\-rename - Rename a local container. ## SYNOPSIS **buildah rename** *container* *new-name* ## DESCRIPTION Rename a local container. ## EXAMPLE buildah rename containerName NewName buildah rename containerID NewName ## SEE ALSO buildah(1) ================================================ FILE: docs/buildah-rm.1.md ================================================ # buildah-rm "1" "March 2017" "buildah" ## NAME buildah\-rm - Removes one or more working containers. ## SYNOPSIS **buildah rm** [*container* ...] ## DESCRIPTION Removes one or more working containers, unmounting them if necessary. ## OPTIONS **--all**, **-a** All Buildah containers will be removed. Buildah containers are denoted with an '*' in the 'BUILDER' column listed by the command 'buildah containers'.A container name or id cannot be provided when this option is used. ## EXAMPLE buildah rm containerID buildah rm containerID1 containerID2 containerID3 buildah rm --all ## SEE ALSO buildah(1) ================================================ FILE: docs/buildah-rmi.1.md ================================================ # buildah-rmi "1" "March 2017" "buildah" ## NAME buildah\-rmi - Removes one or more images. ## SYNOPSIS **buildah rmi** [*image* ...] ## DESCRIPTION Removes one or more locally stored images. Passing an argument _image_ deletes it, along with any of its dangling (untagged) parent images. ## LIMITATIONS * If the image was pushed to a directory path using the 'dir:' transport, the rmi command can not remove the image. Instead, standard file system commands should be used. * If _imageID_ is a name, but does not include a registry name, buildah will attempt to find and remove the named image using the registry name _localhost_, if no such image is found, it will search for the intended image by attempting to expand the given name using the names of registries provided in the system's registries configuration file, registries.conf. * If the _imageID_ refers to a *manifest list* or *image index*, this command will ***not*** do what you expect! This command will remove the images associated with the *manifest list* or *index* (not the manifest list/image index itself). To remove that, use the `buildah manifest rm` subcommand instead. ## OPTIONS **--all**, **-a** All local images will be removed from the system that do not have containers using the image as a reference image. An image name or id cannot be provided when this option is used. Read/Only images configured by modifying the "additionalimagestores" in the /etc/containers/storage.conf file, can not be removed. **--force**, **-f** This option will cause Buildah to remove all containers that are using the image before removing the image from the system. **--prune**, **-p** All local images will be removed from the system that do not have a tag and do not have a child image pointing to them. An image name or id cannot be provided when this option is used. ## EXAMPLE buildah rmi imageID buildah rmi --all buildah rmi --all --force buildah rmi --prune buildah rmi --force imageID buildah rmi imageID1 imageID2 imageID3 ## Files **registries.conf** (`/etc/containers/registries.conf`) registries.conf is the configuration file which specifies which container registries should be consulted when completing image names which do not include a registry or domain portion. **storage.conf** (`/etc/containers/storage.conf`) storage.conf is the storage configuration file for all tools using containers/storage The storage configuration file specifies all of the available container storage options for tools using shared container storage. ## SEE ALSO buildah(1), containers-registries.conf(5), containers-storage.conf(5) ================================================ FILE: docs/buildah-run.1.md ================================================ # buildah-run "1" "March 2017" "buildah" ## NAME buildah\-run - Run a command inside of the container. ## SYNOPSIS **buildah run** [*options*] [**--**] *container* *command* ## DESCRIPTION Launches a container and runs the specified command in that container using the container's root filesystem as a root filesystem, using configuration settings inherited from the container's image or as specified using previous calls to the *buildah config* command. To execute *buildah run* within an interactive shell, specify the --tty option. ## OPTIONS **--add-history** Add an entry to the history which will note what command is being invoked. Defaults to false. Note: You can also override the default value of --add-history by setting the BUILDAH\_HISTORY environment variable. `export BUILDAH_HISTORY=true` **--cap-add**=*CAP\_xxx* Add the specified capability to the set of capabilities which will be granted to the specified command. Certain capabilities are granted by default; this option can be used to add more beyond the defaults, which may have been modified by **--cap-add** and **--cap-drop** options used with the *buildah from* invocation which created the container. **--cap-drop**=*CAP\_xxx* Drop the specified capability from the set of capabilities which will be granted to the specified command. The CAP\_CHOWN, CAP\_DAC\_OVERRIDE, CAP\_FOWNER, CAP\_FSETID, CAP\_KILL, CAP\_NET\_BIND\_SERVICE, CAP\_SETFCAP, CAP\_SETGID, CAP\_SETPCAP, and CAP\_SETUID capabilities are granted by default; this option can be used to remove them from the defaults, which may have been modified by **--cap-add** and **--cap-drop** options used with the *buildah from* invocation which created the container. The list of default capabilities is managed in containers.conf(5). If a capability is specified to both the **--cap-add** and **--cap-drop** options, it will be dropped, regardless of the order in which the options were given. **--cgroupns** *how* Sets the configuration for the cgroup namespaces for the container. The configured value can be "" (the empty string) or "private" to indicate that a new cgroup namespace should be created, or it can be "host" to indicate that the cgroup namespace in which `buildah` itself is being run should be reused. **--contextdir** *directory* Allows setting context directory for current RUN invocation. Specifying a context directory causes RUN context to consider context directory as root directory for specified source in `--mount` of type 'bind'. **--device**=*device* Add a host device, or devices under a directory, to the environment in which the command will be run. The optional *permissions* parameter can be used to specify device permissions, using any one or more of **r** for read, **w** for write, and **m** for **mknod**(2). Example: **--device=/dev/sdc:/dev/xvdc:rwm**. Note: if _host-device_ is a symbolic link then it will be resolved first. The container will only store the major and minor numbers of the host device. The device to share can also be specified using a Container Device Interface (CDI) specification (https://github.com/cncf-tags/container-device-interface). Note: if the user only has access rights via a group, accessing the device from inside a rootless container will fail. The **crun**(1) runtime offers a workaround for this by adding the option **--annotation run.oci.keep_original_groups=1**. **--env**, **-e** *env=value* Temporarily add a value (e.g. env=*value*) to the environment for the running process. Unlike `buildah config --env`, the environment will not persist to later calls to `buildah run` or to the built image. Can be used multiple times. **--hostname** Set the hostname inside of the running container. **--ipc** *how* Sets the configuration for the IPC namespaces for the container. The configured value can be "" (the empty string) or "private" to indicate that a new IPC namespace should be created, or it can be "host" to indicate that the IPC namespace in which `buildah` itself is being run should be reused, or it can be the path to an IPC namespace which is already in use by another process. **--isolation** *type* Controls what type of isolation is used for running the process. Recognized types include *oci* (OCI-compatible runtime, the default), *rootless* (OCI-compatible runtime invoked using a modified configuration, with *--no-new-keyring* added to its *create* invocation, reusing the host's network and UTS namespaces, and creating private IPC, PID, mount, and user namespaces; the default for unprivileged users), and *chroot* (an internal wrapper that leans more toward chroot(1) than container technology, reusing the host's control group, network, IPC, and PID namespaces, and creating private mount and UTS namespaces, and creating user namespaces only when they're required for ID mapping). Note: You can also override the default isolation type by setting the BUILDAH\_ISOLATION environment variable. `export BUILDAH_ISOLATION=oci` **--mount**=*type=TYPE,TYPE-SPECIFIC-OPTION[,...]* Attach a filesystem mount to the container Current supported mount TYPES are bind, cache, secret and tmpfs. Writes to `bind` and `tmpfs` mounts are discarded after the command finishes, while changes to `cache` mounts persist across uses. e.g. type=bind,source=/path/on/host,destination=/path/in/container type=tmpfs,tmpfs-size=512M,destination=/path/in/container type=cache,target=/path/in/container Common Options: · src, source: mount source spec for bind and cache. Mandatory for bind. If `from` is specified, `src` is the subpath in the `from` field. · dst, destination, target: location where the command being run should see the content being mounted. · ro, read-only: (default true for `type=bind`, false for `type=tmpfs`, `type=cache`). · rw, read-write: (default false for `type=bind`, true for `type=tmpfs`, `type=cache`). Options specific to bind: · bind-propagation: shared, slave, private, rshared, rslave, or rprivate(default). See also mount(2). [[1]](#Footnote1) . bind-nonrecursive: do not setup a recursive bind mount. By default it is recursive. · from: image name for the root of the source. Defaults to **--contextdir**, mandatory if **--contextdir** was not specified. · src: location in the context directory (or image, if the **from** option is used) to mount instead of its top-level directory. · z: Set shared SELinux label on mounted destination. Use if SELinux is enabled on host machine. · Z: Set private SELinux label on mounted destination. Use if SELinux is enabled on host machine. Options specific to tmpfs: · tmpfs-size: Size of the tmpfs mount in bytes. Unlimited by default in Linux. · tmpfs-mode: File mode of the tmpfs in octal. (e.g. 700 or 0700.) Defaults to 1777 in Linux. · tmpcopyup: Path that is shadowed by the tmpfs mount is recursively copied up to the tmpfs itself. Options specific to secret: · id: the identifier for the secret passed into the `buildah bud --secret` or `podman build --secret` command. Options specific to cache: · id: Distinguish this cache from other caches using this ID rather than the target mount path. · mode: File mode for new cache directory in octal. Default 0755. · src: location in the cache (or image, if the **from** option is used) to mount instead of its top-level directory. · uid: uid for cache directory. · gid: gid for cache directory. · from: stage name for the root of the source. Defaults to host cache directory. · sharing: Whether other users of this cache need to wait for this command to complete (`sharing=locked`) or not (`sharing=shared`, which is the default). · z: Set shared SELinux label on mounted destination. Enabled by default if SELinux is enabled on the host machine. · Z: Set private SELinux label on mounted destination. Use if SELinux is enabled on host machine. **--network**, **--net**=*mode* Sets the configuration for the network namespace for the container. Valid _mode_ values are: - **none**: no networking. Invalid if using **--dns**, **--dns-opt**, or **--dns-search**; - **host**: use the host network stack. Note: the host mode gives the container full access to local system services such as D-bus and is therefore considered insecure; - **ns:**_path_: path to a network namespace to join; - **private**: create a new namespace for the container (default) - **\**: Join the network with the given name or ID, e.g. use `--network mynet` to join the network with the name mynet. Only supported for rootful users. - **slirp4netns[:OPTIONS,...]**: use **slirp4netns**(1) to create a user network stack. This is the default for rootless containers. It is possible to specify these additional options, they can also be set with `network_cmd_options` in containers.conf: - **allow_host_loopback=true|false**: Allow slirp4netns to reach the host loopback IP (default is 10.0.2.2 or the second IP from slirp4netns cidr subnet when changed, see the cidr option below). The default is false. - **mtu=MTU**: Specify the MTU to use for this network. (Default is `65520`). - **cidr=CIDR**: Specify ip range to use for this network. (Default is `10.0.2.0/24`). - **enable_ipv6=true|false**: Enable IPv6. Default is true. (Required for `outbound_addr6`). - **outbound_addr=INTERFACE**: Specify the outbound interface slirp binds to (ipv4 traffic only). - **outbound_addr=IPv4**: Specify the outbound ipv4 address slirp binds to. - **outbound_addr6=INTERFACE**: Specify the outbound interface slirp binds to (ipv6 traffic only). - **outbound_addr6=IPv6**: Specify the outbound ipv6 address slirp binds to. - **pasta[:OPTIONS,...]**: use **pasta**(1) to create a user-mode networking stack. \ This is only supported in rootless mode. \ By default, IPv4 and IPv6 addresses and routes, as well as the pod interface name, are copied from the host. If port forwarding isn't configured, ports are forwarded dynamically as services are bound on either side (init namespace or container namespace). Port forwarding preserves the original source IP address. Options described in pasta(1) can be specified as comma-separated arguments. \ In terms of pasta(1) options, **--config-net** is given by default, in order to configure networking when the container is started, and **--no-map-gw** is also assumed by default, to avoid direct access from container to host using the gateway address. The latter can be overridden by passing **--map-gw** in the pasta-specific options (despite not being an actual pasta(1) option). \ Also, **-t none** and **-u none** are passed to disable automatic port forwarding based on bound ports. Similarly, **-T none** and **-U none** are given to disable the same functionality from container to host. \ Some examples: - **pasta:--map-gw**: Allow the container to directly reach the host using the gateway address. - **pasta:--mtu,1500**: Specify a 1500 bytes MTU for the _tap_ interface in the container. - **pasta:--ipv4-only,-a,10.0.2.0,-n,24,-g,10.0.2.2,--dns-forward,10.0.2.3,-m,1500,--no-ndp,--no-dhcpv6,--no-dhcp**, equivalent to default slirp4netns(1) options: disable IPv6, assign `10.0.2.0/24` to the `tap0` interface in the container, with gateway `10.0.2.3`, enable DNS forwarder reachable at `10.0.2.3`, set MTU to 1500 bytes, disable NDP, DHCPv6 and DHCP support. - **pasta:-I,tap0,--ipv4-only,-a,10.0.2.0,-n,24,-g,10.0.2.2,--dns-forward,10.0.2.3,--no-ndp,--no-dhcpv6,--no-dhcp**, equivalent to default slirp4netns(1) options with Podman overrides: same as above, but leave the MTU to 65520 bytes - **pasta:-t,auto,-u,auto,-T,auto,-U,auto**: enable automatic port forwarding based on observed bound ports from both host and container sides - **pasta:-T,5201**: enable forwarding of TCP port 5201 from container to host, using the loopback interface instead of the tap interface for improved performance **--no-hostname** Do not create the _/etc/hostname_ file in the container for RUN instructions. By default, Buildah manages the _/etc/hostname_ file, adding the container's own hostname. When the **--no-hostname** option is set, the image's _/etc/hostname_ will be preserved unmodified if it exists. **--no-hosts** Do not create the _/etc/hosts_ file in the container for RUN instructions. By default, Buildah manages _/etc/hosts_, adding the container's own IP address. **--no-hosts** disables this, and the image's _/etc/hosts_ will be preserved unmodified. **--no-pivot** Do not use pivot root to jail process inside rootfs. This should be used whenever the rootfs is on top of a ramdisk. Note: You can make this option the default by setting the BUILDAH\_NOPIVOT environment variable. `export BUILDAH_NOPIVOT=true` **--pid** *how* Sets the configuration for the PID namespace for the container. The configured value can be "" (the empty string) or "private" to indicate that a new PID namespace should be created, or it can be "host" to indicate that the PID namespace in which `buildah` itself is being run should be reused, or it can be the path to a PID namespace which is already in use by another process. **--runtime** *path* The *path* to an alternate OCI-compatible runtime. Default is `runc`, or `crun` when machine is configured to use cgroups V2. Note: You can also override the default runtime by setting the BUILDAH\_RUNTIME environment variable. `export BUILDAH_RUNTIME=/usr/bin/crun` **--runtime-flag** *flag* Adds global flags for the container runtime. To list the supported flags, please consult the manpages of the selected container runtime. Note: Do not pass the leading `--` to the flag. To pass the runc flag `--log-format json` to buildah run, the option given would be `--runtime-flag log-format=json`. **--tty**, **--terminal**, **-t** By default a pseudo-TTY is allocated only when buildah's standard input is attached to a pseudo-TTY. Setting the `--tty` option to `true` will cause a pseudo-TTY to be allocated inside the container connecting the user's "terminal" with the stdin and stdout stream of the container. Setting the `--tty` option to `false` will prevent the pseudo-TTY from being allocated. **--user** *user*[:*group*] Set the *user* to be used for running the command in the container. The user can be specified as a user name or UID, optionally followed by a group name or GID, separated by a colon (':'). If names are used, the container should include entries for those names in its */etc/passwd* and */etc/group* files. **--uts** *how* Sets the configuration for the UTS namespace for the container. The configured value can be "" (the empty string) or "private" to indicate that a new UTS namespace should be created, or it can be "host" to indicate that the UTS namespace in which `buildah` itself is being run should be reused, or it can be the path to a UTS namespace which is already in use by another process. **--volume**, **-v** *source*:*destination*:*options* Create a bind mount. If you specify, ` -v /HOST-DIR:/CONTAINER-DIR`, Buildah bind mounts `/HOST-DIR` in the host to `/CONTAINER-DIR` in the Buildah container. The `OPTIONS` are a comma delimited list and can be: * [rw|ro] * [U] * [z|Z] * [`[r]shared`|`[r]slave`|`[r]private`] [[1]](#Footnote1) The `CONTAINER-DIR` must be an absolute path such as `/src/docs`. The `HOST-DIR` must be an absolute path as well. Buildah bind-mounts the `HOST-DIR` to the path you specify. For example, if you supply `/foo` as the host path, Buildah copies the contents of `/foo` to the container filesystem on the host and bind mounts that into the container. You can specify multiple **-v** options to mount one or more mounts to a container. `Write Protected Volume Mounts` You can add the `:ro` or `:rw` suffix to a volume to mount it read-only or read-write mode, respectively. By default, the volumes are mounted read-write. See examples. `Chowning Volume Mounts` By default, Buildah does not change the owner and group of source volume directories mounted into containers. If a container is created in a new user namespace, the UID and GID in the container may correspond to another UID and GID on the host. The `:U` suffix tells Buildah to use the correct host UID and GID based on the UID and GID within the container, to change the owner and group of the source volume. `Labeling Volume Mounts` Labeling systems like SELinux require that proper labels are placed on volume content mounted into a container. Without a label, the security system might prevent the processes running inside the container from using the content. By default, Buildah does not change the labels set by the OS. To change a label in the container context, you can add either of two suffixes `:z` or `:Z` to the volume mount. These suffixes tell Buildah to relabel file objects on the shared volumes. The `z` option tells Buildah that two containers share the volume content. As a result, Buildah labels the content with a shared content label. Shared volume labels allow all containers to read/write content. The `Z` option tells Buildah to label the content with a private unshared label. Only the current container can use a private volume. By default bind mounted volumes are `private`. That means any mounts done inside container will not be visible on the host and vice versa. This behavior can be changed by specifying a volume mount propagation property. When the mount propagation policy is set to `shared`, any mounts completed inside the container on that volume will be visible to both the host and container. When the mount propagation policy is set to `slave`, one way mount propagation is enabled and any mounts completed on the host for that volume will be visible only inside of the container. To control the mount propagation property of the volume use the `:[r]shared`, `:[r]slave` or `:[r]private` propagation flag. The propagation property can be specified only for bind mounted volumes and not for internal volumes or named volumes. For mount propagation to work on the source mount point (the mount point where source dir is mounted on) it has to have the right propagation properties. For shared volumes, the source mount point has to be shared. And for slave volumes, the source mount has to be either shared or slave. [[1]](#Footnote1) Use `df ` to determine the source mount and then use `findmnt -o TARGET,PROPAGATION ` to determine propagation properties of source mount, if `findmnt` utility is not available, the source mount point can be determined by looking at the mount entry in `/proc/self/mountinfo`. Look at `optional fields` and see if any propagation properties are specified. `shared:X` means the mount is `shared`, `master:X` means the mount is `slave` and if nothing is there that means the mount is `private`. [[1]](#Footnote1) To change propagation properties of a mount point use the `mount` command. For example, to bind mount the source directory `/foo` do `mount --bind /foo /foo` and `mount --make-private --make-shared /foo`. This will convert /foo into a `shared` mount point. The propagation properties of the source mount can be changed directly. For instance if `/` is the source mount for `/foo`, then use `mount --make-shared /` to convert `/` into a `shared` mount. **--workingdir** *directory* Temporarily set the working *directory* for the running process. Unlike `buildah config --workingdir`, the workingdir will not persist to later calls to `buildah run` or the built image. NOTE: End parsing of options with the `--` option, so that other options can be passed to the command inside of the container. ## EXAMPLE buildah run containerID -- ps -auxw buildah run --hostname myhost containerID -- ps -auxw buildah run containerID -- sh -c 'echo $PATH' buildah run --runtime-flag log-format=json containerID /bin/bash buildah run --runtime-flag debug containerID /bin/bash buildah run --tty containerID /bin/bash buildah run --tty=false containerID ls / buildah run --volume /path/on/host:/path/in/container:ro,z containerID sh buildah run -v /path/on/host:/path/in/container:z,U containerID sh buildah run --mount type=bind,src=/tmp/on:host,dst=/in:container,ro containerID sh ## SEE ALSO buildah(1), buildah-from(1), buildah-config(1), namespaces(7), pid\_namespaces(7), crun(1), runc(8), containers.conf(5) ## FOOTNOTES 1: The Buildah project is committed to inclusivity, a core value of open source. The `master` and `slave` mount propagation terminology used here is problematic and divisive, and should be changed. However, these terms are currently used within the Linux kernel and must be used as-is at this time. When the kernel maintainers rectify this usage, Buildah will follow suit immediately. ================================================ FILE: docs/buildah-source-add.1.md ================================================ # buildah-source-add "1" "March 2021" "buildah" ## NAME buildah\-source\-add - Add a source artifact to a source image ## SYNOPSIS **buildah source add** [*options*] *path* *artifact* ## DESCRIPTION Add add a source artifact to a source image. The artifact will be added as a gzip-compressed tar ball. Add attempts to auto-tar and auto-compress only if necessary. Note that the buildah-source command and all its subcommands are experimental and may be subject to future changes ## OPTIONS **--annotation** *key=value* Add an annotation to the layer descriptor in the source-image manifest. The input format is `key=value`. ================================================ FILE: docs/buildah-source-create.1.md ================================================ # buildah-source-create "1" "March 2021" "buildah" ## NAME buildah\-source\-create - Create and initialize a source image ## SYNOPSIS **buildah source create** [*options*] *path* ## DESCRIPTION Create and initialize a source image. A source image is an OCI artifact; an OCI image with a custom config media type. Note that the buildah-source command and all its subcommands are experimental and may be subject to future changes ## OPTIONS **--author** *author* Set the author of the source image mentioned in the config. By default, no author is set. **--time-stamp** *bool-value* Set the created time stamp in the image config. By default, the time stamp is set. ================================================ FILE: docs/buildah-source-pull.1.md ================================================ # buildah-source-pull "1" "March 2021" "buildah" ## NAME buildah\-source\-pull - Pull a source image from a registry to a specified path ## SYNOPSIS **buildah source pull** [*options*] *registry* *path* ## DESCRIPTION Pull a source image from a registry to a specified path. The pull operation will fail if the image does not comply with a source-image OCI artifact. Note that the buildah-source command and all its subcommands are experimental and may be subject to future changes. ## OPTIONS **--creds** *creds* The [username[:password]] to use to authenticate with the registry if required. If one or both values are not supplied, a command line prompt will appear and the value can be entered. The password is entered without echo. **--quiet**, **-q** Suppress the progress output when pulling a source image. **--tls-verify** *bool-value* Require HTTPS and verification of certificates when talking to container registries (defaults to true). TLS verification cannot be used when talking to an insecure registry. ================================================ FILE: docs/buildah-source-push.1.md ================================================ # buildah-source-push "1" "March 2021" "buildah" ## NAME buildah\-source\-push - Push a source image from a specified path to a registry. ## SYNOPSIS **buildah source push** [*options*] *path* *registry* ## DESCRIPTION Push a source image from a specified path to a registry. Note that the buildah-source command and all its subcommands are experimental and may be subject to future changes. ## OPTIONS **--creds** *creds* The [username[:password]] to use to authenticate with the registry if required. If one or both values are not supplied, a command line prompt will appear and the value can be entered. The password is entered without echo. **--digestfile** *digestfile* After copying the image, write the digest of the resulting image to the file. **--quiet**, **-q** Suppress the progress output when pushing a source image. **--tls-verify** *bool-value* Require HTTPS and verification of certificates when talking to container registries (defaults to true). TLS verification cannot be used when talking to an insecure registry. ================================================ FILE: docs/buildah-source.1.md ================================================ # buildah-source "1" "March 2021" "buildah" ## NAME buildah\-source - Create, push, pull and manage source images and associated source artifacts ## SYNOPSIS **buildah source** *subcommand* ## DESCRIPTION Create, push, pull and manage source images and associated source artifacts. A source image contains all source artifacts an ordinary OCI image has been built with. Those artifacts can be any kind of source artifact, such as source RPMs, an entire source tree or text files. Note that the buildah-source command and all its subcommands are experimental and may be subject to future changes. ## COMMANDS | Command | Man Page | Description | | -------- | ------------------------------------------------------ | ---------------------------------------------------------- | | add | [buildah-source-add(1)](buildah-source-add.1.md) | Add a source artifact to a source image. | | create | [buildah-source-create(1)](buildah-source-create.1.md) | Create and initialize a source image. | | pull | [buildah-source-pull(1)](buildah-source-pull.1.md) | Pull a source image from a registry to a specified path. | | push | [buildah-source-push(1)](buildah-source-push.1.md) | Push a source image from a specified path to a registry. | ## SEE ALSO buildah(1) ## HISTORY June 2021, Originally compiled by Valentin Rothberg ================================================ FILE: docs/buildah-tag.1.md ================================================ # buildah-tag "1" "May 2017" "buildah" ## NAME buildah\-tag - Add additional names to local images. ## SYNOPSIS **buildah tag** *name* *new-name* ... ## DESCRIPTION Adds additional names to locally-stored images. ## EXAMPLE buildah tag imageName firstNewName buildah tag imageName firstNewName SecondNewName ## SEE ALSO buildah(1) ================================================ FILE: docs/buildah-umount.1.md ================================================ # buildah-umount "1" "March 2017" "buildah" ## NAME buildah\-umount - Unmount the root file system on the specified working containers. ## SYNOPSIS **buildah umount** [*options*] [*container* ...] ## DESCRIPTION Unmounts the root file system on the specified working containers. ## OPTIONS **--all**, **-a** All of the currently mounted containers will be unmounted. ## EXAMPLE buildah umount containerID buildah umount containerID1 containerID2 containerID3 buildah umount --all ## SEE ALSO buildah(1), buildah-umount(1) ================================================ FILE: docs/buildah-unshare.1.md ================================================ # buildah-unshare "1" "June 2018" "buildah" ## NAME buildah\-unshare - Run a command inside of a modified user namespace. ## SYNOPSIS **buildah unshare** [*options*] [**--**] [*command*] ## DESCRIPTION Launches a process (by default, *$SHELL*) in a new user namespace. The user namespace is configured so that the invoking user's UID and primary GID appear to be UID 0 and GID 0, respectively. Any ranges which match that user and group in /etc/subuid and /etc/subgid are also mapped in as themselves with the help of the *newuidmap(1)* and *newgidmap(1)* helpers. buildah unshare is useful for troubleshooting unprivileged operations and for manually clearing storage and other data related to images and containers. It is also useful if you want to use the `buildah mount` command. If an unprivileged user wants to mount and work with a container, then they need to execute buildah unshare. Executing `buildah mount` fails for unprivileged users unless the user is running inside a `buildah unshare` session. ## OPTIONS **--mount**, **-m** [*VARIABLE=]containerNameOrID* Mount the *containerNameOrID* container while running *command*, and set the environment variable *VARIABLE* to the path of the mountpoint. If *VARIABLE* is not specified, it defaults to *containerNameOrID*, which may not be a valid name for an environment variable. ## EXAMPLE buildah unshare id buildah unshare pwd buildah unshare cat /proc/self/uid\_map /proc/self/gid\_map buildah unshare rm -fr $HOME/.local/share/containers/storage /run/user/\`id -u\`/run buildah unshare --mount containerID sh -c 'cat ${containerID}/etc/os-release' buildah unshare --mount root=containerID sh -c 'cat ${root}/etc/os-release' If you want to use buildah with a 'mount' command then you can create a script that looks something like: ```console cat > buildah-script.sh << _EOF #!/bin/bash ctr=$(buildah from scratch) mnt=$(buildah mount $ctr) dnf -y install --installroot=$mnt --use-host-config --setopt "*.countme=false" PACKAGES dnf -y clean all --installroot=$mnt buildah config --entrypoint="/bin/PACKAGE" --env "FOO=BAR" $ctr buildah commit $ctr imagename buildah unmount $ctr _EOF chmod +x buildah-script.sh ``` Then execute it with: ```console buildah unshare ./buildah-script.sh ``` ## SEE ALSO buildah(1), buildah-mount(1), namespaces(7), newuidmap(1), newgidmap(1), user\_namespaces(7) ================================================ FILE: docs/buildah-version.1.md ================================================ # buildah-version "1" "June 2017" "Buildah" ## NAME buildah\-version - Display the Buildah Version Information. ## SYNOPSIS **buildah version** [*options*] ## DESCRIPTION Shows the following information: Version, Go Version, Image Spec, Runtime Spec, Git Commit, Build Time, OS, and Architecture. ## OPTIONS **--help, -h** Print usage statement **--json** Output in JSON format. ## EXAMPLE buildah version buildah version --help buildah version -h ## SEE ALSO buildah(1) ================================================ FILE: docs/buildah.1.md ================================================ # buildah "1" "March 2017" "buildah" ## NAME Buildah - A command line tool that facilitates building OCI container images. ## SYNOPSIS buildah [OPTIONS] COMMAND [ARG...] ## DESCRIPTION The Buildah package provides a command line tool which can be used to: * Create a working container, either from scratch or using an image as a starting point. * Mount a working container's root filesystem for manipulation. * Unmount a working container's root filesystem. * Use the updated contents of a container's root filesystem as a filesystem layer to create a new image. * Delete a working container or an image. * Rename a local container. ## OPTIONS **--cgroup-manager**=*manager* The CGroup manager to use for container cgroups. Supported values are cgroupfs or systemd. Default is systemd unless overridden in the containers.conf file. Note: Setting this flag can cause certain commands to break when called on containers previously created by the other CGroup manager type. Note: CGroup manager is not supported in rootless mode when using CGroups Version V1. **--imagestore** *path* Path under which content for pulled and built images will be stored. By default, the configured **--root** location is used for pulled and built images as well as containers. If the storage driver is *overlay*, then any images which have previously been written to the **--root** location will still be available. This will override the *imagestore* option in containers-storage.conf(5). **--log-level** *level* The log level to be used. Either "trace", "debug", "info", "warn", "error", "fatal", or "panic", defaulting to "warn". **--help, -h** Show help **--registries-conf** *path* Pathname of the configuration file which specifies which container registries should be consulted when completing image names which do not include a registry or domain portion. It is not recommended that this option be used, as the default behavior of using the system-wide configuration (*/etc/containers/registries.conf*) is most often preferred. **--registries-conf-dir** *path* Pathname of the directory which contains configuration snippets which specify registries which should be consulted when completing image names which do not include a registry or domain portion. It is not recommended that this option be used, as the default behavior of using the system-wide configuration (*/etc/containers/registries.d*) is most often preferred. **--root** *path* Storage root dir (default: "/var/lib/containers/storage" for UID 0, "$HOME/.local/share/containers/storage" for other users). The default root dir is configured in /etc/containers/storage.conf. **--runroot** *path* Storage state dir (default: "/run/containers/storage" for UID 0, "/run/user/$UID" for other users). The default state dir is configured in /etc/containers/storage.conf. **--short-name-alias-conf** *path* Pathname of the file which contains cached mappings between short image names and their corresponding fully-qualified names. It is used for mapping from names of images specified using short names like "ubi8" which don't include a registry component and a corresponding fully-specified name which includes a registry and any other components, such as "registry.access.redhat.com/ubi8". It is not recommended that this option be used, as the default behavior of using the system-wide cache (*/var/cache/containers/short-name-aliases.conf*) or per-user cache (*$HOME/.cache/containers/short-name-aliases.conf*) to supplement system-wide defaults is most often preferred. **--storage-driver** **value** Storage driver. The default storage driver for UID 0 is configured in /etc/containers/storage.conf (`$HOME/.config/containers/storage.conf` in rootless mode), and is *vfs* for other users. The `STORAGE_DRIVER` environment variable overrides the default. The --storage-driver specified driver overrides all. Examples: "overlay", "vfs" Overriding this option will cause the *storage-opt* settings in /etc/containers/storage.conf to be ignored. The user must specify additional options via the `--storage-opt` flag. **--storage-opt** **value** Storage driver option, Default storage driver options are configured in /etc/containers/storage.conf (`$HOME/.config/containers/storage.conf` in rootless mode). The `STORAGE_OPTS` environment variable overrides the default. The --storage-opt specified options overrides all. **--transient-store** *bool-value* Store metadata about containers under the storage state directory (**--runroot**), with the intention that records about them will be removed when the system is rebooted. Additional garbage collection must still be performed at boot-time, so this option should remain disabled in most configurations. (default: false) **--userns-gid-map** *mapping* Directly specifies a GID mapping which should be used to set ownership, at the filesystem level, on the working container's contents. Commands run when handling `RUN` instructions will default to being run in their own user namespaces, configured using the UID and GID maps. Entries in this map take the form of one or more colon-separated triples of a starting in-container GID, a corresponding starting host-level GID, and the number of consecutive IDs which the map entry represents. This option overrides the *remap-gids* setting in the *options* section of /etc/containers/storage.conf. If this option is not specified, but a global --userns-gid-map setting is supplied, settings from the global option will be used. If none of --userns-uid-map-user, --userns-gid-map-group, or --userns-gid-map are specified, but --userns-uid-map is specified, the GID map will be set to use the same numeric values as the UID map. **NOTE:** When this option is specified by a rootless user, the specified mappings are relative to the rootless usernamespace in the container, rather than being relative to the host as it would be when run rootful. **--userns-uid-map** *mapping* Directly specifies a UID mapping which should be used to set ownership, at the filesystem level, on the working container's contents. Commands run when handling `RUN` instructions will default to being run in their own user namespaces, configured using the UID and GID maps. Entries in this map take the form of one or more colon-separated triples of a starting in-container UID, a corresponding starting host-level UID, and the number of consecutive IDs which the map entry represents. This option overrides the *remap-uids* setting in the *options* section of /etc/containers/storage.conf. If this option is not specified, but a global --userns-uid-map setting is supplied, settings from the global option will be used. If none of --userns-uid-map-user, --userns-gid-map-group, or --userns-uid-map are specified, but --userns-gid-map is specified, the UID map will be set to use the same numeric values as the GID map. **NOTE:** When this option is specified by a rootless user, the specified mappings are relative to the rootless usernamespace in the container, rather than being relative to the host as it would be when run rootful. **--version**, **-v** Print the version ## Environment Variables Buildah can set up environment variables from the env entry in the [engine] table in the containers.conf(5). These variables can be overridden by passing environment variables before the `buildah` commands. ## COMMANDS | Command | Man Page | Description | | ---------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | | add | [buildah-add(1)](buildah-add.1.md) | Add the contents of a file, URL, or a directory to the container. | | build | [buildah-build(1)](buildah-build.1.md) | Builds an OCI image using instructions in one or more Containerfiles. | | commit | [buildah-commit(1)](buildah-commit.1.md) | Create an image from a working container. | | config | [buildah-config(1)](buildah-config.1.md) | Update image configuration settings. | | containers | [buildah-containers(1)](buildah-containers.1.md) | List the working containers and their base images. | | copy | [buildah-copy(1)](buildah-copy.1.md) | Copies the contents of a file, URL, or directory into a container's working directory. | | from | [buildah-from(1)](buildah-from.1.md) | Creates a new working container, either from scratch or using a specified image as a starting point. | | images | [buildah-images(1)](buildah-images.1.md) | List images in local storage. | | info | [buildah-info(1)](buildah-info.1.md) | Display Buildah system information. | | inspect | [buildah-inspect(1)](buildah-inspect.1.md) | Inspects the configuration of a container or image | | login | [buildah-login(1)](buildah-login.1.md) | Login to a container registry. | | logout | [buildah-logout(1)](buildah-logout.1.md) | Logout of a container registry | | manifest | [buildah-manifest(1)](buildah-manifest.1.md) | Create and manipulate manifest lists and image indexes. | | mkcw | [buildah-mkcw(1)](buildah-mkcw.1.md) | Convert a conventional container image into a confidential workload image. | mount | [buildah-mount(1)](buildah-mount.1.md) | Mount the working container's root filesystem. | | prune | [buildah-prune(1)](buildah-prune.1.md) | Cleanup intermediate images as well as build and mount cache. | | pull | [buildah-pull(1)](buildah-pull.1.md) | Pull an image from the specified location. | | push | [buildah-push(1)](buildah-push.1.md) | Push an image from local storage to elsewhere. | | rename | [buildah-rename(1)](buildah-rename.1.md) | Rename a local container. | | rm | [buildah-rm(1)](buildah-rm.1.md) | Removes one or more working containers. | | rmi | [buildah-rmi(1)](buildah-rmi.1.md) | Removes one or more images. | | run | [buildah-run(1)](buildah-run.1.md) | Run a command inside of the container. | | source | [buildah-source(1)](buildah-source.1.md) | Create, push, pull and manage source images and associated source artifacts. | | tag | [buildah-tag(1)](buildah-tag.1.md) | Add an additional name to a local image. | | umount | [buildah-umount(1)](buildah-umount.1.md) | Unmount a working container's root file system. | | unshare | [buildah-unshare(1)](buildah-unshare.1.md) | Launch a command in a user namespace with modified ID mappings. | | version | [buildah-version(1)](buildah-version.1.md) | Display the Buildah Version Information | ## Files **storage.conf** (`/etc/containers/storage.conf`) storage.conf is the storage configuration file for all tools using containers/storage The storage configuration file specifies all of the available container storage options for tools using shared container storage. **mounts.conf** (`/usr/share/containers/mounts.conf` and optionally `/etc/containers/mounts.conf`) The mounts.conf files specify volume mount files or directories that are automatically mounted inside containers when executing the `buildah run` or `buildah build` commands. Container processes can then use this content. The volume mount content does not get committed to the final image. Usually these directories are used for passing secrets or credentials required by the package software to access remote package repositories. For example, a mounts.conf with the line "`/usr/share/rhel/secrets:/run/secrets`", the content of `/usr/share/rhel/secrets` directory is mounted on `/run/secrets` inside the container. This mountpoint allows Red Hat Enterprise Linux subscriptions from the host to be used within the container. It is also possible to omit the destination if it's equal to the source path. For example, specifying `/var/lib/secrets` will mount the directory into the same container destination path `/var/lib/secrets`. Note this is not a volume mount. The content of the volumes is copied into container storage, not bind mounted directly from the host. **registries.conf** (`/etc/containers/registries.conf`) registries.conf is the configuration file which specifies which container registries should be consulted when completing image names which do not include a registry or domain portion. **registries.d** (`/etc/containers/registries.d`) Directory which contains configuration snippets which specify registries which should be consulted when completing image names which do not include a registry or domain portion. ## SEE ALSO containers.conf(5), containers-mounts.conf(5), newuidmap(1), newgidmap(1), containers-registries.conf(5), containers-storage.conf(5) ## HISTORY December 2017, Originally compiled by Tom Sweeney ================================================ FILE: docs/containertools/README.md ================================================ # Container Tools Guide ## Introduction The purpose of this guide is to list a number of related Open-source projects that are available on [GitHub.com](https://github.com) that operate on [Open Container Initiative](https://www.opencontainers.org/) (OCI) images and containers. This guide will give a high level explanation of the related container tools and will explain a bit on how they interact amongst each other. The tools are: * [Buildah](https://github.com/containers/buildah) * [CRI-O](https://github.com/kubernetes-sigs/cri-o) * [Podman](https://github.com/containers/podman) * [Skopeo](https://github.com/containers/skopeo) ## Buildah The Buildah project provides a command line tool that can be used to create an OCI or traditional Docker image format image and to then build a working container from the image. The container can be mounted and modified and then an image can be saved based on the updated container. ## CRI-O CRI-O [Website](http://cri-o.io/) The CRI-O project provides an integration path between OCI conformant runtimes and kubelet. Specifically, it implements the Kubelet [Container Runtime Interface (CRI)](https://github.com/kubernetes/community/blob/master/contributors/devel/container-runtime-interface.md) using OCI conformant runtimes. The scope of CRI-O is tied to the scope of the CRI. At a high level CRI-O supports multiple image formats including the existing Docker image format, multiple means to download images including trust & image verification, container image and lifecycle management, monitoring, logging and resource isolation as required by CRI. ## Podman [Podman](https://github.com/containers/podman) allows for full management of a container's lifecycle from creation through removal. It supports multiple image formats including both the Docker and OCI image formats. Support for pods is provided allowing pods to manage groups of containers together. Podman also supports trust and image verification when pulling images along with resource isolation of containers and pods. ## Skopeo Skopeo is a command line tool that performs a variety of operations on container images and image repositories. Skopeo can work on either OCI or Docker images. Skopeo can be used to copy images from and to various container storage mechanisms including container registries. Skopeo also allows you to inspect an image showing its layers without requiring that the image be pulled. Skopeo also allows you to delete an image from a repository. When required by the repository, Skopeo can pass appropriate certificates and credentials for authentication. ## Buildah and Podman relationship Buildah and Podman are two complementary Open-source projects that are available on most Linux platforms and both projects reside at [GitHub.com](https://github.com) with Buildah [here](https://github.com/containers/buildah) and Podman [here](https://github.com/containers/podman). Both Buildah and Podman are command line tools that work on OCI images and containers. The two projects differentiate in their specialization. Buildah specializes in building OCI images. Buildah's commands replicate all of the commands that are found in a Dockerfile. Buildah’s goal is also to provide a lower level coreutils interface to build images, allowing people to build containers without requiring a Dockerfile. The intent with Buildah is to allow other scripting languages to build container images, without requiring a daemon. Podman specializes in all of the commands and functions that help you to maintain and modify OCI images, such as pulling and tagging. It also allows you to create, run, and maintain containers created from those images. A major difference between Podman and Buildah is their concept of a container. Podman allows users to create "traditional containers" where the intent of these containers is to be long lived. While Buildah containers are really just created to allow content to be added back to the container image. An easy way to think of it is the `buildah run` command emulates the RUN command in a Dockerfile while the `podman run` command emulates the `docker run` command in functionality. Because of this you cannot see Podman containers from within Buildah or vice versa. In short Buildah is an efficient way to create OCI images while Podman allows you to manage and maintain those images and containers in a production environment using familiar container cli commands. Some of the commands between the projects overlap: * build The `podman build` and `buildah build` commands have significant overlap as Podman borrows large pieces of the `podman build` implementation from Buildah. * run The `buildah run` and `podman run` commands are similar but different. As explained above Podman and Buildah have a different concept of a container. An easy way to think of it is the `buildah run` command emulates the RUN command in a Dockerfile while the `podman run` command emulates the `docker run` command in functionality. As Buildah and Podman have somewhat different concepts of containers, you can not see Podman containers from within Buildah or vice versa. * pull, push These commands are basically the same between the two and either could be used. * commit Commit works differently because of the differences in `containers`. You cannot commit a Podman container from Buildah nor a Buildah container from Podman. * tag, rmi, images These commands are basically the same between the two and either could be used. * rm This command appears to be equivalent on the surface, but they differ due to the underlying storage differences of containers between the two projects. Given that, Buildah containers can not be removed with a Podman command and Podman containers can not be removed with a Buildah command. * mount Mount command is similar for both in that you can mount the container image and modify content in it, which will be saved to an image when you commit. In short Buildah is an efficient way to create OCI images while Podman allows you to manage and maintain those images and containers in a production environment using familiar container cli commands. ================================================ FILE: docs/links/buildah-bud.1 ================================================ .so man1/buildah-build.1 ================================================ FILE: docs/release-announcements/README.md ================================================ ![buildah logo](../../logos/buildah-logo_large.png) # Buildah Release Announcements **[Buildah v1.3 RA](v1.3.md) - August 7, 2018** Features: Dockerfile handling improvements, added the `buildah pull` command, added the `buildah rename` command, updated ulimits settings, added isolation control and other enhancements and bug fixes. **[Buildah v1.2 RA](v1.2.md) - July 14, 2018** Features: Added ability to control image layers when building an image, CVE’s Fixes, the initial support for user namespace handling and other enhancements and bug fixes. **[Buildah v1.1 RA](v1.1.md) - June 12, 2018** Features: OnBuild support for Dockerfiles, label support for the `buildah bud` command and other enhancements and bug fixes. **[Buildah Alpha v0.16 RA](v0.16.md) - April 2, 2018** Features: SHELL command support in Dockerfiles, added support for three transports for `buildah from`, added the ability to pull compressed docker-archive files and other enhancements and bug fixes. **[Buildah Alpha v0.12 RA](v0.12.md) - February 21, 2018** Features: Set the default certificate directory to /etc/containers/certs.d, improved lookups for a variety of image name formats, added pruning capability to the rmi command, provided authentication to `buildah bud` and other enhancements and bug fixes. ## Buildah == Simplicity ================================================ FILE: docs/release-announcements/v0.12.md ================================================ # Buildah Alpha version 0.12 Release Announcement ![buildah logo](https://cdn.rawgit.com/containers/buildah/main/logos/buildah-logo_large.png) We're pleased to announce the release of Buildah Alpha version 0.12 on both Fedora 26 and Fedora 27. As always, the latest Buildah can also be acquired from [GitHub](https://github.com/containers/buildah) for any other Linux distribution. The Buildah project has been building some steam over the past several weeks, welcoming several new contributors to the mix, launching new functionality and creating a number of improvements and bug fixes. The major highlights for this release are: * Added better handling of error messages for Unknown Dockerfile instructions. * Set the default certificate directory to /etc/containers/certs.d. * Vendored in the latest containers/image and containers/Storage packages. * The build-using-dockerfile (bud) command now sets the images 'author' field to the value provided by MAINTAINER in the Dockerfile. * Return exit code 1 when 'buildah rmi' fails. * Improve lookups of a variety of image name formats. * Adds the --format and --filter parameters to the 'buildah containers' command. * Adds the --prune,-p option to the 'buildah rmi' command allowing dangling images to be pruned. * Adds the --authfile parameter to the 'buildah commit' command. * Fix the --runtime-flag for the 'buildah run' and 'buildah bud' commands when global flags are used. * The format parameter now overrides the quiet parameter for 'buildah images'. * Provide authentication parameters to the build-using-docker-file (bud) command. * Directory permissions are no longer overwritten when using the --chown parameter. * HTML character output to the terminal is no longer escaped. * The container name is now always set to the image's name. * The username or password are prompted for if they are not supplied with the --creds parameter. * Return a better error message when bad credentials are used to pull a private image. * Plus several small bug fixes. If you haven’t yet, install Buildah from the Fedora repository and give it a spin. We’re betting you'll find it’s an easy and quick way to build containers in your environment without a daemon being involved! If you haven't joined our community yet, don't wait any longer! Come join us in [GitHub](https://github.com/containers/buildah), where Open Source communities live. **Buildah == Simplicity** ================================================ FILE: docs/release-announcements/v0.16.md ================================================ # Buildah Alpha version 0.16 Release Announcement ![buildah logo](https://cdn.rawgit.com/containers/buildah/main/logos/buildah-logo_large.png) We're pleased to announce the release of Buildah Alpha version 0.16 which is now available from GitHub for any Linux distro. We will be shipping this release on Fedora, CentOS and Ubuntu in the near future. The Buildah project has continued to grow over the past several weeks, welcoming several new contributors to the mix, launching new functionality and creating a number of improvements and bug fixes. ## The major highlights for this release are: * Add support for the SHELL command in Dockerfiles. * Change the time displayed by the image command to the locale time. * Allow the --cmd parameter for the `buildah config` command to have commands as values. I.e. `buildah config --cmd “--help” {containerID}`. * Documentations added for the mounts.conf file. The mounts config allows you to mount content from the host into the container to be used during the build procedure, but does not get committed to the image. * Fixed a number of man pages to format correctly. * The `buildah from` command now supports pulling images using the following three transports: docker-archive, oci-archive, and dir, as well as normal container registries and the docker daemon. * If the user overrides the storage driver, the options will not be used from the default storage.conf file. * Show the Config and Manifest as a JSON string in `buildah inspect` even when the --format parameter is not set. * Adds feature to pull compressed docker-archive files. * Vendor in latest containers/image * docker-archive generates docker legacy compatible images. * Ensure the layer IDs in legacy docker/tarfile metadata are unique. * docker-archive: repeated layers are symlinked in the tar file. * sysregistries: remove all trailing slashes. * Improve docker/* error messages. * Fix failure to make auth directory. * Create a new slice in Schema1.UpdateLayerInfos. * Drop unused storageImageDestination.{image,systemContext}. * Load a *storage.Image only once in storageImageSource. * Support gzip for docker-archive files. * Remove .tar extension from blob and config file names. * ostree, src: support copy of compressed layers. * ostree: re-pull layer if it misses uncompressed_digest|uncompressed_size. * image: fix docker schema v1 -> OCI conversion. * Add /etc/containers/certs.d as default certs directory. * Plus several small bug fixes. ## Try it Out. If you haven’t yet, install Buildah from the Fedora repo or GitHub and give it a spin. We’re betting you'll find it’s an easy and quick way to build containers in your environment without a daemon being involved! For those of you who contributed to this release, thank you very much for your contributions! If you haven't joined our community yet, don't wait any longer! Come join us in GitHub, where Open Source communities live. ## Buildah == Simplicity ================================================ FILE: docs/release-announcements/v1.1.md ================================================ # Buildah version 1.1 Release Announcement ![buildah logo](https://cdn.rawgit.com/containers/buildah/main/logos/buildah-logo_large.png) We're pleased to announce the release of Buildah version 1.1 which is now available from GitHub for any Linux distro. We are shipping this release on Fedora, RHEL 7, CentOS and Ubuntu in the near future. The Buildah project has continued to grow over the past several weeks, welcoming several new contributors to the mix, launching new functionality and creating a number of improvements and bug fixes. ## The major highlights for this release are: * Drop capabilities if running container processes as non root * Print Warning message if cmd will not be used based on entrypoint * Add OnBuild support for Dockerfiles * Add support for buildah bud --label * Report errors on bad transports specification when pushing images * Add registry errors for pull * Give better messages to users when image can not be found * Add environment variable to buildah --format * Accept json array input for config entrypoint * buildah bud now requires a context directory or URL * buildah bud picks up ENV from base image * Run: set supplemental group IDs * Use CNI to configure container networks * add/secrets/commit: Use mappings when setting permissions on added content * Add CLI options for specifying namespace and cgroup setup * Always set mappings when using user namespaces * Read UID/GID mapping information from containers and images * build-using-dockerfile: add --annotation * Implement --squash for build-using-dockerfile and commit * Manage "Run" containers more closely * Handle /etc/hosts and /etc/resolv.conf properly in container * Make "run --terminal" and "run -t" aliases for "run --tty" * buildah push/from can push and pull images with no reference * Attempt to download file from url, if fails assume Dockerfile * builder-inspect: fix format option * buildah-from: add effective value to mount propagation * Documentation Changes * Change freenode chan to buildah * Add example CNI configurations * Touch Up tutorial for run changes * Update 01-intro.md tutorial * Update troubleshooting with new run workaround * Add console syntax highlighting to troubleshooting page * Touchup man page short options across man pages * Demo Changes * Added demo dir and a demo. * Added Docker compatibility demo * Added a bud demo and tidied up * Update buildah scratch demo to support el7 * Quick fix on demo readme * Test Changes * Add tests for namespace control flags * CI tests and minor fix for cache related noop flags * Add cpu-shares short flag (-c) and cpu-shares CI tests * Add buildah bud CI tests for ENV variables * Additional bud CI tests * Run integration tests under travis_wait in Travis * Re-enable rpm .spec version check and new commit test * Update to F28 and new run format in baseline test * Add context dir to bud command in baseline test * add test to inspect * Test with Go 1.10, too * rmi, rm: add test * add mount test * Fix SELinux test errors when SELinux is enabled * Vendor changes * Vendor in latest container/storage for devicemapper support * Vendor github.com/onsi/ginkgo and github.com/onsi/gomega * Vendor github.com/containernetworking/cni v0.6.0 * Update github.com/containers/storage * Update github.com/containers/libpod * Vendor in latest containers/image * Plus a number of smaller fixes. ## Try it Out. If you haven’t yet, install Buildah from the Fedora repo or GitHub and give it a spin. We’re betting you'll find it’s an easy and quick way to build containers in your environment without a daemon being involved! For those of you who contributed to this release, thank you very much for your contributions! If you haven't joined our community yet, don't wait any longer! Come join us in GitHub, where Open Source communities live. ## Buildah == Simplicity ================================================ FILE: docs/release-announcements/v1.2.md ================================================ # Buildah version 1.2 Release Announcement ![buildah logo](https://cdn.rawgit.com/containers/buildah/main/logos/buildah-logo_large.png) We're pleased to announce the release of Buildah version 1.2 which is now available from GitHub for any Linux distro. We are shipping this release on Fedora, RHEL 7, CentOS and Ubuntu in the near future. The Buildah project has continued to grow over the past several weeks, welcoming several new contributors to the mix. The highlights of this release are the added ability to control image layers when building an image, CVE’s Fixes, the initial support for user namespace handling and several other enhancements and bug fixes. ## The major highlights for this release are: ### Allow the user to control the layers of the image when the image is built with the ‘buildah bud’ command. A container is comprised of a final readable/writeable layer and when the layers are cached, a number of intermediate read only layers. The read only layers are created with each step in the Dockerfile and the final readable/writeable layer contains the intermediate layers. Prior to these changes Buildah did not cache these intermediate read only layers. This release has a new environment variable ‘BUILDAH_LAYERS’ and a new ‘buildah bud’ --layers parameter. When either is set to true, the image layers are cached during the ‘buildah bud’ processing and not discarded. The disadvantage to retaining layers is the space that they use. The advantage to retaining them is if you make a change to your Dockerfile, only the layers for that change and the ones following it will need to be regenerated. The --nocache parameter has also been added to the ‘buildah bud’ command. When this parameter is set to true the ‘buildah bud’ command ignores any existing layers and creates all of the image layers anew. ### Added initial user namespace support. To isolate the container’s processes from running as root on the host machine, user namespaces are used by container technologies. This allows the administrator to configure the containers processes that must run as root to remap the user to a less privileged user on the container’s host machine. This remapping is handled in part by settings in the /etc/subuid and /etc/subgid files on the host machine. The changes for this release does not yet provide full support for user namespaces, but does set up the options to control the mapping with the --userns-uid-map and --userns-gid-map options. Changes have also been made to prevent the container from modifying the /etc/host or /etc/resolv.conf files on the host. Also with this release if a user with a uid that’s not equal to zero creates a container, a namespace is now created based on the users uid and gid and the container will be reexec’d using that namespace. In addition, the storage driver, storage root directory and storage state directory will all be created under alternate locations. Please reference the buildah (1) man page for more details. Further information will be published in upcoming blogs and additional changes are in progress to provide full support of user namespaces in future versions of Buildah. ### CVE security issues with /proc/acpi and /proc/keys have been addressed. The /proc/acpi and /proc/keys were added to the list of blocked kernel files. This prevents the container from manipulating these files on the container’s host. ## Release Changes * Added the ability to remove or retain image layers for ‘buildah bud’: * Add --layers and --no-cache options to 'buildah bud'. * Add --rm and --force-rm options to 'buildah bud'. * Fixed the buildah bud --layers option. * Added environment variable BUILDAH_LAYERS to control image layers creation. * Added environment variable BUILDAH_RUNTIME to setup alternate runtimes. * build-using-dockerfile: let -t include transports again. * Block the use of /proc/acpi and /proc/keys from inside containers. These address potential CVE Security issues. * Add --cidfile option to 'buildah from`. * Add a --loglevel option to build-with-dockerfile. * Begin supporting specification of user namespace for container separation: * Allow --userns-uid-map/--userns-gid-map to be global options. * If unprivileged, reexec in a user namespace. * Force ownership of /etc/hosts and /etc/resolv.conf to 0:0. * Recognize committing to second storage locations with 'buildah commit'. * Add the --all option to 'buildah unmount' to unmount all mounted containers. * When doing multiple mounts, output all pertinent errors, not just the last error. * Implement basic recognition of the "--isolation" option for 'buildah from' and 'buildah run'. * Fix ARGS parsing for run commands. * When building a container the HTTP User-Agent is set to the Buildah version. * Makefile: add the uninstall command. * Support multiple inputs to 'buildah mount'. * Use the right formatting when adding entries to /etc/hosts. * A number of minor performance improvements for 'buildah run' and 'buildah bud'. * Change RunOptions.Stdin/Stdout/Stderr to just be Reader/Writers. * Use conversion code from containers/image instead of converting configs manually. * Do not ignore any parsing errors during initialization. * Explicitly handle "from scratch" images in Builder.initConfig. * Fix parsing of OCI images. * Don't ignore v2s1 history if docker_version is not set. * Add --all,-a flags to 'buildah images'. * Remove tty check from buildah images --format. * Fix usage information for 'buildah images'. * Documentation changes: * Add registries.conf link to a few man pages. * Add information about the configuration files to the install documentation. * Follow man-pages(7) suggestions for SYNOPSIS in all man pages. * Minor update to buildah config documentation for entrypoint. * ONBUILD tutorial created. * Touch up images man page. * Plus a number of smaller fixes. ## Try it Out. If you haven’t yet, install Buildah from the Fedora repo or GitHub and give it a spin. We’re betting you'll find it’s an easy and quick way to build containers in your environment without a daemon being involved! For those of you who contributed to this release, thank you very much for your contributions! If you haven't joined our community yet, don't wait any longer! Come join us in GitHub, where Open Source communities live. ## Buildah == Simplicity ================================================ FILE: docs/release-announcements/v1.3.md ================================================ # Buildah version 1.3 Release Announcement ![buildah logo](https://cdn.rawgit.com/containers/buildah/main/logos/buildah-logo_large.png) We're pleased to announce the release of Buildah version 1.3 which is now available from GitHub for any Linux distro. We are shipping this release on Fedora, RHEL 7, CentOS, openSUSE and Ubuntu in the near future. The Buildah project has continued to grow over the past several weeks, welcoming several new contributors to the mix. The highlights of this release are Dockerfile handling improvements, added the `buildah pull` command, added the `buildah rename` command, updated ulimits settings, added isolation control and several other enhancements and bug fixes. ## The major highlights for this release are: * Dockerfiles with a ‘.in’ suffix are preprocessed during the build process. CPP is now used by the ‘buildah bud’ command to preprocess any Dockerfile that has the ‘.in’ suffix. This allows Dockerfiles to be decomposed and make them reusable via CPP’s #include directive. Notice that those Dockerfiles can still be used by other tools by manually running cpp -E on them. Stay tuned for an upcoming blog with an example. (Many thanks to Valentin Rothberg for providing this functionality.) * Dockerfile input can come from stdin. If you use a dash ‘-’ as the argument to the `buildah bud --file` parameter, Dockerfile contents will be read from stdin. * Created a pull and rename command. The new `buildah pull` command pulls an image without creating a container like the `buildah from` command does. The new `buildah rename` command renames a container. * Ulimits settings now match the settings we add to the Docker unit file. The maximum number of processes and the number of open files that Buildah will handle now match the same number that Docker handles. * Added the ability to select the type of isolation to be used. By setting the new BUILDAH_ISOLATION environment variable or by using the new --isolation parameter found in the bud, from and run commands, one can select the type of isolation to use for running processes as part of the RUN instruction. Recognized types include oci, rootless and chroot. For more details, please refer to the `buildah bud`, `buildah from` and `buildah run` man pages. These new isolations are being added to run buildah inside locked down containers. ## Release Changes * preprocess ".in" suffixed Dockerfiles. * Allow Dockerfile content to come from stdin. * Create buildah pull command. * Create buildah rename command. * Set the default ulimits to match Docker. * Set BUILDAH_ISOLATION=rootless when running unprivileged. * Add and implement IsolationOCIRootless. * Add a value for IsolationOCIRootless. * Fix rmi to remove intermediate images associated with an image. * Switch to github.com/containers/image/pkg/sysregistriesv2. * unshare: error message missed the pid. * bud should not search just the context directory for Dockerfile. * Add support for multiple Short options. * Fixed volume cache issue with buildah bud --layers. * Allow ping command without NET_RAW Capabilities. * usernamespace: assign additional IDs sequentially. * Remove default dev/pts which allows Buildah to be run as non-root. * Documentation changes: * Fix the the in buildah-config man page. * Updated the following packages to newer versions: containers/image, containers/storage, runc, and urfave/cli. * Plus a number of smaller fixes. ## Try it Out. If you haven’t yet, install Buildah from the Fedora repo or GitHub and give it a spin. We’re betting you'll find it’s an easy and quick way to build containers in your environment without a daemon being involved! For those of you who contributed to this release, thank you very much for your contributions! If you haven't joined our community yet, don't wait any longer! Come join us in GitHub, where Open Source communities live. ## Buildah == Simplicity ================================================ FILE: docs/release-announcements/v1.4.md ================================================ # Buildah version 1.4 Release Announcement ![buildah logo](https://buildah.io/images/buildah.png) We're pleased to announce the release of Buildah version 1.4 which is now available from GitHub for any Linux distro. We are shipping this release on Fedora, RHEL 7, CentOS, openSUSE and Ubuntu in the near future. The Buildah project has continued to grow over the past several weeks, welcoming several new contributors to the mix. The highlights of this release are fixes for "rootless" users, improvements in symbolic link and chroot handling in Dockerfiles, the addition of a `pull` command, better error messaging for OCI containers and several other enhancements and bug fixes. ## The major highlights for this release are: * Issues with a rootless user cleaned up. - A variety of issues were cleaned up in this space. They include: * Additional groups were not reset for a rootless user when creating a new user namespace. For example users of the 'docker' group are now able to use the docker-daemon: destination. * Builtin volumes are now owned by the UID/GID of the container. * Removed the --no-pivot functionality as it could cause EPERM issues in a rootless user environment. * Symbolic links handling for ADD and COPY If a symbolic link was used as part of an ADD or COPY command in a Dockerfile, the link itself and not the underlying file(s) were copied. This has been corrected so that the files pointed to by the symbolic link are now copied. * COPY --chown in a Dockerfile The COPY command with the --chown parameter in a Dockerfile is now processed correctly. * The pull command has been created. The `buildah pull` command has been created. It works like the `from` command however it only pulls the image and does not build a container like the `from` command does. * Non-OCI command handling in a Dockerfile If a command that is not OCI compliant is encountered when building an OCI formatted container image, better error messages are now displayed. If a non-OCI formatted container image is desired, the '--format=docker' option should be passed to the bud command. * We've moved! The Buildah project has moved from projectatomic/buildah to containers/buildah in GitHub.com. Come check out our new home! * buildah.io website created! Not really tied to this release, but shortly before this release the buildah.io website was created. There you can find blogs, release announcements, talks and more. Go check it out if you haven't already! ## Release Changes * Add the isolation option to the `from` command. * Change SELinux MCS Label handling to avoid collisions with Podman. * Fixed a number of issues with the `bud` --layers option. * The `rmi --prune` option no longer accepts an ImageID . * Additional groups are not reset for a rootless user when creating a new user namespace. * Builtin volumes are now owned by the UID/GID of the container. * Better error reporting for image-pulling errors with the `from` and `pull` commands. * Allow an empty destination for the `push` command. If empty, the source image parameter is reused for the destination. * Missing parent directories for volume mounts. * A number of commands have added verification to the flag ordering. * Error messages have been cleaned up when options are misordered. * Fixed a rare race condition in the `bud` command when pulling and naming the image. * Symbolic links in ADD/COPY Dockerfile commands are handled correctly. * The `push` command now shows the image digest after it succeeds. * The `rename` command verifies that the container name is not already in use. * The `containers` command no longer exits early when the --json option is used. * Provide better errors when non-OCI commands are in a Dockerfile when building an OCI container image. * After renaming a container, the correct name is now always shown in the `containers` command. * A number of small changes were made to Buildah images building process to more closely match Docker images. * Documented BUILDAH_* environment variables in `buildah bud --help` usage output. * Using the ADD command in a Dockerfile is now handled correctly when the --layers option is used with the bud command. * After deleting an image the correct image ID is now displayed. * COPY --chown in a Dockerfile is processed correctly. * The `run` command processing now bind mounts an empty directory for masking when using chroot. * Removed --no-pivot for rootless isolation. * Removed the stream option from the `bud` command. * Added a `pull` command. * Added a Docker conformance test suite. * Documentation Changes: * Created a Container Tools Guide. * Updated the following packages to newer versions: CNI, containers/image, containers/storage, imagebuilder, libpod, runc, and urfave/cli. * Plus a number of smaller fixes. ## Try it Out. If you haven’t yet, install Buildah from one of the Linux repos or GitHub and give it a spin. We’re betting you'll find it’s an easy and quick way to build containers in your environment without a daemon being involved! For those of you who contributed to this release, thank you very much for your contributions! If you haven't joined our community yet, don't wait any longer! Come join us in GitHub, where Open Source communities live. ## Buildah == Simplicity ================================================ FILE: docs/release-announcements/v1.5.md ================================================ # Buildah version 1.5 Release Announcement ![buildah logo](https://buildah.io/images/buildah.png) We're pleased to announce the release of Buildah version 1.5 which is now available from GitHub for any Linux distro. We are shipping this release on Fedora, RHEL 7, CentOS, openSUSE and Ubuntu in the near future. The Buildah project has continued to grow over the past several weeks, welcoming several new contributors to the mix. Updates were made to rootless user handling, added support for a few Dockerfile commands that were missing, a number of performance changes for the underlying pull commands and bug fixes. ## The major highlights for this release are: * A variety of updates were made in rootless user handling: - Let runc auto-detect rootless capability. This will allow for different runtimes to run in rootless mode. - If slirp4netns is available, it is now used to configure the network for the rootless isolation mode. - Support for fuse-overlayfs has been added. - Usernamespaces are now created only when they are needed. * Dockerfile handling improvements. - If the ARG command was the first line in the Dockerfile, Buildah did not process it. This has been corrected. - The “FROM {image} AS {reference}” command in a Dockerfile is now supported. * A number of performance changes have been made to the underlying pull functionality. * If a directory was passed the ‘bud’ commands --file parameter a panic would occur even if a Dockerfile was in the directory. This has been corrected. ## Release Changes * rootless: do not specify --rootless to the OCI runtime. * rootless: use slirp4netns to setup the network namespace. * rootless: only discard network configuration names. * run: only set up /etc/hosts or /etc/resolv.conf with network. * run: bind mount /etc/hosts and /etc/resolv.conf if not in a volume. * Handle directories better in ‘bud --file’. * common: support a per-user registries conf file. * unshare: do not override the configuration. * common: honor the rootless configuration file. * unshare: create a new mount namespace. * unshare: support libpod rootless pkg. * Use libpod GetDefaultStorage to report proper storage config. * Allow container storage to manage the SELinux labels. * When the value of isolation is provided to the run command, use the value provided instead of the default value. * Fix ‘buildah version’ error when build time could not be determined on some systems. * Walk symlinks when checking cached images for copied/added files. * ReserveSELinuxLabels(): handle wrapped errors from OpenBuilder. * Set WorkingDir to empty, not ‘/’ for conformance to Docker. * Allow setting --no-pivot default with an env var. * Add the --no-pivot flag to the run command. * Improve reporting about individual pull failures. * Return a "search registries were needed but empty" indication in util.ResolveName. * Simplify handling of the "tried to pull an image but found nothing" case. * Don't even invoke the pull loop if options.FromImage == "". * Eliminate the long-running ref and img variables in resolveImage. * In resolveImage, return immediately on success. * Fix From As in Dockerfile. * Sort CLI flags of buildah bud. * unshare: detect when unprivileged userns are disabled. * chroot: fix the args check. * buildah: use the same logic for XDG_RUNTIME_DIR as podman. * Podman --privileged selinux is broken. * parse: Modify the return value. * parse: modify the verification of the isolation value. * Make sure we log or return every error. * pullImage(): when completing an image name, try docker://. * Enforce "blocked" for registries for the "docker" transport. * Correctly set DockerInsecureSkipTLSVerify when pulling images. * chroot: set up seccomp and capabilities after supplemental groups. * chroot: fix capabilities list setup and application. * namespaces.bats: fix handling of uidmap/gidmap options in pairs. * chroot: only create user namespaces when we know we need them. * Check /proc/sys/user/max_user_namespaces on unshare(NEWUSERNS). * bash/buildah: add isolation option to the from command for bash completions. * Updated the following packages to newer versions: containers/image, containers/storage, libpod, and opencontainers/selinux. * Plus a number of smaller fixes. ## Try it Out. If you haven’t yet, install Buildah from one of the Linux repos or GitHub and give it a spin. We’re betting you'll find it’s an easy and quick way to build containers in your environment without a daemon being involved! For those of you who contributed to this release, thank you very much for your contributions! If you haven't joined our community yet, don't wait any longer! Come join us in GitHub, where Open Source communities live. ## Buildah == Simplicity ================================================ FILE: docs/samples/registries.conf ================================================ ## containers-registries.conf(5): System Registry Configuration File # # This is a sample of system-wide configuration file used to # keep track of registries for various container backends. # It adheres to TOML format and does not support recursive # lists of registries. # The default location for this configuration file is /etc/containers/registries.conf. # The only valid categories are: 'registries.search', 'registries.insecure', # and 'registries.block'. [registries.search] registries = ['quay.io', 'docker.io'] # If you need to access insecure registries, add the registry's fully-qualified name. # An insecure registry is one that does not have a valid SSL certificate or only does HTTP. [registries.insecure] registries = [] # If you need to block pull access from a registry, uncomment the section below # and add the registries fully-qualified name. # # Docker only [registries.block] registries = [] ================================================ FILE: docs/tutorials/01-intro.md ================================================ ![buildah logo](https://cdn.rawgit.com/containers/buildah/main/logos/buildah-logo_large.png) # Buildah Tutorial 1 ## Building OCI container images The purpose of this tutorial is to demonstrate how Buildah can be used to build container images compliant with the [Open Container Initiative](https://www.opencontainers.org/) (OCI) [image specification](https://github.com/opencontainers/image-spec). Images can be built based on existing images, from scratch, and using Dockerfiles. OCI images built using the Buildah command line tool (CLI) and the underlying OCI based technologies (e.g. [containers/image](https://github.com/containers/image) and [containers/storage](https://github.com/containers/storage)) are portable and can therefore run in a Docker environment. In brief the `containers/image` project provides mechanisms to copy (push, pull), inspect, and sign container images. The `containers/storage` project provides mechanisms for storing filesystem layers, container images, and containers. Buildah is a CLI that takes advantage of these underlying projects and therefore allows you to build, move, and manage container images and containers. Buildah works on a number of Linux distributions, but is not supported on Windows or Mac platforms at this time. Buildah specializes mainly in building OCI images while [Podman](https://podman.io) provides a broader set of commands and functions that help you to maintain, modify and run OCI images and containers. For more information on the difference between the projects please refer to the [Buildah and Podman relationship](https://github.com/containers/buildah#buildah-and-podman-relationship) section on the main README.md. ## Configure and Install Buildah Note that installation instructions below assume you are running a Linux distro that uses `dnf` as its package manager, and have all prerequisites fulfilled. See Buildah's [installation instructions][buildah-install] for a full list of prerequisites, and the `buildah` installation section in the [official Red Hat documentation][rh-repo-docs] for RHEL-specific instructions. [buildah-install]:../../install.md [rh-repo-docs]:https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/building_running_and_managing_containers/ First step is to install Buildah. Run as root because you will need to be root for installing the Buildah package: ```console $ sudo -s ``` Then install buildah by running: ```console # dnf -y install buildah ``` ## Rootless User Configuration If you plan to run Buildah as a user without root privileges, i.e. a "rootless user", the administrator of the system might have to do a bit of additional configuration beforehand. The setup required for this is listed on the Podman GitHub site [here](https://github.com/containers/podman/blob/main/docs/tutorials/rootless_tutorial.md). Buildah has the same setup and configuration requirements that Podman does for rootless users. ## Post Installation Verification After installing Buildah we can see there are no images installed. The `buildah images` command will list all the images: ```console # buildah images ``` We can also see that there are also no working containers by running: ```console # buildah containers ``` When you build a working container from an existing image, Buildah defaults to appending '-working-container' to the image's name to construct a name for the container. The Buildah CLI conveniently returns the name of the new container. You can take advantage of this by assigning the returned value to a shell variable using standard shell assignment: ```console # container=$(buildah from fedora) ``` It is not required to assign the container's name to a shell variable. Running `buildah from fedora` is sufficient. It just helps simplify commands later. To see the name of the container that we stored in the shell variable: ```console # echo $container ``` What can we do with this new container? Let's try running bash: ```console # buildah run $container bash ``` Notice we get a new shell prompt because we are running a bash shell inside of the container. It should be noted that `buildah run` is primarily intended for debugging and running commands as part of the build process. A more full-featured engine like Podman or a container runtime interface service like [CRI-O](https://github.com/kubernetes-sigs/cri-o) is more suited for starting containers in production. Be sure to `exit` out of the container and let's try running something else: ```console # buildah run $container java ``` Oops. Java is not installed. A message containing something like the following was returned. ``` runc create failed: unable to start start container process: exec: "java": executable file not found in $PATH ``` Let's try installing it inside the container using: ```console # buildah run $container -- dnf -y install java ``` The `--` syntax basically tells Buildah: there are no more `buildah run` command options after this point. The options after this point are for the command that's started inside the container. It is required if the command we specify includes command line options which are not meant for Buildah. Now running `buildah run $container java` will show that Java has been installed. It will return the standard Java `Usage` output. ## Building a container from scratch One of the advantages of using `buildah` to build OCI compliant container images is that you can easily build a container image from scratch and therefore exclude unnecessary packages from your image. Most final container images for production probably don't need a package manager like `dnf`. Let's build a container and image from scratch. The special "image" name "scratch" tells Buildah to create an empty container. The container has a small amount of metadata about the container but no real Linux content. ```console # newcontainer=$(buildah from scratch) ``` You can see this new empty container by running: ```console # buildah containers ``` You should see output similar to the following: ``` CONTAINER ID BUILDER IMAGE ID IMAGE NAME CONTAINER NAME 82af3b9a9488 * 3d85fcda5754 docker.io/library/fedora:latest fedora-working-container ac8fa6be0f0a * scratch working-container ``` Its container name is working-container by default and it's stored in the `$newcontainer` variable. Notice the image name (IMAGE NAME) is "scratch". This is a special value that indicates that the working container wasn't based on an image. When we run: ```console # buildah images ``` We don't see the "scratch" image listed. There is no corresponding scratch image. A container based on "scratch" starts from nothing. So does this container actually do anything? Let's see. ```console # buildah run $newcontainer bash ``` Nope. This really is empty. The package installer `dnf` is not even inside this container. It's essentially an empty layer on top of the kernel. So what can be done with that? Thankfully there is a `buildah mount` command. ```console # scratchmnt=$(buildah mount $newcontainer) ``` Note: If attempting to mount in rootless mode, the command fails. Mounting a container can only be done in a mount namespace that you own. Create and enter a user namespace and mount namespace by executing the `buildah unshare` command. See buildah-mount(1) man page for more information. ```console $ export newcontainer $ buildah unshare # scratchmnt=$(buildah mount $newcontainer) ``` By echoing `$scratchmnt` we can see the path for the [overlay mount point](https://wiki.archlinux.org/index.php/Overlay_filesystem), which is used as the root file system for the container. ```console # echo $scratchmnt /var/lib/containers/storage/overlay/b78d0e11957d15b5d1fe776293bd40a36c28825fb6cf76f407b4d0a95b2a200d/merged ``` Notice that the overlay mount point is somewhere under `/var/lib/containers/storage` if you started out as root, and under your home directory's `.local/share/containers/storage` directory if you're in rootless mode. (See above on `containers/storage` or for more information see [containers/storage](https://github.com/containers/storage).) Now that we have a new empty container we can install or remove software packages or simply copy content into that container. So let's install `bash` and `coreutils` so that we can run bash scripts. This could easily be `nginx` or other packages needed for your container. **NOTE:** the version in the example below (42) relates to a Fedora version which is the Linux platform this example was run on. If you are running dnf on the host to populate the container, the version you specify must be valid for the host or dnf will throw an error. I.e. If you were to run this on a RHEL platform, you'd need to specify `--releasever 8.1` or similar instead of `--releasever 42`. If you want the container to be a particular Linux platform, change `scratch` in the first line of the example to the platform you want, i.e. `# newcontainer=$(buildah from fedora)`, and then you can specify an appropriate version number for that Linux platform. ```console # dnf install --installroot $scratchmnt --releasever 42 bash coreutils --use-host-config --setopt "*.countme=false" --setopt install_weak_deps=false -y ``` Let's try it out (showing the prompt in this example to demonstrate the difference): ```console # buildah run $newcontainer sh sh-5.1# cd /usr/bin sh-5.1# ls sh-5.1# exit ``` Notice we now have a `/usr/bin` directory in the newcontainer's root file system. Let's first copy a simple file from our host into the container. Create a file called runecho.sh which contains the following: ```console #!/usr/bin/env bash for i in `seq 0 9`; do echo "This is a new container from ipbabble [" $i "]" done ``` Change the permissions on the file so that it can be run: ```console # chmod +x runecho.sh ``` With `buildah` files can be copied into the new container. We can then use `buildah run` to run that command within the container by specifying the command. We can also configure the image we'll create from this container to run the command directly when we run it using [Podman](https://github.com/containers/podman) and its `podman run` command. In short the `buildah run` command is equivalent to the "RUN" command in a Dockerfile (it always needs to be told what to run), whereas `podman run` is equivalent to the `docker run` command (it can look at the image's configuration to see what to run). Now let's copy this new command into the container's `/usr/bin` directory, configure the command to be run when the image is run by `podman`, and create an image from the container's root file system and configuration settings: ```console # To test with Podman, first install via: # dnf -y install podman # buildah copy $newcontainer ./runecho.sh /usr/bin/ # buildah config --cmd /usr/bin/runecho.sh $newcontainer # buildah commit $newcontainer newimage ``` We've got a new image named "newimage". The container is still there because we didn't remove it. Now run the command in the container with Buildah specifying the command to run in the container: ```console # buildah run $newcontainer /usr/bin/runecho.sh This is a new container from ipbabble [ 0 ] This is a new container from ipbabble [ 1 ] This is a new container from ipbabble [ 2 ] This is a new container from ipbabble [ 3 ] This is a new container from ipbabble [ 4 ] This is a new container from ipbabble [ 5 ] This is a new container from ipbabble [ 6 ] This is a new container from ipbabble [ 7 ] This is a new container from ipbabble [ 8 ] This is a new container from ipbabble [ 9 ] ``` Now use Podman to run the command in a new container based on our new image (no command required): ```console # podman run --rm newimage This is a new container from ipbabble [ 0 ] This is a new container from ipbabble [ 1 ] This is a new container from ipbabble [ 2 ] This is a new container from ipbabble [ 3 ] This is a new container from ipbabble [ 4 ] This is a new container from ipbabble [ 5 ] This is a new container from ipbabble [ 6 ] This is a new container from ipbabble [ 7 ] This is a new container from ipbabble [ 8 ] This is a new container from ipbabble [ 9 ] ``` It works! Congratulations, you have built a new OCI container image from scratch that uses bash scripting. Back to Buildah, let's add some more configuration information. ```console # buildah config --created-by "ipbabble" $newcontainer # buildah config --author "wgh at redhat.com @ipbabble" --label name=fedora42-bashecho $newcontainer ``` We can inspect the working container's metadata using the `inspect` command: ```console # buildah inspect $newcontainer ``` We should probably unmount the working container's rootfs. We will need to commit the container again to create an image that includes the two configuration changes we just made: ```console # buildah unmount $newcontainer # buildah commit $newcontainer fedora-bashecho # buildah images ``` And you can see there is a new image called `localhost/fedora-bashecho:latest`. You can inspect the new image using: ```console # buildah inspect --type=image fedora-bashecho ``` Later when you want to create a new container or containers from this image, you simply need to do `buildah from fedora-bashecho`. This will create a new container based on this image for you. Now that you have the new image you can remove the scratch container called working-container: ```console # buildah rm $newcontainer ``` or ```console # buildah rm working-container ``` ## OCI images built using Buildah are portable Let's test if this new OCI image is really portable to another container engine like Docker. First you should install Docker and start it. Notice that Docker requires a running daemon process in order to run any client commands. Buildah and Podman have no daemon requirement. ```console # dnf -y install docker # systemctl start docker ``` Let's copy that image from where containers/storage stores it to where the Docker daemon stores its images, so that we can run it using Docker. We can achieve this using `buildah push`. This copies the image to Docker's storage area which is located under `/var/lib/docker`. Docker's storage is managed by the Docker daemon. This needs to be explicitly stated by telling Buildah to push the image to the Docker daemon using `docker-daemon:`. ```console # buildah push fedora-bashecho docker-daemon:fedora-bashecho:latest ``` Under the covers, the containers/image library calls into the containers/storage library to read the image's contents from where buildah keeps them, and sends them to the local Docker daemon, which writes them to where it keeps them. This can take a little while. And usually you won't need to do this. If you're using `buildah` you are probably not using Docker. This is just for demo purposes. Let's try it: ```console # docker run --rm fedora-bashecho This is a new container from ipbabble [ 0 ] This is a new container from ipbabble [ 1 ] This is a new container from ipbabble [ 2 ] This is a new container from ipbabble [ 3 ] This is a new container from ipbabble [ 4 ] This is a new container from ipbabble [ 5 ] This is a new container from ipbabble [ 6 ] This is a new container from ipbabble [ 7 ] This is a new container from ipbabble [ 8 ] This is a new container from ipbabble [ 9 ] ``` OCI container images built with `buildah` are completely standard as expected. So now it might be time to run: ```console # dnf -y remove docker ``` ## Using Containerfiles/Dockerfiles with Buildah What if you have been using Docker for a while and have some existing Dockerfiles? Not a problem. Buildah can build images using a Dockerfile. The `build` command takes a Dockerfile as input and produces an OCI image. Find one of your Dockerfiles or create a file called Dockerfile. Use the following example or some variation if you'd like: ```Dockerfile # Base on the most recently released Fedora FROM fedora:latest MAINTAINER ipbabble email buildahboy@redhat.com # not a real email # Install updates and httpd RUN echo "Updating all fedora packages"; dnf -y update; dnf -y clean all RUN echo "Installing httpd"; dnf -y install httpd && dnf -y clean all # Expose the default httpd port 80 EXPOSE 80 # Run the httpd CMD ["/usr/sbin/httpd", "-DFOREGROUND"] ``` Now run `buildah build` with the name of the Dockerfile and the name to be given to the created image (e.g. fedora-httpd): ```console # buildah build -f Dockerfile -t fedora-httpd . ``` or, because `buildah build` defaults to `Dockerfile` and using the current directory as the build context: ```console # buildah build -t fedora-httpd ``` You will see all the steps of the Dockerfile executing. Afterwards `buildah images` will show you the new image. Now we can create a container from the image and test it with `podman run`: ```console # podman run --rm -p 8123:80 fedora-httpd ``` While that container is running, in another shell run: ```console # curl localhost:8123 ``` You will see the standard Apache webpage. Why not try and modify the Dockerfile. Do not install httpd, but instead ADD the runecho.sh file and have it run as the CMD. ## Congratulations Well done. You have learned a lot about Buildah using this short tutorial. Hopefully you followed along with the examples and found them to be sufficient. Be sure to look at Buildah's man pages to see the other useful commands you can use. Have fun playing. If you have any suggestions or issues please post them at the [Buildah Issues page](https://github.com/containers/buildah/issues). For more information on Buildah and how you might contribute please visit the [Buildah home page on GitHub](https://github.com/containers/buildah). ================================================ FILE: docs/tutorials/02-registries-repositories.md ================================================ ![buildah logo](https://cdn.rawgit.com/containers/buildah/main/logos/buildah-logo_large.png) # Buildah Tutorial 2 ## Using Buildah with container registries The purpose of this tutorial is to demonstrate how Buildah can be used to move OCI compliant images in and out of private or public registries. In the [first tutorial](https://github.com/containers/buildah/blob/main/docs/tutorials/01-intro.md) we built an image from scratch that we called `fedora-bashecho` and we pushed it to a local Docker daemon using the `docker-daemon` protocol. We are going to push the same image to a private container registry. First we must pull down a registry. As a shortcut we will save the container name that is returned from the `buildah from` command, into a bash variable called `registry`. This is just like we did in Tutorial 1: ```console # registryctr=$(buildah from registry) ``` It is worth pointing out that the `from` command can also use other protocols beyond the default (and implicitly assumed) order that first looks in local containers-storage (containers-storage:) and then looks in a container registry (by default, Docker Hub) (docker:). For example, if you already had a registry container image downloaded by a local Docker daemon then you could use the following: ```console # registryctr=$(buildah from docker-daemon:registry:latest) ``` Then we need to start the registry. You should start the registry in a separate shell and leave it running there: ```console # buildah run --net=host $registryctr /entrypoint.sh /etc/docker/registry/config.yml ``` If you would like to see more details as to what is going on inside the registry, especially if you are having problems with the registry, you can run the registry container in debug mode as follows: ```console # buildah --log-level=debug run --net=host $registryctr /entrypoint.sh /etc/docker/registry/config.yml ``` You can use `--log-level=debug` on any Buildah command. The registry is running and is waiting for requests to process. Notice that this registry is a Docker registry that we pulled from Docker Hub and we are running it for this example using `buildah run`. There is no Docker daemon running at this time. Let's push our image to the private registry. By default, Buildah is set up to only make secure connections to a registry. Therefore we will need to turn the TLS verification off using the `--tls-verify` flag. We also need to tell Buildah that the registry is on this local host (i.e. localhost) and listening on port 5000. Similar to what you'd expect to do on multi-tenant Docker Hub, we will explicitly specify that the registry is to store the image under the `ipbabble` repository - so as not to clash with other users' similarly named images. ```console # buildah push --tls-verify=false fedora-bashecho docker://localhost:5000/ipbabble/fedora-bashecho:latest ``` [Skopeo](https://github.com/containers/skopeo) is a containers tool that was created to inspect images in registries without having to pull the image from the registry. It has grown to have many other uses. We will verify that the image has been stored by using Skopeo to inspect the image in the registry: ```console # skopeo inspect --tls-verify=false docker://localhost:5000/ipbabble/fedora-bashecho:latest { "Name": "localhost:5000/ipbabble/fedora-bashecho", "Digest": "sha256:6806f9385f97bc09f54b5c0ef583e58c3bc906c8c0b3e693d8782d0a0acf2137", "RepoTags": [ "latest" ], "Created": "2017-12-05T21:38:12.311901938Z", "DockerVersion": "", "Labels": { "name": "fedora-bashecho" }, "Architecture": "amd64", "Os": "linux", "Layers": [ "sha256:0cb7556c714767b8da6e0299cbeab765abaddede84769475c023785ae66d10ca" ] } ``` We can verify that it is still portable to Docker by starting Docker again, as we did in the first tutorial. Then we can pull down the image and start the container using Docker: ```console # systemctl start docker # docker pull localhost:5000/ipbabble/fedora-bashecho Using default tag: latest Trying to pull repository localhost:5000/ipbabble/fedora-bashecho ... sha256:6806f9385f97bc09f54b5c0ef583e58c3bc906c8c0b3e693d8782d0a0acf2137: Pulling from localhost:5000/ipbabble/fedora-bashecho 0cb7556c7147: Pull complete Digest: sha256:6806f9385f97bc09f54b5c0ef583e58c3bc906c8c0b3e693d8782d0a0acf2137 Status: Downloaded newer image for localhost:5000/ipbabble/fedora-bashecho:latest # docker run --rm localhost:5000/ipbabble/fedora-bashecho This is a new container named ipbabble [ 0 ] This is a new container named ipbabble [ 1 ] This is a new container named ipbabble [ 2 ] This is a new container named ipbabble [ 3 ] This is a new container named ipbabble [ 4 ] This is a new container named ipbabble [ 5 ] This is a new container named ipbabble [ 6 ] This is a new container named ipbabble [ 7 ] This is a new container named ipbabble [ 8 ] This is a new container named ipbabble [ 9 ] # systemctl stop docker ``` Pushing to Docker Hub is just as easy. Of course you must have an account with credentials. In this example I'm using a Docker Hub API key, which has the form "username:password" (example password has been edited for privacy), that I created with my Docker Hub account. I use the `--creds` flag to use my API key. I also specify my local image name `fedora-bashecho` as my image source and I use the `docker` protocol with no registry name or port so that it will look at the default port on the default Docker Hub registry: ```console # buildah push --creds=ipbabble:5bbb9990-6eeb-1234-af1a-aaa80066887c fedora-bashecho docker://ipbabble/fedora-bashecho:latest ``` And let's inspect that with Skopeo: ```console # skopeo inspect --creds ipbabble:5bbb9990-6eeb-1234-af1a-aaa80066887c docker://ipbabble/fedora-bashecho:latest { "Name": "docker.io/ipbabble/fedora-bashecho", "Digest": "sha256:6806f9385f97bc09f54b5c0ef583e58c3bc906c8c0b3e693d8782d0a0acf2137", "RepoTags": [ "latest" ], "Created": "2017-12-05T21:38:12.311901938Z", "DockerVersion": "", "Labels": { "name": "fedora-bashecho" }, "Architecture": "amd64", "Os": "linux", "Layers": [ "sha256:0cb7556c714767b8da6e0299cbeab765abaddede84769475c023785ae66d10ca" ] } ``` We can use Buildah to pull down the image using the `buildah from` command. But before we do let's clean up our local containers-storage so that we don't already have a copy of the fedora-bashecho image - otherwise Buildah will know it already exists and not bother pulling it down. ```console # buildah images IMAGE ID IMAGE NAME CREATED AT SIZE d4cd7d73ee42 docker.io/library/registry:latest Dec 1, 2017 22:15 31.74 MB e31b0f0b0a63 docker.io/library/fedora-bashecho:latest Dec 5, 2017 21:38 772 B # buildah rmi fedora-bashecho untagged: docker.io/library/fedora-bashecho:latest e31b0f0b0a63e94c5a558d438d7490fab930a282a4736364360ab9b92cb25f3a # buildah images IMAGE ID IMAGE NAME CREATED AT SIZE d4cd7d73ee42 docker.io/library/registry:latest Dec 1, 2017 22:15 31.74 MB ``` Okay, so we don't have a fedora-bashecho image anymore. Let's pull the image from Docker Hub: ```console # buildah from ipbabble/fedora-bashecho ``` If you don't want to bother doing the remove image step (`rmi`) you can use the flag `--pull-always` to force the image to be pulled again and overwrite any corresponding local image. Now check that image is in the local containers-storage: ```console # buildah images IMAGE ID IMAGE NAME CREATED AT SIZE d4cd7d73ee42 docker.io/library/registry:latest Dec 1, 2017 22:15 31.74 MB 864871ac1c45 docker.io/ipbabble/fedora-bashecho:latest Dec 5, 2017 21:38 315.4 MB ``` Success! If you have any suggestions or issues please post them at the [Buildah Issues page](https://github.com/containers/buildah/issues). For more information on Buildah and how you might contribute please visit the [Buildah home page on Github](https://github.com/containers/buildah). ================================================ FILE: docs/tutorials/03-on-build.md ================================================ ![buildah logo](../../logos/buildah-logo_large.png) # Buildah Tutorial 3 ## Using ONBUILD in Buildah The purpose of this tutorial is to demonstrate how Buildah can use a Dockerfile with the ONBUILD instruction within it or how the ONBUILD instruction can be used with the `buildah config` command. The ONBUILD instruction stores a command in the meta data of a container image which is then invoked when the image is used as a base image. The image can have multiple ONBUILD instructions. Note: The ONBUILD instructions do not change the content of the image that contain the instructions, only the container images that are created from this image are changed based on the FROM command. Container images that are compliant with the [Open Container Initiative][] (OCI) [image specification][] do not support the ONBUILD instruction. Images that are created by Buildah are in the OCI format by default. Only container images that are created by Buildah in the Docker format can use the ONBUILD instruction. The OCI format can be overridden in Buildah by specifying the Docker format with the `--format=docker` option or by setting the BUILDAH_FORMAT environment variable to 'docker'. Regardless of the format selected, Buildah is capable of working seamlessly with either OCI or Docker compliant images and containers. On to the tutorial. The first step is to install Buildah. In short, the `buildah run` command emulates the RUN command that is found in a Dockerfile while the `podman run` command emulates the `docker run` command. For the purpose of this tutorial Buildah's run command will be used. As an aside, Podman is aimed at managing containers, images, and pods while Buildah focuses on the building of container images. For more info on Podman, please go to [Podman's site][]. ## Setup The following assumes installation on Fedora. Run as root because you will need to be root for installing the Buildah package: ```console $ sudo -s ``` Then install Buildah by running: ```console # dnf -y install buildah ``` After installing Buildah check to see that there are no images installed. The `buildah images` command will list all the images: ```console # buildah images ``` We can also see that there are also no containers by running: ```console # buildah containers ``` ## Examples The two examples that will be shown are relatively simple, but they illustrate how a command or a number of commands can be setup in a primary image such that they will be added to a secondary container image that is created from it. This is extremely useful if you need to setup an environment where your containers have 75% of the same content, but need a few individual tweaks. This can be helpful in setting up an environment for maven or java development containers for instance. In this way you can create a single Dockerfile with all the common setup steps as ONBUILD commands and then really minimize the buildah commands or instructions in a second Dockerfile that would be necessary to complete the creation of the container image. NOTE: In the examples below the option `--format=docker` is used in several places. If you wanted to omit that, you could define the `BUILDAH_FORMAT` environment variable and set it to 'docker'. On Fedora that command would be `export BUILDAH_FORMAT=docker`. ## ONBUILD in a Dockerfile - Example 1 The first example was provided by Chris Collins (GitHub @clcollins), the idea is a file `/bar` will be created in the derived container images only, and not in our original image. First create two Dockerfiles: ```Dockerfile $ cat << EOF > Dockerfile FROM registry.fedoraproject.org/fedora:latest RUN touch /foo ONBUILD RUN touch /bar EOF $ cat << EOF > Dockerfile-2 FROM onbuild-image RUN touch /baz EOF ``` Now to create the first container image and verify that ONBUILD has been set: ```console # buildah build --format=docker -f Dockerfile -t onbuild-image . # buildah inspect --format '{{.Docker.Config.OnBuild}}' onbuild-image [RUN touch /bar] ``` The second container image is now created and the `/bar` file will be created within it: ```console # buildah build --format=docker -f Dockerfile-2 -t result-image . STEP 1: FROM onbuild-image STEP 2: RUN touch /bar # Note /bar is created here based on the ONBUILD in the base image STEP 3: RUN touch /baz COMMIT result-image {output edited for brevity} $ container=$(sudo buildah from result-image:latest) # buildah run $container ls /bar /foo /baz /bar /baz /foo ``` ## ONBUILD via `buildah config` - Example 1 Instead of using a Dockerfile to create the onbuild-image, Buildah allows you to build an image and configure it directly with the same commands that can be found in a Dockerfile. This allows for easy on the fly manipulation of your image. Let's look at the previous example without the use of a Dockerfile when building the primary container image. First a Fedora container will be created with `buildah from`, then the `/foo` file will be added with `buildah run`. The `buildah config` command will configure ONBUILD to add `/bar` when a container image is created from the primary image, and finally the image will be saved with `buildah commit`. ```console # buildah from --format=docker --name onbuild-container registry.fedoraproject.org/fedora:latest # buildah run onbuild-container touch /foo # buildah config --onbuild="RUN touch /bar" onbuild-container # buildah commit --format=docker onbuild-container onbuild-image {output edited for brevity} # buildah inspect --format '{{.Docker.Config.OnBuild}}' onbuild-image [RUN touch /bar] ``` The onbuild-image has been created, so now create a container from it using the same commands as the first example using the second Dockerfile: ```console # buildah build --format=docker -f Dockerfile-2 -t result-image . STEP 1: FROM onbuild-image STEP 2: RUN touch /bar # Note /bar is created here based on the ONBUILD in the base image STEP 3: RUN touch /baz COMMIT result-image {output edited for brevity} $ container=$(buildah from result-image) # buildah run $container ls /bar /foo /baz /bar /baz /foo ``` Or for bonus points, piece the secondary container image together with Buildah commands directly: ```console # buildah from --format=docker --name result-container onbuild-image result-container # buildah run result-container touch /baz # buildah run result-container ls /bar /foo /baz /bar /baz /foo ``` ## ONBUILD via `buildah config` - Example 2 For this example the ONBUILD instructions in the primary container image will be used to copy a shell script and then run it in the secondary container image. For the script, we'll make use of the shell script from the [Introduction Tutorial](01-intro.md). First create a file in the local directory called `runecho.sh` containing the following: ```console #!/usr/bin/env bash for i in `seq 0 9`; do echo "This is a new container from ipbabble [" $i "]" done ``` Change the permissions on the file so that it can be run: ```console $ chmod +x runecho.sh ``` Now create a second primary container image. This image has multiple ONBUILD instructions, the first ONBUILD instruction copies the file into the image and a second ONBUILD instruction to then run it. We're going to do this example using only Buildah commands. A Dockerfile could be translated easily and used from these commands, or these commands could be saved to a script directly. ```console # buildah from --format=docker --name onbuild-container-2 fedora:latest onbuild-container-2 # buildah config --onbuild="COPY ./runecho.sh /usr/bin/runecho.sh" onbuild-container-2 # buildah config --onbuild="RUN /usr/bin/runecho.sh" onbuild-container-2 # buildah commit --format=docker onbuild-container-2 onbuild-image-2 {output edited for brevity} # buildah inspect --format '{{.Docker.Config.OnBuild}}' onbuild-image-2 [COPY ./runecho.sh /usr/bin/runecho.sh RUN /usr/bin/runecho.sh] ``` Now the secondary container can be created from the second primary container image onbuild-image-2. The runecho.sh script will be copied to the container's /usr/bin directory and then run from there when the secondary container is created. ```console # buildah from --format=docker --name result-container-2 onbuild-image-2 STEP 1: COPY ./runecho.sh /usr/bin/runecho.sh STEP 2: RUN /usr/bin/runecho.sh This is a new container pull ipbabble [ 1 ] This is a new container pull ipbabble [ 2 ] This is a new container pull ipbabble [ 3 ] This is a new container pull ipbabble [ 4 ] This is a new container pull ipbabble [ 5 ] This is a new container pull ipbabble [ 6 ] This is a new container pull ipbabble [ 7 ] This is a new container pull ipbabble [ 8 ] This is a new container pull ipbabble [ 9 ] result-container-2 ``` As result-container-2 has a copy of the script stored in its /usr/bin it can be run at anytime. ```console # buildah run result-container-2 /usr/bin/runecho.sh This is a new container pull ipbabble [ 1 ] This is a new container pull ipbabble [ 2 ] This is a new container pull ipbabble [ 3 ] This is a new container pull ipbabble [ 4 ] This is a new container pull ipbabble [ 5 ] This is a new container pull ipbabble [ 6 ] This is a new container pull ipbabble [ 7 ] This is a new container pull ipbabble [ 8 ] This is a new container pull ipbabble [ 9 ] ``` Again these aren't the most extensive examples, but they both illustrate how a primary image can be setup and then a secondary container image can then be created with just a few steps. This way the steps that are set up with the ONBUILD instructions don't have to be typed in each and every time that you need to setup your container. ## Congratulations Well done. You have learned about Buildah's ONBUILD functionality using this short tutorial. Hopefully you followed along with the examples and found them to be sufficient. Be sure to look at Buildah's man pages to see the other useful commands you can use. Have fun playing. If you have any suggestions or issues please post them at the [Buildah Issues page](https://github.com/containers/buildah/issues). For more information on Buildah and how you might contribute please visit the [Buildah home page on GitHub](https://github.com/containers/buildah). [Podman's site]: https://podman.io/ [image specification]: https://github.com/opencontainers/image-spec/blob/main/spec.md [Introduction Tutorial]: 01-intro.md [Open Container Initiative]: https://www.opencontainers.org/ ================================================ FILE: docs/tutorials/04-include-in-your-build-tool.md ================================================ ![buildah logo](../../logos/buildah-logo_large.png) # Buildah Tutorial 4 ## Include Buildah in your build tool The purpose of this tutorial is to demonstrate how to include Buildah as a library in your build tool. You can take advantage of all features provided by Buildah, like using Dockerfiles and building using rootless mode. In this tutorial I'll show you how to create a simple CLI tool that creates an image containing NodeJS and a JS main file. ## Bootstrap the project and install the dependencies Bootstrap the installation of development dependencies of Buildah by following the [Building from scratch](https://github.com/containers/buildah/blob/main/install.md#building-from-scratch) instructions and in particular creating a directory for the Buildah project by completing the instructions in the [Installation from GitHub](https://github.com/containers/buildah/blob/main/install.md#installation-from-github) section of that page. Now let's bootstrap our project. Assuming you are in the directory of the project, run the following to initialize the go modules: ```shell go mod init ``` Next, we should import Buildah as a dependency. However, make sure that you have the following developer packages installed: ```shell dnf install btrfs-progs-devel gpgme-devel passt ``` Depending on your Linux distribution, the names of the packages can be slightly different. For instance, on OpenSUSE it would be ```shell zypper install libbtrfs-devel libgpgme-devel passt ``` On Debian and Ubuntu, it would be ```shell apt install libbtrfs-dev libgpgme-dev passt ``` Now import Buildah as a dependency: ```shell go get github.com/containers/buildah ``` ## Build the image Now you can develop your application. To access to the build features of Buildah, you need to instantiate a `buildah.Builder`. This struct has methods to configure the build, define the build steps and run it. To instantiate a `Builder`, you need a `storage.Store` (the Store interface found in [store.go](https://github.com/containers/storage/blob/main/store.go)) from [`github.com/containers/storage`](https://github.com/containers/storage), where the intermediate and result images will be stored: ```go buildStoreOptions, err := storage.DefaultStoreOptions() buildStore, err := storage.GetStore(buildStoreOptions) ``` Define the builder options: ```go builderOpts := buildah.BuilderOptions{ FromImage: "docker.io/library/node:12-alpine", // base image } ``` Now instantiate the `Builder`: ```go builder, err := buildah.NewBuilder(context.TODO(), buildStore, builderOpts) ``` Let's add our JS file (assuming it is in your local directory with name `script.js`): ```go err = builder.Add("/home/node/", false, buildah.AddAndCopyOptions{}, "script.js") ``` And configure the command to run: ```go builder.SetCmd([]string{"node", "/home/node/script.js"}) ``` Before completing the build, create the image reference: ```go imageRef, err := is.Transport.ParseStoreReference(buildStore, "docker.io/myusername/my-image") ``` Now you can commit the container to create the image: ```go imageId, _, _, err := builder.Commit(context.TODO(), imageRef, buildah.CommitOptions{}) ``` ## Supplying defaults for Run() If you need to run a command as part of the build, you'll have to dig up a couple of defaults that aren't picked up automatically: ```go conf, err := config.Default() capabilitiesForRoot, err := conf.Capabilities("root", nil, nil) isolation, err := parse.IsolationOption("") ``` ## Rootless mode To enable rootless mode, import `github.com/containers/storage/pkg/unshare` and add this code at the beginning of your main() method: ```go if buildah.InitReexec() { return } unshare.MaybeReexecUsingUserNamespace(false) ``` This code ensures that your application is re-executed in a user namespace where it has root privileges. The reexec mechanism which `buildah.InitReexec()` sets up is also used for various other internal functions, so it should always be called. ## Complete code ```go package main import ( "context" "fmt" "github.com/containers/buildah" "github.com/containers/buildah/pkg/parse" "github.com/containers/common/pkg/config" is "github.com/containers/image/v5/storage" "github.com/containers/storage" "github.com/containers/storage/pkg/unshare" ) func main() { if buildah.InitReexec() { return } unshare.MaybeReexecUsingUserNamespace(false) buildStoreOptions, err := storage.DefaultStoreOptions() if err != nil { panic(err) } conf, err := config.Default() if err != nil { panic(err) } capabilitiesForRoot, err := conf.Capabilities("root", nil, nil) if err != nil { panic(err) } buildStore, err := storage.GetStore(buildStoreOptions) if err != nil { panic(err) } defer buildStore.Shutdown(false) builderOpts := buildah.BuilderOptions{ FromImage: "docker.io/library/node:12-alpine", Capabilities: capabilitiesForRoot, } builder, err := buildah.NewBuilder(context.TODO(), buildStore, builderOpts) if err != nil { panic(err) } defer builder.Delete() err = builder.Add("/home/node/", false, buildah.AddAndCopyOptions{}, "script.js") if err != nil { panic(err) } isolation, err := parse.IsolationOption("") if err != nil { panic(err) } err = builder.Run([]string{"sh", "-c", "date > /home/node/build-date.txt"}, buildah.RunOptions{Isolation: isolation, Terminal: buildah.WithoutTerminal}) if err != nil { panic(err) } builder.SetCmd([]string{"node", "/home/node/script.js"}) imageRef, err := is.Transport.ParseStoreReference(buildStore, "docker.io/myusername/my-image") if err != nil { panic(err) } imageId, _, _, err := builder.Commit(context.TODO(), imageRef, buildah.CommitOptions{}) if err != nil { panic(err) } fmt.Printf("Image built! %s\n", imageId) } ``` ================================================ FILE: docs/tutorials/05-openshift-rootless-build.md ================================================ ![buildah logo](https://cdn.rawgit.com/containers/buildah/main/logos/buildah-logo_large.png) # Buildah Tutorial 5 ## Using Buildah to build images in a rootless OpenShift container This tutorial will walk you through setting up a container in OpenShift for building images. The instructions have been tested on OpenShift 4.16 with Buildah 1.36.0. Note that we can use overlay for copy-on-write storage if we ensure that the storage used in the builder container ends up in an emptyDir volume. ### Prepare a new namespace Create a new project in OpenShift called `image-build`. Make the registry URL available to the following steps. *Note that you need to change this so it matches your OpenShift installation.* ```console $ export REGISTRY_URL=default-route-openshift-image-registry.apps.whatever.com ``` Login to OpenShift and its registry: ```console $ oc login -n image-build Username: ... Password: ... Login successful. You have access to N projects, the list has been suppressed. You can list all projects with 'oc projects' Using project "image-build". $ oc whoami -t | buildah login --tls-verify=false -u $(id -u -n) --password-stdin $REGISTRY_URL Login Succeeded! ``` ### Make builder image This is the image that will host the building. It uses the Buildah stable official image, which is based on Fedora 35. The image starts a python web server. This allows us to interact with the container via the OpenShift console terminal, demonstrating that building an image works. First create an ImageStream to hold the image: ```console $ oc create -f - < Containerfile-buildah < /etc/subuid \ && echo build:10000:65536 > /etc/subgid # Use chroot since the default runc does not work when running rootless RUN echo "export BUILDAH_ISOLATION=chroot" >> /home/build/.bashrc # Use overlay RUN mkdir -p /home/build/.config/containers \ && (echo '[storage]';echo 'driver = "overlay"') > /home/build/.config/containers/storage.conf USER build WORKDIR /home/build # Just keep the container running, allowing "oc rsh" access CMD ["python3", "-m", "http.server"] EOF $ buildah build --layers -t $REGISTRY_URL/image-build/buildah -f Containerfile-buildah STEP 1/7: FROM quay.io/buildah/stable:v1.36.0 STEP 2/7: RUN touch /etc/subgid /etc/subuid && chmod g=u /etc/subgid /etc/subuid /etc/passwd && echo build:10000:65536 > /etc/subuid && echo build:10000:65536 > /etc/subgid --> e6ee6fcc2d94 STEP 3/7: RUN echo "export BUILDAH_ISOLATION=chroot" >> /home/build/.bashrc --> 4327c8743bcc STEP 4/7: RUN mkdir -p /home/build/.config/containers && (echo '[storage]';echo 'driver = "overlay"') > /home/build/.config/containers/storage.conf --> c405cbcd1132 STEP 5/7: USER build --> 2c97c1162233 STEP 6/7: WORKDIR /home/build --> 78d6367c298f STEP 7/7: CMD ["python3", "-m", "http.server"] COMMIT default-route-openshift-image-registry.apps-crc.testing/image-build/buildah --> a872961c3fa9 $ buildah push --tls-verify=false $REGISTRY_URL/image-build/buildah Getting image source signatures ... Storing signatures ``` ### Create Service Account for building images Create a service account which is solely used for image building. ```console $ oc create -f - < test-script.sh < Containerfile.test < a73b603bca4d STEP 3/5: RUN /test-script.sh "Hello world" Args Hello world total 8 dr-xr-xr-x. 2 root root 6 Jan 24 2024 afs lrwxrwxrwx. 1 root root 7 Jan 24 2024 bin -> usr/bin dr-xr-xr-x. 2 root root 6 Jan 24 2024 boot drwxr-xr-x. 5 nobody nobody 380 Aug 5 18:32 dev drwxr-xr-x. 1 root root 41 Aug 5 18:35 etc drwxr-xr-x. 2 root root 6 Jan 24 2024 home lrwxrwxrwx. 1 root root 7 Jan 24 2024 lib -> usr/lib lrwxrwxrwx. 1 root root 9 Jan 24 2024 lib64 -> usr/lib64 drwxr-xr-x. 2 root root 6 Jan 24 2024 media drwxr-xr-x. 2 root root 6 Jan 24 2024 mnt drwxr-xr-x. 2 root root 6 Jan 24 2024 opt drwxr-xr-x. 2 root root 6 Aug 5 18:34 output dr-xr-xr-x. 465 nobody nobody 0 Aug 5 18:32 proc dr-xr-x---. 2 root root 91 Aug 5 05:47 root drwxr-xr-x. 1 root root 42 Aug 5 18:35 run lrwxrwxrwx. 1 root root 8 Jan 24 2024 sbin -> usr/sbin drwxr-xr-x. 2 root root 6 Jan 24 2024 srv dr-xr-xr-x. 13 nobody nobody 0 Aug 5 17:26 sys -rwxr-xr-x. 1 root root 34 Aug 5 18:33 test-script.sh drwxrwxrwt. 2 root root 6 Jan 24 2024 tmp drwxr-xr-x. 12 root root 144 Aug 5 05:47 usr drwxr-xr-x. 18 root root 4096 Aug 5 05:47 var --> 3a1192fe0ecf STEP 4/5: RUN dnf update -y | tee /output/update-output.txt Fedora 40 - x86_64 9.4 MB/s | 20 MB 00:02 Fedora 40 openh264 (From Cisco) - x86_64 3.5 kB/s | 1.4 kB 00:00 Fedora 40 - x86_64 - Updates 9.9 MB/s | 9.1 MB 00:00 Dependencies resolved. Nothing to do. Complete! --> 5026f20f0ad1 STEP 5/5: RUN dnf install -y gcc Last metadata expiration check: 0:00:06 ago on Mon Aug 5 18:35:25 2024. Dependencies resolved. ================================================================================ Package Arch Version Repository Size ================================================================================ Installing: gcc x86_64 14.2.1-1.fc40 updates 37 M Installing dependencies: binutils x86_64 2.41-37.fc40 updates 6.2 M binutils-gold x86_64 2.41-37.fc40 updates 781 k cpp x86_64 14.2.1-1.fc40 updates 12 M elfutils-debuginfod-client x86_64 0.191-4.fc40 fedora 38 k gc x86_64 8.2.2-6.fc40 fedora 110 k glibc-devel x86_64 2.39-17.fc40 updates 114 k glibc-headers-x86 noarch 2.39-17.fc40 updates 608 k guile30 x86_64 3.0.7-12.fc40 fedora 8.1 M jansson x86_64 2.13.1-9.fc40 fedora 44 k kernel-headers x86_64 6.9.4-200.fc40 updates 1.6 M libmpc x86_64 1.3.1-5.fc40 fedora 71 k libpkgconf x86_64 2.1.1-1.fc40 updates 38 k libxcrypt-devel x86_64 4.4.36-5.fc40 fedora 29 k make x86_64 1:4.4.1-6.fc40 fedora 588 k pkgconf x86_64 2.1.1-1.fc40 updates 44 k pkgconf-m4 noarch 2.1.1-1.fc40 updates 14 k pkgconf-pkg-config x86_64 2.1.1-1.fc40 updates 9.9 k Transaction Summary ================================================================================ Install 18 Packages Total download size: 67 M Installed size: 230 M Downloading Packages: (1/18): elfutils-debuginfod-client-0.191-4.fc40 57 kB/s | 38 kB 00:00 (2/18): gc-8.2.2-6.fc40.x86_64.rpm 146 kB/s | 110 kB 00:00 (3/18): jansson-2.13.1-9.fc40.x86_64.rpm 254 kB/s | 44 kB 00:00 (4/18): libmpc-1.3.1-5.fc40.x86_64.rpm 420 kB/s | 71 kB 00:00 (5/18): libxcrypt-devel-4.4.36-5.fc40.x86_64.rp 336 kB/s | 29 kB 00:00 (6/18): make-4.4.1-6.fc40.x86_64.rpm 739 kB/s | 588 kB 00:00 (7/18): binutils-2.41-37.fc40.x86_64.rpm 6.4 MB/s | 6.2 MB 00:00 (8/18): cpp-14.2.1-1.fc40.x86_64.rpm 28 MB/s | 12 MB 00:00 (9/18): binutils-gold-2.41-37.fc40.x86_64.rpm 732 kB/s | 781 kB 00:01 (10/18): glibc-devel-2.39-17.fc40.x86_64.rpm 982 kB/s | 114 kB 00:00 (11/18): glibc-headers-x86-2.39-17.fc40.noarch. 3.1 MB/s | 608 kB 00:00 (12/18): kernel-headers-6.9.4-200.fc40.x86_64.r 4.1 MB/s | 1.6 MB 00:00 (13/18): libpkgconf-2.1.1-1.fc40.x86_64.rpm 376 kB/s | 38 kB 00:00 (14/18): gcc-14.2.1-1.fc40.x86_64.rpm 27 MB/s | 37 MB 00:01 (15/18): pkgconf-2.1.1-1.fc40.x86_64.rpm 403 kB/s | 44 kB 00:00 (16/18): pkgconf-m4-2.1.1-1.fc40.noarch.rpm 180 kB/s | 14 kB 00:00 (17/18): pkgconf-pkg-config-2.1.1-1.fc40.x86_64 120 kB/s | 9.9 kB 00:00 (18/18): guile30-3.0.7-12.fc40.x86_64.rpm 685 kB/s | 8.1 MB 00:12 -------------------------------------------------------------------------------- Total 5.4 MB/s | 67 MB 00:12 Running transaction check Transaction check succeeded. Running transaction test Transaction test succeeded. Running transaction Preparing : 1/1 Installing : libmpc-1.3.1-5.fc40.x86_64 1/18 Installing : jansson-2.13.1-9.fc40.x86_64 2/18 Installing : cpp-14.2.1-1.fc40.x86_64 3/18 Installing : pkgconf-m4-2.1.1-1.fc40.noarch 4/18 Installing : libpkgconf-2.1.1-1.fc40.x86_64 5/18 Installing : pkgconf-2.1.1-1.fc40.x86_64 6/18 Installing : pkgconf-pkg-config-2.1.1-1.fc40.x86_64 7/18 Installing : kernel-headers-6.9.4-200.fc40.x86_64 8/18 Installing : glibc-headers-x86-2.39-17.fc40.noarch 9/18 Installing : glibc-devel-2.39-17.fc40.x86_64 10/18 Installing : libxcrypt-devel-4.4.36-5.fc40.x86_64 11/18 Installing : gc-8.2.2-6.fc40.x86_64 12/18 Installing : guile30-3.0.7-12.fc40.x86_64 13/18 Installing : make-1:4.4.1-6.fc40.x86_64 14/18 Installing : elfutils-debuginfod-client-0.191-4.fc40.x86_64 15/18 Installing : binutils-gold-2.41-37.fc40.x86_64 16/18 Running scriptlet: binutils-gold-2.41-37.fc40.x86_64 16/18 Installing : binutils-2.41-37.fc40.x86_64 17/18 Running scriptlet: binutils-2.41-37.fc40.x86_64 17/18 Installing : gcc-14.2.1-1.fc40.x86_64 18/18 Running scriptlet: gcc-14.2.1-1.fc40.x86_64 18/18 Installed: binutils-2.41-37.fc40.x86_64 binutils-gold-2.41-37.fc40.x86_64 cpp-14.2.1-1.fc40.x86_64 elfutils-debuginfod-client-0.191-4.fc40.x86_64 gc-8.2.2-6.fc40.x86_64 gcc-14.2.1-1.fc40.x86_64 glibc-devel-2.39-17.fc40.x86_64 glibc-headers-x86-2.39-17.fc40.noarch guile30-3.0.7-12.fc40.x86_64 jansson-2.13.1-9.fc40.x86_64 kernel-headers-6.9.4-200.fc40.x86_64 libmpc-1.3.1-5.fc40.x86_64 libpkgconf-2.1.1-1.fc40.x86_64 libxcrypt-devel-4.4.36-5.fc40.x86_64 make-1:4.4.1-6.fc40.x86_64 pkgconf-2.1.1-1.fc40.x86_64 pkgconf-m4-2.1.1-1.fc40.noarch pkgconf-pkg-config-2.1.1-1.fc40.x86_64 Complete! COMMIT myimage --> 087a83c62eb7 Successfully tagged localhost/myimage:latest 087a83c62eb73def45ef4542460c18b897a8d018299f15f69a2a6b678d56fcec sh-5.0$ buildah images REPOSITORY TAG IMAGE ID CREATED SIZE localhost/myimage latest 087a83c62eb7 45 seconds ago 533 MB registry.fedoraproject.org/fedora 40 b8638217aa4e 13 hours ago 233 MB sh-5.0$ ls -l output/ total 4 -rw-r--r--. 1 build build 288 Aug 5 18:35 update-output.txt ``` ================================================ FILE: docs/tutorials/README.md ================================================ ![buildah logo](../../logos/buildah-logo_large.png) # Buildah Tutorials ## Links to a number of useful tutorials for the Buildah project. **[Introduction Tutorial](01-intro.md)** Learn how to build container images compliant with the [Open Container Initiative](https://www.opencontainers.org/) (OCI) [image specification](https://github.com/opencontainers/image-spec) using Buildah. This tutorial shows how to [Configure and Setup](01-intro.md#configure-and-install-buildah) Buildah, how to [build containers using a Dockerfile](01-intro.md#using-dockerfiles-with-buildah) and how to [build containers from scratch](01-intro.md#building-a-container-from-scratch). **[Buildah and Registries Tutorial](02-registries-repositories.md)** Learn how Buildah can be used to move OCI compliant images in and out of private or public registries. **[Buildah ONBUILD Tutorial](03-on-build.md)** Learn how Buildah can use the ONBUILD instruction in either a Dockerfile or via the `buildah config --onbuild` command to configure an image to run those instructions when the container is created. In this manner you can front load setup of the container inside the image and minimalize the steps needed to create one or more containers that share a number of initial settings, but need a few differentiators between each. **[Include Buildah in your build tool](04-include-in-your-build-tool.md)** Learn how to include Buildah as a library in your build tool. **[Rootless OpenShift container](05-openshift-rootless-build.md)** Learn how to build an image from a rootless OpenShift container. ================================================ FILE: examples/all-the-things.sh ================================================ #!/usr/bin/env bash set -e set -x read export PATH=`pwd`:$PATH systemctl restart ocid read : " Check if we have some images to work with." ocic image list read : " Create a working container, and capture its name " read echo '[container1=`buildah from ${1:-ubuntu}`]' container1=`buildah from ${1:-ubuntu}` read : " Mount that working container, and capture the mountpoint " read echo '[mountpoint1=`buildah mount $container1`]' mountpoint1=`buildah mount $container1` read : " Add a file to the container " read echo '[echo yay > $mountpoint1/file-in-root]' echo yay > $mountpoint1/file-in-root read : " Produce an image from the container " read buildah commit "$container1" ${2:-first-new-image} read : " Verify that our new image is there " read ocic image list read : " Unmount our working container and delete it " read buildah umount "$container1" buildah rm "$container1" read : " Now try it with ocid not running! " read systemctl stop ocid read : " You know what? Go ahead and use that image we just created, and capture its name " read echo '[container2=`buildah from ${2:-first-new-image}`]' container2=`buildah from ${2:-first-new-image}` read : " Mount that new working container, and capture the mountpoint " read echo '[mountpoint2=`buildah mount $container2`]' mountpoint2=`buildah mount $container2` read : " That file we added to the image is there, right? " read cat $mountpoint2/file-in-root read : " Add a file to the new container" read echo '[echo yay > $mountpoint2/another-file-in-root]' echo yay > $mountpoint2/another-file-in-root read : " Produce an image from the new container" read buildah commit "$container2" ${3:-second-new-image} read : " Unmount our new working container and delete it " read buildah umount "$container2" buildah rm "$container2" read : " Verify that our new new image is there" read systemctl start ocid ocic image list read : " Clean up, because I ran this like fifty times while testing " read ocic image remove --id=${2:-first-new-image} ocic image remove --id=${3:-second-new-image} ================================================ FILE: examples/copy.sh ================================================ #!/usr/bin/env bash set -e set -x : " Build a temporary directory; make sure ocid is running." export PATH=`pwd`:$PATH d=`mktemp -d` trap 'cd /;rm -fr "$d"' EXIT cd "$d" systemctl restart ocid read : " Check if we have some images to work with." read ocic image list read : " Create a working container, and capture its name " read echo '[container1=`buildah from ${1:-alpine}`]' container1=`buildah from ${1:-alpine}` read : " Mount that working container, and capture the mountpoint " read echo '[mountpoint1=`buildah mount $container1`]' mountpoint1=`buildah mount $container1` read : " List random files in the container " read echo '[find $mountpoint1 -name "random*"]' find $mountpoint1 -name "random*" read : " Ensure the default destination for copying files is / " read echo '[buildah config $container1 --workingdir /]' buildah config $container1 --workingdir / read : " Add a file to the container " read echo '[dd if=/dev/urandom of=random1 bs=512 count=1]' echo '[buildah copy $container1 random1]' dd if=/dev/urandom of=random1 bs=512 count=1 buildah copy $container1 random1 read : " Change the default destination for copying files " read echo '[buildah config $container1 --workingdir /tmp]' buildah config $container1 --workingdir /tmp read : " Add another new file to the container " read echo '[dd if=/dev/urandom of=random2 bs=512 count=1]' echo '[buildah copy $container1 random2]' dd if=/dev/urandom of=random2 bs=512 count=1 buildah copy $container1 random2 read : " Copy a subdirectory with some files in it " read echo '[mkdir -p randomsubdir]' echo '[dd if=/dev/urandom of=randomsubdir/random3 bs=512 count=1]' echo '[dd if=/dev/urandom of=randomsubdir/random4 bs=512 count=1]' echo '[buildah copy $container1 randomsubdir]' mkdir -p randomsubdir dd if=/dev/urandom of=randomsubdir/random3 bs=512 count=1 dd if=/dev/urandom of=randomsubdir/random4 bs=512 count=1 buildah copy $container1 randomsubdir read : " List some of the container's contents " read echo '[find $mountpoint1 -name "random*"]' find $mountpoint1 -name "random*" read : " Download a tarball " read echo '[wget -c https://releases.pagure.org/tmpwatch/tmpwatch-2.9.17.tar.bz2]' wget -c https://releases.pagure.org/tmpwatch/tmpwatch-2.9.17.tar.bz2 read : " Copy that tarball to the container " read echo '[mkdir -p $mountpoint1/tmpwatch]' echo '[buildah copy $container1 --dest /tmpwatch tmpwatch-2.9.17.tar.bz2]' mkdir -p $mountpoint1/tmpwatch buildah copy $container1 --dest /tmpwatch tmpwatch-2.9.17.tar.bz2 read : " Download another tarball to the container " read echo '[buildah copy $container1 --dest /tmpwatch https://releases.pagure.org/newt/newt-0.52.19.tar.gz]' buildah copy $container1 --dest /tmpwatch https://releases.pagure.org/newt/newt-0.52.19.tar.gz read : " List the contents of the target directory " read echo '[find $mountpoint1/tmpwatch]' find $mountpoint1/tmpwatch read : " Now 'add' the downloaded tarball to the container " read echo '[buildah add $container1 --dest /tmpwatch tmpwatch-2.9.17.tar.bz2]' buildah add $container1 --dest /tmpwatch tmpwatch-2.9.17.tar.bz2 read : " List the contents of the target directory again " read echo '[find $mountpoint1/tmpwatch]' find $mountpoint1/tmpwatch read : " Clean up, because I ran this like fifty times while testing " read echo '[buildah delete $container1]' buildah rm $container1 ================================================ FILE: examples/lighttpd.sh ================================================ #!/usr/bin/env bash set -x ctr1=$(buildah from "${1:-fedora}") ## Get all updates and install our minimal httpd server buildah run "$ctr1" -- dnf update -y buildah run "$ctr1" -- dnf install -y lighttpd buildah run "$ctr1" -- mkdir /run/lighttpd ## Include some buildtime annotations buildah config --annotation "com.example.build.host=$(uname -n)" "$ctr1" ## Run our server and expose the port buildah config --cmd "/usr/sbin/lighttpd -D -f /etc/lighttpd/lighttpd.conf" "$ctr1" buildah config --port 80 "$ctr1" ## Commit this container to an image name buildah commit "$ctr1" "${2:-$USER/lighttpd}" ================================================ FILE: go.mod ================================================ module github.com/containers/buildah // Warning: Ensure the "go" and "toolchain" versions match exactly to prevent unwanted auto-updates go 1.25.5 require ( github.com/containerd/platforms v1.0.0-rc.2 github.com/containers/luksy v0.0.0-20251208191447-ca096313c38f github.com/containers/ocicrypt v1.2.1 github.com/cyphar/filepath-securejoin v0.6.1 github.com/docker/distribution v2.8.3+incompatible github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 github.com/fsouza/go-dockerclient v1.13.0 github.com/hashicorp/go-multierror v1.1.1 github.com/mattn/go-shellwords v1.0.12 github.com/moby/buildkit v0.28.0 github.com/moby/moby/client v0.3.0 github.com/moby/sys/capability v0.4.0 github.com/moby/sys/userns v0.1.0 github.com/opencontainers/cgroups v0.0.6 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.1 github.com/opencontainers/runc v1.4.1 github.com/opencontainers/runtime-spec v1.3.0 github.com/opencontainers/runtime-tools v0.9.1-0.20251205004911-5e639034dcdc github.com/opencontainers/selinux v1.13.1 github.com/openshift/imagebuilder v1.2.20 github.com/seccomp/libseccomp-golang v0.11.1 github.com/sirupsen/logrus v1.9.4 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 go.etcd.io/bbolt v1.4.3 go.podman.io/common v0.67.1-0.20260316162257-e70c309aabae go.podman.io/image/v5 v5.39.2-0.20260316162257-e70c309aabae go.podman.io/storage v1.62.1-0.20260316162257-e70c309aabae golang.org/x/crypto v0.49.0 golang.org/x/sync v0.20.0 golang.org/x/sys v0.42.0 golang.org/x/term v0.41.0 google.golang.org/grpc v1.79.3 google.golang.org/protobuf v1.36.11 tags.cncf.io/container-device-interface v1.1.0 ) require ( cyphar.com/go-pathrs v0.2.4 // indirect dario.cat/mergo v1.0.2 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/BurntSushi/toml v1.6.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/VividCortex/ewma v1.2.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect github.com/coreos/go-systemd/v22 v22.7.0 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/disiqueira/gotree/v3 v3.0.2 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v28.5.2+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.5 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-containerregistry v0.20.7 // indirect github.com/google/go-intervals v0.0.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/copier v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-runewidth v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.34 // indirect github.com/miekg/pkcs11 v1.1.1 // indirect github.com/mistifyio/go-zfs/v4 v4.0.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.2.0 // indirect github.com/moby/moby/api v1.54.0 // indirect github.com/moby/moby/v2 v2.0.0-beta.6 // indirect github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/sys/mountinfo v0.7.2 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/proglottis/gpgme v0.1.6 // indirect github.com/secure-systems-lab/go-securesystemslib v0.10.0 // indirect github.com/sigstore/fulcio v1.8.5 // indirect github.com/sigstore/protobuf-specs v0.5.0 // indirect github.com/sigstore/sigstore v1.10.4 // indirect github.com/smallstep/pkcs7 v0.1.1 // indirect github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect github.com/sylabs/sif/v2 v2.22.0 // indirect github.com/tchap/go-patricia/v2 v2.3.3 // indirect github.com/ulikunitz/xz v0.5.15 // indirect github.com/vbatts/tar-split v0.12.2 // indirect github.com/vbauerster/mpb/v8 v8.12.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.40.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/mod v0.33.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/text v0.35.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog v1.0.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect tags.cncf.io/container-device-interface/specs-go v1.1.0 // indirect ) ================================================ FILE: go.sum ================================================ cyphar.com/go-pathrs v0.2.4 h1:iD/mge36swa1UFKdINkr1Frkpp6wZsy3YYEildj9cLY= cyphar.com/go-pathrs v0.2.4/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6 h1:5L8Mj9Co9sJVgW3TpYk2gxGJnDjsYuboNTcRmbtGKGs= github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6/go.mod h1:3HgLJ9d18kXMLQlJvIY3+FszZYMxCz8WfE2MQ7hDY0w= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4= github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA= github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY= github.com/containers/luksy v0.0.0-20251208191447-ca096313c38f h1:DWGVgZ9ToKBOMiBMv//ilrKPKEy4tQG752XtTJXYUQ0= github.com/containers/luksy v0.0.0-20251208191447-ca096313c38f/go.mod h1:bynzkJ2rWsqgt0s31fuAdkHZBB3zrPtD6pV2zHt/qRk= github.com/containers/ocicrypt v1.2.1 h1:0qIOTT9DoYwcKmxSt8QJt+VzMY18onl9jUXsxpVhSmM= github.com/containers/ocicrypt v1.2.1/go.mod h1:aD0AAqfMp0MtwqWgHM1bUwe1anx0VazI108CRrSKINQ= github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q= github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/disiqueira/gotree/v3 v3.0.2 h1:ik5iuLQQoufZBNPY518dXhiO5056hyNBIK9lWhkNRq8= github.com/disiqueira/gotree/v3 v3.0.2/go.mod h1:ZuyjE4+mUQZlbpkI24AmruZKhg3VHEgPLDY8Qk+uUu8= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/cli v29.3.0+incompatible h1:z3iWveU7h19Pqx7alZES8j+IeFQZ1lhTwb2F+V9SVvk= github.com/docker/cli v29.3.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY= github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsouza/go-dockerclient v1.13.0 h1:i6VWBKrFprrqDDK6rEYw0Zp5KsN+cVlqZ/ODaIX2erM= github.com/fsouza/go-dockerclient v1.13.0/go.mod h1:slZeNd4OpH+YfOrTc4+fkjtq8fwKymehRReLvUVFfpI= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I= github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM= github.com/google/go-intervals v0.0.2 h1:FGrVEiUnTRKR8yE04qzXYaJMtnIYqobR5QbblK3ixcM= github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mistifyio/go-zfs/v4 v4.0.0 h1:sU0+5dX45tdDK5xNZ3HBi95nxUc48FS92qbIZEvpAg4= github.com/mistifyio/go-zfs/v4 v4.0.0/go.mod h1:weotFtXTHvBwhr9Mv96KYnDkTPBOHFUbm9cBmQpesL0= github.com/moby/buildkit v0.28.0 h1:rKulfRRSduHJPNpLTk481fHElqN9tps0VUx8YV/5zsA= github.com/moby/buildkit v0.28.0/go.mod h1:RCuOcj/bVsCriBG8NeFzRxjiCFQKnKP7KOVlNTS18t4= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= github.com/moby/moby/api v1.54.0 h1:7kbUgyiKcoBhm0UrWbdrMs7RX8dnwzURKVbZGy2GnL0= github.com/moby/moby/api v1.54.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= github.com/moby/moby/client v0.3.0 h1:UUGL5okry+Aomj3WhGt9Aigl3ZOxZGqR7XPo+RLPlKs= github.com/moby/moby/client v0.3.0/go.mod h1:HJgFbJRvogDQjbM8fqc1MCEm4mIAGMLjXbgwoZp6jCQ= github.com/moby/moby/v2 v2.0.0-beta.6 h1:esIpPmQk4oZyPkbu+5hDAE+as2LEKaYxQcWSC5zUGiM= github.com/moby/moby/v2 v2.0.0-beta.6/go.mod h1:nrC3bakOe44FcxsHAuvxsIHWtS4Vcf8Kn1j44kcZRLs= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk= github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/opencontainers/cgroups v0.0.6 h1:tfZFWTIIGaUUFImTyuTg+Mr5x8XRiSdZESgEBW7UxuI= github.com/opencontainers/cgroups v0.0.6/go.mod h1:oWVzJsKK0gG9SCRBfTpnn16WcGEqDI8PAcpMGbqWxcs= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opencontainers/runc v1.4.1 h1:eg1A930KKiZ3IShaYHPRiDi6uMrHMnd7OiElHBLgqfY= github.com/opencontainers/runc v1.4.1/go.mod h1:ufk5PTTsy5pnGBAvTh50e+eqGk01pYH2YcVxh557Qlk= github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg= github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-tools v0.9.1-0.20251205004911-5e639034dcdc h1:82NlZLiQEB6Wp+nmASMlkHsEXz3Q0pXJbK7QO6ck5lo= github.com/opencontainers/runtime-tools v0.9.1-0.20251205004911-5e639034dcdc/go.mod h1:DKDEfzxvRkoQ6n9TGhxQgg2IM1lY4aM0eaQP4e3oElw= github.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE= github.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg= github.com/openshift/imagebuilder v1.2.20 h1:oMGLqJ9Z3xHitzBCL+HECg/c9KCV/Rh7IKJqbSaBnqk= github.com/openshift/imagebuilder v1.2.20/go.mod h1:KyPwIOzCzleBWdDmApxog8sj2SO9l5g4IWPv7S67Fgs= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/proglottis/gpgme v0.1.6 h1:8WpQ8VWggLdxkuTnW+sZ1r1t92XBNd8GZNDhQ4Rz+98= github.com/proglottis/gpgme v0.1.6/go.mod h1:5LoXMgpE4bttgwwdv9bLs/vwqv3qV7F4glEEZ7mRKrM= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E= github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= github.com/seccomp/libseccomp-golang v0.11.1 h1:wuk4ZjSx6kyQII4rj6G6fvVzRHQaSiPvccJazDagu4g= github.com/seccomp/libseccomp-golang v0.11.1/go.mod h1:5m1Lk8E9OwgZTTVz4bBOer7JuazaBa+xTkM895tDiWc= github.com/secure-systems-lab/go-securesystemslib v0.10.0 h1:l+H5ErcW0PAehBNrBxoGv1jjNpGYdZ9RcheFkB2WI14= github.com/secure-systems-lab/go-securesystemslib v0.10.0/go.mod h1:MRKONWmRoFzPNQ9USRF9i1mc7MvAVvF1LlW8X5VWDvk= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sigstore/fulcio v1.8.5 h1:HYTD1/L5wlBp8JxsWxUf8hmfaNBBF/x3r3p5l6tZwbA= github.com/sigstore/fulcio v1.8.5/go.mod h1:tSLYK3JsKvJpDW1BsIsVHZgHj+f8TjXARzqIUWSsSPQ= github.com/sigstore/protobuf-specs v0.5.0 h1:F8YTI65xOHw70NrvPwJ5PhAzsvTnuJMGLkA4FIkofAY= github.com/sigstore/protobuf-specs v0.5.0/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc= github.com/sigstore/sigstore v1.10.4 h1:ytOmxMgLdcUed3w1SbbZOgcxqwMG61lh1TmZLN+WeZE= github.com/sigstore/sigstore v1.10.4/go.mod h1:tDiyrdOref3q6qJxm2G+JHghqfmvifB7hw+EReAfnbI= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU= github.com/smallstep/pkcs7 v0.1.1/go.mod h1:dL6j5AIz9GHjVEBTXtW+QliALcgM19RtXaTeyxI+AfA= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw= github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/sylabs/sif/v2 v2.22.0 h1:Y+xXufp4RdgZe02SR3nWEg7S6q4tPWN237WHYzkDSKA= github.com/sylabs/sif/v2 v2.22.0/go.mod h1:W1XhWTmG1KcG7j5a3KSYdMcUIFvbs240w/MMVW627hs= github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc= github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/vbauerster/mpb/v8 v8.12.0 h1:+gneY3ifzc88tKDzOtfG8k8gfngCx615S2ZmFM4liWg= github.com/vbauerster/mpb/v8 v8.12.0/go.mod h1:V02YIuMVo301Y1VE9VtZlD8s84OMsk+EKN6mwvf/588= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.podman.io/common v0.67.1-0.20260316162257-e70c309aabae h1:VoUmnazJGnPqWoGYF3YzRU0ZgA9DQ3qg1bYCqhkYUyg= go.podman.io/common v0.67.1-0.20260316162257-e70c309aabae/go.mod h1:sugHfpSU9btZ6KRTruHrfbzTO8HbOCX9s++p7kmdbOQ= go.podman.io/image/v5 v5.39.2-0.20260316162257-e70c309aabae h1:jBTnK7tFX0O7b5wuo2b+JunSLxJE0rSsTvhiTGtfNf8= go.podman.io/image/v5 v5.39.2-0.20260316162257-e70c309aabae/go.mod h1:eSTQb66cEeMtWaj3y4mfDsMB7t068tG+UjbPLWj60ho= go.podman.io/storage v1.62.1-0.20260316162257-e70c309aabae h1:2cNSnB3KSKTtmd+PBqAHsQmNjhOkmh2Ne0mhhMRrMRo= go.podman.io/storage v1.62.1-0.20260316162257-e70c309aabae/go.mod h1:k8lWBDknm4IJEfY9Gy3P/wRFT8aWvtk+rPtD8FKxIBE= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= tags.cncf.io/container-device-interface v1.1.0 h1:RnxNhxF1JOu6CJUVpetTYvrXHdxw9j9jFYgZpI+anSY= tags.cncf.io/container-device-interface v1.1.0/go.mod h1:76Oj0Yqp9FwTx/pySDc8Bxjpg+VqXfDb50cKAXVJ34Q= tags.cncf.io/container-device-interface/specs-go v1.1.0 h1:QRZVeAceQM+zTZe12eyfuJuuzp524EKYwhmvLd+h+yQ= tags.cncf.io/container-device-interface/specs-go v1.1.0/go.mod h1:u86hoFWqnh3hWz3esofRFKbI261bUlvUfLKGrDhJkgQ= ================================================ FILE: hack/apparmor_tag.sh ================================================ #!/usr/bin/env bash if pkg-config libapparmor 2>/dev/null; then echo apparmor fi ================================================ FILE: hack/build_speed.sh ================================================ #! /bin/sh # The main goal of this script is test and time builds using Buildah or Docker. # We hope to use it to help optimize Buildah build performance # # It takes two options # First option tells the type of the container image # build to do. Valid options are: # Docker - docker build # Buildah - buildah bud # Both - Do docker build followed by buildah bud # # Second Option specifies a directory or cleanup # The script will 'find' files beginning with Dockerfile, for each Dockerfile # it finds it will run a build with the Dockerfile and directory for the # context. When it does the builds, it will call time on them to show how # long the builds take. The created image name will be a combination of the # lowercased Directory name that the Dockerfile was found in plus the lower # cased dockerfile name. # # if the second field is cleanup, the script will remove all images from the # specified builder. # # The script does not check for conflicts on naming. # # Outputs file: # # # cat /tmp/build_speed.json # { # "/usr/share/fedora-dockerfiles/redis/Dockerfile": { # "docker": { # "command": "docker build -f /usr/share/fedora-dockerfiles/redis/Dockerfile -t redis_dockerfile /usr/share/fedora-dockerfiles/redis", # "real": "3:28.70" # }, # "buildah": { # "command": "buildah bud --layers -f /usr/share/fedora-dockerfiles/redis/Dockerfile -t redis_dockerfile /usr/share/fedora-dockerfiles/redis", # "real": "2:55.48" # } # } # } # # Examples uses # ./build_speed.sh Docker ~/MyImages # ./build_speed.sh Both /usr/share/fedora-dockerfiles/django/Dockerfile #totalsfile=$(mktemp /tmp/buildspeedXXX.json) totalsfile=/tmp/build_speed.json commaDockerfile="" echo -n '{' > $totalsfile Dockerfiles() { find -L $1 -name Dockerfile\* } Buildah() { Name=$1 Dockerfile=$2 Context=$3 echo buildah bud --layers -f ${Dockerfile} -t ${Name} ${Context} Time buildah bud --layers -f ${Dockerfile} -t ${Name} ${Context} } Time() { outfile=$(mktemp /tmp/buildspeedXXX) /usr/bin/time -o $outfile --f "%E" $@ echo "{\"engine\": \"$1\", \"command\": \"$@\", \"real\": \"$(cat ${outfile})\"}" echo -n "${comma}\"$1\": {\"command\": \"$@\", \"real\": \"$(cat ${outfile})\"}" >> $totalsfile comma="," rm -f $outfile } Docker() { Name=$1 Dockerfile=$2 Context=$3 echo docker build -f ${Dockerfile} -t ${Name} ${Context} Time docker build -f ${Dockerfile} -t ${Name} ${Context} } Both() { comma="" echo -n "${commaDockerfile}\"$2\": {" >> $totalsfile commaDockerfile="," Docker $1 $2 $3 Buildah $1 $2 $3 echo -n "}" >> $totalsfile } Docker_cleanup() { docker rmi --force $(docker images -q) } Buildah_cleanup() { buildah rmi --force --all } Both_cleanup() { Docker_cleanup Buildah_cleanup } Cmd=${1?Missing CMD argument} Path=${2?Missing PATH argument} case "$Cmd" in Docker) ;; Buildah) ;; Both) ;; *) echo "Invalid command '$Cmd'; must be Buildah, Docker, or Both"; exit 1;; esac if [ "$Path" == "cleanup" ]; then ${Cmd}_cleanup exit 0 fi for i in $(Dockerfiles ${Path});do name=$(basename $(dirname $i) | sed -e 's/\(.*\)/\L\1/') name=${name}_$(basename $i | sed -e 's/\(.*\)/\L\1/') echo ${Cmd} ${name} $i $(dirname $i) ${Cmd} ${name} $i $(dirname $i) done echo '}'>>$totalsfile echo cat $totalsfile cat $totalsfile ================================================ FILE: hack/check_vendor_toolchain.sh ================================================ #!/bin/bash topdir=$(dirname ${BASH_SOURCE})/.. wantversion=$(sed -e '/^go /!d' -e '/^go /s,.* ,,g' ${topdir}/go.mod) goversion=$(go env GOVERSION) haveversion=${goversion/go} if test "${wantversion%.*}" != "${haveversion%.*}" ; then echo go.mod uses Go "${wantversion%.*}" \("${wantversion}"\), but environment provides "${haveversion%.*}" \("${haveversion}"\). echo "$@" exit 1 fi exit 0 ================================================ FILE: hack/get_ci_vm.sh ================================================ #!/usr/bin/env bash # # For help and usage information, simply execute the script w/o any arguments. # # This script is intended to be run by Red Hat buildah developers who need # to debug problems specifically related to Cirrus-CI automated testing. # It requires that you have been granted prior access to create VMs in # google-cloud. For non-Red Hat contributors, VMs are available as-needed, # with supervision upon request. set -e SCRIPT_FILEPATH=$(realpath "${BASH_SOURCE[0]}") SCRIPT_DIRPATH=$(dirname "$SCRIPT_FILEPATH") REPO_DIRPATH=$(realpath "$SCRIPT_DIRPATH/../") # Help detect if we were called by get_ci_vm container GET_CI_VM="${GET_CI_VM:-0}" in_get_ci_vm() { if ((GET_CI_VM==0)); then echo "Error: $1 is not intended for use in this context" exit 2 fi } # get_ci_vm APIv1 container entrypoint calls into this script # to obtain required repo. specific configuration options. if [[ "$1" == "--config" ]]; then in_get_ci_vm "$1" cat < /dev/stderr source ./contrib/cirrus/lib.sh echo "+ Running environment setup" > /dev/stderr ./contrib/cirrus/setup.sh else # Create and access VM for specified Cirrus-CI task mkdir -p $HOME/.config/gcloud/ssh podman run -it --rm \ --tz=local \ -e NAME="$USER" \ -e SRCDIR=/src \ -e GCLOUD_ZONE="$GCLOUD_ZONE" \ -e A_DEBUG="${A_DEBUG:-0}" \ -v $REPO_DIRPATH:/src:O \ -v $HOME/.config/gcloud:/root/.config/gcloud:z \ -v $HOME/.config/gcloud/ssh:/root/.ssh:z \ quay.io/libpod/get_ci_vm:latest "$@" fi ================================================ FILE: hack/libsubid_tag.sh ================================================ #!/usr/bin/env bash if test $(${GO:-go} env GOOS) != "linux" ; then exit 0 fi tmpdir="$PWD/tmp.$RANDOM" mkdir -p "$tmpdir" trap 'rm -fr "$tmpdir"' EXIT cc -o "$tmpdir"/libsubid_tag -l subid -x c - > /dev/null 2> /dev/null << EOF #include #include #include const char *Prog = "test"; FILE *shadow_logfd = NULL; int main() { struct subid_range *ranges = NULL; #if SUBID_ABI_MAJOR >= 4 subid_get_uid_ranges("root", &ranges); #else get_subuid_ranges("root", &ranges); #endif free(ranges); return 0; } EOF if test $? -eq 0 ; then echo libsubid fi ================================================ FILE: hack/sqlite_tag.sh ================================================ #!/usr/bin/env bash ${CPP:-${CC:-cc} -E} ${CPPFLAGS} - &> /dev/null << EOF #include EOF if test $? -eq 0 ; then echo libsqlite3 fi ================================================ FILE: hack/systemd_tag.sh ================================================ #!/usr/bin/env bash ${CPP:-${CC:-cc} -E} ${CPPFLAGS} - > /dev/null 2> /dev/null << EOF #include EOF if test $? -eq 0 ; then echo systemd fi ================================================ FILE: hack/tree_status.sh ================================================ #!/usr/bin/env bash set -e STATUS=$(git status --porcelain) if [[ -z $STATUS ]] then echo "tree is clean" else echo "tree is dirty, please commit all changes and sync the vendor.conf" echo "" echo "$STATUS" git diff exit 1 fi ================================================ FILE: hack/xref-helpmsgs-manpages ================================================ #!/usr/bin/perl # # xref-helpmsgs-manpages - cross-reference --help options against man pages # package LibPod::CI::XrefHelpmsgsManpages; use v5.14; use utf8; use strict; use warnings; (our $ME = $0) =~ s|.*/||; our $VERSION = '0.1'; # For debugging, show data structures using DumpTree($var) #use Data::TreeDumper; $Data::TreeDumper::Displayaddress = 0; # unbuffer output $| = 1; ############################################################################### # BEGIN user-customizable section # Path to desired executable my $Default_Tool = './bin/buildah'; my $TOOL = $ENV{BUILDAH} || $Default_Tool; # Path to all doc files, including .rst and (down one level) markdown my $Docs_Path = 'docs'; # Path to markdown source files (of the form -*.1.md) my $Markdown_Path = "$Docs_Path"; # Global error count my $Errs = 0; # END user-customizable section ############################################################################### use FindBin; ############################################################################### # BEGIN boilerplate args checking, usage messages sub usage { print <<"END_USAGE"; Usage: $ME [OPTIONS] $ME recursively runs ' --help' against all subcommands; and recursively reads -*.1.md files in $Markdown_Path, then cross-references that each --help option is listed in the appropriate man page and vice-versa. $ME invokes '\$BUILDAH' (default: $Default_Tool). Exit status is zero if no inconsistencies found, one otherwise OPTIONS: -v, --verbose show verbose progress indicators -n, --dry-run make no actual changes --help display this message --version display program name and version END_USAGE exit; } # Command-line options. Note that this operates directly on @ARGV ! our $debug = 0; our $verbose = 0; sub handle_opts { use Getopt::Long; GetOptions( 'debug!' => \$debug, 'verbose|v' => \$verbose, help => \&usage, version => sub { print "$ME version $VERSION\n"; exit 0 }, ) or die "Try `$ME --help' for help\n"; } # END boilerplate args checking, usage messages ############################################################################### ############################## CODE BEGINS HERE ############################### # The term is "modulino". __PACKAGE__->main() unless caller(); # Main code. sub main { # Note that we operate directly on @ARGV, not on function parameters. # This is deliberate: it's because Getopt::Long only operates on @ARGV # and there's no clean way to make it use @_. handle_opts(); # will set package globals # Fetch command-line arguments. Barf if too many. die "$ME: Too many arguments; try $ME --help\n" if @ARGV; my $help = tool_help(); my $man = tool_man('buildah'); xref_by_help($help, $man); xref_by_man($help, $man); # xref_rst($help, $rst); exit !!$Errs; } ############################################################################### # BEGIN cross-referencing ################## # xref_by_help # Find keys in '--help' but not in man ################## sub xref_by_help { my ($help, $man, @subcommand) = @_; for my $k (sort keys %$help) { if (exists $man->{$k}) { if (ref $help->{$k}) { xref_by_help($help->{$k}, $man->{$k}, @subcommand, $k); } # Otherwise, non-ref is leaf node such as a --option } else { my $man = $man->{_path} || 'man'; warn "$ME: buildah @subcommand --help lists $k, but $k not in $man\n"; ++$Errs; } } } ################# # xref_by_man # Find keys in man pages but not in --help ################# # # In an ideal world we could share the functionality in one function; but # there are just too many special cases in man pages. # sub xref_by_man { my ($help, $man, @subcommand) = @_; # FIXME: this generates way too much output for my $k (grep { $_ ne '_path' } sort keys %$man) { if (exists $help->{$k}) { if (ref $man->{$k}) { xref_by_man($help->{$k}, $man->{$k}, @subcommand, $k); } } elsif ($k ne '--help' && $k ne '-h') { my $man = $man->{_path} || 'man'; # Special case: 'buildah run --tty' is an invisible alias for -t next if $k eq '--tty' && $help->{'--terminal'}; # Special case: '--net' is an undocumented shortcut in from,run next if $k eq '--net' && $help->{'--network'}; # Special case: global options, re-documented in buildah-bud.md # next if "@subcommand" eq "bud" && $k =~ /^--userns-.id-map$/; warn "$ME: buildah @subcommand: $k in $man, but not --help\n"; ++$Errs; } } } # END cross-referencing ############################################################################### # BEGIN data gathering ############### # tool_help # Parse output of ' [subcommand] --help' ############### sub tool_help { my %help; open my $fh, '-|', $TOOL, @_, '--help' or die "$ME: Cannot fork: $!\n"; my $section = ''; while (my $line = <$fh>) { # Cobra is blessedly consistent in its output: # Usage: ... # Available Commands: # .... # Flags: # .... # # Start by identifying the section we're in... if ($line =~ /^Available\s+(Commands):/) { $section = lc $1; } elsif ($line =~ /^(Flags):/) { $section = lc $1; } # ...then track commands and options. For subcommands, recurse. elsif ($section eq 'commands') { if ($line =~ /^\s{1,4}(\S+)\s/) { my $subcommand = $1; print "> buildah @_ $subcommand\n" if $debug; $help{$subcommand} = tool_help(@_, $subcommand) unless $subcommand eq 'help'; # 'help' not in man } } elsif ($section eq 'flags') { next if $line =~ /^\s+-h,\s+--help/; # Ignore --help # Handle '--foo' or '-f, --foo' if ($line =~ /^\s{1,10}(--\S+)\s/) { print "> buildah @_ $1\n" if $debug; $help{$1} = 1; } elsif ($line =~ /^\s{1,10}(-\S),\s+(--\S+)\s/) { print "> buildah @_ $1, $2\n" if $debug; $help{$1} = $help{$2} = 1; } } } close $fh or die "$ME: Error running 'buildah @_ --help'\n"; return \%help; } ############## # tool_man # Parse contents of -*.1.md ############## sub tool_man { my $command = shift; my $subpath = "$Markdown_Path/$command.1.md"; my $manpath = "$FindBin::Bin/../$subpath"; print "** $subpath \n" if $debug; my %man = (_path => $subpath); open my $fh, '<', $manpath or die "$ME: Cannot read $manpath: $!\n"; my $section = ''; my @most_recent_flags; my $previous_subcmd = ''; my $previous_flag = ''; my @line_history; # Circular buffer of recent lines while (my $line = <$fh>) { chomp $line; push @line_history, $line; shift @line_history if @line_history > 2; next unless $line; # skip empty lines # .md files designate sections with leading double hash if ($line =~ /^##\s*(GLOBAL\s+)?OPTIONS/) { $section = 'flags'; $previous_flag = ''; } elsif ($line =~ /^\#\#\s+(SUB)?COMMANDS/) { $section = 'commands'; } elsif ($line =~ /^\#\#[^#]/) { $section = ''; } # This will be a table containing subcommand names, links to man pages. # The format is slightly different between buildah.1.md and subcommands. elsif ($section eq 'commands') { # In tool.1.md if ($line =~ /^\|\s*buildah-(\S+?)\(\d\)/) { # $1 will be changed by recursion _*BEFORE*_ left-hand assignment my $subcmd = $1; $man{$subcmd} = tool_man("buildah-$1"); } # In tool-.1.md elsif ($line =~ /^\|\s+(\S+)\s+\|\s+\[\S+\]\((\S+)\.1\.md\)/) { # $1 will be changed by recursion _*BEFORE*_ left-hand assignment my $subcmd = $1; if ($previous_subcmd gt $subcmd) { warn "$ME: $subpath: '$previous_subcmd' and '$subcmd' are out of order\n"; ++$Errs; } $previous_subcmd = $subcmd; $man{$subcmd} = tool_man($2); } } # Flags should always be of the form '**-f**' or '**--flag**', # possibly separated by comma-space. elsif ($section eq 'flags') { # e.g. 'podman run --ip6', documented in man page, but nonexistent if ($line =~ /^not\s+implemented/i) { delete $man{$_} for @most_recent_flags; } # AAAAAAAaaaaargh, workaround for buildah-config --add-history # which enumerates a long list of options. Since buildah man pages # (unlike podman) don't use the '####' convention for options, # it's hard to differentiate 'this is an option' from 'this is # a __mention__ of an option'. Workaround: actual options must # be preceded by an empty line. next if $line_history[-2]; @most_recent_flags = (); # Handle any variation of '**--foo**, **-f**' my $is_first = 1; while ($line =~ s/^\*\*((--[a-z0-9-]+)|(-.))\*\*(,\s+)?//g) { my $flag = $1; $man{$flag} = 1; if ($flag lt $previous_flag && $is_first) { warn "$ME: $subpath:$.: $flag should precede $previous_flag\n"; ++$Errs; } $previous_flag = $flag if $is_first; # Keep track of them, in case we see 'Not implemented' below push @most_recent_flags, $flag; # Further iterations of /g are allowed to be out of order, # e.g., it's OK for --namespace,-ns to precede --nohead $is_first = 0; } } } close $fh; return \%man; } # END data gathering ############################################################################### 1; ================================================ FILE: image.go ================================================ package buildah import ( "archive/tar" "bytes" "context" "encoding/json" "errors" "fmt" "io" "maps" "os" "path" "path/filepath" "slices" "strings" "syscall" "time" "github.com/containers/buildah/copier" "github.com/containers/buildah/define" "github.com/containers/buildah/docker" "github.com/containers/buildah/internal/config" "github.com/containers/buildah/internal/mkcw" "github.com/containers/buildah/internal/tmpdir" digest "github.com/opencontainers/go-digest" specs "github.com/opencontainers/image-spec/specs-go" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" "go.podman.io/image/v5/docker/reference" "go.podman.io/image/v5/image" "go.podman.io/image/v5/manifest" is "go.podman.io/image/v5/storage" "go.podman.io/image/v5/types" "go.podman.io/storage" "go.podman.io/storage/pkg/archive" "go.podman.io/storage/pkg/chrootarchive" "go.podman.io/storage/pkg/idtools" "go.podman.io/storage/pkg/ioutils" ) const ( // OCIv1ImageManifest is the MIME type of an OCIv1 image manifest, // suitable for specifying as a value of the PreferredManifestType // member of a CommitOptions structure. It is also the default. OCIv1ImageManifest = define.OCIv1ImageManifest // Dockerv2ImageManifest is the MIME type of a Docker v2s2 image // manifest, suitable for specifying as a value of the // PreferredManifestType member of a CommitOptions structure. Dockerv2ImageManifest = define.Dockerv2ImageManifest // containerExcludesDir is the subdirectory of the container data // directory where we drop exclusions containerExcludesDir = "commit-excludes" // containerPulledUpDir is the subdirectory of the container // data directory where we drop exclusions when we're not squashing containerPulledUpDir = "commit-pulled-up" // containerExcludesSubstring is the suffix of files under // $cdir/containerExcludesDir and $cdir/containerPulledUpDir which // should be ignored, as they only exist because we use CreateTemp() to // create uniquely-named files, but we don't want to try to use their // contents until after they've been written to containerExcludesSubstring = ".tmp" // Windows-specific PAX record keys keyFileAttr = "MSWINDOWS.fileattr" keySDRaw = "MSWINDOWS.rawsd" keyCreationTime = "LIBARCHIVE.creationtime" // FILE_ATTRIBUTE_ values fileAttributeDirectory = "16" fileAttributeArchive = "32" // Windows Security Descriptors (base64-encoded) // SDDL: O:BAG:SYD:(A;OICI;FA;;;BA)(A;OICI;FA;;;SY)(A;;FA;;;BA)(A;OICIIO;GA;;;CO)(A;OICI;0x1200a9;;;BU)(A;CI;LC;;;BU)(A;CI;DC;;;BU) // Owner: Built-in Administrators (BA) // Group: Local System (SY) // DACL: // - Allow OBJECT_INHERIT+CONTAINER_INHERIT Full Access to Built-in Administrators (BA) // - Allow OBJECT_INHERIT+CONTAINER_INHERIT Full Access to Local System (SY) // - Allow Full Access to Built-in Administrators (BA) // - Allow OBJECT_INHERIT+CONTAINER_INHERIT+INHERIT_ONLY Generic All to Creator Owner (CO) // - Allow OBJECT_INHERIT+CONTAINER_INHERIT Read/Execute permissions to Built-in Users (BU) // - Allow CONTAINER_INHERIT List Contents to Built-in Users (BU) // - Allow CONTAINER_INHERIT Delete Child to Built-in Users (BU) winSecurityDescriptorDirectory = "AQAEgBQAAAAkAAAAAAAAADAAAAABAgAAAAAABSAAAAAgAgAAAQEAAAAAAAUSAAAAAgCoAAcAAAAAAxgA/wEfAAECAAAAAAAFIAAAACACAAAAAxQA/wEfAAEBAAAAAAAFEgAAAAAAGAD/AR8AAQIAAAAAAAUgAAAAIAIAAAALFAAAAAAQAQEAAAAAAAMAAAAAAAMYAKkAEgABAgAAAAAABSAAAAAhAgAAAAIYAAQAAAABAgAAAAAABSAAAAAhAgAAAAIYAAIAAAABAgAAAAAABSAAAAAhAgAA" // SDDL: O:BAG:SYD:(A;;FA;;;BA)(A;;FA;;;SY)(A;;0x1200a9;;;BU) // Owner: Built-in Administrators (BA) // Group: Local System (SY) // DACL: // - Allow Full Access to Built-in Administrators (BA) // - Allow Full Access to Local System (SY) // - Allow Read/Execute permissions to Built-in Users (BU) winSecurityDescriptorFile = "AQAEgBQAAAAkAAAAAAAAADAAAAABAgAAAAAABSAAAAAgAgAAAQEAAAAAAAUSAAAAAgBMAAMAAAAAABgA/wEfAAECAAAAAAAFIAAAACACAAAAABQA/wEfAAEBAAAAAAAFEgAAAAAAGACpABIAAQIAAAAAAAUgAAAAIQIAAA==" ) // ExtractRootfsOptions is consumed by ExtractRootfs() which allows users to // control whether various information like the like setuid and setgid bits and // xattrs are preserved when extracting file system objects. type ExtractRootfsOptions struct { StripSetuidBit bool // strip the setuid bit off of items being extracted. StripSetgidBit bool // strip the setgid bit off of items being extracted. StripXattrs bool // don't record extended attributes of items being extracted. ForceTimestamp *time.Time // force timestamps in output content } type containerImageRef struct { fromImageName string fromImageID string store storage.Store compression archive.Compression name reference.Named names []string containerID string mountLabel string layerID string oconfig []byte dconfig []byte created *time.Time createdBy string layerModTime *time.Time layerLatestModTime *time.Time historyComment string annotations map[string]string preferredManifestType string squash bool confidentialWorkload ConfidentialWorkloadOptions omitHistory bool emptyLayer bool omitLayerHistoryEntry bool idMappingOptions *define.IDMappingOptions parent string blobDirectory string preEmptyLayers []v1.History preLayers []commitLinkedLayerInfo postEmptyLayers []v1.History postLayers []commitLinkedLayerInfo overrideChanges []string overrideConfig *manifest.Schema2Config extraImageContent map[string]string compatSetParent types.OptionalBool layerExclusions []copier.ConditionalRemovePath layerMountTargets []copier.ConditionalRemovePath layerPullUps []copier.EnsureParentPath unsetAnnotations []string setAnnotations []string createdAnnotation types.OptionalBool os string } type blobLayerInfo struct { ID string Size int64 } type commitLinkedLayerInfo struct { layerID string // more like layer "ID" linkedLayer LinkedLayer uncompressedDigest digest.Digest size int64 } type containerImageSource struct { path string ref *containerImageRef store storage.Store containerID string mountLabel string layerID string names []string compression archive.Compression config []byte configDigest digest.Digest manifest []byte manifestType string blobDirectory string blobLayers map[digest.Digest]blobLayerInfo } func (i *containerImageRef) NewImage(ctx context.Context, sc *types.SystemContext) (types.ImageCloser, error) { src, err := i.NewImageSource(ctx, sc) if err != nil { return nil, err } return image.FromSource(ctx, sc, src) } func expectedOCIDiffIDs(image v1.Image) int { expected := 0 for _, history := range image.History { if !history.EmptyLayer { expected = expected + 1 } } return expected } func expectedDockerDiffIDs(image docker.V2Image) int { expected := 0 for _, history := range image.History { if !history.EmptyLayer { expected = expected + 1 } } return expected } // Extract the container's whole filesystem as a filesystem image, wrapped // in LUKS-compatible encryption. func (i *containerImageRef) extractConfidentialWorkloadFS(options ConfidentialWorkloadOptions) (io.ReadCloser, error) { var image v1.Image if err := json.Unmarshal(i.oconfig, &image); err != nil { return nil, fmt.Errorf("recreating OCI configuration for %q: %w", i.containerID, err) } if options.TempDir == "" { cdir, err := i.store.ContainerDirectory(i.containerID) if err != nil { return nil, fmt.Errorf("getting the per-container data directory for %q: %w", i.containerID, err) } tempdir, err := os.MkdirTemp(cdir, "buildah-rootfs") if err != nil { return nil, fmt.Errorf("creating a temporary data directory to hold a rootfs image for %q: %w", i.containerID, err) } defer func() { if err := os.RemoveAll(tempdir); err != nil { logrus.Warnf("removing temporary directory %q: %v", tempdir, err) } }() options.TempDir = tempdir } mountPoint, err := i.store.Mount(i.containerID, i.mountLabel) if err != nil { return nil, fmt.Errorf("mounting container %q: %w", i.containerID, err) } archiveOptions := mkcw.ArchiveOptions{ AttestationURL: options.AttestationURL, CPUs: options.CPUs, Memory: options.Memory, TempDir: options.TempDir, TeeType: options.TeeType, IgnoreAttestationErrors: options.IgnoreAttestationErrors, WorkloadID: options.WorkloadID, DiskEncryptionPassphrase: options.DiskEncryptionPassphrase, Slop: options.Slop, FirmwareLibrary: options.FirmwareLibrary, GraphOptions: i.store.GraphOptions(), ExtraImageContent: i.extraImageContent, } rc, _, err := mkcw.Archive(mountPoint, &image, archiveOptions) if err != nil { if _, err2 := i.store.Unmount(i.containerID, false); err2 != nil { logrus.Debugf("unmounting container %q: %v", i.containerID, err2) } return nil, fmt.Errorf("converting rootfs %q: %w", i.containerID, err) } return ioutils.NewReadCloserWrapper(rc, func() error { if err = rc.Close(); err != nil { err = fmt.Errorf("closing tar archive of container %q: %w", i.containerID, err) } if _, err2 := i.store.Unmount(i.containerID, false); err == nil { if err2 != nil { err2 = fmt.Errorf("unmounting container %q: %w", i.containerID, err2) } err = err2 } else { logrus.Debugf("unmounting container %q: %v", i.containerID, err2) } return err }), nil } // Extract the container's whole filesystem as if it were a single layer. // The ExtractRootfsOptions control whether or not to preserve setuid and // setgid bits and extended attributes on contents. func (i *containerImageRef) extractRootfs(opts ExtractRootfsOptions) (io.ReadCloser, chan error, error) { var uidMap, gidMap []idtools.IDMap mountPoint, err := i.store.Mount(i.containerID, i.mountLabel) if err != nil { return nil, nil, fmt.Errorf("mounting container %q: %w", i.containerID, err) } pipeReader, pipeWriter := io.Pipe() errChan := make(chan error, 1) go func() { defer pipeWriter.Close() defer close(errChan) if len(i.extraImageContent) > 0 { // Abuse the tar format and _prepend_ the synthesized // data items to the archive we'll get from // copier.Get(), in a way that looks right to a reader // as long as we DON'T Close() the tar Writer. filename, _, _, err := i.makeExtraImageContentDiff(false, opts.ForceTimestamp) if err != nil { errChan <- fmt.Errorf("creating part of archive with extra content: %w", err) return } file, err := os.Open(filename) if err != nil { errChan <- err return } defer file.Close() if _, err = io.Copy(pipeWriter, file); err != nil { errChan <- fmt.Errorf("writing contents of %q: %w", filename, err) return } } if i.idMappingOptions != nil { uidMap, gidMap = convertRuntimeIDMaps(i.idMappingOptions.UIDMap, i.idMappingOptions.GIDMap) } copierOptions := copier.GetOptions{ UIDMap: uidMap, GIDMap: gidMap, StripSetuidBit: opts.StripSetuidBit, StripSetgidBit: opts.StripSetgidBit, StripXattrs: opts.StripXattrs, Timestamp: opts.ForceTimestamp, } err := copier.Get(mountPoint, mountPoint, copierOptions, []string{"."}, pipeWriter) errChan <- err }() return ioutils.NewReadCloserWrapper(pipeReader, func() error { if err = pipeReader.Close(); err != nil { err = fmt.Errorf("closing tar archive of container %q: %w", i.containerID, err) } if _, err2 := i.store.Unmount(i.containerID, false); err == nil { if err2 != nil { err2 = fmt.Errorf("unmounting container %q: %w", i.containerID, err2) } err = err2 } return err }), errChan, nil } type manifestBuilder interface { // addLayer adds notes to the manifest and config about the layer. The layer blobs are // identified by their possibly-compressed blob digests and sizes in the manifest, and by // their uncompressed digests (diffIDs) in the config. addLayer(layerBlobSum digest.Digest, layerBlobSize int64, diffID digest.Digest) computeLayerMIMEType(what string, layerCompression archive.Compression) error buildHistory(extraImageContentDiff string, extraImageContentDiffDigest digest.Digest) error manifestAndConfig() ([]byte, []byte, error) } type dockerSchema2ManifestBuilder struct { i *containerImageRef layerMediaType string dimage docker.V2Image dmanifest docker.V2S2Manifest } // Build fresh copies of the container configuration structures so that we can edit them // without making unintended changes to the original Builder (Docker schema 2). func (i *containerImageRef) newDockerSchema2ManifestBuilder() (manifestBuilder, error) { created := time.Now().UTC() if i.created != nil { created = *i.created } // Build an empty image, and then decode over it. dimage := docker.V2Image{} if err := json.Unmarshal(i.dconfig, &dimage); err != nil { return nil, err } // Suppress the hostname and domainname if we're running with the // equivalent of either --timestamp or --source-date-epoch. if i.created != nil { dimage.Config.Hostname = "sandbox" dimage.Config.Domainname = "" } // Set the parent, but only if we want to be compatible with "classic" docker build. if i.compatSetParent == types.OptionalBoolTrue { dimage.Parent = docker.ID(i.parent) } // Set the container ID and containerConfig in the docker format. dimage.Container = i.containerID if i.created != nil { dimage.Container = "" } if dimage.Config != nil { dimage.ContainerConfig = *dimage.Config } // Always replace this value, since we're newer than our base image. dimage.Created = created // Clear the list of diffIDs, since we always repopulate it. dimage.RootFS = &docker.V2S2RootFS{} dimage.RootFS.Type = docker.TypeLayers dimage.RootFS.DiffIDs = []digest.Digest{} // Only clear the history if we're squashing, otherwise leave it be so // that we can append entries to it. Clear the parent, too, to reflect // that we no longer include its layers and history. if i.confidentialWorkload.Convert || i.squash || i.omitHistory { dimage.Parent = "" dimage.History = []docker.V2S2History{} } // If we were supplied with a configuration, copy fields from it to // matching fields in both formats. if err := config.OverrideDocker(dimage.Config, i.overrideChanges, i.overrideConfig); err != nil { return nil, fmt.Errorf("applying changes: %w", err) } // If we're producing a confidential workload, override the command and // assorted other settings that aren't expected to work correctly. if i.confidentialWorkload.Convert { dimage.Config.Entrypoint = []string{"/entrypoint"} dimage.Config.Cmd = nil dimage.Config.User = "" dimage.Config.WorkingDir = "" dimage.Config.Healthcheck = nil dimage.Config.Shell = nil dimage.Config.Volumes = nil dimage.Config.ExposedPorts = nil } // Return partial manifest. The Layers lists will be populated later. return &dockerSchema2ManifestBuilder{ i: i, layerMediaType: docker.V2S2MediaTypeUncompressedLayer, dimage: dimage, dmanifest: docker.V2S2Manifest{ V2Versioned: docker.V2Versioned{ SchemaVersion: 2, MediaType: manifest.DockerV2Schema2MediaType, }, Config: docker.V2S2Descriptor{ MediaType: manifest.DockerV2Schema2ConfigMediaType, }, Layers: []docker.V2S2Descriptor{}, }, }, nil } func (mb *dockerSchema2ManifestBuilder) addLayer(layerBlobSum digest.Digest, layerBlobSize int64, diffID digest.Digest) { dlayerDescriptor := docker.V2S2Descriptor{ MediaType: mb.layerMediaType, Digest: layerBlobSum, Size: layerBlobSize, } mb.dmanifest.Layers = append(mb.dmanifest.Layers, dlayerDescriptor) // Note this layer in the list of diffIDs, again using the uncompressed digest. mb.dimage.RootFS.DiffIDs = append(mb.dimage.RootFS.DiffIDs, diffID) } // Compute the media types which we need to attach to a layer, given the type of // compression that we'll be applying. func (mb *dockerSchema2ManifestBuilder) computeLayerMIMEType(what string, layerCompression archive.Compression) error { dmediaType := docker.V2S2MediaTypeUncompressedLayer if layerCompression != archive.Uncompressed { switch layerCompression { case archive.Gzip: dmediaType = manifest.DockerV2Schema2LayerMediaType logrus.Debugf("compressing %s with gzip", what) case archive.Bzip2: // Until the image specs define a media type for bzip2-compressed layers, even if we know // how to decompress them, we can't try to compress layers with bzip2. return errors.New("media type for bzip2-compressed layers is not defined") case archive.Xz: // Until the image specs define a media type for xz-compressed layers, even if we know // how to decompress them, we can't try to compress layers with xz. return errors.New("media type for xz-compressed layers is not defined") case archive.Zstd: // Until the image specs define a media type for zstd-compressed layers, even if we know // how to decompress them, we can't try to compress layers with zstd. return errors.New("media type for zstd-compressed layers is not defined") default: logrus.Debugf("compressing %s with unknown compressor(?)", what) } } mb.layerMediaType = dmediaType return nil } func (mb *dockerSchema2ManifestBuilder) buildHistory(extraImageContentDiff string, extraImageContentDiffDigest digest.Digest) error { // Build history notes in the image configuration. appendHistory := func(history []v1.History, empty bool) { for i := range history { var created time.Time if history[i].Created != nil { created = *history[i].Created } dnews := docker.V2S2History{ Created: created, CreatedBy: history[i].CreatedBy, Author: history[i].Author, Comment: history[i].Comment, EmptyLayer: empty, } mb.dimage.History = append(mb.dimage.History, dnews) } } // Keep track of how many entries the base image's history had // before we started adding to it. baseImageHistoryLen := len(mb.dimage.History) // Add history entries for prepended empty layers. appendHistory(mb.i.preEmptyLayers, true) // Add history entries for prepended API-supplied layers. for _, h := range mb.i.preLayers { appendHistory([]v1.History{h.linkedLayer.History}, h.linkedLayer.History.EmptyLayer) } // Add a history entry for this layer, empty or not. created := time.Now().UTC() if mb.i.created != nil { created = (*mb.i.created).UTC() } if !mb.i.omitLayerHistoryEntry { dnews := docker.V2S2History{ Created: created, CreatedBy: mb.i.createdBy, Author: mb.dimage.Author, EmptyLayer: mb.i.emptyLayer, Comment: mb.i.historyComment, } mb.dimage.History = append(mb.dimage.History, dnews) } // Add a history entry for the extra image content if we added a layer for it. // This diff was added to the list of layers before API-supplied layers that // needed to be appended, and we need to keep the order of history entries for // not-empty layers consistent with that. if extraImageContentDiff != "" { createdBy := fmt.Sprintf(`/bin/sh -c #(nop) ADD dir:%s in /",`, extraImageContentDiffDigest.Encoded()) dnews := docker.V2S2History{ Created: created, CreatedBy: createdBy, } mb.dimage.History = append(mb.dimage.History, dnews) } // Add history entries for appended empty layers. appendHistory(mb.i.postEmptyLayers, true) // Add history entries for appended API-supplied layers. for _, h := range mb.i.postLayers { appendHistory([]v1.History{h.linkedLayer.History}, h.linkedLayer.History.EmptyLayer) } // Assemble a comment indicating which base image was used, if it // wasn't just an image ID, and add it to the first history entry we // added, if we indeed added one. if len(mb.dimage.History) > baseImageHistoryLen { var fromComment string if strings.Contains(mb.i.parent, mb.i.fromImageID) && mb.i.fromImageName != "" && !strings.HasPrefix(mb.i.fromImageID, mb.i.fromImageName) { if mb.dimage.History[baseImageHistoryLen].Comment != "" { fromComment = " " } fromComment += "FROM " + mb.i.fromImageName } mb.dimage.History[baseImageHistoryLen].Comment += fromComment } // Confidence check that we didn't just create a mismatch between non-empty layers in the // history and the number of diffIDs. Only applicable if the base image (if there was // one) provided us at least one entry to use as a starting point. if baseImageHistoryLen != 0 { expectedDiffIDs := expectedDockerDiffIDs(mb.dimage) if len(mb.dimage.RootFS.DiffIDs) != expectedDiffIDs { return fmt.Errorf("internal error: history lists %d non-empty layers, but we have %d layers on disk", expectedDiffIDs, len(mb.dimage.RootFS.DiffIDs)) } } return nil } func (mb *dockerSchema2ManifestBuilder) manifestAndConfig() ([]byte, []byte, error) { // Encode the image configuration blob. dconfig, err := json.Marshal(&mb.dimage) if err != nil { return nil, nil, fmt.Errorf("encoding %#v as json: %w", mb.dimage, err) } logrus.Debugf("Docker v2s2 config = %s", dconfig) // Add the configuration blob to the manifest. mb.dmanifest.Config.Digest = digest.Canonical.FromBytes(dconfig) mb.dmanifest.Config.Size = int64(len(dconfig)) mb.dmanifest.Config.MediaType = manifest.DockerV2Schema2ConfigMediaType // Encode the manifest. dmanifestbytes, err := json.Marshal(&mb.dmanifest) if err != nil { return nil, nil, fmt.Errorf("encoding %#v as json: %w", mb.dmanifest, err) } logrus.Debugf("Docker v2s2 manifest = %s", dmanifestbytes) return dmanifestbytes, dconfig, nil } type ociManifestBuilder struct { i *containerImageRef layerMediaType string oimage v1.Image omanifest v1.Manifest } // Build fresh copies of the container configuration structures so that we can edit them // without making unintended changes to the original Builder (OCI manifest). func (i *containerImageRef) newOCIManifestBuilder() (manifestBuilder, error) { created := time.Now().UTC() if i.created != nil { created = *i.created } // Build an empty image, and then decode over it. oimage := v1.Image{} if err := json.Unmarshal(i.oconfig, &oimage); err != nil { return nil, err } // Always replace this value, since we're newer than our base image. oimage.Created = &created // Clear the list of diffIDs, since we always repopulate it. oimage.RootFS.Type = docker.TypeLayers oimage.RootFS.DiffIDs = []digest.Digest{} // Only clear the history if we're squashing, otherwise leave it be so that we can append // entries to it. if i.confidentialWorkload.Convert || i.squash || i.omitHistory { oimage.History = []v1.History{} } // If we were supplied with a configuration, copy fields from it to // matching fields in both formats. if err := config.OverrideOCI(&oimage.Config, i.overrideChanges, i.overrideConfig); err != nil { return nil, fmt.Errorf("applying changes: %w", err) } // If we're producing a confidential workload, override the command and // assorted other settings that aren't expected to work correctly. if i.confidentialWorkload.Convert { oimage.Config.Entrypoint = []string{"/entrypoint"} oimage.Config.Cmd = nil oimage.Config.User = "" oimage.Config.WorkingDir = "" oimage.Config.Volumes = nil oimage.Config.ExposedPorts = nil } // Return partial manifest. The Layers lists will be populated later. annotations := make(map[string]string) maps.Copy(annotations, i.annotations) switch i.createdAnnotation { case types.OptionalBoolFalse: delete(annotations, v1.AnnotationCreated) default: fallthrough case types.OptionalBoolTrue, types.OptionalBoolUndefined: annotations[v1.AnnotationCreated] = created.UTC().Format(time.RFC3339Nano) } for _, k := range i.unsetAnnotations { delete(annotations, k) } for _, kv := range i.setAnnotations { k, v, _ := strings.Cut(kv, "=") annotations[k] = v } return &ociManifestBuilder{ i: i, // The default layer media type assumes no compression. layerMediaType: v1.MediaTypeImageLayer, oimage: oimage, omanifest: v1.Manifest{ Versioned: specs.Versioned{ SchemaVersion: 2, }, MediaType: v1.MediaTypeImageManifest, Config: v1.Descriptor{ MediaType: v1.MediaTypeImageConfig, }, Layers: []v1.Descriptor{}, Annotations: annotations, }, }, nil } func (mb *ociManifestBuilder) addLayer(layerBlobSum digest.Digest, layerBlobSize int64, diffID digest.Digest) { olayerDescriptor := v1.Descriptor{ MediaType: mb.layerMediaType, Digest: layerBlobSum, Size: layerBlobSize, } mb.omanifest.Layers = append(mb.omanifest.Layers, olayerDescriptor) // Note this layer in the list of diffIDs, again using the uncompressed digest. mb.oimage.RootFS.DiffIDs = append(mb.oimage.RootFS.DiffIDs, diffID) } // Compute the media types which we need to attach to a layer, given the type of // compression that we'll be applying. func (mb *ociManifestBuilder) computeLayerMIMEType(what string, layerCompression archive.Compression) error { omediaType := v1.MediaTypeImageLayer if layerCompression != archive.Uncompressed { switch layerCompression { case archive.Gzip: omediaType = v1.MediaTypeImageLayerGzip logrus.Debugf("compressing %s with gzip", what) case archive.Bzip2: // Until the image specs define a media type for bzip2-compressed layers, even if we know // how to decompress them, we can't try to compress layers with bzip2. return errors.New("media type for bzip2-compressed layers is not defined") case archive.Xz: // Until the image specs define a media type for xz-compressed layers, even if we know // how to decompress them, we can't try to compress layers with xz. return errors.New("media type for xz-compressed layers is not defined") case archive.Zstd: omediaType = v1.MediaTypeImageLayerZstd logrus.Debugf("compressing %s with zstd", what) default: logrus.Debugf("compressing %s with unknown compressor(?)", what) } } mb.layerMediaType = omediaType return nil } func (mb *ociManifestBuilder) buildHistory(extraImageContentDiff string, extraImageContentDiffDigest digest.Digest) error { // Build history notes in the image configuration. appendHistory := func(history []v1.History, empty bool) { for i := range history { var created *time.Time if history[i].Created != nil { copiedTimestamp := *history[i].Created created = &copiedTimestamp } onews := v1.History{ Created: created, CreatedBy: history[i].CreatedBy, Author: history[i].Author, Comment: history[i].Comment, EmptyLayer: empty, } mb.oimage.History = append(mb.oimage.History, onews) } } // Keep track of how many entries the base image's history had // before we started adding to it. baseImageHistoryLen := len(mb.oimage.History) // Add history entries for prepended empty layers. appendHistory(mb.i.preEmptyLayers, true) // Add history entries for prepended API-supplied layers. for _, h := range mb.i.preLayers { appendHistory([]v1.History{h.linkedLayer.History}, h.linkedLayer.History.EmptyLayer) } // Add a history entry for this layer, empty or not. created := time.Now().UTC() if mb.i.created != nil { created = (*mb.i.created).UTC() } if !mb.i.omitLayerHistoryEntry { onews := v1.History{ Created: &created, CreatedBy: mb.i.createdBy, Author: mb.oimage.Author, EmptyLayer: mb.i.emptyLayer, Comment: mb.i.historyComment, } mb.oimage.History = append(mb.oimage.History, onews) } // Add a history entry for the extra image content if we added a layer for it. // This diff was added to the list of layers before API-supplied layers that // needed to be appended, and we need to keep the order of history entries for // not-empty layers consistent with that. if extraImageContentDiff != "" { createdBy := fmt.Sprintf(`/bin/sh -c #(nop) ADD dir:%s in /",`, extraImageContentDiffDigest.Encoded()) onews := v1.History{ Created: &created, CreatedBy: createdBy, } mb.oimage.History = append(mb.oimage.History, onews) } // Add history entries for appended empty layers. appendHistory(mb.i.postEmptyLayers, true) // Add history entries for appended API-supplied layers. for _, h := range mb.i.postLayers { appendHistory([]v1.History{h.linkedLayer.History}, h.linkedLayer.History.EmptyLayer) } // Assemble a comment indicating which base image was used, if it // wasn't just an image ID, and add it to the first history entry we // added, if we indeed added one. if len(mb.oimage.History) > baseImageHistoryLen { var fromComment string if strings.Contains(mb.i.parent, mb.i.fromImageID) && mb.i.fromImageName != "" && !strings.HasPrefix(mb.i.fromImageID, mb.i.fromImageName) { if mb.oimage.History[baseImageHistoryLen].Comment != "" { fromComment = " " } fromComment += "FROM " + mb.i.fromImageName } mb.oimage.History[baseImageHistoryLen].Comment += fromComment } // Confidence check that we didn't just create a mismatch between non-empty layers in the // history and the number of diffIDs. Only applicable if the base image (if there was // one) provided us at least one entry to use as a starting point. if baseImageHistoryLen != 0 { expectedDiffIDs := expectedOCIDiffIDs(mb.oimage) if len(mb.oimage.RootFS.DiffIDs) != expectedDiffIDs { return fmt.Errorf("internal error: history lists %d non-empty layers, but we have %d layers on disk", expectedDiffIDs, len(mb.oimage.RootFS.DiffIDs)) } } return nil } func (mb *ociManifestBuilder) manifestAndConfig() ([]byte, []byte, error) { // Encode the image configuration blob. oconfig, err := json.Marshal(&mb.oimage) if err != nil { return nil, nil, fmt.Errorf("encoding %#v as json: %w", mb.oimage, err) } logrus.Debugf("OCIv1 config = %s", oconfig) // Add the configuration blob to the manifest. mb.omanifest.Config.Digest = digest.Canonical.FromBytes(oconfig) mb.omanifest.Config.Size = int64(len(oconfig)) mb.omanifest.Config.MediaType = v1.MediaTypeImageConfig // Encode the manifest. omanifestbytes, err := json.Marshal(&mb.omanifest) if err != nil { return nil, nil, fmt.Errorf("encoding %#v as json: %w", mb.omanifest, err) } logrus.Debugf("OCIv1 manifest = %s", omanifestbytes) return omanifestbytes, oconfig, nil } // filterExclusionsByImage returns a slice of the members of "exclusions" which are present in the image with the specified ID func (i containerImageRef) filterExclusionsByImage(ctx context.Context, exclusions []copier.EnsureParentPath, imageID string) ([]copier.EnsureParentPath, error) { if len(exclusions) == 0 || imageID == "" { return nil, nil } var paths []copier.EnsureParentPath mountPoint, err := i.store.MountImage(imageID, nil, i.mountLabel) cleanup := func() { if _, err := i.store.UnmountImage(imageID, false); err != nil { logrus.Debugf("unmounting image %q: %v", imageID, err) } } if err != nil && errors.Is(err, storage.ErrLayerUnknown) { // if an imagestore is being used, this could be expected if b, err2 := NewBuilder(ctx, i.store, BuilderOptions{ FromImage: imageID, PullPolicy: define.PullNever, ContainerSuffix: "tmp", }); err2 == nil { mountPoint, err = b.Mount(i.mountLabel) cleanup = func() { cid := b.ContainerID if err := b.Delete(); err != nil { logrus.Debugf("unmounting image %q as container %q: %v", imageID, cid, err) } } } } if err != nil { return nil, fmt.Errorf("mounting image %q to examine its contents: %w", imageID, err) } defer cleanup() globs := make([]string, 0, len(exclusions)) for _, exclusion := range exclusions { globs = append(globs, exclusion.Path) } options := copier.StatOptions{} stats, err := copier.Stat(mountPoint, mountPoint, options, globs) if err != nil { return nil, fmt.Errorf("checking for potential exclusion items in image %q: %w", imageID, err) } for _, stat := range stats { for _, exclusion := range exclusions { if stat.Glob != exclusion.Path { continue } for result, stat := range stat.Results { if result != exclusion.Path { continue } if exclusion.ModTime != nil && !exclusion.ModTime.Equal(stat.ModTime) { continue } if exclusion.Mode != nil && *exclusion.Mode != stat.Mode { continue } if exclusion.Owner != nil && (int64(exclusion.Owner.UID) != stat.UID && int64(exclusion.Owner.GID) != stat.GID) { continue } paths = append(paths, exclusion) } } } return paths, nil } func (i *containerImageRef) NewImageSource(ctx context.Context, _ *types.SystemContext) (src types.ImageSource, err error) { // These maps will let us check if a layer ID is part of one group or another. parentLayerIDs := make(map[string]bool) apiLayerIDs := make(map[string]bool) // Start building the list of layers with any prepended layers. layers := []string{} for _, preLayer := range i.preLayers { layers = append(layers, preLayer.layerID) apiLayerIDs[preLayer.layerID] = true } // Now look at the read-write layer, and prepare to work our way back // through all of its parent layers. layerID := i.layerID layer, err := i.store.Layer(layerID) if err != nil { return nil, fmt.Errorf("unable to read layer %q: %w", layerID, err) } // Walk the list of parent layers, prepending each as we go. If we're squashing // or making a confidential workload, we're only producing one layer, so stop at // the layer ID of the top layer, which we won't really be using anyway. for layer != nil { if layerID == i.layerID { // append the layer for this container to the list, // whether it's first or after some prepended layers layers = append(layers, layerID) } else { // prepend this parent layer to the list layers = append(append([]string{}, layerID), layers...) parentLayerIDs[layerID] = true } layerID = layer.Parent if layerID == "" || i.confidentialWorkload.Convert || i.squash { err = nil break } layer, err = i.store.Layer(layerID) if err != nil { return nil, fmt.Errorf("unable to read layer %q: %w", layerID, err) } } layer = nil // If we're slipping in a synthesized layer to hold some files, we need // to add a placeholder for it to the list just after the read-write // layer. Confidential workloads and squashed images will just inline // the files, so we don't need to create a layer in those cases. const synthesizedLayerID = "(synthesized layer)" if len(i.extraImageContent) > 0 && !i.confidentialWorkload.Convert && !i.squash { layers = append(layers, synthesizedLayerID) } // Now add any API-supplied layers we have to append. for _, postLayer := range i.postLayers { layers = append(layers, postLayer.layerID) apiLayerIDs[postLayer.layerID] = true } logrus.Debugf("layer list: %q", layers) // It's simpler from here on to keep track of these as a group. apiLayers := append(slices.Clone(i.preLayers), slices.Clone(i.postLayers)...) // Make a temporary directory to hold blobs. path, err := os.MkdirTemp(tmpdir.GetTempDir(), define.Package) if err != nil { return nil, fmt.Errorf("creating temporary directory to hold layer blobs: %w", err) } logrus.Debugf("using %q to hold temporary data", path) defer func() { if src == nil { err2 := os.RemoveAll(path) if err2 != nil { logrus.Errorf("error removing layer blob directory: %v", err) } } }() // Build fresh copies of the configurations and manifest so that we don't mess with any // values in the Builder object itself. var mb manifestBuilder switch i.preferredManifestType { case v1.MediaTypeImageManifest: mb, err = i.newOCIManifestBuilder() if err != nil { return nil, err } case manifest.DockerV2Schema2MediaType: mb, err = i.newDockerSchema2ManifestBuilder() if err != nil { return nil, err } default: return nil, fmt.Errorf("no supported manifest types (attempted to use %q, only know %q and %q)", i.preferredManifestType, v1.MediaTypeImageManifest, manifest.DockerV2Schema2MediaType) } // Extract each layer and compute its digests, both compressed (if requested) and uncompressed. var extraImageContentDiff string var extraImageContentDiffDigest digest.Digest blobLayers := make(map[digest.Digest]blobLayerInfo) for _, layerID := range layers { what := fmt.Sprintf("layer %q", layerID) if i.confidentialWorkload.Convert || i.squash { what = fmt.Sprintf("container %q", i.containerID) } if layerID == synthesizedLayerID { what = synthesizedLayerID } if apiLayerIDs[layerID] { what = layerID } // Look up this layer. var layerUncompressedDigest digest.Digest var layerUncompressedSize int64 linkedLayerHasLayerID := func(l commitLinkedLayerInfo) bool { return l.layerID == layerID } if apiLayerIDs[layerID] { // API-provided prepended or appended layer apiLayerIndex := slices.IndexFunc(apiLayers, linkedLayerHasLayerID) layerUncompressedDigest = apiLayers[apiLayerIndex].uncompressedDigest layerUncompressedSize = apiLayers[apiLayerIndex].size } else if layerID == synthesizedLayerID { // layer diff consisting of extra files to synthesize into a layer diffFilename, digest, size, err := i.makeExtraImageContentDiff(true, nil) if err != nil { return nil, fmt.Errorf("unable to generate layer for additional content: %w", err) } extraImageContentDiff = diffFilename extraImageContentDiffDigest = digest layerUncompressedDigest = digest layerUncompressedSize = size } else { // "normal" layer layer, err := i.store.Layer(layerID) if err != nil { return nil, fmt.Errorf("unable to locate layer %q: %w", layerID, err) } layerID = layer.ID layerUncompressedDigest = layer.UncompressedDigest layerUncompressedSize = layer.UncompressedSize } // We already know the digest of the contents of parent layers, // so if this is a parent layer, and we know its digest, reuse // its blobsum, diff ID, and size. if !i.confidentialWorkload.Convert && !i.squash && parentLayerIDs[layerID] && layerUncompressedDigest != "" { layerBlobSum := layerUncompressedDigest layerBlobSize := layerUncompressedSize diffID := layerUncompressedDigest // Note this layer in the manifest, using the appropriate blobsum. mb.addLayer(layerBlobSum, layerBlobSize, diffID) blobLayers[diffID] = blobLayerInfo{ ID: layerID, Size: layerBlobSize, } continue } // Figure out if we need to change the media type, in case we've changed the compression. if err := mb.computeLayerMIMEType(what, i.compression); err != nil { return nil, err } // Start reading either the layer or the whole container rootfs. noCompression := archive.Uncompressed diffOptions := &storage.DiffOptions{ Compression: &noCompression, } var rc io.ReadCloser var errChan chan error var layerExclusions []copier.ConditionalRemovePath if i.confidentialWorkload.Convert { // Convert the root filesystem into an encrypted disk image. rc, err = i.extractConfidentialWorkloadFS(i.confidentialWorkload) if err != nil { return nil, err } } else if i.squash { // Extract the root filesystem as a single layer. rc, errChan, err = i.extractRootfs(ExtractRootfsOptions{}) if err != nil { return nil, err } } else { if apiLayerIDs[layerID] { // We're reading an API-supplied blob. apiLayerIndex := slices.IndexFunc(apiLayers, linkedLayerHasLayerID) f, err := os.Open(apiLayers[apiLayerIndex].linkedLayer.BlobPath) if err != nil { return nil, fmt.Errorf("opening layer blob for %s: %w", layerID, err) } rc = f } else if layerID == synthesizedLayerID { // Slip in additional content as an additional layer. if rc, err = os.Open(extraImageContentDiff); err != nil { return nil, err } } else { // If we're up to the final layer, but we don't want to // include a diff for it, we're done. if i.emptyLayer && layerID == i.layerID { continue } if layerID == i.layerID { // We need to filter out any mount targets that we created. layerExclusions = append(slices.Clone(i.layerExclusions), i.layerMountTargets...) // And we _might_ need to filter out directories that modified // by creating and removing mount targets, _if_ they were the // same in the base image for this stage. layerPullUps, err := i.filterExclusionsByImage(ctx, i.layerPullUps, i.fromImageID) if err != nil { return nil, fmt.Errorf("checking which exclusions are in base image %q: %w", i.fromImageID, err) } layerExclusions = append(layerExclusions, layerPullUps...) } // Extract this layer, one of possibly many. rc, err = i.store.Diff("", layerID, diffOptions) if err != nil { return nil, fmt.Errorf("extracting %s: %w", what, err) } } } srcHasher := digest.Canonical.Digester() // Set up to write the possibly-recompressed blob. layerFile, err := os.OpenFile(filepath.Join(path, "layer"), os.O_CREATE|os.O_WRONLY, 0o600) if err != nil { rc.Close() return nil, fmt.Errorf("opening file for %s: %w", what, err) } counter := ioutils.NewWriteCounter(layerFile) var destHasher digest.Digester var multiWriter io.Writer // Avoid rehashing when we compress or mess with the layer contents somehow. // At this point, there are multiple ways that can happen. diffBeingAltered := i.compression != archive.Uncompressed diffBeingAltered = diffBeingAltered || i.layerModTime != nil || i.layerLatestModTime != nil diffBeingAltered = diffBeingAltered || len(layerExclusions) != 0 diffBeingAltered = diffBeingAltered || i.os == "windows" if diffBeingAltered { destHasher = digest.Canonical.Digester() multiWriter = io.MultiWriter(counter, destHasher.Hash()) } else { destHasher = srcHasher multiWriter = counter } // Compress the layer, if we're recompressing it. writeCloser, err := archive.CompressStream(multiWriter, i.compression) if err != nil { layerFile.Close() rc.Close() return nil, fmt.Errorf("compressing %s: %w", what, err) } writer := io.MultiWriter(writeCloser, srcHasher.Hash()) // Use specified timestamps in the layer, if we're doing that for history // entries. nestedWriteCloser := ioutils.NewWriteCloserWrapper(writer, writeCloser.Close) writeCloser, err = makeFilteredLayerWriteCloser(nestedWriteCloser, i.layerModTime, i.layerLatestModTime, layerExclusions, i.os == "windows") if err != nil { return nil, fmt.Errorf("creating filter write closer %s: %w", what, err) } writer = writeCloser // Okay, copy from the raw diff through the filter, compressor, and counter and // digesters. size, err := io.Copy(writer, rc) if err != nil { writeCloser.Close() layerFile.Close() rc.Close() return nil, fmt.Errorf("storing %s to file: on copy: %w", what, err) } if err := writeCloser.Close(); err != nil { layerFile.Close() rc.Close() return nil, fmt.Errorf("storing %s to file: on pipe close: %w", what, err) } if err := layerFile.Close(); err != nil { rc.Close() return nil, fmt.Errorf("storing %s to file: on file close: %w", what, err) } rc.Close() if errChan != nil { err = <-errChan if err != nil { return nil, fmt.Errorf("extracting container rootfs: %w", err) } } if err != nil { return nil, fmt.Errorf("storing %s to file: %w", what, err) } if diffBeingAltered { size = counter.Count } else { if size != counter.Count { return nil, fmt.Errorf("storing %s to file: inconsistent layer size (copied %d, wrote %d)", what, size, counter.Count) } } logrus.Debugf("%s size is %d bytes, uncompressed digest %s, possibly-compressed digest %s", what, size, srcHasher.Digest().String(), destHasher.Digest().String()) // Rename the layer so that we can more easily find it by digest later. finalBlobName := filepath.Join(path, destHasher.Digest().String()) if err = os.Rename(filepath.Join(path, "layer"), finalBlobName); err != nil { return nil, fmt.Errorf("storing %s to file while renaming %q to %q: %w", what, filepath.Join(path, "layer"), finalBlobName, err) } mb.addLayer(destHasher.Digest(), size, srcHasher.Digest()) } // Only attempt to append history if history was not disabled explicitly. if !i.omitHistory { if err := mb.buildHistory(extraImageContentDiff, extraImageContentDiffDigest); err != nil { return nil, err } } imageManifest, config, err := mb.manifestAndConfig() if err != nil { return nil, err } src = &containerImageSource{ path: path, ref: i, store: i.store, containerID: i.containerID, mountLabel: i.mountLabel, layerID: i.layerID, names: i.names, compression: i.compression, config: config, configDigest: digest.Canonical.FromBytes(config), manifest: imageManifest, manifestType: i.preferredManifestType, blobDirectory: i.blobDirectory, blobLayers: blobLayers, } return src, nil } func (i *containerImageRef) NewImageDestination(_ context.Context, _ *types.SystemContext) (types.ImageDestination, error) { return nil, errors.New("can't write to a container") } func (i *containerImageRef) DockerReference() reference.Named { return i.name } func (i *containerImageRef) StringWithinTransport() string { if len(i.names) > 0 { return i.names[0] } return "" } func (i *containerImageRef) DeleteImage(context.Context, *types.SystemContext) error { // we were never here return nil } func (i *containerImageRef) PolicyConfigurationIdentity() string { return "" } func (i *containerImageRef) PolicyConfigurationNamespaces() []string { return nil } func (i *containerImageRef) Transport() types.ImageTransport { return is.Transport } func (i *containerImageSource) Close() error { err := os.RemoveAll(i.path) if err != nil { return fmt.Errorf("removing layer blob directory: %w", err) } return nil } func (i *containerImageSource) Reference() types.ImageReference { return i.ref } func (i *containerImageSource) GetSignatures(_ context.Context, _ *digest.Digest) ([][]byte, error) { return nil, nil } func (i *containerImageSource) GetManifest(_ context.Context, _ *digest.Digest) ([]byte, string, error) { return i.manifest, i.manifestType, nil } func (i *containerImageSource) LayerInfosForCopy(_ context.Context, _ *digest.Digest) ([]types.BlobInfo, error) { return nil, nil } func (i *containerImageSource) HasThreadSafeGetBlob() bool { return false } func (i *containerImageSource) GetBlob(_ context.Context, blob types.BlobInfo, _ types.BlobInfoCache) (reader io.ReadCloser, size int64, err error) { if blob.Digest == i.configDigest { logrus.Debugf("start reading config") reader := bytes.NewReader(i.config) closer := func() error { logrus.Debugf("finished reading config") return nil } return ioutils.NewReadCloserWrapper(reader, closer), reader.Size(), nil } var layerReadCloser io.ReadCloser size = -1 if blobLayerInfo, ok := i.blobLayers[blob.Digest]; ok { noCompression := archive.Uncompressed diffOptions := &storage.DiffOptions{ Compression: &noCompression, } layerReadCloser, err = i.store.Diff("", blobLayerInfo.ID, diffOptions) size = blobLayerInfo.Size } else { for _, blobDir := range []string{i.blobDirectory, i.path} { var layerFile *os.File layerFile, err = os.OpenFile(filepath.Join(blobDir, blob.Digest.String()), os.O_RDONLY, 0o600) if err == nil { st, err := layerFile.Stat() if err != nil { logrus.Warnf("error reading size of layer file %q: %v", blob.Digest.String(), err) } else { size = st.Size() layerReadCloser = layerFile break } layerFile.Close() } if !errors.Is(err, os.ErrNotExist) { logrus.Debugf("error checking for layer %q in %q: %v", blob.Digest.String(), blobDir, err) } } } if err != nil || layerReadCloser == nil || size == -1 { logrus.Debugf("error reading layer %q: %v", blob.Digest.String(), err) return nil, -1, fmt.Errorf("opening layer blob: %w", err) } logrus.Debugf("reading layer %q", blob.Digest.String()) closer := func() error { logrus.Debugf("finished reading layer %q", blob.Digest.String()) if err := layerReadCloser.Close(); err != nil { return fmt.Errorf("closing layer %q after reading: %w", blob.Digest.String(), err) } return nil } return ioutils.NewReadCloserWrapper(layerReadCloser, closer), size, nil } // makeExtraImageContentDiff creates an archive file containing the contents of // files named in i.extraImageContent. The footer that marks the end of the // archive may be omitted. func (i *containerImageRef) makeExtraImageContentDiff(includeFooter bool, timestamp *time.Time) (_ string, _ digest.Digest, _ int64, retErr error) { cdir, err := i.store.ContainerDirectory(i.containerID) if err != nil { return "", "", -1, err } diff, err := os.CreateTemp(cdir, "extradiff") if err != nil { return "", "", -1, err } defer diff.Close() defer func() { if retErr != nil { os.Remove(diff.Name()) } }() digester := digest.Canonical.Digester() counter := ioutils.NewWriteCounter(digester.Hash()) tw := tar.NewWriter(io.MultiWriter(diff, counter)) if timestamp == nil { now := time.Now() timestamp = &now if i.created != nil { timestamp = i.created } } for path, contents := range i.extraImageContent { if err := func() error { content, err := os.Open(contents) if err != nil { return err } defer content.Close() st, err := content.Stat() if err != nil { return err } if err := tw.WriteHeader(&tar.Header{ Name: path, Typeflag: tar.TypeReg, Mode: 0o644, ModTime: *timestamp, Size: st.Size(), }); err != nil { return fmt.Errorf("writing header for %q: %w", path, err) } if _, err := io.Copy(tw, content); err != nil { return fmt.Errorf("writing content for %q: %w", path, err) } if err := tw.Flush(); err != nil { return fmt.Errorf("flushing content for %q: %w", path, err) } return nil }(); err != nil { return "", "", -1, fmt.Errorf("writing %q to prepend-to-archive blob: %w", contents, err) } } if includeFooter { if err = tw.Close(); err != nil { return "", "", -1, fmt.Errorf("closingprepend-to-archive blob after final write: %w", err) } } else { if err = tw.Flush(); err != nil { return "", "", -1, fmt.Errorf("flushing prepend-to-archive blob after final write: %w", err) } } return diff.Name(), digester.Digest(), counter.Count, nil } // makeFilteredLayerWriteCloser returns either the passed-in WriteCloser, or if // layerModeTime or layerLatestModTime are set, a WriteCloser which modifies // the tarball that's written to it so that timestamps in headers are set to // layerModTime exactly (if a value is provided for it), and then clamped to be // no later than layerLatestModTime (if a value is provided for it). // This implies that if both values are provided, the archive's timestamps will // be set to the earlier of the two values. func makeFilteredLayerWriteCloser(wc io.WriteCloser, layerModTime, layerLatestModTime *time.Time, exclusions []copier.ConditionalRemovePath, windows bool) (io.WriteCloser, error) { if layerModTime == nil && layerLatestModTime == nil && len(exclusions) == 0 && !windows { return wc, nil } exclusionsMap := make(map[string]copier.ConditionalRemovePath) for _, exclusionSpec := range exclusions { pathSpec := strings.Trim(path.Clean(exclusionSpec.Path), "/") if pathSpec == "" { continue } exclusionsMap[pathSpec] = exclusionSpec } var initialized bool wc = newTarFilterer(wc, func(hdr *tar.Header) (skip, replaceContents bool, replacementContents io.Reader) { modTime := hdr.ModTime if layerModTime != nil || layerLatestModTime != nil || len(exclusions) != 0 { // Changing a zeroed field to a non-zero field can affect the // format that the library uses for writing the header, so only // change fields that are already set to avoid changing the // format (and as a result, changing the length) of the header // that we write. nameSpec := strings.Trim(path.Clean(hdr.Name), "/") if conditions, ok := exclusionsMap[nameSpec]; ok { if (conditions.ModTime == nil || conditions.ModTime.Equal(modTime)) && (conditions.Owner == nil || (conditions.Owner.UID == hdr.Uid && conditions.Owner.GID == hdr.Gid)) && (conditions.Mode == nil || (*conditions.Mode&os.ModePerm == os.FileMode(hdr.Mode)&os.ModePerm)) { return true, false, nil } } } if layerModTime != nil { modTime = *layerModTime } if layerLatestModTime != nil && layerLatestModTime.Before(modTime) { modTime = *layerLatestModTime } if !hdr.ModTime.IsZero() { hdr.ModTime = modTime } if !hdr.AccessTime.IsZero() { hdr.AccessTime = modTime } if !hdr.ChangeTime.IsZero() { hdr.ChangeTime = modTime } if windows { isDir := hdr.Typeflag == tar.TypeDir if initialized { hdr.Name = path.Join("Files", hdr.Name) if isDir { hdr.Name += "/" } if hdr.Linkname != "" { hdr.Linkname = path.Join("Files", hdr.Linkname) if isDir { hdr.Linkname += "/" } } } // ensure all header fields are set in a way that is expected for Windows images if hdr.PAXRecords == nil { hdr.PAXRecords = map[string]string{} } hdr.Format = tar.FormatPAX if isDir { hdr.Mode |= syscall.S_IFDIR hdr.PAXRecords[keyFileAttr] = fileAttributeDirectory hdr.PAXRecords[keySDRaw] = winSecurityDescriptorDirectory } else if hdr.Typeflag == tar.TypeReg { hdr.Mode |= syscall.S_IFREG hdr.PAXRecords[keyFileAttr] = fileAttributeArchive hdr.PAXRecords[keySDRaw] = winSecurityDescriptorFile } if !hdr.ModTime.IsZero() { hdr.PAXRecords[keyCreationTime] = fmt.Sprintf("%d.%09d", hdr.ModTime.Unix(), hdr.ModTime.Nanosecond()) } } return false, false, nil }) if windows { // prep the archive by writing the Files/ and Hives/ directories to the writer. // This fulfills a requirement of Windows images winTarWriter := tar.NewWriter(wc) now := time.Now() h := &tar.Header{ Name: "Hives/", Typeflag: tar.TypeDir, ModTime: now, Mode: 0o755, } if err := winTarWriter.WriteHeader(h); err != nil { return nil, err } h = &tar.Header{ Name: "Files/", Typeflag: tar.TypeDir, ModTime: now, Mode: 0o755, } if err := winTarWriter.WriteHeader(h); err != nil { return nil, err } // As we plan to continue to add to the archive, Flush() needs to be used, rather than Close(). if err := winTarWriter.Flush(); err != nil { return nil, err } } initialized = true return wc, nil } // makeLinkedLayerInfos calculates the size and digest information for a layer // we intend to add to the image that we're committing. func (b *Builder) makeLinkedLayerInfos(layers []LinkedLayer, layerType string, layerModTime, layerLatestModTime *time.Time) ([]commitLinkedLayerInfo, error) { if layers == nil { return nil, nil } infos := make([]commitLinkedLayerInfo, 0, len(layers)) for i, layer := range layers { // complain if EmptyLayer and "is the BlobPath empty" don't agree if layer.History.EmptyLayer != (layer.BlobPath == "") { return nil, fmt.Errorf("internal error: layer-is-empty = %v, but content path is %q", layer.History.EmptyLayer, layer.BlobPath) } // if there's no layer contents, we're done with this one if layer.History.EmptyLayer { continue } // check if it's a directory or a non-directory st, err := os.Stat(layer.BlobPath) if err != nil { return nil, fmt.Errorf("checking if layer content %s is a directory: %w", layer.BlobPath, err) } info := commitLinkedLayerInfo{ layerID: fmt.Sprintf("(%s %d)", layerType, i+1), linkedLayer: layer, } if err = func() error { cdir, err := b.store.ContainerDirectory(b.ContainerID) if err != nil { return fmt.Errorf("determining directory for working container: %w", err) } f, err := os.CreateTemp(cdir, "") if err != nil { return fmt.Errorf("creating temporary file to hold blob for %q: %w", info.linkedLayer.BlobPath, err) } defer f.Close() var rc io.ReadCloser var what string if st.IsDir() { // if it's a directory, archive it and digest the archive while we're storing a copy somewhere what = "directory" rc, err = chrootarchive.Tar(info.linkedLayer.BlobPath, nil, info.linkedLayer.BlobPath) if err != nil { return fmt.Errorf("generating a layer blob from %q: %w", info.linkedLayer.BlobPath, err) } } else { what = "file" // if it's not a directory, just digest it while we're storing a copy somewhere rc, err = os.Open(info.linkedLayer.BlobPath) if err != nil { return err } } digester := digest.Canonical.Digester() sizeCountedFile := ioutils.NewWriteCounter(io.MultiWriter(digester.Hash(), f)) wc, err := makeFilteredLayerWriteCloser(ioutils.NopWriteCloser(sizeCountedFile), layerModTime, layerLatestModTime, nil, false) if err != nil { return err } _, copyErr := io.Copy(wc, rc) wcErr := wc.Close() if err := rc.Close(); err != nil { return fmt.Errorf("storing a copy of %s %q: closing reader: %w", what, info.linkedLayer.BlobPath, err) } if copyErr != nil { return fmt.Errorf("storing a copy of %s %q: copying data: %w", what, info.linkedLayer.BlobPath, copyErr) } if wcErr != nil { return fmt.Errorf("storing a copy of %s %q: closing writer: %w", what, info.linkedLayer.BlobPath, wcErr) } info.uncompressedDigest = digester.Digest() info.size = sizeCountedFile.Count info.linkedLayer.BlobPath = f.Name() return nil }(); err != nil { return nil, err } infos = append(infos, info) } return infos, nil } // makeContainerImageRef creates a containers/image/v5/types.ImageReference // which is mainly used for representing the working container as a source // image that can be copied, which is how we commit the container to create the // image. func (b *Builder) makeContainerImageRef(options CommitOptions) (*containerImageRef, error) { if (len(options.PrependedLinkedLayers) > 0 || len(options.AppendedLinkedLayers) > 0) && (options.ConfidentialWorkloadOptions.Convert || options.Squash) { return nil, errors.New("can't add prebuilt layers and produce an image with only one layer, at the same time") } var name reference.Named container, err := b.store.Container(b.ContainerID) if err != nil { return nil, fmt.Errorf("locating container %q: %w", b.ContainerID, err) } if len(container.Names) > 0 { if parsed, err2 := reference.ParseNamed(container.Names[0]); err2 == nil { name = parsed } } cdir, err := b.store.ContainerDirectory(b.ContainerID) if err != nil { return nil, fmt.Errorf("getting the per-container data directory for %q: %w", b.ContainerID, err) } gatherExclusions := func(excludesFiles []string) ([]copier.ConditionalRemovePath, error) { var excludes []copier.ConditionalRemovePath for _, excludesFile := range excludesFiles { if strings.Contains(excludesFile, containerExcludesSubstring) { continue } excludesData, err := os.ReadFile(excludesFile) if err != nil { return nil, fmt.Errorf("reading commit exclusions for %q: %w", b.ContainerID, err) } var theseExcludes []copier.ConditionalRemovePath if err := json.Unmarshal(excludesData, &theseExcludes); err != nil { return nil, fmt.Errorf("parsing commit exclusions for %q: %w", b.ContainerID, err) } excludes = append(excludes, theseExcludes...) } return excludes, nil } mountTargetFiles, err := filepath.Glob(filepath.Join(cdir, containerExcludesDir, "*")) if err != nil { return nil, fmt.Errorf("checking for commit exclusions for %q: %w", b.ContainerID, err) } pulledUpFiles, err := filepath.Glob(filepath.Join(cdir, containerPulledUpDir, "*")) if err != nil { return nil, fmt.Errorf("checking for commit pulled-up items for %q: %w", b.ContainerID, err) } layerMountTargets, err := gatherExclusions(mountTargetFiles) if err != nil { return nil, err } if len(layerMountTargets) > 0 { logrus.Debugf("these items were created for use as mount targets: %#v", layerMountTargets) } layerPullUps, err := gatherExclusions(pulledUpFiles) if err != nil { return nil, err } if len(layerPullUps) > 0 { logrus.Debugf("these items appear to have been pulled up: %#v", layerPullUps) } var layerExclusions []copier.ConditionalRemovePath if options.CompatLayerOmissions == types.OptionalBoolTrue { layerExclusions = slices.Clone(compatLayerExclusions) } if len(layerExclusions) > 0 { logrus.Debugf("excluding these items from committed layer: %#v", layerExclusions) } manifestType := options.PreferredManifestType if manifestType == "" { manifestType = define.OCIv1ImageManifest } for _, u := range options.UnsetEnvs { b.UnsetEnv(u) } oconfig, err := json.Marshal(&b.OCIv1) if err != nil { return nil, fmt.Errorf("encoding OCI-format image configuration %#v: %w", b.OCIv1, err) } dconfig, err := json.Marshal(&b.Docker) if err != nil { return nil, fmt.Errorf("encoding docker-format image configuration %#v: %w", b.Docker, err) } var created, layerModTime, layerLatestModTime *time.Time if options.HistoryTimestamp != nil { historyTimestampUTC := options.HistoryTimestamp.UTC() created = &historyTimestampUTC layerModTime = &historyTimestampUTC } if options.SourceDateEpoch != nil { sourceDateEpochUTC := options.SourceDateEpoch.UTC() created = &sourceDateEpochUTC if options.RewriteTimestamp { layerLatestModTime = &sourceDateEpochUTC } } createdBy := b.CreatedBy() if createdBy == "" { createdBy = strings.Join(b.Shell(), " ") if createdBy == "" { createdBy = "/bin/sh" } } parent := "" forceOmitHistory := false if b.FromImageID != "" { parentDigest := digest.NewDigestFromEncoded(digest.Canonical, b.FromImageID) if parentDigest.Validate() == nil { parent = parentDigest.String() } if !options.OmitHistory && len(b.OCIv1.History) == 0 && len(b.OCIv1.RootFS.DiffIDs) != 0 { // Parent had layers, but no history. We shouldn't confuse // our own confidence checks by adding history for layers // that we're adding, creating an image with multiple layers, // only some of which have history entries, which would be // broken in confusing ways. b.Logger.Debugf("parent image %q had no history but had %d layers, assuming OmitHistory", b.FromImageID, len(b.OCIv1.RootFS.DiffIDs)) forceOmitHistory = true } } preLayerInfos, err := b.makeLinkedLayerInfos(append(slices.Clone(b.PrependedLinkedLayers), slices.Clone(options.PrependedLinkedLayers)...), "prepended layer", layerModTime, layerLatestModTime) if err != nil { return nil, err } postLayerInfos, err := b.makeLinkedLayerInfos(append(slices.Clone(options.AppendedLinkedLayers), slices.Clone(b.AppendedLinkedLayers)...), "appended layer", layerModTime, layerLatestModTime) if err != nil { return nil, err } ref := &containerImageRef{ fromImageName: b.FromImage, fromImageID: b.FromImageID, store: b.store, compression: options.Compression, name: name, names: container.Names, containerID: container.ID, mountLabel: b.MountLabel, layerID: container.LayerID, oconfig: oconfig, dconfig: dconfig, created: created, createdBy: createdBy, layerModTime: layerModTime, layerLatestModTime: layerLatestModTime, historyComment: b.HistoryComment(), annotations: b.Annotations(), setAnnotations: slices.Clone(options.Annotations), unsetAnnotations: slices.Clone(options.UnsetAnnotations), preferredManifestType: manifestType, squash: options.Squash, confidentialWorkload: options.ConfidentialWorkloadOptions, omitHistory: options.OmitHistory || forceOmitHistory, emptyLayer: (options.EmptyLayer || options.OmitLayerHistoryEntry) && !options.Squash && !options.ConfidentialWorkloadOptions.Convert, omitLayerHistoryEntry: options.OmitLayerHistoryEntry && !options.Squash && !options.ConfidentialWorkloadOptions.Convert, idMappingOptions: &b.IDMappingOptions, parent: parent, blobDirectory: options.BlobDirectory, preEmptyLayers: slices.Clone(b.PrependedEmptyLayers), preLayers: preLayerInfos, postEmptyLayers: slices.Clone(b.AppendedEmptyLayers), postLayers: postLayerInfos, overrideChanges: options.OverrideChanges, overrideConfig: options.OverrideConfig, extraImageContent: maps.Clone(options.ExtraImageContent), compatSetParent: options.CompatSetParent, layerExclusions: layerExclusions, layerMountTargets: layerMountTargets, layerPullUps: layerPullUps, createdAnnotation: options.CreatedAnnotation, os: b.OCIv1.OS, } if ref.created != nil { for i := range ref.preEmptyLayers { ref.preEmptyLayers[i].Created = ref.created } for i := range ref.preLayers { ref.preLayers[i].linkedLayer.History.Created = ref.created } for i := range ref.postEmptyLayers { ref.postEmptyLayers[i].Created = ref.created } for i := range ref.postLayers { ref.postLayers[i].linkedLayer.History.Created = ref.created } } return ref, nil } // Extract the container's whole filesystem as if it were a single layer from current builder instance func (b *Builder) ExtractRootfs(options CommitOptions, opts ExtractRootfsOptions) (io.ReadCloser, chan error, error) { src, err := b.makeContainerImageRef(options) if err != nil { return nil, nil, fmt.Errorf("creating image reference for container %q to extract its contents: %w", b.ContainerID, err) } return src.extractRootfs(opts) } ================================================ FILE: imagebuildah/build.go ================================================ package imagebuildah import ( "bytes" "context" "errors" "fmt" gotypes "go/types" "io" "maps" "net/http" "os" "os/exec" "path/filepath" "runtime" "slices" "strconv" "strings" "sync" "time" "github.com/containerd/platforms" "github.com/containers/buildah" "github.com/containers/buildah/define" "github.com/containers/buildah/internal" internalUtil "github.com/containers/buildah/internal/util" "github.com/containers/buildah/pkg/parse" "github.com/containers/buildah/util" "github.com/hashicorp/go-multierror" "github.com/mattn/go-shellwords" v1 "github.com/opencontainers/image-spec/specs-go/v1" specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/openshift/imagebuilder" "github.com/sirupsen/logrus" "go.podman.io/common/libimage" "go.podman.io/common/pkg/config" "go.podman.io/image/v5/docker" "go.podman.io/image/v5/docker/reference" "go.podman.io/image/v5/image" "go.podman.io/image/v5/manifest" "go.podman.io/image/v5/pkg/shortnames" istorage "go.podman.io/image/v5/storage" "go.podman.io/image/v5/types" "go.podman.io/storage" "go.podman.io/storage/pkg/archive" "golang.org/x/sync/semaphore" ) const ( PullIfMissing = define.PullIfMissing PullAlways = define.PullAlways PullIfNewer = define.PullIfNewer PullNever = define.PullNever Gzip = archive.Gzip Bzip2 = archive.Bzip2 Xz = archive.Xz Zstd = archive.Zstd Uncompressed = archive.Uncompressed ) // Mount is a mountpoint for the build container. type Mount = specs.Mount type BuildOptions = define.BuildOptions // BuildDockerfiles parses a set of one or more Dockerfiles (which may be // URLs), creates one or more new Executors, and then runs // Prepare/Execute/Commit/Delete over the entire set of instructions. // If the Manifest option is set, returns the ID of the manifest list, else it // returns the ID of the built image, and if a name was assigned to it, a // canonical reference for that image. func BuildDockerfiles(ctx context.Context, store storage.Store, options define.BuildOptions, paths ...string) (id string, ref reference.Canonical, err error) { if options.CommonBuildOpts == nil { options.CommonBuildOpts = &define.CommonBuildOptions{} } if options.Args == nil { options.Args = make(map[string]string) } if err := parse.Volumes(options.CommonBuildOpts.Volumes); err != nil { return "", nil, fmt.Errorf("validating volumes: %w", err) } if len(paths) == 0 { return "", nil, errors.New("building: no dockerfiles specified") } if len(options.Platforms) > 1 { if options.IIDFile != "" { return "", nil, fmt.Errorf("building multiple images, but iidfile %q can only be used to store one image ID", options.IIDFile) } if options.IIDFileRaw != "" { return "", nil, fmt.Errorf("building multiple images, but iidfile-raw %q can only be used to store one image ID", options.IIDFileRaw) } if options.MetadataFile != "" { return "", nil, fmt.Errorf("building multiple images, but metadata file %q can only be used to store information about one image", options.MetadataFile) } } logger := logrus.New() if options.Err != nil { logger.SetOutput(options.Err) } else { logger.SetOutput(os.Stderr) } logger.SetLevel(logrus.GetLevel()) var dockerfiles []io.Reader for _, tag := range append([]string{options.Output}, options.AdditionalTags...) { if tag == "" { continue } if _, err := util.VerifyTagName(tag); err != nil { return "", nil, fmt.Errorf("tag %s: %w", tag, err) } } for _, dfile := range paths { var data io.Reader if strings.HasPrefix(dfile, "http://") || strings.HasPrefix(dfile, "https://") { logger.Debugf("reading remote Dockerfile %q", dfile) resp, err := http.Get(dfile) if err != nil { return "", nil, err } defer resp.Body.Close() if resp.ContentLength == 0 { return "", nil, fmt.Errorf("no contents in %q", dfile) } data = resp.Body } else { dinfo, err := os.Stat(dfile) if err != nil { // If the Dockerfile isn't available, try again with // context directory prepended (if not prepended yet). if !strings.HasPrefix(dfile, options.ContextDirectory) { dfile = filepath.Join(options.ContextDirectory, dfile) dinfo, err = os.Stat(dfile) } } if err != nil { return "", nil, err } var contents *os.File // If given a directory error out since `-f` does not supports path to directory if dinfo.Mode().IsDir() { return "", nil, fmt.Errorf("containerfile: %q cannot be path to a directory", dfile) } contents, err = os.Open(dfile) if err != nil { return "", nil, fmt.Errorf("reading build instructions: %w", err) } defer contents.Close() dinfo, err = contents.Stat() if err != nil { return "", nil, fmt.Errorf("reading info about %q: %w", dfile, err) } if dinfo.Mode().IsRegular() && dinfo.Size() == 0 { return "", nil, fmt.Errorf("no contents in %q", dfile) } data = contents } // pre-process Dockerfiles with ".in" suffix if strings.HasSuffix(dfile, ".in") { pData, err := preprocessContainerfileContents(logger, dfile, data, options.ContextDirectory, options.CPPFlags) if err != nil { return "", nil, err } data = pData } dockerfiles = append(dockerfiles, data) } var files [][]byte for _, dockerfile := range dockerfiles { var b bytes.Buffer if _, err := b.ReadFrom(dockerfile); err != nil { return "", nil, err } files = append(files, b.Bytes()) } if options.JobSemaphore == nil { if options.Jobs != nil { if *options.Jobs < 0 { return "", nil, errors.New("building: invalid value for jobs. It must be a positive integer") } if *options.Jobs > 0 { options.JobSemaphore = semaphore.NewWeighted(int64(*options.Jobs)) } } else { options.JobSemaphore = semaphore.NewWeighted(1) } } manifestList := options.Manifest options.Manifest = "" type instance struct { v1.Platform ID string Ref reference.Canonical } var instances []instance var instancesLock sync.Mutex var builds multierror.Group if options.SystemContext == nil { options.SystemContext = &types.SystemContext{} } if options.AdditionalBuildContexts == nil { options.AdditionalBuildContexts = make(map[string]*define.AdditionalBuildContext) } if len(options.Platforms) == 0 { options.Platforms = append(options.Platforms, struct{ OS, Arch, Variant string }{ OS: options.SystemContext.OSChoice, Arch: options.SystemContext.ArchitectureChoice, }) } if options.AllPlatforms { options.Platforms, err = platformsForBaseImages(ctx, logger, paths, files, options.From, options.Args, options.AdditionalBuildContexts, options.SystemContext) if err != nil { return "", nil, err } } if sourceDateEpoch, ok := options.Args[internal.SourceDateEpochName]; ok && options.SourceDateEpoch == nil { sde, err := strconv.ParseInt(sourceDateEpoch, 10, 64) if err != nil { return "", nil, fmt.Errorf("parsing SOURCE_DATE_EPOCH build-arg %q: %w", sourceDateEpoch, err) } sdeTime := time.Unix(sde, 0) options.SourceDateEpoch = &sdeTime } systemContext := options.SystemContext for _, platform := range options.Platforms { platformContext := *systemContext if platform.OS == "" && platform.Arch != "" { platform.OS = runtime.GOOS } if platform.OS == "" && platform.Arch == "" { if targetPlatform, ok := options.Args["TARGETPLATFORM"]; ok { targetPlatform, err := platforms.Parse(targetPlatform) if err != nil { return "", nil, fmt.Errorf("parsing TARGETPLATFORM value %q: %w", targetPlatform, err) } platform.OS = targetPlatform.OS platform.Arch = targetPlatform.Architecture platform.Variant = targetPlatform.Variant } } platformSpec := internalUtil.NormalizePlatform(v1.Platform{ OS: platform.OS, Architecture: platform.Arch, Variant: platform.Variant, }) // internalUtil.NormalizePlatform converts an empty os value to GOOS // so we have to check the original value here to not overwrite the default for no reason if platform.OS != "" { platformContext.OSChoice = platformSpec.OS } if platform.Arch != "" { platformContext.ArchitectureChoice = platformSpec.Architecture platformContext.VariantChoice = platformSpec.Variant } platformOptions := options platformOptions.SystemContext = &platformContext platformOptions.OS = platformContext.OSChoice platformOptions.Architecture = platformContext.ArchitectureChoice logPrefix := "" if len(options.Platforms) > 1 { logPrefix = "[" + platforms.Format(platformSpec) + "] " } // Deep copy args to prevent concurrent read/writes over Args. platformOptions.Args = maps.Clone(options.Args) if options.SourceDateEpoch != nil { if options.Timestamp != nil { return "", nil, errors.New("timestamp and source-date-epoch would be ambiguous if allowed together") } if _, alreadySet := platformOptions.Args[internal.SourceDateEpochName]; !alreadySet { platformOptions.Args[internal.SourceDateEpochName] = fmt.Sprintf("%d", options.SourceDateEpoch.Unix()) } } builds.Go(func() error { contextDirectory, processLabel, mountLabel, usingContextOverlay, cleanupOverlay, err := platformSetupContextDirectoryOverlay(store, &options) if err != nil { return fmt.Errorf("mounting an overlay over build context directory: %w", err) } defer cleanupOverlay() platformOptions.ContextDirectory = contextDirectory loggerPerPlatform := logger if platformOptions.LogFile != "" && platformOptions.LogSplitByPlatform { logFile := platformOptions.LogFile + "_" + platformOptions.OS + "_" + platformOptions.Architecture f, err := os.OpenFile(logFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600) if err != nil { return fmt.Errorf("opening logfile: %q: %w", logFile, err) } defer f.Close() loggerPerPlatform = logrus.New() loggerPerPlatform.SetOutput(f) loggerPerPlatform.SetLevel(logrus.GetLevel()) stdout := f stderr := f reporter := f platformOptions.Out = stdout platformOptions.ReportWriter = reporter platformOptions.Err = stderr } thisID, thisRef, err := buildDockerfilesOnce(ctx, store, loggerPerPlatform, logPrefix, platformOptions, paths, files, processLabel, mountLabel, usingContextOverlay) if err != nil { if errorContext := strings.TrimSpace(logPrefix); errorContext != "" { return fmt.Errorf("%s: %w", errorContext, err) } return err } instancesLock.Lock() instances = append(instances, instance{ ID: thisID, Ref: thisRef, Platform: platformSpec, }) instancesLock.Unlock() return nil }) } if merr := builds.Wait(); merr != nil { if merr.Len() == 1 { return "", nil, merr.Errors[0] } return "", nil, merr.ErrorOrNil() } // Reasons for this id, ref assignment w.r.t to use-case: // // * Single-platform build: On single platform build we only // have one built instance i.e on indice 0 of built instances, // so assign that. // // * Multi-platform build with manifestList: If this is a build for // multiple platforms ( more than one platform ) and --manifest // option then this assignment is insignificant since it will be // overridden anyways with the id and ref of manifest list later in // in this code. // // * Multi-platform build without manifest list: If this is a build for // multiple platforms without --manifest then we are free to return // id and ref of any one of the image in the instance list so always // return indice 0 for predictable output instead returning the id and // ref of the go routine which completed at last. id, ref = instances[0].ID, instances[0].Ref if manifestList != "" { rt, err := libimage.RuntimeFromStore(store, nil) if err != nil { return "", nil, err } // Create the manifest list ourselves, so that it's not in a // partially-populated state at any point if we're creating it // fresh. list, err := rt.LookupManifestList(manifestList) if err != nil && errors.Is(err, storage.ErrImageUnknown) { list, err = rt.CreateManifestList(manifestList) } if err != nil { return "", nil, err } // Add each instance to the list in turn. storeTransportName := istorage.Transport.Name() for _, instance := range instances { instanceDigest, err := list.Add(ctx, storeTransportName+":"+instance.ID, nil) if err != nil { return "", nil, err } err = list.AnnotateInstance(instanceDigest, &libimage.ManifestListAnnotateOptions{ Architecture: instance.Architecture, OS: instance.OS, Variant: instance.Variant, }) if err != nil { return "", nil, err } } id, ref = list.ID(), nil // Put together a canonical reference storeRef, err := istorage.Transport.NewStoreReference(store, nil, list.ID()) if err != nil { return "", nil, err } imgSource, err := storeRef.NewImageSource(ctx, nil) if err != nil { return "", nil, err } defer imgSource.Close() manifestBytes, _, err := image.UnparsedInstance(imgSource, nil).Manifest(ctx) if err != nil { return "", nil, err } manifestDigest, err := manifest.Digest(manifestBytes) if err != nil { return "", nil, err } img, err := store.Image(id) if err != nil { return "", nil, err } for _, name := range img.Names { if named, err := reference.ParseNamed(name); err == nil { if r, err := reference.WithDigest(reference.TrimNamed(named), manifestDigest); err == nil { ref = r break } } } } return id, ref, nil } func buildDockerfilesOnce(ctx context.Context, store storage.Store, logger *logrus.Logger, logPrefix string, options define.BuildOptions, containerFiles []string, dockerfilecontents [][]byte, processLabel, mountLabel string, usingContextOverlay bool) (string, reference.Canonical, error) { mainNode, err := imagebuilder.ParseDockerfile(bytes.NewReader(dockerfilecontents[0])) if err != nil { return "", nil, fmt.Errorf("parsing main Dockerfile: %s: %w", containerFiles[0], err) } // --platform was explicitly selected for this build // so set correct TARGETPLATFORM in args if it is not // already selected by the user. builtinArgDefaults := make(map[string]string) if options.SystemContext.OSChoice != "" && options.SystemContext.ArchitectureChoice != "" { // os component from --platform string populates TARGETOS // buildkit parity: give priority to user's `--build-arg` builtinArgDefaults["TARGETOS"] = options.SystemContext.OSChoice // arch component from --platform string populates TARGETARCH // buildkit parity: give priority to user's `--build-arg` builtinArgDefaults["TARGETARCH"] = options.SystemContext.ArchitectureChoice // variant component from --platform string populates TARGETVARIANT // buildkit parity: give priority to user's `--build-arg` builtinArgDefaults["TARGETVARIANT"] = options.SystemContext.VariantChoice // buildkit parity: give priority to user's `--build-arg` // buildkit parity: TARGETPLATFORM should be always created // from SystemContext and not `TARGETOS` and `TARGETARCH` because // users can always override values of `TARGETOS` and `TARGETARCH` // but `TARGETPLATFORM` should be set independent of those values. builtinArgDefaults["TARGETPLATFORM"] = builtinArgDefaults["TARGETOS"] + "/" + builtinArgDefaults["TARGETARCH"] if options.SystemContext.VariantChoice != "" { builtinArgDefaults["TARGETPLATFORM"] += "/" + options.SystemContext.VariantChoice } } delete(options.Args, "TARGETPLATFORM") for i, d := range dockerfilecontents[1:] { additionalNode, err := imagebuilder.ParseDockerfile(bytes.NewReader(d)) if err != nil { containerFiles := containerFiles[1:] return "", nil, fmt.Errorf("parsing additional Dockerfile %s: %w", containerFiles[i], err) } mainNode.Children = append(mainNode.Children, additionalNode.Children...) } exec, err := newExecutor(logger, logPrefix, store, options, mainNode, containerFiles, processLabel, mountLabel, usingContextOverlay) if err != nil { return "", nil, fmt.Errorf("creating build executor: %w", err) } b := imagebuilder.NewBuilder(options.Args) maps.Copy(b.BuiltinArgDefaults, builtinArgDefaults) defaultContainerConfig, err := config.Default() if err != nil { return "", nil, fmt.Errorf("failed to get container config: %w", err) } b.Env = append(defaultContainerConfig.GetDefaultEnv(), b.Env...) stages, err := imagebuilder.NewStages(mainNode, b) if err != nil { return "", nil, fmt.Errorf("reading multiple stages: %w", err) } if options.Target != "" { stagesTargeted, ok := stages.ThroughTarget(options.Target) if !ok { return "", nil, fmt.Errorf("the target %q was not found in the provided Dockerfile", options.Target) } stages = stagesTargeted } return exec.Build(ctx, stages) } // preprocessContainerfileContents runs CPP(1) in preprocess-only mode on the input // dockerfile content and will use ctxDir as the base include path. func preprocessContainerfileContents(logger *logrus.Logger, containerfile string, r io.Reader, ctxDir string, cppFlags []string) (stdout io.Reader, err error) { cppCommand := "cpp" cppPath, err := exec.LookPath(cppCommand) if err != nil { if errors.Is(err, exec.ErrNotFound) { err = fmt.Errorf("%v: .in support requires %s to be installed", err, cppCommand) } return nil, err } stdoutBuffer := bytes.Buffer{} stderrBuffer := bytes.Buffer{} cppArgs := []string{"-E", "-iquote", ctxDir, "-traditional", "-undef", "-"} if flags, ok := os.LookupEnv("BUILDAH_CPPFLAGS"); ok { args, err := shellwords.Parse(flags) if err != nil { return nil, fmt.Errorf("parsing BUILDAH_CPPFLAGS %q: %v", flags, err) } cppArgs = append(cppArgs, args...) } cppArgs = append(cppArgs, cppFlags...) cmd := exec.Command(cppPath, cppArgs...) cmd.Stdin = r cmd.Stdout = &stdoutBuffer cmd.Stderr = &stderrBuffer if err = cmd.Start(); err != nil { return nil, fmt.Errorf("preprocessing %s: %w", containerfile, err) } if err = cmd.Wait(); err != nil { if stderrBuffer.Len() != 0 { logger.Warnf("Ignoring %s\n", stderrBuffer.String()) } if stdoutBuffer.Len() == 0 { return nil, fmt.Errorf("preprocessing %s: preprocessor produced no output: %w", containerfile, err) } } return &stdoutBuffer, nil } // platformIsUnknown checks if the platform value indicates that the // corresponding index entry is suitable for use as a base image func platformIsAcceptable(platform *v1.Platform, logger *logrus.Logger) bool { if platform == nil { logger.Trace("rejecting potential base image with no platform information") return false } if gotypes.SizesFor("gc", platform.Architecture) == nil { // the compiler's never heard of this logger.Tracef("rejecting potential base image architecture %q for which Go has no knowledge of how to do unsafe code", platform.Architecture) return false } if slices.Contains([]string{"", "unknown"}, platform.OS) { // we're hard-wired to reject images with these values logger.Tracef("rejecting potential base image for which the OS value is always-rejected value %q", platform.OS) return false } return true } // platformsForBaseImages resolves the names of base images from the // dockerfiles, and if they are all valid references to manifest lists, returns // the list of platforms that are supported by all of the base images. func platformsForBaseImages(ctx context.Context, logger *logrus.Logger, dockerfilepaths []string, dockerfiles [][]byte, from string, args map[string]string, additionalBuildContext map[string]*define.AdditionalBuildContext, systemContext *types.SystemContext) ([]struct{ OS, Arch, Variant string }, error) { baseImages, err := baseImages(dockerfilepaths, dockerfiles, from, args, additionalBuildContext) if err != nil { return nil, fmt.Errorf("determining list of base images: %w", err) } logrus.Debugf("unresolved base images: %v", baseImages) if len(baseImages) == 0 { return nil, fmt.Errorf("build uses no non-scratch base images: %w", err) } targetPlatforms := make(map[string]struct{}) var platformList []struct{ OS, Arch, Variant string } for baseImageIndex, baseImage := range baseImages { resolved, err := shortnames.Resolve(systemContext, baseImage) if err != nil { return nil, fmt.Errorf("resolving image name %q: %w", baseImage, err) } var manifestBytes []byte var manifestType string for _, candidate := range resolved.PullCandidates { ref, err := docker.NewReference(candidate.Value) if err != nil { // github.com/containers/common/libimage.Runtime.Pull() will catch // references that include both a tag and a digest, and drop the // tag as part of pulling the image. Fall back to doing roughly // the same here. var nonDigestedRef reference.Named if named, err2 := reference.ParseNamed(candidate.Value.String()); err2 == nil { _, isTagged := named.(reference.NamedTagged) digested, isDigested := named.(reference.Digested) if isTagged && isDigested { if nonDigestedRef, err2 = reference.WithDigest(reference.TrimNamed(named), digested.Digest()); err2 != nil { nonDigestedRef = nil } } } if nonDigestedRef == nil { // not a tagged-and-digested reference, either, so log the original error logrus.Debugf("parsing image reference %q: %v", candidate.Value.String(), err) continue } ref, err = docker.NewReference(nonDigestedRef) if err != nil { logrus.Debugf("re-parsing image reference %q: %v", nonDigestedRef.String(), err) continue } } src, err := ref.NewImageSource(ctx, systemContext) if err != nil { logrus.Debugf("preparing to read image manifest for %q: %v", baseImage, err) continue } candidateBytes, candidateType, err := image.UnparsedInstance(src, nil).Manifest(ctx) _ = src.Close() if err != nil { logrus.Debugf("reading image manifest for %q: %v", baseImage, err) continue } if !manifest.MIMETypeIsMultiImage(candidateType) { logrus.Debugf("base image %q is not a reference to a manifest list: %v", baseImage, err) continue } if err := candidate.Record(); err != nil { logrus.Debugf("error recording name %q for base image %q: %v", candidate.Value.String(), baseImage, err) continue } baseImage = candidate.Value.String() manifestBytes, manifestType = candidateBytes, candidateType break } if len(manifestBytes) == 0 { if len(resolved.PullCandidates) > 0 { return nil, fmt.Errorf("base image name %q didn't resolve to a manifest list", baseImage) } return nil, fmt.Errorf("base image name %q didn't resolve to anything", baseImage) } if manifestType != v1.MediaTypeImageIndex { list, err := manifest.ListFromBlob(manifestBytes, manifestType) if err != nil { return nil, fmt.Errorf("parsing manifest list from base image %q: %w", baseImage, err) } list, err = list.ConvertToMIMEType(v1.MediaTypeImageIndex) if err != nil { return nil, fmt.Errorf("converting manifest list from base image %q to v2s2 list: %w", baseImage, err) } manifestBytes, err = list.Serialize() if err != nil { return nil, fmt.Errorf("encoding converted v2s2 manifest list for base image %q: %w", baseImage, err) } } index, err := manifest.OCI1IndexFromManifest(manifestBytes) if err != nil { return nil, fmt.Errorf("decoding manifest list for base image %q: %w", baseImage, err) } if baseImageIndex == 0 { // populate the list with the first image's normalized platforms for _, instance := range index.Manifests { if !platformIsAcceptable(instance.Platform, logger) { continue } if instance.ArtifactType != "" { continue } platform := internalUtil.NormalizePlatform(*instance.Platform) targetPlatforms[platforms.Format(platform)] = struct{}{} logger.Debugf("image %q supports %q", baseImage, platforms.Format(platform)) } } else { // prune the list of any normalized platforms this base image doesn't support imagePlatforms := make(map[string]struct{}) for _, instance := range index.Manifests { if !platformIsAcceptable(instance.Platform, logger) { continue } if instance.ArtifactType != "" { continue } platform := internalUtil.NormalizePlatform(*instance.Platform) imagePlatforms[platforms.Format(platform)] = struct{}{} logger.Debugf("image %q supports %q", baseImage, platforms.Format(platform)) } var removed []string for platform := range targetPlatforms { if _, present := imagePlatforms[platform]; !present { removed = append(removed, platform) logger.Debugf("image %q does not support %q", baseImage, platform) } } for _, remove := range removed { delete(targetPlatforms, remove) } } if baseImageIndex == len(baseImages)-1 && len(targetPlatforms) > 0 { // extract the list for platform := range targetPlatforms { platform, err := platforms.Parse(platform) if err != nil { return nil, fmt.Errorf("parsing platform double/triple %q: %w", platform, err) } platformList = append(platformList, struct{ OS, Arch, Variant string }{ OS: platform.OS, Arch: platform.Architecture, Variant: platform.Variant, }) logger.Debugf("base images all support %q", platform) } } } if len(platformList) == 0 { return nil, errors.New("base images have no platforms in common") } return platformList, nil } // baseImages parses the dockerfilecontents, possibly replacing the first // stage's base image with FROM, and returns the list of base images as // provided. Each entry in the dockerfilenames slice corresponds to a slice in // dockerfilecontents. func baseImages(dockerfilenames []string, dockerfilecontents [][]byte, from string, args map[string]string, additionalBuildContext map[string]*define.AdditionalBuildContext) ([]string, error) { mainNode, err := imagebuilder.ParseDockerfile(bytes.NewReader(dockerfilecontents[0])) if err != nil { return nil, fmt.Errorf("parsing main Dockerfile: %s: %w", dockerfilenames[0], err) } for i, d := range dockerfilecontents[1:] { additionalNode, err := imagebuilder.ParseDockerfile(bytes.NewReader(d)) if err != nil { dockerfilenames := dockerfilenames[1:] return nil, fmt.Errorf("parsing additional Dockerfile %s: %w", dockerfilenames[i], err) } mainNode.Children = append(mainNode.Children, additionalNode.Children...) } b := imagebuilder.NewBuilder(args) defaultContainerConfig, err := config.Default() if err != nil { return nil, fmt.Errorf("failed to get container config: %w", err) } b.Env = defaultContainerConfig.GetDefaultEnv() stages, err := imagebuilder.NewStages(mainNode, b) if err != nil { return nil, fmt.Errorf("reading multiple stages: %w", err) } var baseImages []string nicknames := make(map[string]struct{}) for stageIndex, stage := range stages { node := stage.Node // first line for node != nil { // each line for _, child := range node.Children { // tokens on this line, though we only care about the first switch strings.ToUpper(child.Value) { // first token - instruction case "FROM": if child.Next != nil { // second token on this line // If we have a fromOverride, replace the value of // image name for the first FROM in the Containerfile. if from != "" { child.Next.Value = from from = "" } if replaceBuildContext, ok := additionalBuildContext[child.Next.Value]; ok { if replaceBuildContext.IsImage { child.Next.Value = replaceBuildContext.Value } else { return nil, fmt.Errorf("build context %q is not an image, can not be used for FROM %q", child.Next.Value, child.Next.Value) } } base := child.Next.Value if base != "" && base != buildah.BaseImageFakeName && !internalUtil.SetHas(nicknames, base) { builtinArgs := argsMapToSlice(stage.Builder.BuiltinArgDefaults) headingArgs := argsMapToSlice(stage.Builder.HeadingArgs) userArgs := argsMapToSlice(stage.Builder.Args) // ProcessWord uses first match; put highest priority first so // --build-arg overrides header ARG overrides builtin. userArgs = slices.Concat(userArgs, headingArgs, builtinArgs) baseWithArg, err := imagebuilder.ProcessWord(base, userArgs) if err != nil { return nil, fmt.Errorf("while replacing arg variables with values for format %q: %w", base, err) } baseImages = append(baseImages, baseWithArg) } } } } node = node.Next // next line } if stage.Name != strconv.Itoa(stageIndex) { nicknames[stage.Name] = struct{}{} } } return baseImages, nil } ================================================ FILE: imagebuildah/build_linux.go ================================================ package imagebuildah import ( "errors" "fmt" "io/fs" "os" "path/filepath" "slices" "github.com/containers/buildah/define" "github.com/containers/buildah/internal/tmpdir" "github.com/containers/buildah/pkg/overlay" "github.com/opencontainers/selinux/go-selinux/label" "github.com/sirupsen/logrus" "go.podman.io/storage" "golang.org/x/sys/unix" ) // platformSetupContextDirectoryOverlay() sets up an overlay _over_ the build // context directory, and sorts out labeling. Returns the location which // should be used as the default build context; the process label and mount // label for the build, if any; a boolean value that indicates whether we did, // in fact, mount an overlay; and a cleanup function which should be called // when the location is no longer needed (on success). Returned errors should // be treated as fatal. func platformSetupContextDirectoryOverlay(store storage.Store, options *define.BuildOptions) (string, string, string, bool, func(), error) { var succeeded bool var tmpDir, contentDir string cleanup := func() { if contentDir != "" { if err := overlay.CleanupContent(tmpDir); err != nil { logrus.Debugf("cleaning up overlay scaffolding for build context directory: %v", err) } } if tmpDir != "" { if err := os.Remove(tmpDir); err != nil && !errors.Is(err, fs.ErrNotExist) { logrus.Debugf("removing should-be-empty temporary directory %q: %v", tmpDir, err) } } } defer func() { if !succeeded { cleanup() } }() // double-check that the context directory location is an absolute path contextDirectoryAbsolute, err := filepath.Abs(options.ContextDirectory) if err != nil { return "", "", "", false, nil, fmt.Errorf("determining absolute path of %q: %w", options.ContextDirectory, err) } var st unix.Stat_t if err := unix.Stat(contextDirectoryAbsolute, &st); err != nil { return "", "", "", false, nil, fmt.Errorf("checking ownership of %q: %w", options.ContextDirectory, err) } // figure out the labeling situation processLabel, mountLabel, err := label.InitLabels(options.CommonBuildOpts.LabelOpts) if err != nil { return "", "", "", false, nil, err } // create a temporary directory tmpDir, err = os.MkdirTemp(tmpdir.GetTempDir(), "buildah-context-") if err != nil { return "", "", "", false, nil, fmt.Errorf("creating temporary directory: %w", err) } // create the scaffolding for an overlay mount under it contentDir, err = overlay.TempDir(tmpDir, 0, 0) if err != nil { return "", "", "", false, nil, fmt.Errorf("creating overlay scaffolding for build context directory: %w", err) } // mount an overlay that uses it as a lower overlayOptions := overlay.Options{ GraphOpts: slices.Clone(store.GraphOptions()), ForceMount: true, MountLabel: mountLabel, } targetDir := filepath.Join(contentDir, "target") contextDirMountSpec, err := overlay.MountWithOptions(contentDir, contextDirectoryAbsolute, targetDir, &overlayOptions) if err != nil { return "", "", "", false, nil, fmt.Errorf("creating overlay scaffolding for build context directory: %w", err) } // going forward, pretend that the merged directory is the actual context directory logrus.Debugf("mounted an overlay at %q over %q", contextDirMountSpec.Source, contextDirectoryAbsolute) succeeded = true return contextDirMountSpec.Source, processLabel, mountLabel, true, cleanup, nil } ================================================ FILE: imagebuildah/build_linux_test.go ================================================ package imagebuildah import ( "context" "errors" "fmt" "io/fs" "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/containers/buildah/define" ) func TestFilesClosedProperlyByBuildDockerfiles(t *testing.T) { // create files in in temp dir var paths []string for _, name := range []string{"Dockerfile", "Dockerfile.in"} { fpath, err := filepath.Abs(filepath.Join(t.TempDir(), name)) assert.Nil(t, err) assert.Nil(t, os.WriteFile(fpath, []byte("FROM scratch"), 0o644)) paths = append(paths, fpath) } // send files as above, and a missing one, so that we error early and return and don't try an actual build _, _, err := BuildDockerfiles(context.Background(), nil, define.BuildOptions{}, append(append(make([]string, 0, len(paths)), paths...), "missing")...) var pathErr *fs.PathError assert.True(t, errors.As(err, &pathErr)) assert.Equal(t, "missing", pathErr.Path) // verify (as best we can) that we don't think these files are still open openFiles, err := currentOpenFiles() assert.Nil(t, err) for _, path := range paths { assert.NotContains(t, openFiles, path) } } // currentOpenFiles makes an effort at returning a map of which files are currently // open by our process. We don't fail if we can't follow symlinks from fds as this // perhaps they now longer exist between when we read them and when we tried to use // them. Instead we just ignore. func currentOpenFiles() (map[string]struct{}, error) { rd := "/proc/self/fd" es, err := os.ReadDir(rd) if err != nil { return nil, err } rv := make(map[string]struct{}) for _, de := range es { if de.Type()&fs.ModeSymlink == fs.ModeSymlink { dest, err := os.Readlink(filepath.Join(rd, de.Name())) if err != nil { fmt.Fprintf(os.Stderr, "cannot follow symlink, ignoring: %v\n", err) continue } rv[dest] = struct{}{} } } return rv, nil } ================================================ FILE: imagebuildah/build_notlinux.go ================================================ //go:build !linux package imagebuildah import ( "github.com/containers/buildah/define" "go.podman.io/storage" ) // platformSetupContextDirectoryOverlay() should set up an overlay _over_ the // build context directory, and sort out labeling. Should return the location // which should be used as the default build context; the process label and // mount label for the build, if any; a boolean value that indicates whether we // did, in fact, mount an overlay; and a cleanup function which should be // called when the location is no longer needed (on success). Returned errors // should be treated as fatal. // TODO: currenty a no-op on this platform. func platformSetupContextDirectoryOverlay(store storage.Store, options *define.BuildOptions) (string, string, string, bool, func(), error) { return options.ContextDirectory, "", "", false, func() {}, nil } ================================================ FILE: imagebuildah/executor.go ================================================ package imagebuildah import ( "context" "encoding/json" "errors" "fmt" "io" "os" "slices" "strconv" "strings" "sync" "time" "github.com/containers/buildah" "github.com/containers/buildah/define" "github.com/containers/buildah/internal" "github.com/containers/buildah/internal/metadata" internalUtil "github.com/containers/buildah/internal/util" "github.com/containers/buildah/pkg/parse" "github.com/containers/buildah/pkg/sourcepolicy" "github.com/containers/buildah/pkg/sshagent" "github.com/containers/buildah/util" encconfig "github.com/containers/ocicrypt/config" digest "github.com/opencontainers/go-digest" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/openshift/imagebuilder" "github.com/openshift/imagebuilder/dockerfile/parser" "github.com/sirupsen/logrus" "go.podman.io/common/libimage" nettypes "go.podman.io/common/libnetwork/types" "go.podman.io/common/pkg/config" "go.podman.io/image/v5/docker/reference" "go.podman.io/image/v5/manifest" storageTransport "go.podman.io/image/v5/storage" "go.podman.io/image/v5/transports" "go.podman.io/image/v5/transports/alltransports" "go.podman.io/image/v5/types" "go.podman.io/storage" "go.podman.io/storage/pkg/archive" "golang.org/x/sync/semaphore" ) // builtinAllowedBuildArgs is list of built-in allowed build args. Normally we // complain if we're given values for arguments which have no corresponding ARG // instruction in the Dockerfile, since that's usually an indication of a user // error, but for these values we make exceptions and ignore them. var builtinAllowedBuildArgs = map[string]struct{}{ "HTTP_PROXY": {}, "http_proxy": {}, "HTTPS_PROXY": {}, "https_proxy": {}, "FTP_PROXY": {}, "ftp_proxy": {}, "NO_PROXY": {}, "no_proxy": {}, "TARGETARCH": {}, "TARGETOS": {}, "TARGETPLATFORM": {}, "TARGETVARIANT": {}, internal.SourceDateEpochName: {}, } // executor is a buildah-based implementation of the imagebuilder.Executor // interface. It coordinates the entire build by using one or more // stageExecutors to handle each stage of the build. type executor struct { cacheFrom []reference.Named cacheTo []reference.Named cacheTTL time.Duration containerSuffix string logger *logrus.Logger stages map[string]*stageExecutor store storage.Store contextDir string contextDirWritesAreDiscarded bool pullPolicy define.PullPolicy registry string ignoreUnrecognizedInstructions bool quiet bool runtime string runtimeArgs []string transientMounts []Mount transientRunMounts []string compression archive.Compression output string outputFormat string additionalTags []string log func(format string, args ...any) // can be nil in io.Reader inheritLabels types.OptionalBool inheritAnnotations types.OptionalBool out io.Writer err io.Writer signaturePolicyPath string sourcePolicy *sourcepolicy.Policy skipUnusedStages types.OptionalBool systemContext *types.SystemContext reportWriter io.Writer isolation define.Isolation namespaceOptions []define.NamespaceOption configureNetwork define.NetworkConfigurationPolicy // networkInterface is the libnetwork network interface used to setup netavark networks. networkInterface nettypes.ContainerNetwork idmappingOptions *define.IDMappingOptions commonBuildOptions *define.CommonBuildOptions defaultMountsFilePath string iidfile string iidfileRaw string squash bool labels []string layerLabels []string annotations []string layers bool saveStages bool stageLabels bool stageImageIDs map[string]string // Tracks image IDs for each stage (by name and position) for label references noHostname bool noHosts bool useCache bool removeIntermediateCtrs bool forceRmIntermediateCtrs bool imageMap map[string]string // Used to map images that we create to handle the AS construct. containerMap map[string]*buildah.Builder // Used to map from image names to only-created-for-the-rootfs containers. baseMap map[string]struct{} // Holds the names of every base image, as given. rootfsMap map[string]struct{} // Holds the names of every stage whose rootfs is referenced in a COPY or ADD instruction. afterDependency map[string]string // Maps stage names to their --after dependency. blobDirectory string excludes []string groupAdd []string ignoreFile string args map[string]string globalArgs map[string]string unusedArgs map[string]struct{} capabilities []string devices define.ContainerDevices deviceSpecs []string signBy string architecture string timestamp *time.Time os string maxPullPushRetries int retryPullPushDelay time.Duration cachePullSourceLookupReferenceFunc libimage.LookupReferenceFunc cachePullDestinationLookupReferenceFunc func(srcRef types.ImageReference) libimage.LookupReferenceFunc cachePushSourceLookupReferenceFunc func(dest types.ImageReference) libimage.LookupReferenceFunc cachePushDestinationLookupReferenceFunc libimage.LookupReferenceFunc ociDecryptConfig *encconfig.DecryptConfig lastError error terminatedStage map[string]error stagesLock sync.Mutex stagesSemaphore *semaphore.Weighted logRusage bool rusageLogFile io.Writer imageInfoLock sync.Mutex imageInfoCache map[string]imageTypeAndHistoryAndDiffIDs fromOverride string additionalBuildContexts map[string]*define.AdditionalBuildContext manifest string secrets map[string]define.Secret sshsources map[string]*sshagent.Source logPrefix string unsetEnvs []string unsetLabels []string unsetAnnotations []string processLabel string // Shares processLabel of first stage container with containers of other stages in same build mountLabel string // Shares mountLabel of first stage container with containers of other stages in same build buildOutputs []string // Specifies instructions for any custom build output osVersion string osFeatures []string envs []string confidentialWorkload define.ConfidentialWorkloadOptions sbomScanOptions []define.SBOMScanOptions cdiConfigDir string compatSetParent types.OptionalBool compatVolumes types.OptionalBool compatScratchConfig types.OptionalBool compatLayerOmissions types.OptionalBool noPivotRoot bool sourceDateEpoch *time.Time rewriteTimestamp bool createdAnnotation types.OptionalBool metadataFile string } type imageTypeAndHistoryAndDiffIDs struct { manifestType string history []v1.History diffIDs []digest.Digest err error architecture string os string } // newExecutor creates a new instance of the imagebuilder.Executor interface. func newExecutor(logger *logrus.Logger, logPrefix string, store storage.Store, options define.BuildOptions, mainNode *parser.Node, containerFiles []string, processLabel, mountLabel string, contextWritesDiscarded bool) (*executor, error) { defaultContainerConfig, err := config.Default() if err != nil { return nil, fmt.Errorf("failed to get container config: %w", err) } excludes := options.Excludes if len(excludes) == 0 { excludes, options.IgnoreFile, err = parse.ContainerIgnoreFile(options.ContextDirectory, options.IgnoreFile, containerFiles) if err != nil { return nil, err } } capabilities, err := defaultContainerConfig.Capabilities("", options.AddCapabilities, options.DropCapabilities) if err != nil { return nil, err } var transientMounts []Mount for _, volume := range append(defaultContainerConfig.Volumes(), options.TransientMounts...) { mount, err := parse.Volume(volume) if err != nil { return nil, err } transientMounts = append([]Mount{mount}, transientMounts...) } secrets, err := parse.Secrets(options.CommonBuildOpts.Secrets) if err != nil { return nil, err } sshsources, err := parse.SSH(options.CommonBuildOpts.SSHSources) if err != nil { return nil, err } // Load source policy if specified var srcPolicy *sourcepolicy.Policy if options.SourcePolicyFile != "" { srcPolicy, err = sourcepolicy.LoadFromFile(options.SourcePolicyFile) if err != nil { return nil, fmt.Errorf("loading source policy: %w", err) } } writer := options.ReportWriter if options.Quiet { writer = io.Discard } var rusageLogFile io.Writer if options.LogRusage && !options.Quiet { if options.RusageLogFile == "" { rusageLogFile = options.Out } else { rusageLogFile, err = os.OpenFile(options.RusageLogFile, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { return nil, fmt.Errorf("creating file to store rusage logs: %w", err) } } } buildOutputs := slices.Clone(options.BuildOutputs) if options.BuildOutput != "" { //nolint:staticcheck buildOutputs = append(buildOutputs, options.BuildOutput) //nolint:staticcheck } exec := executor{ args: options.Args, cacheFrom: options.CacheFrom, cacheTo: options.CacheTo, cacheTTL: options.CacheTTL, containerSuffix: options.ContainerSuffix, logger: logger, stages: make(map[string]*stageExecutor), store: store, contextDir: options.ContextDirectory, contextDirWritesAreDiscarded: contextWritesDiscarded, excludes: excludes, groupAdd: options.GroupAdd, ignoreFile: options.IgnoreFile, pullPolicy: options.PullPolicy, registry: options.Registry, ignoreUnrecognizedInstructions: options.IgnoreUnrecognizedInstructions, quiet: options.Quiet, runtime: options.Runtime, runtimeArgs: options.RuntimeArgs, transientMounts: transientMounts, transientRunMounts: options.TransientRunMounts, compression: options.Compression, output: options.Output, outputFormat: options.OutputFormat, additionalTags: options.AdditionalTags, signaturePolicyPath: options.SignaturePolicyPath, sourcePolicy: srcPolicy, skipUnusedStages: options.SkipUnusedStages, systemContext: options.SystemContext, log: options.Log, in: options.In, out: options.Out, err: options.Err, reportWriter: writer, isolation: options.Isolation, inheritLabels: options.InheritLabels, inheritAnnotations: options.InheritAnnotations, namespaceOptions: options.NamespaceOptions, configureNetwork: options.ConfigureNetwork, networkInterface: options.NetworkInterface, idmappingOptions: options.IDMappingOptions, commonBuildOptions: options.CommonBuildOpts, defaultMountsFilePath: options.DefaultMountsFilePath, iidfile: options.IIDFile, iidfileRaw: options.IIDFileRaw, squash: options.Squash, labels: slices.Clone(options.Labels), layerLabels: slices.Clone(options.LayerLabels), processLabel: processLabel, mountLabel: mountLabel, annotations: slices.Clone(options.Annotations), layers: options.Layers, saveStages: options.SaveStages, stageLabels: options.StageLabels, stageImageIDs: make(map[string]string), noHostname: options.CommonBuildOpts.NoHostname, noHosts: options.CommonBuildOpts.NoHosts, useCache: !options.NoCache, removeIntermediateCtrs: options.RemoveIntermediateCtrs, forceRmIntermediateCtrs: options.ForceRmIntermediateCtrs, imageMap: make(map[string]string), containerMap: make(map[string]*buildah.Builder), baseMap: make(map[string]struct{}), rootfsMap: make(map[string]struct{}), blobDirectory: options.BlobDirectory, unusedArgs: make(map[string]struct{}), capabilities: capabilities, deviceSpecs: options.Devices, signBy: options.SignBy, architecture: options.Architecture, timestamp: options.Timestamp, os: options.OS, maxPullPushRetries: options.MaxPullPushRetries, retryPullPushDelay: options.PullPushRetryDelay, cachePullSourceLookupReferenceFunc: options.CachePullSourceLookupReferenceFunc, cachePullDestinationLookupReferenceFunc: options.CachePullDestinationLookupReferenceFunc, cachePushSourceLookupReferenceFunc: options.CachePushSourceLookupReferenceFunc, cachePushDestinationLookupReferenceFunc: options.CachePushDestinationLookupReferenceFunc, ociDecryptConfig: options.OciDecryptConfig, terminatedStage: make(map[string]error), stagesSemaphore: options.JobSemaphore, logRusage: options.LogRusage, rusageLogFile: rusageLogFile, imageInfoCache: make(map[string]imageTypeAndHistoryAndDiffIDs), fromOverride: options.From, additionalBuildContexts: options.AdditionalBuildContexts, manifest: options.Manifest, secrets: secrets, sshsources: sshsources, logPrefix: logPrefix, unsetEnvs: slices.Clone(options.UnsetEnvs), unsetLabels: slices.Clone(options.UnsetLabels), unsetAnnotations: slices.Clone(options.UnsetAnnotations), buildOutputs: buildOutputs, osVersion: options.OSVersion, osFeatures: slices.Clone(options.OSFeatures), envs: slices.Clone(options.Envs), confidentialWorkload: options.ConfidentialWorkload, sbomScanOptions: options.SBOMScanOptions, cdiConfigDir: options.CDIConfigDir, compatSetParent: options.CompatSetParent, compatVolumes: options.CompatVolumes, compatScratchConfig: options.CompatScratchConfig, compatLayerOmissions: options.CompatLayerOmissions, noPivotRoot: options.NoPivotRoot, sourceDateEpoch: options.SourceDateEpoch, rewriteTimestamp: options.RewriteTimestamp, createdAnnotation: options.CreatedAnnotation, metadataFile: options.MetadataFile, } // sort unsetAnnotations because we will later write these // values to the history of the image therefore we want to // make sure that order is always consistent. slices.Sort(exec.unsetAnnotations) if exec.err == nil { exec.err = os.Stderr } if exec.out == nil { exec.out = os.Stdout } for arg := range options.Args { if _, isBuiltIn := builtinAllowedBuildArgs[arg]; !isBuiltIn { exec.unusedArgs[arg] = struct{}{} } } // Use this flag to collect all args declared before // first stage and treat them as global args which is // accessible to all stages. foundFirstStage := false globalArgs := make(map[string]string) for _, line := range mainNode.Children { node := line for node != nil { // tokens on this line, though we only care about the first switch strings.ToUpper(node.Value) { // first token - instruction case "ARG": for arg := node.Next; arg != nil; arg = arg.Next { // Each token is name=value or name (multiple ARGs in one instruction, see imagebuilder PR #210) argName, argValue, hasValue := strings.Cut(arg.Value, "=") if argName == "" { continue } if !foundFirstStage { if hasValue { globalArgs[argName] = argValue } } delete(exec.unusedArgs, argName) } case "FROM": foundFirstStage = true } break } } exec.globalArgs = globalArgs return &exec, nil } // startStage creates a new stage executor that will be referenced whenever a // COPY or ADD statement uses a --from=NAME flag. func (b *executor) startStage(ctx context.Context, stage *imagebuilder.Stage, stages imagebuilder.Stages, output string) *stageExecutor { // create a copy of systemContext for each stage executor. systemContext := *b.systemContext stageExec := &stageExecutor{ ctx: ctx, executor: b, systemContext: &systemContext, log: b.log, index: stage.Position, stages: stages, name: stage.Name, volumeCache: make(map[string]string), volumeCacheInfo: make(map[string]os.FileInfo), output: output, stage: stage, } b.stages[stage.Name] = stageExec if idx := strconv.Itoa(stage.Position); idx != stage.Name { b.stages[idx] = stageExec } return stageExec } // resolveNameToImageRef creates a types.ImageReference for the output name in local storage func (b *executor) resolveNameToImageRef(output string) (types.ImageReference, error) { if imageRef, err := alltransports.ParseImageName(output); err == nil { return imageRef, nil } resolved, err := libimage.NormalizeName(output) if err != nil { return nil, err } imageRef, err := storageTransport.Transport.ParseStoreReference(b.store, resolved.String()) if err == nil { return imageRef, nil } return imageRef, err } // waitForStage waits for an entry to be added to terminatedStage indicating // that the specified stage has finished. If there is no stage defined by that // name, then it will return (false, nil). If there is a stage defined by that // name, it will return true along with any error it encounters. func (b *executor) waitForStage(ctx context.Context, name string, stages imagebuilder.Stages) (bool, error) { found := false for _, otherStage := range stages { if otherStage.Name == name || strconv.Itoa(otherStage.Position) == name { found = true break } } if !found { return false, nil } for { if b.lastError != nil { return true, b.lastError } b.stagesLock.Lock() terminationError, terminated := b.terminatedStage[name] b.stagesLock.Unlock() if terminationError != nil { return false, terminationError } if terminated { return true, nil } b.stagesSemaphore.Release(1) time.Sleep(time.Millisecond * 10) if err := b.stagesSemaphore.Acquire(ctx, 1); err != nil { return true, fmt.Errorf("reacquiring job semaphore: %w", err) } } } // getImageTypeAndHistoryAndDiffIDs returns the os, architecture, manifest type, history, and diff IDs list of imageID. func (b *executor) getImageTypeAndHistoryAndDiffIDs(ctx context.Context, imageID string) (string, string, string, []v1.History, []digest.Digest, error) { b.imageInfoLock.Lock() imageInfo, ok := b.imageInfoCache[imageID] b.imageInfoLock.Unlock() if ok { return imageInfo.os, imageInfo.architecture, imageInfo.manifestType, imageInfo.history, imageInfo.diffIDs, imageInfo.err } imageRef, err := storageTransport.Transport.ParseStoreReference(b.store, "@"+imageID) if err != nil { return "", "", "", nil, nil, fmt.Errorf("getting image reference %q: %w", imageID, err) } ref, err := imageRef.NewImage(ctx, nil) if err != nil { return "", "", "", nil, nil, fmt.Errorf("creating new image from reference to image %q: %w", imageID, err) } defer ref.Close() oci, err := ref.OCIConfig(ctx) if err != nil { return "", "", "", nil, nil, fmt.Errorf("getting possibly-converted OCI config of image %q: %w", imageID, err) } manifestBytes, manifestFormat, err := ref.Manifest(ctx) if err != nil { return "", "", "", nil, nil, fmt.Errorf("getting manifest of image %q: %w", imageID, err) } if manifestFormat == "" && len(manifestBytes) > 0 { manifestFormat = manifest.GuessMIMEType(manifestBytes) } b.imageInfoLock.Lock() b.imageInfoCache[imageID] = imageTypeAndHistoryAndDiffIDs{ manifestType: manifestFormat, history: oci.History, diffIDs: oci.RootFS.DiffIDs, err: nil, architecture: oci.Architecture, os: oci.OS, } b.imageInfoLock.Unlock() return oci.OS, oci.Architecture, manifestFormat, oci.History, oci.RootFS.DiffIDs, nil } func (b *executor) buildStage(ctx context.Context, cleanupStages map[int]*stageExecutor, stages imagebuilder.Stages, stageIndex int) (imageID string, commitResults *buildah.CommitResults, onlyBaseImage bool, err error) { var prependInstructions, appendInstructions []string stage := stages[stageIndex] ib := stage.Builder node := stage.Node // Wait for any --after deps before ib.From(node) which may try to access images // via local transports (like oci-archive:) populated by those deps. if afterDep, ok := b.afterDependency[stage.Name]; ok { logrus.Debugf("stage %d (%s): waiting for --after dependency %q", stageIndex, stage.Name, afterDep) if isStage, err := b.waitForStage(ctx, afterDep, stages[:stageIndex]); isStage && err != nil { return "", nil, false, fmt.Errorf("waiting for --after=%s: %w", afterDep, err) } } base, err := ib.From(node) if err != nil { logrus.Debugf("buildStage(node.Children=%#v)", node.Children) return "", nil, false, err } // If this is the last stage, then the image that we produce at // its end should be given the desired output name. output := "" if stageIndex == len(stages)-1 { output = b.output // Check if any labels were passed in via the API, and add an instruction at // the end of the Dockerfile stage that would provide the intended result. // Reason: Docker adds label modification as a last step which can be // processed like regular steps, and if no modification is done to // layers, its easier to reuse cached layers. if len(b.labels) > 0 { labelLine := "LABEL" for _, labelSpec := range b.labels { key, value, _ := strings.Cut(labelSpec, "=") // check only for an empty key since docker allows empty values if key != "" { labelLine += fmt.Sprintf(" %q=%q", key, value) } } appendInstructions = slices.Concat(appendInstructions, []string{labelLine}) } } // If we were given environment variables to set via the API, add them as instructions // at the beginning of the stage so that they affect subsequent RUN instructions and // factor into the image history. if len(b.envs) > 0 { envLine := "ENV" for _, envSpec := range b.envs { key, value, hasValue := strings.Cut(envSpec, "=") if hasValue { envLine += fmt.Sprintf(" %q=%q", key, value) } else { return "", nil, false, fmt.Errorf("BUG: unresolved environment variable: %q", key) } } prependInstructions = slices.Concat([]string{envLine}, prependInstructions) } // Create stage labels for all stage images including final stage // Skip if stage has no instructions if b.saveStages && b.stageLabels && len(stage.Node.Children) > 0 { // Wait for base stage if it references a previous stage // This ensures stageImageIDs map is populated before buildStageLabelLine reads it if isStage, err := b.waitForStage(ctx, base, stages[:stageIndex]); isStage && err != nil { return "", nil, false, fmt.Errorf("waiting for base stage %s: %w", base, err) } labelLine := b.buildStageLabelLine(&stage, base, stages[:stageIndex]) prependInstructions = slices.Concat([]string{labelLine}, prependInstructions) } // If we're supposed to be appending or prepending instructions to this stage, add them now. if len(prependInstructions) > 0 { addLines := strings.Join(prependInstructions, "\n") additionalNodes, err := imagebuilder.ParseDockerfile(strings.NewReader(addLines)) if err != nil { return "", nil, false, fmt.Errorf("while adding additional steps %q: %w", prependInstructions, err) } stage.Node.Children = append(additionalNodes.Children, stage.Node.Children...) } if len(appendInstructions) > 0 { addLines := strings.Join(appendInstructions, "\n") additionalNodes, err := imagebuilder.ParseDockerfile(strings.NewReader(addLines)) if err != nil { return "", nil, false, fmt.Errorf("while adding additional steps %q: %w", appendInstructions, err) } stage.Node.Children = append(stage.Node.Children, additionalNodes.Children...) } b.stagesLock.Lock() stageExecutor := b.startStage(ctx, &stage, stages, output) if stageExecutor.log == nil { stepCounter := 0 stageExecutor.log = func(format string, args ...any) { prefix := b.logPrefix if len(stages) > 1 { prefix += fmt.Sprintf("[%d/%d] ", stageIndex+1, len(stages)) } if !strings.HasPrefix(format, "COMMIT") { stepCounter++ prefix += fmt.Sprintf("STEP %d", stepCounter) if stepCounter <= len(stage.Node.Children)+1 { prefix += fmt.Sprintf("/%d", len(stage.Node.Children)+1) } prefix += ": " } suffix := "\n" fmt.Fprintf(stageExecutor.executor.out, prefix+format+suffix, args...) } } b.stagesLock.Unlock() // If this a single-layer build, or if it's a multi-layered // build and b.forceRmIntermediateCtrs is set, make sure we // remove the intermediate/build containers, regardless of // whether or not the stage's build fails. if b.forceRmIntermediateCtrs || !b.layers { b.stagesLock.Lock() cleanupStages[stage.Position] = stageExecutor b.stagesLock.Unlock() } // Build this stage. if imageID, commitResults, onlyBaseImage, err = stageExecutor.execute(ctx, base); err != nil { return "", nil, onlyBaseImage, err } // Store image ID for this stage so subsequent stages can reference it in labels if imageID != "" { b.stagesLock.Lock() if stage.Name != "" { b.stageImageIDs[stage.Name] = imageID } b.stageImageIDs[strconv.Itoa(stageIndex)] = imageID b.stagesLock.Unlock() } // The stage succeeded, so remove its build container if we're // told to delete successful intermediate/build containers for // multi-layered builds. // Skip cleanup if the stage has no instructions. if b.removeIntermediateCtrs && len(stage.Node.Children) > 0 { b.stagesLock.Lock() cleanupStages[stage.Position] = stageExecutor b.stagesLock.Unlock() } return imageID, commitResults, onlyBaseImage, nil } type stageDependencyInfo struct { Name string Position int Needs []string NeededByTarget bool } // Marks `NeededByTarget` as true for the given stage and all its dependency stages as true recursively. func markDependencyStagesForTarget(dependencyMap map[string]*stageDependencyInfo, stage string) { if stageDependencyInfo, ok := dependencyMap[stage]; ok { if !stageDependencyInfo.NeededByTarget { stageDependencyInfo.NeededByTarget = true for _, need := range stageDependencyInfo.Needs { markDependencyStagesForTarget(dependencyMap, need) } } } } // buildStageLabelLine creates a LABEL instruction line for stage metadata func (b *executor) buildStageLabelLine(stage *imagebuilder.Stage, base string, stages imagebuilder.Stages) string { labelLine := "LABEL" labelLine += fmt.Sprintf(" %q=%q", "io.buildah.stage.name", stage.Name) // Check if base of the stage is another (previous) stage. // If yes, base is set as image ID of this stage. // If not original base name is set (pullspec). for i, st := range stages { if st.Name == base || strconv.Itoa(st.Position) == base { b.stagesLock.Lock() if imgID, ok := b.stageImageIDs[base]; ok { base = imgID } else if imgID, ok := b.stageImageIDs[strconv.Itoa(i)]; ok { base = imgID } b.stagesLock.Unlock() break } } labelLine += fmt.Sprintf(" %q=%q", "io.buildah.stage.base", base) return labelLine } func (b *executor) warnOnUnsetBuildArgs(stages imagebuilder.Stages, dependencyMap map[string]*stageDependencyInfo, args map[string]string) { argFound := make(map[string]struct{}) for _, stage := range stages { node := stage.Node // first line for node != nil { // each line for _, child := range node.Children { switch strings.ToUpper(child.Value) { case "ARG": for arg := child.Next; arg != nil; arg = arg.Next { argToken := arg.Value argName, argValue, hasEqual := strings.Cut(argToken, "=") if argName == "" { continue } if hasEqual && argValue != "" { argFound[argName] = struct{}{} } argHasValue := internalUtil.SetHas(argFound, argName) if _, ok := args[argName]; !argHasValue && !ok { shouldWarn := true if stageDependencyInfo, ok := dependencyMap[stage.Name]; ok { if !stageDependencyInfo.NeededByTarget && b.skipUnusedStages != types.OptionalBoolFalse { shouldWarn = false } } if _, isBuiltIn := builtinAllowedBuildArgs[argName]; isBuiltIn { shouldWarn = false } if _, isGlobalArg := b.globalArgs[argName]; isGlobalArg { shouldWarn = false } if shouldWarn { b.logger.Warnf("missing %q build argument. Try adding %q to the command line", argName, fmt.Sprintf("--build-arg %s=", argName)) } } } default: continue } } node = node.Next } } } // Build takes care of the details of running Prepare/Execute/Commit/Delete // over each of the one or more parsed Dockerfiles and stages. func (b *executor) Build(ctx context.Context, stages imagebuilder.Stages) (imageID string, ref reference.Canonical, err error) { if len(stages) == 0 { return "", nil, errors.New("building: no stages to build") } var cleanupImages []string cleanupStages := make(map[int]*stageExecutor) stdout := b.out if b.quiet { b.out = io.Discard } cleanup := func() error { var lastErr error // Clean up any containers associated with the final container // built by a stage, for stages that succeeded, since we no // longer need their filesystem contents. b.stagesLock.Lock() for _, stage := range cleanupStages { if err := stage.Delete(); err != nil { logrus.Debugf("Failed to cleanup stage containers: %v", err) lastErr = err } } cleanupStages = nil b.stagesLock.Unlock() // Clean up any builders that we used to get data from images. for _, builder := range b.containerMap { if err := builder.Delete(); err != nil { logrus.Debugf("Failed to cleanup image containers: %v", err) lastErr = err } } b.containerMap = nil // Clean up any intermediate containers associated with stages, // since we're not keeping them for debugging. if b.removeIntermediateCtrs { if err := b.deleteSuccessfulIntermediateCtrs(); err != nil { logrus.Debugf("Failed to cleanup intermediate containers: %v", err) lastErr = err } } // Remove images from stages except the last one, since we're // not going to use them as a starting point for any new // stages. for i := range cleanupImages { removeID := cleanupImages[len(cleanupImages)-i-1] if removeID == imageID { continue } if _, err := b.store.DeleteImage(removeID, true); err != nil { logrus.Debugf("failed to remove intermediate image %q: %v", removeID, err) if b.forceRmIntermediateCtrs || !errors.Is(err, storage.ErrImageUsedByContainer) { lastErr = err } } } cleanupImages = nil if b.rusageLogFile != nil && b.rusageLogFile != b.out { // we deliberately ignore the error here, as this // function can be called multiple times if closer, ok := b.rusageLogFile.(interface{ Close() error }); ok { closer.Close() } } return lastErr } defer func() { if cleanupErr := cleanup(); cleanupErr != nil { if err == nil { err = cleanupErr } else { err = fmt.Errorf("%v: %w", cleanupErr.Error(), err) } } }() // dependencyMap contains dependencyInfo for each stage, // dependencyInfo is used later to mark if a particular // stage is needed by target or not. dependencyMap := make(map[string]*stageDependencyInfo) // Initialize afterDependency map to track --after= dependency per stage b.afterDependency = make(map[string]string) // Build maps of every named base image and every referenced stage root // filesystem. Individual stages can use them to determine whether or // not they can skip certain steps near the end of their stages. for stageIndex, stage := range stages { dependencyMap[stage.Name] = &stageDependencyInfo{Name: stage.Name, Position: stage.Position} stageLocalScopeArgs := make(map[string]string) node := stage.Node // first line for node != nil { // each line for _, child := range node.Children { // tokens on this line, though we only care about the first switch strings.ToUpper(child.Value) { // first token - instruction case "FROM": if child.Next != nil { // second token on this line // If we have a fromOverride, replace the value of // image name for the first FROM in the Containerfile. if b.fromOverride != "" { child.Next.Value = b.fromOverride b.fromOverride = "" } base := child.Next.Value if base != "" && base != buildah.BaseImageFakeName { if replaceBuildContext, ok := b.additionalBuildContexts[child.Next.Value]; ok { if replaceBuildContext.IsImage { child.Next.Value = replaceBuildContext.Value base = child.Next.Value } } builtinArgs := argsMapToSlice(stage.Builder.BuiltinArgDefaults) headingArgs := argsMapToSlice(stage.Builder.HeadingArgs) userArgs := argsMapToSlice(stage.Builder.Args) localScopeArgs := argsMapToSlice(stageLocalScopeArgs) // ProcessWord uses first match; put highest priority first so // --build-arg overrides stage ARG overrides header ARG overrides builtin. userArgs = slices.Concat(userArgs, localScopeArgs, headingArgs, builtinArgs) baseWithArg, err := imagebuilder.ProcessWord(base, userArgs) if err != nil { return "", nil, fmt.Errorf("while replacing arg variables with values for format %q: %w", base, err) } b.baseMap[baseWithArg] = struct{}{} logrus.Debugf("base for stage %d: %q resolves to %q", stageIndex, base, baseWithArg) // Check if selected base is not an additional // build context and if base is a valid stage // add it to current stage's dependency tree. if _, ok := b.additionalBuildContexts[baseWithArg]; !ok { if _, ok := dependencyMap[baseWithArg]; ok { // update current stage's dependency info currentStageInfo := dependencyMap[stage.Name] currentStageInfo.Needs = append(currentStageInfo.Needs, baseWithArg) } } } } // Parse any --after= flag for explicit stage dependency for _, flag := range child.Flags { if after, ok := strings.CutPrefix(flag, "--after="); ok { // only allow one --after flag per FROM for now; nothing necessarily // semantically wrong with multiple --after, but keeping it conservative until a // use case shows up if _, exists := b.afterDependency[stage.Name]; exists { return "", nil, fmt.Errorf("FROM --after=%s: only one --after flag is allowed per FROM instruction", after) } builtinArgs := argsMapToSlice(stage.Builder.BuiltinArgDefaults) headingArgs := argsMapToSlice(stage.Builder.HeadingArgs) userArgs := argsMapToSlice(stage.Builder.Args) localScopeArgs := argsMapToSlice(stageLocalScopeArgs) // ProcessWord uses first match; put highest priority first so // --build-arg overrides stage ARG overrides header ARG overrides builtin. userArgs = slices.Concat(userArgs, localScopeArgs, headingArgs, builtinArgs) afterResolved, err := imagebuilder.ProcessWord(after, userArgs) if err != nil { return "", nil, fmt.Errorf("while replacing arg variables with values for --after=%q: %w", after, err) } // If --after= convert index to name if index, err := strconv.Atoi(afterResolved); err == nil && index >= 0 && index < stageIndex { afterResolved = stages[index].Name } if depInfo, ok := dependencyMap[afterResolved]; !ok { return "", nil, fmt.Errorf("FROM --after=%s: stage %q not found", after, afterResolved) } else if depInfo.Position >= stageIndex { return "", nil, fmt.Errorf("FROM --after=%s: cannot depend on later stage %q", after, afterResolved) } // Mark the stage as a dep so we actually build it currentStageInfo := dependencyMap[stage.Name] currentStageInfo.Needs = append(currentStageInfo.Needs, afterResolved) // And mark it on the stage executor itself so it knows to wait before even pulling b.afterDependency[stage.Name] = afterResolved logrus.Debugf("stage %d: explicit dependency on %q via --after", stageIndex, afterResolved) } } case "ADD", "COPY": for _, flag := range child.Flags { // flags for this instruction if after, ok := strings.CutPrefix(flag, "--from="); ok { // Populate dependency tree and check // if following ADD or COPY needs any other // stage. stageName := after builtinArgs := argsMapToSlice(stage.Builder.BuiltinArgDefaults) headingArgs := argsMapToSlice(stage.Builder.HeadingArgs) userArgs := argsMapToSlice(stage.Builder.Args) localScopeArgs := argsMapToSlice(stageLocalScopeArgs) // ProcessWord uses first match; put highest priority first so // --build-arg overrides stage ARG overrides header ARG overrides builtin. userArgs = slices.Concat(userArgs, localScopeArgs, headingArgs, builtinArgs) baseWithArg, err := imagebuilder.ProcessWord(stageName, userArgs) if err != nil { return "", nil, fmt.Errorf("while replacing arg variables with values for format %q: %w", stageName, err) } if baseWithArg != "" { b.rootfsMap[baseWithArg] = struct{}{} } logrus.Debugf("stage %d name: %q resolves to %q", stageIndex, stageName, baseWithArg) stageName = baseWithArg // If --from= convert index to name if index, err := strconv.Atoi(stageName); err == nil && index >= 0 && index < stageIndex { stageName = stages[index].Name } // Check if selected base is not an additional // build context and if base is a valid stage // add it to current stage's dependency tree. if _, ok := b.additionalBuildContexts[stageName]; !ok { if _, ok := dependencyMap[stageName]; ok { // update current stage's dependency info currentStageInfo := dependencyMap[stage.Name] currentStageInfo.Needs = append(currentStageInfo.Needs, stageName) } } } } case "ARG": for arg := child.Next; arg != nil; arg = arg.Next { argName, argValue, hasValue := strings.Cut(arg.Value, "=") if hasValue && argName != "" { argValue, err := imagebuilder.ProcessWord(argValue, argsMapToSlice(stage.Builder.BuiltinArgDefaults)) if err != nil { return "", nil, fmt.Errorf("while replacing arg variables with values for format %q: %w", arg.Value, err) } stageLocalScopeArgs[argName] = argValue } } case "RUN": for _, flag := range child.Flags { // flags for this instruction // We need to populate dependency tree of stages // if it is using `--mount` and `from=` field is set // and `from=` points to a stage consider it in // dependency calculation. if strings.HasPrefix(flag, "--mount=") && strings.Contains(flag, "from") { mountFlags := strings.TrimPrefix(flag, "--mount=") fields := strings.SplitSeq(mountFlags, ",") for field := range fields { if mountFrom, hasFrom := strings.CutPrefix(field, "from="); hasFrom { // Check if this base is a stage if yes // add base to current stage's dependency tree // but also confirm if this is not in additional context. if _, ok := b.additionalBuildContexts[mountFrom]; !ok { // Treat from as a rootfs we need to preserve b.rootfsMap[mountFrom] = struct{}{} if _, ok := dependencyMap[mountFrom]; ok { // update current stage's dependency info currentStageInfo := dependencyMap[stage.Name] currentStageInfo.Needs = append(currentStageInfo.Needs, mountFrom) } } } } } } } } node = node.Next // next line } // Last stage is always target stage. // Since last/target stage is processed // let's calculate dependency map of stages // so we can mark stages which can be skipped. if stage.Position == (len(stages) - 1) { markDependencyStagesForTarget(dependencyMap, stage.Name) } } b.warnOnUnsetBuildArgs(stages, dependencyMap, b.args) type Result struct { Index int ImageID string OnlyBaseImage bool CommitResults buildah.CommitResults Error error } ch := make(chan Result, len(stages)) if b.stagesSemaphore == nil { b.stagesSemaphore = semaphore.NewWeighted(int64(len(stages))) } var wg sync.WaitGroup wg.Add(len(stages)) var commitResults buildah.CommitResults go func() { cancel := false for stageIndex := range stages { index := stageIndex // Acquire the semaphore before creating the goroutine so we are sure they // run in the specified order. if err := b.stagesSemaphore.Acquire(ctx, 1); err != nil { cancel = true b.lastError = err ch <- Result{ Index: index, Error: err, } wg.Done() continue } b.stagesLock.Lock() cleanupStages := cleanupStages b.stagesLock.Unlock() go func() { defer b.stagesSemaphore.Release(1) defer wg.Done() if cancel || cleanupStages == nil { var err error if stages[index].Name != strconv.Itoa(index) { err = fmt.Errorf("not building stage %d: build canceled", index) } else { err = fmt.Errorf("not building stage %d (%s): build canceled", index, stages[index].Name) } ch <- Result{ Index: index, Error: err, } return } // Skip stage if it is not needed by TargetStage // or any of its dependency stages and `SkipUnusedStages` // is not set to `false`. if stageDependencyInfo, ok := dependencyMap[stages[index].Name]; ok { if !stageDependencyInfo.NeededByTarget && b.skipUnusedStages != types.OptionalBoolFalse { logrus.Debugf("Skipping stage with Name %q and index %d since its not needed by the target stage", stages[index].Name, index) ch <- Result{ Index: index, Error: nil, } return } } stageID, stageResults, stageOnlyBaseImage, stageErr := b.buildStage(ctx, cleanupStages, stages, index) if stageErr != nil { cancel = true ch <- Result{ Index: index, Error: stageErr, OnlyBaseImage: stageOnlyBaseImage, } return } ch <- Result{ Index: index, ImageID: stageID, CommitResults: *stageResults, OnlyBaseImage: stageOnlyBaseImage, Error: nil, } }() } }() go func() { wg.Wait() close(ch) }() for r := range ch { stage := stages[r.Index] b.stagesLock.Lock() b.terminatedStage[stage.Name] = r.Error b.terminatedStage[strconv.Itoa(stage.Position)] = r.Error if r.Error != nil { b.stagesLock.Unlock() b.lastError = r.Error return "", nil, r.Error } // If this is an intermediate stage, make a note of the ID, so // that we can look it up later. if r.Index < len(stages)-1 && r.ImageID != "" { b.imageMap[stage.Name] = r.ImageID // We're not populating the cache with intermediate // images, so add this one to the list of images that // we'll remove later. // Only remove intermediate image if `--layers` is not provided, // `--save-stages` is not enabled, or following stage was not // only a base image (i.e. a different image). if !b.layers && !b.saveStages && !r.OnlyBaseImage { cleanupImages = append(cleanupImages, r.ImageID) } } if r.Index == len(stages)-1 { imageID = r.ImageID commitResults = r.CommitResults ref = commitResults.Canonical } b.stagesLock.Unlock() } if len(b.unusedArgs) > 0 { unusedList := make([]string, 0, len(b.unusedArgs)) for k := range b.unusedArgs { unusedList = append(unusedList, k) } slices.Sort(unusedList) fmt.Fprintf(b.out, "[Warning] one or more build args were not consumed: %v\n", unusedList) } // Add additional tags and print image names recorded in storage if dest, err := b.resolveNameToImageRef(b.output); err == nil { switch dest.Transport().Name() { case storageTransport.Transport.Name(): _, img, err := storageTransport.ResolveReference(dest) if err != nil { return imageID, ref, fmt.Errorf("locating just-written image %q: %w", transports.ImageName(dest), err) } if len(b.additionalTags) > 0 { if err = util.AddImageNames(b.store, "", b.systemContext, img, b.additionalTags); err != nil { return imageID, ref, fmt.Errorf("setting image names to %v: %w", append(img.Names, b.additionalTags...), err) } logrus.Debugf("assigned names %v to image %q", img.Names, img.ID) } // Report back the caller the tags applied, if any. _, img, err = storageTransport.ResolveReference(dest) if err != nil { return imageID, ref, fmt.Errorf("locating just-written image %q: %w", transports.ImageName(dest), err) } for _, name := range img.Names { fmt.Fprintf(b.out, "Successfully tagged %s\n", name) } default: if len(b.additionalTags) > 0 { b.logger.Warnf("don't know how to add tags to images stored in %q transport", dest.Transport().Name()) } } } if err := cleanup(); err != nil { return "", nil, err } logrus.Debugf("printing final image id %q", imageID) if b.iidfile != "" { iid := imageID if iid != "" { cdigest, err := digest.Parse("sha256:" + imageID) if err != nil { return imageID, ref, fmt.Errorf("coercing image ID into a digest structure: %w", err) } iid = cdigest.String() } if err = os.WriteFile(b.iidfile, []byte(iid), 0o644); err != nil { return imageID, ref, fmt.Errorf("failed to write image ID to file %q: %w", b.iidfile, err) } } if b.iidfileRaw != "" { if err = os.WriteFile(b.iidfileRaw, []byte(imageID), 0o644); err != nil { return imageID, ref, fmt.Errorf("failed to write image ID to file %q: %w", b.iidfileRaw, err) } } if b.iidfile == "" && b.iidfileRaw == "" { if _, err := stdout.Write([]byte(imageID + "\n")); err != nil { return imageID, ref, fmt.Errorf("failed to write image ID to stdout: %w", err) } } if b.metadataFile != "" { var cdigest digest.Digest if imageID != "" { if cdigest, err = digest.Parse("sha256:" + imageID); err != nil { return imageID, ref, fmt.Errorf("coercing image ID into a digest structure: %w", err) } } metadata, err := metadata.Build(cdigest, v1.Descriptor{ MediaType: commitResults.MediaType, Digest: commitResults.Digest, Size: int64(len(commitResults.ImageManifest)), }) if err != nil { return imageID, ref, fmt.Errorf("building metadata for metadata file: %w", err) } metadataBytes, err := json.Marshal(metadata) if err != nil { return imageID, ref, fmt.Errorf("encoding metadata for metadata file: %w", err) } if err = os.WriteFile(b.metadataFile, metadataBytes, 0o644); err != nil { return imageID, ref, fmt.Errorf("failed to write image metadata to file %q: %w", b.metadataFile, err) } } return imageID, ref, nil } // deleteSuccessfulIntermediateCtrs goes through the container IDs in each // stage's containerIDs list and deletes the containers associated with those // IDs. func (b *executor) deleteSuccessfulIntermediateCtrs() error { var lastErr error for _, s := range b.stages { for _, ctr := range s.containerIDs { if err := b.store.DeleteContainer(ctr); err != nil { b.logger.Errorf("error deleting build container %q: %v\n", ctr, err) lastErr = err } } // The stages map includes some stages under multiple keys, so // clearing their lists after we process a given stage is // necessary to avoid triggering errors that would occur if we // tried to delete a given stage's containers multiple times. s.containerIDs = nil } return lastErr } ================================================ FILE: imagebuildah/stage_executor.go ================================================ package imagebuildah import ( "context" "crypto/sha256" "errors" "fmt" "io" "os" "path" "path/filepath" "slices" "strconv" "strings" "time" "github.com/containers/buildah" "github.com/containers/buildah/copier" "github.com/containers/buildah/define" buildahdocker "github.com/containers/buildah/docker" "github.com/containers/buildah/internal" "github.com/containers/buildah/internal/metadata" "github.com/containers/buildah/internal/output" "github.com/containers/buildah/internal/sanitize" "github.com/containers/buildah/internal/tmpdir" internalUtil "github.com/containers/buildah/internal/util" "github.com/containers/buildah/pkg/parse" "github.com/containers/buildah/pkg/rusage" "github.com/containers/buildah/pkg/sourcepolicy" "github.com/containers/buildah/util" docker "github.com/fsouza/go-dockerclient" buildkitparser "github.com/moby/buildkit/frontend/dockerfile/parser" digest "github.com/opencontainers/go-digest" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/opencontainers/runtime-spec/specs-go" "github.com/openshift/imagebuilder" "github.com/openshift/imagebuilder/dockerfile/command" "github.com/openshift/imagebuilder/dockerfile/parser" "github.com/sirupsen/logrus" config "go.podman.io/common/pkg/config" cp "go.podman.io/image/v5/copy" imagedocker "go.podman.io/image/v5/docker" "go.podman.io/image/v5/docker/reference" "go.podman.io/image/v5/image" "go.podman.io/image/v5/manifest" is "go.podman.io/image/v5/storage" "go.podman.io/image/v5/transports" "go.podman.io/image/v5/types" "go.podman.io/storage" "go.podman.io/storage/pkg/chrootarchive" "go.podman.io/storage/pkg/unshare" ) // stageExecutor bundles up what we need to know when executing one stage of a // (possibly multi-stage) build. // Each stage may need to produce an image to be used as the base in a later // stage (with the last stage's image being the end product of the build), and // it may need to leave its working container in place so that the container's // root filesystem's contents can be used as the source for a COPY instruction // in a later stage. // Each stage has its own base image, so it starts with its own configuration // and set of volumes. // If we're naming the result of the build, only the last stage will apply that // name to the image that it produces. type stageExecutor struct { ctx context.Context systemContext *types.SystemContext executor *executor log func(format string, args ...any) index int stages imagebuilder.Stages name string builder *buildah.Builder preserved int volumes imagebuilder.VolumeSet // list of directories which are volumes volumeCache map[string]string // mapping from volume directories to cache archives (used by vfs method) volumeCacheInfo map[string]os.FileInfo // mapping from volume directories to perms/datestamps to reset after restoring mountPoint string output string containerIDs []string stage *imagebuilder.Stage didExecute bool argsFromContainerfile []string hasLink bool isLastStep bool } // Preserve informs the stage executor that from this point on, it needs to // ensure that only COPY and ADD instructions can modify the contents of this // directory or anything below it. // When CompatVolumes is enabled, the stageExecutor handles this by caching the // contents of directories which have been marked this way before executing a // RUN instruction, invalidating that cache when an ADD or COPY instruction // sets any location under the directory as the destination, and using the // cache to reset the contents of the directory tree after processing each RUN // instruction. // It would be simpler if we could just mark the directory as a read-only bind // mount of itself during Run(), but the directory is expected to be remain // writeable while the RUN instruction is being handled, even if any changes // made within the directory are ultimately discarded. func (s *stageExecutor) Preserve(path string) error { logrus.Debugf("PRESERVE %q in %q (already preserving %v)", path, s.builder.ContainerID, s.volumes) // Try and resolve the symlink (if one exists) // Set archivedPath and path based on whether a symlink is found or not var archivedPath string if evaluated, err := copier.Eval(s.mountPoint, filepath.Join(s.mountPoint, path), copier.EvalOptions{}); err == nil { symLink, err := filepath.Rel(s.mountPoint, evaluated) if err != nil { return fmt.Errorf("making evaluated path %q relative to %q: %w", evaluated, s.mountPoint, err) } if strings.HasPrefix(symLink, ".."+string(os.PathSeparator)) { return fmt.Errorf("evaluated path %q was not below %q", evaluated, s.mountPoint) } archivedPath = evaluated path = string(os.PathSeparator) + symLink } else { return fmt.Errorf("evaluating path %q: %w", path, err) } // Whether or not we're caching and restoring the contents of this // directory, we need to ensure it exists now. const createdDirPerms = os.FileMode(0o755) st, err := os.Stat(archivedPath) if errors.Is(err, os.ErrNotExist) { // Yup, we do have to create it. That means it's not in any // cached copy of the path that covers it, so we have to // invalidate such cached copy. logrus.Debugf("have to create volume %q", path) createdDirPerms := createdDirPerms if err := copier.Mkdir(s.mountPoint, archivedPath, copier.MkdirOptions{ChmodNew: &createdDirPerms}); err != nil { return fmt.Errorf("ensuring volume path exists: %w", err) } if err := s.volumeCacheInvalidate(path); err != nil { return fmt.Errorf("ensuring volume path %q is preserved: %w", filepath.Join(s.mountPoint, path), err) } if st, err = os.Stat(archivedPath); err != nil { return fmt.Errorf("checking on just-created volume path: %w", err) } } if err != nil { return fmt.Errorf("reading info cache for volume at %q: %w", path, err) } if s.volumes.Covers(path) { // This path is a subdirectory of a volume path that we're // already preserving, so there's nothing new to be done now // that we've ensured that it exists. s.volumeCacheInfo[path] = st return nil } // Add the new volume path to the ones that we're tracking. if !s.volumes.Add(path) { // This path is not a subdirectory of a volume path that we're // already preserving, so adding it to the list should have // worked. return fmt.Errorf("adding %q to the volume cache", path) } s.volumeCacheInfo[path] = st // If we're not doing save/restore, we're done, since volumeCache // should be empty. if s.executor.compatVolumes != types.OptionalBoolTrue { logrus.Debugf("not doing volume save-and-restore of %q in %q", path, s.builder.ContainerID) return nil } // Decide where the cache for this volume will be stored. s.preserved++ cacheDir, err := s.executor.store.ContainerDirectory(s.builder.ContainerID) if err != nil { return fmt.Errorf("unable to locate temporary directory for container") } cacheFile := filepath.Join(cacheDir, fmt.Sprintf("volume%d.tar", s.preserved)) s.volumeCache[path] = cacheFile // Now prune cache files for volumes that are newly supplanted by this one. removed := []string{} for cachedPath := range s.volumeCache { // Walk our list of cached volumes, and check that they're // still in the list of locations that we need to cache. found := slices.Contains(s.volumes, cachedPath) if !found { // We don't need to keep this volume's cache. Make a // note to remove it. removed = append(removed, cachedPath) } } // Actually remove the caches that we decided to remove. for _, cachedPath := range removed { archivedPath := filepath.Join(s.mountPoint, cachedPath) logrus.Debugf("no longer need cache of %q in %q", archivedPath, s.volumeCache[cachedPath]) if err := os.Remove(s.volumeCache[cachedPath]); err != nil { if errors.Is(err, os.ErrNotExist) { continue } return fmt.Errorf("removing cache of %q: %w", archivedPath, err) } delete(s.volumeCache, cachedPath) } return nil } // Remove any volume cache item which will need to be re-saved because we're // writing to part of it. func (s *stageExecutor) volumeCacheInvalidate(path string) error { invalidated := []string{} for cachedPath := range s.volumeCache { if strings.HasPrefix(path, cachedPath+string(os.PathSeparator)) { invalidated = append(invalidated, cachedPath) } } for _, cachedPath := range invalidated { if err := os.Remove(s.volumeCache[cachedPath]); err != nil { if errors.Is(err, os.ErrNotExist) { continue } return err } archivedPath := filepath.Join(s.mountPoint, cachedPath) logrus.Debugf("invalidated volume cache %q for %q from %q", archivedPath, path, s.volumeCache[cachedPath]) } return nil } // Save the contents of each of the executor's list of volumes for which we // don't already have a cache file. func (s *stageExecutor) volumeCacheSaveVFS() (mounts []specs.Mount, err error) { for cachedPath, cacheFile := range s.volumeCache { archivedPath, err := copier.Eval(s.mountPoint, filepath.Join(s.mountPoint, cachedPath), copier.EvalOptions{}) if err != nil { return nil, fmt.Errorf("evaluating volume path: %w", err) } relativePath, err := filepath.Rel(s.mountPoint, archivedPath) if err != nil { return nil, fmt.Errorf("converting %q into a path relative to %q: %w", archivedPath, s.mountPoint, err) } if strings.HasPrefix(relativePath, ".."+string(os.PathSeparator)) { return nil, fmt.Errorf("converting %q into a path relative to %q", archivedPath, s.mountPoint) } _, err = os.Stat(cacheFile) if err == nil { logrus.Debugf("contents of volume %q are already cached in %q", archivedPath, cacheFile) continue } if !errors.Is(err, os.ErrNotExist) { return nil, fmt.Errorf("checking for presence of a cached copy of %q at %q: %w", cachedPath, cacheFile, err) } logrus.Debugf("caching contents of volume %q in %q", archivedPath, cacheFile) cache, err := os.Create(cacheFile) if err != nil { return nil, fmt.Errorf("creating cache for volume %q: %w", archivedPath, err) } defer cache.Close() rc, err := chrootarchive.Tar(archivedPath, nil, s.mountPoint) if err != nil { return nil, fmt.Errorf("archiving %q: %w", archivedPath, err) } defer rc.Close() _, err = io.Copy(cache, rc) if err != nil { return nil, fmt.Errorf("archiving %q to %q: %w", archivedPath, cacheFile, err) } mount := specs.Mount{ Source: archivedPath, Destination: string(os.PathSeparator) + relativePath, Type: "bind", Options: []string{"private"}, } mounts = append(mounts, mount) } return nil, nil } // Restore the contents of each of the executor's list of volumes. func (s *stageExecutor) volumeCacheRestoreVFS() (err error) { for cachedPath, cacheFile := range s.volumeCache { archivedPath, err := copier.Eval(s.mountPoint, filepath.Join(s.mountPoint, cachedPath), copier.EvalOptions{}) if err != nil { return fmt.Errorf("evaluating volume path: %w", err) } logrus.Debugf("restoring contents of volume %q from %q", archivedPath, cacheFile) cache, err := os.Open(cacheFile) if err != nil { return fmt.Errorf("restoring contents of volume %q: %w", archivedPath, err) } defer cache.Close() if err := copier.Remove(s.mountPoint, archivedPath, copier.RemoveOptions{All: true}); err != nil { return err } err = chrootarchive.Untar(cache, archivedPath, nil) if err != nil { return fmt.Errorf("extracting archive at %q: %w", archivedPath, err) } if st, ok := s.volumeCacheInfo[cachedPath]; ok { if err := os.Chmod(archivedPath, st.Mode()); err != nil { return err } uid := 0 gid := 0 if st.Sys() != nil { uid = util.UID(st) gid = util.GID(st) } if err := os.Chown(archivedPath, uid, gid); err != nil { return err } if err := os.Chtimes(archivedPath, st.ModTime(), st.ModTime()); err != nil { return err } } } return nil } // Save the contents of each of the executor's list of volumes for which we // don't already have a cache file. For overlay, we "save" and "restore" by // using it as a lower for an overlay mount in the same location, and then // discarding the upper. func (s *stageExecutor) volumeCacheSaveOverlay() (mounts []specs.Mount, err error) { for cachedPath := range s.volumeCache { volumePath := filepath.Join(s.mountPoint, cachedPath) mount := specs.Mount{ Source: volumePath, Destination: cachedPath, Options: []string{"O", "private"}, } mounts = append(mounts, mount) } return mounts, nil } // Reset the contents of each of the executor's list of volumes. func (s *stageExecutor) volumeCacheRestoreOverlay() error { return nil } // Save the contents of each of the executor's list of volumes for which we // don't already have a cache file. func (s *stageExecutor) volumeCacheSave() (mounts []specs.Mount, err error) { switch s.executor.store.GraphDriverName() { case "overlay": return s.volumeCacheSaveOverlay() } return s.volumeCacheSaveVFS() } // Reset the contents of each of the executor's list of volumes. func (s *stageExecutor) volumeCacheRestore() error { switch s.executor.store.GraphDriverName() { case "overlay": return s.volumeCacheRestoreOverlay() } return s.volumeCacheRestoreVFS() } // Copy copies data into the working tree. The "Download" field is how // imagebuilder tells us the instruction was "ADD" and not "COPY". func (s *stageExecutor) Copy(excludes []string, copies ...imagebuilder.Copy) error { for _, cp := range copies { if cp.KeepGitDir { if cp.Download { return errors.New("ADD --keep-git-dir is not supported") } return errors.New("COPY --keep-git-dir is not supported") } if cp.Link && s.executor.layers { s.hasLink = true } else if cp.Link { s.executor.logger.Warn("--link is not supported when building without --layers, ignoring --link") s.hasLink = false } if len(cp.Excludes) > 0 { excludes = append(slices.Clone(excludes), cp.Excludes...) } } s.builder.ContentDigester.Restart() return s.performCopy(excludes, copies...) } func (s *stageExecutor) performCopy(excludes []string, copies ...imagebuilder.Copy) error { copiesExtend := []imagebuilder.Copy{} for _, copy := range copies { if err := s.volumeCacheInvalidate(copy.Dest); err != nil { return err } var sources []string // The From field says to read the content from another // container. Update the ID mappings and // all-content-comes-from-below-this-directory value. var idMappingOptions *define.IDMappingOptions var copyExcludes []string stripSetuid := false stripSetgid := false preserveOwnership := false contextDir := s.executor.contextDir // If we are copying files via heredoc syntax, then // its time to create these temporary files on host // and copy these to container if len(copy.Files) > 0 { // If we are copying files from heredoc syntax, there // maybe regular files from context as well so split and // process them differently if len(copy.Src) > len(copy.Files) { regularSources := []string{} for _, src := range copy.Src { // If this source is not a heredoc, then it is a regular file from // build context or from another stage (`--from=`) so treat this differently. if !strings.HasPrefix(src, "<<") { regularSources = append(regularSources, src) } } copyEntry := copy // Remove heredoc if any, since we are already processing them // so create new entry with sources containing regular files // only, since regular files can have different context then // heredoc files. copyEntry.Files = nil copyEntry.Src = regularSources copiesExtend = append(copiesExtend, copyEntry) } copySources := []string{} for _, file := range copy.Files { data := file.Data // remove first break line added while parsing heredoc data = strings.TrimPrefix(data, "\n") // add breakline when heredoc ends for docker compat data = data + "\n" // Create separate subdir for this file. tmpDir, err := os.MkdirTemp(parse.GetTempDir(), "buildah-heredoc") if err != nil { return fmt.Errorf("unable to create tmp dir for heredoc run %q: %w", parse.GetTempDir(), err) } defer os.RemoveAll(tmpDir) tmpFile, err := os.Create(filepath.Join(tmpDir, path.Base(filepath.ToSlash(file.Name)))) if err != nil { return fmt.Errorf("unable to create tmp file for COPY instruction at %q: %w", parse.GetTempDir(), err) } err = tmpFile.Chmod(0o644) // 644 is consistent with buildkit if err != nil { tmpFile.Close() return fmt.Errorf("unable to chmod tmp file created for COPY instruction at %q: %w", tmpFile.Name(), err) } defer os.Remove(tmpFile.Name()) _, err = tmpFile.WriteString(data) if err != nil { tmpFile.Close() return fmt.Errorf("unable to write contents of heredoc file at %q: %w", tmpFile.Name(), err) } copySources = append(copySources, filepath.Join(filepath.Base(tmpDir), filepath.Base(tmpFile.Name()))) tmpFile.Close() } contextDir = parse.GetTempDir() copy.Src = copySources } if copy.From != "" && len(copy.Files) == 0 { // If from has an argument within it, resolve it to its // value. Otherwise just return the value found. from, fromErr := imagebuilder.ProcessWord(copy.From, s.stage.Builder.Arguments()) if fromErr != nil { return fmt.Errorf("unable to resolve argument %q: %w", copy.From, fromErr) } var additionalBuildContext *define.AdditionalBuildContext if foundContext, ok := s.executor.additionalBuildContexts[from]; ok { additionalBuildContext = foundContext } else { // Maybe index is given in COPY --from=index // if that's the case check if provided index // exists and if stage short_name matches any // additionalContext replace stage with additional // build context. if index, err := strconv.Atoi(from); err == nil && index >= 0 && index < s.index { from = s.stages[index].Name } if foundContext, ok := s.executor.additionalBuildContexts[from]; ok { additionalBuildContext = foundContext } } if additionalBuildContext != nil { if !additionalBuildContext.IsImage { contextDir = additionalBuildContext.Value if additionalBuildContext.IsURL { // Check if following buildContext was already // downloaded before in any other RUN step. If not // download it and populate DownloadCache field for // future RUN steps. if additionalBuildContext.DownloadedCache == "" { // additional context contains a tar file // so download and explode tar to buildah // temp and point context to that. path, subdir, err := define.TempDirForURL(tmpdir.GetTempDir(), internal.BuildahExternalArtifactsDir, additionalBuildContext.Value) if err != nil { return fmt.Errorf("unable to download context from external source %q: %w", additionalBuildContext.Value, err) } // point context dir to the extracted path contextDir = filepath.Join(path, subdir) // populate cache for next RUN step additionalBuildContext.DownloadedCache = contextDir } else { contextDir = additionalBuildContext.DownloadedCache } } else { // This points to a path on the filesystem // Check to see if there's a .containerignore // file, update excludes for this stage before // proceeding buildContextExcludes, _, err := parse.ContainerIgnoreFile(additionalBuildContext.Value, "", nil) if err != nil { return err } excludes = append(excludes, buildContextExcludes...) } } else { copy.From = additionalBuildContext.Value } } if additionalBuildContext == nil { if isStage, err := s.executor.waitForStage(s.ctx, from, s.stages[:s.index]); isStage && err != nil { return err } if other, ok := s.executor.stages[from]; ok && other.index < s.index { contextDir = other.mountPoint idMappingOptions = &other.builder.IDMappingOptions } else if builder, ok := s.executor.containerMap[copy.From]; ok { contextDir = builder.MountPoint idMappingOptions = &builder.IDMappingOptions } else { return fmt.Errorf("the stage %q has not been built", copy.From) } } else if additionalBuildContext.IsImage { // Image was selected as additionalContext so only process image. mountPoint, err := s.getImageRootfs(s.ctx, copy.From) if err != nil { return err } contextDir = mountPoint } // With --from set, the content being copied isn't coming from the default // build context directory, so we're not expected to force everything to 0:0 preserveOwnership = true copyExcludes = excludes } else { copyExcludes = append(s.executor.excludes, excludes...) stripSetuid = true // did this change between 18.06 and 19.03? stripSetgid = true // did this change between 18.06 and 19.03? } if copy.Download { logrus.Debugf("ADD %#v, %#v", excludes, copy) } else { logrus.Debugf("COPY %#v, %#v", excludes, copy) } for _, src := range copy.Src { if strings.HasPrefix(src, "http://") || strings.HasPrefix(src, "https://") { // Source is a URL, allowed for ADD but not COPY. if copy.Download { sources = append(sources, src) } else { // returns an error to be compatible with docker return fmt.Errorf("source can't be a URL for COPY") } } else { // filepath.Join clean path so /./ is removed if _, suffix, found := strings.Cut(src, "/./"); found && copy.Parents { fullPath := filepath.Join(contextDir, src) suffix = filepath.Clean(suffix) prefix := strings.TrimSuffix(fullPath, suffix) prefix = filepath.Clean(prefix) src = prefix + "/./" + suffix } else { src = filepath.Join(contextDir, src) } sources = append(sources, src) } } labelsAndAnnotations := s.buildMetadata(s.isLastStep, true) options := buildah.AddAndCopyOptions{ Chmod: copy.Chmod, Chown: copy.Chown, Checksum: copy.Checksum, PreserveOwnership: preserveOwnership, ContextDir: contextDir, Excludes: copyExcludes, IgnoreFile: s.executor.ignoreFile, IDMappingOptions: idMappingOptions, StripSetuidBit: stripSetuid, StripSetgidBit: stripSetgid, // The values for these next two fields are ultimately // based on command line flags with names that sound // much more generic. CertPath: s.systemContext.DockerCertPath, InsecureSkipTLSVerify: s.systemContext.DockerInsecureSkipTLSVerify, MaxRetries: s.executor.maxPullPushRetries, RetryDelay: s.executor.retryPullPushDelay, Parents: copy.Parents, Link: s.hasLink, BuildMetadata: labelsAndAnnotations, } if len(copy.Files) > 0 { // If we are copying heredoc files, we need to temporary place // them in the context dir and then move to container via copier // there are cases where .containerignore can have a patterns like // '*' which can match our heredoc files so let's not set any excludes // or IgnoreFile for this copy. options.Excludes = nil options.IgnoreFile = "" } if err := s.builder.Add(copy.Dest, copy.Download, options, sources...); err != nil { return err } } if len(copiesExtend) > 0 { // If we found heredocs and regularfiles together // in same statement then we produced new copies to // process regular files separately since they need // different context. return s.performCopy(excludes, copiesExtend...) } return nil } // Returns a map of StageName/ImageName:internal.StageMountDetails for the // items in the passed-in mounts list which include a "from=" value. The "" // key in the returned map corresponds to the default build context. func (s *stageExecutor) runStageMountPoints(mountList []string) (map[string]internal.StageMountDetails, error) { stageMountPoints := make(map[string]internal.StageMountDetails) stageMountPoints[""] = internal.StageMountDetails{ MountPoint: s.executor.contextDir, IsWritesDiscardedOverlay: s.executor.contextDirWritesAreDiscarded, } for _, flag := range mountList { if strings.Contains(flag, "from") { tokens := strings.Split(flag, ",") if len(tokens) < 2 { return nil, fmt.Errorf("invalid --mount command: %s", flag) } for _, token := range tokens { key, val, hasVal := strings.Cut(token, "=") switch key { case "from": if !hasVal { return nil, fmt.Errorf("unable to resolve argument for `from=`: bad argument") } if val == "" { return nil, fmt.Errorf("unable to resolve argument for `from=`: from points to an empty value") } from, fromErr := imagebuilder.ProcessWord(val, s.stage.Builder.Arguments()) if fromErr != nil { return nil, fmt.Errorf("unable to resolve argument %q: %w", val, fromErr) } // If the value corresponds to an additional build context, // the mount source is either either the rootfs of the image, // the filesystem path, or a temporary directory populated // with the contents of the URL, all in preference to any // stage which might have the value as its name. if additionalBuildContext, ok := s.executor.additionalBuildContexts[from]; ok { if additionalBuildContext.IsImage { mountPoint, err := s.getImageRootfs(s.ctx, additionalBuildContext.Value) if err != nil { return nil, fmt.Errorf("%s from=%s: image not found with that name", flag, from) } // The `from` in stageMountPoints should point // to `mountPoint` replaced from additional // build-context. Reason: Parser will use this // `from` to refer from stageMountPoints map later. stageMountPoints[from] = internal.StageMountDetails{ IsAdditionalBuildContext: true, IsImage: true, DidExecute: true, MountPoint: mountPoint, } break } // Most likely this points to path on filesystem // or external tar archive, Treat it as a stage // nothing is different for this. So process and // point mountPoint to path on host and it will // be automatically handled correctly by since // GetBindMount will honor IsStage:false while // processing stageMountPoints. mountPoint := additionalBuildContext.Value if additionalBuildContext.IsURL { // Check if following buildContext was already // downloaded before in any other RUN step. If not // download it and populate DownloadCache field for // future RUN steps. if additionalBuildContext.DownloadedCache == "" { // additional context contains a tar file // so download and explode tar to buildah // temp and point context to that. path, subdir, err := define.TempDirForURL(tmpdir.GetTempDir(), internal.BuildahExternalArtifactsDir, additionalBuildContext.Value) if err != nil { return nil, fmt.Errorf("unable to download context from external source %q: %w", additionalBuildContext.Value, err) } // point context dir to the extracted path mountPoint = filepath.Join(path, subdir) // populate cache for next RUN step additionalBuildContext.DownloadedCache = mountPoint } else { mountPoint = additionalBuildContext.DownloadedCache } } stageMountPoints[from] = internal.StageMountDetails{ IsAdditionalBuildContext: true, DidExecute: true, MountPoint: mountPoint, } break } // If the source's name corresponds to the // result of an earlier stage, wait for that // stage to finish being built. if isStage, err := s.executor.waitForStage(s.ctx, from, s.stages[:s.index]); isStage && err != nil { return nil, err } // If the source's name is a stage, return a // pointer to its rootfs. if otherStage, ok := s.executor.stages[from]; ok && otherStage.index < s.index { stageMountPoints[from] = internal.StageMountDetails{ IsStage: true, DidExecute: otherStage.didExecute, MountPoint: otherStage.mountPoint, } break } // Otherwise, treat the source's name as the name of an image. mountPoint, err := s.getImageRootfs(s.ctx, from) if err != nil { return nil, fmt.Errorf("%s from=%s: no stage or image found with that name", flag, from) } stageMountPoints[from] = internal.StageMountDetails{ IsImage: true, DidExecute: true, MountPoint: mountPoint, } default: continue } } } } return stageMountPoints, nil } func (s *stageExecutor) createNeededHeredocMountsForRun(files []imagebuilder.File) ([]Mount, error) { mountResult := []Mount{} for _, file := range files { f, err := os.CreateTemp(parse.GetTempDir(), "buildahheredoc") if err != nil { return nil, err } if _, err := f.WriteString(file.Data); err != nil { f.Close() return nil, err } err = f.Chmod(0o755) if err != nil { f.Close() return nil, err } // dest path is same as buildkit for compat dest := filepath.Join("/dev/pipes/", filepath.Base(f.Name())) mount := Mount{Destination: dest, Type: define.TypeBind, Source: f.Name(), Options: append(define.BindOptions, "rprivate", "z", "Z")} mountResult = append(mountResult, mount) f.Close() } return mountResult, nil } func parseSheBang(data string) string { lines := strings.Split(data, "\n") if len(lines) > 2 && strings.HasPrefix(lines[1], "#!") { shebang := strings.TrimLeft(lines[1], "#!") return shebang } return "" } // Run executes a RUN instruction using the stage's current working container // as a root directory. func (s *stageExecutor) Run(run imagebuilder.Run, config docker.Config) error { logrus.Debugf("RUN %#v, %#v", run, config) args := run.Args heredocMounts := []Mount{} if len(run.Files) > 0 { if heredoc := buildkitparser.MustParseHeredoc(args[0]); heredoc != nil { if strings.HasPrefix(run.Files[0].Data, "#!") || strings.HasPrefix(run.Files[0].Data, "\n#!") { // This is a single heredoc with a shebang, so create a file // and run it with program specified in shebang. heredocMount, err := s.createNeededHeredocMountsForRun(run.Files) if err != nil { return err } shebangArgs := parseSheBang(run.Files[0].Data) if shebangArgs != "" { args = []string{shebangArgs + " " + heredocMount[0].Destination} } else { args = []string{heredocMount[0].Destination} } heredocMounts = append(heredocMounts, heredocMount...) } else { args = []string{run.Files[0].Data} } } else { full := args[0] for _, file := range run.Files { full += file.Data + "\n" + file.Name } args = []string{full} } } stageMountPoints, err := s.runStageMountPoints(slices.Concat(run.Mounts, s.executor.transientRunMounts)) if err != nil { return err } if s.builder == nil { return fmt.Errorf("no build container available") } stdin := s.executor.in if stdin == nil { devNull, err := os.Open(os.DevNull) if err != nil { return fmt.Errorf("opening %q for reading: %v", os.DevNull, err) } defer devNull.Close() stdin = devNull } namespaceOptions := slices.Clone(s.executor.namespaceOptions) options := buildah.RunOptions{ Args: s.executor.runtimeArgs, Cmd: config.Cmd, ContextDir: s.executor.contextDir, ConfigureNetwork: s.executor.configureNetwork, Entrypoint: config.Entrypoint, Env: config.Env, Hostname: config.Hostname, Logger: s.executor.logger, Mounts: slices.Clone(s.executor.transientMounts), NamespaceOptions: namespaceOptions, NoHostname: s.executor.noHostname, NoHosts: s.executor.noHosts, NoPivot: os.Getenv("BUILDAH_NOPIVOT") != "" || s.executor.noPivotRoot, Quiet: s.executor.quiet, CompatBuiltinVolumes: types.OptionalBoolFalse, RunMounts: slices.Concat(run.Mounts, s.executor.transientRunMounts), Runtime: s.executor.runtime, Secrets: s.executor.secrets, SSHSources: s.executor.sshsources, StageMountPoints: stageMountPoints, Stderr: s.executor.err, Stdin: stdin, Stdout: s.executor.out, SystemContext: s.systemContext, Terminal: buildah.WithoutTerminal, User: config.User, WorkingDir: config.WorkingDir, } // Honor `RUN --network=<>`. switch run.Network { case "host": options.NamespaceOptions.AddOrReplace(define.NamespaceOption{Name: "network", Host: true}) options.ConfigureNetwork = define.NetworkEnabled case "none": options.ConfigureNetwork = define.NetworkDisabled case "", "default": // do nothing default: return fmt.Errorf(`unsupported value %q for "RUN --network", must be either "host" or "none"`, run.Network) } if config.NetworkDisabled { options.ConfigureNetwork = buildah.NetworkDisabled } if run.Shell { if len(config.Shell) > 0 { args = append(config.Shell, args...) } else { args = append([]string{"/bin/sh", "-c"}, args...) } } if s.executor.compatVolumes == types.OptionalBoolTrue { // Only bother with saving/restoring the contents of volumes if // we've been specifically asked to. mounts, err := s.volumeCacheSave() if err != nil { return err } options.Mounts = append(options.Mounts, mounts...) } // The list of built-in volumes isn't passed in via RunOptions, so make // sure the builder's list of built-in volumes includes anything that // the configuration thinks is a built-in volume. s.builder.ClearVolumes() for v := range config.Volumes { s.builder.AddVolume(v) } if len(heredocMounts) > 0 { options.Mounts = append(options.Mounts, heredocMounts...) } err = s.builder.Run(args, options) if s.executor.compatVolumes == types.OptionalBoolTrue { // Only bother with saving/restoring the contents of volumes if // we've been specifically asked to. if err2 := s.volumeCacheRestore(); err2 != nil { if err == nil { return err2 } } } return err } // UnrecognizedInstruction is called when we encounter an instruction that the // imagebuilder parser didn't understand. func (s *stageExecutor) UnrecognizedInstruction(step *imagebuilder.Step) error { errStr := fmt.Sprintf("Build error: Unknown instruction: %q ", strings.ToUpper(step.Command)) err := fmt.Sprintf(errStr+"%#v", step) if s.executor.ignoreUnrecognizedInstructions { logrus.Debug(err) return nil } switch logrus.GetLevel() { case logrus.ErrorLevel: s.executor.logger.Error(errStr) case logrus.DebugLevel: logrus.Debug(err) default: s.executor.logger.Errorf("+(UNHANDLED LOGLEVEL) %#v", step) } return errors.New(err) } // sanitizeFrom limits which image names (with or without transport prefixes) // we'll accept. For those it accepts which refer to filesystem objects, where // relative path names are evaluated relative to "contextDir", it will create a // copy of the original image, under "tmpdir", which contains no symbolic // links, and return either the original image reference or a reference to a // sanitized copy which should be used instead. func (s *stageExecutor) sanitizeFrom(from, tmpdir string) (newFrom string, err error) { transportName, restOfImageName, maybeHasTransportName := strings.Cut(from, ":") if !maybeHasTransportName || transports.Get(transportName) == nil { if _, err = reference.ParseNormalizedNamed(from); err == nil { // this is a normal-looking image-in-a-registry-or-named-in-storage name return from, nil } if img, err := s.executor.store.Image(from); img != nil && err == nil { // this is an image ID return from, nil } return "", fmt.Errorf("parsing image name %q: %w", from, err) } // TODO: drop this part and just return an error... someday return sanitize.ImageName(transportName, restOfImageName, s.executor.contextDir, tmpdir) } // prepare creates a working container based on the specified image, or if one // isn't specified, the first argument passed to the first FROM instruction we // can find in the stage's parsed tree. func (s *stageExecutor) prepare(ctx context.Context, from string, initializeIBConfig, rebase, preserveBaseImageAnnotations bool, pullPolicy define.PullPolicy) (builder *buildah.Builder, err error) { stage := s.stage ib := stage.Builder node := stage.Node if from == "" { base, err := ib.From(node) if err != nil { logrus.Debugf("prepare(node.Children=%#v)", node.Children) return nil, fmt.Errorf("determining starting point for build: %w", err) } from = base } // Apply source policy if one is configured and this is not "scratch" or a stage reference. // Stage references are handled separately and don't need policy evaluation since they // refer to images built within this same build. if s.executor.sourcePolicy != nil && from != "scratch" { // Check if 'from' references a previous stage by name, index, or image ID isStageRef := false for i, st := range s.stages[:s.index] { if st.Name == from || strconv.Itoa(i) == from { isStageRef = true break } } // Also check if 'from' is an image ID that was created by a previous stage // (this happens when execute() resolves stage names to image IDs before calling prepare) if !isStageRef { s.executor.stagesLock.Lock() for _, imgID := range s.executor.imageMap { if imgID == from { isStageRef = true break } } s.executor.stagesLock.Unlock() } if !isStageRef { sourceID := sourcepolicy.ImageSourceIdentifier(from) decision, matched, err := s.executor.sourcePolicy.Evaluate(sourceID) if err != nil { return nil, fmt.Errorf("evaluating source policy for %q: %w", from, err) } if matched { switch decision.Action { case sourcepolicy.ActionDeny: return nil, fmt.Errorf("source %q denied by source policy: %s", from, decision.Reason) case sourcepolicy.ActionConvert: // Extract the new image reference from the policy decision newFrom := sourcepolicy.ExtractImageRef(decision.TargetRef) logrus.Debugf("source policy: converting %q to %q (%s)", from, newFrom, decision.Reason) from = newFrom case sourcepolicy.ActionAllow: logrus.Debugf("source policy: allowing %q (%s)", from, decision.Reason) } } } } sanitizedFrom, err := s.sanitizeFrom(from, tmpdir.GetTempDir()) if err != nil { return nil, fmt.Errorf("invalid base image specification %q: %w", from, err) } displayFrom := from if ib.Platform != "" { displayFrom = "--platform=" + ib.Platform + " " + displayFrom } // stage.Name will be a numeric string for all stages without an "AS" clause asImageName := stage.Name if asImageName != "" { if _, err := strconv.Atoi(asImageName); err != nil { displayFrom += " AS " + asImageName } } if initializeIBConfig && rebase { logrus.Debugf("FROM %#v", displayFrom) if !s.executor.quiet { s.log("FROM %s", displayFrom) } } // In a multi-stage build where `FROM --platform=<>` was used then we must // reset context for new stages so that new stages don't inherit unexpected // `--platform` from prior stages. if stage.Builder.Platform != "" || (len(s.stages) > 1 && (s.systemContext.ArchitectureChoice == "" && s.systemContext.VariantChoice == "" && s.systemContext.OSChoice == "")) { imageOS, imageArch, imageVariant, err := parse.Platform(stage.Builder.Platform) if err != nil { return nil, fmt.Errorf("unable to parse platform %q: %w", stage.Builder.Platform, err) } if imageArch != "" || imageVariant != "" { s.systemContext.ArchitectureChoice = imageArch s.systemContext.VariantChoice = imageVariant } if imageOS != "" { s.systemContext.OSChoice = imageOS } } builderOptions := buildah.BuilderOptions{ Args: ib.Args, FromImage: sanitizedFrom, GroupAdd: s.executor.groupAdd, PullPolicy: pullPolicy, ContainerSuffix: s.executor.containerSuffix, Registry: s.executor.registry, BlobDirectory: s.executor.blobDirectory, SignaturePolicyPath: s.executor.signaturePolicyPath, ReportWriter: s.executor.reportWriter, SystemContext: s.systemContext, Isolation: s.executor.isolation, NamespaceOptions: s.executor.namespaceOptions, ConfigureNetwork: s.executor.configureNetwork, NetworkInterface: s.executor.networkInterface, IDMappingOptions: s.executor.idmappingOptions, CommonBuildOpts: s.executor.commonBuildOptions, DefaultMountsFilePath: s.executor.defaultMountsFilePath, Format: s.executor.outputFormat, Capabilities: s.executor.capabilities, Devices: s.executor.devices, DeviceSpecs: s.executor.deviceSpecs, MaxPullRetries: s.executor.maxPullPushRetries, PullRetryDelay: s.executor.retryPullPushDelay, OciDecryptConfig: s.executor.ociDecryptConfig, Logger: s.executor.logger, ProcessLabel: s.executor.processLabel, MountLabel: s.executor.mountLabel, PreserveBaseImageAnns: preserveBaseImageAnnotations, CDIConfigDir: s.executor.cdiConfigDir, CompatScratchConfig: s.executor.compatScratchConfig, } builder, err = buildah.NewBuilder(ctx, s.executor.store, builderOptions) if err != nil { return nil, fmt.Errorf("creating build container: %w", err) } if initializeIBConfig { volumes := map[string]struct{}{} for _, v := range builder.Volumes() { volumes[v] = struct{}{} } ports := map[docker.Port]struct{}{} for _, p := range builder.Ports() { ports[docker.Port(p)] = struct{}{} } hostname, domainname := builder.Hostname(), builder.Domainname() containerName := builder.Container if s.executor.timestamp != nil || s.executor.sourceDateEpoch != nil { hostname, domainname, containerName = "sandbox", "", "" } dConfig := docker.Config{ Hostname: hostname, Domainname: domainname, User: builder.User(), Env: builder.Env(), Cmd: builder.Cmd(), Image: from, Volumes: volumes, WorkingDir: builder.WorkDir(), Entrypoint: builder.Entrypoint(), Healthcheck: (*docker.HealthConfig)(builder.Healthcheck()), Labels: builder.Labels(), Shell: builder.Shell(), StopSignal: builder.StopSignal(), OnBuild: builder.OnBuild(), ExposedPorts: ports, } var rootfs *docker.RootFS if builder.Docker.RootFS != nil { rootfs = &docker.RootFS{ Type: builder.Docker.RootFS.Type, } for _, id := range builder.Docker.RootFS.DiffIDs { rootfs.Layers = append(rootfs.Layers, id.String()) } } dImage := docker.Image{ Parent: builder.FromImageID, ContainerConfig: dConfig, Container: containerName, Author: builder.Maintainer(), Architecture: builder.Architecture(), RootFS: rootfs, } dImage.Config = &dImage.ContainerConfig if s.executor.inheritLabels == types.OptionalBoolFalse { // If user has selected `--inherit-labels=false` let's not // inherit labels from base image. dImage.Config.Labels = nil } err = ib.FromImage(&dImage, node) if err != nil { if err2 := builder.Delete(); err2 != nil { logrus.Debugf("error deleting container which we failed to update: %v", err2) } return nil, fmt.Errorf("updating build context: %w", err) } } mountPoint, err := builder.Mount(builder.MountLabel) if err != nil { if err2 := builder.Delete(); err2 != nil { logrus.Debugf("error deleting container which we failed to mount: %v", err2) } return nil, fmt.Errorf("mounting new container: %w", err) } if rebase { // Make this our "current" working container. s.mountPoint = mountPoint s.builder = builder // Now that the rootfs is mounted, set up handling of volumes from the base image. s.volumes = make([]string, 0, len(s.volumes)) s.volumeCache = make(map[string]string) s.volumeCacheInfo = make(map[string]os.FileInfo) for _, v := range builder.Volumes() { if err := s.Preserve(v); err != nil { return nil, fmt.Errorf("marking base image volume %q for preservation: %w", v, err) } } } logrus.Debugln("Container ID:", builder.ContainerID) return builder, nil } // Delete deletes the stage's working container, if we have one. func (s *stageExecutor) Delete() (err error) { if s.builder != nil { err = s.builder.Delete() s.builder = nil } return err } // stepRequiresLayer indicates whether or not the step should be followed by // committing a layer container when creating an intermediate image. func (*stageExecutor) stepRequiresLayer(step *imagebuilder.Step) bool { switch strings.ToUpper(step.Command) { case "ADD", "COPY", "RUN": return true } return false } // getImageRootfs checks for an image matching the passed-in name in local // storage. If it isn't found, it pulls down a copy. Then, if we don't have a // working container root filesystem based on the image, it creates one. Then // it returns that root filesystem's location. func (s *stageExecutor) getImageRootfs(ctx context.Context, image string) (mountPoint string, err error) { if builder, ok := s.executor.containerMap[image]; ok { return builder.MountPoint, nil } builder, err := s.prepare(ctx, image, false, false, false, s.executor.pullPolicy) if err != nil { return "", err } s.executor.containerMap[image] = builder return builder.MountPoint, nil } // getContentSummary generates a description of what was most recently added to the container, // typically in the form "file", "dir", or "multi" followed by a colon and the hex part of the // digest of the content, for inclusion in the corresponding history entry's "createdBy" field func (s *stageExecutor) getContentSummaryAfterAddingContent() string { contentType, digest := s.builder.ContentDigester.Digest() summary := contentType if digest != "" { if summary != "" { summary = summary + ":" } summary = summary + digest.Encoded() logrus.Debugf("added content %s", summary) } return summary } // Execute runs each of the steps in the stage's parsed tree, in turn. func (s *stageExecutor) execute(ctx context.Context, base string) (imgID string, commitResults *buildah.CommitResults, onlyBaseImg bool, err error) { var resourceUsage rusage.Rusage stage := s.stage ib := stage.Builder checkForLayers := s.executor.layers && s.executor.useCache moreStages := s.index < len(s.stages)-1 lastStage := !moreStages onlyBaseImage := false imageIsUsedLater := moreStages && (internalUtil.SetHas(s.executor.baseMap, stage.Name) || internalUtil.SetHas(s.executor.baseMap, strconv.Itoa(stage.Position))) rootfsIsUsedLater := moreStages && (internalUtil.SetHas(s.executor.rootfsMap, stage.Name) || internalUtil.SetHas(s.executor.rootfsMap, strconv.Itoa(stage.Position))) // If the base image's name corresponds to the result of an earlier // stage, make sure that stage has finished building an image, and // substitute that image's ID for the base image's name here and force // the pull policy to "never" to avoid triggering an error when it's // set to "always", which doesn't make sense for image IDs. // If not, then go on assuming that it's just a regular image that's // either in local storage, or one that we have to pull from a // registry, subject to the passed-in pull policy. if isStage, err := s.executor.waitForStage(ctx, base, s.stages[:s.index]); isStage && err != nil { return "", nil, false, err } pullPolicy := s.executor.pullPolicy s.executor.stagesLock.Lock() var preserveBaseImageAnnotationsAtStageStart bool if stageImage, isPreviousStage := s.executor.imageMap[base]; isPreviousStage { base = stageImage pullPolicy = define.PullNever preserveBaseImageAnnotationsAtStageStart = true } s.executor.stagesLock.Unlock() // Set things up so that we can log resource usage as we go. logRusage := func() { if rusage.Supported() { usage, err := rusage.Get() if err != nil { fmt.Fprintf(s.executor.out, "error gathering resource usage information: %v\n", err) return } if s.executor.rusageLogFile != nil { fmt.Fprintf(s.executor.rusageLogFile, "%s\n", rusage.FormatDiff(usage.Subtract(resourceUsage))) } resourceUsage = usage } } // Start counting resource usage before we potentially pull a base image. if rusage.Supported() { if resourceUsage, err = rusage.Get(); err != nil { return "", nil, false, err } // Log the final incremental resource usage counter before we return. defer logRusage() } // Create the (first) working container for this stage. Reinitializing // the imagebuilder configuration may alter the list of steps we have, // so take a snapshot of them *after* that. if _, err := s.prepare(ctx, base, true, true, preserveBaseImageAnnotationsAtStageStart, pullPolicy); err != nil { return "", nil, false, err } children := stage.Node.Children // A helper function to only log "COMMIT" as an explicit step if it's // the very last step of a (possibly multi-stage) build. logCommit := func(output string, instruction int) { moreInstructions := instruction < len(children)-1 if moreInstructions || moreStages { return } commitMessage := "COMMIT" if output != "" { commitMessage = fmt.Sprintf("%s %s", commitMessage, output) } logrus.Debug(commitMessage) if !s.executor.quiet { s.log(commitMessage) } } // logCachePulled produces build log for cases when `--cache-from` // is used and a valid intermediate image is pulled from remote source. logCachePulled := func(cacheKey string, remote reference.Named) { if !s.executor.quiet { cachePullMessage := "--> Cache pulled from remote" fmt.Fprintf(s.executor.out, "%s %s\n", cachePullMessage, fmt.Sprintf("%s:%s", remote.String(), cacheKey)) } } // logCachePush produces build log for cases when `--cache-to` // is used and a valid intermediate image is pushed tp remote source. logCachePush := func(cacheKey string) { if !s.executor.quiet { cachePushMessage := "--> Pushing cache" fmt.Fprintf(s.executor.out, "%s %s\n", cachePushMessage, fmt.Sprintf("%s:%s", s.executor.cacheTo, cacheKey)) } } logCacheHit := func(cacheID string) { if !s.executor.quiet { cacheHitMessage := "--> Using cache" fmt.Fprintf(s.executor.out, "%s %s\n", cacheHitMessage, cacheID) } } logImageID := func(imgID string) { if len(imgID) > 12 { imgID = imgID[:12] } if s.executor.iidfile == "" { fmt.Fprintf(s.executor.out, "--> %s\n", imgID) } } // Parse and populate buildOutputOption if needed var buildOutputOptions []output.BuildOutputOption if lastStage && len(s.executor.buildOutputs) > 0 { for _, buildOutput := range s.executor.buildOutputs { logrus.Debugf("generating custom build output with options %q", buildOutput) buildOutputOption, err := output.GetBuildOutput(buildOutput) if err != nil { return "", nil, false, fmt.Errorf("failed to parse build output %q: %w", buildOutput, err) } buildOutputOptions = append(buildOutputOptions, buildOutputOption) } } if len(children) == 0 { // There are no steps. if s.builder.FromImageID == "" || s.executor.squash || s.executor.confidentialWorkload.Convert || len(s.executor.annotations) > 0 || len(s.executor.unsetEnvs) > 0 || len(s.executor.unsetLabels) > 0 || len(s.executor.sbomScanOptions) > 0 || len(s.executor.unsetAnnotations) > 0 || s.executor.inheritLabels == types.OptionalBoolFalse || s.executor.inheritAnnotations == types.OptionalBoolFalse { // We either don't have a base image, or we need to // transform the contents of the base image, or we need // to make some changes to just the config blob. Whichever // is the case, we need to commit() to create a new image. logCommit(s.output, -1) // No base image means there's nothing to put in a // layer, so don't create one. emptyLayer := (s.builder.FromImageID == "") createdBy, err := s.getCreatedBy(nil, "", lastStage) if err != nil { return "", nil, false, fmt.Errorf("unable to get createdBy for the node: %w", err) } if imgID, commitResults, err = s.commit(ctx, createdBy, emptyLayer, s.output, s.executor.squash || s.executor.confidentialWorkload.Convert, lastStage); err != nil { return "", nil, false, fmt.Errorf("committing base container: %w", err) } } else { // We don't need to squash or otherwise transform the // base image, and the image wouldn't be modified by // the command line options, so just reuse the base // image. logCommit(s.output, -1) if imgID, commitResults, err = s.tagExistingImage(ctx, s.builder.FromImageID, s.output); err != nil { return "", nil, onlyBaseImage, err } onlyBaseImage = true } // Generate build output from the new image, or the preexisting // one if we didn't actually do anything, if needed. for _, buildOutputOption := range buildOutputOptions { if err := s.generateBuildOutput(buildOutputOption); err != nil { return "", nil, onlyBaseImage, err } } logImageID(imgID) } executedLayerStep := false for i, node := range children { logRusage() moreInstructions := i < len(children)-1 lastInstruction := !moreInstructions s.isLastStep = lastStage && lastInstruction // Resolve any arguments in this instruction. step := ib.Step() if err := step.Resolve(node); err != nil { return "", nil, false, fmt.Errorf("resolving step %+v: %w", *node, err) } logrus.Debugf("Parsed Step: %+v", *step) if !s.executor.quiet { logMsg := step.Original if len(step.Heredocs) > 0 { summarizeHeredoc := func(doc string) string { doc = strings.TrimSpace(doc) lines := strings.Split(strings.ReplaceAll(doc, "\r\n", "\n"), "\n") summary := lines[0] if len(lines) > 1 { summary += "..." } return summary } for _, doc := range node.Heredocs { heredocContent := summarizeHeredoc(doc.Content) logMsg = logMsg + " (" + heredocContent + ")" } } s.log("%s", logMsg) } // Check if there's a --from if the step command is COPY. // Also check the chmod and the chown flags for validity. for _, flag := range step.Flags { command := strings.ToUpper(step.Command) // chmod, chown and from flags should have an '=' sign, '--chmod=', '--chown=' or '--from=' or '--exclude=' if command == "COPY" && (flag == "--chmod" || flag == "--chown" || flag == "--from" || flag == "--exclude") { return "", nil, false, fmt.Errorf("COPY only supports the --chmod= --chown= --from= and the --exclude= flags") } if command == "ADD" && (flag == "--chmod" || flag == "--chown" || flag == "--checksum" || flag == "--exclude") { return "", nil, false, fmt.Errorf("ADD only supports the --chmod=, --chown=, and --checksum= --exclude= flags") } if strings.Contains(flag, "--from") && command == "COPY" { arr := strings.Split(flag, "=") if len(arr) != 2 { return "", nil, false, fmt.Errorf("%s: invalid --from flag %q, should be --from=", command, flag) } // If arr[1] has an argument within it, resolve it to its // value. Otherwise just return the value found. from, fromErr := imagebuilder.ProcessWord(arr[1], s.stage.Builder.Arguments()) if fromErr != nil { return "", nil, false, fmt.Errorf("unable to resolve argument %q: %w", arr[1], fromErr) } // Before looking into additional context // also account if the index is given instead // of name so convert index in --from= // to name. if index, err := strconv.Atoi(from); err == nil && index >= 0 && index < s.index { from = s.stages[index].Name } // If additional buildContext contains this // give priority to that and break if additional // is not an external image. if additionalBuildContext, ok := s.executor.additionalBuildContexts[from]; ok { if !additionalBuildContext.IsImage { // We don't need to pull this // since this additional context // is not an image. break } // replace with image set in build context from = additionalBuildContext.Value if _, err := s.getImageRootfs(ctx, from); err != nil { return "", nil, false, fmt.Errorf("%s --from=%s: no stage or image found with that name", command, from) } break } // If the source's name corresponds to the // result of an earlier stage, wait for that // stage to finish being built. if isStage, err := s.executor.waitForStage(ctx, from, s.stages[:s.index]); isStage && err != nil { return "", nil, false, err } if otherStage, ok := s.executor.stages[from]; ok && otherStage.index < s.index { break } else if _, err = s.getImageRootfs(ctx, from); err != nil { return "", nil, false, fmt.Errorf("%s --from=%s: no stage or image found with that name", command, from) } break } } // Determine if there are any RUN instructions to be run after // this step. If not, we won't have to bother preserving the // contents of any volumes declared between now and when we // finish. noRunsRemaining := false if moreInstructions { noRunsRemaining = !ib.RequiresStart(&parser.Node{Children: children[i+1:]}) } // If we're doing a single-layer build, just process the // instruction. if !s.executor.layers { s.didExecute = true if s.stepRequiresLayer(step) { executedLayerStep = true } err := ib.Run(step, s, noRunsRemaining) if err != nil { logrus.Debugf("Error building at step %+v: %v", *step, err) return "", nil, false, fmt.Errorf("building at STEP \"%s\": %w", step.Message, err) } // In case we added content, retrieve its digest. addedContentSummary := s.getContentSummaryAfterAddingContent() if moreInstructions { // There are still more instructions to process // for this stage. Make a note of the // instruction in the history that we'll write // for the image when we eventually commit it. timestamp := time.Now().UTC() if s.executor.timestamp != nil { timestamp = *s.executor.timestamp } createdBy, err := s.getCreatedBy(node, addedContentSummary, false) if err != nil { return "", nil, false, fmt.Errorf("unable to get createdBy for the node: %w", err) } s.builder.AddPrependedEmptyLayer(×tamp, createdBy, "", "") continue } // This is the last instruction for this stage, // so we should commit this container to create // an image, but only if it's the last stage, // or if it's used as the basis for a later // stage or we are forcing saving stages by // --save-stages if lastStage || imageIsUsedLater || s.executor.saveStages { logCommit(s.output, i) createdBy, err := s.getCreatedBy(node, addedContentSummary, lastStage && lastInstruction) if err != nil { return "", nil, false, fmt.Errorf("unable to get createdBy for the node: %w", err) } imgID, commitResults, err = s.commit(ctx, createdBy, !executedLayerStep, s.output, s.executor.squash, lastStage && lastInstruction) if err != nil { return "", nil, false, fmt.Errorf("committing container for step %+v: %w", *step, err) } logImageID(imgID) // Generate build output if needed. for _, buildOutputOption := range buildOutputOptions { if err := s.generateBuildOutput(buildOutputOption); err != nil { return "", nil, false, err } } } else { imgID = "" commitResults = &buildah.CommitResults{} } break } // We're in a multi-layered build. s.didExecute = false var ( commitName string cacheID string cacheKey string pulledAndUsedCacheImage bool err error rebase bool addedContentSummary string canMatchCacheOnlyAfterRun bool ) // Only attempt to find cache if its needed, this part is needed // so that if a step is using RUN --mount and mounts content from // previous stages then it uses the freshly built stage instead // of re-using the older stage from the store. avoidLookingCache := false var mounts []string for _, a := range node.Flags { arg, err := imagebuilder.ProcessWord(a, s.stage.Builder.Arguments()) if err != nil { return "", nil, false, err } switch { case strings.HasPrefix(arg, "--mount="): mount := strings.TrimPrefix(arg, "--mount=") mounts = append(mounts, mount) default: continue } } stageMountPoints, err := s.runStageMountPoints(mounts) if err != nil { return "", nil, false, err } for _, mountPoint := range stageMountPoints { if mountPoint.DidExecute && mountPoint.IsStage { avoidLookingCache = true } } needsCacheKey := (len(s.executor.cacheFrom) != 0 && !avoidLookingCache) || len(s.executor.cacheTo) != 0 // If we have to commit for this instruction, only assign the // stage's configured output name to the last layer. if lastInstruction { commitName = s.output } // If --cache-from or --cache-to is specified make sure to populate // cacheKey since it will be used either while pulling or pushing the // cache images. if needsCacheKey { cacheKey, err = s.generateCacheKey(ctx, node, addedContentSummary, s.stepRequiresLayer(step), lastInstruction && lastStage) if err != nil { return "", nil, false, fmt.Errorf("failed while generating cache key: %w", err) } } // Check if there's already an image based on our parent that // has the same change that we're about to make, so far as we // can tell. // Only do this if the step we are on is not an ARG step, // we need to call ib.Run() to correctly put the args together before // determining if a cached layer with the same build args already exists // and that is done in the if block below. if checkForLayers && step.Command != "arg" && (!s.executor.squash || !lastInstruction || !lastStage) && !avoidLookingCache { // For `COPY` and `ADD`, history entries include digests computed from // the content that's copied in. We need to compute that information so that // it can be used to evaluate the cache, which means we need to go ahead // and copy the content. canMatchCacheOnlyAfterRun = (step.Command == command.Add || step.Command == command.Copy) if canMatchCacheOnlyAfterRun { if err = ib.Run(step, s, noRunsRemaining); err != nil { logrus.Debugf("Error building at step %+v: %v", *step, err) return "", nil, false, fmt.Errorf("building at STEP \"%s\": %w", step.Message, err) } // Retrieve the digest info for the content that we just copied // into the rootfs. addedContentSummary = s.getContentSummaryAfterAddingContent() // regenerate cache key with updated content summary if needsCacheKey { cacheKey, err = s.generateCacheKey(ctx, node, addedContentSummary, s.stepRequiresLayer(step), lastInstruction && lastStage) if err != nil { return "", nil, false, fmt.Errorf("failed while generating cache key: %w", err) } } } cacheID, err = s.intermediateImageExists(ctx, node, addedContentSummary, s.stepRequiresLayer(step), lastInstruction && lastStage) if err != nil { return "", nil, false, fmt.Errorf("checking if cached image exists from a previous build: %w", err) } // All the best effort to find cache on localstorage have failed try pulling // cache from remote repo if `--cache-from` was configured. if cacheID == "" && len(s.executor.cacheFrom) != 0 { // only attempt to use cache again if pulling was successful // otherwise do nothing and attempt to run the step, err != nil // is ignored and will be automatically logged for --log-level debug if ref, id, err := s.pullCache(ctx, cacheKey); ref != nil && id != "" && err == nil { logCachePulled(cacheKey, ref) cacheID, err = s.intermediateImageExists(ctx, node, addedContentSummary, s.stepRequiresLayer(step), lastInstruction && lastStage) if err != nil { return "", nil, false, fmt.Errorf("checking if cached image exists from a previous build: %w", err) } if cacheID != "" { pulledAndUsedCacheImage = true } } } if canMatchCacheOnlyAfterRun && cacheID == "" { s.didExecute = true } } // If we didn't find a cache entry, or we need to add content // to find the digest of the content to check for a cached // image, run the step so that we can check if the result // matches a cache. // We already called ib.Run() for the `canMatchCacheOnlyAfterRun` // cases above, so we shouldn't do it again. if cacheID == "" && !canMatchCacheOnlyAfterRun { // Process the instruction directly. s.didExecute = true if err = ib.Run(step, s, noRunsRemaining); err != nil { logrus.Debugf("Error building at step %+v: %v", *step, err) return "", nil, false, fmt.Errorf("building at STEP \"%s\": %w", step.Message, err) } // In case we added content, retrieve its digest. addedContentSummary = s.getContentSummaryAfterAddingContent() // regenerate cache key with updated content summary if needsCacheKey { cacheKey, err = s.generateCacheKey(ctx, node, addedContentSummary, s.stepRequiresLayer(step), lastInstruction && lastStage) if err != nil { return "", nil, false, fmt.Errorf("failed while generating cache key: %w", err) } } // Check if there's already an image based on our parent that // has the same change that we just made. if checkForLayers && !avoidLookingCache { cacheID, err = s.intermediateImageExists(ctx, node, addedContentSummary, s.stepRequiresLayer(step), lastInstruction && lastStage) if err != nil { return "", nil, false, fmt.Errorf("checking if cached image exists from a previous build: %w", err) } // All the best effort to find cache on localstorage have failed try pulling // cache from remote repo if `--cache-from` was configured and cacheKey was // generated again after adding content summary. if cacheID == "" && len(s.executor.cacheFrom) != 0 { // only attempt to use cache again if pulling was successful // otherwise do nothing and attempt to run the step, err != nil // is ignored and will be automatically logged for --log-level debug if ref, id, err := s.pullCache(ctx, cacheKey); ref != nil && id != "" && err == nil { logCachePulled(cacheKey, ref) cacheID, err = s.intermediateImageExists(ctx, node, addedContentSummary, s.stepRequiresLayer(step), lastInstruction && lastStage) if err != nil { return "", nil, false, fmt.Errorf("checking if cached image exists from a previous build: %w", err) } if cacheID != "" { pulledAndUsedCacheImage = true } } } } } else { // This log line is majorly here so we can verify in tests // that our cache is performing in the most optimal way for // various cases. logrus.Debugf("Found a cache hit in the first iteration with id %s", cacheID) // If the instruction would affect our configuration, // process the configuration change so that, if we fall // off the cache path, the filesystem changes from the // last cache image will be all that we need, since we // still don't want to restart using the image's // configuration blob. if !s.stepRequiresLayer(step) { s.didExecute = true err := ib.Run(step, s, noRunsRemaining) if err != nil { logrus.Debugf("Error building at step %+v: %v", *step, err) return "", nil, false, fmt.Errorf("building at STEP \"%s\": %w", step.Message, err) } } } // Note: If the build has squash, we must try to reuse as many layers as possible if cache is found. // So only perform commit if it's the lastInstruction of lastStage. if cacheID != "" { logCacheHit(cacheID) // A suitable cached image was found, so we can just // reuse it. If we need to add a name to the resulting // image because it's the last step in this stage, add // the name to the image. imgID = cacheID if commitName != "" { logCommit(commitName, i) } if imgID, commitResults, err = s.tagExistingImage(ctx, cacheID, commitName); err != nil { return "", nil, false, err } } else { // We're not going to find any more cache hits, so we // can stop looking for them. checkForLayers = false createdBy, err := s.getCreatedBy(node, addedContentSummary, lastStage && lastInstruction) if err != nil { return "", nil, false, fmt.Errorf("unable to get createdBy for the node: %w", err) } // Create a new image, maybe with a new layer, with the // name for this stage if it's the last instruction. logCommit(s.output, i) // While committing we always set squash to false here // because at this point we want to save history for // layers even if its a squashed build so that they // can be part of the build cache. imgID, commitResults, err = s.commit(ctx, createdBy, !s.stepRequiresLayer(step), commitName, false, lastStage && lastInstruction) if err != nil { return "", nil, false, fmt.Errorf("committing container for step %+v: %w", *step, err) } // Generate build output if needed. for _, buildOutputOption := range buildOutputOptions { if err := s.generateBuildOutput(buildOutputOption); err != nil { return "", nil, false, err } } } // Following step is just built and was not used from // cache so check if --cache-to was specified if yes // then attempt pushing this cache to remote repo and // fail accordingly. // // Or // // Try to push this cache to remote repository only // if cache was present on local storage and not // pulled from remote source while processing this if len(s.executor.cacheTo) != 0 && (!pulledAndUsedCacheImage || cacheID == "") && needsCacheKey { logCachePush(cacheKey) if err = s.pushCache(ctx, imgID, cacheKey); err != nil { return "", nil, false, err } } if lastInstruction && lastStage { if s.executor.squash || s.executor.confidentialWorkload.Convert || len(s.executor.sbomScanOptions) != 0 { createdBy, err := s.getCreatedBy(node, addedContentSummary, lastStage && lastInstruction) if err != nil { return "", nil, false, fmt.Errorf("unable to get createdBy for the node: %w", err) } // If this is the last instruction of the last stage, // create a squashed or confidential workload // version of the image if that's what we're after, // or a normal one if we need to scan the image while // committing it. imgID, commitResults, err = s.commit(ctx, createdBy, !s.stepRequiresLayer(step), commitName, s.executor.squash || s.executor.confidentialWorkload.Convert, lastStage && lastInstruction) if err != nil { return "", nil, false, fmt.Errorf("committing final squash step %+v: %w", *step, err) } // Generate build output if needed. for _, buildOutputOption := range buildOutputOptions { if err := s.generateBuildOutput(buildOutputOption); err != nil { return "", nil, false, err } } } else if cacheID != "" { // If we found a valid cache hit and this is lastStage // and not a squashed build then there is no opportunity // for us to perform a `commit` later in the code since // everything will be used from cache. // // If above statement is true and --output was provided // then generate output manually since there is no opportunity // for us to perform `commit` anywhere in the code. // Generate build output if needed. for _, buildOutputOption := range buildOutputOptions { if err := s.generateBuildOutput(buildOutputOption); err != nil { return "", nil, false, err } } } } logImageID(imgID) // Update our working container to be based off of the cached // image, if we might need to use it as a basis for the next // instruction, or if we need the root filesystem to match the // image contents for the sake of a later stage that wants to // copy content from it. rebase = moreInstructions || rootfsIsUsedLater if rebase { // Since we either committed the working container or // are about to replace it with one based on a cached // image, add the current working container's ID to the // list of successful intermediate containers that // we'll clean up later. s.containerIDs = append(s.containerIDs, s.builder.ContainerID) // Prepare for the next step or subsequent phases by // creating a new working container with the // just-committed or updated cached image as its new // base image. // Enforce pull "never" since we already have an image // ID that we really should not be pulling anymore (see // containers/podman/issues/10307). if _, err := s.prepare(ctx, imgID, false, true, true, define.PullNever); err != nil { return "", nil, false, fmt.Errorf("preparing container for next step: %w", err) } } s.hasLink = false } return imgID, commitResults, onlyBaseImage, nil } func historyEntriesEqual(base, derived v1.History) bool { if base.CreatedBy != derived.CreatedBy { return false } if base.Comment != derived.Comment { return false } if base.Author != derived.Author { return false } if base.EmptyLayer != derived.EmptyLayer { return false } if base.Created != nil && derived.Created == nil { return false } if base.Created == nil && derived.Created != nil { return false } if base.Created != nil && derived.Created != nil && !base.Created.Equal(*derived.Created) { return false } return true } // historyAndDiffIDsMatch returns true if a candidate history matches the // history of our base image (if we have one), plus the current instruction, // and if the list of diff IDs for the images do for the part of the history // that we're comparing. // Used to verify whether a cache of the intermediate image exists and whether // to run the build again. func (s *stageExecutor) historyAndDiffIDsMatch(baseHistory []v1.History, baseDiffIDs []digest.Digest, child *parser.Node, history []v1.History, diffIDs []digest.Digest, addedContentSummary string, buildAddsLayer bool, lastInstruction bool) (bool, error) { // our history should be as long as the base's, plus one entry for what // we're doing if len(history) != len(baseHistory)+1 { return false, nil } // check that each entry in the base history corresponds to an entry in // our history, and count how many of them add a layer diff expectedDiffIDs := 0 for i := range baseHistory { if !historyEntriesEqual(baseHistory[i], history[i]) { return false, nil } if !baseHistory[i].EmptyLayer { expectedDiffIDs++ } } if len(baseDiffIDs) != expectedDiffIDs { return false, nil } if buildAddsLayer { // we're adding a layer, so we should have exactly one more // layer than the base image if len(diffIDs) != expectedDiffIDs+1 { return false, nil } } else { // we're not adding a layer, so we should have exactly the same // layers as the base image if len(diffIDs) != expectedDiffIDs { return false, nil } } // compare the diffs for the layers that we should have in common for i := range baseDiffIDs { if diffIDs[i] != baseDiffIDs[i] { return false, nil } } createdBy, err := s.getCreatedBy(child, addedContentSummary, lastInstruction) if err != nil { return false, fmt.Errorf("unable to get createdBy for the node: %w", err) } return history[len(baseHistory)].CreatedBy == createdBy, nil } // getCreatedBy returns the value to store in the history entry for the node. // If the the passed-in addedContentSummary is not an empty string, it is // assumed to have the digest information for the content if the node is ADD or // COPY. // // The metadata string which is appended to the instruction may need to // indicate that certain last-minute changes (generally things which couldn't // be done by appending to the parsed Dockerfile, such as modifying timestamps // in the layer, unsetting labels, or anything having to do with annotations) // were made so that a future build won't mistake this result for a cache hit // unless the same flags are being used at that time. func (s *stageExecutor) getCreatedBy(node *parser.Node, addedContentSummary string, isLastStep bool) (string, error) { if node == nil { return "/bin/sh", nil } command := strings.ToUpper(node.Value) addcopy := command == "ADD" || command == "COPY" labelsAndAnnotations := s.buildMetadata(isLastStep, addcopy) switch command { case "ARG": for variable := range strings.FieldsSeq(node.Original) { if variable != "ARG" { s.argsFromContainerfile = append(s.argsFromContainerfile, variable) } } buildArgs := s.getBuildArgsKey() return "/bin/sh -c #(nop) ARG " + buildArgs + labelsAndAnnotations, nil case "RUN": shArg := "" buildArgs := s.getBuildArgsResolvedForRun() appendCheckSum := "" for _, flag := range node.Flags { var err error mountOptionSource := "" mountOptionFrom := "" mountCheckSum := "" if strings.HasPrefix(flag, "--mount=") { mountInfo := getFromAndSourceKeysFromMountFlag(flag) if mountInfo.Type != "bind" { continue } mountOptionSource = mountInfo.Source mountOptionSource, err = imagebuilder.ProcessWord(mountOptionSource, s.stage.Builder.Arguments()) if err != nil { return "", fmt.Errorf("getCreatedBy: while replacing arg variables with values for format %q: %w", mountOptionSource, err) } mountOptionFrom = mountInfo.From // If source is not specified then default is '.' if mountOptionSource == "" { mountOptionSource = "." } } // Source specified is part of stage, image or additional-build-context. if mountOptionFrom != "" { // If this is not a stage then get digest of image or additional build context if _, ok := s.executor.stages[mountOptionFrom]; !ok { if builder, ok := s.executor.containerMap[mountOptionFrom]; ok { // Found valid image, get image digest. mountCheckSum = builder.FromImageDigest } else { if s.executor.additionalBuildContexts[mountOptionFrom].IsImage { if builder, ok := s.executor.containerMap[s.executor.additionalBuildContexts[mountOptionFrom].Value]; ok { // Found valid image, get image digest. mountCheckSum = builder.FromImageDigest } } else { // Found additional build context, get directory sha. basePath := s.executor.additionalBuildContexts[mountOptionFrom].Value if s.executor.additionalBuildContexts[mountOptionFrom].IsURL { basePath = s.executor.additionalBuildContexts[mountOptionFrom].DownloadedCache } mountCheckSum, err = generatePathChecksum(filepath.Join(basePath, mountOptionSource)) if err != nil { return "", fmt.Errorf("generating checksum for directory %q in %q: %w", mountOptionSource, basePath, err) } } } } } else { if mountOptionSource != "" { mountCheckSum, err = generatePathChecksum(filepath.Join(s.executor.contextDir, mountOptionSource)) if err != nil { return "", fmt.Errorf("generating checksum for directory %q in %q: %w", mountOptionSource, s.executor.contextDir, err) } } } if mountCheckSum != "" { // add a separator to appendCheckSum appendCheckSum += ":" + mountCheckSum } } if len(node.Original) > 4 { shArg = node.Original[4:] } heredoc := "" result := "" if len(node.Heredocs) > 0 { for _, doc := range node.Heredocs { heredocContent := strings.TrimSpace(doc.Content) heredoc = heredoc + "\n" + heredocContent } } if buildArgs != "" { result = result + "|" + strconv.Itoa(len(strings.Split(buildArgs, " "))) + " " + buildArgs + " " } result = result + "/bin/sh -c " + shArg + heredoc + appendCheckSum + labelsAndAnnotations return result, nil case "ADD", "COPY": destination := node for destination.Next != nil { destination = destination.Next } hasLink := "" if s.hasLink { hasLink = " --link" } return "/bin/sh -c #(nop) " + strings.ToUpper(node.Value) + hasLink + " " + addedContentSummary + " in " + destination.Value + " " + labelsAndAnnotations, nil default: return "/bin/sh -c #(nop) " + node.Original + labelsAndAnnotations, nil } } // getBuildArgs returns a string of the build-args specified during the build process // it excludes any build-args that were not used in the build process // values for args are overridden by the values specified using ENV. // Reason: Values from ENV will always override values specified arg. func (s *stageExecutor) getBuildArgsResolvedForRun() string { var envs []string configuredEnvs := make(map[string]string) dockerConfig := s.stage.Builder.Config() for _, env := range dockerConfig.Env { key, val, hasVal := strings.Cut(env, "=") if hasVal { configuredEnvs[key] = val } } for key, value := range s.stage.Builder.Args { if _, ok := s.stage.Builder.AllowedArgs[key]; ok { // if value was in image it will be given higher priority // so please embed that into build history _, inImage := configuredEnvs[key] if inImage { envs = append(envs, fmt.Sprintf("%s=%s", key, configuredEnvs[key])) } else { // By default everything must be added to history. // Following variable is configured to false only for special cases. addToHistory := true // Following value is being assigned from build-args, // check if this key belongs to any of the predefined allowlist args e.g Proxy Variables // and if that arg is not manually set in Containerfile/Dockerfile // then don't write its value to history. // Following behaviour ensures parity with docker/buildkit. for _, variable := range config.ProxyEnv { if key == variable { // found in predefined args // so don't add to history // unless user did explicit `ARG ` addToHistory = false for _, processedArg := range s.argsFromContainerfile { if key == processedArg { addToHistory = true } } } } if addToHistory { envs = append(envs, fmt.Sprintf("%s=%s", key, value)) } } } } slices.Sort(envs) return strings.Join(envs, " ") } // getBuildArgs key returns the set of args which were specified during the // build process, formatted for inclusion in the build history func (s *stageExecutor) getBuildArgsKey() string { var args []string for key := range s.stage.Builder.Args { if _, ok := s.stage.Builder.AllowedArgs[key]; ok { args = append(args, key) } } slices.Sort(args) return strings.Join(args, " ") } // tagExistingImage adds names to an image already in the store func (s *stageExecutor) tagExistingImage(ctx context.Context, cacheID, output string) (string, *buildah.CommitResults, error) { var manifestBytes []byte var manifestType string var ref reference.Canonical var dref reference.Named // If we don't need to attach a name to the image, read its manifest back. if output == "" { // Look up the source image, expecting it to be in local storage ref, err := is.Transport.ParseStoreReference(s.executor.store, cacheID) if err != nil { return "", nil, fmt.Errorf("getting source imageReference for %q: %w", cacheID, err) } srcSrc, err := ref.NewImageSource(ctx, s.systemContext) if err != nil { return "", nil, fmt.Errorf("instantiating image for %q: %w", transports.ImageName(ref), err) } defer srcSrc.Close() unparsedTop := image.UnparsedInstance(srcSrc, nil) // Make sure we have the manifest and its type before continuing. manifestBytes, manifestType, err = unparsedTop.Manifest(ctx) if err != nil { return "", nil, fmt.Errorf("loading image manifest for %q: %w", transports.ImageName(ref), err) } } else { // Get the destination image reference for the name we want to assign. dest, err := s.executor.resolveNameToImageRef(output) if err != nil { return "", nil, err } policyContext, err := util.GetPolicyContext(s.systemContext) if err != nil { return "", nil, err } defer func() { if destroyErr := policyContext.Destroy(); destroyErr != nil { if err == nil { err = destroyErr } else { err = fmt.Errorf("%v: %w", destroyErr.Error(), err) } } }() // Look up the source image, expecting it to be in local storage src, err := is.Transport.ParseStoreReference(s.executor.store, cacheID) if err != nil { return "", nil, fmt.Errorf("getting source imageReference for %q: %w", cacheID, err) } destinationTimestamp := s.executor.timestamp if s.executor.sourceDateEpoch != nil { destinationTimestamp = s.executor.sourceDateEpoch } options := cp.Options{ RemoveSignatures: true, // more like "ignore signatures", since they don't get removed when src and dest are the same image DestinationTimestamp: destinationTimestamp, } // Make sure we have the manifest and its type before continuing. manifestBytes, err = cp.Image(ctx, policyContext, dest, src, &options) if err != nil { return "", nil, fmt.Errorf("copying image %q: %w", cacheID, err) } manifestType = manifest.GuessMIMEType(manifestBytes) // And we can also return a canonical reference, since we had a name to assign. dref = dest.DockerReference() } // Parse and digest the manifest to dig out info about the configuration. parsedManifest, err := manifest.FromBlob(manifestBytes, manifestType) if err != nil { return "", nil, fmt.Errorf("parsing manifest for image %q: %w", cacheID, err) } manifestDigest, err := manifest.Digest(manifestBytes) if err != nil { return "", nil, fmt.Errorf("computing digest of manifest for image %q: %w", cacheID, err) } // Reconstruct the metadata that would be returned if we were committing it fresh. metadata, err := metadata.Build(parsedManifest.ConfigInfo().Digest, v1.Descriptor{ MediaType: manifestType, Digest: manifestDigest, Size: int64(len(manifestBytes)), }) if err != nil { return "", nil, fmt.Errorf("reconstructing metadata for image %q: %w", cacheID, err) } // If we had a name, generate the canonical reference for it. if dref != nil { if ref, err = reference.WithDigest(dref, manifestDigest); err != nil { return "", nil, fmt.Errorf("computing canonical reference for new image %q: %w", cacheID, err) } } // Construct the return values. commitResults := buildah.CommitResults{ ImageID: cacheID, Canonical: ref, MediaType: manifestType, ImageManifest: manifestBytes, Digest: manifestDigest, Metadata: metadata, } return cacheID, &commitResults, nil } // generateCacheKey returns a computed digest for the current STEP // running its history and diff against a hash algorithm and this // generated CacheKey is further used by buildah to lock and decide // tag for the intermediate image which can be pushed and pulled to/from // the remote repository. func (s *stageExecutor) generateCacheKey(ctx context.Context, currNode *parser.Node, addedContentDigest string, buildAddsLayer bool, lastInstruction bool) (string, error) { hash := sha256.New() var baseHistory []v1.History var diffIDs []digest.Digest var manifestType string var err error if s.builder.FromImageID != "" { _, _, manifestType, baseHistory, diffIDs, err = s.executor.getImageTypeAndHistoryAndDiffIDs(ctx, s.builder.FromImageID) if err != nil { return "", fmt.Errorf("getting history of base image %q: %w", s.builder.FromImageID, err) } for i := range len(diffIDs) { fmt.Fprintln(hash, diffIDs[i].String()) } } createdBy, err := s.getCreatedBy(currNode, addedContentDigest, lastInstruction) if err != nil { return "", err } fmt.Fprintf(hash, "%t", buildAddsLayer) fmt.Fprintln(hash, createdBy) fmt.Fprintln(hash, manifestType) for _, element := range baseHistory { fmt.Fprintln(hash, element.CreatedBy) fmt.Fprintln(hash, element.Author) fmt.Fprintln(hash, element.Comment) fmt.Fprintln(hash, element.Created) fmt.Fprintf(hash, "%t", element.EmptyLayer) fmt.Fprintln(hash) } return fmt.Sprintf("%x", hash.Sum(nil)), nil } // cacheImageReference is internal function which generates ImageReference from Named repo sources // and a tag. func cacheImageReferences(repos []reference.Named, cachekey string) ([]types.ImageReference, error) { var result []types.ImageReference for _, repo := range repos { tagged, err := reference.WithTag(repo, cachekey) if err != nil { return nil, fmt.Errorf("failed generating tagged reference for %q: %w", repo, err) } dest, err := imagedocker.NewReference(tagged) if err != nil { return nil, fmt.Errorf("failed generating docker reference for %q: %w", tagged, err) } result = append(result, dest) } return result, nil } // pushCache takes the image id of intermediate image and attempts // to perform push at the remote repository with cacheKey as the tag. // Returns error if fails otherwise returns nil. func (s *stageExecutor) pushCache(ctx context.Context, src, cacheKey string) error { destList, err := cacheImageReferences(s.executor.cacheTo, cacheKey) if err != nil { return err } for _, dest := range destList { logrus.Debugf("trying to push cache to dest: %+v from src:%+v", dest, src) options := buildah.PushOptions{ Compression: s.executor.compression, SignaturePolicyPath: s.executor.signaturePolicyPath, Store: s.executor.store, SystemContext: s.systemContext, BlobDirectory: s.executor.blobDirectory, SignBy: s.executor.signBy, MaxRetries: s.executor.maxPullPushRetries, RetryDelay: s.executor.retryPullPushDelay, } if s.executor.cachePushSourceLookupReferenceFunc != nil { options.SourceLookupReferenceFunc = s.executor.cachePushSourceLookupReferenceFunc(dest) } if s.executor.cachePushDestinationLookupReferenceFunc != nil { options.DestinationLookupReferenceFunc = s.executor.cachePushDestinationLookupReferenceFunc } ref, digest, err := buildah.Push(ctx, src, dest, options) if err != nil { return fmt.Errorf("failed pushing cache to %q: %w", dest, err) } logrus.Debugf("successfully pushed cache to dest: %+v with ref:%+v and digest: %v", dest, ref, digest) } return nil } // pullCache takes the image source of the cache assuming tag // already points to the valid cacheKey and pulls the image to // local storage only if it was not already present on local storage // or a newer version of cache was found in the upstream repo. If new // image was pulled function returns image id otherwise returns empty // string "" or error if any error was encontered while pulling the cache. func (s *stageExecutor) pullCache(ctx context.Context, cacheKey string) (reference.Named, string, error) { srcList, err := cacheImageReferences(s.executor.cacheFrom, cacheKey) if err != nil { return nil, "", err } for _, src := range srcList { srcDockerRef := src.DockerReference() logrus.Debugf("trying to pull cache from remote repo: %+v", srcDockerRef) options := buildah.PullOptions{ SignaturePolicyPath: s.executor.signaturePolicyPath, Store: s.executor.store, SystemContext: s.systemContext, BlobDirectory: s.executor.blobDirectory, MaxRetries: s.executor.maxPullPushRetries, RetryDelay: s.executor.retryPullPushDelay, AllTags: false, ReportWriter: nil, PullPolicy: define.PullIfNewer, } if s.executor.cachePullSourceLookupReferenceFunc != nil { options.SourceLookupReferenceFunc = s.executor.cachePullSourceLookupReferenceFunc } if s.executor.cachePullDestinationLookupReferenceFunc != nil { options.DestinationLookupReferenceFunc = s.executor.cachePullDestinationLookupReferenceFunc(src) } id, err := buildah.Pull(ctx, srcDockerRef.String(), options) if err != nil { logrus.Debugf("failed pulling cache from source %s: %v", src, err) continue // failed pulling this one try next // return "", fmt.Errorf("failed while pulling cache from %q: %w", src, err) } logrus.Debugf("successfully pulled cache from repo %s: %s", src, id) return src.DockerReference(), id, nil } return nil, "", fmt.Errorf("failed pulling cache from all available sources %q", srcList) } // intermediateImageExists returns image ID if an intermediate image of currNode exists in the image store from a previous build. // It verifies this by checking the parent of the top layer of the image and the history. // If more than one image matches as potential candidates then priority is given to the most recently built image. func (s *stageExecutor) intermediateImageExists(ctx context.Context, currNode *parser.Node, addedContentDigest string, buildAddsLayer bool, lastInstruction bool) (string, error) { cacheCandidates := []storage.Image{} // Get the list of images available in the image store images, err := s.executor.store.Images() if err != nil { return "", fmt.Errorf("getting image list from store: %w", err) } var baseHistory []v1.History var baseDiffIDs []digest.Digest if s.builder.FromImageID != "" { _, _, _, baseHistory, baseDiffIDs, err = s.executor.getImageTypeAndHistoryAndDiffIDs(ctx, s.builder.FromImageID) if err != nil { return "", fmt.Errorf("getting history of base image %q: %w", s.builder.FromImageID, err) } } for _, image := range images { // If s.executor.cacheTTL was specified // then ignore processing image if it // was created before the specified // duration. if int64(s.executor.cacheTTL) != 0 { timeNow := time.Now() imageDuration := timeNow.Sub(image.Created) if s.executor.cacheTTL < imageDuration { continue } } var imageTopLayer *storage.Layer var imageParentLayerID string if image.TopLayer != "" { imageTopLayer, err = s.executor.store.Layer(image.TopLayer) if err != nil { if errors.Is(err, storage.ErrLayerUnknown) { continue } return "", fmt.Errorf("getting top layer info: %w", err) } // Figure out which layer from this image we should // compare our container's base layer to. imageParentLayerID = imageTopLayer.ID // If we haven't added a layer here, then our base // layer should be the same as the image's layer. If // did add a layer, then our base layer should be the // same as the parent of the image's layer. if buildAddsLayer { imageParentLayerID = imageTopLayer.Parent } } // If the parent of the top layer of an image is equal to the current build image's top layer, // it means that this image is potentially a cached intermediate image from a previous // build. if s.builder.TopLayer != imageParentLayerID { continue } // Next we double check that the history of this image is equivalent to the previous // lines in the Dockerfile up till the point we are at in the build. imageOS, imageArchitecture, manifestType, history, diffIDs, err := s.executor.getImageTypeAndHistoryAndDiffIDs(ctx, image.ID) if err != nil { // It's possible that this image is for another architecture, which results // in a custom-crafted error message that we'd have to use substring matching // to recognize. Instead, ignore the image. logrus.Debugf("error getting history of %q (%v), ignoring it", image.ID, err) continue } // If this candidate isn't of the type that we're building, then it may have lost // some format-specific information that a building-without-cache run wouldn't lose. if manifestType != s.executor.outputFormat { continue } // Compare the cached image's platform with the current build's target platform currentArch := s.executor.architecture currentOS := s.executor.os if currentArch == "" && currentOS == "" { currentOS, currentArch, _, err = parse.Platform(s.stage.Builder.Platform) if err != nil { logrus.Debugf("unable to parse default OS and Arch for the current build: %v", err) } } if currentArch != "" && imageArchitecture != currentArch { logrus.Debugf("cached image %q has architecture %q but current build targets %q, ignoring it", image.ID, imageArchitecture, currentArch) continue } if currentOS != "" && imageOS != currentOS { logrus.Debugf("cached image %q has OS %q but current build targets %q, ignoring it", image.ID, imageOS, currentOS) continue } // children + currNode is the point of the Dockerfile we are currently at. foundMatch, err := s.historyAndDiffIDsMatch(baseHistory, baseDiffIDs, currNode, history, diffIDs, addedContentDigest, buildAddsLayer, lastInstruction) if err != nil { return "", err } if foundMatch { cacheCandidates = append(cacheCandidates, image) } } if len(cacheCandidates) > 0 { slices.SortFunc(cacheCandidates, func(a, b storage.Image) int { return a.Created.Compare(b.Created) }) return cacheCandidates[len(cacheCandidates)-1].ID, nil } return "", nil } // commit writes the container's contents to an image, using a passed-in tag as // the name if there is one, generating a unique ID-based one otherwise. // or commit via any custom exporter if specified. func (s *stageExecutor) commit(ctx context.Context, createdBy string, emptyLayer bool, output string, squash, finalInstruction bool) (string, *buildah.CommitResults, error) { ib := s.stage.Builder var imageRef types.ImageReference if output != "" { imageRef2, err := s.executor.resolveNameToImageRef(output) if err != nil { return "", nil, err } imageRef = imageRef2 } if ib.Author != "" { s.builder.SetMaintainer(ib.Author) } config := ib.Config() if createdBy != "" { s.builder.SetCreatedBy(createdBy) } s.builder.SetHostname(config.Hostname) s.builder.SetDomainname(config.Domainname) if s.executor.architecture != "" { s.builder.SetArchitecture(s.executor.architecture) } if s.executor.os != "" { s.builder.SetOS(s.executor.os) } if s.executor.osVersion != "" { s.builder.SetOSVersion(s.executor.osVersion) } for _, osFeatureSpec := range s.executor.osFeatures { switch { case strings.HasSuffix(osFeatureSpec, "-"): s.builder.UnsetOSFeature(strings.TrimSuffix(osFeatureSpec, "-")) default: s.builder.SetOSFeature(osFeatureSpec) } } s.builder.SetUser(config.User) s.builder.ClearPorts() for p := range config.ExposedPorts { s.builder.SetPort(string(p)) } for _, envSpec := range config.Env { key, val, _ := strings.Cut(envSpec, "=") s.builder.SetEnv(key, val) } for _, envSpec := range s.executor.unsetEnvs { s.builder.UnsetEnv(envSpec) } s.builder.SetCmd(config.Cmd) s.builder.ClearVolumes() for v := range config.Volumes { s.builder.AddVolume(v) } s.builder.ClearOnBuild() for _, onBuildSpec := range config.OnBuild { s.builder.SetOnBuild(onBuildSpec) } s.builder.SetWorkDir(config.WorkingDir) s.builder.SetEntrypoint(config.Entrypoint) s.builder.SetShell(config.Shell) s.builder.SetStopSignal(config.StopSignal) if config.Healthcheck != nil { s.builder.SetHealthcheck(&buildahdocker.HealthConfig{ Test: slices.Clone(config.Healthcheck.Test), Interval: config.Healthcheck.Interval, Timeout: config.Healthcheck.Timeout, StartPeriod: config.Healthcheck.StartPeriod, StartInterval: config.Healthcheck.StartInterval, Retries: config.Healthcheck.Retries, }) } else { s.builder.SetHealthcheck(nil) } s.builder.ClearLabels() if output == "" { // If output is not set then we are committing // an intermediate image, in such case we must // honor layer labels if they are configured. for _, labelString := range s.executor.layerLabels { labelk, labelv, _ := strings.Cut(labelString, "=") s.builder.SetLabel(labelk, labelv) } } for k, v := range config.Labels { s.builder.SetLabel(k, v) } switch s.executor.commonBuildOptions.IdentityLabel { case types.OptionalBoolTrue: s.builder.SetLabel(buildah.BuilderIdentityAnnotation, define.Version) case types.OptionalBoolFalse: // nothing - don't clear it if there's a value set in the base image default: if s.executor.timestamp == nil && s.executor.sourceDateEpoch == nil { s.builder.SetLabel(buildah.BuilderIdentityAnnotation, define.Version) } } for _, key := range s.executor.unsetLabels { s.builder.UnsetLabel(key) } if finalInstruction { if s.executor.inheritAnnotations == types.OptionalBoolFalse { // If user has selected `--inherit-annotations=false` let's not // inherit annotations from base image. s.builder.ClearAnnotations() } // Add new annotations to the last step. for _, annotationSpec := range s.executor.annotations { annotationk, annotationv, _ := strings.Cut(annotationSpec, "=") s.builder.SetAnnotation(annotationk, annotationv) } for _, key := range s.executor.unsetAnnotations { s.builder.UnsetAnnotation(key) } } if imageRef != nil { logName := transports.ImageName(imageRef) logrus.Debugf("COMMIT %q", logName) } else { logrus.Debugf("COMMIT") } writer := s.executor.reportWriter if s.executor.layers || !s.executor.useCache { writer = nil } options := buildah.CommitOptions{ Compression: s.executor.compression, SignaturePolicyPath: s.executor.signaturePolicyPath, ReportWriter: writer, PreferredManifestType: s.executor.outputFormat, SystemContext: s.systemContext, Squash: squash, OmitHistory: s.executor.commonBuildOptions.OmitHistory, EmptyLayer: emptyLayer, OmitLayerHistoryEntry: s.hasLink, BlobDirectory: s.executor.blobDirectory, SignBy: s.executor.signBy, MaxRetries: s.executor.maxPullPushRetries, RetryDelay: s.executor.retryPullPushDelay, HistoryTimestamp: s.executor.timestamp, Manifest: s.executor.manifest, CompatSetParent: s.executor.compatSetParent, SourceDateEpoch: s.executor.sourceDateEpoch, RewriteTimestamp: s.executor.rewriteTimestamp, CompatLayerOmissions: s.executor.compatLayerOmissions, UnsetAnnotations: s.executor.unsetAnnotations, Annotations: s.executor.annotations, CreatedAnnotation: s.executor.createdAnnotation, } if finalInstruction { options.ConfidentialWorkloadOptions = s.executor.confidentialWorkload options.SBOMScanOptions = s.executor.sbomScanOptions } results, err := s.builder.CommitResults(ctx, imageRef, options) if err != nil { return "", nil, err } return results.ImageID, results, nil } func (s *stageExecutor) generateBuildOutput(buildOutputOpts output.BuildOutputOption) error { forceTimestamp := s.executor.timestamp if s.executor.sourceDateEpoch != nil { forceTimestamp = s.executor.sourceDateEpoch } extractRootfsOpts := buildah.ExtractRootfsOptions{ ForceTimestamp: forceTimestamp, } if unshare.IsRootless() { // In order to maintain as much parity as possible // with buildkit's version of --output and to avoid // unsafe invocation of exported executables it was // decided to strip setuid,setgid and extended attributes. // Since modes like setuid,setgid leaves room for executable // to get invoked with different file-system permission its safer // to strip them off for unprivileged invocation. // See: https://github.com/containers/buildah/pull/3823#discussion_r829376633 extractRootfsOpts.StripSetuidBit = true extractRootfsOpts.StripSetgidBit = true extractRootfsOpts.StripXattrs = true } rc, errChan, err := s.builder.ExtractRootfs(buildah.CommitOptions{ HistoryTimestamp: s.executor.timestamp, SourceDateEpoch: s.executor.sourceDateEpoch, RewriteTimestamp: s.executor.rewriteTimestamp, CompatLayerOmissions: s.executor.compatLayerOmissions, }, extractRootfsOpts) if err != nil { return fmt.Errorf("failed to extract rootfs from given container image: %w", err) } defer rc.Close() err = internalUtil.ExportFromReader(rc, buildOutputOpts) if err != nil { return fmt.Errorf("failed to export build output: %w", err) } if errChan != nil { err = <-errChan if err != nil { return err } } return nil } func (s *stageExecutor) EnsureContainerPath(path string) error { logrus.Debugf("EnsureContainerPath %q in %q", path, s.builder.ContainerID) return s.builder.EnsureContainerPathAs(path, "", nil) } func (s *stageExecutor) EnsureContainerPathAs(path, user string, mode *os.FileMode) error { logrus.Debugf("EnsureContainerPath %q (owner %q, mode %o) in %q", path, user, mode, s.builder.ContainerID) return s.builder.EnsureContainerPathAs(path, user, mode) } // buildMetadata constructs the text at the end of the createdBy value for the // history entry that we'll generate for the instruction that we're currently // processing. Any flags that affect the output image in a way that affects // whether or not it should be used as a cache hit for another build with that // flag set differently should be reflected in its result. Some build settings // only take affect at the final step, so only note those when they're applied. func (s *stageExecutor) buildMetadata(isLastStep bool, isAddOrCopy bool) string { unsetLabels := "" inheritLabels := "" unsetAnnotations := "" inheritAnnotations := "" newAnnotations := "" layerMutations := "" // If --inherit-label was manually set to false then update history. if s.executor.inheritLabels == types.OptionalBoolFalse { inheritLabels = "|inheritLabels=false" } // If --unsetlabel was used to clear a label, make a note of it. for _, label := range s.executor.unsetLabels { unsetLabels += "|unsetLabel=" + label } if isLastStep { // If --unsetannotation was used to clear an annotation, make a note of it. for _, annotation := range s.executor.unsetAnnotations { unsetAnnotations += "|unsetAnnotation=" + annotation } // If --inherit-annotation was manually set to false then we cleared the inherited annotations. if s.executor.inheritAnnotations == types.OptionalBoolFalse { inheritAnnotations = "|inheritAnnotations=false" } // If new annotations are added, they must be added as part of the last step of the build, // so mention in history that new annotations were added in order to make sure that subsequent builds // only use this image as a cache hit if it was built with the same set of annotations. if len(s.executor.annotations) > 0 { newAnnotations += strings.Join(s.executor.annotations, ",") } } // If we're messing with timestamps in layer contents, make a note of how we're doing it. if s.executor.timestamp != nil || (s.executor.sourceDateEpoch != nil && s.executor.rewriteTimestamp) { var t time.Time modtype := "" if s.executor.timestamp != nil { t = s.executor.timestamp.UTC() modtype = "force-mtime" } if s.executor.sourceDateEpoch != nil && s.executor.rewriteTimestamp { t = s.executor.sourceDateEpoch.UTC() modtype = "clamp-mtime" if s.executor.timestamp != nil && s.executor.timestamp.Before(*s.executor.sourceDateEpoch) { t = s.executor.timestamp.UTC() modtype = "force-mtime" } } layerMutations = "|" + modtype + "=" + strconv.FormatInt(t.Unix(), 10) } if isAddOrCopy { return unsetLabels + " " + inheritLabels + " " + unsetAnnotations + " " + inheritAnnotations + " " + layerMutations + " " + newAnnotations } return unsetLabels + inheritLabels + unsetAnnotations + inheritAnnotations + layerMutations + newAnnotations } ================================================ FILE: imagebuildah/stage_executor_test.go ================================================ package imagebuildah import ( "encoding/json" "strconv" "testing" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestHistoryEntriesEqual(t *testing.T) { t.Parallel() testCases := []struct { a, b string equal bool }{ { a: `{}`, b: `{}`, equal: true, }, { a: `{"created":"2020-06-17T00:22:25.47282687Z"}`, b: `{"created":"2020-06-17T00:22:25.47282687Z"}`, equal: true, }, { a: `{"created":"2020-07-16T12:38:26.733333497-04:00"}`, b: `{"created":"2020-07-16T12:38:26.733333497-04:00"}`, equal: true, }, { a: `{"created":"2020-07-16T12:38:26.733333497-04:00"}`, b: `{"created":"2020-07-16T12:38:26.733333497Z"}`, equal: false, }, { a: `{"created":"2020-07-16T12:38:26.733333497Z"}`, b: `{}`, equal: false, }, { a: `{}`, b: `{"created":"2020-07-16T12:38:26.733333497Z"}`, equal: false, }, { a: `{"comment":"thing"}`, b: `{"comment":"thing"}`, equal: true, }, { a: `{"comment":"thing","ignored-field-for-testing":"ignored"}`, b: `{"comment":"thing"}`, equal: true, }, { a: `{"CoMmEnT":"thing"}`, b: `{"comment":"thing"}`, equal: true, }, { a: `{"comment":"thing"}`, b: `{"comment":"things"}`, equal: false, }, { a: `{"author":"respected"}`, b: `{"author":"respected"}`, equal: true, }, { a: `{"author":"respected"}`, b: `{"author":"discredited"}`, equal: false, }, { a: `{"created_by":"actions"}`, b: `{"created_by":"actions"}`, equal: true, }, { a: `{"created_by":"jiggery"}`, b: `{"created_by":"pokery"}`, equal: false, }, } for i := range testCases { t.Run(strconv.Itoa(i), func(t *testing.T) { var a, b v1.History err := json.Unmarshal([]byte(testCases[i].a), &a) require.Nil(t, err, "error unmarshalling history %q: %v", testCases[i].a, err) err = json.Unmarshal([]byte(testCases[i].b), &b) require.Nil(t, err, "error unmarshalling history %q: %v", testCases[i].b, err) equal := historyEntriesEqual(a, b) assert.Equal(t, testCases[i].equal, equal, "historyEntriesEqual(%q, %q) != %v", testCases[i].a, testCases[i].b, testCases[i].equal) }) } } ================================================ FILE: imagebuildah/util.go ================================================ package imagebuildah import ( "archive/tar" "io" "os" "path/filepath" "strings" "time" "github.com/containers/buildah" digest "github.com/opencontainers/go-digest" ) type mountInfo struct { Type string Source string From string } // Consumes mount flag in format of `--mount=type=bind,src=/path,from=image` and // return mountInfo with values, otherwise values are empty if keys are not present in the option. func getFromAndSourceKeysFromMountFlag(mount string) mountInfo { tokens := strings.Split(strings.TrimPrefix(mount, "--mount="), ",") source := "" from := "" mountType := "" for _, option := range tokens { if optionSplit := strings.Split(option, "="); len(optionSplit) == 2 { if optionSplit[0] == "src" || optionSplit[0] == "source" { source = optionSplit[1] } if optionSplit[0] == "from" { from = optionSplit[1] } if optionSplit[0] == "type" { mountType = optionSplit[1] } } } return mountInfo{Source: source, From: from, Type: mountType} } // generatePathChecksum generates the SHA-256 checksum for a file or a directory. func generatePathChecksum(sourcePath string) (string, error) { digester := digest.SHA256.Digester() tarWriter := tar.NewWriter(digester.Hash()) err := filepath.Walk(sourcePath, func(path string, info os.FileInfo, err error) error { if err != nil { return err } var linkTarget string if info.Mode()&os.ModeSymlink != 0 { // If the file is a symlink, get the target linkTarget, err = os.Readlink(path) if err != nil { return err } } header, err := tar.FileInfoHeader(info, linkTarget) if err != nil { return err } relPath, err := filepath.Rel(sourcePath, path) if err != nil { return err } header.Name = filepath.ToSlash(relPath) // Zero out timestamp fields to ignore modification time in checksum calculation header.ModTime = time.Time{} header.AccessTime = time.Time{} header.ChangeTime = time.Time{} if err := tarWriter.WriteHeader(header); err != nil { return err } if !info.Mode().IsRegular() { return nil } file, err := os.Open(path) if err != nil { return err } defer file.Close() _, err = io.Copy(tarWriter, file) return err }) tarWriter.Close() if err != nil { return "", err } return digester.Digest().String(), nil } // InitReexec is a wrapper for buildah.InitReexec(). It should be called at // the start of main(), and if it returns true, main() should return // successfully immediately. func InitReexec() bool { return buildah.InitReexec() } // argsMapToSlice returns the contents of a map[string]string as a slice of keys // and values joined with "=". func argsMapToSlice(m map[string]string) []string { s := make([]string, 0, len(m)) for k, v := range m { s = append(s, k+"="+v) } return s } ================================================ FILE: imagebuildah/util_test.go ================================================ package imagebuildah import ( "archive/tar" "io" "os" "path/filepath" "testing" "time" "github.com/opencontainers/go-digest" "github.com/stretchr/testify/assert" ) func TestGeneratePathChecksum(t *testing.T) { t.Parallel() tempDir := t.TempDir() tempFile, err := os.CreateTemp(tempDir, "testfile") if err != nil { t.Fatalf("Failed to create temp file: %v", err) } defer tempFile.Close() // Write some data to the file data := []byte("Hello, world!") if _, err := tempFile.Write(data); err != nil { t.Fatalf("Failed to write data to temp file: %v", err) } // Generate the checksum for the directory checksum, err := generatePathChecksum(tempDir) if err != nil { t.Fatalf("Failed to generate checksum: %v", err) } digester := digest.SHA256.Digester() tarWriter := tar.NewWriter(digester.Hash()) err = filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } header, err := tar.FileInfoHeader(info, "") if err != nil { return err } relPath, err := filepath.Rel(tempDir, path) if err != nil { return err } header.Name = filepath.ToSlash(relPath) // Zero out timestamp fields to match the modified generatePathChecksum function header.ModTime = time.Time{} header.AccessTime = time.Time{} header.ChangeTime = time.Time{} if err := tarWriter.WriteHeader(header); err != nil { return err } if !info.Mode().IsRegular() { return nil } file, err := os.Open(path) if err != nil { return err } defer file.Close() _, err = io.Copy(tarWriter, file) return err }) if err != nil { tarWriter.Close() t.Fatalf("Failed to manually generate checksum: %v", err) } tarWriter.Close() expectedChecksum := digester.Digest().String() // Compare the generated checksum to the expected checksum assert.Equal(t, expectedChecksum, checksum, "didn't get expected checksum over a sample directory") } ================================================ FILE: import.go ================================================ package buildah import ( "context" "errors" "fmt" "github.com/containers/buildah/define" "github.com/containers/buildah/docker" "github.com/containers/buildah/util" digest "github.com/opencontainers/go-digest" "go.podman.io/image/v5/image" "go.podman.io/image/v5/manifest" is "go.podman.io/image/v5/storage" "go.podman.io/image/v5/transports" "go.podman.io/image/v5/types" "go.podman.io/storage" ) func importBuilderDataFromImage(ctx context.Context, store storage.Store, systemContext *types.SystemContext, imageID, containerName, containerID string) (*Builder, error) { if imageID == "" { return nil, errors.New("internal error: imageID is empty in importBuilderDataFromImage") } storeopts, err := storage.DefaultStoreOptions() if err != nil { return nil, err } uidmap, gidmap := convertStorageIDMaps(storeopts.UIDMap, storeopts.GIDMap) ref, err := is.Transport.ParseStoreReference(store, imageID) if err != nil { return nil, fmt.Errorf("no such image %q: %w", imageID, err) } src, err := ref.NewImageSource(ctx, systemContext) if err != nil { return nil, fmt.Errorf("instantiating image source: %w", err) } defer src.Close() imageDigest := "" unparsedTop := image.UnparsedInstance(src, nil) manifestBytes, manifestType, err := unparsedTop.Manifest(ctx) if err != nil { return nil, fmt.Errorf("loading image manifest for %q: %w", transports.ImageName(ref), err) } if manifestDigest, err := manifest.Digest(manifestBytes); err == nil { imageDigest = manifestDigest.String() } var instanceDigest *digest.Digest unparsedInstance := unparsedTop // for instanceDigest if manifest.MIMETypeIsMultiImage(manifestType) { list, err := manifest.ListFromBlob(manifestBytes, manifestType) if err != nil { return nil, fmt.Errorf("parsing image manifest for %q as list: %w", transports.ImageName(ref), err) } instance, err := list.ChooseInstance(systemContext) if err != nil { return nil, fmt.Errorf("finding an appropriate image in manifest list %q: %w", transports.ImageName(ref), err) } instanceDigest = &instance unparsedInstance = image.UnparsedInstance(src, instanceDigest) } image, err := image.FromUnparsedImage(ctx, systemContext, unparsedInstance) if err != nil { return nil, fmt.Errorf("instantiating image for %q instance %q: %w", transports.ImageName(ref), instanceDigest, err) } imageName := "" if img, err3 := store.Image(imageID); err3 == nil { if len(img.Names) > 0 { imageName = img.Names[0] } if img.TopLayer != "" { layer, err4 := store.Layer(img.TopLayer) if err4 != nil { return nil, fmt.Errorf("reading information about image's top layer: %w", err4) } uidmap, gidmap = convertStorageIDMaps(layer.UIDMap, layer.GIDMap) } } defaultNamespaceOptions, err := DefaultNamespaceOptions() if err != nil { return nil, err } netInt, err := getNetworkInterface(store) if err != nil { return nil, err } builder := &Builder{ store: store, Type: containerType, FromImage: imageName, FromImageID: imageID, FromImageDigest: imageDigest, Container: containerName, ContainerID: containerID, ImageCreatedBy: "", NamespaceOptions: defaultNamespaceOptions, IDMappingOptions: define.IDMappingOptions{ HostUIDMapping: len(uidmap) == 0, HostGIDMapping: len(uidmap) == 0, UIDMap: uidmap, GIDMap: gidmap, }, NetworkInterface: netInt, CommonBuildOpts: &CommonBuildOptions{}, } if err := builder.initConfig(ctx, systemContext, image, nil); err != nil { return nil, fmt.Errorf("preparing image configuration: %w", err) } return builder, nil } func importBuilder(ctx context.Context, store storage.Store, options ImportOptions) (*Builder, error) { if options.Container == "" { return nil, errors.New("container name must be specified") } c, err := store.Container(options.Container) if err != nil { return nil, err } systemContext := getSystemContext(store, &types.SystemContext{}, options.SignaturePolicyPath) builder, err := importBuilderDataFromImage(ctx, store, systemContext, c.ImageID, options.Container, c.ID) if err != nil { return nil, err } if builder.FromImageID != "" { if d, err2 := digest.Parse(builder.FromImageID); err2 == nil { builder.Docker.Parent = docker.ID(d) } else { builder.Docker.Parent = docker.ID(digest.NewDigestFromHex(digest.Canonical.String(), builder.FromImageID)) } } if builder.FromImage != "" { builder.Docker.ContainerConfig.Image = builder.FromImage } builder.IDMappingOptions.UIDMap, builder.IDMappingOptions.GIDMap = convertStorageIDMaps(c.UIDMap, c.GIDMap) err = builder.Save() if err != nil { return nil, fmt.Errorf("saving builder state: %w", err) } return builder, nil } func importBuilderFromImage(ctx context.Context, store storage.Store, options ImportFromImageOptions) (*Builder, error) { if options.Image == "" { return nil, errors.New("image name must be specified") } systemContext := getSystemContext(store, options.SystemContext, options.SignaturePolicyPath) _, img, err := util.FindImage(store, "", systemContext, options.Image) if err != nil { return nil, fmt.Errorf("importing settings: %w", err) } builder, err := importBuilderDataFromImage(ctx, store, systemContext, img.ID, "", "") if err != nil { return nil, fmt.Errorf("importing build settings from image %q: %w", options.Image, err) } builder.setupLogger() return builder, nil } ================================================ FILE: info.go ================================================ package buildah import ( "bufio" "bytes" "fmt" "os" "runtime" "strconv" "strings" internalUtil "github.com/containers/buildah/internal/util" putil "github.com/containers/buildah/pkg/util" "github.com/containers/buildah/util" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" "go.podman.io/common/pkg/cgroups" "go.podman.io/storage" "go.podman.io/storage/pkg/system" "go.podman.io/storage/pkg/unshare" ) // InfoData holds the info type, i.e store, host etc and the data for each type type InfoData struct { Type string Data map[string]any } // Info returns the store and host information func Info(store storage.Store) ([]InfoData, error) { info := []InfoData{} // get host information hostInfo := hostInfo() info = append(info, InfoData{Type: "host", Data: hostInfo}) // get store information storeInfo, err := storeInfo(store) if err != nil { logrus.Error(err, "error getting store info") } info = append(info, InfoData{Type: "store", Data: storeInfo}) return info, nil } func hostInfo() map[string]any { info := map[string]any{} ps := internalUtil.NormalizePlatform(v1.Platform{OS: runtime.GOOS, Architecture: runtime.GOARCH}) info["os"] = ps.OS info["arch"] = ps.Architecture info["variant"] = ps.Variant info["cpus"] = runtime.NumCPU() info["rootless"] = unshare.IsRootless() _, err := cgroups.IsCgroup2UnifiedMode() if err != nil { logrus.Error(err, "err reading cgroups mode") } ociruntime := util.Runtime() info["OCIRuntime"] = ociruntime mi, err := system.ReadMemInfo() if err != nil { logrus.Error(err, "err reading memory info") info["MemTotal"] = "" info["MemFree"] = "" info["SwapTotal"] = "" info["SwapFree"] = "" } else { info["MemTotal"] = mi.MemTotal info["MemFree"] = mi.MemFree info["SwapTotal"] = mi.SwapTotal info["SwapFree"] = mi.SwapFree } hostDistributionInfo := getHostDistributionInfo() info["Distribution"] = map[string]any{ "distribution": hostDistributionInfo["Distribution"], "version": hostDistributionInfo["Version"], } kv, err := putil.ReadKernelVersion() if err != nil { logrus.Error(err, "error reading kernel version") } info["kernel"] = kv upDuration, err := putil.ReadUptime() if err != nil { logrus.Error(err, "error reading up time") } hoursFound := false var timeBuffer bytes.Buffer var hoursBuffer bytes.Buffer for _, elem := range upDuration.String() { timeBuffer.WriteRune(elem) if elem == 'h' || elem == 'm' { timeBuffer.WriteRune(' ') if elem == 'h' { hoursFound = true } } if !hoursFound { hoursBuffer.WriteRune(elem) } } info["uptime"] = timeBuffer.String() if hoursFound { hours, err := strconv.ParseFloat(hoursBuffer.String(), 64) if err == nil { days := hours / 24 info["uptime"] = fmt.Sprintf("%s (Approximately %.2f days)", info["uptime"], days) } } host, err := os.Hostname() if err != nil { logrus.Error(err, "error getting hostname") } info["hostname"] = host return info } // top-level "store" info func storeInfo(store storage.Store) (map[string]any, error) { // lets say storage driver in use, number of images, number of containers info := map[string]any{} info["GraphRoot"] = store.GraphRoot() info["RunRoot"] = store.RunRoot() info["GraphImageStore"] = store.ImageStore() info["GraphTransientStore"] = store.TransientStore() info["GraphDriverName"] = store.GraphDriverName() info["GraphOptions"] = store.GraphOptions() statusPairs, err := store.Status() if err != nil { return nil, err } status := map[string]string{} for _, pair := range statusPairs { status[pair[0]] = pair[1] } info["GraphStatus"] = status images, err := store.Images() if err != nil { logrus.Error(err, "error getting number of images") } info["ImageStore"] = map[string]any{ "number": len(images), } containers, err := store.Containers() if err != nil { logrus.Error(err, "error getting number of containers") } info["ContainerStore"] = map[string]any{ "number": len(containers), } return info, nil } // getHostDistributionInfo returns a map containing the host's distribution and version func getHostDistributionInfo() map[string]string { dist := make(map[string]string) // Populate values in case we cannot find the values // or the file dist["Distribution"] = "unknown" dist["Version"] = "unknown" f, err := os.Open("/etc/os-release") if err != nil { return dist } defer f.Close() l := bufio.NewScanner(f) for l.Scan() { if after, ok := strings.CutPrefix(l.Text(), "ID="); ok { dist["Distribution"] = after } if after, ok := strings.CutPrefix(l.Text(), "VERSION_ID="); ok { dist["Version"] = strings.Trim(after, "\"") } } return dist } ================================================ FILE: install.md ================================================ ![buildah logo](https://cdn.rawgit.com/containers/buildah/main/logos/buildah-logo_large.png) # Installation Instructions ## Installing packaged versions of buildah ### [Arch Linux](https://www.archlinux.org) ```bash sudo pacman -S buildah ``` ### [CentOS](https://www.centos.org) Buildah is available in the default Extras repos for CentOS 7 and in the AppStream repo for CentOS 8 and Stream, however the available version often lags the upstream release. ```bash sudo yum -y install buildah ``` ### [Debian](https://debian.org) The buildah package is available in the [Bookworm](https://packages.debian.org/bookworm/buildah), which is the current stable release (Debian 12), as well as Debian Unstable/Sid. ```bash # Debian Stable/Bookworm or Unstable/Sid sudo apt-get update sudo apt-get -y install buildah ``` ### [Fedora](https://www.fedoraproject.org) ```bash sudo dnf -y install buildah ``` ### [Fedora SilverBlue](https://silverblue.fedoraproject.org) Installed by default ### [Fedora CoreOS](https://coreos.fedoraproject.org) Not Available. Must be installed via package layering. rpm-ostree install buildah Note: [`podman`](https://podman.io) build is available by default. ### [Gentoo](https://www.gentoo.org) [app-containers/buildah](https://packages.gentoo.org/packages/app-containers/buildah) ```bash sudo emerge app-containers/buildah ``` ### [openSUSE](https://www.opensuse.org) ```bash sudo zypper install buildah ``` ### [openSUSE Kubic](https://kubic.opensuse.org) transactional-update pkg in buildah ### [RHEL7](https://www.redhat.com/en/technologies/linux-platforms/enterprise-linux) Subscribe, then enable Extras channel and install buildah. ```bash sudo subscription-manager repos --enable=rhel-7-server-extras-rpms sudo yum -y install buildah ``` #### [Raspberry Pi OS arm64 (beta)](https://downloads.raspberrypi.org/raspios_arm64/images/) Raspberry Pi OS use the standard Debian's repositories, so it is fully compatible with Debian's arm64 repository. You can simply follow the [steps for Debian](#debian) to install buildah. ### [RHEL8 Beta](https://www.redhat.com/en/blog/powering-its-future-while-preserving-present-introducing-red-hat-enterprise-linux-8-beta?intcmp=701f2000001Cz6OAAS) ```bash sudo yum module enable -y container-tools:1.0 sudo yum module install -y buildah ``` ### [Ubuntu](https://www.ubuntu.com) The buildah package is available in the official repositories for Ubuntu 20.10 and newer. ```bash # Ubuntu 20.10 and newer sudo apt-get -y update sudo apt-get -y install buildah ``` # Building from scratch ## System Requirements ### Kernel Version Requirements To run Buildah on Red Hat Enterprise Linux or CentOS, version 7.4 or higher is required. On other Linux distributions Buildah requires a kernel version that supports the OverlayFS and/or fuse-overlayfs filesystem -- you'll need to consult your distribution's documentation to determine a minimum version number. ### runc Requirement Buildah uses `runc` to run commands when `buildah run` is used, or when `buildah build` encounters a `RUN` instruction, so you'll also need to build and install a compatible version of [runc](https://github.com/opencontainers/runc) for Buildah to call for those cases. If Buildah is installed via a package manager such as yum, dnf or apt-get, runc will be installed as part of that process. ## Package Installation Buildah is available on several software repositories and can be installed via a package manager such as yum, dnf or apt-get on a number of Linux distributions. ## Installation from GitHub Prior to installing Buildah, install the following packages on your Linux distro: * make * golang (Requires version 1.13 or higher.) * bats * btrfs-progs-devel * bzip2 * git * go-md2man * gpgme-devel * glib2-devel * libassuan-devel * libseccomp-devel * runc (Requires version 1.0 RC4 or higher.) * containers-common ### Fedora In Fedora, you can use this command: ``` dnf -y install \ make \ golang \ bats \ btrfs-progs-devel \ glib2-devel \ gpgme-devel \ libassuan-devel \ libseccomp-devel \ git \ bzip2 \ go-md2man \ runc \ containers-common ``` Then to install Buildah on Fedora follow the steps in this example: ``` git clone https://github.com/containers/buildah cd buildah make sudo make install buildah --help ``` ### RHEL, CentOS In RHEL and CentOS, run this command to install the build dependencies: ``` yum -y install \ make \ golang \ bats \ btrfs-progs-devel \ glib2-devel \ gpgme-devel \ libassuan-devel \ libseccomp-devel \ git \ bzip2 \ go-md2man \ runc \ skopeo-containers ``` The build steps for Buildah on RHEL or CentOS are the same as for Fedora, above. ### openSUSE On openSUSE Tumbleweed, install go via `zypper in go`, then run this command: ``` zypper in make \ git \ golang \ runc \ bzip2 \ libgpgme-devel \ libseccomp-devel \ libbtrfs-devel \ go-md2man ``` The build steps for Buildah on SUSE / openSUSE are the same as for Fedora, above. ### Ubuntu/Debian In Ubuntu 22.10 (Karmic) or Debian 12 (Bookworm) you can use these commands: ``` sudo apt-get -y -qq update sudo apt-get -y install bats btrfs-progs git go-md2man golang libapparmor-dev libglib2.0-dev libgpgme11-dev libseccomp-dev libselinux1-dev make runc skopeo libbtrfs-dev ``` The build steps for Buildah on Debian or Ubuntu are the same as for Fedora, above. ## Vendoring - Dependency Management This project is using [go modules](https://github.com/golang/go/wiki/Modules) for dependency management. If the CI is complaining about a pull request leaving behind an unclean state, it is very likely right about it. After changing dependencies, make sure to run `make vendor-in-container` to synchronize the code with the go module and repopulate the `./vendor` directory. ## Configuration files The following configuration files are required in order for Buildah to run appropriately. The majority of these files are commonly contained in the `containers-common` package. ### [registries.conf](https://github.com/containers/buildah/blob/main/docs/samples/registries.conf) #### Man Page: [registries.conf.5](https://github.com/containers/image/blob/main/docs/containers-registries.conf.5.md) `/etc/containers/registries.conf` registries.conf is the configuration file which specifies which container registries should be consulted when completing image names which do not include a registry or domain portion. #### Example from the Fedora `containers-common` package ``` cat /etc/containers/registries.conf # This is a system-wide configuration file used to # keep track of registries for various container backends. # It adheres to TOML format and does not support recursive # lists of registries. # The default location for this configuration file is /etc/containers/registries.conf. # The only valid categories are: 'registries.search', 'registries.insecure', # and 'registries.block'. [registries.search] registries = ['docker.io', 'registry.fedoraproject.org', 'quay.io', 'registry.access.redhat.com'] # If you need to access insecure registries, add the registry's fully-qualified name. # An insecure registry is one that does not have a valid SSL certificate or only does HTTP. [registries.insecure] registries = [] # If you need to block pull access from a registry, uncomment the section below # and add the registries fully-qualified name. # # Docker only [registries.block] registries = [] ``` ### [mounts.conf](https://src.fedoraproject.org/rpms/skopeo/blob/main/f/mounts.conf) `/usr/share/containers/mounts.conf` and optionally `/etc/containers/mounts.conf` The mounts.conf files specify volume mount files or directories that are automatically mounted inside containers when executing the `buildah run` or `buildah build` commands. Container processes can then use this content. The volume mount content does not get committed to the final image. This file is usually provided by the containers-common package. Usually these directories are used for passing secrets or credentials required by the package software to access remote package repositories. For example, a mounts.conf with the line "`/usr/share/rhel/secrets:/run/secrets`", the content of `/usr/share/rhel/secrets` directory is mounted on `/run/secrets` inside the container. This mountpoint allows Red Hat Enterprise Linux subscriptions from the host to be used within the container. It is also possible to omit the destination if it's equal to the source path. For example, specifying `/var/lib/secrets` will mount the directory into the same container destination path `/var/lib/secrets`. Note this is not a volume mount. The content of the volumes is copied into container storage, not bind mounted directly from the host. #### Example from the Fedora `containers-common` package: ``` cat /usr/share/containers/mounts.conf /usr/share/rhel/secrets:/run/secrets ``` ### [seccomp.json](https://src.fedoraproject.org/rpms/skopeo/blob/main/f/seccomp.json) `/usr/share/containers/seccomp.json` seccomp.json contains the list of seccomp rules to be allowed inside of containers. This file is usually provided by the containers-common package. The link above takes you to the seccomp.json ### [policy.json](https://github.com/containers/skopeo/blob/main/default-policy.json) `/etc/containers/policy.json` #### Man Page: [policy.json.5](https://github.com/containers/image/blob/main/docs/policy.json.md) #### Example from the Fedora `containers-common` package: ``` cat /etc/containers/policy.json { "default": [ { "type": "insecureAcceptAnything" } ], "transports": { "docker-daemon": { "": [{"type":"insecureAcceptAnything"}] } } } ``` ## Debug with Delve and the like To make a source debug build without optimizations use `BUILDDEBUG=1`, like: ``` make all BUILDDEBUG=1 ``` ## Vendoring Buildah uses Go Modules for vendoring purposes. If you need to update or add a vendored package into Buildah, please follow this procedure: * Enter into your sandbox `src/github.com/containers/buildah` and ensure that the GOPATH variable is set to the directory prior as noted above. * `export GO111MODULE=on` * `go get` the needed version: * Assuming you want to 'bump' the `github.com/containers/storage` package to version 1.12.13, use this command: `go get github.com/containers/storage@v1.12.13` * Assuming that you want to 'bump' the `github.com/containers/storage` package to a particular commit, use this command: `go get github.com/containers/storage@e307568568533c4afccdf7b56df7b4493e4e9a7b` * `make vendor-in-container` * `make` * `make install` * Then add any updated or added files with `git add` then do a `git commit` and create a PR. ### Vendor from your own fork If you wish to vendor in your personal fork to try changes out (assuming containers/storage in the below example): * `go mod edit -replace github.com/containers/storage=github.com/{mygithub_username}/storage@YOUR_BRANCH` * `make vendor-in-container` To revert * `go mod edit -dropreplace github.com/containers/storage` * `make vendor-in-container` To speed up fetching dependencies, you can use a [Go Module Proxy](https://proxy.golang.org) by setting `GOPROXY=https://proxy.golang.org`. ================================================ FILE: internal/config/convert.go ================================================ package config import ( "maps" "slices" dockerclient "github.com/fsouza/go-dockerclient" "go.podman.io/image/v5/manifest" ) // Schema2ConfigFromGoDockerclientConfig converts a go-dockerclient Config // structure to a manifest Schema2Config. func Schema2ConfigFromGoDockerclientConfig(config *dockerclient.Config) *manifest.Schema2Config { overrideExposedPorts := make(map[manifest.Schema2Port]struct{}) for port := range config.ExposedPorts { overrideExposedPorts[manifest.Schema2Port(port)] = struct{}{} } var overrideHealthCheck *manifest.Schema2HealthConfig if config.Healthcheck != nil { overrideHealthCheck = &manifest.Schema2HealthConfig{ Test: config.Healthcheck.Test, StartPeriod: config.Healthcheck.StartPeriod, Interval: config.Healthcheck.Interval, Timeout: config.Healthcheck.Timeout, Retries: config.Healthcheck.Retries, } } labels := make(map[string]string) maps.Copy(labels, config.Labels) volumes := make(map[string]struct{}) for v := range config.Volumes { volumes[v] = struct{}{} } s2config := &manifest.Schema2Config{ Hostname: config.Hostname, Domainname: config.Domainname, User: config.User, AttachStdin: config.AttachStdin, AttachStdout: config.AttachStdout, AttachStderr: config.AttachStderr, ExposedPorts: overrideExposedPorts, Tty: config.Tty, OpenStdin: config.OpenStdin, StdinOnce: config.StdinOnce, Env: slices.Clone(config.Env), Cmd: slices.Clone(config.Cmd), Healthcheck: overrideHealthCheck, ArgsEscaped: config.ArgsEscaped, Image: config.Image, Volumes: volumes, WorkingDir: config.WorkingDir, Entrypoint: slices.Clone(config.Entrypoint), NetworkDisabled: config.NetworkDisabled, MacAddress: config.MacAddress, OnBuild: slices.Clone(config.OnBuild), Labels: labels, StopSignal: config.StopSignal, Shell: config.Shell, } if config.StopTimeout != 0 { s2config.StopTimeout = &config.StopTimeout } return s2config } // GoDockerclientConfigFromSchema2Config converts a manifest Schema2Config // to a go-dockerclient config structure. func GoDockerclientConfigFromSchema2Config(s2config *manifest.Schema2Config) *dockerclient.Config { overrideExposedPorts := make(map[dockerclient.Port]struct{}) for port := range s2config.ExposedPorts { overrideExposedPorts[dockerclient.Port(port)] = struct{}{} } var healthCheck *dockerclient.HealthConfig if s2config.Healthcheck != nil { healthCheck = &dockerclient.HealthConfig{ Test: s2config.Healthcheck.Test, StartPeriod: s2config.Healthcheck.StartPeriod, Interval: s2config.Healthcheck.Interval, Timeout: s2config.Healthcheck.Timeout, Retries: s2config.Healthcheck.Retries, } } labels := make(map[string]string) maps.Copy(labels, s2config.Labels) volumes := make(map[string]struct{}) for v := range s2config.Volumes { volumes[v] = struct{}{} } config := &dockerclient.Config{ Hostname: s2config.Hostname, Domainname: s2config.Domainname, User: s2config.User, AttachStdin: s2config.AttachStdin, AttachStdout: s2config.AttachStdout, AttachStderr: s2config.AttachStderr, PortSpecs: nil, ExposedPorts: overrideExposedPorts, Tty: s2config.Tty, OpenStdin: s2config.OpenStdin, StdinOnce: s2config.StdinOnce, Env: slices.Clone(s2config.Env), Cmd: slices.Clone(s2config.Cmd), Healthcheck: healthCheck, ArgsEscaped: s2config.ArgsEscaped, Image: s2config.Image, Volumes: volumes, WorkingDir: s2config.WorkingDir, Entrypoint: slices.Clone(s2config.Entrypoint), NetworkDisabled: s2config.NetworkDisabled, MacAddress: s2config.MacAddress, OnBuild: slices.Clone(s2config.OnBuild), Labels: labels, StopSignal: s2config.StopSignal, Shell: s2config.Shell, } if s2config.StopTimeout != nil { config.StopTimeout = *s2config.StopTimeout } return config } ================================================ FILE: internal/config/convert_test.go ================================================ package config import ( "reflect" "slices" "strconv" "testing" dockerclient "github.com/fsouza/go-dockerclient" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.podman.io/image/v5/manifest" ) // fillAllFields recursively fills in 1 or "1" for every field in the passed-in // structure, and that slices and maps have at least one value in them. func fillAllFields[pStruct any](t *testing.T, st pStruct) { v := reflect.ValueOf(st) if v.Kind() == reflect.Pointer { v = reflect.Indirect(v) } fillAllValueFields(t, v) } func fillAllValueFields(t *testing.T, v reflect.Value) { fields := reflect.VisibleFields(v.Type()) for _, field := range fields { if field.Anonymous { // all right, fine, keep your secrets continue } f := v.FieldByName(field.Name) var keyType, elemType reflect.Type if field.Type.Kind() == reflect.Map { keyType = field.Type.Key() } switch field.Type.Kind() { case reflect.Array, reflect.Chan, reflect.Map, reflect.Pointer, reflect.Slice: elemType = field.Type.Elem() } fillValue(t, f, field.Name, field.Type.Kind(), keyType, elemType) } } func fillValue(t *testing.T, value reflect.Value, name string, kind reflect.Kind, keyType, elemType reflect.Type) { switch kind { case reflect.Invalid, reflect.Array, reflect.Chan, reflect.Func, reflect.Interface, reflect.UnsafePointer, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: require.NotEqualf(t, kind, kind, "unhandled %s field %s: tests require updating", kind, name) case reflect.Bool: value.SetBool(true) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: value.SetInt(1) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: value.SetUint(1) case reflect.Map: if value.IsNil() { value.Set(reflect.MakeMap(value.Type())) } keyPtr := reflect.New(keyType) key := reflect.Indirect(keyPtr) fillValue(t, key, name, keyType.Kind(), nil, nil) elemPtr := reflect.New(elemType) elem := reflect.Indirect(elemPtr) fillValue(t, elem, name, elemType.Kind(), nil, nil) value.SetMapIndex(key, reflect.Indirect(elem)) case reflect.Slice: vPtr := reflect.New(elemType) v := reflect.Indirect(vPtr) fillValue(t, v, name, elemType.Kind(), nil, nil) value.Set(reflect.Append(reflect.MakeSlice(value.Type(), 0, 1), v)) case reflect.String: value.SetString("1") case reflect.Struct: fillAllValueFields(t, value) case reflect.Pointer: p := reflect.New(elemType) fillValue(t, reflect.Indirect(p), name, elemType.Kind(), nil, nil) value.Set(p) } } // checkAllFields recursively checks that every field not listed in allowZeroed // is not set to its zero value, that every slice is not empty, and that every // map has at least one entry. It makes an additional exception for structs // which have no defined fields. func checkAllFields[pStruct any](t *testing.T, st pStruct, allowZeroed []string) { v := reflect.ValueOf(st) if v.Kind() == reflect.Pointer { v = reflect.Indirect(v) } checkAllValueFields(t, v, "", allowZeroed) } func checkAllValueFields(t *testing.T, v reflect.Value, name string, allowedToBeZero []string) { fields := reflect.VisibleFields(v.Type()) for _, field := range fields { if field.Anonymous { // all right, fine, keep your secrets continue } fieldName := field.Name if name != "" { fieldName = name + "." + field.Name } if slices.Contains(allowedToBeZero, fieldName) { continue } f := v.FieldByName(field.Name) var elemType reflect.Type switch field.Type.Kind() { case reflect.Array, reflect.Chan, reflect.Map, reflect.Pointer, reflect.Slice: elemType = field.Type.Elem() } checkValue(t, f, fieldName, field.Type.Kind(), elemType, allowedToBeZero) } } func checkValue(t *testing.T, value reflect.Value, name string, kind reflect.Kind, elemType reflect.Type, allowedToBeZero []string) { if kind != reflect.Invalid { switch kind { case reflect.Map: assert.Falsef(t, value.IsZero(), "map field %s not set when it was not already expected to be left unpopulated by conversion", name) keys := value.MapKeys() for i := range len(keys) { v := value.MapIndex(keys[i]) checkValue(t, v, name+"{"+keys[i].String()+"}", elemType.Kind(), nil, allowedToBeZero) } case reflect.Slice: assert.Falsef(t, value.IsZero(), "slice field %s not set when it was not already expected to be left unpopulated by conversion", name) for i := range value.Len() { v := value.Index(i) checkValue(t, v, name+"["+strconv.Itoa(i)+"]", elemType.Kind(), nil, allowedToBeZero) } case reflect.Struct: if fields := reflect.VisibleFields(value.Type()); len(fields) != 0 { // structs which are defined with no fields are okay assert.Falsef(t, value.IsZero(), "slice field %s not set when it was not already expected to be left unpopulated by conversion", name) } checkAllValueFields(t, value, name, allowedToBeZero) case reflect.Pointer: assert.Falsef(t, value.IsZero(), "pointer field %s not set when it was not already expected to be left unpopulated by conversion", name) checkValue(t, reflect.Indirect(value), name, elemType.Kind(), nil, allowedToBeZero) } } } func TestGoDockerclientConfigFromSchema2Config(t *testing.T) { t.Parallel() var input manifest.Schema2Config fillAllFields(t, &input) output := GoDockerclientConfigFromSchema2Config(&input) // make exceptions for fields in "output" which have no corresponding field in "input" notInSchema2Config := []string{"CPUSet", "CPUShares", "DNS", "Memory", "KernelMemory", "MemorySwap", "MemoryReservation", "Mounts", "PortSpecs", "PublishService", "SecurityOpts", "VolumeDriver", "VolumesFrom"} checkAllFields(t, output, notInSchema2Config) } func TestSchema2ConfigFromGoDockerclientConfig(t *testing.T) { t.Parallel() var input dockerclient.Config fillAllFields(t, &input) output := Schema2ConfigFromGoDockerclientConfig(&input) // make exceptions for fields in "output" which have no corresponding field in "input" notInDockerConfig := []string{} checkAllFields(t, output, notInDockerConfig) } ================================================ FILE: internal/config/executor.go ================================================ package config import ( "errors" "fmt" "os" dockerclient "github.com/fsouza/go-dockerclient" "github.com/openshift/imagebuilder" ) // configOnlyExecutor implements the Executor interface that an // imagebuilder.Builder expects to be able to call to do some heavy lifting, // but it just refuses to do the work of ADD, COPY, or RUN. It also doesn't // care if the working directory exists in a container, because it's really // only concerned with letting the Builder's RunConfig get updated by changes // from a Dockerfile. Try anything more than that and it'll return an error. type configOnlyExecutor struct{} func (g *configOnlyExecutor) Preserve(_ string) error { return errors.New("ADD/COPY/RUN not supported as changes") } func (g *configOnlyExecutor) EnsureContainerPath(_ string) error { return nil } func (g *configOnlyExecutor) EnsureContainerPathAs(_, _ string, _ *os.FileMode) error { return nil } func (g *configOnlyExecutor) Copy(_ []string, copies ...imagebuilder.Copy) error { if len(copies) == 0 { return nil } return errors.New("ADD/COPY not supported as changes") } func (g *configOnlyExecutor) Run(_ imagebuilder.Run, _ dockerclient.Config) error { return errors.New("RUN not supported as changes") } func (g *configOnlyExecutor) UnrecognizedInstruction(step *imagebuilder.Step) error { return fmt.Errorf("did not understand change instruction %q", step.Original) } ================================================ FILE: internal/config/executor_test.go ================================================ package config import "github.com/openshift/imagebuilder" var _ imagebuilder.Executor = &configOnlyExecutor{} ================================================ FILE: internal/config/override.go ================================================ package config import ( "fmt" "maps" "os" "slices" "strings" "github.com/containers/buildah/docker" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/openshift/imagebuilder" "go.podman.io/image/v5/manifest" ) // firstStringElseSecondString takes two strings, and returns the first // string if it isn't empty, else the second string func firstStringElseSecondString(first, second string) string { if first != "" { return first } return second } // firstSliceElseSecondSlice takes two string slices, and returns the first // slice of strings if it has contents, else the second slice func firstSliceElseSecondSlice(first, second []string) []string { if len(first) > 0 { return slices.Clone(first) } return slices.Clone(second) } // firstSlicePairElseSecondSlicePair takes two pairs of string slices, and // returns the first pair of slices if either has contents, else the second // pair func firstSlicePairElseSecondSlicePair(firstA, firstB, secondA, secondB []string) ([]string, []string) { if len(firstA) > 0 || len(firstB) > 0 { return slices.Clone(firstA), slices.Clone(firstB) } return slices.Clone(secondA), slices.Clone(secondB) } // mergeEnv combines variables from a and b into a single environment slice. if // a and b both provide values for the same variable, the value from b is // preferred func mergeEnv(a, b []string) []string { index := make(map[string]int) results := make([]string, 0, len(a)+len(b)) for _, kv := range slices.Concat(a, b) { k, _, specifiesValue := strings.Cut(kv, "=") if !specifiesValue { if value, ok := os.LookupEnv(kv); ok { kv = kv + "=" + value } else { kv = kv + "=" } } if i, seen := index[k]; seen { results[i] = kv } else { index[k] = len(results) results = append(results, kv) } } return results } func parseOverrideChanges(overrideChanges []string, overrideConfig *manifest.Schema2Config) (*manifest.Schema2Config, error) { if len(overrideChanges) == 0 { return overrideConfig, nil } if overrideConfig == nil { overrideConfig = &manifest.Schema2Config{} } // Parse the set of changes as we would a Dockerfile. changes := strings.Join(overrideChanges, "\n") parsed, err := imagebuilder.ParseDockerfile(strings.NewReader(changes)) if err != nil { return overrideConfig, fmt.Errorf("parsing change set %+v: %w", changes, err) } // Create a dummy builder object to process configuration-related // instructions. subBuilder := imagebuilder.NewBuilder(nil) // Convert the incoming data into an initial RunConfig. subBuilder.RunConfig = *GoDockerclientConfigFromSchema2Config(overrideConfig) // Process the change instructions one by one. for _, node := range parsed.Children { var step imagebuilder.Step if err := step.Resolve(node); err != nil { return overrideConfig, fmt.Errorf("resolving change %q: %w", node.Original, err) } if err := subBuilder.Run(&step, &configOnlyExecutor{}, true); err != nil { return overrideConfig, fmt.Errorf("processing change %q: %w", node.Original, err) } } // Pull settings out of the dummy builder's RunConfig. return Schema2ConfigFromGoDockerclientConfig(&subBuilder.RunConfig), nil } // OverrideOCI takes a buildah docker config and an OCI ImageConfig, and applies a // mixture of a slice of Dockerfile-style instructions and fields from a config // blob to them both func OverrideOCI(oconfig *v1.ImageConfig, overrideChanges []string, overrideConfig *manifest.Schema2Config) error { overrideConfig, err := parseOverrideChanges(overrideChanges, overrideConfig) if err != nil { return err } if overrideConfig != nil { // Apply changes from a possibly-provided possibly-changed config struct. oconfig.User = firstStringElseSecondString(overrideConfig.User, oconfig.User) if len(overrideConfig.ExposedPorts) > 0 { oexposedPorts := make(map[string]struct{}) for port := range oconfig.ExposedPorts { oexposedPorts[port] = struct{}{} } for port := range overrideConfig.ExposedPorts { oexposedPorts[string(port)] = struct{}{} } oconfig.ExposedPorts = oexposedPorts } if len(overrideConfig.Env) > 0 { oconfig.Env = mergeEnv(oconfig.Env, overrideConfig.Env) } oconfig.Entrypoint, oconfig.Cmd = firstSlicePairElseSecondSlicePair(overrideConfig.Entrypoint, overrideConfig.Cmd, oconfig.Entrypoint, oconfig.Cmd) if len(overrideConfig.Volumes) > 0 { if oconfig.Volumes == nil { oconfig.Volumes = make(map[string]struct{}) } for volume := range overrideConfig.Volumes { oconfig.Volumes[volume] = struct{}{} } } oconfig.WorkingDir = firstStringElseSecondString(overrideConfig.WorkingDir, oconfig.WorkingDir) if len(overrideConfig.Labels) > 0 { if oconfig.Labels == nil { oconfig.Labels = make(map[string]string) } maps.Copy(oconfig.Labels, overrideConfig.Labels) } oconfig.StopSignal = overrideConfig.StopSignal } return nil } // OverrideDocker takes a buildah docker config and an Docker Config, and applies a // mixture of a slice of Dockerfile-style instructions and fields from a config // blob to them both func OverrideDocker(dconfig *docker.Config, overrideChanges []string, overrideConfig *manifest.Schema2Config) error { overrideConfig, err := parseOverrideChanges(overrideChanges, overrideConfig) if err != nil { return err } if overrideConfig != nil { // Apply changes from a possibly-provided possibly-changed config struct. dconfig.Hostname = firstStringElseSecondString(overrideConfig.Hostname, dconfig.Hostname) dconfig.Domainname = firstStringElseSecondString(overrideConfig.Domainname, dconfig.Domainname) dconfig.User = firstStringElseSecondString(overrideConfig.User, dconfig.User) dconfig.AttachStdin = overrideConfig.AttachStdin dconfig.AttachStdout = overrideConfig.AttachStdout dconfig.AttachStderr = overrideConfig.AttachStderr if len(overrideConfig.ExposedPorts) > 0 { dexposedPorts := make(map[docker.Port]struct{}) for port := range dconfig.ExposedPorts { dexposedPorts[port] = struct{}{} } for port := range overrideConfig.ExposedPorts { dexposedPorts[docker.Port(port)] = struct{}{} } dconfig.ExposedPorts = dexposedPorts } dconfig.Tty = overrideConfig.Tty dconfig.OpenStdin = overrideConfig.OpenStdin dconfig.StdinOnce = overrideConfig.StdinOnce if len(overrideConfig.Env) > 0 { dconfig.Env = mergeEnv(dconfig.Env, overrideConfig.Env) } dconfig.Entrypoint, dconfig.Cmd = firstSlicePairElseSecondSlicePair(overrideConfig.Entrypoint, overrideConfig.Cmd, dconfig.Entrypoint, dconfig.Cmd) if overrideConfig.Healthcheck != nil { dconfig.Healthcheck = &docker.HealthConfig{ Test: slices.Clone(overrideConfig.Healthcheck.Test), Interval: overrideConfig.Healthcheck.Interval, Timeout: overrideConfig.Healthcheck.Timeout, StartPeriod: overrideConfig.Healthcheck.StartPeriod, Retries: overrideConfig.Healthcheck.Retries, } } dconfig.ArgsEscaped = overrideConfig.ArgsEscaped dconfig.Image = firstStringElseSecondString(overrideConfig.Image, dconfig.Image) if len(overrideConfig.Volumes) > 0 { if dconfig.Volumes == nil { dconfig.Volumes = make(map[string]struct{}) } for volume := range overrideConfig.Volumes { dconfig.Volumes[volume] = struct{}{} } } dconfig.WorkingDir = firstStringElseSecondString(overrideConfig.WorkingDir, dconfig.WorkingDir) dconfig.NetworkDisabled = overrideConfig.NetworkDisabled dconfig.MacAddress = overrideConfig.MacAddress dconfig.OnBuild = overrideConfig.OnBuild if len(overrideConfig.Labels) > 0 { if dconfig.Labels == nil { dconfig.Labels = make(map[string]string) } maps.Copy(dconfig.Labels, overrideConfig.Labels) } dconfig.StopSignal = overrideConfig.StopSignal dconfig.StopTimeout = overrideConfig.StopTimeout dconfig.Shell = firstSliceElseSecondSlice(overrideConfig.Shell, dconfig.Shell) } return nil } ================================================ FILE: internal/metadata/metadata.go ================================================ package metadata import ( "github.com/containers/buildah/docker" "github.com/opencontainers/go-digest" v1 "github.com/opencontainers/image-spec/specs-go/v1" ) // Build constructs a map containing the passed-in information about a just-committed or reused-as-cache image. func Build(imageConfigDigest digest.Digest, descriptor v1.Descriptor) (map[string]any, error) { metadata := make(map[string]any) if imageConfigDigest.Validate() == nil { metadata[docker.ExporterImageConfigDigestKey] = imageConfigDigest.String() } if descriptor.MediaType != "" && descriptor.Digest.Validate() == nil && descriptor.Size > 0 { metadata[docker.ExporterImageDescriptorKey] = descriptor } if descriptor.Digest.Validate() == nil { metadata[docker.ExporterImageDigestKey] = descriptor.Digest } return metadata, nil } ================================================ FILE: internal/mkcw/archive.go ================================================ package mkcw import ( "archive/tar" "bytes" "compress/gzip" "encoding/binary" "encoding/json" "errors" "fmt" "io" "io/fs" "os" "os/exec" "path/filepath" "strconv" "strings" "time" "github.com/containers/buildah/internal/tmpdir" "github.com/containers/buildah/pkg/overlay" "github.com/containers/luksy" "github.com/docker/go-units" digest "github.com/opencontainers/go-digest" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" "go.podman.io/storage/pkg/idtools" "go.podman.io/storage/pkg/ioutils" "go.podman.io/storage/pkg/mount" "go.podman.io/storage/pkg/system" ) const minimumImageSize = 10 * 1024 * 1024 // ArchiveOptions includes optional settings for generating an archive. type ArchiveOptions struct { // If supplied, we'll register the workload with this server. // Practically necessary if DiskEncryptionPassphrase is not set, in // which case we'll generate one and throw it away after. AttestationURL string // Used to measure the environment. If left unset (0, ""), defaults will be applied. CPUs int Memory int // Can be manually set. If left unset ("", false, nil), reasonable values will be used. TempDir string TeeType TeeType IgnoreAttestationErrors bool ImageSize int64 WorkloadID string Slop string DiskEncryptionPassphrase string FirmwareLibrary string Logger *logrus.Logger GraphOptions []string // passed in from a storage Store, probably ExtraImageContent map[string]string } type chainRetrievalError struct { stderr string err error } func (c chainRetrievalError) Error() string { if trimmed := strings.TrimSpace(c.stderr); trimmed != "" { return fmt.Sprintf("retrieving SEV certificate chain: sevctl: %v: %v", strings.TrimSpace(c.stderr), c.err) } return fmt.Sprintf("retrieving SEV certificate chain: sevctl: %v", c.err) } // Archive generates a WorkloadConfig for a specified directory and produces a // tar archive of a container image's rootfs with the expected contents. func Archive(rootfsPath string, ociConfig *v1.Image, options ArchiveOptions) (io.ReadCloser, WorkloadConfig, error) { const ( teeDefaultCPUs = 2 teeDefaultMemory = 512 teeDefaultFilesystem = "ext4" teeDefaultTeeType = SNP ) if rootfsPath == "" { return nil, WorkloadConfig{}, fmt.Errorf("required path not specified") } logger := options.Logger if logger == nil { logger = logrus.StandardLogger() } teeType := options.TeeType if teeType == "" { teeType = teeDefaultTeeType } cpus := options.CPUs if cpus == 0 { cpus = teeDefaultCPUs } memory := options.Memory if memory == 0 { memory = teeDefaultMemory } filesystem := teeDefaultFilesystem workloadID := options.WorkloadID if workloadID == "" { digestInput := rootfsPath + filesystem + time.Now().String() workloadID = digest.Canonical.FromString(digestInput).Encoded() } workloadConfig := WorkloadConfig{ Type: teeType, WorkloadID: workloadID, CPUs: cpus, Memory: memory, AttestationURL: options.AttestationURL, } if options.TempDir == "" { options.TempDir = tmpdir.GetTempDir() } // Do things which are specific to the type of TEE we're building for. var chainBytes []byte var chainBytesFile string var chainInfo fs.FileInfo switch teeType { default: return nil, WorkloadConfig{}, fmt.Errorf("don't know how to generate TeeData for TEE type %q", teeType) case SEV, SEV_NO_ES: // If we need a certificate chain, get it. chain, err := os.CreateTemp(options.TempDir, "chain") if err != nil { return nil, WorkloadConfig{}, err } chain.Close() defer func() { if err := os.Remove(chain.Name()); err != nil { logger.Warnf("error removing temporary file %q: %v", chain.Name(), err) } }() logrus.Debugf("sevctl export -f %s", chain.Name()) cmd := exec.Command("sevctl", "export", "-f", chain.Name()) var stdout, stderr bytes.Buffer cmd.Stdout, cmd.Stderr = &stdout, &stderr if err := cmd.Run(); err != nil { if !options.IgnoreAttestationErrors { return nil, WorkloadConfig{}, chainRetrievalError{stderr.String(), err} } logger.Warn(chainRetrievalError{stderr.String(), err}.Error()) } if chainBytes, err = os.ReadFile(chain.Name()); err != nil { chainBytes = []byte{} } var teeData SevWorkloadData if len(chainBytes) > 0 { chainBytesFile = "sev.chain" chainInfo, err = os.Stat(chain.Name()) if err != nil { return nil, WorkloadConfig{}, err } teeData.VendorChain = "/" + chainBytesFile } encodedTeeData, err := json.Marshal(teeData) if err != nil { return nil, WorkloadConfig{}, fmt.Errorf("encoding tee data: %w", err) } workloadConfig.TeeData = string(encodedTeeData) case SNP: teeData := SnpWorkloadData{ Generation: "milan", } encodedTeeData, err := json.Marshal(teeData) if err != nil { return nil, WorkloadConfig{}, fmt.Errorf("encoding tee data: %w", err) } workloadConfig.TeeData = string(encodedTeeData) } // We're going to want to add some content to the rootfs, so set up an // overlay that uses it as a lower layer so that we can write to it. st, err := system.Stat(rootfsPath) if err != nil { return nil, WorkloadConfig{}, fmt.Errorf("reading information about the container root filesystem: %w", err) } // Create a temporary directory to hold all of this. Use tmpdir.GetTempDir() // instead of the passed-in location, which a crafty caller might have put in an // overlay filesystem in storage because there tends to be more room there than // in, say, /var/tmp, and the plaintext disk image, which we put in the passed-in // location, can get quite large. rootfsParentDir, err := os.MkdirTemp(tmpdir.GetTempDir(), "buildah-rootfs") if err != nil { return nil, WorkloadConfig{}, fmt.Errorf("setting up parent for container root filesystem: %w", err) } defer func() { if err := os.RemoveAll(rootfsParentDir); err != nil { logger.Warnf("cleaning up parent for container root filesystem: %v", err) } }() // Create a mountpoint for the new overlay, which we'll use as the rootfs. rootfsDir := filepath.Join(rootfsParentDir, "rootfs") if err := idtools.MkdirAndChown(rootfsDir, fs.FileMode(st.Mode()), idtools.IDPair{UID: int(st.UID()), GID: int(st.GID())}); err != nil { return nil, WorkloadConfig{}, fmt.Errorf("creating mount target for container root filesystem: %w", err) } defer func() { if err := os.Remove(rootfsDir); err != nil { logger.Warnf("removing mount target for container root filesystem: %v", err) } }() // Create a directory to hold all of the overlay package's working state. tempDir := filepath.Join(rootfsParentDir, "tmp") if err = os.Mkdir(tempDir, 0o700); err != nil { return nil, WorkloadConfig{}, err } // Create some working state in there. overlayTempDir, err := overlay.TempDir(tempDir, int(st.UID()), int(st.GID())) if err != nil { return nil, WorkloadConfig{}, fmt.Errorf("setting up mount of container root filesystem: %w", err) } defer func() { if err := overlay.RemoveTemp(overlayTempDir); err != nil { logger.Warnf("cleaning up mount of container root filesystem: %v", err) } }() // Create a mount point using that working state. rootfsMount, err := overlay.Mount(overlayTempDir, rootfsPath, rootfsDir, 0, 0, options.GraphOptions) if err != nil { return nil, WorkloadConfig{}, fmt.Errorf("setting up support for overlay of container root filesystem: %w", err) } defer func() { if err := overlay.Unmount(overlayTempDir); err != nil { logger.Warnf("unmounting support for overlay of container root filesystem: %v", err) } }() // Follow through on the overlay or bind mount, whatever the overlay package decided // to leave to us to do. rootfsMountOptions := strings.Join(rootfsMount.Options, ",") logrus.Debugf("mounting %q to %q as %q with options %v", rootfsMount.Source, rootfsMount.Destination, rootfsMount.Type, rootfsMountOptions) if err := mount.Mount(rootfsMount.Source, rootfsMount.Destination, rootfsMount.Type, rootfsMountOptions); err != nil { return nil, WorkloadConfig{}, fmt.Errorf("mounting overlay of container root filesystem: %w", err) } defer func() { logrus.Debugf("unmounting %q", rootfsMount.Destination) if err := mount.Unmount(rootfsMount.Destination); err != nil { logger.Warnf("unmounting overlay of container root filesystem: %v", err) } }() // Pretend that we didn't have to do any of the preceding. rootfsPath = rootfsDir // Write extra content to the rootfs, creating intermediate directories if necessary. for location, content := range options.ExtraImageContent { err := func() error { if err := idtools.MkdirAllAndChownNew(filepath.Dir(filepath.Join(rootfsPath, location)), 0o755, idtools.IDPair{UID: int(st.UID()), GID: int(st.GID())}); err != nil { return fmt.Errorf("ensuring %q is present in container root filesystem: %w", filepath.Dir(location), err) } output, err := os.OpenFile(filepath.Join(rootfsPath, location), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644) if err != nil { return fmt.Errorf("preparing to write %q to container root filesystem: %w", location, err) } defer output.Close() input, err := os.Open(content) if err != nil { return err } defer input.Close() if _, err := io.Copy(output, input); err != nil { return fmt.Errorf("copying contents of %q to %q in container root filesystem: %w", content, location, err) } if err := output.Chown(int(st.UID()), int(st.GID())); err != nil { return fmt.Errorf("setting owner of %q in the container root filesystem: %w", location, err) } if err := output.Chmod(0o644); err != nil { return fmt.Errorf("setting permissions on %q in the container root filesystem: %w", location, err) } return nil }() if err != nil { return nil, WorkloadConfig{}, err } } // Write part of the config blob where the krun init process will be // looking for it. The oci2cw tool used `buildah inspect` output, but // init is just looking for fields that have the right names in any // object, and the image's config will have that, so let's try encoding // it directly. krunConfigPath := filepath.Join(rootfsPath, ".krun_config.json") krunConfigBytes, err := json.Marshal(ociConfig) if err != nil { return nil, WorkloadConfig{}, fmt.Errorf("creating .krun_config from image configuration: %w", err) } if err := ioutils.AtomicWriteFile(krunConfigPath, krunConfigBytes, 0o600); err != nil { return nil, WorkloadConfig{}, fmt.Errorf("saving krun config: %w", err) } // Encode the workload config, in case it fails for any reason. cleanedUpWorkloadConfig := workloadConfig switch cleanedUpWorkloadConfig.Type { default: return nil, WorkloadConfig{}, fmt.Errorf("don't know how to canonicalize TEE type %q", cleanedUpWorkloadConfig.Type) case SEV, SEV_NO_ES: cleanedUpWorkloadConfig.Type = SEV case SNP: cleanedUpWorkloadConfig.Type = SNP } workloadConfigBytes, err := json.Marshal(cleanedUpWorkloadConfig) if err != nil { return nil, WorkloadConfig{}, err } // Make sure we have the passphrase to use for encrypting the disk image. diskEncryptionPassphrase := options.DiskEncryptionPassphrase if diskEncryptionPassphrase == "" { diskEncryptionPassphrase, err = GenerateDiskEncryptionPassphrase() if err != nil { return nil, WorkloadConfig{}, err } } // If we weren't told how big the image should be, get a rough estimate // of the input data size, then add a hedge to it. imageSize := slop(options.ImageSize, options.Slop) if imageSize == 0 { var sourceSize int64 if err := filepath.WalkDir(rootfsPath, func(_ string, d fs.DirEntry, err error) error { if err != nil && !errors.Is(err, os.ErrNotExist) && !errors.Is(err, os.ErrPermission) { return err } info, err := d.Info() if err != nil && !errors.Is(err, os.ErrNotExist) && !errors.Is(err, os.ErrPermission) { return err } sourceSize += info.Size() return nil }); err != nil { return nil, WorkloadConfig{}, err } imageSize = slop(sourceSize, options.Slop) } if imageSize%4096 != 0 { imageSize += (4096 - (imageSize % 4096)) } if imageSize < minimumImageSize { imageSize = minimumImageSize } // Create a file to use as the unencrypted version of the disk image. plain, err := os.CreateTemp(options.TempDir, "plain.img") if err != nil { return nil, WorkloadConfig{}, err } removePlain := true defer func() { if removePlain { if err := os.Remove(plain.Name()); err != nil { logger.Warnf("removing temporary file %q: %v", plain.Name(), err) } } }() // Lengthen the plaintext disk image file. if err := plain.Truncate(imageSize); err != nil { plain.Close() return nil, WorkloadConfig{}, err } plainInfo, err := plain.Stat() plain.Close() if err != nil { return nil, WorkloadConfig{}, err } // Format the disk image with the filesystem contents. if _, stderr, err := MakeFS(rootfsPath, plain.Name(), filesystem); err != nil { if strings.TrimSpace(stderr) != "" { return nil, WorkloadConfig{}, fmt.Errorf("%s: %w", strings.TrimSpace(stderr), err) } return nil, WorkloadConfig{}, err } // If we're registering the workload, we can do that now. if workloadConfig.AttestationURL != "" { if err := SendRegistrationRequest(workloadConfig, diskEncryptionPassphrase, options.FirmwareLibrary, options.IgnoreAttestationErrors, logger); err != nil { return nil, WorkloadConfig{}, err } } // Try to encrypt on the fly. pipeReader, pipeWriter := io.Pipe() removePlain = false go func() { var err error defer func() { if err := os.Remove(plain.Name()); err != nil { logger.Warnf("removing temporary file %q: %v", plain.Name(), err) } if err != nil { pipeWriter.CloseWithError(err) } else { pipeWriter.Close() } }() plain, err := os.Open(plain.Name()) if err != nil { logrus.Errorf("opening unencrypted disk image %q: %v", plain.Name(), err) return } defer plain.Close() tw := tar.NewWriter(pipeWriter) defer tw.Flush() // Write /entrypoint var decompressedEntrypoint bytes.Buffer decompressor, err := gzip.NewReader(bytes.NewReader(entrypointCompressedBytes)) if err != nil { logrus.Errorf("decompressing copy of entrypoint: %v", err) return } defer decompressor.Close() if _, err = io.Copy(&decompressedEntrypoint, decompressor); err != nil { logrus.Errorf("decompressing copy of entrypoint: %v", err) return } entrypointHeader, err := tar.FileInfoHeader(plainInfo, "") if err != nil { logrus.Errorf("building header for entrypoint: %v", err) return } entrypointHeader.Name = "entrypoint" entrypointHeader.Mode = 0o755 entrypointHeader.Uname, entrypointHeader.Gname = "", "" entrypointHeader.Uid, entrypointHeader.Gid = 0, 0 entrypointHeader.Size = int64(decompressedEntrypoint.Len()) if err = tw.WriteHeader(entrypointHeader); err != nil { logrus.Errorf("writing header for %q: %v", entrypointHeader.Name, err) return } if _, err = io.Copy(tw, &decompressedEntrypoint); err != nil { logrus.Errorf("writing %q: %v", entrypointHeader.Name, err) return } // Write /sev.chain if chainInfo != nil { chainHeader, err := tar.FileInfoHeader(chainInfo, "") if err != nil { logrus.Errorf("building header for %q: %v", chainInfo.Name(), err) return } chainHeader.Name = chainBytesFile chainHeader.Mode = 0o600 chainHeader.Uname, chainHeader.Gname = "", "" chainHeader.Uid, chainHeader.Gid = 0, 0 chainHeader.Size = int64(len(chainBytes)) if err = tw.WriteHeader(chainHeader); err != nil { logrus.Errorf("writing header for %q: %v", chainHeader.Name, err) return } if _, err = tw.Write(chainBytes); err != nil { logrus.Errorf("writing %q: %v", chainHeader.Name, err) return } } // Write /krun-sev.json. workloadConfigHeader, err := tar.FileInfoHeader(plainInfo, "") if err != nil { logrus.Errorf("building header for %q: %v", plainInfo.Name(), err) return } workloadConfigHeader.Name = "krun-sev.json" workloadConfigHeader.Mode = 0o600 workloadConfigHeader.Uname, workloadConfigHeader.Gname = "", "" workloadConfigHeader.Uid, workloadConfigHeader.Gid = 0, 0 workloadConfigHeader.Size = int64(len(workloadConfigBytes)) if err = tw.WriteHeader(workloadConfigHeader); err != nil { logrus.Errorf("writing header for %q: %v", workloadConfigHeader.Name, err) return } if _, err = tw.Write(workloadConfigBytes); err != nil { logrus.Errorf("writing %q: %v", workloadConfigHeader.Name, err) return } // Write /tmp. tmpHeader, err := tar.FileInfoHeader(plainInfo, "") if err != nil { logrus.Errorf("building header for %q: %v", plainInfo.Name(), err) return } tmpHeader.Name = "tmp/" tmpHeader.Typeflag = tar.TypeDir tmpHeader.Mode = 0o1777 tmpHeader.Uname, tmpHeader.Gname = "", "" tmpHeader.Uid, tmpHeader.Gid = 0, 0 tmpHeader.Size = 0 if err = tw.WriteHeader(tmpHeader); err != nil { logrus.Errorf("writing header for %q: %v", tmpHeader.Name, err) return } // Now figure out the footer that we'll append to the encrypted disk. var footer bytes.Buffer lengthBuffer := make([]byte, 8) footer.Write(workloadConfigBytes) footer.WriteString("KRUN") binary.LittleEndian.PutUint64(lengthBuffer, uint64(len(workloadConfigBytes))) footer.Write(lengthBuffer) // Start encrypting and write /disk.img. header, encrypt, blockSize, err := luksy.EncryptV1([]string{diskEncryptionPassphrase}, "") paddingBoundary := int64(4096) paddingNeeded := (paddingBoundary - ((int64(len(header)) + imageSize + int64(footer.Len())) % paddingBoundary)) % paddingBoundary diskHeader := workloadConfigHeader diskHeader.Name = "disk.img" diskHeader.Mode = 0o600 diskHeader.Size = int64(len(header)) + imageSize + paddingNeeded + int64(footer.Len()) if err = tw.WriteHeader(diskHeader); err != nil { logrus.Errorf("writing archive header for disk.img: %v", err) return } if _, err = io.Copy(tw, bytes.NewReader(header)); err != nil { logrus.Errorf("writing encryption header for disk.img: %v", err) return } encryptWrapper := luksy.EncryptWriter(encrypt, tw, blockSize) if _, err = io.Copy(encryptWrapper, plain); err != nil { logrus.Errorf("encrypting disk.img: %v", err) return } encryptWrapper.Close() if _, err = tw.Write(make([]byte, paddingNeeded)); err != nil { logrus.Errorf("writing padding for disk.img: %v", err) return } if _, err = io.Copy(tw, &footer); err != nil { logrus.Errorf("writing footer for disk.img: %v", err) return } tw.Close() }() return pipeReader, workloadConfig, nil } func slop(size int64, slop string) int64 { if slop == "" { return size * 5 / 4 } for factor := range strings.SplitSeq(slop, "+") { factor = strings.TrimSpace(factor) if factor == "" { continue } if strings.HasSuffix(factor, "%") { percentage := strings.TrimSuffix(factor, "%") percent, err := strconv.ParseInt(percentage, 10, 8) if err != nil { logrus.Warnf("parsing percentage %q: %v", factor, err) } else { size *= (percent + 100) size /= 100 } } else { more, err := units.RAMInBytes(factor) if err != nil { logrus.Warnf("parsing %q as a size: %v", factor, err) } else { size += more } } } return size } ================================================ FILE: internal/mkcw/archive_test.go ================================================ package mkcw import ( "archive/tar" "bytes" "encoding/json" "errors" "fmt" "io" "net" "net/http" "os" "path/filepath" "sync" "testing" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestSlop(t *testing.T) { t.Parallel() testCases := []struct { input int64 slop string output int64 }{ {100, "", 125}, {100, "10%", 110}, {100, "100%", 200}, {100, "10GB", 10*1024*1024*1024 + 100}, {100, "10%+10GB", 10*1024*1024*1024 + 110}, {100, "10% + 10GB", 10*1024*1024*1024 + 110}, } for _, testCase := range testCases { t.Run(testCase.slop, func(t *testing.T) { assert.Equal(t, testCase.output, slop(testCase.input, testCase.slop)) }) } } // dummyAttestationHandler replies with a fixed response code to requests to // the right path, and caches passphrases indexed by workload ID type dummyAttestationHandler struct { t *testing.T status int passphrases map[string]string passphrasesLock sync.Mutex } func (d *dummyAttestationHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { var body bytes.Buffer if req.Body != nil { if _, err := io.Copy(&body, req.Body); err != nil { d.t.Logf("reading request body: %v", err) return } req.Body.Close() } if req.URL != nil && req.URL.Path == "/kbs/v0/register_workload" { var registrationRequest RegistrationRequest // if we can't decode the client request, bail if err := json.Unmarshal(body.Bytes(), ®istrationRequest); err != nil { rw.WriteHeader(http.StatusInternalServerError) return } // cache the passphrase d.passphrasesLock.Lock() if d.passphrases == nil { d.passphrases = make(map[string]string) } d.passphrases[registrationRequest.WorkloadID] = registrationRequest.Passphrase d.passphrasesLock.Unlock() // return the predetermined status status := d.status if status == 0 { status = http.StatusOK } rw.WriteHeader(status) return } // no such handler rw.WriteHeader(http.StatusInternalServerError) } func TestArchive(t *testing.T) { t.Parallel() ociConfig := &v1.Image{ Config: v1.ImageConfig{ User: "root", Env: []string{"PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/usr/sbin:/sbin:/usr/sbin:/sbin"}, Cmd: []string{"/bin/bash"}, WorkingDir: "/root", Labels: map[string]string{ "label_a": "b", "label_c": "d", }, }, } for _, status := range []int{http.StatusOK, http.StatusInternalServerError} { for _, ignoreChainRetrievalErrors := range []bool{false, true} { for _, ignoreAttestationErrors := range []bool{false, true} { t.Run(fmt.Sprintf("status=%d,ignoreChainRetrievalErrors=%v,ignoreAttestationErrors=%v", status, ignoreChainRetrievalErrors, ignoreAttestationErrors), func(t *testing.T) { // listen on a system-assigned port listener, err := net.Listen("tcp", ":0") require.NoError(t, err) // keep track of our listener address addr := listener.Addr() // serve requests on that listener handler := &dummyAttestationHandler{t: t, status: status} server := http.Server{ Handler: handler, } go func() { if err := server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { t.Logf("serve: %v", err) } }() // clean up at the end of this test t.Cleanup(func() { assert.NoError(t, server.Close()) }) // generate the container rootfs using a temporary empty directory archiveOptions := ArchiveOptions{ CPUs: 4, Memory: 256, TempDir: t.TempDir(), AttestationURL: "http://" + addr.String(), IgnoreAttestationErrors: ignoreAttestationErrors, } inputPath := t.TempDir() rc, workloadConfig, err := Archive(inputPath, ociConfig, archiveOptions) // bail now if we got an error we didn't expect if err != nil { if errors.As(err, &chainRetrievalError{}) { if !ignoreChainRetrievalErrors { return } } if errors.As(err, &attestationError{}) { if !ignoreAttestationErrors { require.NoError(t, err) } } return } if err == nil { defer rc.Close() } // read each archive entry's contents into a map contents := make(map[string][]byte) tr := tar.NewReader(rc) hdr, err := tr.Next() for hdr != nil { contents[hdr.Name], err = io.ReadAll(tr) require.NoError(t, err) hdr, err = tr.Next() } if err != nil { require.ErrorIs(t, err, io.EOF) } // check that krun-sev.json is a JSON-encoded copy of the workload config var writtenWorkloadConfig WorkloadConfig err = json.Unmarshal(contents["krun-sev.json"], &writtenWorkloadConfig) require.NoError(t, err) assert.Equal(t, workloadConfig, writtenWorkloadConfig) // save the disk image to a file encryptedFile := filepath.Join(t.TempDir(), "encrypted.img") err = os.WriteFile(encryptedFile, contents["disk.img"], 0o600) require.NoError(t, err) // check that we have a configuration footer in there _, err = ReadWorkloadConfigFromImage(encryptedFile) require.NoError(t, err) // check that the attestation server got the encryption passphrase handler.passphrasesLock.Lock() passphrase := handler.passphrases[workloadConfig.WorkloadID] handler.passphrasesLock.Unlock() err = CheckLUKSPassphrase(encryptedFile, passphrase) require.NoError(t, err) }) } } } } ================================================ FILE: internal/mkcw/attest.go ================================================ package mkcw import ( "bufio" "bytes" "encoding/json" "errors" "fmt" "net/http" "net/url" "os" "os/exec" "path" "path/filepath" "strings" types "github.com/containers/buildah/internal/mkcw/types" "github.com/sirupsen/logrus" "go.podman.io/storage/pkg/fileutils" ) type ( RegistrationRequest = types.RegistrationRequest TeeConfig = types.TeeConfig TeeConfigFlags = types.TeeConfigFlags TeeConfigMinFW = types.TeeConfigMinFW ) type measurementError struct { err error } func (m measurementError) Error() string { return fmt.Sprintf("generating measurement for attestation: %v", m.err) } type attestationError struct { err error } func (a attestationError) Error() string { return fmt.Sprintf("registering workload: %v", a.err) } type httpError struct { statusCode int } func (h httpError) Error() string { if statusText := http.StatusText(h.statusCode); statusText != "" { return fmt.Sprintf("received server status %d (%q)", h.statusCode, statusText) } return fmt.Sprintf("received server status %d", h.statusCode) } // SendRegistrationRequest registers a workload with the specified decryption // passphrase with the service whose location is part of the WorkloadConfig. func SendRegistrationRequest(workloadConfig WorkloadConfig, diskEncryptionPassphrase, firmwareLibrary string, ignoreAttestationErrors bool, logger *logrus.Logger) error { if workloadConfig.AttestationURL == "" { return errors.New("attestation URL not provided") } // Measure the execution environment. measurement, err := GenerateMeasurement(workloadConfig, firmwareLibrary) if err != nil { if !ignoreAttestationErrors { return &measurementError{err} } logger.Warnf("generating measurement for attestation: %v", err) } // Build the workload registration (attestation) request body. var teeConfigBytes []byte switch workloadConfig.Type { case SEV, SEV_NO_ES, SNP: var cbits types.TeeConfigFlagBits switch workloadConfig.Type { case SEV: cbits = types.SEV_CONFIG_NO_DEBUG | types.SEV_CONFIG_NO_KEY_SHARING | types.SEV_CONFIG_ENCRYPTED_STATE | types.SEV_CONFIG_NO_SEND | types.SEV_CONFIG_DOMAIN | types.SEV_CONFIG_SEV case SEV_NO_ES: cbits = types.SEV_CONFIG_NO_DEBUG | types.SEV_CONFIG_NO_KEY_SHARING | types.SEV_CONFIG_NO_SEND | types.SEV_CONFIG_DOMAIN | types.SEV_CONFIG_SEV case SNP: cbits = types.SNP_CONFIG_SMT | types.SNP_CONFIG_MANDATORY | types.SNP_CONFIG_MIGRATE_MA | types.SNP_CONFIG_DEBUG default: panic("internal error") // shouldn't happen } teeConfig := TeeConfig{ Flags: TeeConfigFlags{ Bits: cbits, }, MinFW: TeeConfigMinFW{ Major: 0, Minor: 0, }, } teeConfigBytes, err = json.Marshal(teeConfig) if err != nil { return err } default: return fmt.Errorf("don't know how to generate tee_config for %q TEEs", workloadConfig.Type) } registrationRequest := RegistrationRequest{ WorkloadID: workloadConfig.WorkloadID, LaunchMeasurement: measurement, TeeConfig: string(teeConfigBytes), Passphrase: diskEncryptionPassphrase, } registrationRequestBytes, err := json.Marshal(registrationRequest) if err != nil { return err } // Register the workload. parsedURL, err := url.Parse(workloadConfig.AttestationURL) if err != nil { return err } parsedURL.Path = path.Join(parsedURL.Path, "/kbs/v0/register_workload") if err != nil { return err } url := parsedURL.String() requestContentType := "application/json" requestBody := bytes.NewReader(registrationRequestBytes) defer http.DefaultClient.CloseIdleConnections() resp, err := http.Post(url, requestContentType, requestBody) if resp != nil { if resp.Body != nil { resp.Body.Close() } switch resp.StatusCode { default: if !ignoreAttestationErrors { return &attestationError{&httpError{resp.StatusCode}} } logger.Warn(attestationError{&httpError{resp.StatusCode}}.Error()) case http.StatusOK, http.StatusAccepted: // great! } } if err != nil { if !ignoreAttestationErrors { return &attestationError{err} } logger.Warn(attestationError{err}.Error()) } return nil } // GenerateMeasurement generates the runtime measurement using the CPU count, // memory size, and the firmware shared library, whatever it's called, wherever // it is. // If firmwareLibrary is a path, it will be the only one checked. // If firmwareLibrary is a filename, it will be checked for in a hard-coded set // of directories. // If firmwareLibrary is empty, both the filename and the directory it is in // will be taken from a hard-coded set of candidates. func GenerateMeasurement(workloadConfig WorkloadConfig, firmwareLibrary string) (string, error) { cpuString := fmt.Sprintf("%d", workloadConfig.CPUs) memoryString := fmt.Sprintf("%d", workloadConfig.Memory) var prefix string switch workloadConfig.Type { case SEV: prefix = "SEV-ES" case SEV_NO_ES: prefix = "SEV" case SNP: prefix = "SNP" default: return "", fmt.Errorf("don't know which measurement to use for TEE type %q", workloadConfig.Type) } sharedLibraryDirs := []string{ "/usr/local/lib64", "/usr/local/lib", "/lib64", "/lib", "/usr/lib64", "/usr/lib", } if llp, ok := os.LookupEnv("LD_LIBRARY_PATH"); ok { sharedLibraryDirs = append(sharedLibraryDirs, strings.Split(llp, ":")...) } libkrunfwNames := []string{ "libkrunfw-sev.so.4", "libkrunfw-sev.so.3", "libkrunfw-sev.so", } var pathsToCheck []string if firmwareLibrary == "" { for _, sharedLibraryDir := range sharedLibraryDirs { if sharedLibraryDir == "" { continue } for _, libkrunfw := range libkrunfwNames { candidate := filepath.Join(sharedLibraryDir, libkrunfw) pathsToCheck = append(pathsToCheck, candidate) } } } else { if filepath.IsAbs(firmwareLibrary) { pathsToCheck = append(pathsToCheck, firmwareLibrary) } else { for _, sharedLibraryDir := range sharedLibraryDirs { if sharedLibraryDir == "" { continue } candidate := filepath.Join(sharedLibraryDir, firmwareLibrary) pathsToCheck = append(pathsToCheck, candidate) } } } for _, candidate := range pathsToCheck { if err := fileutils.Lexists(candidate); err == nil { var stdout, stderr bytes.Buffer logrus.Debugf("krunfw_measurement -c %s -m %s %s", cpuString, memoryString, candidate) cmd := exec.Command("krunfw_measurement", "-c", cpuString, "-m", memoryString, candidate) cmd.Stdout = &stdout cmd.Stderr = &stderr if err := cmd.Run(); err != nil { if stderr.Len() > 0 { err = fmt.Errorf("krunfw_measurement: %s: %w", strings.TrimSpace(stderr.String()), err) } return "", err } scanner := bufio.NewScanner(&stdout) for scanner.Scan() { line := scanner.Text() if after, ok := strings.CutPrefix(line, prefix+":"); ok { return strings.TrimSpace(after), nil } } return "", fmt.Errorf("generating measurement: no line starting with %q found in output from krunfw_measurement", prefix+":") } } return "", fmt.Errorf("generating measurement: none of %v found: %w", pathsToCheck, os.ErrNotExist) } ================================================ FILE: internal/mkcw/embed/asm/doc.md ================================================ If we have a toolchain for the target that can handle plain assembly, build with that. ================================================ FILE: internal/mkcw/embed/asm/entrypoint_amd64.s ================================================ .section .rodata.1,"aMS",@progbits,1 msg: .string "This image is designed to be run as a confidential workload using libkrun.\n" .section .text._start,"ax",@progbits .globl _start .type _start,@function _start: movq $1, %rax # write movq $2, %rdi # fd=stderr_fileno movq $msg, %rsi # message movq $75, %rdx # length syscall movq $60, %rax # exit movq $1, %rdi # status=1 syscall .section .note.GNU-stack,"",@progbits ================================================ FILE: internal/mkcw/embed/check.sh ================================================ #!/usr/bin/env bash expected="This image is designed to be run as a confidential workload using libkrun." cd $(dirname ${BASH_SOURCE[0]}) for GOARCH in amd64 arm64 ppc64le s390x ; do make -C ../../.. internal/mkcw/embed/entrypoint_$GOARCH case $GOARCH in amd64) QEMUARCH=x86_64;; arm64) QEMUARCH=aarch64;; ppc64le|s390x) QEMUARCH=$GOARCH;; esac actual="$(qemu-$QEMUARCH ./entrypoint_$GOARCH 2>&1)" if test "$actual" != "$expected" ; then echo unexpected error from entrypoint_$GOARCH: "$actual" exit 1 fi done ================================================ FILE: internal/mkcw/embed/doc.go ================================================ // Supplying our own _start that just writes the message and exits avoids // pulling in the proper standard library, which produces a smaller binary, but // we still end up pulling in the language runtime. package main ================================================ FILE: internal/mkcw/embed/entrypoint.go ================================================ package main ================================================ FILE: internal/mkcw/embed/entrypoint_amd64.s ================================================ DATA msg+0(SB)/75, $"This image is designed to be run as a confidential workload using libkrun.\n" GLOBL msg(SB),8,$75 TEXT _start(SB),8-0,$0 MOVQ $1, AX // syscall=write MOVQ $2, DI // descriptor=2 MOVQ $msg(SB), SI // buffer (msg) address MOVQ $75, DX // buffer (msg) length SYSCALL MOVQ $60, AX // syscall=exit MOVQ $1, DI // status=1 SYSCALL ================================================ FILE: internal/mkcw/embed/entrypoint_arm64.s ================================================ DATA msg+0(SB)/75, $"This image is designed to be run as a confidential workload using libkrun.\n" GLOBL msg(SB),8,$75 TEXT _start(SB),8-0,$0 MOVD $64, R8 // syscall=write MOVD $2, R0 // descriptor=2 MOVD $msg(SB), R1 // buffer (msg) address MOVD $75, R2 // buffer (msg) length SVC MOVD $93, R8 // syscall=exit MOVD $1, R0 // status=1 SVC ================================================ FILE: internal/mkcw/embed/entrypoint_ppc64le.s ================================================ DATA msg+0(SB)/75, $"This image is designed to be run as a confidential workload using libkrun.\n" GLOBL msg(SB),8,$75 TEXT _start(SB),8-0,$0 MOVD $4, R0 // syscall=write MOVD $2, R3 // descriptor=2 MOVD $msg(SB), R4 // buffer (msg) address MOVD $75, R5 // buffer (msg) length SYSCALL MOVD $1, R0 // syscall=exit MOVD $1, R3 // status=1 SYSCALL ================================================ FILE: internal/mkcw/embed/entrypoint_s390x.s ================================================ DATA msg+0(SB)/75, $"This image is designed to be run as a confidential workload using libkrun.\n" GLOBL msg(SB),8,$75 TEXT _start(SB),8-0,$0 MOVD $4, R1 // syscall=write MOVD $2, R2 // descriptor=2 MOVD $msg(SB), R3 // buffer (msg) address MOVD $75, R4 // buffer (msg) length SYSCALL MOVD $1, R1 // syscall=exit MOVD $1, R2 // status=1 SYSCALL ================================================ FILE: internal/mkcw/entrypoint.go ================================================ package mkcw import _ "embed" //go:embed "embed/entrypoint_amd64.gz" var entrypointCompressedBytes []byte ================================================ FILE: internal/mkcw/luks.go ================================================ package mkcw import ( "crypto/rand" "encoding/hex" "fmt" "os" "github.com/containers/luksy" ) // CheckLUKSPassphrase checks that the specified LUKS-encrypted file can be // decrypted using the specified passphrase. func CheckLUKSPassphrase(path, decryptionPassphrase string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() v1header, v2headerA, v2headerB, v2json, err := luksy.ReadHeaders(f, luksy.ReadHeaderOptions{}) if err != nil { return err } if v1header != nil { _, _, _, _, err = v1header.Decrypt(decryptionPassphrase, f) return err } if v2headerA == nil && v2headerB == nil { return fmt.Errorf("no LUKS headers read from %q", path) } if v2headerA != nil { if _, _, _, _, err = v2headerA.Decrypt(decryptionPassphrase, f, *v2json); err != nil { return err } } if v2headerB != nil { if _, _, _, _, err = v2headerB.Decrypt(decryptionPassphrase, f, *v2json); err != nil { return err } } return nil } // GenerateDiskEncryptionPassphrase generates a random disk encryption password func GenerateDiskEncryptionPassphrase() (string, error) { randomizedBytes := make([]byte, 32) if _, err := rand.Read(randomizedBytes); err != nil { return "", err } return hex.EncodeToString(randomizedBytes), nil } ================================================ FILE: internal/mkcw/luks_test.go ================================================ package mkcw import ( "fmt" "os" "path/filepath" "testing" "github.com/containers/luksy" "github.com/stretchr/testify/require" ) func TestCheckLUKSPassphrase(t *testing.T) { t.Parallel() passphrase, err := GenerateDiskEncryptionPassphrase() require.NoError(t, err) secondPassphrase, err := GenerateDiskEncryptionPassphrase() require.NoError(t, err) t.Run("v1", func(t *testing.T) { header, encrypter, blockSize, err := luksy.EncryptV1([]string{secondPassphrase, passphrase}, "") require.NoError(t, err) f, err := os.Create(filepath.Join(t.TempDir(), "v1")) require.NoError(t, err) n, err := f.Write(header) require.NoError(t, err) require.Equal(t, len(header), n) wrapper := luksy.EncryptWriter(encrypter, f, blockSize) _, err = wrapper.Write(make([]byte, blockSize*10)) require.NoError(t, err) wrapper.Close() f.Close() err = CheckLUKSPassphrase(f.Name(), passphrase) require.NoError(t, err) err = CheckLUKSPassphrase(f.Name(), secondPassphrase) require.NoError(t, err) err = CheckLUKSPassphrase(f.Name(), "nope, this is not a correct passphrase") require.Error(t, err) }) t.Run("v2", func(t *testing.T) { for _, sectorSize := range []int{512, 1024, 2048, 4096} { t.Run(fmt.Sprintf("sectorSize=%d", sectorSize), func(t *testing.T) { header, encrypter, blockSize, err := luksy.EncryptV2([]string{secondPassphrase, passphrase}, "", sectorSize) require.NoError(t, err) f, err := os.Create(filepath.Join(t.TempDir(), "v2")) require.NoError(t, err) n, err := f.Write(header) require.NoError(t, err) require.Equal(t, len(header), n) wrapper := luksy.EncryptWriter(encrypter, f, blockSize) _, err = wrapper.Write(make([]byte, blockSize*10)) require.NoError(t, err) wrapper.Close() f.Close() err = CheckLUKSPassphrase(f.Name(), passphrase) require.NoError(t, err) err = CheckLUKSPassphrase(f.Name(), secondPassphrase) require.NoError(t, err) err = CheckLUKSPassphrase(f.Name(), "nope, this is not one of the correct passphrases") require.Error(t, err) }) } }) } ================================================ FILE: internal/mkcw/makefs.go ================================================ package mkcw import ( "fmt" "os/exec" "strings" "github.com/sirupsen/logrus" ) // MakeFS formats the imageFile as a filesystem of the specified type, // populating it with the contents of the directory at sourcePath. // Recognized filesystem types are "ext2", "ext3", "ext4", and "btrfs". // Note that krun's init is currently hard-wired to assume "ext4". // Returns the stdout, stderr, and any error returned by the mkfs command. func MakeFS(sourcePath, imageFile, filesystem string) (string, string, error) { var stdout, stderr strings.Builder // N.B. mkfs.xfs can accept a protofile via its -p option, but the // protofile format doesn't allow us to supply timestamp information or // specify that files are hard linked switch filesystem { case "ext2", "ext3", "ext4": logrus.Debugf("mkfs -t %s --rootdir %q %q", filesystem, sourcePath, imageFile) cmd := exec.Command("mkfs", "-t", filesystem, "-d", sourcePath, imageFile) cmd.Stdout = &stdout cmd.Stderr = &stderr err := cmd.Run() return stdout.String(), stderr.String(), err case "btrfs": logrus.Debugf("mkfs -t %s --rootdir %q %q", filesystem, sourcePath, imageFile) cmd := exec.Command("mkfs", "-t", filesystem, "--rootdir", sourcePath, imageFile) cmd.Stdout = &stdout cmd.Stderr = &stderr err := cmd.Run() return stdout.String(), stderr.String(), err } return "", "", fmt.Errorf("don't know how to make a %q filesystem with contents", filesystem) } ================================================ FILE: internal/mkcw/types/attest.go ================================================ package mkcwtypes // RegistrationRequest is the body of the request which we use for registering // this confidential workload with the attestation server. // https://github.com/virtee/reference-kbs/blob/10b2a4c0f8caf78a077210b172863bbae54f66aa/src/main.rs#L83 type RegistrationRequest struct { WorkloadID string `json:"workload_id"` LaunchMeasurement string `json:"launch_measurement"` Passphrase string `json:"passphrase"` TeeConfig string `json:"tee_config"` // JSON-encoded teeConfig? or specific to the type of TEE? } // TeeConfig contains information about a trusted execution environment. type TeeConfig struct { Flags TeeConfigFlags `json:"flags"` // runtime requirement bits MinFW TeeConfigMinFW `json:"minfw"` // minimum platform firmware version } // TeeConfigFlags is a bit field containing policy flags specific to the environment. // https://github.com/virtee/sev/blob/d3e40917fd8531c69f47c2498e9667fe8a5303aa/src/launch/sev.rs#L172 // https://github.com/virtee/sev/blob/d3e40917fd8531c69f47c2498e9667fe8a5303aa/src/launch/snp.rs#L114 type TeeConfigFlags struct { Bits TeeConfigFlagBits `json:"bits"` } // TeeConfigFlagBits are bits representing run-time expectations. type TeeConfigFlagBits int //nolint:revive,staticcheck // Don't warn about bad naming. const ( SEV_CONFIG_NO_DEBUG TeeConfigFlagBits = 0b00000001 // no debugging of guests SEV_CONFIG_NO_KEY_SHARING TeeConfigFlagBits = 0b00000010 // no sharing keys between guests SEV_CONFIG_ENCRYPTED_STATE TeeConfigFlagBits = 0b00000100 // requires SEV-ES SEV_CONFIG_NO_SEND TeeConfigFlagBits = 0b00001000 // no transferring the guest to another platform SEV_CONFIG_DOMAIN TeeConfigFlagBits = 0b00010000 // no transferring the guest out of the domain (?) SEV_CONFIG_SEV TeeConfigFlagBits = 0b00100000 // no transferring the guest to non-SEV platforms SNP_CONFIG_SMT TeeConfigFlagBits = 0b00000001 // SMT is enabled on the host machine SNP_CONFIG_MANDATORY TeeConfigFlagBits = 0b00000010 // reserved bit which should always be set SNP_CONFIG_MIGRATE_MA TeeConfigFlagBits = 0b00000100 // allowed to use a migration agent SNP_CONFIG_DEBUG TeeConfigFlagBits = 0b00001000 // allow debugging ) // TeeConfigFlagMinFW corresponds to a minimum version of the kernel+initrd // combination that should be booted. type TeeConfigMinFW struct { Major int `json:"major"` Minor int `json:"minor"` } ================================================ FILE: internal/mkcw/types/workload.go ================================================ package mkcwtypes import "github.com/containers/buildah/define" // WorkloadConfig is the data type which is encoded and stored in /krun-sev.json in a container // image, and included directly in the disk image. // https://github.com/containers/libkrun/blob/57c59dc5359bdeeb8260b3493e9f63d3708f9ab9/src/vmm/src/resources.rs#L57 type WorkloadConfig struct { Type define.TeeType `json:"tee"` TeeData string `json:"tee_data"` // Type == SEV: JSON-encoded SevWorkloadData, SNP: JSON-encoded SnpWorkloadData, others? WorkloadID string `json:"workload_id"` CPUs int `json:"cpus"` Memory int `json:"ram_mib"` AttestationURL string `json:"attestation_url"` } // SevWorkloadData contains the path to the SEV certificate chain and optionally, // the attestation server's public key(?) // https://github.com/containers/libkrun/blob/d31747aa92cf83df2abaeb87e2a83311c135d003/src/vmm/src/linux/tee/amdsev.rs#L222 type SevWorkloadData struct { VendorChain string `json:"vendor_chain"` AttestationServerPubkey string `json:"attestation_server_pubkey"` } // SnpWorkloadData contains the required CPU generation name. // https://github.com/virtee/oci2cw/blob/1502d5be33c2fa82d49aaa95781bbab2aa932781/examples/tee-config-snp.json type SnpWorkloadData struct { Generation string `json:"gen"` // "milan" (naples=1, rome=2, milan=3, genoa/bergamo/siena=4, turin=5) } //nolint:revive,staticcheck // Don't warn about bad naming. const ( // SEV_NO_ES is a known trusted execution environment type: AMD-SEV (secure encrypted virtualization without encrypted state, requires epyc 1000 "naples") SEV_NO_ES define.TeeType = "sev_no_es" ) ================================================ FILE: internal/mkcw/workload.go ================================================ package mkcw import ( "bytes" "encoding/binary" "encoding/json" "errors" "fmt" "io" "os" "github.com/containers/buildah/define" types "github.com/containers/buildah/internal/mkcw/types" ) type ( // WorkloadConfig is the data type which is encoded and stored in an image. WorkloadConfig = types.WorkloadConfig // SevWorkloadData is the type of data in WorkloadConfig.TeeData when the type is SEV. SevWorkloadData = types.SevWorkloadData // SnpWorkloadData is the type of data in WorkloadConfig.TeeData when the type is SNP. SnpWorkloadData = types.SnpWorkloadData // TeeType is one of the known types of trusted execution environments for which we // can generate suitable image contents. TeeType = define.TeeType ) const ( maxWorkloadConfigSize = 1024 * 1024 preferredPaddingBoundary = 4096 // krun looks for its configuration JSON directly in a disk image if the last twelve bytes // of the disk image are this magic value followed by a little-endian 64-bit // length-of-the-configuration krunMagic = "KRUN" ) //nolint:revive,staticcheck const ( // SEV is a known trusted execution environment type: AMD-SEV SEV = define.SEV // SEV_NO_ES is a known trusted execution environment type: AMD-SEV without encrypted state SEV_NO_ES = types.SEV_NO_ES // SNP is a known trusted execution environment type: AMD-SNP SNP = define.SNP ) // ReadWorkloadConfigFromImage reads the workload configuration from the // specified disk image file func ReadWorkloadConfigFromImage(path string) (WorkloadConfig, error) { // Read the last 12 bytes, which should be "KRUN" followed by a 64-bit // little-endian length. The (length) bytes immediately preceding // these hold the JSON-encoded workloadConfig. var wc WorkloadConfig f, err := os.Open(path) if err != nil { return wc, err } defer f.Close() // Read those last 12 bytes. finalTwelve := make([]byte, 12) if _, err = f.Seek(-12, io.SeekEnd); err != nil { return wc, fmt.Errorf("checking for workload config signature: %w", err) } if n, err := f.Read(finalTwelve); err != nil || n != len(finalTwelve) { if err != nil && !errors.Is(err, io.EOF) { return wc, fmt.Errorf("reading workload config signature (%d bytes read): %w", n, err) } if n != len(finalTwelve) { return wc, fmt.Errorf("short read (expected 12 bytes at the end of %q, got %d)", path, n) } } if magic := string(finalTwelve[0:4]); magic != "KRUN" { return wc, fmt.Errorf("expected magic string KRUN in %q, found %q)", path, magic) } length := binary.LittleEndian.Uint64(finalTwelve[4:]) if length > maxWorkloadConfigSize { return wc, fmt.Errorf("workload config in %q is %d bytes long, which seems unreasonable (max allowed %d)", path, length, maxWorkloadConfigSize) } // Read and decode the config. configBytes := make([]byte, length) if _, err = f.Seek(-(int64(length) + 12), io.SeekEnd); err != nil { return wc, fmt.Errorf("looking for workload config from disk image: %w", err) } if n, err := f.Read(configBytes); err != nil || n != len(configBytes) { if err != nil { return wc, fmt.Errorf("reading workload config from disk image: %w", err) } return wc, fmt.Errorf("short read (expected %d bytes near the end of %q, got %d)", len(configBytes), path, n) } err = json.Unmarshal(configBytes, &wc) if err != nil { err = fmt.Errorf("unmarshalling configuration %q: %w", string(configBytes), err) } return wc, err } // WriteWorkloadConfigToImage writes the workload configuration to the // specified disk image file, overwriting a previous configuration if it's // asked to and it finds one func WriteWorkloadConfigToImage(imageFile *os.File, workloadConfigBytes []byte, overwrite bool) error { // Read those last 12 bytes to check if there's a configuration there already, which we should overwrite. var overwriteOffset int64 if overwrite { finalTwelve := make([]byte, 12) if _, err := imageFile.Seek(-12, io.SeekEnd); err != nil { return fmt.Errorf("checking for workload config signature: %w", err) } if n, err := imageFile.Read(finalTwelve); err != nil || n != len(finalTwelve) { if err != nil && !errors.Is(err, io.EOF) { return fmt.Errorf("reading workload config signature (%d bytes read): %w", n, err) } if n != len(finalTwelve) { return fmt.Errorf("short read (expected 12 bytes at the end of %q, got %d)", imageFile.Name(), n) } } if magic := string(finalTwelve[0:4]); magic == "KRUN" { length := binary.LittleEndian.Uint64(finalTwelve[4:]) if length < maxWorkloadConfigSize { overwriteOffset = int64(length + 12) } } } // If we found a configuration in the file, try to figure out how much padding was used. paddingSize := int64(preferredPaddingBoundary) if overwriteOffset != 0 { st, err := imageFile.Stat() if err != nil { return err } for _, possiblePaddingLength := range []int64{0x100000, 0x10000, 0x1000, 0x200, 0x100} { if overwriteOffset > possiblePaddingLength { continue } if st.Size()%possiblePaddingLength != 0 { continue } if _, err := imageFile.Seek(-possiblePaddingLength, io.SeekEnd); err != nil { return fmt.Errorf("checking size of padding at end of file: %w", err) } buf := make([]byte, possiblePaddingLength) n, err := imageFile.Read(buf) if err != nil { return fmt.Errorf("reading possible padding at end of file: %w", err) } if n != len(buf) { return fmt.Errorf("short read checking size of padding at end of file: %d != %d", n, len(buf)) } if bytes.Equal(buf[:possiblePaddingLength-overwriteOffset], make([]byte, possiblePaddingLength-overwriteOffset)) { // everything up to the configuration was zero bytes, so it was padding overwriteOffset = possiblePaddingLength paddingSize = possiblePaddingLength break } } } // Append the krun configuration to a new buffer. var formatted bytes.Buffer nWritten, err := formatted.Write(workloadConfigBytes) if err != nil { return fmt.Errorf("building workload config: %w", err) } if nWritten != len(workloadConfigBytes) { return fmt.Errorf("short write appending configuration to buffer: %d != %d", nWritten, len(workloadConfigBytes)) } // Append the magic string to the buffer. nWritten, err = formatted.WriteString(krunMagic) if err != nil { return fmt.Errorf("building workload config signature: %w", err) } if nWritten != len(krunMagic) { return fmt.Errorf("short write appending krun magic to buffer: %d != %d", nWritten, len(krunMagic)) } // Append the 64-bit little-endian length of the workload configuration to the buffer. workloadConfigLengthBytes := make([]byte, 8) binary.LittleEndian.PutUint64(workloadConfigLengthBytes, uint64(len(workloadConfigBytes))) nWritten, err = formatted.Write(workloadConfigLengthBytes) if err != nil { return fmt.Errorf("building workload config signature size: %w", err) } if nWritten != len(workloadConfigLengthBytes) { return fmt.Errorf("short write appending configuration length to buffer: %d != %d", nWritten, len(workloadConfigLengthBytes)) } // Build a copy of that data, with padding preceding it. var padded bytes.Buffer if int64(formatted.Len())%paddingSize != 0 { extra := paddingSize - (int64(formatted.Len()) % paddingSize) nWritten, err := padded.Write(make([]byte, extra)) if err != nil { return fmt.Errorf("buffering padding: %w", err) } if int64(nWritten) != extra { return fmt.Errorf("short write buffering padding for disk image: %d != %d", nWritten, extra) } } extra := int64(formatted.Len()) nWritten, err = padded.Write(formatted.Bytes()) if err != nil { return fmt.Errorf("buffering workload config: %w", err) } if int64(nWritten) != extra { return fmt.Errorf("short write buffering workload config: %d != %d", nWritten, extra) } // Write the buffer to the file, starting with padding. if _, err = imageFile.Seek(-overwriteOffset, io.SeekEnd); err != nil { return fmt.Errorf("preparing to write workload config: %w", err) } nWritten, err = imageFile.Write(padded.Bytes()) if err != nil { return fmt.Errorf("writing workload config: %w", err) } if nWritten != padded.Len() { return fmt.Errorf("short write writing configuration to disk image: %d != %d", nWritten, padded.Len()) } offset, err := imageFile.Seek(0, io.SeekCurrent) if err != nil { return fmt.Errorf("preparing mark end of disk image: %w", err) } if err = imageFile.Truncate(offset); err != nil { return fmt.Errorf("marking end of disk image: %w", err) } return nil } ================================================ FILE: internal/mkcw/workload_test.go ================================================ package mkcw import ( "crypto/rand" "os" "path/filepath" "testing" "github.com/stretchr/testify/require" ) func TestReadWriteWorkloadConfig(t *testing.T) { t.Parallel() // Create a temporary file to stand in for a disk image. temp := filepath.Join(t.TempDir(), "disk.img") f, err := os.OpenFile(temp, os.O_CREATE|os.O_RDWR, 0o600) require.NoError(t, err) err = f.Truncate(0x1000000) require.NoError(t, err) defer f.Close() // Generate a random "encoded workload config". workloadConfig := make([]byte, 0x100) n, err := rand.Read(workloadConfig) require.NoError(t, err) require.Equal(t, len(workloadConfig), n) // Read the size of our temporary file. st, err := f.Stat() require.NoError(t, err) originalSize := st.Size() // Should get an error, since there's no workloadConfig in there to read. _, err = ReadWorkloadConfigFromImage(f.Name()) require.Error(t, err) // File should grow, even though we looked for an old config to overwrite. err = WriteWorkloadConfigToImage(f, workloadConfig, true) require.NoError(t, err) st, err = f.Stat() require.NoError(t, err) require.Greater(t, st.Size(), originalSize) originalSize = st.Size() // File shouldn't grow, even overwriting the config with a slightly larger one. err = WriteWorkloadConfigToImage(f, append([]byte("slightly longer"), workloadConfig...), true) require.NoError(t, err) st, err = f.Stat() require.NoError(t, err) require.Equal(t, originalSize, st.Size()) originalSize = st.Size() // File should grow if we're not trying to replace an old one config with a new one. err = WriteWorkloadConfigToImage(f, []byte("{\"comment\":\"quite a bit shorter\"}"), false) require.NoError(t, err) st, err = f.Stat() require.NoError(t, err) require.Greater(t, st.Size(), originalSize) // Should read successfully. _, err = ReadWorkloadConfigFromImage(f.Name()) require.NoError(t, err) } ================================================ FILE: internal/open/open.go ================================================ package open import ( "errors" "fmt" "syscall" ) // InChroot opens the file at `path` after chrooting to `root` and then // changing its working directory to `wd`. Both `wd` and `path` are evaluated // in the chroot. // Returns a file handle, an Errno value if there was an error and the // underlying error was a standard library error code, and a non-empty error if // one was detected. func InChroot(root, wd, path string, mode int, perm uint32) (fd int, errno syscall.Errno, err error) { requests := requests{ Root: root, Wd: wd, Open: []request{ { Path: path, Mode: mode, Perms: perm, }, }, } results := inChroot(requests) if len(results.Open) != 1 { return -1, 0, fmt.Errorf("got %d results back instead of 1", len(results.Open)) } if results.Open[0].Err != "" { if results.Open[0].Errno != 0 { err = fmt.Errorf("%s: %w", results.Open[0].Err, results.Open[0].Errno) } else { err = errors.New(results.Open[0].Err) } } return int(results.Open[0].Fd), results.Open[0].Errno, err } ================================================ FILE: internal/open/open_linux.go ================================================ package open import ( "bytes" "encoding/json" "errors" "fmt" "os" "strings" "github.com/sirupsen/logrus" "go.podman.io/storage/pkg/reexec" "golang.org/x/sys/unix" ) const ( bindFdToPathCommand = "buildah-bind-fd-to-path" ) func init() { reexec.Register(bindFdToPathCommand, bindFdToPathMain) } // BindFdToPath creates a bind mount from the open file (which is actually a // directory) to the specified location. If it succeeds, the caller will need // to unmount the targetPath when it's finished using it. Regardless, it // closes the passed-in descriptor. func BindFdToPath(fd uintptr, targetPath string) error { f := os.NewFile(fd, "passed-in directory descriptor") defer func() { if err := f.Close(); err != nil { logrus.Debugf("closing descriptor %d after attempting to bind to %q: %v", fd, targetPath, err) } }() pipeReader, pipeWriter, err := os.Pipe() if err != nil { return err } cmd := reexec.Command(bindFdToPathCommand) cmd.Stdin = pipeReader var stdout bytes.Buffer var stderr bytes.Buffer cmd.Stdout, cmd.Stderr = &stdout, &stderr cmd.ExtraFiles = append(cmd.ExtraFiles, f) err = cmd.Start() pipeReader.Close() if err != nil { pipeWriter.Close() return fmt.Errorf("starting child: %w", err) } encoder := json.NewEncoder(pipeWriter) if err := encoder.Encode(&targetPath); err != nil { return fmt.Errorf("sending target path to child: %w", err) } pipeWriter.Close() err = cmd.Wait() trimmedOutput := strings.TrimSpace(stdout.String()) + strings.TrimSpace(stderr.String()) if err != nil { if len(trimmedOutput) > 0 { err = fmt.Errorf("%s: %w", trimmedOutput, err) } } else { if len(trimmedOutput) > 0 { err = errors.New(trimmedOutput) } } return err } func bindFdToPathMain() { var targetPath string decoder := json.NewDecoder(os.Stdin) if err := decoder.Decode(&targetPath); err != nil { fmt.Fprintf(os.Stderr, "error decoding target path") os.Exit(1) } if err := unix.Fchdir(3); err != nil { fmt.Fprintf(os.Stderr, "fchdir(): %v", err) os.Exit(1) } if err := unix.Mount(".", targetPath, "bind", unix.MS_BIND, ""); err != nil { fmt.Fprintf(os.Stderr, "bind-mounting passed-in directory to %q: %v", targetPath, err) os.Exit(1) } os.Exit(0) } ================================================ FILE: internal/open/open_linux_test.go ================================================ package open import ( "os" "path/filepath" "testing" "github.com/stretchr/testify/require" "golang.org/x/sys/unix" ) func TestBindFdToPath(t *testing.T) { t.Parallel() first := t.TempDir() sampleData := []byte("sample data") err := os.WriteFile(filepath.Join(first, "testfile"), sampleData, 0o600) require.NoError(t, err, "writing sample data to first directory") fd, err := unix.Open(first, unix.O_DIRECTORY, 0) require.NoError(t, err, "opening descriptor for first directory") second := t.TempDir() err = BindFdToPath(uintptr(fd), second) require.NoError(t, err) t.Cleanup(func() { err := unix.Unmount(second, unix.MNT_DETACH) require.NoError(t, err, "unmounting as part of cleanup") }) readBack, err := os.ReadFile(filepath.Join(second, "testfile")) require.NoError(t, err) require.Equal(t, sampleData, readBack, "expected to read back data via the bind mount") } ================================================ FILE: internal/open/open_test.go ================================================ package open import ( "io" "os" "path/filepath" "testing" "github.com/stretchr/testify/require" "go.podman.io/storage/pkg/reexec" "golang.org/x/sys/unix" ) func TestMain(m *testing.M) { if reexec.Init() { return } os.Exit(m.Run()) } func TestOpenInChroot(t *testing.T) { t.Parallel() tmpdir := t.TempDir() firstContents := []byte{0, 1, 2, 3} secondContents := []byte{4, 5, 6, 7} require.NoErrorf(t, os.WriteFile(filepath.Join(tmpdir, "a"), firstContents, 0o644), "creating first test file") require.NoErrorf(t, os.MkdirAll(filepath.Join(tmpdir, tmpdir), 0o755), "creating test subdirectory") require.NoErrorf(t, os.WriteFile(filepath.Join(tmpdir, tmpdir, "a"), secondContents, 0o644), "creating second test file") result := inChroot(requests{ Open: []request{ { Path: filepath.Join(tmpdir, "a"), Mode: unix.O_RDONLY, }, }, }) require.Empty(t, result.Err, "result from first client") require.Equal(t, 1, len(result.Open), "results from first client") require.Empty(t, result.Open[0].Err, "first (only) result from first client") f := os.NewFile(result.Open[0].Fd, "file from first subprocess") contents, err := io.ReadAll(f) require.NoErrorf(t, err, "reading from file from first subprocess") require.Equalf(t, firstContents, contents, "contents of file from first subprocess") f.Close() result = inChroot(requests{ Root: tmpdir, Open: []request{ { Path: filepath.Join(tmpdir, "a"), Mode: unix.O_RDONLY, }, }, }) require.Empty(t, result.Err, "result from second client") require.Equal(t, 1, len(result.Open), "results from second client") require.Empty(t, result.Open[0].Err, "first (only) result from second client") f = os.NewFile(result.Open[0].Fd, "file from second subprocess") contents, err = io.ReadAll(f) require.NoErrorf(t, err, "reading from file from second subprocess") require.Equalf(t, secondContents, contents, "contents of file from second subprocess") f.Close() fd, errno, err := InChroot(tmpdir, "", filepath.Join(tmpdir, "a"), unix.O_RDONLY, 0) require.NoErrorf(t, err, "wrapper for opening just one item") require.Zero(t, errno, "errno from open file") f = os.NewFile(uintptr(fd), "file from third subprocess") require.NoErrorf(t, err, "reading from file from third subprocess") require.Equalf(t, secondContents, contents, "contents of file from third subprocess") f.Close() fd, errno, err = InChroot(tmpdir, "", filepath.Join(tmpdir, "b"), unix.O_RDONLY, 0) require.Errorf(t, err, "attempting to open a non-existent file") require.NotZero(t, errno, "attempting to open a non-existent file") require.Equal(t, -1, fd, "returned descriptor when open fails") f.Close() } ================================================ FILE: internal/open/open_types.go ================================================ package open import ( "syscall" ) type request struct { Path string Mode int Perms uint32 } type requests struct { Root string Wd string Open []request } type result struct { Fd uintptr // as returned by open() Err string // if err was not `nil`, err.Error() Errno syscall.Errno // if err was not `nil` and included a syscall.Errno, its value } type results struct { Err string Open []result } ================================================ FILE: internal/open/open_unix.go ================================================ //go:build linux || freebsd || darwin package open import ( "bytes" "encoding/json" "errors" "fmt" "os" "syscall" "go.podman.io/storage/pkg/reexec" "golang.org/x/sys/unix" ) const ( inChrootCommand = "buildah-open-in-chroot" ) func init() { reexec.Register(inChrootCommand, inChrootMain) } func inChroot(requests requests) results { sock, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0) if err != nil { return results{Err: fmt.Errorf("creating socket pair: %w", err).Error()} } parentSock := sock[0] childSock := sock[1] parentEnd := os.NewFile(uintptr(parentSock), "parent end of socket pair") childEnd := os.NewFile(uintptr(childSock), "child end of socket pair") cmd := reexec.Command(inChrootCommand) cmd.ExtraFiles = append(cmd.ExtraFiles, childEnd) err = cmd.Start() childEnd.Close() defer parentEnd.Close() if err != nil { return results{Err: err.Error()} } encoder := json.NewEncoder(parentEnd) if err := encoder.Encode(&requests); err != nil { return results{Err: fmt.Errorf("sending request down socket: %w", err).Error()} } if err := unix.Shutdown(parentSock, unix.SHUT_WR); err != nil { return results{Err: fmt.Errorf("finishing sending request down socket: %w", err).Error()} } b := make([]byte, 65536) oob := make([]byte, 65536) n, oobn, _, _, err := unix.Recvmsg(parentSock, b, oob, 0) if err != nil { return results{Err: fmt.Errorf("receiving message: %w", err).Error()} } if err := unix.Shutdown(parentSock, unix.SHUT_RD); err != nil { return results{Err: fmt.Errorf("finishing socket: %w", err).Error()} } if n > len(b) { return results{Err: fmt.Errorf("too much regular data: %d > %d", n, len(b)).Error()} } if oobn > len(oob) { return results{Err: fmt.Errorf("too much OOB data: %d > %d", oobn, len(oob)).Error()} } scms, err := unix.ParseSocketControlMessage(oob[:oobn]) if err != nil { return results{Err: fmt.Errorf("parsing control message: %w", err).Error()} } var receivedFds []int for i := range scms { fds, err := unix.ParseUnixRights(&scms[i]) if err != nil { return results{Err: fmt.Errorf("parsing rights message %d: %w", i, err).Error()} } receivedFds = append(receivedFds, fds...) } decoder := json.NewDecoder(bytes.NewReader(b[:n])) var result results if err := decoder.Decode(&result); err != nil { return results{Err: fmt.Errorf("decoding results: %w", err).Error()} } j := 0 for i := range result.Open { if result.Open[i].Err == "" { if j >= len(receivedFds) { for _, fd := range receivedFds { unix.Close(fd) } return results{Err: fmt.Errorf("didn't receive enough FDs").Error()} } result.Open[i].Fd = uintptr(receivedFds[j]) j++ } } return result } func inChrootMain() { var theseRequests requests var theseResults results sockFd := 3 sock := os.NewFile(uintptr(sockFd), "socket connection to parent process") defer sock.Close() encoder := json.NewEncoder(sock) decoder := json.NewDecoder(sock) if err := decoder.Decode(&theseRequests); err != nil { if err := encoder.Encode(results{Err: fmt.Errorf("decoding request: %w", err).Error()}); err != nil { os.Exit(1) } } if theseRequests.Root != "" { if err := os.Chdir(theseRequests.Root); err != nil { if err := encoder.Encode(results{Err: fmt.Errorf("changing to %q: %w", theseRequests.Root, err).Error()}); err != nil { os.Exit(1) } os.Exit(1) } if err := unix.Chroot(theseRequests.Root); err != nil { if err := encoder.Encode(results{Err: fmt.Errorf("chrooting to %q: %w", theseRequests.Root, err).Error()}); err != nil { os.Exit(1) } os.Exit(1) } if err := os.Chdir("/"); err != nil { if err := encoder.Encode(results{Err: fmt.Errorf("changing to new root: %w", err).Error()}); err != nil { os.Exit(1) } os.Exit(1) } } if theseRequests.Wd != "" { if err := os.Chdir(theseRequests.Wd); err != nil { if err := encoder.Encode(results{Err: fmt.Errorf("changing to %q in chroot: %w", theseRequests.Wd, err).Error()}); err != nil { os.Exit(1) } os.Exit(1) } } var fds []int for _, request := range theseRequests.Open { fd, err := unix.Open(request.Path, request.Mode, request.Perms) thisResult := result{Fd: uintptr(fd)} if err == nil { fds = append(fds, fd) } else { var errno syscall.Errno thisResult.Err = err.Error() if errors.As(err, &errno) { thisResult.Errno = errno } } theseResults.Open = append(theseResults.Open, thisResult) } rights := unix.UnixRights(fds...) inband, err := json.Marshal(&theseResults) if err != nil { if err := encoder.Encode(results{Err: fmt.Errorf("sending response: %w", err).Error()}); err != nil { os.Exit(1) } os.Exit(1) } if err := unix.Sendmsg(sockFd, inband, rights, nil, 0); err != nil { if err := encoder.Encode(results{Err: fmt.Errorf("sending response: %w", err).Error()}); err != nil { os.Exit(1) } os.Exit(1) } os.Exit(0) } ================================================ FILE: internal/open/open_unsupported.go ================================================ //go:build !linux && !freebsd && !darwin package open func inChroot(requests requests) results { return results{Err: "open-in-chroot not available on this platform"} } ================================================ FILE: internal/output/build_output.go ================================================ package output import ( "fmt" "strings" ) type BuildOutputType int const ( BuildOutputInvalid BuildOutputType = 0 BuildOutputStdout BuildOutputType = 1 // stream tar to stdout BuildOutputLocalDir BuildOutputType = 2 BuildOutputTar BuildOutputType = 3 ) // BuildOutputOptions contains the the outcome of parsing the value of a build --output flag type BuildOutputOption struct { Type BuildOutputType Path string // Only valid if Type is local dir or tar } // GetBuildOutput is responsible for parsing custom build output argument i.e `build --output` flag. // Takes `buildOutput` as string and returns BuildOutputOption func GetBuildOutput(buildOutput string) (BuildOutputOption, error) { // Support simple values, in the form --output ./mydir if !strings.Contains(buildOutput, ",") && !strings.Contains(buildOutput, "=") { if buildOutput == "-" { // Feature parity with buildkit, output tar to stdout // Read more here: https://docs.docker.com/engine/reference/commandline/build/#custom-build-outputs return BuildOutputOption{ Type: BuildOutputStdout, Path: "", }, nil } return BuildOutputOption{ Type: BuildOutputLocalDir, Path: buildOutput, }, nil } // Support complex values, in the form --output type=local,dest=./mydir typeSelected := BuildOutputInvalid pathSelected := "" for option := range strings.SplitSeq(buildOutput, ",") { key, value, found := strings.Cut(option, "=") if !found { return BuildOutputOption{}, fmt.Errorf("invalid build output options %q, expected format key=value", buildOutput) } switch key { case "type": if typeSelected != BuildOutputInvalid { return BuildOutputOption{}, fmt.Errorf("duplicate %q not supported", key) } switch value { case "local": typeSelected = BuildOutputLocalDir case "tar": typeSelected = BuildOutputTar default: return BuildOutputOption{}, fmt.Errorf("invalid type %q selected for build output options %q", value, buildOutput) } case "dest": if pathSelected != "" { return BuildOutputOption{}, fmt.Errorf("duplicate %q not supported", key) } pathSelected = value default: return BuildOutputOption{}, fmt.Errorf("unrecognized key %q in build output option: %q", key, buildOutput) } } // Validate there is a type if typeSelected == BuildOutputInvalid { return BuildOutputOption{}, fmt.Errorf("missing required key %q in build output option: %q", "type", buildOutput) } // Validate path if typeSelected == BuildOutputLocalDir || typeSelected == BuildOutputTar { if pathSelected == "" { return BuildOutputOption{}, fmt.Errorf("missing required key %q in build output option: %q", "dest", buildOutput) } } else { // Clear path when not needed by type pathSelected = "" } // Handle redirecting stdout for tar output if pathSelected == "-" { if typeSelected == BuildOutputTar { typeSelected = BuildOutputStdout pathSelected = "" } else { return BuildOutputOption{}, fmt.Errorf(`invalid build output option %q, only "type=tar" can be used with "dest=-"`, buildOutput) } } return BuildOutputOption{ Type: typeSelected, Path: pathSelected, }, nil } ================================================ FILE: internal/output/build_output_test.go ================================================ package output import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestGetBuildOutput(t *testing.T) { testCases := []struct { description string input string output BuildOutputOption }{ { description: "hyphen", input: "-", output: BuildOutputOption{ Type: BuildOutputStdout, }, }, { description: "just-a-path", input: "/tmp", output: BuildOutputOption{ Type: BuildOutputLocalDir, Path: "/tmp", }, }, { description: "normal-path", input: "type=local,dest=/tmp", output: BuildOutputOption{ Type: BuildOutputLocalDir, Path: "/tmp", }, }, } for _, testCase := range testCases { t.Run(testCase.description, func(t *testing.T) { result, err := GetBuildOutput(testCase.input) require.NoErrorf(t, err, "expected to be able to parse %q", testCase.input) assert.Equal(t, testCase.output, result) }) } } ================================================ FILE: internal/parsevolume/parse.go ================================================ package parsevolume import ( "fmt" "path/filepath" "strings" specs "github.com/opencontainers/runtime-spec/specs-go" "go.podman.io/common/pkg/parse" "go.podman.io/storage/pkg/fileutils" ) // ValidateVolumeMountHostDir validates the host path of buildah --volume func ValidateVolumeMountHostDir(hostDir string) error { if !filepath.IsAbs(hostDir) { return fmt.Errorf("invalid host path, must be an absolute path %q", hostDir) } if err := fileutils.Exists(hostDir); err != nil { return err } return nil } // revertEscapedColon converts "\:" to ":" func revertEscapedColon(source string) string { return strings.ReplaceAll(source, "\\:", ":") } // SplitStringWithColonEscape splits string into slice by colon. Backslash-escaped colon (i.e. "\:") will not be regarded as separator func SplitStringWithColonEscape(str string) []string { result := make([]string, 0, 3) sb := &strings.Builder{} for idx, r := range str { if r == ':' { // the colon is backslash-escaped if idx-1 > 0 && str[idx-1] == '\\' { sb.WriteRune(r) } else { // os.Stat will fail if path contains escaped colon result = append(result, revertEscapedColon(sb.String())) sb.Reset() } } else { sb.WriteRune(r) } } if sb.Len() > 0 { result = append(result, revertEscapedColon(sb.String())) } return result } // Volume parses the input of --volume func Volume(volume string) (specs.Mount, error) { mount := specs.Mount{} arr := SplitStringWithColonEscape(volume) if len(arr) < 2 { return mount, fmt.Errorf("incorrect volume format %q, should be host-dir:ctr-dir[:option]", volume) } if err := ValidateVolumeMountHostDir(arr[0]); err != nil { return mount, err } if err := parse.ValidateVolumeCtrDir(arr[1]); err != nil { return mount, err } mountOptions := "" if len(arr) > 2 { mountOptions = arr[2] if _, err := parse.ValidateVolumeOpts(strings.Split(arr[2], ",")); err != nil { return mount, err } } mountOpts := strings.Split(mountOptions, ",") mount.Source = arr[0] mount.Destination = arr[1] mount.Type = "rbind" mount.Options = mountOpts return mount, nil } ================================================ FILE: internal/pty/pty_posix.go ================================================ //go:build freebsd && cgo package pty // #include // #include import "C" import ( "github.com/sirupsen/logrus" "golang.org/x/sys/unix" ) func openpt() (int, error) { fd, err := C.posix_openpt(C.O_RDWR) if err != nil { return -1, err } if _, err := C.grantpt(fd); err != nil { return -1, err } return int(fd), nil } func ptsname(fd int) (string, error) { path, err := C.ptsname(C.int(fd)) if err != nil { return "", err } return C.GoString(path), nil } func unlockpt(fd int) error { if _, err := C.unlockpt(C.int(fd)); err != nil { return err } return nil } // GetPtyDescriptors allocates a new pseudoterminal and returns the control and // pseudoterminal file descriptors. func GetPtyDescriptors() (int, int, error) { // Create a pseudo-terminal and open the control side controlFd, err := openpt() if err != nil { logrus.Errorf("error opening PTY control side using posix_openpt: %v", err) return -1, -1, err } if err = unlockpt(controlFd); err != nil { logrus.Errorf("error unlocking PTY: %v", err) return -1, -1, err } // Get a handle for the other end. ptyName, err := ptsname(controlFd) if err != nil { logrus.Errorf("error getting PTY name: %v", err) return -1, -1, err } ptyFd, err := unix.Open(ptyName, unix.O_RDWR, 0) if err != nil { logrus.Errorf("error opening PTY: %v", err) return -1, -1, err } return controlFd, ptyFd, nil } ================================================ FILE: internal/pty/pty_ptmx.go ================================================ //go:build linux package pty import ( "fmt" "os" "syscall" "unsafe" "golang.org/x/sys/unix" ) // GetPtyDescriptors allocates a new pseudoterminal and returns the control and // pseudoterminal file descriptors. This implementation uses the /dev/ptmx // device. The main advantage of using this instead of posix_openpt is that it // avoids cgo. func GetPtyDescriptors() (int, int, error) { // Create a pseudo-terminal -- open a copy of the master side. controlFd, err := unix.Open("/dev/ptmx", os.O_RDWR, 0o600) if err != nil { return -1, -1, fmt.Errorf("opening PTY master using /dev/ptmx: %v", err) } // Set the kernel's lock to "unlocked". locked := 0 if result, _, err := unix.Syscall(unix.SYS_IOCTL, uintptr(controlFd), unix.TIOCSPTLCK, uintptr(unsafe.Pointer(&locked))); int(result) == -1 { return -1, -1, fmt.Errorf("unlocking PTY descriptor: %v", err) } // Get a handle for the other end. ptyFd, _, err := unix.Syscall(unix.SYS_IOCTL, uintptr(controlFd), unix.TIOCGPTPEER, unix.O_RDWR|unix.O_NOCTTY) if int(ptyFd) == -1 { if errno, isErrno := err.(syscall.Errno); !isErrno || (errno != syscall.EINVAL && errno != syscall.ENOTTY) { return -1, -1, fmt.Errorf("getting PTY descriptor: %v", err) } // EINVAL means the kernel's too old to understand TIOCGPTPEER. Try TIOCGPTN. ptyN, err := unix.IoctlGetInt(controlFd, unix.TIOCGPTN) if err != nil { return -1, -1, fmt.Errorf("getting PTY number: %v", err) } ptyName := fmt.Sprintf("/dev/pts/%d", ptyN) fd, err := unix.Open(ptyName, unix.O_RDWR|unix.O_NOCTTY, 0o620) if err != nil { return -1, -1, fmt.Errorf("opening PTY %q: %v", ptyName, err) } ptyFd = uintptr(fd) } return controlFd, int(ptyFd), nil } ================================================ FILE: internal/pty/pty_unsupported.go ================================================ //go:build !linux && !(freebsd && cgo) package pty import ( "errors" ) // GetPtyDescriptors would allocate a new pseudoterminal and return the control and // pseudoterminal file descriptors, if only it could. func GetPtyDescriptors() (int, int, error) { return -1, -1, errors.New("GetPtyDescriptors not supported on this platform") } ================================================ FILE: internal/rpc/listen/listen.go ================================================ package listen import ( "errors" "fmt" "net" "os" "path/filepath" "github.com/containers/buildah/internal/tmpdir" ) func Listen(location string) (net.Listener, func() error, error) { cleanup := func() error { return nil } if location == "" { newParentDir, err := os.MkdirTemp(tmpdir.GetTempDir(), "buildah-socket") if err != nil { return nil, nil, fmt.Errorf("creating a temporary directory to hold a listening socket: %w", err) } location = filepath.Join(newParentDir, "build.sock") cleanup = func() error { return os.RemoveAll(newParentDir) } } l, err := net.ListenUnix("unix", &net.UnixAddr{Net: "unix", Name: location}) if err != nil { cerr := cleanup() return nil, nil, errors.Join(err, cerr) } closeAndCleanup := func() error { lerr := l.Close() if lerr != nil && errors.Is(lerr, net.ErrClosed) { // if the listening socket was used by an rpc server that was explicitly // stopped, the rpc server will have closed the socket lerr = nil } cerr := cleanup() return errors.Join(lerr, cerr) } return l, closeAndCleanup, nil } ================================================ FILE: internal/rpc/noop/noop.go ================================================ package noop import ( "context" "syscall" "github.com/containers/buildah/internal/rpc/noop/pb" "google.golang.org/grpc" ) type noopServer struct { pb.UnimplementedNoopServer } func (n *noopServer) Noop(_ context.Context, req *pb.NoopRequest) (*pb.NoopResponse, error) { if req == nil { return nil, syscall.EINVAL } resp := &pb.NoopResponse{} resp.Ignored = req.Ignored return resp, nil } func Register(s grpc.ServiceRegistrar) { pb.RegisterNoopServer(s, &noopServer{}) } ================================================ FILE: internal/rpc/noop/pb/build.sh ================================================ #!/bin/bash set -e cd $(dirname ${BASH_SOURCE[0]}) TOP=../../../.. PATH=${TOP}/tests/tools/build:${PATH} set -x for proto in *.proto ; do protoc \ --go_opt=paths=source_relative --go_out . \ --go-grpc_opt=paths=source_relative --go-grpc_out . \ ${proto} done ================================================ FILE: internal/rpc/noop/pb/noop.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10-devel // protoc v3.19.6 // source: noop.proto package pb import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) type NoopRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Ignored string `protobuf:"bytes,1,opt,name=ignored,proto3" json:"ignored,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *NoopRequest) Reset() { *x = NoopRequest{} mi := &file_noop_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *NoopRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*NoopRequest) ProtoMessage() {} func (x *NoopRequest) ProtoReflect() protoreflect.Message { mi := &file_noop_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use NoopRequest.ProtoReflect.Descriptor instead. func (*NoopRequest) Descriptor() ([]byte, []int) { return file_noop_proto_rawDescGZIP(), []int{0} } func (x *NoopRequest) GetIgnored() string { if x != nil { return x.Ignored } return "" } type NoopResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Ignored string `protobuf:"bytes,1,opt,name=ignored,proto3" json:"ignored,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *NoopResponse) Reset() { *x = NoopResponse{} mi := &file_noop_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *NoopResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*NoopResponse) ProtoMessage() {} func (x *NoopResponse) ProtoReflect() protoreflect.Message { mi := &file_noop_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use NoopResponse.ProtoReflect.Descriptor instead. func (*NoopResponse) Descriptor() ([]byte, []int) { return file_noop_proto_rawDescGZIP(), []int{1} } func (x *NoopResponse) GetIgnored() string { if x != nil { return x.Ignored } return "" } var File_noop_proto protoreflect.FileDescriptor const file_noop_proto_rawDesc = "" + "\n" + "\n" + "noop.proto\x12\rio.buildah.v1\"'\n" + "\vNoopRequest\x12\x18\n" + "\aignored\x18\x01 \x01(\tR\aignored\"(\n" + "\fNoopResponse\x12\x18\n" + "\aignored\x18\x01 \x01(\tR\aignored2G\n" + "\x04Noop\x12?\n" + "\x04Noop\x12\x1a.io.buildah.v1.NoopRequest\x1a\x1b.io.buildah.v1.NoopResponseB4Z2github.com/containers/buildah/internal/rpc/noop/pbb\x06proto3" var ( file_noop_proto_rawDescOnce sync.Once file_noop_proto_rawDescData []byte ) func file_noop_proto_rawDescGZIP() []byte { file_noop_proto_rawDescOnce.Do(func() { file_noop_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_noop_proto_rawDesc), len(file_noop_proto_rawDesc))) }) return file_noop_proto_rawDescData } var file_noop_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_noop_proto_goTypes = []any{ (*NoopRequest)(nil), // 0: io.buildah.v1.NoopRequest (*NoopResponse)(nil), // 1: io.buildah.v1.NoopResponse } var file_noop_proto_depIdxs = []int32{ 0, // 0: io.buildah.v1.Noop.Noop:input_type -> io.buildah.v1.NoopRequest 1, // 1: io.buildah.v1.Noop.Noop:output_type -> io.buildah.v1.NoopResponse 1, // [1:2] is the sub-list for method output_type 0, // [0:1] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name } func init() { file_noop_proto_init() } func file_noop_proto_init() { if File_noop_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_noop_proto_rawDesc), len(file_noop_proto_rawDesc)), NumEnums: 0, NumMessages: 2, NumExtensions: 0, NumServices: 1, }, GoTypes: file_noop_proto_goTypes, DependencyIndexes: file_noop_proto_depIdxs, MessageInfos: file_noop_proto_msgTypes, }.Build() File_noop_proto = out.File file_noop_proto_goTypes = nil file_noop_proto_depIdxs = nil } ================================================ FILE: internal/rpc/noop/pb/noop.proto ================================================ syntax = "proto3"; package io.buildah.v1; option go_package = "github.com/containers/buildah/internal/rpc/noop/pb"; service Noop { rpc Noop(NoopRequest) returns (NoopResponse); } message NoopRequest { string ignored = 1; } message NoopResponse { string ignored = 1; } ================================================ FILE: internal/rpc/noop/pb/noop_grpc.pb.go ================================================ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.0 // - protoc v3.19.6 // source: noop.proto package pb import ( context "context" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" ) // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. // Requires gRPC-Go v1.64.0 or later. const _ = grpc.SupportPackageIsVersion9 const ( Noop_Noop_FullMethodName = "/io.buildah.v1.Noop/Noop" ) // NoopClient is the client API for Noop service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type NoopClient interface { Noop(ctx context.Context, in *NoopRequest, opts ...grpc.CallOption) (*NoopResponse, error) } type noopClient struct { cc grpc.ClientConnInterface } func NewNoopClient(cc grpc.ClientConnInterface) NoopClient { return &noopClient{cc} } func (c *noopClient) Noop(ctx context.Context, in *NoopRequest, opts ...grpc.CallOption) (*NoopResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(NoopResponse) err := c.cc.Invoke(ctx, Noop_Noop_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } // NoopServer is the server API for Noop service. // All implementations must embed UnimplementedNoopServer // for forward compatibility. type NoopServer interface { Noop(context.Context, *NoopRequest) (*NoopResponse, error) mustEmbedUnimplementedNoopServer() } // UnimplementedNoopServer must be embedded to have // forward compatible implementations. // // NOTE: this should be embedded by value instead of pointer to avoid a nil // pointer dereference when methods are called. type UnimplementedNoopServer struct{} func (UnimplementedNoopServer) Noop(context.Context, *NoopRequest) (*NoopResponse, error) { return nil, status.Error(codes.Unimplemented, "method Noop not implemented") } func (UnimplementedNoopServer) mustEmbedUnimplementedNoopServer() {} func (UnimplementedNoopServer) testEmbeddedByValue() {} // UnsafeNoopServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to NoopServer will // result in compilation errors. type UnsafeNoopServer interface { mustEmbedUnimplementedNoopServer() } func RegisterNoopServer(s grpc.ServiceRegistrar, srv NoopServer) { // If the following call panics, it indicates UnimplementedNoopServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { t.testEmbeddedByValue() } s.RegisterService(&Noop_ServiceDesc, srv) } func _Noop_Noop_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(NoopRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(NoopServer).Noop(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: Noop_Noop_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(NoopServer).Noop(ctx, req.(*NoopRequest)) } return interceptor(ctx, in, info, handler) } // Noop_ServiceDesc is the grpc.ServiceDesc for Noop service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) var Noop_ServiceDesc = grpc.ServiceDesc{ ServiceName: "io.buildah.v1.Noop", HandlerType: (*NoopServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "Noop", Handler: _Noop_Noop_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "noop.proto", } ================================================ FILE: internal/sanitize/sanitize.go ================================================ package sanitize import ( "archive/tar" "errors" "fmt" "io" "os" "path" "path/filepath" "strings" "github.com/sirupsen/logrus" directoryTransport "go.podman.io/image/v5/directory" dockerTransport "go.podman.io/image/v5/docker" dockerArchiveTransport "go.podman.io/image/v5/docker/archive" ociArchiveTransport "go.podman.io/image/v5/oci/archive" ociLayoutTransport "go.podman.io/image/v5/oci/layout" openshiftTransport "go.podman.io/image/v5/openshift" "go.podman.io/image/v5/pkg/compression" "go.podman.io/storage/pkg/archive" "go.podman.io/storage/pkg/chrootarchive" ) // create a temporary file to use as a destination archive func newArchiveDestination(tmpdir string) (tw *tar.Writer, f *os.File, err error) { if f, err = os.CreateTemp(tmpdir, "buildah-archive-"); err != nil { return nil, nil, fmt.Errorf("creating temporary copy of base image: %w", err) } tw = tar.NewWriter(f) return tw, f, nil } // create a temporary directory to use as a new OCI layout or "image in a directory" image func newDirectoryDestination(tmpdir string) (string, error) { newDirectory, err := os.MkdirTemp(tmpdir, "buildah-layout-") if err != nil { return "", fmt.Errorf("creating temporary copy of base image: %w", err) } return newDirectory, nil } // create an archive containing a single item from the build context func newSingleItemArchive(contextDir, archiveSource string) (io.ReadCloser, error) { for { // try to make sure the archiver doesn't get thrown by relative prefixes if strings.HasPrefix(archiveSource, "/") && archiveSource != "/" { archiveSource = strings.TrimPrefix(archiveSource, "/") continue } else if strings.HasPrefix(archiveSource, "./") && archiveSource != "./" { archiveSource = strings.TrimPrefix(archiveSource, "./") continue } break } // grab only that one file, ignore anything and everything else tarOptions := &archive.TarOptions{ IncludeFiles: []string{path.Clean(archiveSource)}, ExcludePatterns: []string{"**"}, } return chrootarchive.Tar(contextDir, tarOptions, contextDir) } // Write this header/content combination to a tar writer, making sure that it // doesn't include any symbolic links that point to something which hasn't // already been seen in this archive. Overwrites the contents of `*hdr`. func writeToArchive(tw *tar.Writer, hdr *tar.Header, content io.Reader, seenEntries map[string]struct{}, convertSymlinksToHardlinks bool) error { // write to the archive writer hdr.Name = path.Clean("/" + hdr.Name) if hdr.Name != "/" { hdr.Name = strings.TrimPrefix(hdr.Name, "/") } seenEntries[hdr.Name] = struct{}{} switch hdr.Typeflag { case tar.TypeDir, tar.TypeReg, tar.TypeLink: // all good case tar.TypeSymlink: // resolve the target of the symlink linkname := hdr.Linkname if !path.IsAbs(linkname) { linkname = path.Join("/"+path.Dir(hdr.Name), linkname) } linkname = path.Clean(linkname) if linkname != "/" { linkname = strings.TrimPrefix(linkname, "/") } if _, validTarget := seenEntries[linkname]; !validTarget { return fmt.Errorf("invalid symbolic link from %q to %q (%q) in archive", hdr.Name, hdr.Linkname, linkname) } rel, err := filepath.Rel(filepath.FromSlash(path.Dir("/"+hdr.Name)), filepath.FromSlash("/"+linkname)) if err != nil { return fmt.Errorf("computing relative path from %q to %q in archive", hdr.Name, linkname) } rel = filepath.ToSlash(rel) if convertSymlinksToHardlinks { // rewrite as a hard link for oci-archive, which gets // extracted into a temporary directory to be read, but // not for docker-archive, which is read directly from // the unextracted archive file, in a way which doesn't // understand hard links hdr.Typeflag = tar.TypeLink hdr.Linkname = linkname } else { // ensure it's a relative symlink inside of the tree // for docker-archive hdr.Linkname = rel } default: return fmt.Errorf("rewriting archive of base image: disallowed entry type %c", hdr.Typeflag) } if err := tw.WriteHeader(hdr); err != nil { return fmt.Errorf("rewriting archive of base image: %w", err) } if hdr.Typeflag == tar.TypeReg { n, err := io.Copy(tw, content) if err != nil { return fmt.Errorf("copying content for %q in base image: %w", hdr.Name, err) } if n != hdr.Size { return fmt.Errorf("short write writing %q in base image: %d != %d", hdr.Name, n, hdr.Size) } } return nil } // write this header and possible content to a directory tree func writeToDirectory(root string, hdr *tar.Header, content io.Reader) error { var err error // write this item directly to a directory tree. the reader won't care // much about permissions or datestamps, so don't bother setting them hdr.Name = path.Clean("/" + hdr.Name) newName := filepath.Join(root, filepath.FromSlash(hdr.Name)) switch hdr.Typeflag { case tar.TypeDir: err = os.Mkdir(newName, 0o700) case tar.TypeReg: err = func() error { var f *os.File f, err := os.OpenFile(newName, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600) if err != nil { return fmt.Errorf("copying content for %q in base image: %w", hdr.Name, err) } n, err := io.Copy(f, content) if err != nil { f.Close() return fmt.Errorf("copying content for %q in base image: %w", hdr.Name, err) } if n != hdr.Size { f.Close() return fmt.Errorf("short write writing %q in base image: %d != %d", hdr.Name, n, hdr.Size) } return f.Close() }() case tar.TypeLink: linkName := path.Clean("/" + hdr.Linkname) oldName := filepath.Join(root, filepath.FromSlash(linkName)) err = os.Link(oldName, newName) case tar.TypeSymlink: // convert it to a hard link or absolute symlink var oldName string if !path.IsAbs(path.Clean("/" + hdr.Linkname)) { linkName := path.Join("/"+path.Dir(hdr.Name), hdr.Linkname) oldName = filepath.Join(root, filepath.FromSlash(linkName)) } else { oldName = filepath.Join(root, filepath.FromSlash(path.Clean("/"+hdr.Linkname))) } err = os.Link(oldName, newName) if err != nil && errors.Is(err, os.ErrPermission) { // EPERM could mean it's a directory if oldInfo, err2 := os.Stat(oldName); err2 == nil && oldInfo.IsDir() { err = os.Symlink(oldName, newName) } } default: return fmt.Errorf("extracting archive of base image: disallowed entry type %c", hdr.Typeflag) } if err != nil { return fmt.Errorf("creating %q: %w", newName, err) } return nil } // ImageName limits which image transports we'll accept. For those it accepts // which refer to filesystem objects, where relative path names are evaluated // relative to "contextDir", it will create a copy of the original image, under // "tmpdir", which contains no symbolic links. It it returns a parseable // reference to the image which should be used. func ImageName(transportName, restOfImageName, contextDir, tmpdir string) (newFrom string, err error) { seenEntries := make(map[string]struct{}) // we're going to try to create a temporary directory or file, but if // we fail, make sure that they get removed immediately newImageDestination := "" succeeded := false defer func() { if !succeeded && newImageDestination != "" { if err := os.RemoveAll(newImageDestination); err != nil { logrus.Warnf("removing temporary copy of base image in %q: %v", newImageDestination, err) } } }() // create an archive of the base image, whatever kind it is, chrooting into // the build context directory while doing so, to be sure that it can't // be tricked into including anything from outside of the context directory isEmbeddedArchive := false var f *os.File var tw *tar.Writer var archiveSource string var imageArchive io.ReadCloser switch transportName { case dockerTransport.Transport.Name(), "docker-daemon", openshiftTransport.Transport.Name(): // ok, these are all remote return transportName + ":" + restOfImageName, nil case dockerArchiveTransport.Transport.Name(), ociArchiveTransport.Transport.Name(): // these are, basically, tarballs // these take the form path[:stuff] transportRef := restOfImageName archiveSource, refLeftover, ok := strings.Cut(transportRef, ":") if ok { refLeftover = ":" + refLeftover } // create a temporary file to use as our new archive tw, f, err = newArchiveDestination(tmpdir) if err != nil { return "", fmt.Errorf("creating temporary copy of base image: %w", err) } newImageDestination = f.Name() defer func() { if tw != nil { if err := tw.Close(); err != nil { logrus.Warnf("wrapping up writing copy of base image to archive %q: %v", newImageDestination, err) } } if f != nil { if err := f.Close(); err != nil { logrus.Warnf("closing copy of base image in archive file %q: %v", newImageDestination, err) } } }() // archive only the archive file for copying to the new archive file imageArchive, err = newSingleItemArchive(contextDir, archiveSource) isEmbeddedArchive = true // generate the new reference using the temporary file's name newFrom = transportName + ":" + newImageDestination + refLeftover case ociLayoutTransport.Transport.Name(): // this is a directory tree // this takes the form path[:stuff] transportRef := restOfImageName archiveSource, refLeftover, ok := strings.Cut(transportRef, ":") if ok { refLeftover = ":" + refLeftover } // create a new directory to use as our new layout directory if newImageDestination, err = newDirectoryDestination(tmpdir); err != nil { return "", fmt.Errorf("creating temporary copy of base image: %w", err) } // archive the entire layout directory for copying to the new layout directory tarOptions := &archive.TarOptions{} imageArchive, err = chrootarchive.Tar(filepath.Join(contextDir, archiveSource), tarOptions, contextDir) // generate the new reference using the directory newFrom = transportName + ":" + newImageDestination + refLeftover case directoryTransport.Transport.Name(): // this is also a directory tree // this takes the form of just a path transportRef := restOfImageName // create a new directory to use as our new image directory if newImageDestination, err = newDirectoryDestination(tmpdir); err != nil { return "", fmt.Errorf("creating temporary copy of base image: %w", err) } // archive the entire directory for copying to the new directory archiveSource = transportRef tarOptions := &archive.TarOptions{} imageArchive, err = chrootarchive.Tar(filepath.Join(contextDir, archiveSource), tarOptions, contextDir) // generate the new reference using the directory newFrom = transportName + ":" + newImageDestination default: return "", fmt.Errorf("unexpected container image transport %q", transportName) } if err != nil { return "", fmt.Errorf("error archiving source at %q under %q", archiveSource, contextDir) } // start reading the archived content defer func() { if err := imageArchive.Close(); err != nil { logrus.Warn(err) } }() tr := tar.NewReader(imageArchive) hdr, err := tr.Next() for err == nil { // if the archive we're parsing is expected to have an archive as its only item, // assume it's the first (and hopefully, only) item, and switch to stepping through // it as the archive if isEmbeddedArchive { if hdr.Typeflag != tar.TypeReg { return "", fmt.Errorf("internal error passing archive contents: embedded archive type was %c instead of %c", hdr.Typeflag, tar.TypeReg) } decompressed, _, decompressErr := compression.AutoDecompress(tr) if decompressErr != nil { return "", fmt.Errorf("error decompressing-if-necessary archive %q: %w", archiveSource, decompressErr) } defer func() { if err := decompressed.Close(); err != nil { logrus.Warn(err) } }() tr = tar.NewReader(decompressed) hdr, err = tr.Next() isEmbeddedArchive = false continue } // write this item from the source archive to either the new archive or the new // directory, which ever one we're doing var writeError error if tw != nil { writeError = writeToArchive(tw, hdr, io.LimitReader(tr, hdr.Size), seenEntries, transportName == ociArchiveTransport.Transport.Name()) } else { writeError = writeToDirectory(newImageDestination, hdr, io.LimitReader(tr, hdr.Size)) } if writeError != nil { return "", fmt.Errorf("writing copy of image to %q: %w", newImageDestination, writeError) } hdr, err = tr.Next() } if err != nil && !errors.Is(err, io.EOF) { return "", fmt.Errorf("reading archive of base image: %w", err) } if isEmbeddedArchive { logrus.Warnf("expected to have archived a copy of %q, missed it", archiveSource) } if tw != nil { if err := tw.Close(); err != nil { return "", fmt.Errorf("wrapping up writing copy of base image to archive %q: %w", newImageDestination, err) } tw = nil } if f != nil { if err := f.Close(); err != nil { return "", fmt.Errorf("closing copy of base image in archive file %q: %w", newImageDestination, err) } f = nil } succeeded = true return newFrom, nil } ================================================ FILE: internal/sanitize/sanitize_test.go ================================================ package sanitize import ( "archive/tar" "bytes" "context" "encoding/json" "errors" "io" "io/fs" "os" "path" "path/filepath" "strings" "testing" "github.com/opencontainers/go-digest" imgspec "github.com/opencontainers/image-spec/specs-go" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" imageCopy "go.podman.io/image/v5/copy" "go.podman.io/image/v5/pkg/compression" "go.podman.io/image/v5/signature" "go.podman.io/image/v5/transports" "go.podman.io/image/v5/transports/alltransports" "go.podman.io/storage/pkg/reexec" ) func TestMain(m *testing.M) { if reexec.Init() { return } result := m.Run() os.Exit(result) } func TestSanitizeImageName(t *testing.T) { const badLinkTarget = "../../../../../../../../../../../etc/passwd" // prepare to copy from the layout to other types of destinations ctx := context.Background() // sys := &types.SystemContext{} policy, err := signature.NewPolicyFromBytes([]byte(`{"default":[{"type":"insecureAcceptAnything"}]}`)) require.NoErrorf(t, err, "creating a policy") policyContext, err := signature.NewPolicyContext(policy) require.NoErrorf(t, err, "creating a policy context") t.Cleanup(func() { if err := policyContext.Destroy(); err != nil { t.Logf("destroying policy context: %v", err) } }) scanDir := func(dir string) (bool, error) { // look for anything that isn't a plain directory or file foundSuspiciousStuff := false err := filepath.WalkDir(dir, func(_ string, d fs.DirEntry, err error) error { if err != nil { return err } if !d.Type().IsRegular() && !d.Type().IsDir() { foundSuspiciousStuff = true } return nil }) return foundSuspiciousStuff, err } scanArchive := func(file string) (bool, error) { // look for anything that isn't a plain directory, file, or hard link foundSuspiciousStuff := false f, err := os.Open(file) if err != nil { return foundSuspiciousStuff, err } dc, _, err := compression.AutoDecompress(f) if err != nil { return foundSuspiciousStuff, err } tr := tar.NewReader(dc) hdr, err := tr.Next() for hdr != nil { if hdr.Typeflag != tar.TypeReg && hdr.Typeflag != tar.TypeDir && hdr.Typeflag != tar.TypeLink { foundSuspiciousStuff = true break } if hdr.Typeflag == tar.TypeLink { if strings.TrimPrefix(path.Clean("/"+hdr.Linkname), "/") != hdr.Linkname { foundSuspiciousStuff = true break } } hdr, err = tr.Next() } if err := dc.Close(); err != nil { if err2 := f.Close(); err2 != nil { err = errors.Join(err, err2) } return foundSuspiciousStuff, err } if err := f.Close(); err != nil { return foundSuspiciousStuff, err } if err != nil && !errors.Is(err, io.EOF) { return foundSuspiciousStuff, err } return foundSuspiciousStuff, nil } generateLayout := func(t *testing.T, layoutDir string) (uncompressed digest.Digest, compressed digest.Digest) { t.Helper() var layer bytes.Buffer layerDigester := digest.Canonical.Digester() wc, err := compression.CompressStream(io.MultiWriter(&layer, layerDigester.Hash()), compression.Gzip, nil) require.NoErrorf(t, err, "compressing empty layer") diffID := digest.Canonical.Digester() tw := tar.NewWriter(io.MultiWriter(wc, diffID.Hash())) require.NoErrorf(t, tw.Close(), "flushing empty layer") require.NoErrorf(t, wc.Close(), "flushing compressor") diffDigest := diffID.Digest() layerBytes := layer.Bytes() layerDigest := layerDigester.Digest() ociConfig := v1.Image{ RootFS: v1.RootFS{ Type: "layers", DiffIDs: []digest.Digest{diffDigest}, }, } configBytes, err := json.Marshal(&ociConfig) require.NoError(t, err, "encoding oci config") configDigest := digest.Canonical.FromBytes(configBytes) ociManifest := v1.Manifest{ Versioned: imgspec.Versioned{ SchemaVersion: 2, }, MediaType: v1.MediaTypeImageManifest, Config: v1.Descriptor{ MediaType: v1.MediaTypeImageConfig, Digest: configDigest, Size: int64(len(configBytes)), }, Layers: []v1.Descriptor{{ MediaType: v1.MediaTypeImageLayerGzip, Digest: layerDigest, Size: int64(len(layerBytes)), }}, } manifestBytes, err := json.Marshal(&ociManifest) require.NoError(t, err, "encoding oci manifest") manifestDigest := digest.Canonical.FromBytes(manifestBytes) index := v1.Index{ Versioned: imgspec.Versioned{ SchemaVersion: 2, }, MediaType: v1.MediaTypeImageIndex, Manifests: []v1.Descriptor{{ MediaType: v1.MediaTypeImageManifest, Digest: manifestDigest, Size: int64(len(manifestBytes)), Annotations: map[string]string{ v1.AnnotationRefName: "latest", }, }}, } indexBytes, err := json.Marshal(&index) require.NoError(t, err, "encoding oci index") layoutBytes, err := json.Marshal(&v1.ImageLayout{Version: v1.ImageLayoutVersion}) require.NoError(t, err, "encoding oci layout") blobsDir := filepath.Join(layoutDir, v1.ImageBlobsDir) require.NoError(t, os.MkdirAll(blobsDir, 0o700), "creating blobs subdirectory") blobsDir = filepath.Join(blobsDir, digest.Canonical.String()) require.NoErrorf(t, os.MkdirAll(blobsDir, 0o700), "creating %q/%q subdirectory", v1.ImageBlobsDir, digest.Canonical.String()) require.NoError(t, os.WriteFile(filepath.Join(blobsDir, layerDigest.Encoded()), layerBytes, 0o600), "writing layer") sus, err := scanArchive(filepath.Join(blobsDir, layerDigest.Encoded())) require.NoError(t, err, "unexpected error scanning empty layer") require.False(t, sus, "empty layer should pass scan") require.NoError(t, os.WriteFile(filepath.Join(blobsDir, configDigest.Encoded()), configBytes, 0o600), "writing config") require.NoError(t, os.WriteFile(filepath.Join(blobsDir, manifestDigest.Encoded()), manifestBytes, 0o600), "writing manifest") require.NoError(t, os.WriteFile(filepath.Join(layoutDir, v1.ImageIndexFile), indexBytes, 0o600), "writing index") require.NoError(t, os.WriteFile(filepath.Join(layoutDir, v1.ImageLayoutFile), layoutBytes, 0o600), "writing layout") sus, err = scanDir(layoutDir) require.NoError(t, err, "scanning layout directory") require.False(t, sus, "check on layout directory") return diffDigest, layerDigest } mutateDirectory := func(t *testing.T, parentdir, input, replace string) string { t.Helper() tmpdir, err := os.MkdirTemp(parentdir, "directory") require.NoError(t, err, "creating mutated directory") found := false err = filepath.Walk(input, func(path string, info fs.FileInfo, err error) error { if err != nil { return err } rel, err := filepath.Rel(input, path) if err != nil { return err } if rel == replace { found = true return os.Symlink(badLinkTarget, filepath.Join(tmpdir, rel)) } if info.IsDir() { if rel == "." { return nil } return os.Mkdir(filepath.Join(tmpdir, rel), info.Mode()) } switch info.Mode() & os.ModeType { case os.ModeSymlink: target, err := os.Readlink(path) require.NoErrorf(t, err, "reading link target from %q", path) return os.Symlink(target, filepath.Join(tmpdir, rel)) case os.ModeCharDevice | os.ModeDevice: t.Fatalf("unexpected character device %q", path) case os.ModeDevice: t.Fatalf("unexpected block device %q", path) case os.ModeNamedPipe: t.Fatalf("unexpected named pipe %q", path) case os.ModeSocket: t.Fatalf("unexpected socket %q", path) case os.ModeIrregular: t.Fatalf("unexpected irregularity %q", path) case os.ModeDir: t.Fatalf("unexpected directory %q after we should have created it", path) default: inf, err := os.Open(path) require.NoErrorf(t, err, "opening %q to read it", path) outpath := filepath.Join(tmpdir, rel) outf, err := os.OpenFile(outpath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, info.Mode()) require.NoErrorf(t, err, "opening %q to write it", outpath) n, err := io.Copy(outf, inf) require.NoErrorf(t, err, "copying contents of %q", path) require.Equalf(t, info.Size(), n, "unexpected write length to %q (%d != %d)", outpath, n, info.Size()) require.NoErrorf(t, outf.Close(), "closing new file %q", outpath) require.NoErrorf(t, inf.Close(), "closing old file %q", path) } return nil }) require.NoError(t, err, "mutating directory") require.Truef(t, found, "did not replace %q", replace) return tmpdir } mutateArchive := func(t *testing.T, parentdir, input, replace string) string { t.Helper() f, err := os.Open(input) require.NoError(t, err, "opening archive for mutating") tmpfile, err := os.CreateTemp(parentdir, "archive") require.NoError(t, err, "creating mutated archive") tr := tar.NewReader(f) tw := tar.NewWriter(tmpfile) found := false hdr, err := tr.Next() for hdr != nil { if hdr.Name == replace { err := tw.WriteHeader(&tar.Header{ Name: hdr.Name, Typeflag: tar.TypeSymlink, Linkname: badLinkTarget, }) require.NoErrorf(t, err, "copying contents of %q", hdr.Name) found = true } else { err := tw.WriteHeader(hdr) require.NoErrorf(t, err, "copying contents of %q", hdr.Name) _, err = io.Copy(tw, tr) require.NoErrorf(t, err, "copying contents of %q", hdr.Name) } if err != nil { break } hdr, err = tr.Next() } if err != nil && !errors.Is(err, io.EOF) { require.NoError(t, err, "when finished reading archive for mutating") } require.NoError(t, f.Close(), "closing archive for mutating") require.NoError(t, tw.Close(), "finishing up writing mutated archive") tmpfileName := tmpfile.Name() require.NoError(t, tmpfile.Close(), "closing mutated archive") require.Truef(t, found, "did not replace %q", replace) return tmpfileName } requireImageReadable := func(t *testing.T, imageName string) { t.Helper() tmpdir := t.TempDir() ref, err := alltransports.ParseImageName(imageName) require.NoErrorf(t, err, "parsing image reference %q", imageName) dir, err := alltransports.ParseImageName("dir:" + tmpdir) require.NoErrorf(t, err, "parsing image reference %q", "dir:"+tmpdir) _, err = imageCopy.Image(ctx, policyContext, dir, ref, nil) require.NoErrorf(t, err, "copying image %q, which should have been successful", transports.ImageName(ref)) } contextDir := t.TempDir() subdirsDir := filepath.Join(contextDir, "subdirs") require.NoError(t, os.Mkdir(subdirsDir, 0o700), "creating subdirectory directory") archiveDir := filepath.Join(contextDir, "archives") require.NoError(t, os.Mkdir(archiveDir, 0o700), "creating archives directory") // create a normal layout somewhere under contextDir goodLayout, err := os.MkdirTemp(subdirsDir, "goodlayout") require.NoErrorf(t, err, "creating a known-good OCI layout") diffDigest, blobDigest := generateLayout(t, goodLayout) goodLayoutRef, err := alltransports.ParseImageName("oci:" + goodLayout) require.NoErrorf(t, err, "parsing image reference to known-good OCI layout") sus, err := scanDir(goodLayout) require.NoError(t, err, "scanning known-good OCI layout") assert.False(t, sus, "check on known-good OCI layout") // copy to a directory goodDir, err := os.MkdirTemp(subdirsDir, "gooddir") require.NoErrorf(t, err, "creating a temporary directory to store a good directory") goodDirRef, err := alltransports.ParseImageName("dir:" + goodDir) require.NoErrorf(t, err, "parsing image reference to good directory") _, err = imageCopy.Image(ctx, policyContext, goodDirRef, goodLayoutRef, nil) require.NoError(t, err, "copying an acceptable OCI layout to a directory") // scan the directory sus, err = scanDir(goodDir) require.NoError(t, err, "scanning good directory") assert.False(t, sus, "check on good directory") // copy to a docker-archive goodDockerArchiveFile, err := os.CreateTemp(archiveDir, "gooddockerarchive") require.NoErrorf(t, err, "creating a temporary file to store a good docker archive") goodDockerArchive := goodDockerArchiveFile.Name() goodDockerArchiveRef, err := alltransports.ParseImageName("docker-archive:" + goodDockerArchive) require.NoErrorf(t, err, "parsing image reference to good docker archive") require.NoError(t, err, goodDockerArchiveFile.Close()) require.NoError(t, err, os.Remove(goodDockerArchive)) _, err = imageCopy.Image(ctx, policyContext, goodDockerArchiveRef, goodLayoutRef, nil) require.NoError(t, err, "copying an acceptable OCI layout to a docker archive") // scan the docker-archive sus, err = scanArchive(goodDockerArchive) require.NoError(t, err, "scanning good docker archive") assert.True(t, sus, "check on good docker archive") // there are symlinks in there // copy to an oci-archive goodOCIArchiveFile, err := os.CreateTemp(archiveDir, "goodociarchive") require.NoErrorf(t, err, "creating a temporary file to store a good oci archive") goodOCIArchive := goodOCIArchiveFile.Name() goodOCIArchiveRef, err := alltransports.ParseImageName("oci-archive:" + goodOCIArchive) require.NoErrorf(t, err, "parsing image reference to good oci archive") require.NoError(t, err, goodOCIArchiveFile.Close()) require.NoError(t, err, os.Remove(goodOCIArchive)) _, err = imageCopy.Image(ctx, policyContext, goodOCIArchiveRef, goodLayoutRef, nil) require.NoError(t, err, "copying an acceptable OCI layout to an OCI archive") // scan the oci-archive sus, err = scanArchive(goodOCIArchive) require.NoError(t, err, "scanning good OCI archive") assert.False(t, sus, "check on good OCI archive") // make sure the original versions can all be read without error requireImageReadable(t, transports.ImageName(goodLayoutRef)) requireImageReadable(t, transports.ImageName(goodDirRef)) requireImageReadable(t, transports.ImageName(goodOCIArchiveRef)) requireImageReadable(t, transports.ImageName(goodDockerArchiveRef)) // sanitize them all goodLayoutRel, err := filepath.Rel(contextDir, goodLayout) require.NoErrorf(t, err, "converting absolute path %q to a relative one", goodLayout) newGoodLayout, err := ImageName("oci", goodLayoutRel, contextDir, t.TempDir()) require.NoError(t, err, "sanitizing good OCI layout") goodDirRel, err := filepath.Rel(contextDir, goodDir) require.NoErrorf(t, err, "converting absolute path %q to a relative one", goodDir) newGoodDir, err := ImageName("dir", goodDirRel, contextDir, t.TempDir()) require.NoError(t, err, "sanitizing good directory") goodOCIArchiveRel, err := filepath.Rel(contextDir, goodOCIArchive) require.NoErrorf(t, err, "converting absolute path %q to a relative one", goodOCIArchive) newGoodOCIArchive, err := ImageName("oci-archive", goodOCIArchiveRel, contextDir, t.TempDir()) require.NoError(t, err, "sanitizing good OCI archive") goodDockerArchiveRel, err := filepath.Rel(contextDir, goodDockerArchive) require.NoErrorf(t, err, "converting absolute path %q to a relative one", goodDockerArchive) newGoodDockerArchive, err := ImageName("docker-archive", goodDockerArchiveRel, contextDir, t.TempDir()) require.NoError(t, err, "sanitizing good docker archive") // make sure the sanitized versions can all be read without error requireImageReadable(t, newGoodLayout) requireImageReadable(t, newGoodDir) requireImageReadable(t, newGoodOCIArchive) requireImageReadable(t, newGoodDockerArchive) // mutate them all badLayout := mutateDirectory(t, contextDir, goodLayout, filepath.Join(v1.ImageBlobsDir, blobDigest.Algorithm().String(), blobDigest.Encoded())) badLayoutRel, err := filepath.Rel(contextDir, badLayout) require.NoErrorf(t, err, "converting absolute path %q to a relative one", badLayout) _, err = ImageName("oci", badLayoutRel, contextDir, t.TempDir()) require.ErrorIs(t, err, os.ErrNotExist, "sanitizing bad OCI layout") badDir := mutateDirectory(t, contextDir, goodDir, blobDigest.Encoded()) badDirRel, err := filepath.Rel(contextDir, badDir) require.NoErrorf(t, err, "converting absolute path %q to a relative one", badDir) _, err = ImageName("dir", badDirRel, contextDir, t.TempDir()) require.ErrorIs(t, err, os.ErrNotExist, "sanitizing bad directory") badOCIArchive := mutateArchive(t, contextDir, goodOCIArchive, filepath.Join(v1.ImageBlobsDir, blobDigest.Algorithm().String(), blobDigest.Encoded())) badOCIArchiveRel, err := filepath.Rel(contextDir, badOCIArchive) require.NoErrorf(t, err, "converting absolute path %q to a relative one", badOCIArchive) _, err = ImageName("oci-archive", badOCIArchiveRel, contextDir, t.TempDir()) require.ErrorContains(t, err, "invalid symbolic link", "sanitizing bad oci archive") badDockerArchive := mutateArchive(t, contextDir, goodDockerArchive, diffDigest.Encoded()+".tar") badDockerArchiveRel, err := filepath.Rel(contextDir, badDockerArchive) require.NoErrorf(t, err, "converting absolute path %q to a relative one", badDockerArchive) _, err = ImageName("docker-archive", badDockerArchiveRel, contextDir, t.TempDir()) require.ErrorContains(t, err, "invalid symbolic link", "sanitizing bad docker archive") } ================================================ FILE: internal/sbom/merge.go ================================================ package sbom import ( "encoding/json" "fmt" "io" "os" "sort" "github.com/containers/buildah/define" ) // getComponentNameVersionPurl extracts the "name", "version", and "purl" // fields of a CycloneDX component record func getComponentNameVersionPurl(anyComponent any) (string, string, error) { if component, ok := anyComponent.(map[string]any); ok { // read the "name" field anyName, ok := component["name"] if !ok { return "", "", fmt.Errorf("no name in component %v", anyComponent) } name, ok := anyName.(string) if !ok { return "", "", fmt.Errorf("name %v is not a string", anyName) } // read the optional "version" field var version string anyVersion, ok := component["version"] if ok { if version, ok = anyVersion.(string); !ok { return "", "", fmt.Errorf("version %v is not a string", anyVersion) } } // combine them nameWithVersion := name if version != "" { nameWithVersion += ("@" + version) } // read the optional "purl" field var purl string anyPurl, ok := component["purl"] if ok { if purl, ok = anyPurl.(string); !ok { return "", "", fmt.Errorf("purl %v is not a string", anyPurl) } } return nameWithVersion, purl, nil } return "", "", fmt.Errorf("component %v is not an object", anyComponent) } // getPackageNameVersionInfoPurl extracts the "name", "versionInfo", and "purl" // fields of an SPDX package record func getPackageNameVersionInfoPurl(anyPackage any) (string, string, error) { if pkg, ok := anyPackage.(map[string]any); ok { // read the "name" field anyName, ok := pkg["name"] if !ok { return "", "", fmt.Errorf("no name in package %v", anyPackage) } name, ok := anyName.(string) if !ok { return "", "", fmt.Errorf("name %v is not a string", anyName) } // read the optional "versionInfo" field var versionInfo string if anyVersionInfo, ok := pkg["versionInfo"]; ok { if versionInfo, ok = anyVersionInfo.(string); !ok { return "", "", fmt.Errorf("versionInfo %v is not a string", anyVersionInfo) } } // combine them nameWithVersionInfo := name if versionInfo != "" { nameWithVersionInfo += ("@" + versionInfo) } // now look for optional externalRefs[].purl if "referenceCategory" // is "PACKAGE-MANAGER" and "referenceType" is "purl" var purl string if anyExternalRefs, ok := pkg["externalRefs"]; ok { if externalRefs, ok := anyExternalRefs.([]any); ok { for _, anyExternalRef := range externalRefs { if externalRef, ok := anyExternalRef.(map[string]any); ok { anyReferenceCategory, ok := externalRef["referenceCategory"] if !ok { continue } if referenceCategory, ok := anyReferenceCategory.(string); !ok || referenceCategory != "PACKAGE-MANAGER" { continue } anyReferenceType, ok := externalRef["referenceType"] if !ok { continue } if referenceType, ok := anyReferenceType.(string); !ok || referenceType != "purl" { continue } if anyReferenceLocator, ok := externalRef["referenceLocator"]; ok { if purl, ok = anyReferenceLocator.(string); !ok { return "", "", fmt.Errorf("purl %v is not a string", anyReferenceLocator) } } } } } } return nameWithVersionInfo, purl, nil } return "", "", fmt.Errorf("package %v is not an object", anyPackage) } // getLicenseID extracts the "licenseId" field of an SPDX license record func getLicenseID(anyLicense any) (string, error) { var licenseID string if lic, ok := anyLicense.(map[string]any); ok { anyID, ok := lic["licenseId"] if !ok { return "", fmt.Errorf("no licenseId in license %v", anyID) } id, ok := anyID.(string) if !ok { return "", fmt.Errorf("licenseId %v is not a string", anyID) } licenseID = id } return licenseID, nil } // mergeSlicesWithoutDuplicates merges a named slice in "base" with items from // the same slice in "merge", so long as getKey() returns values for them that // it didn't for items from the "base" slice func mergeSlicesWithoutDuplicates(base, merge map[string]any, sliceField string, getKey func(record any) (string, error)) error { uniqueKeys := make(map[string]struct{}) // go through all of the values in the base slice, grab their // keys, and note them baseRecords := base[sliceField] baseRecordsSlice, ok := baseRecords.([]any) if !ok { baseRecordsSlice = []any{} } for _, anyRecord := range baseRecordsSlice { key, err := getKey(anyRecord) if err != nil { return err } uniqueKeys[key] = struct{}{} } // go through all of the record values in the merge doc, grab their // associated keys, and append them to the base records slice if we // haven't seen the key yet mergeRecords := merge[sliceField] mergeRecordsSlice, ok := mergeRecords.([]any) if !ok { mergeRecordsSlice = []any{} } for _, anyRecord := range mergeRecordsSlice { key, err := getKey(anyRecord) if err != nil { return err } if _, present := uniqueKeys[key]; !present { baseRecordsSlice = append(baseRecordsSlice, anyRecord) uniqueKeys[key] = struct{}{} } } if len(baseRecordsSlice) > 0 { base[sliceField] = baseRecordsSlice } return nil } // decodeJSON decodes a file into a map func decodeJSON(inputFile string, document *map[string]any) error { src, err := os.Open(inputFile) if err != nil { return err } defer src.Close() if err = json.NewDecoder(src).Decode(document); err != nil { return fmt.Errorf("decoding JSON document from %q: %w", inputFile, err) } return nil } // encodeJSON encodes a map and saves it to a file func encodeJSON(outputFile string, document any) error { dst, err := os.Create(outputFile) if err != nil { return err } defer dst.Close() if err = json.NewEncoder(dst).Encode(document); err != nil { return fmt.Errorf("writing JSON document to %q: %w", outputFile, err) } return nil } // Merge adds the contents of inputSBOM to inputOutputSBOM using one of a // handful of named strategies. func Merge(mergeStrategy define.SBOMMergeStrategy, inputOutputSBOM, inputSBOM, outputPURL string) (err error) { type purlImageContents struct { Dependencies []string `json:"dependencies,omitempty"` } type purlDocument struct { ImageContents purlImageContents `json:"image_contents"` } purls := []string{} seenPurls := make(map[string]struct{}) switch mergeStrategy { case define.SBOMMergeStrategyCycloneDXByComponentNameAndVersion: var base, merge map[string]any if err = decodeJSON(inputOutputSBOM, &base); err != nil { return fmt.Errorf("reading first SBOM to be merged from %q: %w", inputOutputSBOM, err) } if err = decodeJSON(inputSBOM, &merge); err != nil { return fmt.Errorf("reading second SBOM to be merged from %q: %w", inputSBOM, err) } // merge the "components" lists based on unique combinations of // "name" and "version" fields, and save unique package URL // values err = mergeSlicesWithoutDuplicates(base, merge, "components", func(anyPackage any) (string, error) { nameWithVersion, purl, err := getComponentNameVersionPurl(anyPackage) if purl != "" { if _, seen := seenPurls[purl]; !seen { purls = append(purls, purl) seenPurls[purl] = struct{}{} } } return nameWithVersion, err }) if err != nil { return fmt.Errorf("merging the %q field of CycloneDX SBOMs: %w", "components", err) } // save the updated doc err = encodeJSON(inputOutputSBOM, base) case define.SBOMMergeStrategySPDXByPackageNameAndVersionInfo: var base, merge map[string]any if err = decodeJSON(inputOutputSBOM, &base); err != nil { return fmt.Errorf("reading first SBOM to be merged from %q: %w", inputOutputSBOM, err) } if err = decodeJSON(inputSBOM, &merge); err != nil { return fmt.Errorf("reading second SBOM to be merged from %q: %w", inputSBOM, err) } // merge the "packages" lists based on unique combinations of // "name" and "versionInfo" fields, and save unique package URL // values err = mergeSlicesWithoutDuplicates(base, merge, "packages", func(anyPackage any) (string, error) { nameWithVersionInfo, purl, err := getPackageNameVersionInfoPurl(anyPackage) if purl != "" { if _, seen := seenPurls[purl]; !seen { purls = append(purls, purl) seenPurls[purl] = struct{}{} } } return nameWithVersionInfo, err }) if err != nil { return fmt.Errorf("merging the %q field of SPDX SBOMs: %w", "packages", err) } // merge the "hasExtractedLicensingInfos" lists based on unique // "licenseId" values err = mergeSlicesWithoutDuplicates(base, merge, "hasExtractedLicensingInfos", getLicenseID) if err != nil { return fmt.Errorf("merging the %q field of SPDX SBOMs: %w", "hasExtractedLicensingInfos", err) } // save the updated doc err = encodeJSON(inputOutputSBOM, base) case define.SBOMMergeStrategyCat: dst, err := os.OpenFile(inputOutputSBOM, os.O_RDWR|os.O_APPEND, 0o644) if err != nil { return err } defer dst.Close() src, err := os.Open(inputSBOM) if err != nil { return err } defer src.Close() if _, err = io.Copy(dst, src); err != nil { return err } } if err == nil { sort.Strings(purls) err = encodeJSON(outputPURL, &purlDocument{purlImageContents{Dependencies: purls}}) } return err } ================================================ FILE: internal/sbom/merge_test.go ================================================ package sbom import ( "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestEncodeJSON(t *testing.T) { t.Parallel() tmp := t.TempDir() map1 := map[string]any{ "string": "yeah", "number": 1, "struct": map[string]any{ "string": "yep", "number": 2, }, } err := encodeJSON(filepath.Join(tmp, "1.json"), map1) require.NoError(t, err) st1, err := os.Stat(filepath.Join(tmp, "1.json")) require.NoError(t, err) assert.NotZero(t, st1.Size()) map2 := struct { String string `json:"string"` Number int `json:"number"` Struct struct { String string `json:"string"` Number int `json:"number"` } `json:"struct"` }{ String: "yeah", Number: 1, Struct: struct { String string `json:"string"` Number int `json:"number"` }{ String: "yep", Number: 2, }, } err = encodeJSON(filepath.Join(tmp, "2.json"), map2) require.NoError(t, err) st2, err := os.Stat(filepath.Join(tmp, "2.json")) require.NoError(t, err) assert.NotZero(t, st2.Size()) c1, err := os.ReadFile(filepath.Join(tmp, "1.json")) require.NoError(t, err) c2, err := os.ReadFile(filepath.Join(tmp, "2.json")) require.NoError(t, err) assert.Equalf(t, len(c2), len(c1), "length of %q is not the same as length of %q", string(c1), string(c2)) } func TestDecodeJSON(t *testing.T) { t.Parallel() tmp := t.TempDir() var map1, map2, map3 map[string]any err := os.WriteFile(filepath.Join(tmp, "1.json"), []byte(` { "string":"yeah", "number":1, "struct":{"string":"yep", "number":2 }} `), 0o666) require.NoError(t, err) err = decodeJSON(filepath.Join(tmp, "1.json"), &map1) require.NoError(t, err) err = os.WriteFile(filepath.Join(tmp, "2.json"), []byte(` {"string":"yeah", "number":1, "struct":{"string":"yep", "number":2} } `), 0o666) require.NoError(t, err) err = decodeJSON(filepath.Join(tmp, "2.json"), &map2) require.NoError(t, err) assert.Equal(t, map2, map1) err = os.WriteFile(filepath.Join(tmp, "3.txt"), []byte(` what a lovely, lovely day `), 0o666) require.NoError(t, err) err = decodeJSON(filepath.Join(tmp, "3.txt"), &map3) require.Error(t, err) } func TestGetComponentNameVersionPurl(t *testing.T) { t.Parallel() input := map[string]any{ "name": "alice", "version": "1.0", "purl": "purl://...", } s, purl, err := getComponentNameVersionPurl(input) require.NoError(t, err) assert.Equal(t, "alice@1.0", s) assert.Equal(t, "purl://...", purl) input = map[string]any{ "name": "alice", "purl": "pkg:/...", } s, purl, err = getComponentNameVersionPurl(input) require.NoError(t, err) assert.Equal(t, "alice", s) assert.Equal(t, "pkg:/...", purl) input = map[string]any{ "name": "alice", "version": "2.0", } s, purl, err = getComponentNameVersionPurl(input) require.NoError(t, err) assert.Equal(t, "alice@2.0", s) assert.Empty(t, purl) } func TestGetLicenseID(t *testing.T) { t.Parallel() input := map[string]any{ "licenseId": "driver", } s, err := getLicenseID(input) require.NoError(t, err) assert.Equal(t, "driver", s) } func TestGetPackageNameVersionInfoPurl(t *testing.T) { t.Parallel() input := map[string]any{ "name": "alice", "versionInfo": "1.0", "externalRefs": []any{ map[string]any{ "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", "referenceLocator": "pkg://....", }, }, } s, purl, err := getPackageNameVersionInfoPurl(input) require.NoError(t, err) assert.Equal(t, "alice@1.0", s) assert.Equal(t, "pkg://....", purl) input = map[string]any{ "name": "alice", "externalRefs": []any{ map[string]any{ "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", "referenceLocator": "pkg:///...", }, }, } s, purl, err = getPackageNameVersionInfoPurl(input) require.NoError(t, err) assert.Equal(t, "alice", s) assert.Equal(t, "pkg:///...", purl) input = map[string]any{ "name": "alice", "externalRefs": []any{ map[string]any{ "referenceCategory": "NOT-THE-PACKAGE-MANAGER", "referenceType": "obscure", "referenceLocator": "beep:...", }, }, } s, purl, err = getPackageNameVersionInfoPurl(input) require.NoError(t, err) assert.Equal(t, "alice", s) assert.Empty(t, purl) input = map[string]any{ "name": "alice", } s, purl, err = getPackageNameVersionInfoPurl(input) require.NoError(t, err) assert.Equal(t, "alice", s) assert.Empty(t, purl) input = map[string]any{ "not-name": "alice", } _, _, err = getPackageNameVersionInfoPurl(input) require.Error(t, err) } func TestMergeSlicesWithoutDuplicatesFixed(t *testing.T) { t.Parallel() base := map[string]any{ "array": []any{ map[string]any{"first": 1}, }, } merge := map[string]any{ "array": []any{ map[string]any{"second": 2}, }, } expected := map[string]any{ "array": []any{ map[string]any{"first": 1}, }, } err := mergeSlicesWithoutDuplicates(base, merge, "array", func(_ any) (string, error) { return "fixed", nil }) assert.NoError(t, err) assert.Equal(t, expected, base) } func TestMergeSlicesWithoutDuplicatesDynamic(t *testing.T) { t.Parallel() base := map[string]any{ "array": []any{ map[string]any{"first": 1}, }, } merge := map[string]any{ "array": []any{ map[string]any{"second": 2}, }, } expected := map[string]any{ "array": []any{ map[string]any{"first": 1}, map[string]any{"second": 2}, }, } err := mergeSlicesWithoutDuplicates(base, merge, "array", func(record any) (string, error) { if m, ok := record.(map[string]any); ok { for key := range m { return key, nil } } return "broken", nil }) assert.NoError(t, err) assert.Equal(t, expected, base) } func TestMergeSlicesWithoutDuplicatesNoop(t *testing.T) { t.Parallel() base := map[string]any{ "array": []any{ map[string]any{"first": 1}, }, } expected := map[string]any{ "array": []any{ map[string]any{"first": 1}, }, } err := mergeSlicesWithoutDuplicates(base, nil, "array", func(record any) (string, error) { if m, ok := record.(map[string]any); ok { for key := range m { return key, nil } } return "broken", nil }) assert.NoError(t, err) assert.Equal(t, expected, base) } func TestMergeSlicesWithoutDuplicatesMissing(t *testing.T) { t.Parallel() base := map[string]any{ "array": []any{ map[string]any{"first": 1}, }, } merge := map[string]any{ "array": []any{ map[string]any{"second": 2}, }, } expected := map[string]any{ "array": []any{ map[string]any{"first": 1}, }, } err := mergeSlicesWithoutDuplicates(base, merge, "otherarray", func(record any) (string, error) { if m, ok := record.(map[string]any); ok { for key := range m { return key, nil } } return "broken", nil }) assert.NoError(t, err) assert.Equal(t, expected, base) } ================================================ FILE: internal/sbom/presets.go ================================================ package sbom import ( "slices" "github.com/containers/buildah/define" ) // Preset returns a predefined SBOMScanOptions structure that has the passed-in // name as one of its "Type" values. func Preset(name string) (preset *define.SBOMScanOptions, err error) { // If you change these, make sure you update references in // buildah-commit.1.md and buildah-build.1.md to match! presets := []define.SBOMScanOptions{ { Type: []string{"", "syft", "syft-cyclonedx"}, Image: "ghcr.io/anchore/syft", Commands: []string{ "/syft scan -q dir:{ROOTFS} --output cyclonedx-json={OUTPUT}", "/syft scan -q dir:{CONTEXT} --output cyclonedx-json={OUTPUT}", }, // ImageSBOMOutput: "/root/buildinfo/content_manifests/sbom-cyclonedx.json", // ImagePURLOutput: "/root/buildinfo/content_manifests/sbom-purl.json", MergeStrategy: define.SBOMMergeStrategyCycloneDXByComponentNameAndVersion, }, { Type: []string{"syft-spdx"}, Image: "ghcr.io/anchore/syft", Commands: []string{ "/syft scan -q dir:{ROOTFS} --output spdx-json={OUTPUT}", "/syft scan -q dir:{CONTEXT} --output spdx-json={OUTPUT}", }, // ImageSBOMOutput: "/root/buildinfo/content_manifests/sbom-spdx.json", // ImagePURLOutput: "/root/buildinfo/content_manifests/sbom-purl.json", MergeStrategy: define.SBOMMergeStrategySPDXByPackageNameAndVersionInfo, }, { Type: []string{"trivy", "trivy-cyclonedx"}, Image: "ghcr.io/aquasecurity/trivy", Commands: []string{ "trivy filesystem -q {ROOTFS} --format cyclonedx --output {OUTPUT}", "trivy filesystem -q {CONTEXT} --format cyclonedx --output {OUTPUT}", }, // ImageSBOMOutput: "/root/buildinfo/content_manifests/sbom-cyclonedx.json", // ImagePURLOutput: "/root/buildinfo/content_manifests/sbom-purl.json", MergeStrategy: define.SBOMMergeStrategyCycloneDXByComponentNameAndVersion, }, { Type: []string{"trivy-spdx"}, Image: "ghcr.io/aquasecurity/trivy", Commands: []string{ "trivy filesystem -q {ROOTFS} --format spdx-json --output {OUTPUT}", "trivy filesystem -q {CONTEXT} --format spdx-json --output {OUTPUT}", }, // ImageSBOMOutput: "/root/buildinfo/content_manifests/sbom-spdx.json", // ImagePURLOutput: "/root/buildinfo/content_manifests/sbom-purl.json", MergeStrategy: define.SBOMMergeStrategySPDXByPackageNameAndVersionInfo, }, } for _, preset := range presets { if slices.Contains(preset.Type, name) { return &preset, nil } } return nil, nil } ================================================ FILE: internal/sbom/presets_test.go ================================================ package sbom import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestPreset(t *testing.T) { t.Parallel() for presetName, expectToFind := range map[string]bool{ "": true, "syft": true, "syft-cyclonedx": true, "syft-spdx": true, "trivy": true, "trivy-cyclonedx": true, "trivy-spdx": true, "rpc": false, "justmakestuffup": false, } { desc := presetName if desc == "" { desc = "(blank)" } t.Run(desc, func(t *testing.T) { settings, err := Preset(presetName) require.NoError(t, err) if expectToFind { assert.NotNil(t, settings) assert.NotEmpty(t, settings.Commands) } else { assert.Nil(t, settings) } }) } } ================================================ FILE: internal/source/add.go ================================================ package source import ( "context" "encoding/json" "fmt" "os" "path/filepath" "strings" specV1 "github.com/opencontainers/image-spec/specs-go/v1" "go.podman.io/image/v5/types" "go.podman.io/storage/pkg/archive" "go.podman.io/storage/pkg/fileutils" ) // AddOptions include data to alter certain knobs when adding a source artifact // to a source image. type AddOptions struct { // Annotations for the source artifact. Annotations []string } // annotations parses the specified annotations and transforms them into a map. // A given annotation can be specified only once. func (o *AddOptions) annotations() (map[string]string, error) { annotations := make(map[string]string) for _, unparsed := range o.Annotations { key, value, hasValue := strings.Cut(unparsed, "=") if !hasValue { return nil, fmt.Errorf("invalid annotation %q (expected format is \"key=value\")", unparsed) } if _, exists := annotations[key]; exists { return nil, fmt.Errorf("annotation %q specified more than once", key) } annotations[key] = value } return annotations, nil } // Add adds the specified source artifact at `artifactPath` to the source image // at `sourcePath`. Note that the artifact will be added as a gzip-compressed // tar ball. Add attempts to auto-tar and auto-compress only if necessary. func Add(ctx context.Context, sourcePath string, artifactPath string, options AddOptions) error { // Let's first make sure `sourcePath` exists and that we can access it. if err := fileutils.Exists(sourcePath); err != nil { return err } annotations, err := options.annotations() if err != nil { return err } ociDest, err := openOrCreateSourceImage(ctx, sourcePath) if err != nil { return err } defer ociDest.Close() tarStream, err := archive.TarWithOptions(artifactPath, &archive.TarOptions{Compression: archive.Gzip}) if err != nil { return fmt.Errorf("creating compressed tar stream: %w", err) } info := types.BlobInfo{ Size: -1, // "unknown": we'll get that information *after* adding } addedBlob, err := ociDest.PutBlob(ctx, tarStream, info, nil, false) if err != nil { return fmt.Errorf("adding source artifact: %w", err) } // Add the new layers to the source image's manifest. manifest, oldManifestDigest, _, err := readManifestFromOCIPath(ctx, sourcePath) if err != nil { return err } manifest.Layers = append(manifest.Layers, specV1.Descriptor{ MediaType: specV1.MediaTypeImageLayerGzip, Digest: addedBlob.Digest, Size: addedBlob.Size, Annotations: annotations, }, ) manifestDigest, manifestSize, err := writeManifest(ctx, manifest, ociDest) if err != nil { return err } // Now, as we've written the updated manifest, we can delete the // previous one. `types.ImageDestination` doesn't expose a high-level // API to manage multi-manifest destination, so we need to do it // manually. Not an issue, since paths are predictable for an OCI // layout. if err := removeBlob(oldManifestDigest, sourcePath); err != nil { return fmt.Errorf("removing old manifest: %w", err) } manifestDescriptor := specV1.Descriptor{ MediaType: specV1.MediaTypeImageManifest, Digest: *manifestDigest, Size: manifestSize, } if err := updateIndexWithNewManifestDescriptor(&manifestDescriptor, sourcePath); err != nil { return err } return nil } func updateIndexWithNewManifestDescriptor(manifest *specV1.Descriptor, sourcePath string) error { index := specV1.Index{} indexPath := filepath.Join(sourcePath, "index.json") rawData, err := os.ReadFile(indexPath) if err != nil { return err } if err := json.Unmarshal(rawData, &index); err != nil { return err } index.Manifests = []specV1.Descriptor{*manifest} rawData, err = json.Marshal(&index) if err != nil { return err } return os.WriteFile(indexPath, rawData, 0o644) } ================================================ FILE: internal/source/create.go ================================================ package source import ( "context" "fmt" "time" spec "github.com/opencontainers/image-spec/specs-go" specV1 "github.com/opencontainers/image-spec/specs-go/v1" "go.podman.io/storage/pkg/fileutils" ) // CreateOptions includes data to alter certain knobs when creating a source // image. type CreateOptions struct { // Author is the author of the source image. Author string // TimeStamp controls whether a "created" timestamp is set or not. TimeStamp bool } // createdTime returns `time.Now()` if the options are configured to include a // time stamp. func (o *CreateOptions) createdTime() *time.Time { if !o.TimeStamp { return nil } now := time.Now() return &now } // Create creates an empty source image at the specified `sourcePath`. Note // that `sourcePath` must not exist. func Create(ctx context.Context, sourcePath string, options CreateOptions) error { if err := fileutils.Exists(sourcePath); err == nil { return fmt.Errorf("creating source image: %q already exists", sourcePath) } ociDest, err := openOrCreateSourceImage(ctx, sourcePath) if err != nil { return err } defer ociDest.Close() // Create and add a config. config := ImageConfig{ Author: options.Author, Created: options.createdTime(), } configBlob, err := addConfig(ctx, &config, ociDest) if err != nil { return err } // Create and write the manifest. manifest := specV1.Manifest{ Versioned: spec.Versioned{SchemaVersion: 2}, MediaType: specV1.MediaTypeImageManifest, Config: specV1.Descriptor{ MediaType: MediaTypeSourceImageConfig, Digest: configBlob.Digest, Size: configBlob.Size, }, } if _, _, err := writeManifest(ctx, &manifest, ociDest); err != nil { return err } return ociDest.Commit(ctx, nil) } ================================================ FILE: internal/source/pull.go ================================================ package source import ( "context" "fmt" "os" "github.com/containers/buildah/pkg/parse" "go.podman.io/image/v5/copy" "go.podman.io/image/v5/oci/layout" "go.podman.io/image/v5/pkg/shortnames" "go.podman.io/image/v5/signature" "go.podman.io/image/v5/transports/alltransports" "go.podman.io/image/v5/types" "go.podman.io/storage/pkg/fileutils" ) // PullOptions includes data to alter certain knobs when pulling a source // image. type PullOptions struct { // Require HTTPS and verify certificates when accessing the registry. TLSVerify bool // [username[:password] to use when connecting to the registry. Credentials string // Quiet the progress bars when pushing. Quiet bool } // Pull `imageInput` from a container registry to `sourcePath`. func Pull(ctx context.Context, imageInput string, sourcePath string, options PullOptions) error { if err := fileutils.Exists(sourcePath); err == nil { return fmt.Errorf("%q already exists", sourcePath) } srcRef, err := stringToImageReference(imageInput) if err != nil { return err } destRef, err := layout.ParseReference(sourcePath) if err != nil { return err } sysCtx := &types.SystemContext{ DockerInsecureSkipTLSVerify: types.NewOptionalBool(!options.TLSVerify), } if options.Credentials != "" { authConf, err := parse.AuthConfig(options.Credentials) if err != nil { return err } sysCtx.DockerAuthConfig = authConf } if err := validateSourceImageReference(ctx, srcRef, sysCtx); err != nil { return err } policy, err := signature.DefaultPolicy(sysCtx) if err != nil { return fmt.Errorf("obtaining default signature policy: %w", err) } policyContext, err := signature.NewPolicyContext(policy) if err != nil { return fmt.Errorf("creating new signature policy context: %w", err) } copyOpts := copy.Options{ SourceCtx: sysCtx, } if !options.Quiet { copyOpts.ReportWriter = os.Stderr } if _, err := copy.Image(ctx, policyContext, destRef, srcRef, ©Opts); err != nil { return fmt.Errorf("pulling source image: %w", err) } return nil } func stringToImageReference(imageInput string) (types.ImageReference, error) { if shortnames.IsShortName(imageInput) { return nil, fmt.Errorf("pulling source images by short name (%q) is not supported, please use a fully-qualified name", imageInput) } ref, err := alltransports.ParseImageName("docker://" + imageInput) if err != nil { return nil, fmt.Errorf("parsing image name: %w", err) } return ref, nil } func validateSourceImageReference(ctx context.Context, ref types.ImageReference, sysCtx *types.SystemContext) error { src, err := ref.NewImageSource(ctx, sysCtx) if err != nil { return fmt.Errorf("creating image source from reference: %w", err) } defer src.Close() ociManifest, _, _, err := readManifestFromImageSource(ctx, src) if err != nil { return err } if ociManifest.Config.MediaType != MediaTypeSourceImageConfig { return fmt.Errorf("invalid media type of image config %q (expected: %q)", ociManifest.Config.MediaType, MediaTypeSourceImageConfig) } return nil } ================================================ FILE: internal/source/push.go ================================================ package source import ( "context" "fmt" "os" "github.com/containers/buildah/pkg/parse" "go.podman.io/image/v5/copy" "go.podman.io/image/v5/manifest" "go.podman.io/image/v5/oci/layout" "go.podman.io/image/v5/signature" "go.podman.io/image/v5/types" ) // PushOptions includes data to alter certain knobs when pushing a source // image. type PushOptions struct { // Require HTTPS and verify certificates when accessing the registry. TLSVerify bool // [username[:password] to use when connecting to the registry. Credentials string // Quiet the progress bars when pushing. Quiet bool // If set after copying the artifact, write the digest of the resulting image to the file DigestFile string } // Push the source image at `sourcePath` to `imageInput` at a container // registry. func Push(ctx context.Context, sourcePath string, imageInput string, options PushOptions) error { srcRef, err := layout.ParseReference(sourcePath) if err != nil { return err } destRef, err := stringToImageReference(imageInput) if err != nil { return err } sysCtx := &types.SystemContext{ DockerInsecureSkipTLSVerify: types.NewOptionalBool(!options.TLSVerify), } if options.Credentials != "" { authConf, err := parse.AuthConfig(options.Credentials) if err != nil { return err } sysCtx.DockerAuthConfig = authConf } policy, err := signature.DefaultPolicy(sysCtx) if err != nil { return fmt.Errorf("obtaining default signature policy: %w", err) } policyContext, err := signature.NewPolicyContext(policy) if err != nil { return fmt.Errorf("creating new signature policy context: %w", err) } copyOpts := ©.Options{ DestinationCtx: sysCtx, } if !options.Quiet { copyOpts.ReportWriter = os.Stderr } manifestBytes, err := copy.Image(ctx, policyContext, destRef, srcRef, copyOpts) if err != nil { return fmt.Errorf("pushing source image: %w", err) } if options.DigestFile != "" { manifestDigest, err := manifest.Digest(manifestBytes) if err != nil { return fmt.Errorf("computing digest of manifest of source: %w", err) } if err = os.WriteFile(options.DigestFile, []byte(manifestDigest.String()), 0o644); err != nil { return fmt.Errorf("failed to write digest to file %q: %w", options.DigestFile, err) } } return nil } ================================================ FILE: internal/source/source.go ================================================ package source import ( "bytes" "context" "encoding/json" "fmt" "os" "path/filepath" "strings" "time" "github.com/opencontainers/go-digest" specV1 "github.com/opencontainers/image-spec/specs-go/v1" "go.podman.io/image/v5/image" "go.podman.io/image/v5/oci/layout" "go.podman.io/image/v5/types" ) // MediaTypeSourceImageConfig specifies the media type of a source-image config. const MediaTypeSourceImageConfig = "application/vnd.oci.source.image.config.v1+json" // ImageConfig specifies the config of a source image. type ImageConfig struct { // Created is the combined date and time at which the layer was created, formatted as defined by RFC 3339, section 5.6. Created *time.Time `json:"created,omitempty"` // Author is the author of the source image. Author string `json:"author,omitempty"` } // writeManifest writes the specified OCI `manifest` to the source image at // `ociDest`. func writeManifest(ctx context.Context, manifest *specV1.Manifest, ociDest types.ImageDestination) (*digest.Digest, int64, error) { rawData, err := json.Marshal(&manifest) if err != nil { return nil, -1, fmt.Errorf("marshalling manifest: %w", err) } if err := ociDest.PutManifest(ctx, rawData, nil); err != nil { return nil, -1, fmt.Errorf("writing manifest: %w", err) } manifestDigest := digest.FromBytes(rawData) return &manifestDigest, int64(len(rawData)), nil } // readManifestFromImageSource reads the manifest from the specified image // source. Note that the manifest is expected to be an OCI v1 manifest. func readManifestFromImageSource(ctx context.Context, src types.ImageSource) (*specV1.Manifest, *digest.Digest, int64, error) { rawData, mimeType, err := image.UnparsedInstance(src, nil).Manifest(ctx) if err != nil { return nil, nil, -1, err } if mimeType != specV1.MediaTypeImageManifest { return nil, nil, -1, fmt.Errorf("image %q is of type %q (expected: %q)", strings.TrimPrefix(src.Reference().StringWithinTransport(), "//"), mimeType, specV1.MediaTypeImageManifest) } manifest := specV1.Manifest{} if err := json.Unmarshal(rawData, &manifest); err != nil { return nil, nil, -1, fmt.Errorf("reading manifest: %w", err) } manifestDigest := digest.FromBytes(rawData) return &manifest, &manifestDigest, int64(len(rawData)), nil } // readManifestFromOCIPath returns the manifest of the specified source image // at `sourcePath` along with its digest. The digest can later on be used to // locate the manifest on the file system. func readManifestFromOCIPath(ctx context.Context, sourcePath string) (*specV1.Manifest, *digest.Digest, int64, error) { ociRef, err := layout.ParseReference(sourcePath) if err != nil { return nil, nil, -1, err } ociSource, err := ociRef.NewImageSource(ctx, &types.SystemContext{}) if err != nil { return nil, nil, -1, err } defer ociSource.Close() return readManifestFromImageSource(ctx, ociSource) } // openOrCreateSourceImage returns an OCI types.ImageDestination of the the // specified `sourcePath`. Note that if the path doesn't exist, it'll be // created along with the OCI directory layout. func openOrCreateSourceImage(ctx context.Context, sourcePath string) (types.ImageDestination, error) { ociRef, err := layout.ParseReference(sourcePath) if err != nil { return nil, err } // This will implicitly create an OCI directory layout at `path`. return ociRef.NewImageDestination(ctx, &types.SystemContext{}) } // addConfig adds `config` to `ociDest` and returns the corresponding blob // info. func addConfig(ctx context.Context, config *ImageConfig, ociDest types.ImageDestination) (*types.BlobInfo, error) { rawData, err := json.Marshal(config) if err != nil { return nil, fmt.Errorf("marshalling config: %w", err) } info := types.BlobInfo{ Size: -1, // "unknown": we'll get that information *after* adding } addedBlob, err := ociDest.PutBlob(ctx, bytes.NewReader(rawData), info, nil, true) if err != nil { return nil, fmt.Errorf("adding config: %w", err) } return &addedBlob, nil } // removeBlob removes the specified `blob` from the source image at `sourcePath`. func removeBlob(blob *digest.Digest, sourcePath string) error { blobPath := filepath.Join(filepath.Join(sourcePath, "blobs/sha256"), blob.Encoded()) return os.Remove(blobPath) } ================================================ FILE: internal/tmpdir/tmpdir.go ================================================ package tmpdir import ( "os" "path/filepath" "github.com/sirupsen/logrus" "go.podman.io/common/pkg/config" ) // GetTempDir returns the path of the preferred temporary directory on the host. func GetTempDir() string { if tmpdir, ok := os.LookupEnv("TMPDIR"); ok { abs, err := filepath.Abs(tmpdir) if err == nil { return abs } logrus.Warnf("ignoring TMPDIR from environment, evaluating it: %v", err) } if containerConfig, err := config.Default(); err == nil { if tmpdir, err := containerConfig.ImageCopyTmpDir(); err == nil { return tmpdir } } return "/var/tmp" } ================================================ FILE: internal/tmpdir/tmpdir_test.go ================================================ package tmpdir import ( "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.podman.io/common/pkg/config" ) func TestGetTempDir(t *testing.T) { t.Parallel() // test default err := os.Unsetenv("TMPDIR") require.NoError(t, err) err = os.Setenv("CONTAINERS_CONF", "/dev/null") require.NoError(t, err) tmpdir := GetTempDir() assert.Equal(t, "/var/tmp", tmpdir) // test TMPDIR Environment err = os.Setenv("TMPDIR", "/tmp/bogus") require.NoError(t, err) tmpdir = GetTempDir() assert.Equal(t, tmpdir, "/tmp/bogus") err = os.Unsetenv("TMPDIR") require.NoError(t, err) // relative TMPDIR should be automatically converted to absolute err = os.Setenv("TMPDIR", ".") require.NoError(t, err) tmpdir = GetTempDir() assert.True(t, filepath.IsAbs(tmpdir), "path from GetTempDir should always be absolute") err = os.Unsetenv("TMPDIR") require.NoError(t, err) f, err := os.CreateTemp("", "containers.conf-") require.NoError(t, err) // close and remove the temporary file at the end of the program defer f.Close() defer os.Remove(f.Name()) data := []byte("[engine]\nimage_copy_tmp_dir=\"/mnt\"\n") _, err = f.Write(data) require.NoError(t, err) err = os.Setenv("CONTAINERS_CONF", f.Name()) require.NoError(t, err) // force config reset of default containers.conf options := config.Options{ SetDefault: true, } _, err = config.New(&options) require.NoError(t, err) tmpdir = GetTempDir() assert.Equal(t, "/mnt", tmpdir) } ================================================ FILE: internal/types.go ================================================ package internal const ( // BuildahExternalArtifactsDir is the pattern passed to os.MkdirTemp() // to generate a temporary directory which will be used to hold // external items which are downloaded for a build, typically a tarball // being used as an additional build context. BuildahExternalArtifactsDir = "buildah-external-artifacts" // SourceDateEpochName is both the name of the SOURCE_DATE_EPOCH // environment variable and the built-in ARG that carries its value, // whether it's read from the environment by our main(), or passed in // via CLI or API flags. SourceDateEpochName = "SOURCE_DATE_EPOCH" ) // StageMountDetails holds the Stage/Image mountpoint returned by StageExecutor // StageExecutor has ability to mount stages/images in current context and // automatically clean them up. type StageMountDetails struct { DidExecute bool // true if this is a freshly-executed stage, or an image, possibly from a non-local cache IsStage bool // true if the mountpoint is a stage's rootfs IsImage bool // true if the mountpoint is an image's rootfs IsAdditionalBuildContext bool // true if the mountpoint is an additional build context MountPoint string // mountpoint of the stage or image's root directory or path of the additional build context IsWritesDiscardedOverlay bool // this is an overlay that discards writes } ================================================ FILE: internal/util/util.go ================================================ package internalutil import ( "fmt" "io" "os" "path/filepath" "github.com/containers/buildah/internal/output" v1 "github.com/opencontainers/image-spec/specs-go/v1" "go.podman.io/common/libimage" lplatform "go.podman.io/common/libimage/platform" "go.podman.io/image/v5/types" "go.podman.io/storage" "go.podman.io/storage/pkg/archive" "go.podman.io/storage/pkg/chrootarchive" "go.podman.io/storage/pkg/unshare" ) // LookupImage returns *Image to corresponding imagename or id func LookupImage(ctx *types.SystemContext, store storage.Store, image string) (*libimage.Image, error) { systemContext := ctx if systemContext == nil { systemContext = &types.SystemContext{} } runtime, err := libimage.RuntimeFromStore(store, &libimage.RuntimeOptions{SystemContext: systemContext}) if err != nil { return nil, err } localImage, _, err := runtime.LookupImage(image, nil) if err != nil { return nil, err } return localImage, nil } // NormalizePlatform validates and translate the platform to the canonical value. // // For example, if "Aarch64" is encountered, we change it to "arm64" or if // "x86_64" is encountered, it becomes "amd64". // // Wrapper around libimage.NormalizePlatform to return and consume // v1.Platform instead of independent os, arch and variant. func NormalizePlatform(platform v1.Platform) v1.Platform { os, arch, variant := lplatform.Normalize(platform.OS, platform.Architecture, platform.Variant) return v1.Platform{ OS: os, Architecture: arch, Variant: variant, } } // ExportFromReader reads bytes from given reader and exports to external tar, directory or stdout. func ExportFromReader(input io.Reader, opts output.BuildOutputOption) error { var err error // Only process path for types that require it. if opts.Type == output.BuildOutputLocalDir || opts.Type == output.BuildOutputTar { if !filepath.IsAbs(opts.Path) { if opts.Path, err = filepath.Abs(opts.Path); err != nil { return err } } } // Process output type switch opts.Type { case output.BuildOutputLocalDir: // In order to keep this feature as close as possible to // buildkit it was decided to preserve ownership when // invoked as root since caller already has access to artifacts // therefore we can preserve ownership as is, however for rootless users // ownership has to be changed so exported artifacts can still // be accessible by unprivileged users. // See: https://github.com/containers/buildah/pull/3823#discussion_r829376633 noLChown := false if unshare.IsRootless() { noLChown = true } if err = os.MkdirAll(opts.Path, 0o700); err != nil { return fmt.Errorf("failed while creating the destination path %q: %w", opts.Path, err) } if err = chrootarchive.Untar(input, opts.Path, &archive.TarOptions{NoLchown: noLChown}); err != nil { return fmt.Errorf("failed while performing untar at %q: %w", opts.Path, err) } case output.BuildOutputStdout: if _, err = io.Copy(os.Stdout, input); err != nil { return fmt.Errorf("failed while writing to stdout: %w", err) } case output.BuildOutputTar: outFile, err := os.Create(opts.Path) if err != nil { return fmt.Errorf("failed while creating destination tar at %q: %w", opts.Path, err) } defer outFile.Close() if _, err = io.Copy(outFile, input); err != nil { return fmt.Errorf("failed while performing copy to %q: %w", opts.Path, err) } default: return fmt.Errorf("unsupported output type %q", opts.Type) } return nil } func SetHas[K comparable, V any](m map[K]V, k K) bool { _, ok := m[k] return ok } ================================================ FILE: internal/util/util_test.go ================================================ package internalutil import ( "testing" "github.com/stretchr/testify/assert" ) func TestSetHas(t *testing.T) { m := map[string]string{ "key1": "ignored", } assert.True(t, SetHas(m, "key1")) assert.False(t, SetHas(m, "key2")) } ================================================ FILE: internal/volumes/bind_linux.go ================================================ package volumes import ( "errors" "fmt" "os" "github.com/containers/buildah/internal/open" "github.com/sirupsen/logrus" "go.podman.io/storage/pkg/mount" "golang.org/x/sys/unix" ) // bindFromChroot opens "path" inside of "root" using a chrooted subprocess // that returns a descriptor, then creates a uniquely-named temporary directory // or file under "tmp" and bind-mounts the opened descriptor to it, returning // the path of the temporary file or directory. The caller is responsible for // unmounting and removing the temporary. func bindFromChroot(root, path, tmp string) (string, error) { fd, _, err := open.InChroot(root, "", path, unix.O_DIRECTORY|unix.O_RDONLY, 0) if err != nil { if !errors.Is(err, unix.ENOTDIR) { return "", fmt.Errorf("opening directory %q under %q: %w", path, root, err) } fd, _, err = open.InChroot(root, "", path, unix.O_RDWR, 0) if err != nil { return "", fmt.Errorf("opening non-directory %q under %q: %w", path, root, err) } } defer func() { if err := unix.Close(fd); err != nil { logrus.Debugf("closing %q under %q: %v", path, root, err) } }() succeeded := false var dest string var destF *os.File defer func() { if !succeeded { if destF != nil { if err := destF.Close(); err != nil { logrus.Debugf("closing bind target %q: %v", dest, err) } } if dest != "" { if err := os.Remove(dest); err != nil { logrus.Debugf("removing bind target %q: %v", dest, err) } } } }() var st unix.Stat_t if err = unix.Fstat(fd, &st); err != nil { return "", fmt.Errorf("checking if %q under %q was a directory: %w", path, root, err) } if st.Mode&unix.S_IFDIR == unix.S_IFDIR { if dest, err = os.MkdirTemp(tmp, "bind"); err != nil { return "", fmt.Errorf("creating a bind target directory: %w", err) } } else { if destF, err = os.CreateTemp(tmp, "bind"); err != nil { return "", fmt.Errorf("creating a bind target non-directory: %w", err) } if err := destF.Close(); err != nil { logrus.Debugf("closing bind target %q: %v", dest, err) } dest = destF.Name() } defer func() { if !succeeded { if err := os.Remove(dest); err != nil { logrus.Debugf("removing bind target %q: %v", dest, err) } } }() if err := unix.Mount(fmt.Sprintf("/proc/self/fd/%d", fd), dest, "bind", unix.MS_BIND, ""); err != nil { return "", fmt.Errorf("bind-mounting passed-in descriptor to %q: %w", dest, err) } defer func() { if !succeeded { if err := mount.Unmount(dest); err != nil { logrus.Debugf("unmounting bound target %q: %v", dest, err) } } }() var st2 unix.Stat_t if err = unix.Stat(dest, &st2); err != nil { return "", fmt.Errorf("looking up device/inode of newly-bind-mounted %q: %w", dest, err) } if st2.Dev != st.Dev || st2.Ino != st.Ino { return "", fmt.Errorf("device/inode weren't what we expected after bind mounting: %w", err) } succeeded = true return dest, nil } ================================================ FILE: internal/volumes/bind_linux_test.go ================================================ package volumes import ( "os" "path/filepath" "testing" "github.com/stretchr/testify/require" "go.podman.io/storage/pkg/mount" ) func TestBindFromChroot(t *testing.T) { t.Parallel() if os.Getuid() != 0 { t.Skip("not running as root, assuming we can't mount or chroot") } contents1 := "file1" contents2 := "file2" rootdir := t.TempDir() destdir := t.TempDir() require.NoError(t, os.Mkdir(filepath.Join(rootdir, "subdirectory"), 0o700), "creating bind mount source directory") require.NoError(t, os.WriteFile(filepath.Join(rootdir, "subdirectory", "file"), []byte(contents1), 0o600)) require.NoError(t, os.WriteFile(filepath.Join(rootdir, "file"), []byte(contents2), 0o600)) subdir, err := bindFromChroot(rootdir, "subdirectory", destdir) require.NoError(t, err, "bind mounting from a directory") bytes1, err := os.ReadFile(filepath.Join(subdir, "file")) require.NoError(t, err, "reading file from bind-mounted directory") subfile, err := bindFromChroot(rootdir, "file", destdir) require.NoError(t, err, "bind mounting from a file") bytes2, err := os.ReadFile(subfile) require.NoError(t, err, "reading file from bind mounted file") require.Equal(t, contents1, string(bytes1), "contents of file in bind-mounted directory") require.Equal(t, contents2, string(bytes2), "contents of bind-mounted file") require.NoError(t, mount.Unmount(subdir), "unmounting bind-mounted directory") require.NoError(t, mount.Unmount(subfile), "unmounting bind-mounted file") } ================================================ FILE: internal/volumes/bind_notlinux.go ================================================ //go:build !linux package volumes import "errors" // bindFromChroot would open "path" inside of "root" using a chrooted // subprocess that returns a descriptor, then would create a uniquely-named // temporary directory or file under "tmp" and bind-mount the opened descriptor // to it, returning the path of the temporary file or directory. The caller // would be responsible for unmounting and removing the temporary. For now, // this just returns an error because it is not implemented for this platform. func bindFromChroot(root, path, tmp string) (string, error) { return "", errors.New("not available on this system") } ================================================ FILE: internal/volumes/bind_test.go ================================================ package volumes import ( "os" "testing" "go.podman.io/storage/pkg/reexec" ) func TestMain(m *testing.M) { if reexec.Init() { return } os.Exit(m.Run()) } ================================================ FILE: internal/volumes/volumes.go ================================================ package volumes import ( "context" "errors" "fmt" "os" "path" "path/filepath" "slices" "strconv" "strings" "github.com/containers/buildah/copier" "github.com/containers/buildah/define" "github.com/containers/buildah/internal" internalParse "github.com/containers/buildah/internal/parsevolume" "github.com/containers/buildah/internal/tmpdir" internalUtil "github.com/containers/buildah/internal/util" "github.com/containers/buildah/pkg/overlay" "github.com/containers/buildah/util" digest "github.com/opencontainers/go-digest" specs "github.com/opencontainers/runtime-spec/specs-go" selinux "github.com/opencontainers/selinux/go-selinux" "github.com/sirupsen/logrus" "go.podman.io/common/pkg/parse" "go.podman.io/image/v5/types" "go.podman.io/storage" "go.podman.io/storage/pkg/idtools" "go.podman.io/storage/pkg/lockfile" "go.podman.io/storage/pkg/mount" "go.podman.io/storage/pkg/unshare" ) const ( // TypeTmpfs is the type for mounting tmpfs TypeTmpfs = "tmpfs" // TypeCache is the type for mounting a common persistent cache from host TypeCache = "cache" // mount=type=cache must create a persistent directory on host so its available for all consecutive builds. // Lifecycle of following directory will be inherited from how host machine treats temporary directory buildahCacheDir = "buildah-cache" // mount=type=cache allows users to lock a cache store while its being used by another build BuildahCacheLockfile = "buildah-cache-lockfile" // All the lockfiles are stored in a separate directory inside `BuildahCacheDir` // Example `/var/tmp/buildah-cache//buildah-cache-lockfile` BuildahCacheLockfileDir = "buildah-cache-lockfiles" ) var ( errBadMntOption = errors.New("invalid mount option") errBadOptionArg = errors.New("must provide an argument for option") errBadOptionNoArg = errors.New("must not provide an argument for option") errBadVolDest = errors.New("must set volume destination") errBadVolSrc = errors.New("must set volume source") errDuplicateDest = errors.New("duplicate mount destination") ) // CacheParent returns a cache parent for --mount=type=cache func CacheParent() string { return filepath.Join(tmpdir.GetTempDir(), buildahCacheDir+"-"+strconv.Itoa(unshare.GetRootlessUID())) } func mountIsReadWrite(m specs.Mount) bool { // in case of conflicts, the last one wins, so it's not enough // to check for the presence of either "rw" or "ro" anywhere // with e.g. slices.Contains() rw := true for _, option := range m.Options { switch option { case "rw": rw = true case "ro": rw = false } } return rw } func convertToOverlay(m specs.Mount, store storage.Store, mountLabel, tmpDir string, uid, gid int) (specs.Mount, string, error) { overlayDir, err := overlay.TempDir(tmpDir, uid, gid) if err != nil { return specs.Mount{}, "", fmt.Errorf("setting up overlay for %q: %w", m.Destination, err) } options := overlay.Options{GraphOpts: slices.Clone(store.GraphOptions()), ForceMount: true, MountLabel: mountLabel} fileInfo, err := os.Stat(m.Source) if err != nil { return specs.Mount{}, "", fmt.Errorf("setting up overlay of %q: %w", m.Source, err) } // we might be trying to "overlay" for a non-directory, and the kernel doesn't like that very much var mountThisInstead specs.Mount if fileInfo.IsDir() { // do the normal thing of mounting this directory as a lower with a temporary upper mountThisInstead, err = overlay.MountWithOptions(overlayDir, m.Source, m.Destination, &options) if err != nil { return specs.Mount{}, "", fmt.Errorf("setting up overlay of %q: %w", m.Source, err) } } else { // mount the parent directory as the lower with a temporary upper, and return a // bind mount from the non-directory in the merged directory to the destination sourceDir := filepath.Dir(m.Source) sourceBase := filepath.Base(m.Source) destination := m.Destination mountedOverlay, err := overlay.MountWithOptions(overlayDir, sourceDir, destination, &options) if err != nil { return specs.Mount{}, "", fmt.Errorf("setting up overlay of %q: %w", sourceDir, err) } if mountedOverlay.Type != define.TypeBind { if err2 := overlay.RemoveTemp(overlayDir); err2 != nil { return specs.Mount{}, "", fmt.Errorf("cleaning up after failing to set up overlay: %v, while setting up overlay for %q: %w", err2, destination, err) } return specs.Mount{}, "", fmt.Errorf("setting up overlay for %q at %q: %w", mountedOverlay.Source, destination, err) } mountThisInstead = mountedOverlay mountThisInstead.Source = filepath.Join(mountedOverlay.Source, sourceBase) mountThisInstead.Destination = destination } return mountThisInstead, overlayDir, nil } // FIXME: this code needs to be merged with pkg/parse/parse.go ValidateVolumeOpts // // GetBindMount parses a single bind mount entry from the --mount flag. // // Returns a Mount to add to the runtime spec's list of mounts, the ID of the // image we mounted if we mounted one, the path of a mounted location if one // needs to be unmounted and removed, and the path of an overlay mount if one // needs to be cleaned up, or an error. // // The caller is expected to, after the command which uses the mount exits, // clean up the overlay filesystem (if we provided a path to it), unmount and // remove the mountpoint for the mounted filesystem (if we provided the path to // its mountpoint), and then unmount the image (if we mounted one). func GetBindMount(sys *types.SystemContext, args []string, contextDir string, store storage.Store, mountLabel string, additionalMountPoints map[string]internal.StageMountDetails, workDir, tmpDir string) (specs.Mount, string, string, string, error) { newMount := specs.Mount{ Type: define.TypeBind, } setRelabel := "" mountReadability := "" setDest := "" bindNonRecursive := false fromWhere := "" skipOverlay := false for _, val := range args { argName, argValue, hasArgValue := strings.Cut(val, "=") switch argName { case "type": // This is already processed, and should be "bind" continue case "bind-nonrecursive": if hasArgValue { return newMount, "", "", "", fmt.Errorf("%v: %w", argName, errBadOptionNoArg) } newMount.Options = append(newMount.Options, "bind") bindNonRecursive = true case "nosuid", "nodev", "noexec": if hasArgValue { return newMount, "", "", "", fmt.Errorf("%v: %w", argName, errBadOptionNoArg) } newMount.Options = append(newMount.Options, argName) case "rw", "readwrite": if hasArgValue { return newMount, "", "", "", fmt.Errorf("%v: %w", argName, errBadOptionNoArg) } newMount.Options = append(newMount.Options, "rw") mountReadability = "rw" case "ro", "readonly": if hasArgValue { return newMount, "", "", "", fmt.Errorf("%v: %w", argName, errBadOptionNoArg) } newMount.Options = append(newMount.Options, "ro") mountReadability = "ro" case "shared", "rshared", "private", "rprivate", "slave", "rslave", "Z", "z", "U", "no-dereference": if hasArgValue { return newMount, "", "", "", fmt.Errorf("%v: %w", val, errBadOptionNoArg) } newMount.Options = append(newMount.Options, argName) case "from": if !hasArgValue || argValue == "" { return newMount, "", "", "", fmt.Errorf("%v: %w", argName, errBadOptionArg) } fromWhere = argValue case "bind-propagation": if !hasArgValue || argValue == "" { return newMount, "", "", "", fmt.Errorf("%v: %w", argName, errBadOptionArg) } switch argValue { default: return newMount, "", "", "", fmt.Errorf("%v: %q: %w", argName, argValue, errBadMntOption) case "shared", "rshared", "private", "rprivate", "slave", "rslave": // this should be the relevant parts of the same list of options we accepted above } newMount.Options = append(newMount.Options, argValue) case "src", "source": if !hasArgValue || argValue == "" { return newMount, "", "", "", fmt.Errorf("%v: %w", argName, errBadOptionArg) } newMount.Source = argValue case "target", "dst", "destination": if !hasArgValue || argValue == "" { return newMount, "", "", "", fmt.Errorf("%v: %w", argName, errBadOptionArg) } targetPath := argValue setDest = targetPath if !path.IsAbs(targetPath) { targetPath = filepath.Join(workDir, targetPath) } if err := parse.ValidateVolumeCtrDir(targetPath); err != nil { return newMount, "", "", "", err } newMount.Destination = targetPath case "relabel": if !hasArgValue || argValue == "" { return newMount, "", "", "", fmt.Errorf("%v: %w", argName, errBadOptionArg) } if setRelabel != "" { return newMount, "", "", "", fmt.Errorf("cannot pass 'relabel' option more than once: %w", errBadOptionArg) } setRelabel = argValue switch argValue { case "private": newMount.Options = append(newMount.Options, "Z") case "shared": newMount.Options = append(newMount.Options, "z") default: return newMount, "", "", "", fmt.Errorf("%s mount option must be 'private' or 'shared': %w", argName, errBadMntOption) } case "consistency": // Option for OS X only, has no meaning on other platforms // and can thus be safely ignored. // See also the handling of the equivalent "delegated" and "cached" in ValidateVolumeOpts default: return newMount, "", "", "", fmt.Errorf("%v: %w", argName, errBadMntOption) } } // default mount readability is always readonly if mountReadability == "" { newMount.Options = append(newMount.Options, "ro") } // Following variable ensures that we return imagename only if we did additional mount succeeded := false mountedImage := "" if fromWhere != "" { mountPoint := "" if additionalMountPoints != nil { if val, ok := additionalMountPoints[fromWhere]; ok { mountPoint = val.MountPoint skipOverlay = val.IsWritesDiscardedOverlay } } // if mountPoint of image was not found in additionalMap // or additionalMap was nil, try mounting image if mountPoint == "" { image, err := internalUtil.LookupImage(sys, store, fromWhere) if err != nil { return newMount, "", "", "", err } mountPoint, err = image.Mount(context.Background(), nil, mountLabel) if err != nil { return newMount, "", "", "", err } mountedImage = image.ID() // unmount the image if we don't end up returning successfully defer func() { if !succeeded { if _, err := store.UnmountImage(mountedImage, false); err != nil { logrus.Debugf("unmounting bind-mounted image %q: %v", fromWhere, err) } } }() } contextDir = mountPoint } else { // special case an additional mount point for "" as shorthand for "preferred location of the default build context" if additionalMountPoints != nil { if val, ok := additionalMountPoints[""]; ok { contextDir = val.MountPoint skipOverlay = val.IsWritesDiscardedOverlay } } } // buildkit parity: default bind option must be `rbind` // unless specified if !bindNonRecursive { newMount.Options = append(newMount.Options, "rbind") } if setDest == "" { return newMount, "", "", "", errBadVolDest } // buildkit parity: support absolute path for sources from current build context if contextDir != "" { // path should be /contextDir/specified path evaluated, err := copier.Eval(contextDir, contextDir+string(filepath.Separator)+newMount.Source, copier.EvalOptions{}) if err != nil { return newMount, "", "", "", err } newMount.Source = evaluated } else { // looks like its coming from `build run --mount=type=bind` allow using absolute path // error out if no source is set if newMount.Source == "" { return newMount, "", "", "", errBadVolSrc } if err := parse.ValidateVolumeHostDir(newMount.Source); err != nil { return newMount, "", "", "", err } } opts, err := parse.ValidateVolumeOpts(newMount.Options) if err != nil { return newMount, "", "", "", err } newMount.Options = opts var intermediateMount string if contextDir != "" && newMount.Source != contextDir { rel, err := filepath.Rel(contextDir, newMount.Source) if err != nil { return newMount, "", "", "", fmt.Errorf("computing pathname of bind subdirectory: %w", err) } if rel != "." && rel != "/" { mnt, err := bindFromChroot(contextDir, rel, tmpDir) if err != nil { return newMount, "", "", "", fmt.Errorf("sanitizing bind subdirectory %q: %w", newMount.Source, err) } logrus.Debugf("bind-mounted %q under %q to %q", rel, contextDir, mnt) intermediateMount = mnt newMount.Source = intermediateMount } } overlayDir := "" if !skipOverlay && (mountedImage != "" || mountIsReadWrite(newMount)) { if newMount, overlayDir, err = convertToOverlay(newMount, store, mountLabel, tmpDir, 0, 0); err != nil { return newMount, "", "", "", err } } succeeded = true return newMount, mountedImage, intermediateMount, overlayDir, nil } // GetCacheMount parses a single cache mount entry from the --mount flag. // // Returns a Mount to add to the runtime spec's list of mounts, the ID of the // image we mounted if we mounted one, the path of a mounted filesystem if one // needs to be unmounted, the path of an overlay if one needs to be cleaned up, // and an optional lock that needs to be released, or an error. // // The caller is expected to, after the command which uses the mount exits, // clean up the overlay filesystem (if we provided the path of one), unmount // and remove the mountpoint of the mounted filesystem (if we provided the path // to its mountpoint), unmount the image (if we mounted one), and release the // lock (if we took one). func GetCacheMount(sys *types.SystemContext, args []string, store storage.Store, mountLabel string, additionalMountPoints map[string]internal.StageMountDetails, uidmap, gidmap []specs.LinuxIDMapping, workDir, tmpDir string) (specs.Mount, string, string, string, *lockfile.LockFile, error) { var err error var mode uint64 var buildahLockFilesDir string var setShared bool setDest := "" setRelabel := "" setReadOnly := "" fromWhere := "" newMount := specs.Mount{ Type: define.TypeBind, } // if id is set a new subdirectory with `id` will be created under /host-temp/buildah-build-cache/id id := "" // buildkit parity: cache directory defaults to 0o755 mode = 0o755 // buildkit parity: cache directory defaults to uid 0 if not specified uid := uint64(0) // buildkit parity: cache directory defaults to gid 0 if not specified gid := uint64(0) // sharing mode sharing := "shared" for _, val := range args { argName, argValue, hasArgValue := strings.Cut(val, "=") switch argName { case "type": // This is already processed, and should be "cache" continue case "nosuid", "nodev", "noexec", "U": if hasArgValue { return newMount, "", "", "", nil, fmt.Errorf("%v: %w", argName, errBadOptionNoArg) } newMount.Options = append(newMount.Options, argName) case "rw", "readwrite": if hasArgValue { return newMount, "", "", "", nil, fmt.Errorf("%v: %w", argName, errBadOptionNoArg) } newMount.Options = append(newMount.Options, "rw") setReadOnly = "rw" case "readonly", "ro": if hasArgValue { return newMount, "", "", "", nil, fmt.Errorf("%v: %w", argName, errBadOptionNoArg) } newMount.Options = append(newMount.Options, "ro") setReadOnly = "ro" case "Z", "z": if hasArgValue { return newMount, "", "", "", nil, fmt.Errorf("%v: %w", argName, errBadOptionNoArg) } newMount.Options = append(newMount.Options, argName) setRelabel = argName case "shared", "rshared", "private", "rprivate", "slave", "rslave": if hasArgValue { return newMount, "", "", "", nil, fmt.Errorf("%v: %w", argName, errBadOptionNoArg) } newMount.Options = append(newMount.Options, argName) setShared = true case "sharing": if !hasArgValue || argValue == "" { return newMount, "", "", "", nil, fmt.Errorf("%v: %w", argName, errBadOptionArg) } sharing = argValue case "bind-propagation": if !hasArgValue || argValue == "" { return newMount, "", "", "", nil, fmt.Errorf("%v: %w", argName, errBadOptionArg) } switch argValue { default: return newMount, "", "", "", nil, fmt.Errorf("%v: %q: %w", argName, argValue, errBadMntOption) case "shared", "rshared", "private", "rprivate", "slave", "rslave": // this should be the relevant parts of the same list of options we accepted above } newMount.Options = append(newMount.Options, argValue) setShared = true case "id": if !hasArgValue || argValue == "" { return newMount, "", "", "", nil, fmt.Errorf("%v: %w", argName, errBadOptionArg) } id = argValue case "from": if !hasArgValue || argValue == "" { return newMount, "", "", "", nil, fmt.Errorf("%v: %w", argName, errBadOptionArg) } fromWhere = argValue case "target", "dst", "destination": if !hasArgValue || argValue == "" { return newMount, "", "", "", nil, fmt.Errorf("%v: %w", argName, errBadOptionArg) } targetPath := argValue if !path.IsAbs(targetPath) { targetPath = filepath.Join(workDir, targetPath) } if err := parse.ValidateVolumeCtrDir(targetPath); err != nil { return newMount, "", "", "", nil, err } newMount.Destination = targetPath setDest = targetPath case "src", "source": if !hasArgValue || argValue == "" { return newMount, "", "", "", nil, fmt.Errorf("%v: %w", argName, errBadOptionArg) } newMount.Source = argValue case "mode": if !hasArgValue || argValue == "" { return newMount, "", "", "", nil, fmt.Errorf("%v: %w", argName, errBadOptionArg) } mode, err = strconv.ParseUint(argValue, 8, 32) if err != nil { return newMount, "", "", "", nil, fmt.Errorf("unable to parse cache mode %q: %w", argValue, err) } case "uid": if !hasArgValue || argValue == "" { return newMount, "", "", "", nil, fmt.Errorf("%v: %w", argName, errBadOptionArg) } uid, err = strconv.ParseUint(argValue, 10, 32) if err != nil { return newMount, "", "", "", nil, fmt.Errorf("unable to parse cache uid %q: %w", argValue, err) } case "gid": if !hasArgValue || argValue == "" { return newMount, "", "", "", nil, fmt.Errorf("%v: %w", argName, errBadOptionArg) } gid, err = strconv.ParseUint(argValue, 10, 32) if err != nil { return newMount, "", "", "", nil, fmt.Errorf("unable to parse cache gid %q: %w", argValue, err) } default: return newMount, "", "", "", nil, fmt.Errorf("%v: %w", argName, errBadMntOption) } } // If selinux is enabled and no selinux option was configured // default to `z` i.e shared content label. if setRelabel == "" && (selinux.EnforceMode() != selinux.Disabled) && fromWhere == "" { newMount.Options = append(newMount.Options, "z") } if setDest == "" { return newMount, "", "", "", nil, errBadVolDest } hostUID, hostGID, err := util.GetHostIDs(uidmap, gidmap, uint32(uid), uint32(gid)) if err != nil { return newMount, "", "", "", nil, err } succeeded := false needToOverlay := false mountedImage := "" thisCacheRoot := "" if fromWhere != "" { // do not create and use a cache directory on the host, // instead use the location in the mounted stage or // temporary directory as the cache mountPoint := "" if additionalMountPoints != nil { if val, ok := additionalMountPoints[fromWhere]; ok { mountPoint = val.MountPoint needToOverlay = val.IsImage } } // it's not an additional build context, stage, or // already-mounted image, but it might still be an image if mountPoint == "" { image, err := internalUtil.LookupImage(sys, store, fromWhere) if err != nil { return newMount, "", "", "", nil, err } mountPoint, err = image.Mount(context.Background(), nil, mountLabel) if err != nil { return newMount, "", "", "", nil, err } // unmount the image if we don't end up returning successfully mountedImage = image.ID() defer func() { if !succeeded { if _, err := store.UnmountImage(mountedImage, false); err != nil { logrus.Debugf("unmounting image %q: %v", fromWhere, err) } } }() needToOverlay = true } thisCacheRoot = mountPoint // decide where the lock file for this cache's root should go, if we need one cacheParent := CacheParent() mountPointID := digest.FromString(mountPoint).Encoded()[:16] buildahLockFilesDir = filepath.Join(cacheParent, BuildahCacheLockfileDir, mountPointID) } else { // we need to create the cache directory on the host if no stage is being used // since type is cache and a cache can be reused by consecutive builds // create a common cache directory, which persists on hosts within temp lifecycle // add subdirectory if specified // cache parent directory: creates separate cache parent for each user. cacheParent := CacheParent() // create cache on host if not present err = os.MkdirAll(cacheParent, os.FileMode(0o755)) if err != nil { return newMount, "", "", "", nil, fmt.Errorf("unable to create build cache directory: %w", err) } ownerInfo := fmt.Sprintf(":%d:%d", uid, gid) if id != "" { // Don't let the user try to inject pathname components by directly using // the ID when constructing the cache directory location; distinguish // between caches by ID and ownership dirID := digest.FromString(id + ownerInfo).Encoded()[:16] thisCacheRoot = filepath.Join(cacheParent, dirID) buildahLockFilesDir = filepath.Join(cacheParent, BuildahCacheLockfileDir, dirID) } else { // Don't let the user try to inject pathname components by directly using // the target path when constructing the cache directory location; // distinguish between caches by mount target location and ownership dirID := digest.FromString(newMount.Destination + ownerInfo).Encoded()[:16] thisCacheRoot = filepath.Join(cacheParent, dirID) buildahLockFilesDir = filepath.Join(cacheParent, BuildahCacheLockfileDir, dirID) } idPair := idtools.IDPair{ UID: int(hostUID), GID: int(hostGID), } // buildkit parity: change uid and gid if specified, otherwise keep `0` err = idtools.MkdirAllAndChownNew(thisCacheRoot, os.FileMode(mode), idPair) if err != nil { return newMount, "", "", "", nil, fmt.Errorf("unable to change uid,gid of cache directory: %w", err) } } // path should be /mountPoint/specified path evaluated, err := copier.Eval(thisCacheRoot, thisCacheRoot+string(filepath.Separator)+newMount.Source, copier.EvalOptions{}) if err != nil { return newMount, "", "", "", nil, err } newMount.Source = evaluated var targetLock *lockfile.LockFile switch sharing { case "locked": // create cache parent directories on host if not already present err = os.MkdirAll(buildahLockFilesDir, os.FileMode(0o755)) if err != nil { return newMount, "", "", "", nil, fmt.Errorf("unable to create build cache directory: %w", err) } // lock parent cache lockfile, err := lockfile.GetLockFile(filepath.Join(buildahLockFilesDir, BuildahCacheLockfile)) if err != nil { return newMount, "", "", "", nil, fmt.Errorf("unable to acquire lock when sharing mode is locked: %w", err) } // will be unlocked after the RUN step is executed lockfile.Lock() targetLock = lockfile defer func() { if !succeeded { targetLock.Unlock() } }() case "shared": // do nothing since default is `shared` break default: // error out for unknown values return newMount, "", "", "", nil, fmt.Errorf("unrecognized value %q for field `sharing`: %w", sharing, err) } // buildkit parity: default sharing should be shared // unless specified if !setShared { newMount.Options = append(newMount.Options, "shared") } // buildkit parity: cache must be writable unless `ro` or `readonly` is configured explicitly if setReadOnly == "" { newMount.Options = append(newMount.Options, "rw") } newMount.Options = append(newMount.Options, "bind") opts, err := parse.ValidateVolumeOpts(newMount.Options) if err != nil { return newMount, "", "", "", nil, err } newMount.Options = opts var intermediateMount string if newMount.Source != thisCacheRoot { rel, err := filepath.Rel(thisCacheRoot, newMount.Source) if err != nil { return newMount, "", "", "", nil, fmt.Errorf("computing pathname of cache subdirectory: %w", err) } if rel != "." && rel != "/" { mnt, err := bindFromChroot(thisCacheRoot, rel, tmpDir) if err != nil { return newMount, "", "", "", nil, fmt.Errorf("sanitizing cache subdirectory %q: %w", newMount.Source, err) } logrus.Debugf("bind-mounted %q under %q to %q", rel, thisCacheRoot, mnt) intermediateMount = mnt newMount.Source = intermediateMount } } overlayDir := "" if needToOverlay { if newMount, overlayDir, err = convertToOverlay(newMount, store, mountLabel, tmpDir, 0, 0); err != nil { return newMount, "", "", "", nil, err } } succeeded = true return newMount, mountedImage, intermediateMount, overlayDir, targetLock, nil } func getVolumeMounts(volumes []string) (map[string]specs.Mount, error) { finalVolumeMounts := make(map[string]specs.Mount) for _, volume := range volumes { volumeMount, err := internalParse.Volume(volume) if err != nil { return nil, err } if _, ok := finalVolumeMounts[volumeMount.Destination]; ok { return nil, fmt.Errorf("%v: %w", volumeMount.Destination, errDuplicateDest) } finalVolumeMounts[volumeMount.Destination] = volumeMount } return finalVolumeMounts, nil } // UnlockLockArray is a helper for cleaning up after GetVolumes and the like. func UnlockLockArray(locks []*lockfile.LockFile) { for _, lock := range locks { lock.Unlock() } } // GetVolumes gets the volumes from --volume and --mount flags. // // Returns a slice of Mounts to add to the runtime spec's list of mounts, the // IDs of any images we mounted, a slice of bind-mounted paths, a slice of // overlay directories and a slice of locks that we acquired, or an error. // // The caller is expected to, after the command which uses the mounts and // volumes exits, clean up the overlay directories, unmount and remove the // mountpoints for the bind-mounted paths, unmount any images we mounted, and // release the locks we returned (either using UnlockLockArray() or by // iterating over them and unlocking them). func GetVolumes(ctx *types.SystemContext, store storage.Store, mountLabel string, volumes []string, mounts []string, contextDir string, idMaps define.IDMappingOptions, workDir, tmpDir string) ([]specs.Mount, []string, []string, []string, []*lockfile.LockFile, error) { unifiedMounts, mountedImages, intermediateMounts, overlayMounts, targetLocks, err := getMounts(ctx, store, mountLabel, mounts, contextDir, idMaps.UIDMap, idMaps.GIDMap, workDir, tmpDir) if err != nil { return nil, nil, nil, nil, nil, err } succeeded := false defer func() { if !succeeded { for _, overlayMount := range overlayMounts { if err := overlay.RemoveTemp(overlayMount); err != nil { logrus.Debugf("unmounting overlay at %q: %v", overlayMount, err) } } for _, intermediateMount := range intermediateMounts { if err := mount.Unmount(intermediateMount); err != nil { logrus.Debugf("unmounting intermediate mount point %q: %v", intermediateMount, err) } if err := os.Remove(intermediateMount); err != nil { logrus.Debugf("removing should-be-empty directory %q: %v", intermediateMount, err) } } for _, image := range mountedImages { if _, err := store.UnmountImage(image, false); err != nil { logrus.Debugf("unmounting image %q: %v", image, err) } } UnlockLockArray(targetLocks) } }() volumeMounts, err := getVolumeMounts(volumes) if err != nil { return nil, nil, nil, nil, nil, err } for dest, mount := range volumeMounts { if _, ok := unifiedMounts[dest]; ok { return nil, nil, nil, nil, nil, fmt.Errorf("%v: %w", dest, errDuplicateDest) } unifiedMounts[dest] = mount } finalMounts := make([]specs.Mount, 0, len(unifiedMounts)) for _, mount := range unifiedMounts { finalMounts = append(finalMounts, mount) } succeeded = true return finalMounts, mountedImages, intermediateMounts, overlayMounts, targetLocks, nil } // getMounts takes user-provided inputs from the --mount flag and returns a // slice of OCI spec mounts, a slice of mounted image IDs, a slice of other // mount locations, a slice of overlay mounts, and a slice of locks, or an // error. // // buildah run --mount type=bind,src=/etc/resolv.conf,target=/etc/resolv.conf ... // buildah run --mount type=cache,target=/var/cache ... // buildah run --mount type=tmpfs,target=/dev/shm ... // // The caller is expected to, after the command which uses the mounts exits, // unmount the overlay filesystems (if we mounted any), unmount the other // mounted filesystems and remove their mountpoints (if we provided any paths // to mountpoints), unmount any mounted images (if we provided the IDs of any), // and then unlock the locks we returned (either using UnlockLockArray() or by // iterating over them and unlocking them). func getMounts(ctx *types.SystemContext, store storage.Store, mountLabel string, mounts []string, contextDir string, uidmap, gidmap []specs.LinuxIDMapping, workDir, tmpDir string) (map[string]specs.Mount, []string, []string, []string, []*lockfile.LockFile, error) { // If `type` is not set default to "bind" mountType := define.TypeBind finalMounts := make(map[string]specs.Mount, len(mounts)) mountedImages := make([]string, 0, len(mounts)) intermediateMounts := make([]string, 0, len(mounts)) overlayMounts := make([]string, 0, len(mounts)) targetLocks := make([]*lockfile.LockFile, 0, len(mounts)) succeeded := false defer func() { if !succeeded { for _, overlayDir := range overlayMounts { if err := overlay.RemoveTemp(overlayDir); err != nil { logrus.Debugf("unmounting overlay mount at %q: %v", overlayDir, err) } } for _, intermediateMount := range intermediateMounts { if err := mount.Unmount(intermediateMount); err != nil { logrus.Debugf("unmounting intermediate mount point %q: %v", intermediateMount, err) } if err := os.Remove(intermediateMount); err != nil { logrus.Debugf("removing should-be-empty directory %q: %v", intermediateMount, err) } } for _, image := range mountedImages { if _, err := store.UnmountImage(image, false); err != nil { logrus.Debugf("unmounting image %q: %v", image, err) } } UnlockLockArray(targetLocks) } }() errInvalidSyntax := errors.New("incorrect mount format: should be --mount type=,[src=,]target=[,options]") for _, mount := range mounts { tokens := strings.Split(mount, ",") if len(tokens) < 2 { return nil, nil, nil, nil, nil, fmt.Errorf("%q: %w", mount, errInvalidSyntax) } for _, field := range tokens { if strings.HasPrefix(field, "type=") { kv := strings.Split(field, "=") if len(kv) != 2 { return nil, nil, nil, nil, nil, fmt.Errorf("%q: %w", mount, errInvalidSyntax) } mountType = kv[1] } } switch mountType { case define.TypeBind: mount, image, intermediateMount, overlayMount, err := GetBindMount(ctx, tokens, contextDir, store, mountLabel, nil, workDir, tmpDir) if err != nil { return nil, nil, nil, nil, nil, err } if image != "" { mountedImages = append(mountedImages, image) } if intermediateMount != "" { intermediateMounts = append(intermediateMounts, intermediateMount) } if overlayMount != "" { overlayMounts = append(overlayMounts, overlayMount) } if _, ok := finalMounts[mount.Destination]; ok { return nil, nil, nil, nil, nil, fmt.Errorf("%v: %w", mount.Destination, errDuplicateDest) } finalMounts[mount.Destination] = mount case TypeCache: mount, image, intermediateMount, overlayMount, tl, err := GetCacheMount(ctx, tokens, store, "", nil, uidmap, gidmap, workDir, tmpDir) if err != nil { return nil, nil, nil, nil, nil, err } if image != "" { mountedImages = append(mountedImages, image) } if intermediateMount != "" { intermediateMounts = append(intermediateMounts, intermediateMount) } if overlayMount != "" { overlayMounts = append(overlayMounts, overlayMount) } if tl != nil { targetLocks = append(targetLocks, tl) } if _, ok := finalMounts[mount.Destination]; ok { return nil, nil, nil, nil, nil, fmt.Errorf("%v: %w", mount.Destination, errDuplicateDest) } finalMounts[mount.Destination] = mount case TypeTmpfs: mount, err := GetTmpfsMount(tokens, workDir) if err != nil { return nil, nil, nil, nil, nil, err } if _, ok := finalMounts[mount.Destination]; ok { return nil, nil, nil, nil, nil, fmt.Errorf("%v: %w", mount.Destination, errDuplicateDest) } finalMounts[mount.Destination] = mount default: return nil, nil, nil, nil, nil, fmt.Errorf("invalid filesystem type %q", mountType) } } succeeded = true return finalMounts, mountedImages, intermediateMounts, overlayMounts, targetLocks, nil } // GetTmpfsMount parses a single tmpfs mount entry from the --mount flag func GetTmpfsMount(args []string, workDir string) (specs.Mount, error) { newMount := specs.Mount{ Type: TypeTmpfs, Source: TypeTmpfs, } setDest := false for _, val := range args { argName, argValue, hasArgValue := strings.Cut(val, "=") switch argName { case "type": // This is already processed, and should be "tmpfs" continue case "nosuid", "nodev", "noexec": if hasArgValue { return newMount, fmt.Errorf("%v: %w", argName, errBadOptionNoArg) } newMount.Options = append(newMount.Options, argName) case "ro", "readonly": if hasArgValue { return newMount, fmt.Errorf("%v: %w", argName, errBadOptionNoArg) } newMount.Options = append(newMount.Options, "ro") case "tmpcopyup": if hasArgValue { return newMount, fmt.Errorf("%v: %w", argName, errBadOptionNoArg) } // the path that is shadowed by the tmpfs mount is recursively copied up to the tmpfs itself. newMount.Options = append(newMount.Options, argName) case "tmpfs-mode": if !hasArgValue || argValue == "" { return newMount, fmt.Errorf("%v: %w", argName, errBadOptionArg) } newMount.Options = append(newMount.Options, fmt.Sprintf("mode=%s", argValue)) case "tmpfs-size": if !hasArgValue || argValue == "" { return newMount, fmt.Errorf("%v: %w", argName, errBadOptionArg) } newMount.Options = append(newMount.Options, fmt.Sprintf("size=%s", argValue)) case "target", "dst", "destination": if !hasArgValue || argValue == "" { return newMount, fmt.Errorf("%v: %w", argName, errBadOptionArg) } targetPath := argValue if !path.IsAbs(targetPath) { targetPath = filepath.Join(workDir, targetPath) } if err := parse.ValidateVolumeCtrDir(targetPath); err != nil { return newMount, err } newMount.Destination = targetPath setDest = true default: return newMount, fmt.Errorf("%v: %w", argName, errBadMntOption) } } if !setDest { return newMount, errBadVolDest } return newMount, nil } ================================================ FILE: internal/volumes/volumes_test.go ================================================ package volumes import ( "testing" specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.podman.io/image/v5/types" "go.podman.io/storage" storageTypes "go.podman.io/storage/types" ) func TestGetMount(t *testing.T) { t.Parallel() tempDir := t.TempDir() rootDir := t.TempDir() runDir := t.TempDir() sys := &types.SystemContext{} var emptyIDmap []specs.LinuxIDMapping store, err := storage.GetStore(storageTypes.StoreOptions{ GraphDriverName: "vfs", GraphRoot: rootDir, RunRoot: runDir, }) require.NoError(t, err) t.Cleanup(func() { if _, err := store.Shutdown(true); err != nil { t.Logf("shutting down temporary store: %v", err) } }) t.Run("GetBindMount", func(t *testing.T) { for _, argNeeder := range []string{"from", "bind-propagation", "src", "source", "target", "dst", "destination", "relabel"} { _, _, _, _, err := GetBindMount(sys, []string{argNeeder}, tempDir, store, "", nil, tempDir, tempDir) assert.ErrorIsf(t, err, errBadOptionArg, "option %q was supposed to have an arg, but wasn't flagged when it didn't have one", argNeeder) _, _, _, _, err = GetBindMount(sys, []string{argNeeder + "="}, tempDir, store, "", nil, tempDir, tempDir) assert.ErrorIsf(t, err, errBadOptionArg, "option %q was supposed to have an arg, but wasn't flagged when it didn't have one", argNeeder) } for _, argHater := range []string{"bind-nonrecursive", "nodev", "noexec", "nosuid", "ro", "readonly", "rw", "readwrite", "shared", "rshared", "private", "rprivate", "slave", "rslave", "Z", "z", "U", "no-dereference"} { _, _, _, _, err := GetBindMount(sys, []string{argHater + "=nonce"}, tempDir, store, "", nil, tempDir, tempDir) assert.ErrorIsf(t, err, errBadOptionNoArg, "option %q is not supposed to have an arg, but wasn't flagged when it tried to supply one", argHater) } }) t.Run("GetCacheMount", func(t *testing.T) { for _, argNeeder := range []string{"sharing", "id", "from", "bind-propagation", "src", "source", "target", "dst", "destination", "mode", "uid", "gid"} { _, _, _, _, _, err := GetCacheMount(sys, []string{argNeeder}, store, "", nil, emptyIDmap, emptyIDmap, tempDir, tempDir) assert.ErrorIsf(t, err, errBadOptionArg, "option %q was supposed to have an arg, but wasn't flagged when it didn't have one", argNeeder) _, _, _, _, _, err = GetCacheMount(sys, []string{argNeeder + "="}, store, "", nil, emptyIDmap, emptyIDmap, tempDir, tempDir) assert.ErrorIsf(t, err, errBadOptionArg, "option %q was supposed to have an arg, but wasn't flagged when it didn't have one", argNeeder) } for _, argHater := range []string{"nodev", "noexec", "nosuid", "U", "rw", "readwrite", "ro", "readonly", "shared", "Z", "z", "rshared", "private", "rprivate", "slave", "rslave"} { _, _, _, _, _, err := GetCacheMount(sys, []string{argHater + "=nonce"}, store, "", nil, emptyIDmap, emptyIDmap, tempDir, tempDir) assert.ErrorIsf(t, err, errBadOptionNoArg, "option %q is not supposed to have an arg, but wasn't flagged when it tried to supply one", argHater) } }) t.Run("GetTmpfsMount", func(t *testing.T) { for _, argNeeder := range []string{"tmpfs-mode", "tmpfs-size", "target", "dst", "destination"} { _, err := GetTmpfsMount([]string{argNeeder}, tempDir) assert.ErrorIsf(t, err, errBadOptionArg, "option %q was supposed to have an arg, but wasn't flagged when it didn't have one", argNeeder) _, err = GetTmpfsMount([]string{argNeeder + "="}, tempDir) assert.ErrorIsf(t, err, errBadOptionArg, "option %q was supposed to have an arg, but wasn't flagged when it didn't have one", argNeeder) } for _, argHater := range []string{"nodev", "noexec", "nosuid", "ro", "readonly", "tmpcopyup"} { _, err := GetTmpfsMount([]string{argHater + "=nonce"}, tempDir) assert.ErrorIsf(t, err, errBadOptionNoArg, "option %q is not supposed to have an arg, but wasn't flagged when it tried to supply one", argHater) } }) } ================================================ FILE: manifests/compat.go ================================================ // This package is deprecated. Its functionality has been moved to // github.com/containers/common/libimage/manifests, which provides the same // API. The stubs here are present for compatibility with older code. New // implementations should use github.com/containers/common/libimage/manifests // directly. package manifests import ( "go.podman.io/common/libimage/manifests" "go.podman.io/storage" ) type ( // List is an alias for github.com/containers/common/libimage/manifests.List. List = manifests.List // PushOptions is an alias for github.com/containers/common/libimage/manifests.PushOptions. PushOptions = manifests.PushOptions ) // ErrListImageUnknown is an alias for github.com/containers/common/libimage/manifests.ErrListImageUnknown var ErrListImageUnknown = manifests.ErrListImageUnknown // Create wraps github.com/containers/common/libimage/manifests.Create(). func Create() List { return manifests.Create() } // LoadFromImage wraps github.com/containers/common/libimage/manifests.LoadFromImage(). func LoadFromImage(store storage.Store, image string) (string, List, error) { return manifests.LoadFromImage(store, image) } ================================================ FILE: mount.go ================================================ package buildah import "fmt" // Mount mounts a container's root filesystem in a location which can be // accessed from the host, and returns the location. func (b *Builder) Mount(label string) (string, error) { mountpoint, err := b.store.Mount(b.ContainerID, label) if err != nil { return "", fmt.Errorf("mounting build container %q: %w", b.ContainerID, err) } b.MountPoint = mountpoint err = b.Save() if err != nil { return "", fmt.Errorf("saving updated state for build container %q: %w", b.ContainerID, err) } return mountpoint, nil } func (b *Builder) setMountPoint(mountPoint string) error { b.MountPoint = mountPoint if err := b.Save(); err != nil { return fmt.Errorf("saving updated state for build container %q: %w", b.ContainerID, err) } return nil } // Mounted returns whether the container is mounted or not func (b *Builder) Mounted() (bool, error) { mountCnt, err := b.store.Mounted(b.ContainerID) if err != nil { return false, fmt.Errorf("determining if mounting build container %q is mounted: %w", b.ContainerID, err) } mounted := mountCnt > 0 if mounted && b.MountPoint == "" { ctr, err := b.store.Container(b.ContainerID) if err != nil { return mountCnt > 0, fmt.Errorf("determining if mounting build container %q is mounted: %w", b.ContainerID, err) } layer, err := b.store.Layer(ctr.LayerID) if err != nil { return mountCnt > 0, fmt.Errorf("determining if mounting build container %q is mounted: %w", b.ContainerID, err) } return mounted, b.setMountPoint(layer.MountPoint) } if !mounted && b.MountPoint != "" { return mounted, b.setMountPoint("") } return mounted, nil } ================================================ FILE: new.go ================================================ package buildah import ( "context" "errors" "fmt" "maps" "math/rand" "slices" "strings" "github.com/containers/buildah/define" digest "github.com/opencontainers/go-digest" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/openshift/imagebuilder" "github.com/sirupsen/logrus" "go.podman.io/common/libimage" "go.podman.io/common/pkg/config" "go.podman.io/image/v5/image" "go.podman.io/image/v5/manifest" "go.podman.io/image/v5/pkg/shortnames" "go.podman.io/image/v5/transports" "go.podman.io/image/v5/types" "go.podman.io/storage" "go.podman.io/storage/pkg/stringid" ) const ( // BaseImageFakeName is the "name" of a source image which we interpret // as "no image". BaseImageFakeName = imagebuilder.NoBaseImageSpecifier ) func getImageName(name string, img *storage.Image) string { imageName := name if len(img.Names) > 0 { imageName = img.Names[0] // When the image used by the container is a tagged image // the container name might be set to the original image instead of // the image given in the "from" command line. // This loop is supposed to fix this. for _, n := range img.Names { if strings.Contains(n, name) { imageName = n break } } } return imageName } func imageNamePrefix(imageName string) string { prefix := imageName if d, err := digest.Parse(imageName); err == nil { prefix = d.Encoded() if len(prefix) > 12 { prefix = prefix[:12] } } if stringid.ValidateID(prefix) == nil { prefix = stringid.TruncateID(prefix) } s := strings.Split(prefix, ":") if len(s) > 0 { prefix = s[0] } s = strings.Split(prefix, "/") if len(s) > 0 { prefix = s[len(s)-1] } s = strings.Split(prefix, "@") if len(s) > 0 { prefix = s[0] } return prefix } func newContainerIDMappingOptions(idmapOptions *define.IDMappingOptions) storage.IDMappingOptions { var options storage.IDMappingOptions if idmapOptions != nil { if idmapOptions.AutoUserNs { options.AutoUserNs = true options.AutoUserNsOpts = idmapOptions.AutoUserNsOpts } else { options.HostUIDMapping = idmapOptions.HostUIDMapping options.HostGIDMapping = idmapOptions.HostGIDMapping uidmap, gidmap := convertRuntimeIDMaps(idmapOptions.UIDMap, idmapOptions.GIDMap) if len(uidmap) > 0 && len(gidmap) > 0 { options.UIDMap = uidmap options.GIDMap = gidmap } else { options.HostUIDMapping = true options.HostGIDMapping = true } } } return options } func containerNameExist(name string, containers []storage.Container) bool { for _, container := range containers { if slices.Contains(container.Names, name) { return true } } return false } func findUnusedContainer(name string, containers []storage.Container) string { suffix := 1 tmpName := name for containerNameExist(tmpName, containers) { tmpName = fmt.Sprintf("%s-%d", name, suffix) suffix++ } return tmpName } func newBuilder(ctx context.Context, store storage.Store, options BuilderOptions) (*Builder, error) { var ( ref types.ImageReference img *storage.Image err error ) if options.FromImage == BaseImageFakeName { options.FromImage = "" } if options.NetworkInterface == nil { // create the network interface options.NetworkInterface, err = getNetworkInterface(store) if err != nil { return nil, err } } systemContext := getSystemContext(store, options.SystemContext, options.SignaturePolicyPath) if options.FromImage != "" && options.FromImage != BaseImageFakeName { imageRuntime, err := libimage.RuntimeFromStore(store, &libimage.RuntimeOptions{SystemContext: systemContext}) if err != nil { return nil, err } pullPolicy, err := config.ParsePullPolicy(options.PullPolicy.String()) if err != nil { return nil, err } // Note: options.Format does *not* relate to the image we're // about to pull (see tests/digests.bats). So we're not // forcing a MIMEType in the pullOptions below. pullOptions := libimage.PullOptions{} pullOptions.RetryDelay = &options.PullRetryDelay pullOptions.OciDecryptConfig = options.OciDecryptConfig pullOptions.SignaturePolicyPath = options.SignaturePolicyPath pullOptions.Writer = options.ReportWriter pullOptions.DestinationLookupReferenceFunc = cacheLookupReferenceFunc(options.BlobDirectory, types.PreserveOriginal) maxRetries := uint(options.MaxPullRetries) pullOptions.MaxRetries = &maxRetries pulledImages, err := imageRuntime.Pull(ctx, options.FromImage, pullPolicy, &pullOptions) if err != nil { return nil, err } if len(pulledImages) > 0 { img = pulledImages[0].StorageImage() ref, err = pulledImages[0].StorageReference() if err != nil { return nil, err } } } imageSpec := options.FromImage imageID := "" imageDigest := "" topLayer := "" if img != nil { imageSpec = getImageName(imageNamePrefix(imageSpec), img) imageID = img.ID topLayer = img.TopLayer } var src types.Image if ref != nil { srcSrc, err := ref.NewImageSource(ctx, systemContext) if err != nil { return nil, fmt.Errorf("instantiating image for %q: %w", transports.ImageName(ref), err) } defer srcSrc.Close() unparsedTop := image.UnparsedInstance(srcSrc, nil) manifestBytes, manifestType, err := unparsedTop.Manifest(ctx) if err != nil { return nil, fmt.Errorf("loading image manifest for %q: %w", transports.ImageName(ref), err) } if manifestDigest, err := manifest.Digest(manifestBytes); err == nil { imageDigest = manifestDigest.String() } var instanceDigest *digest.Digest unparsedInstance := unparsedTop // for instanceDigest if manifest.MIMETypeIsMultiImage(manifestType) { list, err := manifest.ListFromBlob(manifestBytes, manifestType) if err != nil { return nil, fmt.Errorf("parsing image manifest for %q as list: %w", transports.ImageName(ref), err) } instance, err := list.ChooseInstance(systemContext) if err != nil { return nil, fmt.Errorf("finding an appropriate image in manifest list %q: %w", transports.ImageName(ref), err) } instanceDigest = &instance unparsedInstance = image.UnparsedInstance(srcSrc, instanceDigest) } src, err = image.FromUnparsedImage(ctx, systemContext, unparsedInstance) if err != nil { return nil, fmt.Errorf("instantiating image for %q instance %q: %w", transports.ImageName(ref), instanceDigest, err) } } name := "working-container" if options.ContainerSuffix != "" { name = options.ContainerSuffix } if options.Container != "" { name = options.Container } else { if imageSpec != "" { name = imageNamePrefix(imageSpec) + "-" + name } } var container *storage.Container tmpName := name if options.Container == "" { containers, err := store.Containers() if err != nil { return nil, fmt.Errorf("unable to check for container names: %w", err) } tmpName = findUnusedContainer(tmpName, containers) } suffixDigitsModulo := 100 for { var flags map[string]any // check if we have predefined ProcessLabel and MountLabel // this could be true if this is another stage in a build if options.ProcessLabel != "" && options.MountLabel != "" { flags = map[string]any{ "ProcessLabel": options.ProcessLabel, "MountLabel": options.MountLabel, } } coptions := storage.ContainerOptions{ LabelOpts: options.CommonBuildOpts.LabelOpts, IDMappingOptions: newContainerIDMappingOptions(options.IDMappingOptions), Flags: flags, Volatile: true, } container, err = store.CreateContainer("", []string{tmpName}, imageID, "", "", &coptions) if err == nil { name = tmpName break } if !errors.Is(err, storage.ErrDuplicateName) || options.Container != "" { return nil, fmt.Errorf("creating container: %w", err) } tmpName = fmt.Sprintf("%s-%d", name, rand.Int()%suffixDigitsModulo) if suffixDigitsModulo < 1_000_000_000 { suffixDigitsModulo *= 10 } } defer func() { if err != nil { if err2 := store.DeleteContainer(container.ID); err2 != nil { logrus.Errorf("error deleting container %q: %v", container.ID, err2) } } }() uidmap, gidmap := convertStorageIDMaps(container.UIDMap, container.GIDMap) defaultNamespaceOptions, err := DefaultNamespaceOptions() if err != nil { return nil, err } namespaceOptions := defaultNamespaceOptions namespaceOptions.AddOrReplace(options.NamespaceOptions...) builder := &Builder{ store: store, Type: containerType, FromImage: imageSpec, FromImageID: imageID, FromImageDigest: imageDigest, GroupAdd: options.GroupAdd, Container: name, ContainerID: container.ID, ImageAnnotations: map[string]string{}, ImageCreatedBy: "", ProcessLabel: container.ProcessLabel(), MountLabel: container.MountLabel(), DefaultMountsFilePath: options.DefaultMountsFilePath, Isolation: options.Isolation, NamespaceOptions: namespaceOptions, ConfigureNetwork: options.ConfigureNetwork, IDMappingOptions: define.IDMappingOptions{ HostUIDMapping: len(uidmap) == 0, HostGIDMapping: len(uidmap) == 0, UIDMap: uidmap, GIDMap: gidmap, }, Capabilities: slices.Clone(options.Capabilities), CommonBuildOpts: options.CommonBuildOpts, TopLayer: topLayer, Args: maps.Clone(options.Args), Format: options.Format, Devices: options.Devices, DeviceSpecs: options.DeviceSpecs, Logger: options.Logger, NetworkInterface: options.NetworkInterface, CDIConfigDir: options.CDIConfigDir, } if options.Mount { _, err = builder.Mount(container.MountLabel()) if err != nil { return nil, fmt.Errorf("mounting build container %q: %w", builder.ContainerID, err) } } if err := builder.initConfig(ctx, systemContext, src, &options); err != nil { return nil, fmt.Errorf("preparing image configuration: %w", err) } if !options.PreserveBaseImageAnns { builder.SetAnnotation(v1.AnnotationBaseImageDigest, imageDigest) if !shortnames.IsShortName(imageSpec) { // If the base image was specified as a fully-qualified // image name, let's set it. builder.SetAnnotation(v1.AnnotationBaseImageName, imageSpec) } else { builder.UnsetAnnotation(v1.AnnotationBaseImageName) } } err = builder.Save() if err != nil { return nil, fmt.Errorf("saving builder state for container %q: %w", builder.ContainerID, err) } return builder, nil } ================================================ FILE: new_test.go ================================================ package buildah import ( "testing" "github.com/openshift/imagebuilder" "github.com/stretchr/testify/assert" "go.podman.io/storage" ) func TestGetImageName(t *testing.T) { t.Parallel() tt := []struct { caseName string name string names []string expected string }{ {"tagged image", "busybox1", []string{"docker.io/library/busybox:latest", "docker.io/library/busybox1:latest"}, "docker.io/library/busybox1:latest"}, {"image name not in the resolved image names", "image1", []string{"docker.io/library/busybox:latest", "docker.io/library/busybox1:latest"}, "docker.io/library/busybox:latest"}, {"resolved image with empty name list", "image1", []string{}, "image1"}, } for _, tc := range tt { img := &storage.Image{Names: tc.names} res := getImageName(tc.name, img) if res != tc.expected { t.Errorf("test case '%s' failed: expected %#v but got %#v", tc.caseName, tc.expected, res) } } } func TestNoBaseImageSpecifierIsScratch(t *testing.T) { t.Parallel() assert.Equal(t, "scratch", imagebuilder.NoBaseImageSpecifier) // juuuuust in case assert.Equal(t, "scratch", BaseImageFakeName) } ================================================ FILE: pkg/binfmt/binfmt.go ================================================ //go:build linux package binfmt import ( "bufio" "errors" "fmt" "os" "path/filepath" "strings" "syscall" "github.com/sirupsen/logrus" "go.podman.io/storage/pkg/unshare" "golang.org/x/sys/unix" ) // MaybeRegister() calls Register() if the current context is a rootless one, // or if the "container" environment variable suggests that we're in a // container. func MaybeRegister(configurationSearchDirectories []string) error { if unshare.IsRootless() || os.Getenv("container") != "" { // we _also_ own our own mount namespace return Register(configurationSearchDirectories) } return nil } // Register() registers binfmt.d emulators described by configuration files in // the passed-in slice of directories, or in the union of /etc/binfmt.d, // /run/binfmt.d, and /usr/lib/binfmt.d if the slice has no items. If any // emulators are configured, it will attempt to mount a binfmt_misc filesystem // in the current mount namespace first, ignoring only EPERM and EACCES errors. func Register(configurationSearchDirectories []string) error { if len(configurationSearchDirectories) == 0 { configurationSearchDirectories = []string{"/etc/binfmt.d", "/run/binfmt.d", "/usr/lib/binfmt.d"} } mounted := false for _, searchDir := range configurationSearchDirectories { globs, err := filepath.Glob(filepath.Join(searchDir, "*.conf")) if err != nil { return fmt.Errorf("looking for binfmt.d configuration in %q: %w", searchDir, err) } for _, conf := range globs { f, err := os.Open(conf) if err != nil { return fmt.Errorf("reading binfmt.d configuration: %w", err) } scanner := bufio.NewScanner(f) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if len(line) == 0 || line[0] == ';' || line[0] == '#' { continue } if !mounted { if err = unix.Mount("none", "/proc/sys/fs/binfmt_misc", "binfmt_misc", 0, ""); err != nil { if errors.Is(err, syscall.EPERM) || errors.Is(err, syscall.EACCES) { // well, we tried. no need to make a stink about it return nil } return fmt.Errorf("mounting binfmt_misc: %w", err) } mounted = true } reg, err := os.Create("/proc/sys/fs/binfmt_misc/register") if err != nil { return fmt.Errorf("registering(open): %w", err) } if _, err = fmt.Fprintf(reg, "%s\n", line); err != nil { return fmt.Errorf("registering(write): %w", err) } logrus.Tracef("registered binfmt %q", line) if err = reg.Close(); err != nil { return fmt.Errorf("registering(close): %w", err) } } if err := f.Close(); err != nil { return fmt.Errorf("reading binfmt.d configuration: %w", err) } } } return nil } ================================================ FILE: pkg/binfmt/binfmt_unsupported.go ================================================ //go:build !linux package binfmt import "syscall" // MaybeRegister() returns no error. func MaybeRegister(configurationSearchDirectories []string) error { return nil } // Register() returns an error. func Register(configurationSearchDirectories []string) error { return syscall.ENOSYS } ================================================ FILE: pkg/blobcache/blobcache.go ================================================ package blobcache import ( imageBlobCache "go.podman.io/image/v5/pkg/blobcache" "go.podman.io/image/v5/types" ) // BlobCache is an object which saves copies of blobs that are written to it while passing them // through to some real destination, and which can be queried directly in order to read them // back. type BlobCache interface { types.ImageReference // HasBlob checks if a blob that matches the passed-in digest (and // size, if not -1), is present in the cache. HasBlob(types.BlobInfo) (bool, int64, error) // Directories returns the list of cache directories. Directory() string // ClearCache() clears the contents of the cache directories. Note // that this also clears content which was not placed there by this // cache implementation. ClearCache() error } // NewBlobCache creates a new blob cache that wraps an image reference. Any blobs which are // written to the destination image created from the resulting reference will also be stored // as-is to the specified directory or a temporary directory. // The compress argument controls whether or not the cache will try to substitute a compressed // or different version of a blob when preparing the list of layers when reading an image. func NewBlobCache(ref types.ImageReference, directory string, compress types.LayerCompression) (BlobCache, error) { return imageBlobCache.NewBlobCache(ref, directory, compress) } ================================================ FILE: pkg/chrootuser/user.go ================================================ package chrootuser import ( "errors" "fmt" "os/user" "strconv" "strings" ) // ErrNoSuchUser indicates that the user provided by the caller does not // exist in /etc/passws var ErrNoSuchUser = errors.New("user does not exist in /etc/passwd") // GetUser will return the uid, gid of the user specified in the userspec // it will use the /etc/passwd and /etc/group files inside of the rootdir // to return this information. // userspec format [user | user:group | uid | uid:gid | user:gid | uid:group ] func GetUser(rootdir, userspec string) (uint32, uint32, string, error) { var gid64 uint64 var gerr error = user.UnknownGroupError("error looking up group") spec := strings.SplitN(userspec, ":", 2) userspec = spec[0] groupspec := "" if userspec == "" { userspec = "0" } if len(spec) > 1 { groupspec = spec[1] } uid64, uerr := strconv.ParseUint(userspec, 10, 32) if uerr == nil && groupspec == "" { // We parsed the user name as a number, and there's no group // component, so try to look up the primary GID of the user who // has this UID. var name string name, gid64, gerr = lookupGroupForUIDInContainer(rootdir, uid64) if gerr == nil { userspec = name } else { // Leave userspec alone, but swallow the error and just // use GID 0. gid64 = 0 gerr = nil } } if uerr != nil { // The user ID couldn't be parsed as a number, so try to look // up the user's UID and primary GID. uid64, gid64, uerr = lookupUserInContainer(rootdir, userspec) gerr = uerr } if groupspec != "" { // We have a group name or number, so parse it. gid64, gerr = strconv.ParseUint(groupspec, 10, 32) if gerr != nil { // The group couldn't be parsed as a number, so look up // the group's GID. gid64, gerr = lookupGroupInContainer(rootdir, groupspec) } } homedir, err := lookupHomedirInContainer(rootdir, uid64) if err != nil { homedir = "/" } if uerr == nil && gerr == nil { return uint32(uid64), uint32(gid64), homedir, nil } err = fmt.Errorf("determining run uid: %w", uerr) if uerr == nil { err = fmt.Errorf("determining run gid: %w", gerr) } return 0, 0, homedir, err } // GetGroup returns the gid by looking it up in the /etc/group file // groupspec format [ group | gid ] func GetGroup(rootdir, groupspec string) (uint32, error) { gid64, gerr := strconv.ParseUint(groupspec, 10, 32) if gerr != nil { // The group couldn't be parsed as a number, so look up // the group's GID. gid64, gerr = lookupGroupInContainer(rootdir, groupspec) } if gerr != nil { return 0, fmt.Errorf("looking up group for gid %q: %w", groupspec, gerr) } return uint32(gid64), nil } // GetAdditionalGroupsForUser returns a list of gids that userid is associated with func GetAdditionalGroupsForUser(rootdir string, userid uint64) ([]uint32, error) { gids, err := lookupAdditionalGroupsForUIDInContainer(rootdir, userid) if err != nil { return nil, fmt.Errorf("looking up supplemental groups for uid %d: %w", userid, err) } return gids, nil } // LookupUIDInContainer returns username and gid associated with a UID in a container // it will use the /etc/passwd files inside of the rootdir // to return this information. func LookupUIDInContainer(rootdir string, uid uint64) (user string, gid uint64, err error) { return lookupUIDInContainer(rootdir, uid) } ================================================ FILE: pkg/chrootuser/user_basic.go ================================================ //go:build !linux && !freebsd package chrootuser import ( "errors" ) func lookupUserInContainer(rootdir, username string) (uint64, uint64, error) { return 0, 0, errors.New("user lookup not supported") } func lookupGroupInContainer(rootdir, groupname string) (uint64, error) { return 0, errors.New("group lookup not supported") } func lookupGroupForUIDInContainer(rootdir string, userid uint64) (string, uint64, error) { return "", 0, errors.New("primary group lookup by uid not supported") } func lookupAdditionalGroupsForUIDInContainer(rootdir string, userid uint64) (gid []uint32, err error) { return nil, errors.New("supplemental groups list lookup by uid not supported") } func lookupUIDInContainer(rootdir string, uid uint64) (string, uint64, error) { return "", 0, errors.New("UID lookup not supported") } func lookupHomedirInContainer(rootdir string, uid uint64) (string, error) { return "", errors.New("Home directory lookup not supported") } ================================================ FILE: pkg/chrootuser/user_test.go ================================================ package chrootuser import ( "bufio" "strings" "testing" "github.com/stretchr/testify/assert" ) var testGroupData = `# comment # indented comment wheel:*:0:root daemon:*:1: kmem:*:2: ` func TestParseStripComments(t *testing.T) { t.Parallel() // Test reading group file, ignoring comment lines rc := bufio.NewScanner(strings.NewReader(testGroupData)) line, ok := scanWithoutComments(rc) assert.Equal(t, ok, true) assert.Equal(t, line, "wheel:*:0:root") } func TestParseNextGroup(t *testing.T) { t.Parallel() // Test parsing group file rc := bufio.NewScanner(strings.NewReader(testGroupData)) expected := []lookupGroupEntry{ {"wheel", 0, "root"}, {"daemon", 1, ""}, {"kmem", 2, ""}, } for _, exp := range expected { grp := parseNextGroup(rc) assert.NotNil(t, grp) assert.Equal(t, *grp, exp) } assert.Nil(t, parseNextGroup(rc)) } ================================================ FILE: pkg/chrootuser/user_unix.go ================================================ //go:build linux || freebsd package chrootuser import ( "bufio" "flag" "fmt" "io" "os" "os/exec" "os/user" "strconv" "strings" "sync" "go.podman.io/storage/pkg/reexec" "golang.org/x/sys/unix" ) const ( openChrootedCommand = "chrootuser-open" ) func init() { reexec.Register(openChrootedCommand, openChrootedFileMain) } func openChrootedFileMain() { status := 0 flag.Parse() if len(flag.Args()) < 1 { os.Exit(1) } // Our first parameter is the directory to chroot into. if err := unix.Chdir(flag.Arg(0)); err != nil { fmt.Fprintf(os.Stderr, "chdir(): %v", err) os.Exit(1) } if err := unix.Chroot(flag.Arg(0)); err != nil { fmt.Fprintf(os.Stderr, "chroot(): %v", err) os.Exit(1) } // Anything else is a file we want to dump out. for _, filename := range flag.Args()[1:] { f, err := os.Open(filename) if err != nil { fmt.Fprintf(os.Stderr, "open(%q): %v", filename, err) status = 1 continue } _, err = io.Copy(os.Stdout, f) if err != nil { fmt.Fprintf(os.Stderr, "read(%q): %v", filename, err) } f.Close() } os.Exit(status) } func openChrootedFile(rootdir, filename string) (*exec.Cmd, io.ReadCloser, error) { // The child process expects a chroot and one or more filenames that // will be consulted relative to the chroot directory and concatenated // to its stdout. Start it up. cmd := reexec.Command(openChrootedCommand, rootdir, filename) stdout, err := cmd.StdoutPipe() if err != nil { return nil, nil, err } err = cmd.Start() if err != nil { return nil, nil, err } // Hand back the child's stdout for reading, and the child to reap. return cmd, stdout, nil } var lookupUser, lookupGroup sync.Mutex type lookupPasswdEntry struct { name string uid uint64 gid uint64 home string } type lookupGroupEntry struct { name string gid uint64 user string } func scanWithoutComments(rc *bufio.Scanner) (string, bool) { for { if !rc.Scan() { return "", false } line := rc.Text() if strings.HasPrefix(strings.TrimSpace(line), "#") { continue } return line, true } } func parseNextPasswd(rc *bufio.Scanner) *lookupPasswdEntry { if !rc.Scan() { return nil } line := rc.Text() fields := strings.Split(line, ":") if len(fields) != 7 { return nil } uid, err := strconv.ParseUint(fields[2], 10, 32) if err != nil { return nil } gid, err := strconv.ParseUint(fields[3], 10, 32) if err != nil { return nil } return &lookupPasswdEntry{ name: fields[0], uid: uid, gid: gid, home: fields[5], } } func parseNextGroup(rc *bufio.Scanner) *lookupGroupEntry { // On FreeBSD, /etc/group may contain comments: // https://man.freebsd.org/cgi/man.cgi?query=group&sektion=5&format=html // We need to ignore those lines rather than trying to parse them. line, ok := scanWithoutComments(rc) if !ok { return nil } fields := strings.Split(line, ":") if len(fields) != 4 { return nil } gid, err := strconv.ParseUint(fields[2], 10, 32) if err != nil { return nil } return &lookupGroupEntry{ name: fields[0], gid: gid, user: fields[3], } } func lookupUserInContainer(rootdir, username string) (uid uint64, gid uint64, err error) { cmd, f, err := openChrootedFile(rootdir, "/etc/passwd") if err != nil { return 0, 0, err } defer func() { _ = cmd.Wait() }() rc := bufio.NewScanner(f) defer f.Close() lookupUser.Lock() defer lookupUser.Unlock() pwd := parseNextPasswd(rc) for pwd != nil { if pwd.name != username { pwd = parseNextPasswd(rc) continue } return pwd.uid, pwd.gid, nil } return 0, 0, user.UnknownUserError(fmt.Sprintf("error looking up user %q", username)) } func lookupGroupForUIDInContainer(rootdir string, userid uint64) (username string, gid uint64, err error) { cmd, f, err := openChrootedFile(rootdir, "/etc/passwd") if err != nil { return "", 0, err } defer func() { _ = cmd.Wait() }() rc := bufio.NewScanner(f) defer f.Close() lookupUser.Lock() defer lookupUser.Unlock() pwd := parseNextPasswd(rc) for pwd != nil { if pwd.uid != userid { pwd = parseNextPasswd(rc) continue } return pwd.name, pwd.gid, nil } return "", 0, ErrNoSuchUser } func lookupAdditionalGroupsForUIDInContainer(rootdir string, userid uint64) (gid []uint32, err error) { // Get the username associated with userid username, _, err := lookupGroupForUIDInContainer(rootdir, userid) if err != nil { return nil, err } cmd, f, err := openChrootedFile(rootdir, "/etc/group") if err != nil { return nil, err } defer func() { _ = cmd.Wait() }() rc := bufio.NewScanner(f) defer f.Close() lookupGroup.Lock() defer lookupGroup.Unlock() grp := parseNextGroup(rc) for grp != nil { if strings.Contains(grp.user, username) { gid = append(gid, uint32(grp.gid)) } grp = parseNextGroup(rc) } return gid, nil } func lookupGroupInContainer(rootdir, groupname string) (gid uint64, err error) { cmd, f, err := openChrootedFile(rootdir, "/etc/group") if err != nil { return 0, err } defer func() { _ = cmd.Wait() }() rc := bufio.NewScanner(f) defer f.Close() lookupGroup.Lock() defer lookupGroup.Unlock() grp := parseNextGroup(rc) for grp != nil { if grp.name != groupname { grp = parseNextGroup(rc) continue } return grp.gid, nil } return 0, user.UnknownGroupError(fmt.Sprintf("error looking up group %q", groupname)) } func lookupUIDInContainer(rootdir string, uid uint64) (string, uint64, error) { cmd, f, err := openChrootedFile(rootdir, "/etc/passwd") if err != nil { return "", 0, err } defer func() { _ = cmd.Wait() }() rc := bufio.NewScanner(f) defer f.Close() lookupUser.Lock() defer lookupUser.Unlock() pwd := parseNextPasswd(rc) for pwd != nil { if pwd.uid != uid { pwd = parseNextPasswd(rc) continue } return pwd.name, pwd.gid, nil } return "", 0, user.UnknownUserError(fmt.Sprintf("error looking up uid %q", uid)) } func lookupHomedirInContainer(rootdir string, uid uint64) (string, error) { cmd, f, err := openChrootedFile(rootdir, "/etc/passwd") if err != nil { return "", err } defer func() { _ = cmd.Wait() }() rc := bufio.NewScanner(f) defer f.Close() lookupUser.Lock() defer lookupUser.Unlock() pwd := parseNextPasswd(rc) for pwd != nil { if pwd.uid != uid { pwd = parseNextPasswd(rc) continue } return pwd.home, nil } return "", user.UnknownUserError(fmt.Sprintf("error looking up uid %q for homedir", uid)) } ================================================ FILE: pkg/cli/build.go ================================================ package cli // the cli package contains spf13/cobra related structs that help make up // the command line for buildah commands. this file's contents are better // suited for pkg/parse, but since pkg/parse imports pkg/util which also // imports pkg/parse, having it there would create a cyclic dependency, so // here we are. import ( "errors" "fmt" "io" "os" "path/filepath" "slices" "strconv" "strings" "time" "github.com/containers/buildah/define" "github.com/containers/buildah/internal/output" "github.com/containers/buildah/pkg/parse" "github.com/containers/buildah/pkg/util" "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "go.podman.io/common/pkg/auth" "go.podman.io/image/v5/docker/reference" "go.podman.io/image/v5/types" ) type BuildOptions struct { *LayerResults *BudResults *UserNSResults *FromAndBudResults *NameSpaceResults Logwriter *os.File } const ( MaxPullPushRetries = 3 PullPushRetryDelay = 2 * time.Second ) // GenBuildOptions translates command line flags into a BuildOptions structure func GenBuildOptions(c *cobra.Command, inputArgs []string, iopts BuildOptions) (define.BuildOptions, []string, []string, error) { options := define.BuildOptions{} var removeAll []string outputSpec := "" cleanTmpFile := false tags := []string{} if iopts.Network == "none" { if c.Flag("dns").Changed { return options, nil, nil, errors.New("the --dns option cannot be used with --network=none") } if c.Flag("dns-option").Changed { return options, nil, nil, errors.New("the --dns-option option cannot be used with --network=none") } if c.Flag("dns-search").Changed { return options, nil, nil, errors.New("the --dns-search option cannot be used with --network=none") } } if c.Flag("tag").Changed { tags = iopts.Tag if len(tags) > 0 { outputSpec = tags[0] tags = tags[1:] } if c.Flag("manifest").Changed { if slices.Contains(tags, iopts.Manifest) { return options, nil, nil, errors.New("the same name must not be specified for both '--tag' and '--manifest'") } } } if err := auth.CheckAuthFile(iopts.BudResults.Authfile); err != nil { return options, nil, nil, err } if c.Flag("logsplit").Changed { if !c.Flag("logfile").Changed { return options, nil, nil, errors.New("cannot use --logsplit without --logfile") } } iopts.BudResults.Authfile, cleanTmpFile = util.MirrorToTempFileIfPathIsDescriptor(iopts.BudResults.Authfile) if cleanTmpFile { removeAll = append(removeAll, iopts.BudResults.Authfile) } pullPolicy, err := parse.PullPolicyFromOptions(c) if err != nil { return options, nil, nil, err } args := make(map[string]string) if c.Flag("build-arg-file").Changed { for _, argfile := range iopts.BuildArgFile { if err := readBuildArgFile(argfile, args); err != nil { return options, nil, nil, err } } } if c.Flag("build-arg").Changed { for _, arg := range iopts.BuildArg { readBuildArg(arg, args) } } additionalBuildContext := make(map[string]*define.AdditionalBuildContext) if c.Flag("build-context").Changed { for _, contextString := range iopts.BuildContext { av := strings.SplitN(contextString, "=", 2) // the key should be non-empty: we use "" as internal // shorthand for the default build context when there's // an overlay mounted over it if len(av) > 1 && av[0] != "" { parseAdditionalBuildContext, err := parse.GetAdditionalBuildContext(av[1]) if err != nil { return options, nil, nil, fmt.Errorf("while parsing additional build context: %w", err) } additionalBuildContext[av[0]] = &parseAdditionalBuildContext } else { return options, nil, nil, fmt.Errorf("while parsing additional build context: %q, accepts value in the form of key=value", av) } } } containerfiles := getContainerfiles(iopts.File) format, err := GetFormat(iopts.Format) if err != nil { return options, nil, nil, err } layers := UseLayers() if c.Flag("layers").Changed { layers = iopts.Layers } contextDir := "" cliArgs := inputArgs // Nothing provided, we assume the current working directory as build // context if len(cliArgs) == 0 { contextDir, err = os.Getwd() if err != nil { return options, nil, nil, fmt.Errorf("unable to choose current working directory as build context: %w", err) } } else { // The context directory could be a URL. Try to handle that. tempDir, subDir, err := define.TempDirForURL("", "buildah", cliArgs[0]) if err != nil { return options, nil, nil, fmt.Errorf("prepping temporary context directory: %w", err) } if tempDir != "" { // We had to download it to a temporary directory. // Delete it later. removeAll = append(removeAll, tempDir) contextDir = filepath.Join(tempDir, subDir) } else { // Nope, it was local. Use it as is. absDir, err := filepath.Abs(cliArgs[0]) if err != nil { return options, nil, nil, fmt.Errorf("determining path to directory: %w", err) } contextDir = absDir } } if len(containerfiles) == 0 { // Try to find the Containerfile/Dockerfile within the contextDir containerfile, err := util.DiscoverContainerfile(contextDir) if err != nil { return options, nil, nil, err } containerfiles = append(containerfiles, containerfile) contextDir = filepath.Dir(containerfile) } contextDir, err = filepath.EvalSymlinks(contextDir) if err != nil { return options, nil, nil, fmt.Errorf("evaluating symlinks in build context path: %w", err) } var stdin io.Reader if iopts.Stdin { stdin = os.Stdin } var stdout, stderr, reporter *os.File stdout = os.Stdout stderr = os.Stderr reporter = os.Stderr if iopts.Logwriter != nil { logrus.SetOutput(iopts.Logwriter) stdout = iopts.Logwriter stderr = iopts.Logwriter reporter = iopts.Logwriter } systemContext, err := parse.SystemContextFromOptions(c) if err != nil { return options, nil, nil, fmt.Errorf("building system context: %w", err) } isolation, err := parse.IsolationOption(iopts.Isolation) if err != nil { return options, nil, nil, err } runtimeFlags := []string{} for _, arg := range iopts.RuntimeFlags { runtimeFlags = append(runtimeFlags, "--"+arg) } commonOpts, err := parse.CommonBuildOptions(c) if err != nil { return options, nil, nil, err } if (c.Flag("rm").Changed || c.Flag("force-rm").Changed) && (!c.Flag("layers").Changed && !c.Flag("no-cache").Changed) { return options, nil, nil, errors.New("'rm' and 'force-rm' can only be set with either 'layers' or 'no-cache'") } if iopts.StageLabels && !iopts.SaveStages { return options, nil, nil, errors.New(`"--stage-labels" requires "--save-stages"`) } if c.Flag("compress").Changed { logrus.Debugf("--compress option specified but is ignored") } compression := define.Gzip if iopts.DisableCompression { compression = define.Uncompressed } if c.Flag("disable-content-trust").Changed { logrus.Debugf("--disable-content-trust option specified but is ignored") } namespaceOptions, networkPolicy, err := parse.NamespaceOptions(c) if err != nil { return options, nil, nil, err } usernsOption, idmappingOptions, err := parse.IDMappingOptions(c, isolation) if err != nil { return options, nil, nil, fmt.Errorf("parsing ID mapping options: %w", err) } namespaceOptions.AddOrReplace(usernsOption...) platforms, err := parse.PlatformsFromOptions(c) if err != nil { return options, nil, nil, err } decryptConfig, err := DecryptConfig(iopts.DecryptionKeys) if err != nil { return options, nil, nil, fmt.Errorf("unable to obtain decrypt config: %w", err) } var excludes []string if iopts.IgnoreFile != "" { if excludes, _, err = parse.ContainerIgnoreFile(contextDir, iopts.IgnoreFile, containerfiles); err != nil { return options, nil, nil, err } } var timestamp, sourceDateEpoch *time.Time if c.Flag("timestamp").Changed { t := time.Unix(iopts.Timestamp, 0).UTC() timestamp = &t } if iopts.SourceDateEpoch != "" { u, err := strconv.ParseInt(iopts.SourceDateEpoch, 10, 64) if err != nil { return options, nil, nil, fmt.Errorf("error parsing source-date-epoch offset %q: %w", iopts.SourceDateEpoch, err) } s := time.Unix(u, 0).UTC() sourceDateEpoch = &s } if c.Flag("output").Changed { for _, buildOutput := range iopts.BuildOutputs { // if any of these go to stdout, we need to avoid // interspersing our random output in with it buildOption, err := output.GetBuildOutput(buildOutput) if err != nil { return options, nil, nil, err } if buildOption.Type == output.BuildOutputStdout { iopts.Quiet = true } } } var confidentialWorkloadOptions define.ConfidentialWorkloadOptions if c.Flag("cw").Changed { confidentialWorkloadOptions, err = parse.GetConfidentialWorkloadOptions(iopts.CWOptions) if err != nil { return options, nil, nil, err } } var cacheTo []reference.Named var cacheFrom []reference.Named cacheTo = nil cacheFrom = nil if c.Flag("cache-to").Changed { cacheTo, err = parse.RepoNamesToNamedReferences(iopts.CacheTo) if err != nil { return options, nil, nil, fmt.Errorf("unable to parse value provided `%s` to --cache-to: %w", iopts.CacheTo, err) } } if c.Flag("cache-from").Changed { cacheFrom, err = parse.RepoNamesToNamedReferences(iopts.CacheFrom) if err != nil { return options, nil, nil, fmt.Errorf("unable to parse value provided `%s` to --cache-from: %w", iopts.CacheTo, err) } } var cacheTTL time.Duration if c.Flag("cache-ttl").Changed { cacheTTL, err = time.ParseDuration(iopts.CacheTTL) if err != nil { return options, nil, nil, fmt.Errorf("unable to parse value provided %q as --cache-ttl: %w", iopts.CacheTTL, err) } // If user explicitly specified `--cache-ttl=0s` // it would effectively mean that user is asking // to use no cache at all. In such use cases // buildah can skip looking for cache entirely // by setting `--no-cache=true` internally. if int64(cacheTTL) == 0 { logrus.Debug("Setting --no-cache=true since --cache-ttl was set to 0s which effectively means user wants to ignore cache") if c.Flag("no-cache").Changed && !iopts.NoCache { return options, nil, nil, fmt.Errorf("cannot use --cache-ttl with duration as 0 and --no-cache=false") } iopts.NoCache = true } } if c.Flag("network").Changed { if isolation == define.IsolationChroot { if ns := namespaceOptions.Find(string(specs.NetworkNamespace)); ns != nil { if !ns.Host { return options, nil, nil, fmt.Errorf("cannot set --network other than host with --isolation %s", c.Flag("isolation").Value.String()) } } } } var sbomScanOptions []define.SBOMScanOptions if c.Flag("sbom").Changed || c.Flag("sbom-scanner-command").Changed || c.Flag("sbom-scanner-image").Changed || c.Flag("sbom-image-output").Changed || c.Flag("sbom-merge-strategy").Changed || c.Flag("sbom-output").Changed || c.Flag("sbom-image-output").Changed || c.Flag("sbom-purl-output").Changed || c.Flag("sbom-image-purl-output").Changed { sbomScanOption, err := parse.SBOMScanOptions(c) if err != nil { return options, nil, nil, err } if !slices.Contains(sbomScanOption.ContextDir, contextDir) { sbomScanOption.ContextDir = append(sbomScanOption.ContextDir, contextDir) } for _, abc := range additionalBuildContext { if !abc.IsURL && !abc.IsImage { sbomScanOption.ContextDir = append(sbomScanOption.ContextDir, abc.Value) } } sbomScanOption.PullPolicy = pullPolicy sbomScanOptions = append(sbomScanOptions, *sbomScanOption) } var compatVolumes, createdAnnotation, inheritAnnotations, inheritLabels, skipUnusedStages types.OptionalBool if c.Flag("compat-volumes").Changed { compatVolumes = types.NewOptionalBool(iopts.CompatVolumes) } if c.Flag("created-annotation").Changed { createdAnnotation = types.NewOptionalBool(iopts.CreatedAnnotation) } if c.Flag("inherit-annotations").Changed { inheritAnnotations = types.NewOptionalBool(iopts.InheritAnnotations) } if c.Flag("inherit-labels").Changed { inheritLabels = types.NewOptionalBool(iopts.InheritLabels) } if c.Flag("skip-unused-stages").Changed { skipUnusedStages = types.NewOptionalBool(iopts.SkipUnusedStages) } options = define.BuildOptions{ AddCapabilities: iopts.CapAdd, AdditionalBuildContexts: additionalBuildContext, AdditionalTags: tags, AllPlatforms: iopts.AllPlatforms, Annotations: iopts.Annotation, Architecture: systemContext.ArchitectureChoice, Args: args, BlobDirectory: iopts.BlobCache, BuildOutputs: iopts.BuildOutputs, CacheFrom: cacheFrom, CacheTo: cacheTo, CacheTTL: cacheTTL, CDIConfigDir: iopts.CDIConfigDir, CompatVolumes: compatVolumes, ConfidentialWorkload: confidentialWorkloadOptions, CPPFlags: iopts.CPPFlags, CommonBuildOpts: commonOpts, Compression: compression, ConfigureNetwork: networkPolicy, ContextDirectory: contextDir, CreatedAnnotation: createdAnnotation, Devices: iopts.Devices, DropCapabilities: iopts.CapDrop, Err: stderr, Excludes: excludes, ForceRmIntermediateCtrs: iopts.ForceRm, From: iopts.From, GroupAdd: iopts.GroupAdd, IDMappingOptions: idmappingOptions, IIDFile: iopts.Iidfile, IIDFileRaw: iopts.IidfileRaw, IgnoreFile: iopts.IgnoreFile, In: stdin, InheritLabels: inheritLabels, InheritAnnotations: inheritAnnotations, Isolation: isolation, Jobs: &iopts.Jobs, Labels: iopts.Label, LayerLabels: iopts.LayerLabel, Layers: layers, LogFile: iopts.Logfile, LogRusage: iopts.LogRusage, LogSplitByPlatform: iopts.LogSplitByPlatform, Manifest: iopts.Manifest, MetadataFile: iopts.MetadataFile, MaxPullPushRetries: iopts.Retry, NamespaceOptions: namespaceOptions, NoCache: iopts.NoCache, OS: systemContext.OSChoice, OSFeatures: iopts.OSFeatures, OSVersion: iopts.OSVersion, OciDecryptConfig: decryptConfig, Out: stdout, Output: outputSpec, OutputFormat: format, Platforms: platforms, PullPolicy: pullPolicy, Quiet: iopts.Quiet, RemoveIntermediateCtrs: iopts.Rm, ReportWriter: reporter, RewriteTimestamp: iopts.RewriteTimestamp, Runtime: iopts.Runtime, RuntimeArgs: runtimeFlags, RusageLogFile: iopts.RusageLogFile, SaveStages: iopts.SaveStages, SBOMScanOptions: sbomScanOptions, SignBy: iopts.SignBy, SignaturePolicyPath: iopts.SignaturePolicy, SourcePolicyFile: iopts.SourcePolicyFile, SkipUnusedStages: skipUnusedStages, SourceDateEpoch: sourceDateEpoch, Squash: iopts.Squash, StageLabels: iopts.StageLabels, SystemContext: systemContext, Target: iopts.Target, Timestamp: timestamp, TransientMounts: iopts.Volumes, TransientRunMounts: iopts.TransientRunMounts, UnsetEnvs: iopts.UnsetEnvs, UnsetLabels: iopts.UnsetLabels, UnsetAnnotations: iopts.UnsetAnnotations, } if iopts.RetryDelay != "" { options.PullPushRetryDelay, err = time.ParseDuration(iopts.RetryDelay) if err != nil { return options, nil, nil, fmt.Errorf("unable to parse value provided %q as --retry-delay: %w", iopts.RetryDelay, err) } // Following log line is used in integration test. logrus.Debugf("Setting MaxPullPushRetries to %d and PullPushRetryDelay to %v", iopts.Retry, options.PullPushRetryDelay) } if iopts.Quiet { options.ReportWriter = io.Discard } options.Envs = LookupEnvVarReferences(iopts.Envs, os.Environ()) return options, containerfiles, removeAll, nil } func readBuildArgFile(buildargfile string, args map[string]string) error { argfile, err := os.ReadFile(buildargfile) if err != nil { return err } for arg := range strings.SplitSeq(string(argfile), "\n") { if len(arg) == 0 || arg[0] == '#' { continue } readBuildArg(arg, args) } return err } func readBuildArg(buildarg string, args map[string]string) { av := strings.SplitN(buildarg, "=", 2) if len(av) > 1 { args[av[0]] = av[1] } else { // check if the env is set in the local environment and use that value if it is if val, present := os.LookupEnv(av[0]); present { args[av[0]] = val } else { delete(args, av[0]) } } } func getContainerfiles(files []string) []string { var containerfiles []string for _, f := range files { if f == "-" { containerfiles = append(containerfiles, "/dev/stdin") } else { containerfiles = append(containerfiles, f) } } return containerfiles } ================================================ FILE: pkg/cli/common.go ================================================ package cli // the cli package contains spf13/cobra related structs that help make up // the command line for buildah commands. it resides here so other projects // that vendor in this code can use them too. import ( "fmt" "maps" "os" "runtime" "strings" "github.com/containers/buildah/define" "github.com/containers/buildah/internal" "github.com/containers/buildah/pkg/completion" "github.com/containers/buildah/pkg/parse" encconfig "github.com/containers/ocicrypt/config" enchelpers "github.com/containers/ocicrypt/helpers" "github.com/opencontainers/runtime-spec/specs-go" "github.com/spf13/pflag" commonComp "go.podman.io/common/pkg/completion" "go.podman.io/common/pkg/config" "go.podman.io/storage/pkg/unshare" ) // LayerResults represents the results of the layer flags type LayerResults struct { ForceRm bool Layers bool SaveStages bool StageLabels bool } // UserNSResults represents the results for the UserNS flags type UserNSResults struct { UserNS string GroupAdd []string UserNSUIDMap []string UserNSGIDMap []string UserNSUIDMapUser string UserNSGIDMapGroup string } // NameSpaceResults represents the results for Namespace flags type NameSpaceResults struct { Cgroup string IPC string Network string // Deprecated: CNIConfigDir is no longer used and is expected to be empty. CNIConfigDir string // Deprecated: CNIPlugInPath is no longer used and is expected to be empty. CNIPlugInPath string PID string UTS string } // BudResults represents the results for Build flags type BudResults struct { AllPlatforms bool Annotation []string Authfile string BuildArg []string BuildArgFile []string BuildContext []string CacheFrom []string CacheTo []string CacheTTL string CertDir string Compress bool Creds string CPPFlags []string DisableCompression bool DisableContentTrust bool IgnoreFile string File []string Format string From string Iidfile string IidfileRaw string InheritLabels bool InheritAnnotations bool Label []string LayerLabel []string Logfile string LogSplitByPlatform bool Manifest string MetadataFile string NoHostname bool NoHosts bool NoCache bool Timestamp int64 OmitHistory bool OCIHooksDir []string Pull string PullAlways bool PullNever bool Quiet bool IdentityLabel bool Rm bool Runtime string RuntimeFlags []string SbomPreset string SbomScannerImage string SbomScannerCommand []string SbomMergeStrategy string SbomOutput string SbomImgOutput string SbomPurlOutput string SbomImgPurlOutput string Secrets []string SSH []string SignaturePolicy string SignBy string Squash bool SkipUnusedStages bool Stdin bool Tag []string BuildOutputs []string Target string TLSVerify bool Jobs int LogRusage bool RusageLogFile string UnsetEnvs []string UnsetLabels []string UnsetAnnotations []string Envs []string OSFeatures []string OSVersion string CWOptions string SBOMOptions []string CompatVolumes bool SourceDateEpoch string RewriteTimestamp bool CreatedAnnotation bool SourcePolicyFile string TransientRunMounts []string } // FromAndBugResults represents the results for common flags // in build and from type FromAndBudResults struct { AddHost []string BlobCache string CapAdd []string CapDrop []string CDIConfigDir string CgroupParent string CPUPeriod uint64 CPUQuota int64 CPUSetCPUs string CPUSetMems string CPUShares uint64 DecryptionKeys []string Devices []string DNSSearch []string DNSServers []string DNSOptions []string HTTPProxy bool Isolation string Memory string MemorySwap string Retry int RetryDelay string SecurityOpt []string ShmSize string Ulimit []string Volumes []string } // GetUserNSFlags returns the common flags for usernamespace func GetUserNSFlags(flags *UserNSResults) pflag.FlagSet { usernsFlags := pflag.FlagSet{} usernsFlags.StringSliceVar(&flags.GroupAdd, "group-add", nil, "add additional groups to the primary container process. 'keep-groups' allows container processes to use supplementary groups.") usernsFlags.StringVar(&flags.UserNS, "userns", "", "'container', `path` of user namespace to join, or 'host'") usernsFlags.StringSliceVar(&flags.UserNSUIDMap, "userns-uid-map", []string{}, "`containerUID:hostUID:length` UID mapping to use in user namespace") usernsFlags.StringSliceVar(&flags.UserNSGIDMap, "userns-gid-map", []string{}, "`containerGID:hostGID:length` GID mapping to use in user namespace") usernsFlags.StringVar(&flags.UserNSUIDMapUser, "userns-uid-map-user", "", "`name` of entries from /etc/subuid to use to set user namespace UID mapping") usernsFlags.StringVar(&flags.UserNSGIDMapGroup, "userns-gid-map-group", "", "`name` of entries from /etc/subgid to use to set user namespace GID mapping") return usernsFlags } // GetUserNSFlagsCompletions returns the FlagCompletions for the userns flags func GetUserNSFlagsCompletions() commonComp.FlagCompletions { flagCompletion := commonComp.FlagCompletions{} flagCompletion["group-add"] = commonComp.AutocompleteNone flagCompletion["userns"] = completion.AutocompleteNamespaceFlag flagCompletion["userns-uid-map"] = commonComp.AutocompleteNone flagCompletion["userns-gid-map"] = commonComp.AutocompleteNone flagCompletion["userns-uid-map-user"] = commonComp.AutocompleteSubuidName flagCompletion["userns-gid-map-group"] = commonComp.AutocompleteSubgidName return flagCompletion } // GetNameSpaceFlags returns the common flags for a namespace menu func GetNameSpaceFlags(flags *NameSpaceResults) pflag.FlagSet { fs := pflag.FlagSet{} fs.StringVar(&flags.Cgroup, "cgroupns", "", "'private', or 'host'") fs.StringVar(&flags.IPC, string(specs.IPCNamespace), "", "'private', `path` of IPC namespace to join, or 'host'") fs.StringVar(&flags.Network, string(specs.NetworkNamespace), "", "'private', 'none', 'ns:path' of network namespace to join, or 'host'") fs.StringVar(&flags.PID, string(specs.PIDNamespace), "", "private, `path` of PID namespace to join, or 'host'") fs.StringVar(&flags.UTS, string(specs.UTSNamespace), "", "private, :`path` of UTS namespace to join, or 'host'") return fs } // GetNameSpaceFlagsCompletions returns the FlagCompletions for the namespace flags func GetNameSpaceFlagsCompletions() commonComp.FlagCompletions { flagCompletion := commonComp.FlagCompletions{} flagCompletion["cgroupns"] = completion.AutocompleteNamespaceFlag flagCompletion[string(specs.IPCNamespace)] = completion.AutocompleteNamespaceFlag flagCompletion[string(specs.NetworkNamespace)] = completion.AutocompleteNamespaceFlag flagCompletion[string(specs.PIDNamespace)] = completion.AutocompleteNamespaceFlag flagCompletion[string(specs.UTSNamespace)] = completion.AutocompleteNamespaceFlag return flagCompletion } // GetLayerFlags returns the common flags for layers func GetLayerFlags(flags *LayerResults) pflag.FlagSet { fs := pflag.FlagSet{} fs.BoolVar(&flags.ForceRm, "force-rm", false, "always remove intermediate containers after a build, even if the build is unsuccessful.") fs.BoolVar(&flags.Layers, "layers", UseLayers(), "use intermediate layers during build. Use BUILDAH_LAYERS environment variable to override.") fs.BoolVar(&flags.SaveStages, "save-stages", false, "save intermediate stage images.") fs.BoolVar(&flags.StageLabels, "stage-labels", false, "add metadata labels to intermediate stage images (requires --save-stages).") return fs } // Note: GetLayerFlagsCompletion is not needed since GetLayerFlags only contains bool flags // GetBudFlags returns common build flags func GetBudFlags(flags *BudResults) pflag.FlagSet { fs := pflag.FlagSet{} fs.BoolVar(&flags.AllPlatforms, "all-platforms", false, "attempt to build for all base image platforms") fs.String("arch", runtime.GOARCH, "set the ARCH of the image to the provided value instead of the architecture of the host") fs.StringArrayVar(&flags.Annotation, "annotation", []string{}, "set metadata for an image (default [])") fs.StringVar(&flags.Authfile, "authfile", "", "path of the authentication file.") fs.StringArrayVar(&flags.OCIHooksDir, "hooks-dir", []string{}, "set the OCI hooks directory path (may be set multiple times)") fs.StringArrayVar(&flags.BuildArg, "build-arg", []string{}, "`argument=value` to supply to the builder") fs.StringArrayVar(&flags.BuildArgFile, "build-arg-file", []string{}, "`argfile.conf` containing lines of argument=value to supply to the builder") fs.StringArrayVar(&flags.BuildContext, "build-context", []string{}, "`argument=value` to supply additional build context to the builder") fs.StringArrayVar(&flags.CacheFrom, "cache-from", []string{}, "remote repository list to utilise as potential cache source.") fs.StringArrayVar(&flags.CacheTo, "cache-to", []string{}, "remote repository list to utilise as potential cache destination.") fs.StringVar(&flags.CacheTTL, "cache-ttl", "", "only consider cache images under specified duration.") fs.StringVar(&flags.CertDir, "cert-dir", "", "use certificates at the specified path to access the registry") fs.BoolVar(&flags.Compress, "compress", false, "this is a legacy option, which has no effect on the image") fs.BoolVar(&flags.CompatVolumes, "compat-volumes", false, "preserve the contents of VOLUMEs during RUN instructions") fs.BoolVar(&flags.InheritLabels, "inherit-labels", true, "inherit the labels from the base image or base stages.") fs.BoolVar(&flags.InheritAnnotations, "inherit-annotations", true, "inherit the annotations from the base image or base stages.") fs.StringArrayVar(&flags.CPPFlags, "cpp-flag", []string{}, "set additional flag to pass to C preprocessor (cpp)") fs.BoolVar(&flags.CreatedAnnotation, "created-annotation", true, `set an "org.opencontainers.image.created" annotation in the image`) fs.StringVar(&flags.Creds, "creds", "", "use `[username[:password]]` for accessing the registry") fs.StringVarP(&flags.CWOptions, "cw", "", "", "confidential workload `options`") fs.BoolVarP(&flags.DisableCompression, "disable-compression", "D", true, "don't compress layers by default") fs.BoolVar(&flags.DisableContentTrust, "disable-content-trust", false, "this is a Docker specific option and is a NOOP") fs.StringArrayVar(&flags.Envs, "env", []string{}, "set environment variable for the image") fs.StringVar(&flags.From, "from", "", "image name used to replace the value in the first FROM instruction in the Containerfile") fs.StringVar(&flags.IgnoreFile, "ignorefile", "", "path to an alternate .dockerignore file") fs.StringSliceVarP(&flags.File, "file", "f", []string{}, "`pathname or URL` of a Dockerfile") fs.StringVar(&flags.Format, "format", DefaultFormat(), "`format` of the built image's manifest and metadata. Use BUILDAH_FORMAT environment variable to override.") fs.StringVar(&flags.Iidfile, "iidfile", "", "`file` to write the image ID to") fs.StringVar(&flags.IidfileRaw, "iidfile-raw", "", "`file` to write the image ID to (without algorithm prefix)") fs.IntVar(&flags.Jobs, "jobs", 1, "how many stages to run in parallel") fs.StringArrayVar(&flags.Label, "label", []string{}, "set metadata for an image (default [])") fs.StringArrayVar(&flags.LayerLabel, "layer-label", []string{}, "set metadata for an intermediate image (default [])") fs.StringVar(&flags.Logfile, "logfile", "", "log to `file` instead of stdout/stderr") fs.BoolVar(&flags.LogSplitByPlatform, "logsplit", false, "split logfile to different files for each platform") fs.Int("loglevel", 0, "NO LONGER USED, flag ignored, and hidden") if err := fs.MarkHidden("loglevel"); err != nil { panic(fmt.Sprintf("error marking the loglevel flag as hidden: %v", err)) } fs.BoolVar(&flags.LogRusage, "log-rusage", false, "log resource usage at each build step") if err := fs.MarkHidden("log-rusage"); err != nil { panic(fmt.Sprintf("error marking the log-rusage flag as hidden: %v", err)) } fs.StringVar(&flags.RusageLogFile, "rusage-logfile", "", "destination file to which rusage should be logged to instead of stdout (= the default).") if err := fs.MarkHidden("rusage-logfile"); err != nil { panic(fmt.Sprintf("error marking the rusage-logfile flag as hidden: %v", err)) } fs.StringVar(&flags.Manifest, "manifest", "", "add the image to the specified manifest list. Creates manifest list if it does not exist") fs.StringVar(&flags.MetadataFile, "metadata-file", "", "`file` to write metadata about the image to") fs.BoolVar(&flags.NoCache, "no-cache", false, "do not use existing cached images for the container build. Build from the start with a new set of cached layers.") fs.BoolVar(&flags.NoHostname, "no-hostname", false, "do not create new /etc/hostname file for RUN instructions, use the one from the base image.") fs.BoolVar(&flags.NoHosts, "no-hosts", false, "do not create new /etc/hosts file for RUN instructions, use the one from the base image.") fs.String("os", runtime.GOOS, "set the OS to the provided value instead of the current operating system of the host") fs.StringArrayVar(&flags.OSFeatures, "os-feature", []string{}, "set required OS `feature` for the target image in addition to values from the base image") fs.StringVar(&flags.OSVersion, "os-version", "", "set required OS `version` for the target image instead of the value from the base image") fs.StringVar(&flags.Pull, "pull", "missing", `pull base and SBOM scanner images from the registry. Values: always: pull base and SBOM scanner images even if the named images are present in store. missing: pull base and SBOM scanner images if the named images are not present in store. never: only use images present in store if available. newer: only pull base and SBOM scanner images when newer images exist on the registry than those in the store.`) fs.Lookup("pull").NoOptDefVal = "always" // treat a --pull with no argument like --pull=always fs.BoolVar(&flags.PullAlways, "pull-always", false, "pull the image even if the named image is present in store") if err := fs.MarkHidden("pull-always"); err != nil { panic(fmt.Sprintf("error marking the pull-always flag as hidden: %v", err)) } fs.BoolVar(&flags.PullNever, "pull-never", false, "do not pull the image, use the image present in store if available") if err := fs.MarkHidden("pull-never"); err != nil { panic(fmt.Sprintf("error marking the pull-never flag as hidden: %v", err)) } fs.BoolVarP(&flags.Quiet, "quiet", "q", false, "refrain from announcing build instructions and image read/write progress") fs.BoolVar(&flags.OmitHistory, "omit-history", false, "omit build history information from built image") fs.BoolVar(&flags.IdentityLabel, "identity-label", true, "add default identity label") fs.BoolVar(&flags.Rm, "rm", true, "remove intermediate containers after a successful build") // "runtime" definition moved to avoid name collision in podman build. Defined in cmd/buildah/build.go. fs.StringSliceVar(&flags.RuntimeFlags, "runtime-flag", []string{}, "add global flags for the container runtime") fs.StringArrayVar(&flags.TransientRunMounts, "mount", []string{}, "set transient mounts for each RUN instruction, e.g. type=secret,id=mysecret") fs.StringVar(&flags.SbomPreset, "sbom", "", "scan working container using `preset` configuration") fs.StringVar(&flags.SbomScannerImage, "sbom-scanner-image", "", "scan working container using scanner command from `image`") fs.StringArrayVar(&flags.SbomScannerCommand, "sbom-scanner-command", nil, "scan working container using `command` in scanner image") fs.StringVar(&flags.SbomMergeStrategy, "sbom-merge-strategy", "", "merge scan results using `strategy`") fs.StringVar(&flags.SbomOutput, "sbom-output", "", "save scan results to `file`") fs.StringVar(&flags.SbomImgOutput, "sbom-image-output", "", "add scan results to image as `path`") fs.StringVar(&flags.SbomPurlOutput, "sbom-purl-output", "", "save scan results to `file``") fs.StringVar(&flags.SbomImgPurlOutput, "sbom-image-purl-output", "", "add scan results to image as `path`") fs.StringArrayVar(&flags.Secrets, "secret", []string{}, "secret file to expose to the build") fs.StringVar(&flags.SignBy, "sign-by", "", "sign the image using a GPG key with the specified `FINGERPRINT`") fs.StringVar(&flags.SignaturePolicy, "signature-policy", "", "`pathname` of signature policy file (not usually used)") if err := fs.MarkHidden("signature-policy"); err != nil { panic(fmt.Sprintf("error marking the signature-policy flag as hidden: %v", err)) } fs.StringVar(&flags.SourcePolicyFile, "source-policy-file", "", "`pathname` of source policy file for controlling source references during build") fs.BoolVar(&flags.SkipUnusedStages, "skip-unused-stages", true, "skips stages in multi-stage builds which do not affect the final target") sourceDateEpochUsageDefault := ", defaults to current time" if v := os.Getenv(internal.SourceDateEpochName); v != "" { sourceDateEpochUsageDefault = "" } fs.StringVar(&flags.SourceDateEpoch, "source-date-epoch", os.Getenv(internal.SourceDateEpochName), "set new timestamps in image info to `seconds` after the epoch"+sourceDateEpochUsageDefault) fs.BoolVar(&flags.RewriteTimestamp, "rewrite-timestamp", false, "set timestamps in layers to no later than the value for --source-date-epoch") fs.BoolVar(&flags.Squash, "squash", false, "squash all image layers into a single layer") fs.StringArrayVar(&flags.SSH, "ssh", []string{}, "SSH agent socket or keys to expose to the build. (format: default|[=|[,]])") fs.BoolVar(&flags.Stdin, "stdin", false, "pass stdin into containers") fs.StringArrayVarP(&flags.Tag, "tag", "t", []string{}, "tagged `name` to apply to the built image") fs.StringArrayVarP(&flags.BuildOutputs, "output", "o", nil, "output destination (format: type=local,dest=path)") fs.StringVar(&flags.Target, "target", "", "set the target build stage to build") fs.Int64Var(&flags.Timestamp, "timestamp", 0, "set new timestamps in image info and layer to `seconds` after the epoch, defaults to current times") fs.BoolVar(&flags.TLSVerify, "tls-verify", true, "require HTTPS and verify certificates when accessing the registry") fs.String("variant", "", "override the `variant` of the specified image") fs.StringSliceVar(&flags.UnsetEnvs, "unsetenv", nil, "unset environment variable from final image") fs.StringSliceVar(&flags.UnsetLabels, "unsetlabel", nil, "unset label when inheriting labels from base image") fs.StringSliceVar(&flags.UnsetAnnotations, "unsetannotation", nil, "unset annotation when inheriting annotations from base image") return fs } // GetBudFlagsCompletions returns the FlagCompletions for the common build flags func GetBudFlagsCompletions() commonComp.FlagCompletions { flagCompletion := commonComp.FlagCompletions{} flagCompletion["annotation"] = commonComp.AutocompleteNone flagCompletion["arch"] = commonComp.AutocompleteNone flagCompletion["authfile"] = commonComp.AutocompleteDefault flagCompletion["build-arg"] = commonComp.AutocompleteNone flagCompletion["build-arg-file"] = commonComp.AutocompleteDefault flagCompletion["build-context"] = commonComp.AutocompleteNone flagCompletion["cache-from"] = commonComp.AutocompleteNone flagCompletion["cache-to"] = commonComp.AutocompleteNone flagCompletion["cache-ttl"] = commonComp.AutocompleteNone flagCompletion["cert-dir"] = commonComp.AutocompleteDefault flagCompletion["cpp-flag"] = commonComp.AutocompleteNone flagCompletion["creds"] = commonComp.AutocompleteNone flagCompletion["cw"] = commonComp.AutocompleteNone flagCompletion["env"] = commonComp.AutocompleteNone flagCompletion["file"] = commonComp.AutocompleteDefault flagCompletion["format"] = commonComp.AutocompleteNone flagCompletion["from"] = commonComp.AutocompleteDefault flagCompletion["hooks-dir"] = commonComp.AutocompleteNone flagCompletion["ignorefile"] = commonComp.AutocompleteDefault flagCompletion["iidfile"] = commonComp.AutocompleteDefault flagCompletion["iidfile-raw"] = commonComp.AutocompleteDefault flagCompletion["jobs"] = commonComp.AutocompleteNone flagCompletion["label"] = commonComp.AutocompleteNone flagCompletion["layer-label"] = commonComp.AutocompleteNone flagCompletion["logfile"] = commonComp.AutocompleteDefault flagCompletion["manifest"] = commonComp.AutocompleteDefault flagCompletion["metadata-file"] = commonComp.AutocompleteDefault flagCompletion["mount"] = commonComp.AutocompleteNone flagCompletion["os"] = commonComp.AutocompleteNone flagCompletion["os-feature"] = commonComp.AutocompleteNone flagCompletion["os-version"] = commonComp.AutocompleteNone flagCompletion["output"] = commonComp.AutocompleteNone flagCompletion["pull"] = commonComp.AutocompleteDefault flagCompletion["runtime-flag"] = commonComp.AutocompleteNone flagCompletion["sbom"] = commonComp.AutocompleteNone flagCompletion["sbom-scanner-image"] = commonComp.AutocompleteNone flagCompletion["sbom-scanner-command"] = commonComp.AutocompleteNone flagCompletion["sbom-merge-strategy"] = commonComp.AutocompleteNone flagCompletion["sbom-output"] = commonComp.AutocompleteDefault flagCompletion["sbom-image-output"] = commonComp.AutocompleteNone flagCompletion["sbom-purl-output"] = commonComp.AutocompleteDefault flagCompletion["sbom-image-purl-output"] = commonComp.AutocompleteNone flagCompletion["secret"] = commonComp.AutocompleteNone flagCompletion["sign-by"] = commonComp.AutocompleteNone flagCompletion["signature-policy"] = commonComp.AutocompleteNone flagCompletion["source-policy-file"] = commonComp.AutocompleteDefault flagCompletion["ssh"] = commonComp.AutocompleteNone flagCompletion["source-date-epoch"] = commonComp.AutocompleteNone flagCompletion["tag"] = commonComp.AutocompleteNone flagCompletion["target"] = commonComp.AutocompleteNone flagCompletion["timestamp"] = commonComp.AutocompleteNone flagCompletion["unsetenv"] = commonComp.AutocompleteNone flagCompletion["unsetlabel"] = commonComp.AutocompleteNone flagCompletion["unsetannotation"] = commonComp.AutocompleteNone flagCompletion["variant"] = commonComp.AutocompleteNone return flagCompletion } // GetFromAndBudFlags returns from and build flags func GetFromAndBudFlags(flags *FromAndBudResults, usernsResults *UserNSResults, namespaceResults *NameSpaceResults) (pflag.FlagSet, error) { fs := pflag.FlagSet{} defaultContainerConfig, err := config.Default() if err != nil { return fs, fmt.Errorf("failed to get default container config: %w", err) } fs.StringSliceVar(&flags.AddHost, "add-host", []string{}, "add a custom host-to-IP mapping (`host:ip`) (default [])") fs.StringVar(&flags.BlobCache, "blob-cache", "", "assume image blobs in the specified directory will be available for pushing") if err := fs.MarkHidden("blob-cache"); err != nil { panic(fmt.Sprintf("error marking net flag as hidden: %v", err)) } fs.StringSliceVar(&flags.CapAdd, "cap-add", []string{}, "add the specified capability when running (default [])") fs.StringSliceVar(&flags.CapDrop, "cap-drop", []string{}, "drop the specified capability when running (default [])") fs.StringVar(&flags.CDIConfigDir, "cdi-config-dir", "", "`directory` of CDI configuration files") _ = fs.MarkHidden("cdi-config-dir") fs.StringVar(&flags.CgroupParent, "cgroup-parent", "", "optional parent cgroup for the container") fs.Uint64Var(&flags.CPUPeriod, "cpu-period", 0, "limit the CPU CFS (Completely Fair Scheduler) period") fs.Int64Var(&flags.CPUQuota, "cpu-quota", 0, "limit the CPU CFS (Completely Fair Scheduler) quota") fs.Uint64VarP(&flags.CPUShares, "cpu-shares", "c", 0, "CPU shares (relative weight)") fs.StringVar(&flags.CPUSetCPUs, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)") fs.StringVar(&flags.CPUSetMems, "cpuset-mems", "", "memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems.") fs.StringSliceVar(&flags.DecryptionKeys, "decryption-key", nil, "key needed to decrypt the image") fs.StringArrayVar(&flags.Devices, "device", defaultContainerConfig.Containers.Devices.Get(), "additional devices to provide") fs.StringSliceVar(&flags.DNSSearch, "dns-search", defaultContainerConfig.Containers.DNSSearches.Get(), "set custom DNS search domains") fs.StringSliceVar(&flags.DNSServers, "dns", defaultContainerConfig.Containers.DNSServers.Get(), "set custom DNS servers or disable it completely by setting it to 'none', which prevents the automatic creation of `/etc/resolv.conf`.") fs.StringSliceVar(&flags.DNSOptions, "dns-option", defaultContainerConfig.Containers.DNSOptions.Get(), "set custom DNS options") fs.BoolVar(&flags.HTTPProxy, "http-proxy", true, "pass through HTTP Proxy environment variables") fs.StringVar(&flags.Isolation, "isolation", DefaultIsolation(), "`type` of process isolation to use. Use BUILDAH_ISOLATION environment variable to override.") fs.StringVarP(&flags.Memory, "memory", "m", "", "memory limit (format: [], where unit = b, k, m or g)") fs.StringVar(&flags.MemorySwap, "memory-swap", "", "swap limit equal to memory plus swap: '-1' to enable unlimited swap") fs.IntVar(&flags.Retry, "retry", int(defaultContainerConfig.Engine.Retry), "number of times to retry in case of failure when performing push/pull") fs.StringVar(&flags.RetryDelay, "retry-delay", defaultContainerConfig.Engine.RetryDelay, "delay between retries in case of push/pull failures") fs.String("arch", runtime.GOARCH, "set the ARCH of the image to the provided value instead of the architecture of the host") fs.String("os", runtime.GOOS, "prefer `OS` instead of the running OS when pulling images") fs.StringSlice("platform", []string{parse.DefaultPlatform()}, "set the `OS/ARCH[/VARIANT]` of the image to the provided value instead of the current operating system and architecture of the host (for example \"linux/arm\")") fs.String("variant", "", "override the `variant` of the specified image") fs.StringArrayVar(&flags.SecurityOpt, "security-opt", []string{}, "security options (default [])") fs.StringVar(&flags.ShmSize, "shm-size", defaultContainerConfig.Containers.ShmSize, "size of '/dev/shm'. The format is ``.") fs.StringSliceVar(&flags.Ulimit, "ulimit", defaultContainerConfig.Containers.DefaultUlimits.Get(), "ulimit options") fs.StringArrayVarP(&flags.Volumes, "volume", "v", defaultContainerConfig.Volumes(), "bind mount a volume into the container") // Add in the usernamespace and namespaceflags usernsFlags := GetUserNSFlags(usernsResults) namespaceFlags := GetNameSpaceFlags(namespaceResults) fs.AddFlagSet(&usernsFlags) fs.AddFlagSet(&namespaceFlags) return fs, nil } // GetFromAndBudFlagsCompletions returns the FlagCompletions for the from and build flags func GetFromAndBudFlagsCompletions() commonComp.FlagCompletions { flagCompletion := commonComp.FlagCompletions{} flagCompletion["arch"] = commonComp.AutocompleteNone flagCompletion["add-host"] = commonComp.AutocompleteNone flagCompletion["blob-cache"] = commonComp.AutocompleteNone flagCompletion["cap-add"] = commonComp.AutocompleteCapabilities flagCompletion["cap-drop"] = commonComp.AutocompleteCapabilities flagCompletion["cgroup-parent"] = commonComp.AutocompleteDefault // FIXME: This would be a path right?! flagCompletion["cpu-period"] = commonComp.AutocompleteNone flagCompletion["cpu-quota"] = commonComp.AutocompleteNone flagCompletion["cpu-shares"] = commonComp.AutocompleteNone flagCompletion["cpuset-cpus"] = commonComp.AutocompleteNone flagCompletion["cpuset-mems"] = commonComp.AutocompleteNone flagCompletion["decryption-key"] = commonComp.AutocompleteNone flagCompletion["device"] = commonComp.AutocompleteDefault flagCompletion["dns-search"] = commonComp.AutocompleteNone flagCompletion["dns"] = commonComp.AutocompleteNone flagCompletion["dns-option"] = commonComp.AutocompleteNone flagCompletion["isolation"] = commonComp.AutocompleteNone flagCompletion["memory"] = commonComp.AutocompleteNone flagCompletion["memory-swap"] = commonComp.AutocompleteNone flagCompletion["os"] = commonComp.AutocompleteNone flagCompletion["platform"] = commonComp.AutocompleteNone flagCompletion["retry"] = commonComp.AutocompleteNone flagCompletion["retry-delay"] = commonComp.AutocompleteNone flagCompletion["security-opt"] = commonComp.AutocompleteNone flagCompletion["shm-size"] = commonComp.AutocompleteNone flagCompletion["ulimit"] = commonComp.AutocompleteNone flagCompletion["volume"] = commonComp.AutocompleteDefault flagCompletion["variant"] = commonComp.AutocompleteNone // Add in the usernamespace and namespace flag completions userNsComp := GetUserNSFlagsCompletions() maps.Copy(flagCompletion, userNsComp) namespaceComp := GetNameSpaceFlagsCompletions() maps.Copy(flagCompletion, namespaceComp) return flagCompletion } // UseLayers returns true if BUILDAH_LAYERS is set to "1" or "true" // otherwise it returns false func UseLayers() bool { layers := os.Getenv("BUILDAH_LAYERS") if strings.ToLower(layers) == "true" || layers == "1" { return true } return false } // DefaultFormat returns the default image format func DefaultFormat() string { format := os.Getenv("BUILDAH_FORMAT") if format != "" { return format } return define.OCI } // DefaultIsolation returns the default image format func DefaultIsolation() string { isolation := os.Getenv("BUILDAH_ISOLATION") if isolation != "" { return isolation } if unshare.IsRootless() { return "rootless" } return define.OCI } // DefaultHistory returns the default add-history setting func DefaultHistory() bool { history := os.Getenv("BUILDAH_HISTORY") if strings.ToLower(history) == "true" || history == "1" { return true } return false } func VerifyFlagsArgsOrder(args []string) error { for _, arg := range args { if strings.HasPrefix(arg, "-") { return fmt.Errorf("no options (%s) can be specified after the image or container name", arg) } } return nil } // AliasFlags is a function to handle backwards compatibility with old flags func AliasFlags(_ *pflag.FlagSet, name string) pflag.NormalizedName { switch name { case "net": name = "network" case "override-arch": name = "arch" case "override-os": name = "os" case "purge": name = "rm" case "raw-iidfile": name = "iidfile-raw" case "tty": name = "terminal" } return pflag.NormalizedName(name) } // LookupEnvVarReferences returns a copy of specs with keys and values resolved // from environ. Strings are in "key=value" form, the same as [os.Environ]. // // - When a string in specs lacks "=", it is treated as a key and the value // is retrieved from environ. When the key is missing from environ, neither // the key nor value are returned. // // - When a string in specs lacks "=" and ends with "*", it is treated as // a key prefix and any keys with the same prefix in environ are returned. // // - When a string in specs is exactly "*", all keys and values in environ // are returned. func LookupEnvVarReferences(specs, environ []string) []string { result := make([]string, 0, len(specs)) for _, spec := range specs { if key, _, ok := strings.Cut(spec, "="); ok { result = append(result, spec) } else if key == "*" { result = append(result, environ...) } else { prefix := key + "=" if strings.HasSuffix(key, "*") { prefix = strings.TrimSuffix(key, "*") } for _, spec := range environ { if strings.HasPrefix(spec, prefix) { result = append(result, spec) } } } } return result } // DecryptConfig translates decryptionKeys into a DescriptionConfig structure func DecryptConfig(decryptionKeys []string) (*encconfig.DecryptConfig, error) { var decryptConfig *encconfig.DecryptConfig if len(decryptionKeys) > 0 { // decryption dcc, err := enchelpers.CreateCryptoConfig([]string{}, decryptionKeys) if err != nil { return nil, fmt.Errorf("invalid decryption keys: %w", err) } cc := encconfig.CombineCryptoConfigs([]encconfig.CryptoConfig{dcc}) decryptConfig = cc.DecryptConfig } return decryptConfig, nil } // EncryptConfig translates encryptionKeys into a EncriptionsConfig structure func EncryptConfig(encryptionKeys []string, encryptLayers []int) (*encconfig.EncryptConfig, *[]int, error) { var encLayers *[]int var encConfig *encconfig.EncryptConfig if len(encryptionKeys) > 0 { // encryption encLayers = &encryptLayers ecc, err := enchelpers.CreateCryptoConfig(encryptionKeys, []string{}) if err != nil { return nil, nil, fmt.Errorf("invalid encryption keys: %w", err) } cc := encconfig.CombineCryptoConfigs([]encconfig.CryptoConfig{ecc}) encConfig = cc.EncryptConfig } return encConfig, encLayers, nil } // GetFormat translates format string into either docker or OCI format constant func GetFormat(format string) (string, error) { switch format { case define.OCI: return define.OCIv1ImageManifest, nil case define.DOCKER: return define.Dockerv2ImageManifest, nil default: return "", fmt.Errorf("unrecognized image type %q", format) } } ================================================ FILE: pkg/cli/common_test.go ================================================ package cli import ( "testing" "github.com/containers/buildah/define" "github.com/spf13/pflag" "github.com/stretchr/testify/assert" "go.podman.io/common/pkg/completion" ) func testFlagCompletion(t *testing.T, flags pflag.FlagSet, flagCompletions completion.FlagCompletions) { // lookup if for each flag a flag completion function exists flags.VisitAll(func(f *pflag.Flag) { // skip hidden and deprecated flags if f.Hidden || len(f.Deprecated) > 0 { return } if _, ok := flagCompletions[f.Name]; !ok && f.Value.Type() != "bool" { t.Errorf("Flag %q has no shell completion function set.", f.Name) } else if ok && f.Value.Type() == "bool" { // make sure bool flags don't have a completion function t.Errorf(`Flag %q is a bool flag but has a shell completion function set. You have to remove this shell completion function.`, f.Name) return } }) // make sure no unnecessary flag completion functions are defined for name := range flagCompletions { if flag := flags.Lookup(name); flag == nil { t.Errorf("Flag %q does not exist but has a shell completion function set.", name) } } } func TestUserNsFlagsCompletion(t *testing.T) { t.Parallel() flags := GetUserNSFlags(&UserNSResults{}) flagCompletions := GetUserNSFlagsCompletions() testFlagCompletion(t, flags, flagCompletions) } func TestNameSpaceFlagsCompletion(t *testing.T) { t.Parallel() flags := GetNameSpaceFlags(&NameSpaceResults{}) flagCompletions := GetNameSpaceFlagsCompletions() testFlagCompletion(t, flags, flagCompletions) } func TestBudFlagsCompletion(t *testing.T) { t.Parallel() flags := GetBudFlags(&BudResults{}) flagCompletions := GetBudFlagsCompletions() testFlagCompletion(t, flags, flagCompletions) } func TestFromAndBudFlagsCompletions(t *testing.T) { t.Parallel() flags, err := GetFromAndBudFlags(&FromAndBudResults{}, &UserNSResults{}, &NameSpaceResults{}) if err != nil { t.Error("Could load the from and build flags.") } flagCompletions := GetFromAndBudFlagsCompletions() testFlagCompletion(t, flags, flagCompletions) } func TestLookupEnvVarReferences(t *testing.T) { t.Parallel() t.Run("EmptyInput", func(t *testing.T) { assert.Empty(t, LookupEnvVarReferences(nil, nil)) assert.Empty(t, LookupEnvVarReferences([]string{}, nil)) }) t.Run("EmptyEnvironment", func(t *testing.T) { assert.Equal(t, []string{"a=b"}, LookupEnvVarReferences([]string{"a=b"}, nil)) assert.Equal(t, []string{"a="}, LookupEnvVarReferences([]string{"a="}, nil)) assert.Equal(t, []string{}, LookupEnvVarReferences([]string{"a"}, nil)) assert.Equal(t, []string{}, LookupEnvVarReferences([]string{"*"}, nil)) }) t.Run("MissingEnvironment", func(t *testing.T) { assert.Equal(t, []string{"a=b", "c="}, LookupEnvVarReferences([]string{"a=b", "c="}, []string{"x=y"})) assert.Equal(t, []string{"a=b"}, LookupEnvVarReferences([]string{"a=b", "c"}, []string{"x=y"})) assert.Equal(t, []string{"a=b"}, LookupEnvVarReferences([]string{"a=b", "c*"}, []string{"x=y"})) }) t.Run("MatchingEnvironment", func(t *testing.T) { assert.Equal(t, []string{"a=b", "c="}, LookupEnvVarReferences([]string{"a=b", "c="}, []string{"c=d", "x=y"})) assert.Equal(t, []string{"a=b", "c=d"}, LookupEnvVarReferences([]string{"a=b", "c"}, []string{"c=d", "x=y"})) assert.Equal(t, []string{"a=b", "c=d"}, LookupEnvVarReferences([]string{"a=b", "c*"}, []string{"c=d", "x=y"})) assert.Equal(t, []string{"a=b", "c=d", "cg=i"}, LookupEnvVarReferences([]string{"a=b", "c*"}, []string{"c=d", "x=y", "cg=i"})) }) t.Run("MultipleMatches", func(t *testing.T) { assert.Equal(t, []string{"a=b", "c=d", "cg=i", "c=d", "x=y", "cg=i", "cg=i"}, LookupEnvVarReferences([]string{"a=b", "c*", "*", "cg*"}, []string{"c=d", "x=y", "cg=i"})) }) } func TestDecryptConfig(t *testing.T) { t.Parallel() // Just a smoke test for the default path. res, err := DecryptConfig(nil) assert.NoError(t, err) assert.Nil(t, res) } func TestEncryptConfig(t *testing.T) { t.Parallel() // Just a smoke test for the default path. cfg, layers, err := EncryptConfig(nil, nil) assert.NoError(t, err) assert.Nil(t, cfg) assert.Nil(t, layers) } func TestGetFormat(t *testing.T) { t.Parallel() _, err := GetFormat("bogus") assert.NotNil(t, err) format, err := GetFormat("oci") assert.Nil(t, err) assert.Equalf(t, define.OCIv1ImageManifest, format, "expected oci format but got %v.", format) format, err = GetFormat("docker") assert.Nil(t, err) assert.Equalf(t, define.Dockerv2ImageManifest, format, "expected docker format but got %v.", format) } ================================================ FILE: pkg/cli/exec_codes.go ================================================ package cli const ( // ExecErrorCodeGeneric is the default error code to return from an exec session if libpod failed // prior to calling the runtime ExecErrorCodeGeneric = 125 // ExecErrorCodeCannotInvoke is the error code to return when the runtime fails to invoke a command // an example of this can be found by trying to execute a directory: // `podman exec -l /etc` ExecErrorCodeCannotInvoke = 126 // ExecErrorCodeNotFound is the error code to return when a command cannot be found ExecErrorCodeNotFound = 127 ) ================================================ FILE: pkg/completion/completion.go ================================================ package completion import ( "strings" "github.com/spf13/cobra" ) /* Autocomplete Functions for cobra ValidArgsFunction */ // AutocompleteNamespaceFlag - Autocomplete the userns flag. // -> host, private, container, ns:[path], [path] func AutocompleteNamespaceFlag(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { var completions []string // If we don't filter on "toComplete", zsh and fish will not do file completion // even if the prefix typed by the user does not match the returned completions for _, comp := range []string{"host", "private", "container", "ns:"} { if strings.HasPrefix(comp, toComplete) { completions = append(completions, comp) } } return completions, cobra.ShellCompDirectiveDefault } ================================================ FILE: pkg/dummy/dummy_test.go ================================================ package dummy import ( "testing" ) func TestDummy(_ *testing.T) { } ================================================ FILE: pkg/formats/doc.go ================================================ // The formats package is an API compatibility wrapper which merely calls into github.com/containers/common/pkg/formats. Newer code should instead call that package directly. package formats ================================================ FILE: pkg/formats/formats.go ================================================ package formats import ( "go.podman.io/common/pkg/formats" ) const ( // JSONString const to save on duplicate variable names JSONString = formats.JSONString // IDString const to save on duplicates for Go templates IDString = formats.IDString ) // Writer interface for outputs type Writer = formats.Writer // JSONStructArray for JSON output type JSONStructArray = formats.JSONStructArray // StdoutTemplateArray for Go template output type StdoutTemplateArray = formats.StdoutTemplateArray // JSONStruct for JSON output type JSONStruct = formats.JSONStruct // StdoutTemplate for Go template output type StdoutTemplate = formats.StdoutTemplate // YAMLStruct for YAML output type YAMLStruct = formats.YAMLStruct ================================================ FILE: pkg/formats/templates.go ================================================ package formats import ( "text/template" "go.podman.io/common/pkg/formats" ) // Parse creates a new anonymous template with the basic functions // and parses the given format. func Parse(format string) (*template.Template, error) { return formats.Parse(format) } // NewParse creates a new tagged template with the basic functions // and parses the given format. func NewParse(tag, format string) (*template.Template, error) { return formats.NewParse(tag, format) } ================================================ FILE: pkg/jail/jail.go ================================================ //go:build freebsd // +build freebsd package jail import ( "fmt" "strconv" "strings" "sync" "syscall" "unsafe" "github.com/containers/buildah/pkg/util" "github.com/sirupsen/logrus" "golang.org/x/sys/unix" ) type NS int32 const ( DISABLED NS = 0 NEW NS = 1 INHERIT NS = 2 JAIL_CREATE = 0x01 JAIL_UPDATE = 0x02 JAIL_ATTACH = 0x04 ) type config struct { params map[string]any } var ( needVnetJailOnce sync.Once needVnetJail bool ) func NewConfig() *config { return &config{ params: make(map[string]any), } } func handleBoolSetting(key string, val bool) (string, any) { // jail doesn't deal with booleans - it uses paired parameter // names, e.g. "persist"/"nopersist". If the key contains '.', // the "no" prefix is applied to the last element. if val == false { parts := strings.Split(key, ".") parts[len(parts)-1] = "no" + parts[len(parts)-1] key = strings.Join(parts, ".") } return key, nil } func (c *config) Set(key string, value any) { // Normalise integer types to int32 switch v := value.(type) { case int: value = int32(v) case uint32: value = int32(v) } switch key { case "jid", "devfs_ruleset", "enforce_statfs", "children.max", "securelevel": if _, ok := value.(int32); !ok { logrus.Fatalf("value for parameter %s must be an int32", key) } case "ip4", "ip6", "host", "vnet": nsval, ok := value.(NS) if !ok { logrus.Fatalf("value for parameter %s must be a jail.NS", key) } if (key == "host" || key == "vnet") && nsval == DISABLED { logrus.Fatalf("value for parameter %s cannot be DISABLED", key) } case "persist", "sysvmsg", "sysvsem", "sysvshm": bval, ok := value.(bool) if !ok { logrus.Fatalf("value for parameter %s must be bool", key) } key, value = handleBoolSetting(key, bval) default: if strings.HasPrefix(key, "allow.") { bval, ok := value.(bool) if !ok { logrus.Fatalf("value for parameter %s must be bool", key) } key, value = handleBoolSetting(key, bval) } else { if _, ok := value.(string); !ok { logrus.Fatalf("value for parameter %s must be a string", key) } } } c.params[key] = value } func (c *config) getIovec() ([]syscall.Iovec, error) { jiov := make([]syscall.Iovec, 0) for key, value := range c.params { iov, err := stringToIovec(key) if err != nil { return nil, err } jiov = append(jiov, iov) switch v := value.(type) { case string: iov, err := stringToIovec(v) if err != nil { return nil, err } jiov = append(jiov, iov) case int32: jiov = append(jiov, syscall.Iovec{ Base: (*byte)(unsafe.Pointer(&v)), Len: 4, }) case NS: jiov = append(jiov, syscall.Iovec{ Base: (*byte)(unsafe.Pointer(&v)), Len: 4, }) default: jiov = append(jiov, syscall.Iovec{ Base: nil, Len: 0, }) } } return jiov, nil } type jail struct { jid int32 } func jailSet(jconf *config, flags int) (*jail, error) { jiov, err := jconf.getIovec() if err != nil { return nil, err } jid, _, errno := syscall.Syscall(unix.SYS_JAIL_SET, uintptr(unsafe.Pointer(&jiov[0])), uintptr(len(jiov)), uintptr(flags)) if errno != 0 { return nil, errno } return &jail{ jid: int32(jid), }, nil } func jailGet(jconf *config, flags int) (*jail, error) { jiov, err := jconf.getIovec() if err != nil { return nil, err } jid, _, errno := syscall.Syscall(unix.SYS_JAIL_GET, uintptr(unsafe.Pointer(&jiov[0])), uintptr(len(jiov)), uintptr(flags)) if errno != 0 { return nil, errno } return &jail{ jid: int32(jid), }, nil } func Create(jconf *config) (*jail, error) { return jailSet(jconf, JAIL_CREATE) } func CreateAndAttach(jconf *config) (*jail, error) { return jailSet(jconf, JAIL_CREATE|JAIL_ATTACH) } func FindByName(name string) (*jail, error) { jconf := NewConfig() jconf.Set("name", name) return jailGet(jconf, 0) } func (j *jail) Set(jconf *config) error { jconf.Set("jid", j.jid) _, err := jailSet(jconf, JAIL_UPDATE) return err } func parseVersion(version string) (string, int, int, int, error) { // Expected formats: // .-RELEASE optionally followed by -p // -STABLE // -CURRENT parts := strings.Split(string(version), "-") if len(parts) < 2 || len(parts) > 3 { return "", -1, -1, -1, fmt.Errorf("unexpected OS version: %s", version) } ver := strings.Split(parts[0], ".") if len(ver) != 2 { return "", -1, -1, -1, fmt.Errorf("unexpected OS version: %s", version) } major, err := strconv.Atoi(ver[0]) if err != nil { return "", -1, -1, -1, fmt.Errorf("unexpected OS version: %s", version) } minor, err := strconv.Atoi(ver[1]) if err != nil { return "", -1, -1, -1, fmt.Errorf("unexpected OS version: %s", version) } patchlevel := 0 if len(parts) == 3 { if parts[1] != "RELEASE" || !strings.HasPrefix(parts[2], "p") { return "", -1, -1, -1, fmt.Errorf("unexpected OS version: %s", version) } patchlevel, err = strconv.Atoi(strings.TrimPrefix(parts[2], "p")) if err != nil { return "", -1, -1, -1, fmt.Errorf("unexpected OS version: %s", version) } } return parts[1], major, minor, patchlevel, nil } // Return true if its necessary to have a separate jail to own the vnet. For // FreeBSD 13.3 and later, we don't need a separate vnet jail since it is // possible to configure the network without either attaching to the container's // jail or trusting the ifconfig and route utilities in the container. If for // any reason, we fail to parse the OS version, we default to returning true. func NeedVnetJail() bool { needVnetJailOnce.Do(func() { // FreeBSD 13.3 and later have support for 'ifconfig -j' and 'route -j' needVnetJail = true version, err := util.ReadKernelVersion() if err != nil { logrus.Errorf("failed to determine OS version: %v", err) return } _, major, minor, _, err := parseVersion(version) if major > 13 || (major == 13 && minor > 2) { needVnetJail = false } }) return needVnetJail } ================================================ FILE: pkg/jail/jail_int32.go ================================================ //go:build (386 || arm) && freebsd // +build 386 arm // +build freebsd package jail import ( "syscall" ) func stringToIovec(val string) (syscall.Iovec, error) { bs, err := syscall.ByteSliceFromString(val) if err != nil { return syscall.Iovec{}, err } var res syscall.Iovec res.Base = &bs[0] res.Len = uint32(len(bs)) return res, nil } ================================================ FILE: pkg/jail/jail_int64.go ================================================ //go:build !(386 || arm) && freebsd // +build !386,!arm,freebsd package jail import ( "syscall" ) func stringToIovec(val string) (syscall.Iovec, error) { bs, err := syscall.ByteSliceFromString(val) if err != nil { return syscall.Iovec{}, err } var res syscall.Iovec res.Base = &bs[0] res.Len = uint64(len(bs)) return res, nil } ================================================ FILE: pkg/jail/jail_test.go ================================================ //go:build freebsd // +build freebsd package jail import ( "testing" "github.com/stretchr/testify/assert" ) func TestParseVersion(t *testing.T) { t.Parallel() tt := []struct { version string shouldFail bool kind string major, minor, patchlevel int }{ {"14.0-RELEASE", false, "RELEASE", 14, 0, 0}, {"14.0-RELEASE-p3", false, "RELEASE", 14, 0, 3}, {"13.2-STABLE", false, "STABLE", 13, 2, 0}, {"14.0-STABLE", false, "STABLE", 14, 0, 0}, {"15.0-CURRENT", false, "CURRENT", 15, 0, 0}, {"14-RELEASE", true, "", -1, -1, -1}, {"14.1-STABLE-p1", true, "", -1, -1, -1}, {"14-RELEASE-p3", true, "", -1, -1, -1}, } for _, tc := range tt { kind, major, minor, patchlevel, err := parseVersion(tc.version) if tc.shouldFail { assert.Error(t, err) } else { assert.NoError(t, err) assert.Equal(t, kind, tc.kind) assert.Equal(t, major, tc.major) assert.Equal(t, minor, tc.minor) assert.Equal(t, patchlevel, tc.patchlevel) } } } ================================================ FILE: pkg/manifests/compat.go ================================================ // This package is deprecated. Its functionality has been moved to // github.com/containers/common/pkg/manifests, which provides the same API. // The stubs and aliases here are present for compatibility with older code. // New implementations should use github.com/containers/common/pkg/manifests // directly. package manifests import "go.podman.io/common/pkg/manifests" // List is an alias for github.com/containers/common/pkg/manifests.List. type List = manifests.List var ( // ErrDigestNotFound is an alias for github.com/containers/common/pkg/manifests.ErrDigestNotFound. ErrDigestNotFound = manifests.ErrDigestNotFound // ErrManifestTypeNotSupported is an alias for github.com/containers/common/pkg/manifests.ErrManifestTypeNotSupported. ErrManifestTypeNotSupported = manifests.ErrManifestTypeNotSupported ) // Create wraps github.com/containers/common/pkg/manifests.Create(). func Create() List { return manifests.Create() } // FromBlob wraps github.com/containers/common/pkg/manifests.FromBlob(). func FromBlob(manifestBytes []byte) (List, error) { return manifests.FromBlob(manifestBytes) } ================================================ FILE: pkg/overlay/overlay.go ================================================ package overlay import ( "errors" "fmt" "os" "os/exec" "path/filepath" "strings" "syscall" "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" "go.podman.io/storage/pkg/idtools" "go.podman.io/storage/pkg/mount" "go.podman.io/storage/pkg/system" "go.podman.io/storage/pkg/unshare" ) // Options for MountWithOptions(). type Options struct { // The Upper directory is normally the writable layer in an overlay mount. // Note!! : Following API does not handle escaping or validate the correctness of the values // passed to UpperDirOptionFragment; instead the API will try to pass values as-given // to the `mount` command. It is the caller's responsibility to make sure they pre-validate // these values. Invalid inputs may lead to undefined behaviour. // This is provided as-is, use it if it works for you, we can/will change/break that in the future. // See discussion here for more context: https://github.com/containers/buildah/pull/3715#discussion_r786036959 // TODO: Should we address above comment and handle escaping of metacharacters like // `comma`, `backslash` ,`colon` and any other special characters UpperDirOptionFragment string // The Workdir is used to prepare files as they are switched between the layers. // Note!! : Following API does not handle escaping or validate the correctness of the values // passed to WorkDirOptionFragment; instead the API will try to pass values as-given // to the `mount` command. It is the caller's responsibility to make sure they pre-validate // these values. Invalid inputs may lead to undefined behaviour. // This is provided as-is, use it if it works for you, we can/will change/break that in the future. // See discussion here for more context: https://github.com/containers/buildah/pull/3715#discussion_r786036959 // TODO: Should we address above comment and handle escaping of metacharacters like // `comma`, `backslash` ,`colon` and any other special characters WorkDirOptionFragment string // Graph options being used by the caller, will be searched when choosing mount program GraphOpts []string // Mark if following overlay is read only ReadOnly bool // Deprecated: RootUID is not used RootUID int // Deprecated: RootGID is not used RootGID int // Force overlay mounting and return a bind mount, rather than // attempting to optimize by having the runtime actually mount and // manage the overlay filesystem. ForceMount bool // MountLabel is a label to force for the overlay filesystem. MountLabel string } // TempDir generates a uniquely-named directory under ${containerDir}/overlay // which can be used as a parent directory for the upper and working // directories for an overlay mount, creates "upper" and "work" directories // beneath it, and then returns the path of the new directory. func TempDir(containerDir string, rootUID, rootGID int) (string, error) { contentDir := filepath.Join(containerDir, "overlay") if err := idtools.MkdirAllAndChown(contentDir, 0o700, idtools.IDPair{UID: rootUID, GID: rootGID}); err != nil { return "", fmt.Errorf("failed to create the overlay %s directory: %w", contentDir, err) } contentDir, err := os.MkdirTemp(contentDir, "") if err != nil { return "", fmt.Errorf("failed to create the overlay tmpdir in %s directory: %w", contentDir, err) } return contentDir, generateOverlayStructure(contentDir, rootUID, rootGID) } // GenerateStructure generates an overlay directory structure for container content func GenerateStructure(containerDir, containerID, name string, rootUID, rootGID int) (string, error) { contentDir := filepath.Join(containerDir, "overlay-containers", containerID, name) if err := idtools.MkdirAllAndChown(contentDir, 0o700, idtools.IDPair{UID: rootUID, GID: rootGID}); err != nil { return "", fmt.Errorf("failed to create the overlay %s directory: %w", contentDir, err) } return contentDir, generateOverlayStructure(contentDir, rootUID, rootGID) } // generateOverlayStructure generates upper, work and merge directories under the specified directory func generateOverlayStructure(containerDir string, rootUID, rootGID int) error { upperDir := filepath.Join(containerDir, "upper") workDir := filepath.Join(containerDir, "work") if err := idtools.MkdirAllAndChown(upperDir, 0o700, idtools.IDPair{UID: rootUID, GID: rootGID}); err != nil { return fmt.Errorf("creating overlay upper directory %s: %w", upperDir, err) } if err := idtools.MkdirAllAndChown(workDir, 0o700, idtools.IDPair{UID: rootUID, GID: rootGID}); err != nil { return fmt.Errorf("creating overlay work directory %s: %w", workDir, err) } mergeDir := filepath.Join(containerDir, "merge") if err := idtools.MkdirAllAndChown(mergeDir, 0o700, idtools.IDPair{UID: rootUID, GID: rootGID}); err != nil { return fmt.Errorf("creating overlay merge directory %s: %w", mergeDir, err) } return nil } // Mount creates a subdir of the contentDir based on the source directory // from the source system. It then mounts up the source directory on to the // generated mount point and returns the mount point to the caller. func Mount(contentDir, source, dest string, rootUID, rootGID int, graphOptions []string) (mount specs.Mount, Err error) { overlayOpts := Options{GraphOpts: graphOptions, ReadOnly: false, RootUID: rootUID, RootGID: rootGID} return MountWithOptions(contentDir, source, dest, &overlayOpts) } // MountReadOnly creates a subdir of the contentDir based on the source directory // from the source system. It then mounts up the source directory on to the // generated mount point and returns the mount point to the caller. Note that no // upper layer will be created rendering it a read-only mount func MountReadOnly(contentDir, source, dest string, rootUID, rootGID int, graphOptions []string) (mount specs.Mount, Err error) { overlayOpts := Options{GraphOpts: graphOptions, ReadOnly: true, RootUID: rootUID, RootGID: rootGID} return MountWithOptions(contentDir, source, dest, &overlayOpts) } // findMountProgram finds if any mount program is specified in the graph options. func findMountProgram(graphOptions []string) string { mountMap := map[string]struct{}{ ".mount_program": {}, "overlay.mount_program": {}, "overlay2.mount_program": {}, } for _, i := range graphOptions { s := strings.SplitN(i, "=", 2) if len(s) != 2 { continue } key := s[0] val := s[1] if _, has := mountMap[key]; has { return val } } return "" } // mountWithMountProgram mounts an overlay at mergeDir using the specified // mount program and overlay options. func mountWithMountProgram(mountProgram, overlayOptions, mergeDir string) error { cmd := exec.Command(mountProgram, "-o", overlayOptions, mergeDir) if err := cmd.Run(); err != nil { return fmt.Errorf("exec %s: %w", mountProgram, err) } return nil } // mountNatively mounts an overlay at mergeDir using the kernel's mount() // system call. func mountNatively(overlayOptions, mergeDir string) error { return mount.Mount("overlay", mergeDir, "overlay", overlayOptions) } // Convert ":" to "\:", the path which will be overlay mounted need to be escaped func escapeColon(source string) string { return strings.ReplaceAll(source, ":", "\\:") } // RemoveTemp unmounts a filesystem mounted at ${contentDir}/merge, and then // removes ${contentDir}, which is typically a path returned by TempDir(), // along with any contents it might still have. func RemoveTemp(contentDir string) error { if err := Unmount(contentDir); err != nil { return err } return os.RemoveAll(contentDir) } // Unmount the overlay mountpoint at ${contentDir}/merge, where ${contentDir} // is typically a path returned by TempDir(). The mountpoint itself is left // unmodified. func Unmount(contentDir string) error { mergeDir := filepath.Join(contentDir, "merge") if unshare.IsRootless() { // Attempt to unmount the FUSE mount using either fusermount or fusermount3. // If they fail, fallback to unix.Unmount for _, v := range []string{"fusermount3", "fusermount"} { err := exec.Command(v, "-u", mergeDir).Run() if err != nil && !errors.Is(err, exec.ErrNotFound) { logrus.Debugf("Error unmounting %s with %s - %v", mergeDir, v, err) } if err == nil { return nil } } // If fusermount|fusermount3 failed to unmount the FUSE file system, attempt unmount } // Ignore EINVAL as the specified merge dir is not a mount point if err := system.Unmount(mergeDir); err != nil && !errors.Is(err, os.ErrNotExist) && !errors.Is(err, syscall.EINVAL) { return fmt.Errorf("unmount overlay %s: %w", mergeDir, err) } return nil } // recreate removes a directory tree and then recreates the top of that tree // with the same mode and ownership. func recreate(contentDir string) error { st, err := system.Stat(contentDir) if err != nil { if errors.Is(err, os.ErrNotExist) { return nil } return fmt.Errorf("failed to stat overlay upper directory: %w", err) } if err := os.RemoveAll(contentDir); err != nil { return err } if err := idtools.MkdirAllAndChown(contentDir, os.FileMode(st.Mode()), idtools.IDPair{UID: int(st.UID()), GID: int(st.GID())}); err != nil { return fmt.Errorf("failed to create overlay directory: %w", err) } return nil } // CleanupMount removes all temporary mountpoint content func CleanupMount(contentDir string) (Err error) { if err := recreate(filepath.Join(contentDir, "upper")); err != nil { return err } if err := recreate(filepath.Join(contentDir, "work")); err != nil { return err } return nil } // CleanupContent removes every temporary mountpoint created under // ${containerDir}/overlay as a result of however many calls to TempDir(), // roughly equivalent to calling RemoveTemp() for each of the directories whose // paths it returned, and then removes ${containerDir} itself. func CleanupContent(containerDir string) (Err error) { contentDir := filepath.Join(containerDir, "overlay") files, err := os.ReadDir(contentDir) if err != nil { if errors.Is(err, os.ErrNotExist) { return nil } return fmt.Errorf("read directory: %w", err) } for _, f := range files { dir := filepath.Join(contentDir, f.Name()) if err := Unmount(dir); err != nil { return err } } if err := os.RemoveAll(contentDir); err != nil && !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("failed to cleanup overlay directory: %w", err) } return nil } ================================================ FILE: pkg/overlay/overlay_freebsd.go ================================================ package overlay import ( //"fmt" //"os" //"path/filepath" //"strings" //"syscall" "errors" //"go.podman.io/storage/pkg/unshare" "github.com/opencontainers/runtime-spec/specs-go" ) // MountWithOptions returns a specs.Mount which makes the contents of ${source} // visible at ${dest} in the container. // Options allows the caller to configure whether or not the mount should be // read-only. // This API is used by podman. func MountWithOptions(contentDir, source, dest string, opts *Options) (mount specs.Mount, Err error) { if opts == nil { opts = &Options{} } if opts.ReadOnly { // Read-only overlay mounts can be simulated with nullfs mount.Source = source mount.Destination = dest mount.Type = "nullfs" mount.Options = []string{"ro"} return mount, nil } else { return mount, errors.New("read/write overlay mounts not supported on freebsd") } } ================================================ FILE: pkg/overlay/overlay_linux.go ================================================ package overlay import ( "errors" "fmt" "os" "path/filepath" "strconv" "strings" "syscall" "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/selinux/go-selinux/label" "go.podman.io/storage/pkg/unshare" ) // MountWithOptions creates ${contentDir}/merge, where ${contentDir} was // presumably created and returned by a call to TempDir(), and either mounts a // filesystem there and returns a mounts.Spec which bind-mounts the mountpoint // to ${dest}, or returns a mounts.Spec which mounts a filesystem at ${dest}. // Options allows the caller to configure a custom workdir and upperdir, // indicate whether or not the overlay should be read-only, and provide the // graph driver options that we'll search to determine whether or not we should // be using a mount helper (i.e., fuse-overlayfs). // This API is used by podman. func MountWithOptions(contentDir, source, dest string, opts *Options) (mount specs.Mount, Err error) { if opts == nil { opts = &Options{} } mergeDir := filepath.Join(contentDir, "merge") // Create overlay mount options for rw/ro. var overlayOptions string if opts.ReadOnly { // Read-only overlay mounts require two lower layers. lowerTwo := filepath.Join(contentDir, "lower") if err := os.Mkdir(lowerTwo, 0o755); err != nil { return mount, err } overlayOptions = fmt.Sprintf("lowerdir=%s:%s,private", escapeColon(source), lowerTwo) } else { // Read-write overlay mounts want a lower, upper and a work layer. workDir := filepath.Join(contentDir, "work") upperDir := filepath.Join(contentDir, "upper") if opts.WorkDirOptionFragment != "" && opts.UpperDirOptionFragment != "" { workDir = opts.WorkDirOptionFragment if !filepath.IsAbs(workDir) { workDir = filepath.Join(contentDir, workDir) } upperDir = opts.UpperDirOptionFragment if !filepath.IsAbs(upperDir) { upperDir = filepath.Join(contentDir, upperDir) } } else { st, err := os.Stat(source) if err != nil { return mount, err } if err := os.Chmod(upperDir, st.Mode()); err != nil { return mount, err } if stat, ok := st.Sys().(*syscall.Stat_t); ok { if err := os.Chown(upperDir, int(stat.Uid), int(stat.Gid)); err != nil { if !errors.Is(err, syscall.EINVAL) { return mount, err } overflowed := false overflowUIDText, uerr := os.ReadFile("/proc/sys/kernel/overflowuid") overflowGIDText, gerr := os.ReadFile("/proc/sys/kernel/overflowgid") if uerr == nil && gerr == nil { overflowUID, uerr := strconv.Atoi(strings.TrimSpace(string(overflowUIDText))) overflowGID, gerr := strconv.Atoi(strings.TrimSpace(string(overflowGIDText))) if uerr == nil && gerr == nil && int(stat.Uid) == overflowUID && int(stat.Gid) == overflowGID { overflowed = true } } if !overflowed { return mount, err } } times := []syscall.Timespec{ stat.Atim, stat.Mtim, } if err := syscall.UtimesNano(upperDir, times); err != nil { return mount, err } } } overlayOptions = fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s,private", escapeColon(source), upperDir, workDir) } if opts.MountLabel != "" { overlayOptions = overlayOptions + "," + label.FormatMountLabel("", opts.MountLabel) } mountProgram := findMountProgram(opts.GraphOpts) if mountProgram != "" { if err := mountWithMountProgram(mountProgram, overlayOptions, mergeDir); err != nil { return mount, err } mount.Source = mergeDir mount.Destination = dest mount.Type = "bind" mount.Options = []string{"bind", "slave"} return mount, nil } if unshare.IsRootless() { // If a mount_program is not specified, fallback to try mounting native overlay. overlayOptions = fmt.Sprintf("%s,userxattr", overlayOptions) } mount.Source = mergeDir mount.Destination = dest mount.Type = "overlay" mount.Options = strings.Split(overlayOptions, ",") if opts.ForceMount { if err := mountNatively(overlayOptions, mergeDir); err != nil { return mount, err } mount.Source = mergeDir mount.Destination = dest mount.Type = "bind" mount.Options = []string{"bind", "slave"} return mount, nil } return mount, nil } ================================================ FILE: pkg/overlay/overlay_unsupported.go ================================================ //go:build !freebsd && !linux package overlay import ( "fmt" "runtime" "github.com/opencontainers/runtime-spec/specs-go" ) // MountWithOptions creates a subdir of the contentDir based on the source directory // from the source system. It then mounts up the source directory on to the // generated mount point and returns the mount point to the caller. // But allows api to set custom workdir, upperdir and other overlay options // Following API is being used by podman at the moment func MountWithOptions(contentDir, source, dest string, opts *Options) (mount specs.Mount, err error) { return mount, fmt.Errorf("read/write overlay mounts not supported on %q", runtime.GOOS) } ================================================ FILE: pkg/parse/parse.go ================================================ package parse //nolint:revive,nolintlint // this package should contain functions that parse and validate // user input and is shared either amongst buildah subcommands or // would be useful to projects vendoring buildah import ( "errors" "fmt" "io/fs" "net" "os" "path/filepath" "slices" "strconv" "strings" "unicode" "github.com/containerd/platforms" "github.com/containers/buildah/define" mkcwtypes "github.com/containers/buildah/internal/mkcw/types" internalParse "github.com/containers/buildah/internal/parsevolume" "github.com/containers/buildah/internal/sbom" "github.com/containers/buildah/internal/tmpdir" "github.com/containers/buildah/pkg/sshagent" securejoin "github.com/cyphar/filepath-securejoin" units "github.com/docker/go-units" specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/selinux/go-selinux" "github.com/openshift/imagebuilder" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" "go.podman.io/common/libnetwork/etchosts" "go.podman.io/common/pkg/auth" "go.podman.io/common/pkg/config" "go.podman.io/common/pkg/parse" "go.podman.io/image/v5/docker/reference" "go.podman.io/image/v5/types" "go.podman.io/storage/pkg/fileutils" "go.podman.io/storage/pkg/idtools" "go.podman.io/storage/pkg/unshare" storageTypes "go.podman.io/storage/types" "golang.org/x/term" ) const ( // SeccompDefaultPath defines the default seccomp path SeccompDefaultPath = config.SeccompDefaultPath // SeccompOverridePath if this exists it overrides the default seccomp path SeccompOverridePath = config.SeccompOverridePath // TypeBind is the type for mounting host dir TypeBind = "bind" // TypeTmpfs is the type for mounting tmpfs TypeTmpfs = "tmpfs" // TypeCache is the type for mounting a common persistent cache from host TypeCache = "cache" // mount=type=cache must create a persistent directory on host so it's available for all consecutive builds. // Lifecycle of following directory will be inherited from how host machine treats temporary directory BuildahCacheDir = "buildah-cache" ) var ( errInvalidSecretSyntax = errors.New("incorrect secret flag format: should be --secret id=foo,src=bar[,env=ENV][,type=file|env]") errInvalidBuildContextPathname = errors.New(`invalid build context path ""`) errInvalidBuildContextImage = errors.New(`invalid build context image name ""`) errInvalidBuildContextURL = errors.New(`invalid build context image URL ""`) ) // RepoNamesToNamedReferences parse the raw string to Named reference func RepoNamesToNamedReferences(destList []string) ([]reference.Named, error) { var result []reference.Named for _, dest := range destList { named, err := reference.ParseNormalizedNamed(dest) if err != nil { return nil, fmt.Errorf("invalid repo %q: must contain registry and repository: %w", dest, err) } if !reference.IsNameOnly(named) { return nil, fmt.Errorf("repository must contain neither a tag nor digest: %v", named) } result = append(result, named) } return result, nil } // CommonBuildOptions parses the build options from the bud cli func CommonBuildOptions(c *cobra.Command) (*define.CommonBuildOptions, error) { return CommonBuildOptionsFromFlagSet(c.Flags(), c.Flag) } // If user selected to run with currentLabelOpts then append on the current user and role func currentLabelOpts() ([]string, error) { label, err := selinux.CurrentLabel() if err != nil { return nil, err } if label == "" { return nil, nil } con, err := selinux.NewContext(label) if err != nil { return nil, err } return []string{ fmt.Sprintf("label=user:%s", con["user"]), fmt.Sprintf("label=role:%s", con["role"]), }, nil } // CommonBuildOptionsFromFlagSet parses the build options from the bud cli func CommonBuildOptionsFromFlagSet(flags *pflag.FlagSet, findFlagFunc func(name string) *pflag.Flag) (*define.CommonBuildOptions, error) { var ( memoryLimit int64 memorySwap int64 noDNS bool err error ) memVal, _ := flags.GetString("memory") if memVal != "" { memoryLimit, err = units.RAMInBytes(memVal) if err != nil { return nil, fmt.Errorf("invalid value for memory: %w", err) } } memSwapValue, _ := flags.GetString("memory-swap") if memSwapValue != "" { if memSwapValue == "-1" { memorySwap = -1 } else { memorySwap, err = units.RAMInBytes(memSwapValue) if err != nil { return nil, fmt.Errorf("invalid value for memory-swap: %w", err) } } } noHostname, _ := flags.GetBool("no-hostname") noHosts, _ := flags.GetBool("no-hosts") addHost, _ := flags.GetStringSlice("add-host") if len(addHost) > 0 { if noHosts { return nil, errors.New("--no-hosts and --add-host conflict, can not be used together") } for _, host := range addHost { if err := validateExtraHost(host); err != nil { return nil, fmt.Errorf("invalid value for add-host: %w", err) } } } noDNS = false dnsServers := []string{} if flags.Changed("dns") { dnsServers, _ = flags.GetStringSlice("dns") for _, server := range dnsServers { if strings.ToLower(server) == "none" { noDNS = true } } if noDNS && len(dnsServers) > 1 { return nil, errors.New("invalid --dns, --dns=none may not be used with any other --dns options") } } dnsSearch := []string{} if flags.Changed("dns-search") { dnsSearch, _ = flags.GetStringSlice("dns-search") if noDNS && len(dnsSearch) > 0 { return nil, errors.New("invalid --dns-search, --dns-search may not be used with --dns=none") } } dnsOptions := []string{} if flags.Changed("dns-option") { dnsOptions, _ = flags.GetStringSlice("dns-option") if noDNS && len(dnsOptions) > 0 { return nil, errors.New("invalid --dns-option, --dns-option may not be used with --dns=none") } } if _, err := units.FromHumanSize(findFlagFunc("shm-size").Value.String()); err != nil { return nil, fmt.Errorf("invalid --shm-size: %w", err) } volumes, _ := flags.GetStringArray("volume") cpuPeriod, _ := flags.GetUint64("cpu-period") cpuQuota, _ := flags.GetInt64("cpu-quota") cpuShares, _ := flags.GetUint64("cpu-shares") httpProxy, _ := flags.GetBool("http-proxy") var identityLabel types.OptionalBool if flags.Changed("identity-label") { b, _ := flags.GetBool("identity-label") identityLabel = types.NewOptionalBool(b) } omitHistory, _ := flags.GetBool("omit-history") ulimit := []string{} if flags.Changed("ulimit") { ulimit, _ = flags.GetStringSlice("ulimit") } secrets, _ := flags.GetStringArray("secret") sshsources, _ := flags.GetStringArray("ssh") ociHooks, _ := flags.GetStringArray("hooks-dir") commonOpts := &define.CommonBuildOptions{ AddHost: addHost, CPUPeriod: cpuPeriod, CPUQuota: cpuQuota, CPUSetCPUs: findFlagFunc("cpuset-cpus").Value.String(), CPUSetMems: findFlagFunc("cpuset-mems").Value.String(), CPUShares: cpuShares, CgroupParent: findFlagFunc("cgroup-parent").Value.String(), DNSOptions: dnsOptions, DNSSearch: dnsSearch, DNSServers: dnsServers, HTTPProxy: httpProxy, IdentityLabel: identityLabel, Memory: memoryLimit, MemorySwap: memorySwap, NoHostname: noHostname, NoHosts: noHosts, OmitHistory: omitHistory, ShmSize: findFlagFunc("shm-size").Value.String(), Ulimit: ulimit, Volumes: volumes, Secrets: secrets, SSHSources: sshsources, OCIHooksDir: ociHooks, } securityOpts, _ := flags.GetStringArray("security-opt") defConfig, err := config.Default() if err != nil { return nil, fmt.Errorf("failed to get container config: %w", err) } if defConfig.Containers.EnableLabeledUsers { defSecurityOpts, err := currentLabelOpts() if err != nil { return nil, err } securityOpts = append(defSecurityOpts, securityOpts...) } if err := parseSecurityOpts(securityOpts, commonOpts); err != nil { return nil, err } return commonOpts, nil } // GetAdditionalBuildContext consumes a raw string and returns a parsed // AdditionalBuildContext describing the build context. func GetAdditionalBuildContext(value string) (define.AdditionalBuildContext, error) { if value == "" { // reject empty values (filesystem paths?), because elsewhere we use an // empty string as an internal nickname for the default build context return define.AdditionalBuildContext{}, errInvalidBuildContextPathname } ret := define.AdditionalBuildContext{IsURL: false, IsImage: false, Value: value} if strings.HasPrefix(value, "docker-image://") { ret.IsImage = true ret.Value = strings.TrimPrefix(value, "docker-image://") if ret.Value == "" { return define.AdditionalBuildContext{}, errInvalidBuildContextImage } } else if strings.HasPrefix(value, "container-image://") { ret.IsImage = true ret.Value = strings.TrimPrefix(value, "container-image://") if ret.Value == "" { return define.AdditionalBuildContext{}, errInvalidBuildContextImage } } else if strings.HasPrefix(value, "docker://") { ret.IsImage = true ret.Value = strings.TrimPrefix(value, "docker://") if ret.Value == "" { return define.AdditionalBuildContext{}, errInvalidBuildContextImage } } else if strings.HasPrefix(value, "http://") || strings.HasPrefix(value, "https://") { ret.IsImage = false ret.IsURL = true if strings.TrimPrefix(ret.Value, "http://") == "" || strings.TrimPrefix(ret.Value, "https://") == "" { return define.AdditionalBuildContext{}, errInvalidBuildContextURL } } else { path, err := filepath.Abs(value) if err != nil { return define.AdditionalBuildContext{}, fmt.Errorf("unable to convert additional build-context %q path to absolute: %w", value, err) } ret.Value = path } return ret, nil } func parseSecurityOpts(securityOpts []string, commonOpts *define.CommonBuildOptions) error { for _, opt := range securityOpts { if opt == "no-new-privileges" { commonOpts.NoNewPrivileges = true continue } con := strings.SplitN(opt, "=", 2) if len(con) != 2 { return fmt.Errorf("invalid --security-opt name=value pair: %q", opt) } switch con[0] { case "label": commonOpts.LabelOpts = append(commonOpts.LabelOpts, con[1]) case "apparmor": commonOpts.ApparmorProfile = con[1] case "seccomp": commonOpts.SeccompProfilePath = con[1] case "mask": commonOpts.Masks = append(commonOpts.Masks, strings.Split(con[1], ":")...) case "unmask": unmasks := strings.Split(con[1], ":") for _, unmask := range unmasks { matches, _ := filepath.Glob(unmask) if len(matches) > 0 { commonOpts.Unmasks = append(commonOpts.Unmasks, matches...) continue } commonOpts.Unmasks = append(commonOpts.Unmasks, unmask) } default: return fmt.Errorf("invalid --security-opt 2: %q", opt) } } if commonOpts.SeccompProfilePath == "" { if err := fileutils.Exists(SeccompOverridePath); err == nil { commonOpts.SeccompProfilePath = SeccompOverridePath } else { if !errors.Is(err, fs.ErrNotExist) { return err } if err := fileutils.Exists(SeccompDefaultPath); err != nil { if !errors.Is(err, fs.ErrNotExist) { return err } } else { commonOpts.SeccompProfilePath = SeccompDefaultPath } } } return nil } // Split string into slice by colon. Backslash-escaped colon (i.e. "\:") will not be regarded as separator func SplitStringWithColonEscape(str string) []string { return internalParse.SplitStringWithColonEscape(str) } // Volume parses the input of --volume func Volume(volume string) (specs.Mount, error) { return internalParse.Volume(volume) } // Volumes validates the host and container paths passed in to the --volume flag func Volumes(volumes []string) error { if len(volumes) == 0 { return nil } for _, volume := range volumes { if _, err := Volume(volume); err != nil { return err } } return nil } // ValidateVolumeHostDir validates a volume mount's source directory func ValidateVolumeHostDir(hostDir string) error { return parse.ValidateVolumeHostDir(hostDir) } // ValidateVolumeCtrDir validates a volume mount's destination directory. func ValidateVolumeCtrDir(ctrDir string) error { return parse.ValidateVolumeCtrDir(ctrDir) } // ValidateVolumeOpts validates a volume's options func ValidateVolumeOpts(options []string) ([]string, error) { return parse.ValidateVolumeOpts(options) } // validateExtraHost validates that the specified string is a valid extrahost and returns it. // ExtraHost is in the form of name:ip where the ip has to be a valid ip (ipv4 or ipv6). // for add-host flag func validateExtraHost(val string) error { // allow for IPv6 addresses in extra hosts by only splitting on first ":" arr := strings.SplitN(val, ":", 2) if len(arr) != 2 || len(arr[0]) == 0 { return fmt.Errorf("bad format for add-host: %q", val) } if arr[1] == etchosts.HostGateway { return nil } if _, err := validateIPAddress(arr[1]); err != nil { return fmt.Errorf("invalid IP address in add-host: %q", arr[1]) } return nil } // validateIPAddress validates an Ip address. // for dns, ip, and ip6 flags also func validateIPAddress(val string) (string, error) { ip := net.ParseIP(strings.TrimSpace(val)) if ip != nil { return ip.String(), nil } return "", fmt.Errorf("%s is not an ip address", val) } // SystemContextFromOptions returns a SystemContext populated with values // per the input parameters provided by the caller for the use in authentication. func SystemContextFromOptions(c *cobra.Command) (*types.SystemContext, error) { return SystemContextFromFlagSet(c.Flags(), c.Flag) } // SystemContextFromFlagSet returns a SystemContext populated with values // per the input parameters provided by the caller for the use in authentication. func SystemContextFromFlagSet(flags *pflag.FlagSet, findFlagFunc func(name string) *pflag.Flag) (*types.SystemContext, error) { certDir, err := flags.GetString("cert-dir") if err != nil { certDir = "" } ctx := &types.SystemContext{ DockerCertPath: certDir, } tlsVerify, err := flags.GetBool("tls-verify") if err == nil && findFlagFunc("tls-verify").Changed { ctx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!tlsVerify) ctx.OCIInsecureSkipTLSVerify = !tlsVerify ctx.DockerDaemonInsecureSkipTLSVerify = !tlsVerify } insecure, err := flags.GetBool("insecure") if err == nil && findFlagFunc("insecure").Changed { if ctx.DockerInsecureSkipTLSVerify != types.OptionalBoolUndefined { return nil, errors.New("--insecure may not be used with --tls-verify") } ctx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(insecure) ctx.OCIInsecureSkipTLSVerify = insecure ctx.DockerDaemonInsecureSkipTLSVerify = insecure } disableCompression, err := flags.GetBool("disable-compression") if err == nil { if disableCompression { ctx.OCIAcceptUncompressedLayers = true } else { ctx.DirForceCompress = true } } creds, err := flags.GetString("creds") if err == nil && findFlagFunc("creds").Changed { var err error ctx.DockerAuthConfig, err = AuthConfig(creds) if err != nil { return nil, err } } sigPolicy, err := flags.GetString("signature-policy") if err == nil && findFlagFunc("signature-policy").Changed { ctx.SignaturePolicyPath = sigPolicy } authfile, err := flags.GetString("authfile") if err == nil { ctx.AuthFilePath = getAuthFile(authfile) } regConf, err := flags.GetString("registries-conf") if err == nil && findFlagFunc("registries-conf").Changed { ctx.SystemRegistriesConfPath = regConf } regConfDir, err := flags.GetString("registries-conf-dir") if err == nil && findFlagFunc("registries-conf-dir").Changed { ctx.RegistriesDirPath = regConfDir } shortNameAliasConf, err := flags.GetString("short-name-alias-conf") if err == nil && findFlagFunc("short-name-alias-conf").Changed { ctx.UserShortNameAliasConfPath = shortNameAliasConf } ctx.DockerRegistryUserAgent = fmt.Sprintf("Buildah/%s", define.Version) if findFlagFunc("os") != nil && findFlagFunc("os").Changed { var os string if os, err = flags.GetString("os"); err != nil { return nil, err } ctx.OSChoice = os } if findFlagFunc("arch") != nil && findFlagFunc("arch").Changed { var arch string if arch, err = flags.GetString("arch"); err != nil { return nil, err } ctx.ArchitectureChoice = arch } if findFlagFunc("variant") != nil && findFlagFunc("variant").Changed { var variant string if variant, err = flags.GetString("variant"); err != nil { return nil, err } ctx.VariantChoice = variant } if findFlagFunc("platform") != nil && findFlagFunc("platform").Changed { var specs []string if specs, err = flags.GetStringSlice("platform"); err != nil { return nil, err } if len(specs) == 0 || specs[0] == "" { return nil, fmt.Errorf("unable to parse --platform value %v", specs) } platform := specs[0] os, arch, variant, err := Platform(platform) if err != nil { return nil, err } if ctx.OSChoice != "" || ctx.ArchitectureChoice != "" || ctx.VariantChoice != "" { return nil, errors.New("invalid --platform may not be used with --os, --arch, or --variant") } ctx.OSChoice = os ctx.ArchitectureChoice = arch ctx.VariantChoice = variant } ctx.BigFilesTemporaryDir = GetTempDir() return ctx, nil } // pullPolicyWithFlags parses a string value of a pull policy, evaluating it in // combination with "always" and "never" boolean flags. // Allow for: // * --pull // * --pull="" // * --pull=true // * --pull=false // * --pull=never // * --pull=always // * --pull=ifmissing // * --pull=missing // * --pull=notpresent // * --pull=newer // * --pull=ifnewer // and --pull-always and --pull-never as boolean flags. func pullPolicyWithFlags(policySpec string, always, never bool) (define.PullPolicy, error) { if always { return define.PullAlways, nil } if never { return define.PullNever, nil } policy := strings.ToLower(policySpec) switch policy { case "missing", "ifmissing", "notpresent": return define.PullIfMissing, nil case "true", "always": return define.PullAlways, nil case "false", "never": return define.PullNever, nil case "ifnewer", "newer": return define.PullIfNewer, nil } return 0, fmt.Errorf("unrecognized pull policy %q", policySpec) } // PullPolicyFromOptions returns a PullPolicy that reflects the combination of // the specified "pull" and undocumented "pull-always" and "pull-never" flags. func PullPolicyFromOptions(c *cobra.Command) (define.PullPolicy, error) { return PullPolicyFromFlagSet(c.Flags(), c.Flag) } // PullPolicyFromFlagSet returns a PullPolicy that reflects the combination of // the specified "pull" and undocumented "pull-always" and "pull-never" flags. func PullPolicyFromFlagSet(flags *pflag.FlagSet, findFlagFunc func(name string) *pflag.Flag) (define.PullPolicy, error) { pullFlagsCount := 0 if findFlagFunc("pull").Changed { pullFlagsCount++ } if findFlagFunc("pull-always").Changed { pullFlagsCount++ } if findFlagFunc("pull-never").Changed { pullFlagsCount++ } if pullFlagsCount > 1 { return 0, errors.New("can only set one of 'pull' or 'pull-always' or 'pull-never'") } // The --pull-never and --pull-always options will not be documented. pullAlwaysFlagValue, err := flags.GetBool("pull-always") if err != nil { return 0, fmt.Errorf("checking the --pull-always flag value: %w", err) } pullNeverFlagValue, err := flags.GetBool("pull-never") if err != nil { return 0, fmt.Errorf("checking the --pull-never flag value: %w", err) } // The --pull[=...] flag is the one we really care about. pullFlagValue := findFlagFunc("pull").Value.String() pullPolicy, err := pullPolicyWithFlags(pullFlagValue, pullAlwaysFlagValue, pullNeverFlagValue) if err != nil { return 0, err } logrus.Debugf("Pull Policy for pull [%v]", pullPolicy) return pullPolicy, nil } func getAuthFile(authfile string) string { if authfile != "" { absAuthfile, err := filepath.Abs(authfile) if err == nil { return absAuthfile } logrus.Warnf("ignoring passed-in auth file path, evaluating it: %v", err) } return auth.GetDefaultAuthFile() } // PlatformFromOptions parses the operating system (os) and architecture (arch) // from the provided command line options. Deprecated in favor of // PlatformsFromOptions(), but kept here because it's part of our API. func PlatformFromOptions(c *cobra.Command) (os, arch string, err error) { platforms, err := PlatformsFromOptions(c) if err != nil { return "", "", err } if len(platforms) < 1 { return "", "", errors.New("invalid platform syntax for --platform (use OS/ARCH[/VARIANT])") } return platforms[0].OS, platforms[0].Arch, nil } // PlatformsFromOptions parses the operating system (os) and architecture // (arch) from the provided command line options. If --platform used, it // also returns the list of platforms that were passed in as its argument. func PlatformsFromOptions(c *cobra.Command) (platforms []struct{ OS, Arch, Variant string }, err error) { var os, arch, variant string if c.Flag("os").Changed { if os, err = c.Flags().GetString("os"); err != nil { return nil, err } } if c.Flag("arch").Changed { if arch, err = c.Flags().GetString("arch"); err != nil { return nil, err } } if c.Flag("variant").Changed { if variant, err = c.Flags().GetString("variant"); err != nil { return nil, err } } platforms = []struct{ OS, Arch, Variant string }{{os, arch, variant}} if c.Flag("platform").Changed { platforms = nil platformSpecs, err := c.Flags().GetStringSlice("platform") if err != nil { return nil, fmt.Errorf("unable to parse platform: %w", err) } if os != "" || arch != "" || variant != "" { return nil, fmt.Errorf("invalid --platform may not be used with --os, --arch, or --variant") } for _, pf := range platformSpecs { if os, arch, variant, err = Platform(pf); err != nil { return nil, fmt.Errorf("unable to parse platform %q: %w", pf, err) } platforms = append(platforms, struct{ OS, Arch, Variant string }{os, arch, variant}) } } return platforms, nil } // DefaultPlatform returns the standard platform for the current system func DefaultPlatform() string { return platforms.DefaultString() } // Platform separates the platform string into os, arch and variant, // accepting any of $arch, $os/$arch, or $os/$arch/$variant. func Platform(platform string) (os, arch, variant string, err error) { platform = strings.Trim(platform, "/") if platform == "local" || platform == "" { return Platform(DefaultPlatform()) } platformSpec, err := platforms.Parse(platform) if err != nil { return "", "", "", fmt.Errorf("invalid platform syntax for --platform=%q: %w", platform, err) } return platformSpec.OS, platformSpec.Architecture, platformSpec.Variant, nil } func parseCreds(creds string) (string, string) { if creds == "" { return "", "" } up := strings.SplitN(creds, ":", 2) if len(up) == 1 { return up[0], "" } if up[0] == "" { return "", up[1] } return up[0], up[1] } // AuthConfig parses the creds in format [username[:password] into an auth // config. func AuthConfig(creds string) (*types.DockerAuthConfig, error) { username, password := parseCreds(creds) if username == "" { fmt.Print("Username: ") if _, err := fmt.Scanln(&username); err != nil { return nil, fmt.Errorf("reading user name: %w", err) } } if password == "" { fmt.Print("Password: ") termPassword, err := term.ReadPassword(0) if err != nil { return nil, fmt.Errorf("could not read password from terminal: %w", err) } password = string(termPassword) } return &types.DockerAuthConfig{ Username: username, Password: password, }, nil } // GetBuildOutput is responsible for parsing custom build output argument i.e `build --output` flag. // Takes `buildOutput` as string and returns BuildOutputOption // This function will read an argument of `type=tar` as "output in a local folder names type=tar" // // Deprecated: This function is now internal func GetBuildOutput(buildOutput string) (define.BuildOutputOption, error) { if buildOutput == "-" { // Feature parity with buildkit, output tar to stdout // Read more here: https://docs.docker.com/engine/reference/commandline/build/#custom-build-outputs return define.BuildOutputOption{ Path: "", IsDir: false, IsStdout: true, }, nil } if !strings.Contains(buildOutput, ",") { // expect default --output return define.BuildOutputOption{ Path: buildOutput, IsDir: true, IsStdout: false, }, nil } isDir := true isStdout := false typeSelected := "" pathSelected := "" for option := range strings.SplitSeq(buildOutput, ",") { key, value, found := strings.Cut(option, "=") if !found { return define.BuildOutputOption{}, fmt.Errorf("invalid build output options %q, expected format key=value", buildOutput) } switch key { case "type": if typeSelected != "" { return define.BuildOutputOption{}, fmt.Errorf("duplicate %q not supported", key) } typeSelected = value switch typeSelected { case "local": isDir = true case "tar": isDir = false default: return define.BuildOutputOption{}, fmt.Errorf("invalid type %q selected for build output options %q", value, buildOutput) } case "dest": if pathSelected != "" { return define.BuildOutputOption{}, fmt.Errorf("duplicate %q not supported", key) } pathSelected = value default: return define.BuildOutputOption{}, fmt.Errorf("unrecognized key %q in build output option: %q", key, buildOutput) } } if typeSelected == "" || pathSelected == "" { return define.BuildOutputOption{}, fmt.Errorf(`invalid build output option %q, accepted keys are "type" and "dest" must be present`, buildOutput) } if pathSelected == "-" { if isDir { return define.BuildOutputOption{}, fmt.Errorf(`invalid build output option %q, "type=local" can not be used with "dest=-"`, buildOutput) } } return define.BuildOutputOption{Path: pathSelected, IsDir: isDir, IsStdout: isStdout}, nil } // TeeType parses a string value and returns a TeeType func TeeType(teeType string) define.TeeType { return define.TeeType(strings.ToLower(teeType)) } // GetConfidentialWorkloadOptions parses a confidential workload settings // argument, which controls both whether or not we produce an image that // expects to be run using krun, and how we handle things like encrypting // the disk image that the container image will contain. func GetConfidentialWorkloadOptions(arg string) (define.ConfidentialWorkloadOptions, error) { options := define.ConfidentialWorkloadOptions{ TempDir: GetTempDir(), } defaults := options for option := range strings.SplitSeq(arg, ",") { var err error switch { case strings.HasPrefix(option, "type="): options.TeeType = TeeType(strings.TrimPrefix(option, "type=")) switch options.TeeType { case define.SEV, define.SNP, mkcwtypes.SEV_NO_ES: default: return options, fmt.Errorf("parsing type= value %q: unrecognized value", options.TeeType) } case strings.HasPrefix(option, "attestation_url="), strings.HasPrefix(option, "attestation-url="): options.Convert = true options.AttestationURL = strings.TrimPrefix(option, "attestation_url=") if options.AttestationURL == option { options.AttestationURL = strings.TrimPrefix(option, "attestation-url=") } case strings.HasPrefix(option, "passphrase="): options.Convert = true options.DiskEncryptionPassphrase = strings.TrimPrefix(option, "passphrase=") case strings.HasPrefix(option, "workload_id="), strings.HasPrefix(option, "workload-id="): options.WorkloadID = strings.TrimPrefix(option, "workload_id=") if options.WorkloadID == option { options.WorkloadID = strings.TrimPrefix(option, "workload-id=") } case strings.HasPrefix(option, "cpus="): options.CPUs, err = strconv.Atoi(strings.TrimPrefix(option, "cpus=")) if err != nil { return options, fmt.Errorf("parsing cpus= value %q: %w", strings.TrimPrefix(option, "cpus="), err) } case strings.HasPrefix(option, "memory="): options.Memory, err = strconv.Atoi(strings.TrimPrefix(option, "memory=")) if err != nil { return options, fmt.Errorf("parsing memory= value %q: %w", strings.TrimPrefix(option, "memorys"), err) } case option == "ignore_attestation_errors", option == "ignore-attestation-errors": options.IgnoreAttestationErrors = true case strings.HasPrefix(option, "ignore_attestation_errors="), strings.HasPrefix(option, "ignore-attestation-errors="): val := strings.TrimPrefix(option, "ignore_attestation_errors=") if val == option { val = strings.TrimPrefix(option, "ignore-attestation-errors=") } options.IgnoreAttestationErrors = val == "true" || val == "yes" || val == "on" || val == "1" case strings.HasPrefix(option, "firmware-library="), strings.HasPrefix(option, "firmware_library="): val := strings.TrimPrefix(option, "firmware-library=") if val == option { val = strings.TrimPrefix(option, "firmware_library=") } options.FirmwareLibrary = val case strings.HasPrefix(option, "slop="): options.Slop = strings.TrimPrefix(option, "slop=") default: knownOptions := []string{"type", "attestation_url", "passphrase", "workload_id", "cpus", "memory", "firmware_library", "slop"} return options, fmt.Errorf("expected one or more of %q as arguments for --cw, not %q", knownOptions, option) } } if options != defaults && !options.Convert { return options, fmt.Errorf("--cw arguments missing one or more of (%q, %q)", "passphrase", "attestation_url") } return options, nil } // SBOMScanOptions parses the build options from the cli func SBOMScanOptions(c *cobra.Command) (*define.SBOMScanOptions, error) { return SBOMScanOptionsFromFlagSet(c.Flags(), c.Flag) } // SBOMScanOptionsFromFlagSet parses scan settings from the cli func SBOMScanOptionsFromFlagSet(flags *pflag.FlagSet, _ func(name string) *pflag.Flag) (*define.SBOMScanOptions, error) { preset, err := flags.GetString("sbom") if err != nil { return nil, fmt.Errorf("invalid value for --sbom: %w", err) } options, err := sbom.Preset(preset) if err != nil { return nil, err } if options == nil { return nil, fmt.Errorf("parsing --sbom flag: unrecognized preset name %q", preset) } image, err := flags.GetString("sbom-scanner-image") if err != nil { return nil, fmt.Errorf("invalid value for --sbom-scanner-image: %w", err) } commands, err := flags.GetStringArray("sbom-scanner-command") if err != nil { return nil, fmt.Errorf("invalid value for --sbom-scanner-command: %w", err) } mergeStrategy, err := flags.GetString("sbom-merge-strategy") if err != nil { return nil, fmt.Errorf("invalid value for --sbom-merge-strategy: %w", err) } if image != "" || len(commands) > 0 || mergeStrategy != "" { options = &define.SBOMScanOptions{ Image: image, Commands: slices.Clone(commands), MergeStrategy: define.SBOMMergeStrategy(mergeStrategy), } } if options.ImageSBOMOutput, err = flags.GetString("sbom-image-output"); err != nil { return nil, fmt.Errorf("invalid value for --sbom-image-output: %w", err) } if options.SBOMOutput, err = flags.GetString("sbom-output"); err != nil { return nil, fmt.Errorf("invalid value for --sbom-output: %w", err) } if options.ImagePURLOutput, err = flags.GetString("sbom-image-purl-output"); err != nil { return nil, fmt.Errorf("invalid value for --sbom-image-purl-output: %w", err) } if options.PURLOutput, err = flags.GetString("sbom-purl-output"); err != nil { return nil, fmt.Errorf("invalid value for --sbom-purl-output: %w", err) } if options.Image == "" || len(options.Commands) == 0 { return options, fmt.Errorf("sbom configuration missing one or more of (%q or %q)", "--sbom-scanner-image", "--sbom-scanner-command") } if options.SBOMOutput == "" && options.ImageSBOMOutput == "" && options.PURLOutput == "" && options.ImagePURLOutput == "" { return options, fmt.Errorf("sbom configuration missing one or more of (%q, %q, %q or %q)", "--sbom-output", "--sbom-image-output", "--sbom-purl-output", "--sbom-image-purl-output") } if len(options.Commands) > 1 && options.MergeStrategy == "" { return options, fmt.Errorf("sbom configuration included multiple %q values but no %q value", "--sbom-scanner-command", "--sbom-merge-strategy") } switch options.MergeStrategy { default: return options, fmt.Errorf("sbom arguments included unrecognized merge strategy %q", string(options.MergeStrategy)) case define.SBOMMergeStrategyCat, define.SBOMMergeStrategyCycloneDXByComponentNameAndVersion, define.SBOMMergeStrategySPDXByPackageNameAndVersionInfo: // all good here } return options, nil } // IDMappingOptions parses the build options related to user namespaces and ID mapping. func IDMappingOptions(c *cobra.Command, _ define.Isolation) (usernsOptions define.NamespaceOptions, idmapOptions *define.IDMappingOptions, err error) { return IDMappingOptionsFromFlagSet(c.Flags(), c.PersistentFlags(), c.Flag) } // GetAutoOptions returns a AutoUserNsOptions with the settings to setup automatically // a user namespace. func GetAutoOptions(base string) (*storageTypes.AutoUserNsOptions, error) { parts := strings.SplitN(base, ":", 2) if parts[0] != "auto" { return nil, errors.New("wrong user namespace mode") } options := storageTypes.AutoUserNsOptions{} if len(parts) == 1 { return &options, nil } for o := range strings.SplitSeq(parts[1], ",") { v := strings.SplitN(o, "=", 2) if len(v) != 2 { return nil, fmt.Errorf("invalid option specified: %q", o) } switch v[0] { case "size": s, err := strconv.ParseUint(v[1], 10, 32) if err != nil { return nil, err } options.Size = uint32(s) case "uidmapping": mapping, err := storageTypes.ParseIDMapping([]string{v[1]}, nil, "", "") if err != nil { return nil, err } options.AdditionalUIDMappings = append(options.AdditionalUIDMappings, mapping.UIDMap...) case "gidmapping": mapping, err := storageTypes.ParseIDMapping(nil, []string{v[1]}, "", "") if err != nil { return nil, err } options.AdditionalGIDMappings = append(options.AdditionalGIDMappings, mapping.GIDMap...) default: return nil, fmt.Errorf("unknown option specified: %q", v[0]) } } return &options, nil } // IDMappingOptionsFromFlagSet parses the build options related to user namespaces and ID mapping. func IDMappingOptionsFromFlagSet(flags *pflag.FlagSet, persistentFlags *pflag.FlagSet, findFlagFunc func(name string) *pflag.Flag) (usernsOptions define.NamespaceOptions, idmapOptions *define.IDMappingOptions, err error) { isAuto := false autoOpts := &storageTypes.AutoUserNsOptions{} user := findFlagFunc("userns-uid-map-user").Value.String() group := findFlagFunc("userns-gid-map-group").Value.String() // If only the user or group was specified, use the same value for the // other, since we need both in order to initialize the maps using the // names. if user == "" && group != "" { user = group } if group == "" && user != "" { group = user } // Either start with empty maps or the name-based maps. mappings := idtools.NewIDMappingsFromMaps(nil, nil) if user != "" && group != "" { submappings, err := idtools.NewIDMappings(user, group) if err != nil { return nil, nil, err } mappings = submappings } globalOptions := persistentFlags // We'll parse the UID and GID mapping options the same way. buildIDMap := func(basemap []idtools.IDMap, option string) ([]specs.LinuxIDMapping, error) { outmap := make([]specs.LinuxIDMapping, 0, len(basemap)) // Start with the name-based map entries. for _, m := range basemap { outmap = append(outmap, specs.LinuxIDMapping{ ContainerID: uint32(m.ContainerID), HostID: uint32(m.HostID), Size: uint32(m.Size), }) } // Parse the flag's value as one or more triples (if it's even // been set), and append them. var spec []string if globalOptions.Lookup(option) != nil && globalOptions.Lookup(option).Changed { spec, _ = globalOptions.GetStringSlice(option) } if findFlagFunc(option).Changed { spec, _ = flags.GetStringSlice(option) } idmap, err := parseIDMap(spec) if err != nil { return nil, err } for _, m := range idmap { outmap = append(outmap, specs.LinuxIDMapping{ ContainerID: m[0], HostID: m[1], Size: m[2], }) } return outmap, nil } uidmap, err := buildIDMap(mappings.UIDs(), "userns-uid-map") if err != nil { return nil, nil, err } gidmap, err := buildIDMap(mappings.GIDs(), "userns-gid-map") if err != nil { return nil, nil, err } // If we only have one map or the other populated at this point, then // use the same mapping for both, since we know that no user or group // name was specified, but a specific mapping was for one or the other. if len(uidmap) == 0 && len(gidmap) != 0 { uidmap = gidmap } if len(gidmap) == 0 && len(uidmap) != 0 { gidmap = uidmap } // By default, having mappings configured means we use a user // namespace. Otherwise, we don't. usernsOption := define.NamespaceOption{ Name: string(specs.UserNamespace), Host: len(uidmap) == 0 && len(gidmap) == 0, } // If the user specifically requested that we either use or don't use // user namespaces, override that default. if findFlagFunc("userns").Changed { how := findFlagFunc("userns").Value.String() if strings.HasPrefix(how, "auto") { autoOpts, err = GetAutoOptions(how) if err != nil { return nil, nil, err } isAuto = true usernsOption.Host = false } else { switch how { case "", "container", "private": usernsOption.Host = false case "host": usernsOption.Host = true default: how = strings.TrimPrefix(how, "ns:") if err := fileutils.Exists(how); err != nil { return nil, nil, fmt.Errorf("checking %s namespace: %w", string(specs.UserNamespace), err) } logrus.Debugf("setting %q namespace to %q", string(specs.UserNamespace), how) usernsOption.Path = how } } } usernsOptions = define.NamespaceOptions{usernsOption} // If the user requested that we use the host namespace, but also that // we use mappings, that's not going to work. if (len(uidmap) != 0 || len(gidmap) != 0) && usernsOption.Host { return nil, nil, fmt.Errorf("can not specify ID mappings while using host's user namespace") } return usernsOptions, &define.IDMappingOptions{ HostUIDMapping: usernsOption.Host, HostGIDMapping: usernsOption.Host, UIDMap: uidmap, GIDMap: gidmap, AutoUserNs: isAuto, AutoUserNsOpts: *autoOpts, }, nil } func parseIDMap(spec []string) (m [][3]uint32, err error) { for _, s := range spec { args := strings.FieldsFunc(s, func(r rune) bool { return !unicode.IsDigit(r) }) if len(args)%3 != 0 { return nil, fmt.Errorf("mapping %q is not in the form containerid:hostid:size[,...]", s) } for len(args) >= 3 { cid, err := strconv.ParseUint(args[0], 10, 32) if err != nil { return nil, fmt.Errorf("parsing container ID %q from mapping %q as a number: %w", args[0], s, err) } hostid, err := strconv.ParseUint(args[1], 10, 32) if err != nil { return nil, fmt.Errorf("parsing host ID %q from mapping %q as a number: %w", args[1], s, err) } size, err := strconv.ParseUint(args[2], 10, 32) if err != nil { return nil, fmt.Errorf("parsing %q from mapping %q as a number: %w", args[2], s, err) } m = append(m, [3]uint32{uint32(cid), uint32(hostid), uint32(size)}) args = args[3:] } } return m, nil } // NamespaceOptions parses the build options for all namespaces except for user namespace. func NamespaceOptions(c *cobra.Command) (namespaceOptions define.NamespaceOptions, networkPolicy define.NetworkConfigurationPolicy, err error) { return NamespaceOptionsFromFlagSet(c.Flags(), c.Flag) } // NamespaceOptionsFromFlagSet parses the build options for all namespaces except for user namespace. func NamespaceOptionsFromFlagSet(flags *pflag.FlagSet, findFlagFunc func(name string) *pflag.Flag) (namespaceOptions define.NamespaceOptions, networkPolicy define.NetworkConfigurationPolicy, err error) { options := make(define.NamespaceOptions, 0, 7) policy := define.NetworkDefault for _, what := range []string{"cgroupns", string(specs.IPCNamespace), "network", string(specs.PIDNamespace), string(specs.UTSNamespace)} { if flags.Lookup(what) != nil && findFlagFunc(what).Changed { how := findFlagFunc(what).Value.String() switch what { case "cgroupns": what = string(specs.CgroupNamespace) } switch how { case "", "container", "private": logrus.Debugf("setting %q namespace to %q", what, "") policy = define.NetworkEnabled options.AddOrReplace(define.NamespaceOption{ Name: what, }) case "host": logrus.Debugf("setting %q namespace to host", what) policy = define.NetworkEnabled options.AddOrReplace(define.NamespaceOption{ Name: what, Host: true, }) default: if what == string(specs.NetworkNamespace) { if how == "none" { options.AddOrReplace(define.NamespaceOption{ Name: what, }) policy = define.NetworkDisabled logrus.Debugf("setting network to disabled") break } } how = strings.TrimPrefix(how, "ns:") // if not a path we assume it is a comma separated network list, see setupNamespaces() in run_linux.go if filepath.IsAbs(how) || what != string(specs.NetworkNamespace) { if err := fileutils.Exists(how); err != nil { return nil, define.NetworkDefault, fmt.Errorf("checking %s namespace: %w", what, err) } } policy = define.NetworkEnabled logrus.Debugf("setting %q namespace to %q", what, how) options.AddOrReplace(define.NamespaceOption{ Name: what, Path: how, }) } } } return options, policy, nil } func defaultIsolation() (define.Isolation, error) { isolation, isSet := os.LookupEnv("BUILDAH_ISOLATION") if isSet { switch strings.ToLower(isolation) { case "oci": return define.IsolationOCI, nil case "rootless": return define.IsolationOCIRootless, nil case "chroot": return define.IsolationChroot, nil default: return 0, fmt.Errorf("unrecognized $BUILDAH_ISOLATION value %q", isolation) } } if unshare.IsRootless() { return define.IsolationOCIRootless, nil } return define.IsolationDefault, nil } // IsolationOption parses the --isolation flag. func IsolationOption(isolation string) (define.Isolation, error) { if isolation != "" { switch strings.ToLower(isolation) { case "oci", "default": return define.IsolationOCI, nil case "rootless": return define.IsolationOCIRootless, nil case "chroot": return define.IsolationChroot, nil default: return 0, fmt.Errorf("unrecognized isolation type %q", isolation) } } return defaultIsolation() } // Device parses device mapping string to a src, dest & permissions string // Valid values for device look like: // // '/dev/sdc" // '/dev/sdc:/dev/xvdc" // '/dev/sdc:/dev/xvdc:rwm" // '/dev/sdc:rm" func Device(device string) (string, string, string, error) { src := "" dst := "" permissions := "rwm" arr := strings.Split(device, ":") switch len(arr) { case 3: if !isValidDeviceMode(arr[2]) { return "", "", "", fmt.Errorf("invalid device mode: %s", arr[2]) } permissions = arr[2] fallthrough case 2: if isValidDeviceMode(arr[1]) { permissions = arr[1] } else { if len(arr[1]) == 0 || arr[1][0] != '/' { return "", "", "", fmt.Errorf("invalid device mode: %s", arr[1]) } dst = arr[1] } fallthrough case 1: if len(arr[0]) > 0 { src = arr[0] break } fallthrough default: return "", "", "", fmt.Errorf("invalid device specification: %s", device) } if dst == "" { dst = src } return src, dst, permissions, nil } // isValidDeviceMode checks if the mode for device is valid or not. // isValid mode is a composition of r (read), w (write), and m (mknod). func isValidDeviceMode(mode string) bool { legalDeviceMode := map[rune]struct{}{ 'r': {}, 'w': {}, 'm': {}, } if mode == "" { return false } for _, c := range mode { if _, has := legalDeviceMode[c]; !has { return false } delete(legalDeviceMode, c) } return true } // GetTempDir returns the path of the preferred temporary directory on the host. func GetTempDir() string { return tmpdir.GetTempDir() } // Secrets parses the --secret flag func Secrets(secrets []string) (map[string]define.Secret, error) { parsed := make(map[string]define.Secret) for _, secret := range secrets { tokens := strings.Split(secret, ",") var id, src, typ string for _, val := range tokens { kv := strings.SplitN(val, "=", 2) switch kv[0] { case "id": id = kv[1] case "src": src = kv[1] case "env": src = kv[1] typ = "env" case "type": if kv[1] != "file" && kv[1] != "env" { return nil, errors.New("invalid secret type, must be file or env") } typ = kv[1] default: return nil, errInvalidSecretSyntax } } if id == "" { return nil, errInvalidSecretSyntax } if src == "" { src = id } if typ == "" { if _, ok := os.LookupEnv(id); ok { typ = "env" } else { typ = "file" } } if typ == "file" { fullPath, err := filepath.Abs(src) if err != nil { return nil, fmt.Errorf("could not parse secrets: %w", err) } err = fileutils.Exists(fullPath) if err != nil { return nil, fmt.Errorf("could not parse secrets: %w", err) } src = fullPath } newSecret := define.Secret{ ID: id, Source: src, SourceType: typ, } parsed[id] = newSecret } return parsed, nil } // SSH parses the --ssh flag func SSH(sshSources []string) (map[string]*sshagent.Source, error) { parsed := make(map[string]*sshagent.Source) var paths []string for _, v := range sshSources { parts := strings.SplitN(v, "=", 2) if len(parts) > 1 { paths = strings.Split(parts[1], ",") } source, err := sshagent.NewSource(paths) if err != nil { return nil, err } parsed[parts[0]] = source } return parsed, nil } // ContainerIgnoreFile consumes path to `dockerignore` or `containerignore` // and returns list of files to exclude along with the path to processed ignore // file. Deprecated since this might become internal only, please avoid relying // on this function. func ContainerIgnoreFile(contextDir, path string, containerFiles []string) ([]string, string, error) { if path != "" { excludes, err := imagebuilder.ParseIgnore(path) return excludes, path, err } // If path was not supplied give priority to `.containerignore` first. for _, containerfile := range containerFiles { if !filepath.IsAbs(containerfile) { containerfile = filepath.Join(contextDir, containerfile) } containerfileIgnore := "" if err := fileutils.Exists(containerfile + ".containerignore"); err == nil { containerfileIgnore = containerfile + ".containerignore" } if err := fileutils.Exists(containerfile + ".dockerignore"); err == nil { containerfileIgnore = containerfile + ".dockerignore" } if containerfileIgnore != "" { excludes, err := imagebuilder.ParseIgnore(containerfileIgnore) return excludes, containerfileIgnore, err } } path, symlinkErr := securejoin.SecureJoin(contextDir, ".containerignore") if symlinkErr != nil { return nil, "", symlinkErr } excludes, err := imagebuilder.ParseIgnore(path) if errors.Is(err, os.ErrNotExist) { path, symlinkErr = securejoin.SecureJoin(contextDir, ".dockerignore") if symlinkErr != nil { return nil, "", symlinkErr } excludes, err = imagebuilder.ParseIgnore(path) } if errors.Is(err, os.ErrNotExist) { return excludes, "", nil } return excludes, path, err } ================================================ FILE: pkg/parse/parse_test.go ================================================ package parse //nolint:revive,nolintlint import ( "fmt" "runtime" "testing" "github.com/containers/buildah/define" specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/spf13/pflag" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.podman.io/image/v5/types" ) func TestCommonBuildOptionsFromFlagSet(t *testing.T) { t.Parallel() fs := pflag.NewFlagSet("testme", pflag.PanicOnError) fs.String("memory", "1GB", "") fs.String("shm-size", "5TB", "") fs.String("cpuset-cpus", "1", "") fs.String("cpuset-mems", "2", "") fs.String("cgroup-parent", "none", "") err := fs.Parse([]string{"--memory", "2GB"}) assert.NoError(t, err) cbo, err := CommonBuildOptionsFromFlagSet(fs, fs.Lookup) assert.NoError(t, err) assert.Equal(t, cbo.Memory, int64(2147483648)) } // TestDeviceParser verifies the given device strings is parsed correctly func TestDeviceParser(t *testing.T) { t.Parallel() if runtime.GOOS != "linux" { t.Skip("Devices is only supported on Linux") } // Test defaults src, dest, permissions, err := Device("/dev/foo") assert.NoError(t, err) assert.Equal(t, src, "/dev/foo") assert.Equal(t, dest, "/dev/foo") assert.Equal(t, permissions, "rwm") // Test defaults, different dest src, dest, permissions, err = Device("/dev/foo:/dev/bar") assert.NoError(t, err) assert.Equal(t, src, "/dev/foo") assert.Equal(t, dest, "/dev/bar") assert.Equal(t, permissions, "rwm") // Test fully specified src, dest, permissions, err = Device("/dev/foo:/dev/bar:rm") assert.NoError(t, err) assert.Equal(t, src, "/dev/foo") assert.Equal(t, dest, "/dev/bar") assert.Equal(t, permissions, "rm") // Test device, permissions src, dest, permissions, err = Device("/dev/foo:rm") assert.NoError(t, err) assert.Equal(t, src, "/dev/foo") assert.Equal(t, dest, "/dev/foo") assert.Equal(t, permissions, "rm") // test bogus permissions _, _, _, err = Device("/dev/fuse1:BOGUS") assert.Error(t, err) _, _, _, err = Device("") assert.Error(t, err) _, _, _, err = Device("/dev/foo:/dev/bar:rm:") assert.Error(t, err) _, _, _, err = Device("/dev/foo::rm") assert.Error(t, err) } func TestIsValidDeviceMode(t *testing.T) { t.Parallel() if runtime.GOOS != "linux" { t.Skip("Devices is only supported on Linux") } assert.False(t, isValidDeviceMode("BOGUS")) assert.False(t, isValidDeviceMode("rwx")) assert.True(t, isValidDeviceMode("r")) assert.True(t, isValidDeviceMode("rw")) assert.True(t, isValidDeviceMode("rm")) assert.True(t, isValidDeviceMode("rwm")) } func TestDeviceFromPath(t *testing.T) { t.Parallel() if runtime.GOOS != "linux" { t.Skip("Devices is only supported on Linux") } // Path is valid dev, err := DeviceFromPath("/dev/null") assert.NoError(t, err) assert.Equal(t, len(dev), 1) assert.Equal(t, dev[0].Major, int64(1)) assert.Equal(t, dev[0].Minor, int64(3)) assert.Equal(t, string(dev[0].Permissions), "rwm") assert.Equal(t, dev[0].Uid, uint32(0)) assert.Equal(t, dev[0].Gid, uint32(0)) // Path does not exists _, err = DeviceFromPath("/dev/BOGUS") assert.Error(t, err) // Path is a directory of devices _, err = DeviceFromPath("/dev/pts") assert.NoError(t, err) // path of directory has no device _, err = DeviceFromPath("/etc/passwd") assert.Error(t, err) } func TestIDMappingOptions(t *testing.T) { t.Parallel() fs := pflag.NewFlagSet("testme", pflag.PanicOnError) pfs := pflag.NewFlagSet("persist", pflag.PanicOnError) fs.String("userns-uid-map-user", "", "") fs.String("userns-gid-map-group", "", "") fs.String("userns-uid-map", "", "") fs.String("userns-gid-map", "", "") fs.String("userns", "", "") err := fs.Parse([]string{}) assert.NoError(t, err) uos, _, err := IDMappingOptionsFromFlagSet(fs, pfs, fs.Lookup) assert.NoError(t, err) nso := uos.Find(string(specs.UserNamespace)) assert.Equal(t, *nso, define.NamespaceOption{ Host: true, Name: string(specs.UserNamespace), }) } func TestIsolation(t *testing.T) { t.Parallel() def, err := defaultIsolation() if err != nil { assert.Error(t, err) } isolations := []string{"", "default", "oci", "chroot", "rootless"} for _, i := range isolations { isolation, err := IsolationOption(i) if err != nil { assert.Error(t, fmt.Errorf("isolation %q not supported", i)) } var expected string switch i { case "": expected = def.String() case "default": expected = "oci" default: expected = i } if isolation.String() != expected { assert.Error(t, fmt.Errorf("isolation %q not equal to user input %q", isolation.String(), expected)) } } } func TestNamespaceOptions(t *testing.T) { t.Parallel() fs := pflag.NewFlagSet("testme", pflag.PanicOnError) fs.String("cgroupns", "", "") err := fs.Parse([]string{"--cgroupns", "private"}) assert.NoError(t, err) nsos, np, err := NamespaceOptionsFromFlagSet(fs, fs.Lookup) assert.NoError(t, err) assert.Equal(t, np, define.NetworkEnabled) nso := nsos.Find(string(specs.CgroupNamespace)) assert.Equal(t, *nso, define.NamespaceOption{ Name: string(specs.CgroupNamespace), }) } func TestParsePlatform(t *testing.T) { t.Parallel() os, arch, variant, err := Platform("a/b/c") assert.NoError(t, err) assert.NoError(t, err) assert.Equal(t, os, "a") assert.Equal(t, arch, "b") assert.Equal(t, variant, "c") os, arch, variant, err = Platform("a/b") assert.NoError(t, err) assert.NoError(t, err) assert.Equal(t, os, "a") assert.Equal(t, arch, "b") assert.Equal(t, variant, "") _, _, _, err = Platform("a") assert.Error(t, err) } func TestParsePullPolicy(t *testing.T) { t.Parallel() testCases := map[string]bool{ "missing": true, "ifmissing": true, "notpresent": true, "always": true, "true": true, "ifnewer": true, "newer": true, "false": true, "never": true, "try": false, "truth": false, } for value, result := range testCases { t.Run(value, func(t *testing.T) { policy, err := pullPolicyWithFlags(value, false, false) if result { require.NoErrorf(t, err, "expected value %q to be recognized", value) } else { require.Errorf(t, err, "did not expect value %q to be recognized as %q", value, policy.String()) } }) } } func TestSplitStringWithColonEscape(t *testing.T) { t.Parallel() tests := []struct { volume string expectedResult []string }{ {"/root/a:/root/test:O", []string{"/root/a", "/root/test", "O"}}, {"/root/a\\:b/c:/root/test:O", []string{"/root/a:b/c", "/root/test", "O"}}, {"/root/a:/root/test\\:test1/a:O", []string{"/root/a", "/root/test:test1/a", "O"}}, {"/root/a\\:b/c:/root/test\\:test1/a:O", []string{"/root/a:b/c", "/root/test:test1/a", "O"}}, } for _, args := range tests { val := SplitStringWithColonEscape(args.volume) assert.Equal(t, val, args.expectedResult) } } func TestSystemContextFromFlagSet(t *testing.T) { t.Parallel() fs := pflag.NewFlagSet("testme", pflag.PanicOnError) fs.Bool("tls-verify", false, "") err := fs.Parse([]string{"--tls-verify", "false"}) assert.NoError(t, err) sc, err := SystemContextFromFlagSet(fs, fs.Lookup) assert.NoError(t, err) assert.Equal(t, sc, &types.SystemContext{ BigFilesTemporaryDir: GetTempDir(), DockerInsecureSkipTLSVerify: types.OptionalBoolFalse, DockerRegistryUserAgent: fmt.Sprintf("Buildah/%s", define.Version), }) } func TestGetBuildOutput(t *testing.T) { testCases := []struct { description string input string output define.BuildOutputOption }{ { description: "hyphen", input: "-", output: define.BuildOutputOption{ IsStdout: true, }, }, { description: "just-a-path", input: "/tmp", output: define.BuildOutputOption{ IsDir: true, Path: "/tmp", }, }, { description: "normal-path", input: "type=local,dest=/tmp", output: define.BuildOutputOption{ IsDir: true, Path: "/tmp", }, }, } for _, testCase := range testCases { t.Run(testCase.description, func(t *testing.T) { result, err := GetBuildOutput(testCase.input) require.NoErrorf(t, err, "expected to be able to parse %q", testCase.input) assert.Equal(t, testCase.output, result) }) } } ================================================ FILE: pkg/parse/parse_unix.go ================================================ //go:build linux || darwin package parse //nolint:revive,nolintlint import ( "fmt" "os" "path/filepath" "github.com/containers/buildah/define" "github.com/opencontainers/cgroups/devices/config" "github.com/opencontainers/runc/libcontainer/devices" ) func DeviceFromPath(device string) (define.ContainerDevices, error) { var devs define.ContainerDevices src, dst, permissions, err := Device(device) if err != nil { return nil, err } if linkTarget, err := os.Readlink(src); err == nil { if filepath.IsAbs(linkTarget) { src = linkTarget } else { src = filepath.Join(filepath.Dir(src), linkTarget) } } srcInfo, err := os.Stat(src) if err != nil { return nil, fmt.Errorf("getting info of source device %s: %w", src, err) } if !srcInfo.IsDir() { dev, err := devices.DeviceFromPath(src, permissions) if err != nil { return nil, fmt.Errorf("%s is not a valid device: %w", src, err) } dev.Path = dst device := define.BuildahDevice{Device: *dev, Source: src, Destination: dst} devs = append(devs, device) return devs, nil } // If source device is a directory srcDevices, err := devices.GetDevices(src) if err != nil { return nil, fmt.Errorf("getting source devices from directory %s: %w", src, err) } for _, d := range srcDevices { d.Path = filepath.Join(dst, filepath.Base(d.Path)) d.Permissions = config.Permissions(permissions) device := define.BuildahDevice{Device: *d, Source: src, Destination: dst} devs = append(devs, device) } return devs, nil } ================================================ FILE: pkg/parse/parse_unsupported.go ================================================ //go:build !linux && !darwin package parse //nolint:revive,nolintlint import ( "errors" "github.com/containers/buildah/define" ) func getDefaultProcessLimits() []string { return []string{} } func DeviceFromPath(device string) (define.ContainerDevices, error) { return nil, errors.New("devices not supported") } ================================================ FILE: pkg/rusage/rusage.go ================================================ package rusage import ( "fmt" "time" units "github.com/docker/go-units" ) // Rusage is a subset of a Unix-style resource usage counter for the current // process and its children. The counters are always 0 on platforms where the // system call is not available (i.e., systems where getrusage() doesn't // exist). type Rusage struct { Date time.Time Elapsed time.Duration Utime, Stime time.Duration Inblock, Outblock int64 } // FormatDiff formats the result of rusage.Rusage.Subtract() for logging. func FormatDiff(diff Rusage) string { return fmt.Sprintf("%s(system) %s(user) %s(elapsed) %s input %s output", diff.Stime.Round(time.Millisecond), diff.Utime.Round(time.Millisecond), diff.Elapsed.Round(time.Millisecond), units.HumanSize(float64(diff.Inblock*512)), units.HumanSize(float64(diff.Outblock*512))) } // Subtract subtracts the items in delta from r, and returns the difference. // The Date field is zeroed for easier comparison with the zero value for the // Rusage type. func (r Rusage) Subtract(baseline Rusage) Rusage { return Rusage{ Elapsed: r.Date.Sub(baseline.Date), Utime: r.Utime - baseline.Utime, Stime: r.Stime - baseline.Stime, Inblock: r.Inblock - baseline.Inblock, Outblock: r.Outblock - baseline.Outblock, } } // Get returns the counters for the current process and its children, // subtracting any values in the passed in "since" value, or an error. // The Elapsed field will always be set to zero. func Get() (Rusage, error) { counters, err := get() if err != nil { return Rusage{}, err } return counters, nil } ================================================ FILE: pkg/rusage/rusage_test.go ================================================ package rusage import ( "flag" "os" "testing" "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "go.podman.io/storage/pkg/reexec" ) const ( noopCommand = "noop" ) func noopMain() { } func init() { reexec.Register(noopCommand, noopMain) } func TestMain(m *testing.M) { if reexec.Init() { return } flag.Parse() if testing.Verbose() { logrus.SetLevel(logrus.DebugLevel) } os.Exit(m.Run()) } func TestRusage(t *testing.T) { t.Parallel() if !Supported() { t.Skip("not supported on this platform") } before, err := Get() require.Nil(t, err, "unexpected error from GetRusage before running child: %v", err) cmd := reexec.Command(noopCommand) err = cmd.Run() require.Nil(t, err, "unexpected error running child process: %v", err) after, err := Get() require.Nil(t, err, "unexpected error from GetRusage after running child: %v", err) t.Logf("rusage from child: %#v", FormatDiff(after.Subtract(before))) require.NotZero(t, after.Subtract(before), "running a child process didn't use any resources?") } ================================================ FILE: pkg/rusage/rusage_unix.go ================================================ //go:build !windows package rusage import ( "fmt" "syscall" "time" ) func mkduration(tv syscall.Timeval) time.Duration { return time.Duration(tv.Sec)*time.Second + time.Duration(tv.Usec)*time.Microsecond } func get() (Rusage, error) { var rusage syscall.Rusage err := syscall.Getrusage(syscall.RUSAGE_CHILDREN, &rusage) if err != nil { return Rusage{}, fmt.Errorf("getting resource usage: %w", err) } r := Rusage{ Date: time.Now(), Utime: mkduration(rusage.Utime), Stime: mkduration(rusage.Stime), Inblock: int64(rusage.Inblock), //nolint:unconvert Outblock: int64(rusage.Oublock), //nolint:unconvert } return r, nil } // Supported returns true if resource usage counters are supported on this OS. func Supported() bool { return true } ================================================ FILE: pkg/rusage/rusage_unsupported.go ================================================ //go:build windows package rusage import ( "fmt" "syscall" ) func get() (Rusage, error) { return Rusage{}, fmt.Errorf("getting resource usage: %w", syscall.ENOTSUP) } // Supported returns true if resource usage counters are supported on this OS. func Supported() bool { return false } ================================================ FILE: pkg/sourcepolicy/policy.go ================================================ // Package sourcepolicy implements BuildKit-compatible source policy evaluation // for controlling and transforming source references during builds. // // Source policies allow users to: // - Pin base image tags to specific digests at build time // - Deny specific sources from being used // - Transform source references without modifying Containerfiles or Dockerfiles // // The policy file format is compatible with BuildKit's source policy JSON schema. package sourcepolicy import ( "encoding/json" "fmt" "os" "strings" "go.podman.io/image/v5/docker/reference" ) // Action represents the action to take when a rule matches. type Action string const ( // ActionAllow explicitly allows a source (no transformation). ActionAllow Action = "ALLOW" // ActionDeny blocks the source and fails the build. ActionDeny Action = "DENY" // ActionConvert transforms the source to a different reference. ActionConvert Action = "CONVERT" ) // MatchType represents how the selector identifier should be matched. type MatchType string const ( // MatchTypeExact requires an exact string match. MatchTypeExact MatchType = "EXACT" // MatchTypeWildcard allows * and ? glob patterns. MatchTypeWildcard MatchType = "WILDCARD" // MatchTypeRegex allows regular expression patterns (not implemented in MVP). MatchTypeRegex MatchType = "REGEX" ) // Selector specifies which sources a rule applies to. type Selector struct { // Identifier is the source identifier to match. // For docker images, this is typically "docker-image://registry/repo:tag". Identifier string `json:"identifier"` // MatchType specifies how the identifier should be matched. // Defaults to EXACT if not specified. MatchType MatchType `json:"matchType,omitempty"` } // Updates specifies how to transform a matched source. type Updates struct { // Identifier is the new source identifier to use. // For CONVERT actions, this replaces the original identifier. Identifier string `json:"identifier,omitempty"` // Attrs contains additional attributes (e.g., http.checksum). // Reserved for future use with HTTP sources. Attrs map[string]string `json:"attrs,omitempty"` } // Rule represents a single policy rule. type Rule struct { // Action specifies what to do when this rule matches. Action Action `json:"action"` // Selector specifies which sources this rule applies to. Selector Selector `json:"selector"` // Updates specifies how to transform the source (for CONVERT action). Updates *Updates `json:"updates,omitempty"` } // Policy represents a source policy containing multiple rules. type Policy struct { // Rules is the list of policy rules, evaluated in order. // First matching rule wins. Rules []Rule `json:"rules"` } // Decision represents the result of evaluating a source against a policy. type Decision struct { // Action is the action to take (ALLOW, DENY, or CONVERT). Action Action // TargetRef is the new reference to use (for CONVERT actions). TargetRef string // Reason provides context for the decision (e.g., which rule matched). Reason string } // LoadFromFile loads a source policy from a JSON file. func LoadFromFile(path string) (*Policy, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("reading source policy file %q: %w", path, err) } return Parse(data) } // Parse parses a source policy from JSON data. func Parse(data []byte) (*Policy, error) { var policy Policy if err := json.Unmarshal(data, &policy); err != nil { return nil, fmt.Errorf("parsing source policy JSON: %w", err) } if err := policy.Validate(); err != nil { return nil, fmt.Errorf("validating source policy: %w", err) } return &policy, nil } // Validate checks that the policy is well-formed. func (p *Policy) Validate() error { if len(p.Rules) == 0 { // Empty policy is valid - it just means no rules apply return nil } for i, rule := range p.Rules { if err := rule.Validate(); err != nil { return fmt.Errorf("rule %d: %w", i, err) } } return nil } // Validate checks that a rule is well-formed. func (r *Rule) Validate() error { // Validate action switch r.Action { case ActionAllow, ActionDeny, ActionConvert: // Valid actions case "": return fmt.Errorf("action is required") default: return fmt.Errorf("unknown action %q (valid: ALLOW, DENY, CONVERT)", r.Action) } // Validate selector if r.Selector.Identifier == "" { return fmt.Errorf("selector.identifier is required") } // Validate match type switch r.Selector.MatchType { case MatchTypeExact, MatchTypeWildcard, "": // Valid match types (empty defaults to EXACT) case MatchTypeRegex: return fmt.Errorf("REGEX match type is not supported in this version") default: return fmt.Errorf("unknown matchType %q (valid: EXACT, WILDCARD)", r.Selector.MatchType) } // Validate updates for CONVERT action if r.Action == ActionConvert { if r.Updates == nil || r.Updates.Identifier == "" { return fmt.Errorf("updates.identifier is required for CONVERT action") } } return nil } // Evaluate checks a source identifier against the policy and returns a decision. // The first matching rule wins. If no rule matches, returns (Decision{}, false, nil). func (p *Policy) Evaluate(sourceIdentifier string) (Decision, bool, error) { if p == nil || len(p.Rules) == 0 { return Decision{}, false, nil } for i, rule := range p.Rules { matched, err := rule.Matches(sourceIdentifier) if err != nil { return Decision{}, false, fmt.Errorf("evaluating rule %d: %w", i, err) } if matched { decision := Decision{ Action: rule.Action, Reason: fmt.Sprintf("matched rule %d (selector: %q)", i, rule.Selector.Identifier), } if rule.Action == ActionConvert && rule.Updates != nil { decision.TargetRef = rule.Updates.Identifier } return decision, true, nil } } return Decision{}, false, nil } // Matches checks if a source identifier matches this rule's selector. func (r *Rule) Matches(sourceIdentifier string) (bool, error) { matchType := r.Selector.MatchType if matchType == "" { matchType = MatchTypeWildcard } switch matchType { case MatchTypeExact: return r.Selector.Identifier == sourceIdentifier, nil case MatchTypeWildcard: return matchWildcard(r.Selector.Identifier, sourceIdentifier), nil default: return false, fmt.Errorf("unsupported match type: %s", matchType) } } // matchWildcard performs glob-style pattern matching. // Supports * (matches any sequence of characters) and ? (matches any single character). func matchWildcard(pattern, str string) bool { // Use a simple recursive approach for wildcard matching return wildcardMatch(pattern, str) } // wildcardMatch implements recursive wildcard matching. func wildcardMatch(pattern, str string) bool { for len(pattern) > 0 { switch pattern[0] { case '*': // * matches zero or more characters // Try matching the rest of the pattern against all possible suffixes pattern = pattern[1:] if len(pattern) == 0 { // Trailing * matches everything return true } // Try matching at each position for i := 0; i <= len(str); i++ { if wildcardMatch(pattern, str[i:]) { return true } } return false case '?': // ? matches exactly one character if len(str) == 0 { return false } pattern = pattern[1:] str = str[1:] default: // Regular character must match exactly if len(str) == 0 || pattern[0] != str[0] { return false } pattern = pattern[1:] str = str[1:] } } return len(str) == 0 } // ImageSourceIdentifier creates a BuildKit-style source identifier for a docker image. // This normalizes image references to the format "docker-image://registry/repo:tag". func ImageSourceIdentifier(imageRef string) string { // If already in docker-image:// format, return as-is if strings.HasPrefix(imageRef, "docker-image://") { return imageRef } // Normalize the image reference normalized := normalizeImageRef(imageRef) return "docker-image://" + normalized } // normalizeImageRef normalizes an image reference to include registry and library prefix. func normalizeImageRef(ref string) string { // Handle scratch specially if ref == "scratch" { return ref } // Use go.podman.io/image/v5/docker/reference for proper normalization named, err := reference.ParseNormalizedNamed(ref) if err != nil { // If parsing fails, return the original reference return ref } return named.String() } // ExtractImageRef extracts the image reference from a BuildKit-style source identifier. // It returns the original identifier if it's not a docker-image:// reference. func ExtractImageRef(sourceIdentifier string) string { const prefix = "docker-image://" if strings.HasPrefix(sourceIdentifier, prefix) { return sourceIdentifier[len(prefix):] } return sourceIdentifier } ================================================ FILE: pkg/sourcepolicy/policy_test.go ================================================ package sourcepolicy import ( "os" "path/filepath" "strings" "testing" ) func TestParse(t *testing.T) { tests := []struct { name string json string wantErr bool errContains string }{ { name: "valid empty policy", json: `{"rules": []}`, }, { name: "valid policy with DENY rule", json: `{ "rules": [ { "action": "DENY", "selector": { "identifier": "docker-image://docker.io/library/ubuntu:latest" } } ] }`, }, { name: "valid policy with CONVERT rule", json: `{ "rules": [ { "action": "CONVERT", "selector": { "identifier": "docker-image://docker.io/library/alpine:latest" }, "updates": { "identifier": "docker-image://docker.io/library/alpine:latest@sha256:abc123" } } ] }`, }, { name: "valid policy with ALLOW rule", json: `{ "rules": [ { "action": "ALLOW", "selector": { "identifier": "docker-image://docker.io/library/alpine:3.18" } } ] }`, }, { name: "valid policy with WILDCARD match type", json: `{ "rules": [ { "action": "DENY", "selector": { "identifier": "docker-image://docker.io/library/*:latest", "matchType": "WILDCARD" } } ] }`, }, { name: "invalid JSON", json: `{invalid}`, wantErr: true, errContains: "parsing source policy JSON", }, { name: "missing action", json: `{ "rules": [ { "selector": { "identifier": "docker-image://test" } } ] }`, wantErr: true, errContains: "action is required", }, { name: "unknown action", json: `{ "rules": [ { "action": "UNKNOWN", "selector": { "identifier": "docker-image://test" } } ] }`, wantErr: true, errContains: "unknown action", }, { name: "missing selector identifier", json: `{ "rules": [ { "action": "DENY", "selector": {} } ] }`, wantErr: true, errContains: "selector.identifier is required", }, { name: "CONVERT without updates", json: `{ "rules": [ { "action": "CONVERT", "selector": { "identifier": "docker-image://test" } } ] }`, wantErr: true, errContains: "updates.identifier is required for CONVERT", }, { name: "CONVERT with empty updates identifier", json: `{ "rules": [ { "action": "CONVERT", "selector": { "identifier": "docker-image://test" }, "updates": { "identifier": "" } } ] }`, wantErr: true, errContains: "updates.identifier is required for CONVERT", }, { name: "REGEX match type not supported", json: `{ "rules": [ { "action": "DENY", "selector": { "identifier": "docker-image://.*", "matchType": "REGEX" } } ] }`, wantErr: true, errContains: "REGEX match type is not supported", }, { name: "unknown match type", json: `{ "rules": [ { "action": "DENY", "selector": { "identifier": "docker-image://test", "matchType": "UNKNOWN" } } ] }`, wantErr: true, errContains: "unknown matchType", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { policy, err := Parse([]byte(tt.json)) if tt.wantErr { if err == nil { t.Errorf("Parse() error = nil, wantErr %v", tt.wantErr) return } if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) { t.Errorf("Parse() error = %v, want error containing %q", err, tt.errContains) } return } if err != nil { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) return } if policy == nil { t.Error("Parse() returned nil policy without error") } }) } } func TestLoadFromFile(t *testing.T) { // Create a temporary directory for test files tmpDir := t.TempDir() // Create a valid policy file validPolicy := `{ "rules": [ { "action": "DENY", "selector": { "identifier": "docker-image://test" } } ] }` validFile := filepath.Join(tmpDir, "valid.json") if err := os.WriteFile(validFile, []byte(validPolicy), 0o644); err != nil { t.Fatalf("Failed to write test file: %v", err) } // Create an invalid policy file invalidPolicy := `{invalid json}` invalidFile := filepath.Join(tmpDir, "invalid.json") if err := os.WriteFile(invalidFile, []byte(invalidPolicy), 0o644); err != nil { t.Fatalf("Failed to write test file: %v", err) } tests := []struct { name string path string wantErr bool errContains string }{ { name: "valid file", path: validFile, }, { name: "non-existent file", path: filepath.Join(tmpDir, "nonexistent.json"), wantErr: true, errContains: "reading source policy file", }, { name: "invalid JSON file", path: invalidFile, wantErr: true, errContains: "parsing source policy JSON", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { policy, err := LoadFromFile(tt.path) if tt.wantErr { if err == nil { t.Errorf("LoadFromFile() error = nil, wantErr %v", tt.wantErr) return } if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) { t.Errorf("LoadFromFile() error = %v, want error containing %q", err, tt.errContains) } return } if err != nil { t.Errorf("LoadFromFile() error = %v, wantErr %v", err, tt.wantErr) return } if policy == nil { t.Error("LoadFromFile() returned nil policy without error") } }) } } func TestEvaluate(t *testing.T) { tests := []struct { name string policyJSON string sourceID string wantMatched bool wantAction Action wantTargetRef string wantErr bool }{ { name: "nil policy returns no match", policyJSON: "", sourceID: "docker-image://docker.io/library/alpine:latest", wantMatched: false, }, { name: "empty policy returns no match", policyJSON: `{"rules": []}`, sourceID: "docker-image://docker.io/library/alpine:latest", wantMatched: false, }, { name: "exact match DENY", policyJSON: `{ "rules": [ { "action": "DENY", "selector": { "identifier": "docker-image://docker.io/library/alpine:latest", "matchType": "EXACT" } } ] }`, sourceID: "docker-image://docker.io/library/alpine:latest", wantMatched: true, wantAction: ActionDeny, }, { name: "exact match no match", policyJSON: `{ "rules": [ { "action": "DENY", "selector": { "identifier": "docker-image://docker.io/library/ubuntu:latest", "matchType": "EXACT" } } ] }`, sourceID: "docker-image://docker.io/library/alpine:latest", wantMatched: false, }, { name: "exact match CONVERT", policyJSON: `{ "rules": [ { "action": "CONVERT", "selector": { "identifier": "docker-image://docker.io/library/alpine:latest", "matchType": "EXACT" }, "updates": { "identifier": "docker-image://docker.io/library/alpine:latest@sha256:abc123" } } ] }`, sourceID: "docker-image://docker.io/library/alpine:latest", wantMatched: true, wantAction: ActionConvert, wantTargetRef: "docker-image://docker.io/library/alpine:latest@sha256:abc123", }, { name: "wildcard match DENY - star matches any", policyJSON: `{ "rules": [ { "action": "DENY", "selector": { "identifier": "docker-image://docker.io/library/*:latest", "matchType": "WILDCARD" } } ] }`, sourceID: "docker-image://docker.io/library/alpine:latest", wantMatched: true, wantAction: ActionDeny, }, { name: "wildcard match - question mark matches single char", policyJSON: `{ "rules": [ { "action": "DENY", "selector": { "identifier": "docker-image://docker.io/library/alpine:3.1?", "matchType": "WILDCARD" } } ] }`, sourceID: "docker-image://docker.io/library/alpine:3.18", wantMatched: true, wantAction: ActionDeny, }, { name: "wildcard no match", policyJSON: `{ "rules": [ { "action": "DENY", "selector": { "identifier": "docker-image://docker.io/library/*:stable", "matchType": "WILDCARD" } } ] }`, sourceID: "docker-image://docker.io/library/alpine:latest", wantMatched: false, }, { name: "first match wins - DENY before ALLOW", policyJSON: `{ "rules": [ { "action": "DENY", "selector": { "identifier": "docker-image://docker.io/library/alpine:latest" } }, { "action": "ALLOW", "selector": { "identifier": "docker-image://docker.io/library/alpine:latest" } } ] }`, sourceID: "docker-image://docker.io/library/alpine:latest", wantMatched: true, wantAction: ActionDeny, }, { name: "first match wins - ALLOW before DENY", policyJSON: `{ "rules": [ { "action": "ALLOW", "selector": { "identifier": "docker-image://docker.io/library/alpine:latest" } }, { "action": "DENY", "selector": { "identifier": "docker-image://docker.io/library/alpine:latest" } } ] }`, sourceID: "docker-image://docker.io/library/alpine:latest", wantMatched: true, wantAction: ActionAllow, }, { name: "multiple rules - second matches", policyJSON: `{ "rules": [ { "action": "DENY", "selector": { "identifier": "docker-image://docker.io/library/ubuntu:latest" } }, { "action": "CONVERT", "selector": { "identifier": "docker-image://docker.io/library/alpine:latest" }, "updates": { "identifier": "docker-image://myregistry/alpine:pinned" } } ] }`, sourceID: "docker-image://docker.io/library/alpine:latest", wantMatched: true, wantAction: ActionConvert, wantTargetRef: "docker-image://myregistry/alpine:pinned", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var policy *Policy var err error if tt.policyJSON != "" { policy, err = Parse([]byte(tt.policyJSON)) if err != nil { t.Fatalf("Failed to parse test policy: %v", err) } } decision, matched, err := policy.Evaluate(tt.sourceID) if tt.wantErr { if err == nil { t.Errorf("Evaluate() error = nil, wantErr %v", tt.wantErr) } return } if err != nil { t.Errorf("Evaluate() error = %v, wantErr %v", err, tt.wantErr) return } if matched != tt.wantMatched { t.Errorf("Evaluate() matched = %v, want %v", matched, tt.wantMatched) } if matched { if decision.Action != tt.wantAction { t.Errorf("Evaluate() action = %v, want %v", decision.Action, tt.wantAction) } if tt.wantTargetRef != "" && decision.TargetRef != tt.wantTargetRef { t.Errorf("Evaluate() targetRef = %v, want %v", decision.TargetRef, tt.wantTargetRef) } } }) } } func TestWildcardMatch(t *testing.T) { tests := []struct { pattern string str string want bool }{ // Basic exact matches {"abc", "abc", true}, {"abc", "abcd", false}, {"abc", "ab", false}, // Star wildcard {"*", "", true}, {"*", "anything", true}, {"a*", "a", true}, {"a*", "abc", true}, {"a*", "b", false}, {"*c", "c", true}, {"*c", "abc", true}, {"*c", "cd", false}, {"a*c", "ac", true}, {"a*c", "abc", true}, {"a*c", "abbc", true}, {"a*c", "ab", false}, // Question mark wildcard {"?", "a", true}, {"?", "", false}, {"?", "ab", false}, {"a?c", "abc", true}, {"a?c", "adc", true}, {"a?c", "ac", false}, {"a?c", "abbc", false}, // Combined wildcards {"a*?", "ab", true}, {"a*?", "abc", true}, {"a*?", "a", false}, {"a?*", "ab", true}, {"a?*", "abc", true}, {"a?*", "a", false}, // Multiple stars {"*a*", "a", true}, {"*a*", "ba", true}, {"*a*", "ab", true}, {"*a*", "bab", true}, {"*a*", "b", false}, // Real-world patterns {"docker-image://docker.io/library/*:latest", "docker-image://docker.io/library/alpine:latest", true}, {"docker-image://docker.io/library/*:latest", "docker-image://docker.io/library/ubuntu:latest", true}, {"docker-image://docker.io/library/*:latest", "docker-image://docker.io/library/alpine:3.18", false}, {"docker-image://*/*:*", "docker-image://docker.io/library/alpine:3.18", true}, {"docker-image://*/library/alpine:*", "docker-image://docker.io/library/alpine:latest", true}, {"docker-image://*/library/alpine:*", "docker-image://gcr.io/library/alpine:v1", true}, } for _, tt := range tests { t.Run(tt.pattern+"_"+tt.str, func(t *testing.T) { got := matchWildcard(tt.pattern, tt.str) if got != tt.want { t.Errorf("matchWildcard(%q, %q) = %v, want %v", tt.pattern, tt.str, got, tt.want) } }) } } func TestImageSourceIdentifier(t *testing.T) { tests := []struct { imageRef string want string }{ // Already in docker-image:// format {"docker-image://docker.io/library/alpine:latest", "docker-image://docker.io/library/alpine:latest"}, // Simple image names (no registry) {"alpine", "docker-image://docker.io/library/alpine"}, {"alpine:latest", "docker-image://docker.io/library/alpine:latest"}, {"alpine:3.18", "docker-image://docker.io/library/alpine:3.18"}, // User images (no registry, with username) {"myuser/myimage", "docker-image://docker.io/myuser/myimage"}, {"myuser/myimage:latest", "docker-image://docker.io/myuser/myimage:latest"}, // Full registry paths {"docker.io/library/alpine:latest", "docker-image://docker.io/library/alpine:latest"}, {"gcr.io/project/image:tag", "docker-image://gcr.io/project/image:tag"}, {"localhost:5000/myimage", "docker-image://localhost:5000/myimage"}, {"myregistry.com:8080/project/image:v1", "docker-image://myregistry.com:8080/project/image:v1"}, // Scratch (special case) {"scratch", "docker-image://scratch"}, // With digest (using valid 64-character hex digests) {"alpine@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "docker-image://docker.io/library/alpine@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, {"docker.io/library/alpine@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "docker-image://docker.io/library/alpine@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, } for _, tt := range tests { t.Run(tt.imageRef, func(t *testing.T) { got := ImageSourceIdentifier(tt.imageRef) if got != tt.want { t.Errorf("ImageSourceIdentifier(%q) = %q, want %q", tt.imageRef, got, tt.want) } }) } } func TestExtractImageRef(t *testing.T) { tests := []struct { sourceID string want string }{ {"docker-image://docker.io/library/alpine:latest", "docker.io/library/alpine:latest"}, {"docker-image://gcr.io/project/image:tag", "gcr.io/project/image:tag"}, {"docker-image://alpine", "alpine"}, // Non-docker-image sources (returned as-is) {"https://example.com/file.tar.gz", "https://example.com/file.tar.gz"}, {"git://github.com/user/repo.git#main", "git://github.com/user/repo.git#main"}, {"alpine:latest", "alpine:latest"}, } for _, tt := range tests { t.Run(tt.sourceID, func(t *testing.T) { got := ExtractImageRef(tt.sourceID) if got != tt.want { t.Errorf("ExtractImageRef(%q) = %q, want %q", tt.sourceID, got, tt.want) } }) } } ================================================ FILE: pkg/sshagent/sshagent.go ================================================ package sshagent import ( "errors" "fmt" "io" "net" "os" "path/filepath" "runtime" "sync" "time" "github.com/containers/buildah/internal/tmpdir" "github.com/opencontainers/selinux/go-selinux" "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" ) // AgentServer is an ssh agent that can be served and shutdown at a later time type AgentServer struct { agent agent.Agent wg sync.WaitGroup conn *net.Conn listener net.Listener shutdown chan bool servePath string serveDir string } // NewAgentServer creates a new agent on the host func NewAgentServer(source *Source) (*AgentServer, error) { if source.Keys != nil { return newAgentServerKeyring(source.Keys) } return newAgentServerSocket(source.Socket) } // newAgentServerKeyring creates a new agent from scratch and adds keys func newAgentServerKeyring(keys []any) (*AgentServer, error) { a := agent.NewKeyring() for _, k := range keys { if err := a.Add(agent.AddedKey{PrivateKey: k}); err != nil { return nil, fmt.Errorf("failed to create ssh agent: %w", err) } } return &AgentServer{ agent: a, shutdown: make(chan bool, 1), }, nil } // newAgentServerSocket creates a new agent from an existing agent on the host func newAgentServerSocket(socketPath string) (*AgentServer, error) { conn, err := net.Dial("unix", socketPath) if err != nil { return nil, err } a := &readOnlyAgent{agent.NewClient(conn)} return &AgentServer{ agent: a, conn: &conn, shutdown: make(chan bool, 1), }, nil } // Serve starts the SSH agent on the host and returns the path of the socket where the agent is serving func (a *AgentServer) Serve(processLabel string) (string, error) { // Calls to `selinux.SetSocketLabel` should be wrapped in // runtime.LockOSThread()/runtime.UnlockOSThread() until // the the socket is created to guarantee another goroutine // does not migrate to the current thread before execution // is complete. // Ref: https://github.com/opencontainers/selinux/blob/main/go-selinux/selinux.go#L158 runtime.LockOSThread() err := selinux.SetSocketLabel(processLabel) if err != nil { return "", err } serveDir, err := os.MkdirTemp(tmpdir.GetTempDir(), ".buildah-ssh-sock") if err != nil { return "", err } servePath := filepath.Join(serveDir, "ssh_auth_sock") a.serveDir = serveDir a.servePath = servePath listener, err := net.Listen("unix", servePath) if err != nil { return "", err } // Reset socket label. err = selinux.SetSocketLabel("") // Unlock the thread only if the process label could be restored // successfully. Otherwise leave the thread locked and the Go runtime // will terminate it once it returns to the threads pool. runtime.UnlockOSThread() if err != nil { return "", err } a.listener = listener go func() { for { // listener.Accept blocks c, err := listener.Accept() if err != nil { select { case <-a.shutdown: return default: logrus.Errorf("error accepting SSH connection: %v", err) continue } } a.wg.Add(1) go func() { // agent.ServeAgent will only ever return with error, err := agent.ServeAgent(a.agent, c) if err != io.EOF { logrus.Errorf("error serving agent: %v", err) } a.wg.Done() }() // the only way to get agent.ServeAgent is to close the connection it's serving on // TODO: ideally we should use some sort of forwarding mechanism for output instead of manually closing connection. go func() { time.Sleep(2000 * time.Millisecond) c.Close() }() } }() return a.servePath, nil } // Shutdown shuts down the agent and closes the socket func (a *AgentServer) Shutdown() error { if a.listener != nil { a.shutdown <- true a.listener.Close() } if a.conn != nil { conn := *a.conn conn.Close() } a.wg.Wait() err := os.RemoveAll(a.serveDir) if err != nil { return err } a.serveDir = "" a.servePath = "" return nil } // ServePath returns the path where the agent is serving func (a *AgentServer) ServePath() string { return a.servePath } // readOnlyAgent and its functions originally from github.com/mopby/buildkit/session/sshforward/sshprovider/agentprovider.go // readOnlyAgent implemetnts the agent.Agent interface // readOnlyAgent allows reads only to prevent keys from being added from the build to the forwarded ssh agent on the host type readOnlyAgent struct { agent.ExtendedAgent } func (a *readOnlyAgent) Add(_ agent.AddedKey) error { return errors.New("adding new keys not allowed by buildah") } func (a *readOnlyAgent) Remove(_ ssh.PublicKey) error { return errors.New("removing keys not allowed by buildah") } func (a *readOnlyAgent) RemoveAll() error { return errors.New("removing keys not allowed by buildah") } func (a *readOnlyAgent) Lock(_ []byte) error { return errors.New("locking agent not allowed by buildah") } func (a *readOnlyAgent) Extension(_ string, _ []byte) ([]byte, error) { return nil, errors.New("extensions not allowed by buildah") } // Source is what the forwarded agent's source is // The source of the forwarded agent can be from a socket on the host, or from individual key files type Source struct { Socket string Keys []any } // NewSource takes paths and checks of they are keys or sockets, and creates a source func NewSource(paths []string) (*Source, error) { var keys []any var socket string if len(paths) == 0 { socket = os.Getenv("SSH_AUTH_SOCK") if socket == "" { return nil, errors.New("SSH_AUTH_SOCK not set in environment") } absSocket, err := filepath.Abs(socket) if err != nil { return nil, fmt.Errorf("evaluating SSH_AUTH_SOCK in environment: %w", err) } socket = absSocket } for _, p := range paths { if socket != "" { return nil, errors.New("only one socket is allowed") } fi, err := os.Stat(p) if err != nil { return nil, err } if fi.Mode()&os.ModeSocket > 0 { if len(keys) == 0 { socket = p } else { return nil, errors.New("cannot mix keys and socket file") } continue } f, err := os.Open(p) if err != nil { return nil, err } dt, err := io.ReadAll(&io.LimitedReader{R: f, N: 100 * 1024}) if err != nil { return nil, err } k, err := ssh.ParseRawPrivateKey(dt) if err != nil { return nil, fmt.Errorf("cannot parse ssh key: %w", err) } keys = append(keys, k) } if socket != "" { return &Source{ Socket: socket, }, nil } return &Source{ Keys: keys, }, nil } ================================================ FILE: pkg/sshagent/sshagent_test.go ================================================ package sshagent import ( "crypto/rand" "crypto/rsa" "net" "testing" "github.com/stretchr/testify/require" "golang.org/x/crypto/ssh/agent" ) func testNewKeySource() (*Source, error) { k, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return nil, err } return &Source{ Keys: []any{k}, }, nil } func testClient(path string) ([]*agent.Key, error) { conn, err := net.Dial("unix", path) if err != nil { return nil, err } ac := agent.NewClient(conn) keys, err := ac.List() if err != nil { return nil, err } return keys, nil } func TestAgentServer(t *testing.T) { t.Parallel() src, err := testNewKeySource() require.NoError(t, err) ag, err := NewAgentServer(src) require.NoError(t, err) sock, err := ag.Serve("") require.NoError(t, err) // Get key from agent keys, err := testClient(sock) require.NoError(t, err) require.Equal(t, len(keys), 1) require.Equal(t, keys[0].Type(), "ssh-rsa") // Check for proper shutdown err = ag.Shutdown() require.NoError(t, err) _, err = testClient(sock) require.Error(t, err) } ================================================ FILE: pkg/supplemented/compat.go ================================================ // This package is deprecated. Its functionality has been moved to // github.com/containers/common/pkg/supplemented, which provides the same API. // The stubs and aliases here are present for compatibility with older code. // New implementations should use github.com/containers/common/pkg/supplemented // directly. package supplemented import ( digest "github.com/opencontainers/go-digest" "go.podman.io/common/pkg/manifests" "go.podman.io/common/pkg/supplemented" cp "go.podman.io/image/v5/copy" "go.podman.io/image/v5/types" ) var ( // ErrDigestNotFound is an alias for github.com/containers/common/pkg/manifests.ErrDigestNotFound. ErrDigestNotFound = manifests.ErrDigestNotFound // ErrBlobNotFound is an alias for github.com/containers/common/pkg/supplemented.ErrBlobNotFound. ErrBlobNotFound = supplemented.ErrBlobNotFound ) // Reference wraps github.com/containers/common/pkg/supplemented.Reference(). func Reference(ref types.ImageReference, supplemental []types.ImageReference, multiple cp.ImageListSelection, instances []digest.Digest) types.ImageReference { return supplemented.Reference(ref, supplemental, multiple, instances) } ================================================ FILE: pkg/umask/umask.go ================================================ package umask import ( "go.podman.io/common/pkg/umask" ) func CheckUmask() { umask.Check() } func SetUmask(value int) int { return umask.Set(value) } ================================================ FILE: pkg/util/resource_unix.go ================================================ //go:build linux || freebsd || darwin package util //nolint:revive,nolintlint import ( "fmt" "syscall" "github.com/docker/go-units" ) func ParseUlimit(ulimit string) (*units.Ulimit, error) { ul, err := units.ParseUlimit(ulimit) if err != nil { return nil, fmt.Errorf("ulimit option %q requires name=SOFT:HARD, failed to be parsed: %w", ulimit, err) } if ul.Hard != -1 && ul.Soft == -1 { return ul, nil } rl, err := ul.GetRlimit() if err != nil { return nil, err } var limit syscall.Rlimit if err := syscall.Getrlimit(rl.Type, &limit); err != nil { return nil, err } if ul.Soft == -1 { ul.Soft = int64(limit.Cur) } if ul.Hard == -1 { ul.Hard = int64(limit.Max) } return ul, nil } ================================================ FILE: pkg/util/resource_unix_test.go ================================================ package util //nolint:revive,nolintlint import ( "syscall" "testing" "github.com/stretchr/testify/assert" ) func TestParseUlimit(t *testing.T) { t.Parallel() _, err := ParseUlimit("bogus") assert.NotNil(t, err) ul, err := ParseUlimit("memlock=100:200") assert.Nil(t, err) assert.Equal(t, ul.Soft, int64(100)) assert.Equal(t, ul.Hard, int64(200)) var limit syscall.Rlimit err = syscall.Getrlimit(syscall.RLIMIT_NOFILE, &limit) assert.Nil(t, err) ul, err = ParseUlimit("nofile=-1:-1") assert.Nil(t, err) assert.Equal(t, ul.Soft, int64(limit.Cur)) assert.Equal(t, ul.Hard, int64(limit.Max)) ul, err = ParseUlimit("nofile=100:-1") assert.Nil(t, err) assert.Equal(t, ul.Soft, int64(100)) assert.Equal(t, ul.Hard, int64(limit.Max)) } ================================================ FILE: pkg/util/resource_windows.go ================================================ package util //nolint:revive,nolintlint import ( "fmt" "github.com/docker/go-units" ) func ParseUlimit(ulimit string) (*units.Ulimit, error) { ul, err := units.ParseUlimit(ulimit) if err != nil { return nil, fmt.Errorf("ulimit option %q requires name=SOFT:HARD, failed to be parsed: %w", ulimit, err) } return ul, nil } ================================================ FILE: pkg/util/test/test1/Containerfile ================================================ from scratch ================================================ FILE: pkg/util/uptime_darwin.go ================================================ package util //nolint:revive,nolintlint import ( "errors" "time" ) func ReadUptime() (time.Duration, error) { return 0, errors.New("readUptime not supported on darwin") } ================================================ FILE: pkg/util/uptime_freebsd.go ================================================ package util //nolint:revive,nolintlint import ( "time" "unsafe" "golang.org/x/sys/unix" ) // For some reason, unix.ClockGettime isn't implemented by x/sys/unix on FreeBSD func clockGettime(clockid int32, time *unix.Timespec) (err error) { _, _, e1 := unix.Syscall(unix.SYS_CLOCK_GETTIME, uintptr(clockid), uintptr(unsafe.Pointer(time)), 0) if e1 != 0 { return e1 } return nil } func ReadUptime() (time.Duration, error) { var uptime unix.Timespec if err := clockGettime(unix.CLOCK_UPTIME, &uptime); err != nil { return 0, err } return time.Duration(unix.TimespecToNsec(uptime)), nil } ================================================ FILE: pkg/util/uptime_linux.go ================================================ package util //nolint:revive,nolintlint import ( "bytes" "errors" "os" "time" ) func ReadUptime() (time.Duration, error) { buf, err := os.ReadFile("/proc/uptime") if err != nil { return 0, err } f := bytes.Fields(buf) if len(f) < 1 { return 0, errors.New("invalid uptime") } // Convert uptime in seconds to a human-readable format up := string(f[0]) upSeconds := up + "s" upDuration, err := time.ParseDuration(upSeconds) if err != nil { return 0, err } return upDuration, nil } ================================================ FILE: pkg/util/uptime_netbsd.go ================================================ package util //nolint:revive,nolintlint import ( "time" "unsafe" "golang.org/x/sys/unix" ) func clockGettime(clockid int32, time *unix.Timespec) (err error) { _, _, e1 := unix.Syscall(unix.SYS_CLOCK_GETTIME, uintptr(clockid), uintptr(unsafe.Pointer(time)), 0) if e1 != 0 { return e1 } return nil } func ReadUptime() (time.Duration, error) { tv, err := unix.SysctlTimeval("kern.boottime") if err != nil { return 0, err } sec, nsec := tv.Unix() return time.Now().Sub(time.Unix(sec, nsec)), nil } ================================================ FILE: pkg/util/uptime_windows.go ================================================ package util //nolint:revive,nolintlint import ( "errors" "time" ) func ReadUptime() (time.Duration, error) { return 0, errors.New("readUptime not supported on windows") } ================================================ FILE: pkg/util/util.go ================================================ package util //nolint:revive,nolintlint import ( "fmt" "os" "path/filepath" "strings" "github.com/containers/buildah/pkg/parse" ) // Mirrors path to a tmpfile if path points to a // file descriptor instead of actual file on filesystem // reason: operations with file descriptors are can lead // to edge cases where content on FD is not in a consumable // state after first consumption. // returns path as string and bool to confirm if temp file // was created and needs to be cleaned up. func MirrorToTempFileIfPathIsDescriptor(file string) (string, bool) { // one use-case is discussed here // https://github.com/containers/buildah/issues/3070 if !strings.HasPrefix(file, "/dev/fd/") { return file, false } b, err := os.ReadFile(file) if err != nil { // if anything goes wrong return original path return file, false } tmpfile, err := os.CreateTemp(parse.GetTempDir(), "buildah-temp-file") if err != nil { return file, false } defer tmpfile.Close() if _, err := tmpfile.Write(b); err != nil { // if anything goes wrong return original path return file, false } return tmpfile.Name(), true } // DiscoverContainerfile tries to find a Containerfile or a Dockerfile within the provided `path`. func DiscoverContainerfile(path string) (foundCtrFile string, err error) { // Test for existence of the file target, err := os.Stat(path) if err != nil { return "", fmt.Errorf("discovering Containerfile: %w", err) } switch mode := target.Mode(); { case mode.IsDir(): // If the path is a real directory, we assume a Containerfile or a Dockerfile within it ctrfile := filepath.Join(path, "Containerfile") // Test for existence of the Containerfile file file, err := os.Stat(ctrfile) if err != nil { // See if we have a Dockerfile within it ctrfile = filepath.Join(path, "Dockerfile") // Test for existence of the Dockerfile file file, err = os.Stat(ctrfile) if err != nil { return "", fmt.Errorf("cannot find Containerfile or Dockerfile in context directory: %w", err) } } // The file exists, now verify the correct mode if mode := file.Mode(); mode.IsRegular() { foundCtrFile = ctrfile } else { return "", fmt.Errorf("assumed Containerfile %q is not a file", ctrfile) } case mode.IsRegular(): // If the context dir is a file, we assume this as Containerfile foundCtrFile = path } return foundCtrFile, nil } ================================================ FILE: pkg/util/util_test.go ================================================ package util //nolint:revive,nolintlint import ( "testing" "github.com/stretchr/testify/assert" ) func TestDiscoverContainerfile(t *testing.T) { t.Parallel() _, err := DiscoverContainerfile("./bogus") assert.NotNil(t, err) _, err = DiscoverContainerfile("./") assert.NotNil(t, err) name, err := DiscoverContainerfile("test/test1/Dockerfile") assert.Nil(t, err) assert.Equal(t, name, "test/test1/Dockerfile") name, err = DiscoverContainerfile("test/test1/Containerfile") assert.Nil(t, err) assert.Equal(t, name, "test/test1/Containerfile") name, err = DiscoverContainerfile("test/test1") assert.Nil(t, err) assert.Equal(t, name, "test/test1/Containerfile") name, err = DiscoverContainerfile("test/test2") assert.Nil(t, err) assert.Equal(t, name, "test/test2/Dockerfile") } ================================================ FILE: pkg/util/version_unix.go ================================================ //go:build !windows package util //nolint:revive,nolintlint import ( "bytes" "golang.org/x/sys/unix" ) func ReadKernelVersion() (string, error) { var uname unix.Utsname if err := unix.Uname(&uname); err != nil { return "", err } n := bytes.IndexByte(uname.Release[:], 0) return string(uname.Release[:n]), nil } ================================================ FILE: pkg/util/version_windows.go ================================================ package util //nolint:revive,nolintlint import ( "errors" ) func ReadKernelVersion() (string, error) { return "", errors.New("readKernelVersion not supported on windows") } ================================================ FILE: pkg/volumes/volumes.go ================================================ package volumes import ( "os" "github.com/containers/buildah/internal/volumes" ) // CleanCacheMount gets the cache parent created by `--mount=type=cache` and removes it. func CleanCacheMount() error { cacheParent := volumes.CacheParent() return os.RemoveAll(cacheParent) } ================================================ FILE: plans/main.fmf ================================================ discover: how: fmf execute: how: tmt prepare: - when: distro == centos-stream or distro == rhel how: shell script: | dnf -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-$(rpm --eval '%{?rhel}').noarch.rpm dnf -y config-manager --set-enabled epel order: 10 - when: initiator == packit how: shell script: | COPR_REPO_FILE="/etc/yum.repos.d/*podman-next*.repo" if compgen -G $COPR_REPO_FILE > /dev/null; then sed -i -n '/^priority=/!p;$apriority=1' $COPR_REPO_FILE fi dnf -y upgrade --allowerasing order: 20 provision: how: artemis hardware: memory: ">= 16 GB" cpu: cores: ">= 4" threads: ">=8" disk: - size: ">= 512 GB" ================================================ FILE: pull.go ================================================ package buildah import ( "context" "fmt" "io" "time" "github.com/containers/buildah/define" encconfig "github.com/containers/ocicrypt/config" "go.podman.io/common/libimage" "go.podman.io/common/pkg/config" "go.podman.io/image/v5/types" "go.podman.io/storage" ) // PullOptions can be used to alter how an image is copied in from somewhere. type PullOptions struct { // SignaturePolicyPath specifies an override location for the signature // policy which should be used for verifying the new image as it is // being written. Except in specific circumstances, no value should be // specified, indicating that the shared, system-wide default policy // should be used. SignaturePolicyPath string // ReportWriter is an io.Writer which will be used to log the writing // of the new image. ReportWriter io.Writer // Store is the local storage store which holds the source image. Store storage.Store // github.com/containers/image/types SystemContext to hold credentials // and other authentication/authorization information. SystemContext *types.SystemContext // BlobDirectory is the name of a directory in which we'll attempt to // store copies of layer blobs that we pull down, if any. It should // already exist. // // Not applicable if DestinationLookupReferenceFunc is set. BlobDirectory string // AllTags is a boolean value that determines if all tagged images // will be downloaded from the repository. The default is false. AllTags bool // RemoveSignatures causes any existing signatures for the image to be // discarded when pulling it. RemoveSignatures bool // MaxRetries is the maximum number of attempts we'll make to pull any // one image from the external registry if the first attempt fails. MaxRetries int // RetryDelay is how long to wait before retrying a pull attempt. RetryDelay time.Duration // OciDecryptConfig contains the config that can be used to decrypt an image if it is // encrypted if non-nil. If nil, it does not attempt to decrypt an image. OciDecryptConfig *encconfig.DecryptConfig // PullPolicy takes the value PullIfMissing, PullAlways, PullIfNewer, or PullNever. PullPolicy define.PullPolicy // SourceLookupReference provides a function to look up source // references. SourceLookupReferenceFunc libimage.LookupReferenceFunc // DestinationLookupReference provides a function to look up destination // references. Overrides BlobDirectory, if set. DestinationLookupReferenceFunc libimage.LookupReferenceFunc } // Pull copies the contents of the image from somewhere else to local storage. Returns the // ID of the local image or an error. func Pull(_ context.Context, imageName string, options PullOptions) (imageID string, err error) { libimageOptions := &libimage.PullOptions{} libimageOptions.SignaturePolicyPath = options.SignaturePolicyPath libimageOptions.Writer = options.ReportWriter libimageOptions.RemoveSignatures = options.RemoveSignatures libimageOptions.OciDecryptConfig = options.OciDecryptConfig libimageOptions.AllTags = options.AllTags libimageOptions.RetryDelay = &options.RetryDelay libimageOptions.SourceLookupReferenceFunc = options.SourceLookupReferenceFunc if options.DestinationLookupReferenceFunc != nil { libimageOptions.DestinationLookupReferenceFunc = options.DestinationLookupReferenceFunc } else { libimageOptions.DestinationLookupReferenceFunc = cacheLookupReferenceFunc(options.BlobDirectory, types.PreserveOriginal) } if options.MaxRetries > 0 { retries := uint(options.MaxRetries) libimageOptions.MaxRetries = &retries } pullPolicy, err := config.ParsePullPolicy(options.PullPolicy.String()) if err != nil { return "", err } runtime, err := libimage.RuntimeFromStore(options.Store, &libimage.RuntimeOptions{SystemContext: options.SystemContext}) if err != nil { return "", err } pulledImages, err := runtime.Pull(context.Background(), imageName, pullPolicy, libimageOptions) if err != nil { return "", err } if len(pulledImages) == 0 { return "", fmt.Errorf("internal error pulling %s: no image pulled and no error", imageName) } return pulledImages[0].ID(), nil } ================================================ FILE: push.go ================================================ package buildah import ( "context" "fmt" "io" "time" "github.com/containers/buildah/pkg/blobcache" encconfig "github.com/containers/ocicrypt/config" digest "github.com/opencontainers/go-digest" "github.com/sirupsen/logrus" "go.podman.io/common/libimage" "go.podman.io/image/v5/docker/reference" "go.podman.io/image/v5/manifest" "go.podman.io/image/v5/pkg/compression" "go.podman.io/image/v5/transports" "go.podman.io/image/v5/types" "go.podman.io/storage" "go.podman.io/storage/pkg/archive" ) // cacheLookupReferenceFunc wraps a BlobCache into a // libimage.LookupReferenceFunc to allow for using a BlobCache during // image-copy operations. func cacheLookupReferenceFunc(directory string, compress types.LayerCompression) libimage.LookupReferenceFunc { // Using a closure here allows us to reference a BlobCache without // having to explicitly maintain it in the libimage API. return func(ref types.ImageReference) (types.ImageReference, error) { if directory == "" { return ref, nil } ref, err := blobcache.NewBlobCache(ref, directory, compress) if err != nil { return nil, fmt.Errorf("using blobcache %q: %w", directory, err) } return ref, nil } } // PushOptions can be used to alter how an image is copied somewhere. type PushOptions struct { // Compression specifies the type of compression which is applied to // layer blobs. The default is to not use compression, but // archive.Gzip is recommended. // OBSOLETE: Use CompressionFormat instead. Compression archive.Compression // SignaturePolicyPath specifies an override location for the signature // policy which should be used for verifying the new image as it is // being written. Except in specific circumstances, no value should be // specified, indicating that the shared, system-wide default policy // should be used. SignaturePolicyPath string // ReportWriter is an io.Writer which will be used to log the writing // of the new image. ReportWriter io.Writer // Store is the local storage store which holds the source image. Store storage.Store // github.com/containers/image/types SystemContext to hold credentials // and other authentication/authorization information. SystemContext *types.SystemContext // ManifestType is the format to use // possible options are oci, v2s1, and v2s2 ManifestType string // BlobDirectory is the name of a directory in which we'll look for // prebuilt copies of layer blobs that we might otherwise need to // regenerate from on-disk layers, substituting them in the list of // blobs to copy whenever possible. // // Not applicable if SourceLookupReferenceFunc is set. BlobDirectory string // Quiet is a boolean value that determines if minimal output to // the user will be displayed, this is best used for logging. // The default is false. Quiet bool // SignBy is the fingerprint of a GPG key to use for signing the image. SignBy string // RemoveSignatures causes any existing signatures for the image to be // discarded for the pushed copy. RemoveSignatures bool // MaxRetries is the maximum number of attempts we'll make to push any // one image to the external registry if the first attempt fails. MaxRetries int // RetryDelay is how long to wait before retrying a push attempt. RetryDelay time.Duration // OciEncryptConfig when non-nil indicates that an image should be encrypted. // The encryption options is derived from the construction of EncryptConfig object. OciEncryptConfig *encconfig.EncryptConfig // OciEncryptLayers represents the list of layers to encrypt. // If nil, don't encrypt any layers. // If non-nil and len==0, denotes encrypt all layers. // integers in the slice represent 0-indexed layer indices, with support for negative // indexing. i.e. 0 is the first layer, -1 is the last (top-most) layer. OciEncryptLayers *[]int // SourceLookupReference provides a function to look up source // references. Overrides BlobDirectory, if set. SourceLookupReferenceFunc libimage.LookupReferenceFunc // DestinationLookupReference provides a function to look up destination // references. DestinationLookupReferenceFunc libimage.LookupReferenceFunc // CompressionFormat is the format to use for the compression of the blobs CompressionFormat *compression.Algorithm // CompressionLevel specifies what compression level is used CompressionLevel *int // ForceCompressionFormat ensures that the compression algorithm set in // CompressionFormat is used exclusively, and blobs of other compression // algorithms are not reused. ForceCompressionFormat bool } // Push copies the contents of the image to a new location. func Push(ctx context.Context, image string, dest types.ImageReference, options PushOptions) (reference.Canonical, digest.Digest, error) { libimageOptions := &libimage.PushOptions{} libimageOptions.SignaturePolicyPath = options.SignaturePolicyPath libimageOptions.Writer = options.ReportWriter libimageOptions.ManifestMIMEType = options.ManifestType libimageOptions.SignBy = options.SignBy libimageOptions.RemoveSignatures = options.RemoveSignatures libimageOptions.RetryDelay = &options.RetryDelay libimageOptions.OciEncryptConfig = options.OciEncryptConfig libimageOptions.OciEncryptLayers = options.OciEncryptLayers libimageOptions.CompressionFormat = options.CompressionFormat libimageOptions.CompressionLevel = options.CompressionLevel libimageOptions.ForceCompressionFormat = options.ForceCompressionFormat libimageOptions.PolicyAllowStorage = true if options.Quiet { libimageOptions.Writer = nil } compress := types.PreserveOriginal if options.Compression == archive.Gzip { compress = types.Compress } if options.SourceLookupReferenceFunc != nil { libimageOptions.SourceLookupReferenceFunc = options.SourceLookupReferenceFunc } else { libimageOptions.SourceLookupReferenceFunc = cacheLookupReferenceFunc(options.BlobDirectory, compress) } libimageOptions.DestinationLookupReferenceFunc = options.DestinationLookupReferenceFunc runtime, err := libimage.RuntimeFromStore(options.Store, &libimage.RuntimeOptions{SystemContext: options.SystemContext}) if err != nil { return nil, "", err } destString := fmt.Sprintf("%s:%s", dest.Transport().Name(), dest.StringWithinTransport()) manifestBytes, err := runtime.Push(ctx, image, destString, libimageOptions) if err != nil { return nil, "", err } manifestDigest, err := manifest.Digest(manifestBytes) if err != nil { return nil, "", fmt.Errorf("computing digest of manifest of new image %q: %w", transports.ImageName(dest), err) } var ref reference.Canonical if name := dest.DockerReference(); name != nil { ref, err = reference.WithDigest(name, manifestDigest) if err != nil { logrus.Warnf("error generating canonical reference with name %q and digest %s: %v", name, manifestDigest.String(), err) } } return ref, manifestDigest, nil } ================================================ FILE: release.sh ================================================ #!/bin/sh # # Cut a buildah release. Usage: # # $ hack/release.sh # # For example: # # $ hack/release.sh 1.2.3 1.3.0 # # for "I'm cutting 1.2.3, and want to use 1.3.0-dev for future work". VERSION="$1" NEXT_VERSION="$2" DATE=$(date '+%Y-%m-%d') LAST_TAG=$(git describe --tags --abbrev=0) write_go_version() { LOCAL_VERSION="$1" sed -i "s/^\(.*Version = \"\).*/\1${LOCAL_VERSION}\"/" define/types.go } write_makefile_epoch() { LOCAL_EPOCH="$1" sed -i "s/^\(EPOCH_TEST_COMMIT ?= \).*/\1${LOCAL_EPOCH}/" Makefile } write_changelog() { echo "- Changelog for v${VERSION} (${DATE})" >.changelog.txt && git log --no-merges --format=' * %s' "${LAST_TAG}..HEAD" >>.changelog.txt && echo >>.changelog.txt && cat changelog.txt >>.changelog.txt && mv -f .changelog.txt changelog.txt echo " ## v${VERSION} (${DATE}) " >.CHANGELOG.md && git log --no-merges --format=' %s' "${LAST_TAG}..HEAD" >>.CHANGELOG.md && sed -i -e '/# Changelog/r .CHANGELOG.md' CHANGELOG.md && rm -f .CHANGELOG.md } release_commit() { write_go_version "${VERSION}" && write_changelog && git commit -asm "Bump to v${VERSION} [NO TESTS NEEDED] " } dev_version_commit() { write_go_version "${NEXT_VERSION}-dev" && git commit -asm "Bump to v${NEXT_VERSION}-dev [NO TESTS NEEDED] " } epoch_commit() { LOCAL_EPOCH="$1" write_makefile_epoch "${LOCAL_EPOCH}" && git commit -asm 'Bump gitvalidation epoch [NO TESTS NEEDED] ' } git fetch origin && git checkout -b "bump-${VERSION}" origin/main && EPOCH=$(git rev-parse HEAD) && release_commit && git tag -s -m "version ${VERSION}" "v${VERSION}" && dev_version_commit && epoch_commit "${EPOCH}" ================================================ FILE: rpm/Makefile ================================================ .PHONY: rpm rpm: $(shell /usr/bin/bash ./update-spec-version.sh) spectool -g buildah.spec rpmbuild -ba \ --define '_sourcedir $(shell pwd)' \ --define '_rpmdir %{_sourcedir}/RPMS' \ --define '_srcrpmdir %{_sourcedir}/SRPMS' \ --define '_builddir %{_sourcedir}/BUILD' \ buildah.spec @echo ___RPMS can be found in rpm/RPMS/.___ @echo ___Undo any changes to Version, Source0 and %autosetup in rpm/buildah.spec before committing.___ ================================================ FILE: rpm/buildah.spec ================================================ %global with_debug 1 %if 0%{?with_debug} %global _find_debuginfo_dwz_opts %{nil} %global _dwz_low_mem_die_limit 0 %else %global debug_package %{nil} %endif %global gomodulesmode GO111MODULE=on %if %{defined fedora} %define build_with_btrfs 1 %if 0%{?fedora} >= 43 %define sequoia 1 %endif %endif %if %{defined rhel} %define fips 1 %endif %global git0 https://github.com/containers/%{name} Name: buildah # Set different Epoch for copr %if %{defined copr_username} Epoch: 102 %else Epoch: 2 %endif # DO NOT TOUCH the Version string! # The TRUE source of this specfile is: # https://github.com/containers/skopeo/blob/main/rpm/skopeo.spec # If that's what you're reading, Version must be 0, and will be updated by Packit for # copr and koji builds. # If you're reading this on dist-git, the version is automatically filled in by Packit. Version: 0 # The `AND` needs to be uppercase in the License for SPDX compatibility License: Apache-2.0 AND BSD-2-Clause AND BSD-3-Clause AND ISC AND MIT AND MPL-2.0 Release: %autorelease %if %{defined golang_arches_future} ExclusiveArch: %{golang_arches_future} %else ExclusiveArch: aarch64 ppc64le s390x x86_64 %endif Summary: A command line tool used for creating OCI Images URL: https://%{name}.io # Tarball fetched from upstream Source: %{git0}/archive/v%{version}.tar.gz BuildRequires: device-mapper-devel BuildRequires: git-core BuildRequires: golang >= 1.16.6 BuildRequires: glib2-devel BuildRequires: glibc-static %if !%{defined gobuild} BuildRequires: go-rpm-macros %endif BuildRequires: gpgme-devel BuildRequires: libassuan-devel BuildRequires: make %if %{defined build_with_btrfs} BuildRequires: btrfs-progs-devel %endif BuildRequires: shadow-utils-subid-devel BuildRequires: sqlite-devel Requires: containers-common-extra %if %{defined fedora} BuildRequires: libseccomp-static %else BuildRequires: libseccomp-devel %endif Requires: libseccomp >= 2.4.1-0 Suggests: cpp %if %{defined sequoia} Requires: podman-sequoia %endif %description The %{name} package provides a command line tool which can be used to * create a working container from scratch or * create a working container from an image as a starting point * mount/umount a working container's root file system for manipulation * save container's root file system layer to create a new image * delete a working container or an image # This subpackage is only intended for CI testing. # Not meant for end user/customer usage. %package tests Summary: Tests for %{name} Requires: %{name} = %{epoch}:%{version}-%{release} %if %{defined bats_epel} Requires: bats %else Recommends: bats %endif Requires: bzip2 Requires: podman Requires: golang Requires: jq Requires: httpd-tools Requires: openssl Requires: nmap-ncat Requires: git-daemon %description tests %{summary} This package contains system tests for %{name} %prep %autosetup -Sgit -n %{name}-%{version} %build %set_build_flags export CGO_CFLAGS=$CFLAGS # These extra flags present in $CFLAGS have been skipped for now as they break the build CGO_CFLAGS=$(echo $CGO_CFLAGS | sed 's/-flto=auto//g') CGO_CFLAGS=$(echo $CGO_CFLAGS | sed 's/-Wp,D_GLIBCXX_ASSERTIONS//g') CGO_CFLAGS=$(echo $CGO_CFLAGS | sed 's/-specs=\/usr\/lib\/rpm\/redhat\/redhat-annobin-cc1//g') %ifarch x86_64 export CGO_CFLAGS+=" -m64 -mtune=generic -fcf-protection=full" %endif export LDFLAGS="-X main.buildInfo=`date +%s`" export BUILDTAGS="seccomp $(hack/systemd_tag.sh) $(hack/libsubid_tag.sh) libsqlite3" %if !%{defined build_with_btrfs} export BUILDTAGS+=" exclude_graphdriver_btrfs" %endif %if %{defined fips} export BUILDTAGS+=" libtrust_openssl" %endif %if %{defined sequoia} export BUILDTAGS+=" containers_image_sequoia" %endif %{__rm} -f internal/mkcw/embed/entrypoint_amd64.gz %{__make} internal/mkcw/embed/entrypoint_amd64.gz %gobuild -o bin/%{name} ./cmd/%{name} %gobuild -o bin/imgtype ./tests/imgtype %gobuild -o bin/copy ./tests/copy %gobuild -o bin/tutorial ./tests/tutorial %gobuild -o bin/inet ./tests/inet %gobuild -o bin/dumpspec ./tests/dumpspec %gobuild -o bin/passwd ./tests/passwd %gobuild -o bin/crash ./tests/crash %gobuild -o bin/wait ./tests/wait %gobuild -o bin/grpcnoop ./tests/rpc/noop %{__make} docs %install make DESTDIR=%{buildroot} PREFIX=%{_prefix} install install.completions install -d -p %{buildroot}/%{_datadir}/%{name}/test/system cp -pav tests/. %{buildroot}/%{_datadir}/%{name}/test/system cp bin/imgtype %{buildroot}/%{_bindir}/%{name}-imgtype cp bin/copy %{buildroot}/%{_bindir}/%{name}-copy cp bin/tutorial %{buildroot}/%{_bindir}/%{name}-tutorial cp bin/inet %{buildroot}/%{_bindir}/%{name}-inet cp bin/dumpspec %{buildroot}/%{_bindir}/%{name}-dumpspec cp bin/passwd %{buildroot}/%{_bindir}/%{name}-passwd cp bin/crash %{buildroot}/%{_bindir}/%{name}-crash cp bin/wait %{buildroot}/%{_bindir}/%{name}-wait cp bin/grpcnoop %{buildroot}/%{_bindir}/%{name}-grpcnoop rm %{buildroot}%{_datadir}/%{name}/test/system/tools/build/* #define license tag if not already defined %{!?_licensedir:%global license %doc} # Include check to silence rpmlint. %check %files %license LICENSE vendor/modules.txt %doc README.md %{_bindir}/%{name} %{_mandir}/man1/%{name}* %dir %{_datadir}/bash-completion %dir %{_datadir}/bash-completion/completions %{_datadir}/bash-completion/completions/%{name} %files tests %license LICENSE %{_bindir}/%{name}-imgtype %{_bindir}/%{name}-copy %{_bindir}/%{name}-tutorial %{_bindir}/%{name}-inet %{_bindir}/%{name}-dumpspec %{_bindir}/%{name}-passwd %{_bindir}/%{name}-crash %{_bindir}/%{name}-wait %{_bindir}/%{name}-grpcnoop %{_datadir}/%{name}/test %changelog %autochangelog ================================================ FILE: rpm/gating.yaml ================================================ --- !Policy product_versions: - fedora-* decision_contexts: - bodhi_update_push_stable - bodhi_update_push_testing subject_type: koji_build rules: - !PassingTestCaseRule {test_case_name: fedora-ci.koji-build.tier0.functional} --- !Policy product_versions: - rhel-* decision_context: osci_compose_gate rules: - !PassingTestCaseRule {test_case_name: osci.brew-build.tier0.functional} ================================================ FILE: rpm/update-spec-version.sh ================================================ #!/usr/bin/env bash # This script will update the Version field in the spec which is set to 0 by # default. Useful for local manual rpm builds where the Version needs to be set # correctly. set -eox pipefail PACKAGE=buildah SPEC_FILE=$PACKAGE.spec VERSION=$(grep 'Version = ' ../define/types.go | cut -d\" -f2) RPM_VERSION=$(echo $VERSION | sed -e 's/^v//' -e 's/-/~/g') # Update spec file to use local changes sed -i "s/^Version:.*/Version: $RPM_VERSION/" $SPEC_FILE sed -i "s/^Source:.*/Source: $PACKAGE-$VERSION.tar.gz/" $SPEC_FILE sed -i "s/^%autosetup.*/%autosetup -Sgit -n %{name}-$VERSION/" $SPEC_FILE # Generate Source0 archive from HEAD (cd .. && git archive --format=tar.gz --prefix=$PACKAGE-$VERSION/ HEAD -o rpm/$PACKAGE-$VERSION.tar.gz) ================================================ FILE: run.go ================================================ package buildah import ( "fmt" "io" "net" "github.com/containers/buildah/copier" "github.com/containers/buildah/define" "github.com/containers/buildah/internal" "github.com/containers/buildah/pkg/sshagent" "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" "go.podman.io/common/libnetwork/etchosts" "go.podman.io/image/v5/types" "go.podman.io/storage/pkg/idtools" "go.podman.io/storage/pkg/lockfile" ) const ( // runUsingRuntimeCommand is a command we use as a key for reexec runUsingRuntimeCommand = define.Package + "-oci-runtime" ) // compatLayerExclusions is the set of items to omit from layers if // options.CompatLayerOmissions is set to true. For whatever reason, the // classic builder didn't bake these into images, but BuildKit does. var compatLayerExclusions = []copier.ConditionalRemovePath{ {Path: "dev", Owner: &idtools.IDPair{UID: 0, GID: 0}}, {Path: "proc", Owner: &idtools.IDPair{UID: 0, GID: 0}}, {Path: "sys", Owner: &idtools.IDPair{UID: 0, GID: 0}}, } // TerminalPolicy takes the value DefaultTerminal, WithoutTerminal, or WithTerminal. type TerminalPolicy int const ( // DefaultTerminal indicates that this Run invocation should be // connected to a pseudoterminal if we're connected to a terminal. DefaultTerminal TerminalPolicy = iota // WithoutTerminal indicates that this Run invocation should NOT be // connected to a pseudoterminal. WithoutTerminal // WithTerminal indicates that this Run invocation should be connected // to a pseudoterminal. WithTerminal ) // String converts a TerminalPolicy into a string. func (t TerminalPolicy) String() string { switch t { case DefaultTerminal: return "DefaultTerminal" case WithoutTerminal: return "WithoutTerminal" case WithTerminal: return "WithTerminal" } return fmt.Sprintf("unrecognized terminal setting %d", t) } // NamespaceOption controls how we set up a namespace when launching processes. type NamespaceOption = define.NamespaceOption // NamespaceOptions provides some helper methods for a slice of NamespaceOption // structs. type NamespaceOptions = define.NamespaceOptions // IDMappingOptions controls how we set up UID/GID mapping when we set up a // user namespace. type IDMappingOptions = define.IDMappingOptions // Isolation provides a way to specify whether we're supposed to use a proper // OCI runtime, or some other method for running commands. type Isolation = define.Isolation const ( // IsolationDefault is whatever we think will work best. IsolationDefault = define.IsolationDefault // IsolationOCI is a proper OCI runtime. IsolationOCI = define.IsolationOCI // IsolationChroot is a more chroot-like environment: less isolation, // but with fewer requirements. IsolationChroot = define.IsolationChroot // IsolationOCIRootless is a proper OCI runtime in rootless mode. IsolationOCIRootless = define.IsolationOCIRootless ) // RunOptions can be used to alter how a command is run in the container. type RunOptions struct { // Logger is the logrus logger to write log messages with Logger *logrus.Logger `json:"-"` // Hostname is the hostname we set for the running container. Hostname string // Isolation is either IsolationDefault, IsolationOCI, IsolationChroot, or IsolationOCIRootless. Isolation define.Isolation // Runtime is the name of the runtime to run. It should accept the // same arguments that runc does, and produce similar output. Runtime string // Args adds global arguments for the runtime. Args []string // NoHostname won't create new /etc/hostname file NoHostname bool // NoHosts won't create new /etc/hosts file NoHosts bool // NoPivot adds the --no-pivot runtime flag. NoPivot bool // Mounts are additional mount points which we want to provide. Mounts []specs.Mount // Env is additional environment variables to set. Env []string // User is the user as whom to run the command. User string // WorkingDir is an override for the working directory. WorkingDir string // ContextDir is used as the root directory for the source location for mounts that are of type "bind". ContextDir string // Shell is default shell to run in a container. Shell string // Cmd is an override for the configured default command. Cmd []string // Entrypoint is an override for the configured entry point. Entrypoint []string // NamespaceOptions controls how we set up the namespaces for the process. NamespaceOptions define.NamespaceOptions // ConfigureNetwork controls whether or not network interfaces and // routing are configured for a new network namespace (i.e., when not // joining another's namespace and not just using the host's // namespace), effectively deciding whether or not the process has a // usable network. ConfigureNetwork define.NetworkConfigurationPolicy // Deprecated: CNIPluginPath was the location of CNI plugin helpers. // It is no longer used and is expected to be empty. CNIPluginPath string // Deprecated: CNIConfigDir was the location of CNI configuration files. // It is no longer used and is expected to be empty. CNIConfigDir string // Terminal provides a way to specify whether or not the command should // be run with a pseudoterminal. By default (DefaultTerminal), a // terminal is used if os.Stdout is connected to a terminal, but that // decision can be overridden by specifying either WithTerminal or // WithoutTerminal. Terminal TerminalPolicy // TerminalSize provides a way to set the number of rows and columns in // a pseudo-terminal, if we create one, and Stdin/Stdout/Stderr aren't // connected to a terminal. TerminalSize *specs.Box // The stdin/stdout/stderr descriptors to use. If set to nil, the // corresponding files in the "os" package are used as defaults. Stdin io.Reader `json:"-"` Stdout io.Writer `json:"-"` Stderr io.Writer `json:"-"` // Quiet tells the run to turn off output to stdout. Quiet bool // AddCapabilities is a list of capabilities to add to the default set. AddCapabilities []string // DropCapabilities is a list of capabilities to remove from the default set, // after processing the AddCapabilities set. If a capability appears in both // lists, it will be dropped. DropCapabilities []string // Devices are parsed additional devices to add Devices define.ContainerDevices // DeviceSpecs are unparsed additional devices to add DeviceSpecs []string // Secrets are the available secrets to use Secrets map[string]define.Secret // SSHSources is the available ssh agents to use SSHSources map[string]*sshagent.Source `json:"-"` // RunMounts are unparsed mounts to be added for this run RunMounts []string // Map of already-mounted stages, images, and container mountpoints // which entries in `RunMounts` might be referring to. If a value for // the "" key is present, it points to the context directory. StageMountPoints map[string]internal.StageMountDetails // IDs of mounted images to be unmounted before returning // Deprecated: before 1.39, these images would not be consistently // unmounted if Run() returned an error ExternalImageMounts []string // System context of current build SystemContext *types.SystemContext `json:"-"` // CgroupManager to use for running OCI containers CgroupManager string // CDIConfigDir is the location of CDI configuration files, if the files in // the default configuration locations shouldn't be used. CDIConfigDir string // CompatBuiltinVolumes causes the contents of locations marked as // volumes in the container's configuration to be set up as bind mounts to // directories which are not in the container's rootfs, hiding changes // made to contents of those changes when the container is subsequently // committed. CompatBuiltinVolumes types.OptionalBool } // RunMountArtifacts are the artifacts created when using a run mount. type runMountArtifacts struct { // RunOverlayDirs are overlay directories which will need to be cleaned up using overlay.RemoveTemp() RunOverlayDirs []string // Any images which were mounted, which should be unmounted MountedImages []string // Agents are the ssh agents started, which should have their Shutdown() methods called Agents []*sshagent.AgentServer // Lock files, which should have their Unlock() methods called TargetLocks []*lockfile.LockFile // Intermediate mount points, which should be Unmount()ed and Removed()d IntermediateMounts []string // Environment variables that should be set for RUN that may contain secrets, each is name=value form EnvVars []string } // RunMountInfo are the available run mounts for this run type runMountInfo struct { // WorkDir is the current working directory inside the container. WorkDir string // ContextDir is the root directory for the source location for bind mounts. ContextDir string // Secrets are the available secrets to use in a RUN Secrets map[string]define.Secret // SSHSources is the available ssh agents to use in a RUN SSHSources map[string]*sshagent.Source `json:"-"` // Map of stages and container mountpoint if any from stage executor StageMountPoints map[string]internal.StageMountDetails // System context of current build SystemContext *types.SystemContext } // IDMaps are the UIDs, GID, and maps for the run type IDMaps struct { uidmap []specs.LinuxIDMapping gidmap []specs.LinuxIDMapping rootUID int rootGID int processUID int processGID int } // netResult type to hold network info for hosts/resolv.conf type netResult struct { entries etchosts.HostEntries dnsServers []string excludeIPs []net.IP ipv6 bool keepHostResolvers bool preferredHostContainersInternalIP string } ================================================ FILE: run_common.go ================================================ //go:build linux || freebsd package buildah import ( "archive/tar" "bytes" "encoding/json" "errors" "fmt" "io" "io/fs" "net" "os" "os/exec" "os/signal" "path" "path/filepath" "runtime" "slices" "strconv" "strings" "sync" "sync/atomic" "syscall" "time" "github.com/containers/buildah/bind" "github.com/containers/buildah/copier" "github.com/containers/buildah/define" "github.com/containers/buildah/internal" "github.com/containers/buildah/internal/tmpdir" "github.com/containers/buildah/internal/volumes" "github.com/containers/buildah/pkg/overlay" "github.com/containers/buildah/pkg/sshagent" "github.com/containers/buildah/util" "github.com/opencontainers/go-digest" "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-tools/generate" "github.com/opencontainers/selinux/go-selinux/label" "github.com/sirupsen/logrus" "go.podman.io/common/libnetwork/etchosts" "go.podman.io/common/libnetwork/network" "go.podman.io/common/libnetwork/resolvconf" netTypes "go.podman.io/common/libnetwork/types" netUtil "go.podman.io/common/libnetwork/util" "go.podman.io/common/pkg/config" "go.podman.io/common/pkg/subscriptions" "go.podman.io/image/v5/types" "go.podman.io/storage" "go.podman.io/storage/pkg/fileutils" "go.podman.io/storage/pkg/idtools" "go.podman.io/storage/pkg/ioutils" "go.podman.io/storage/pkg/lockfile" "go.podman.io/storage/pkg/mount" "go.podman.io/storage/pkg/reexec" "go.podman.io/storage/pkg/regexp" "go.podman.io/storage/pkg/unshare" "golang.org/x/sys/unix" "golang.org/x/term" ) const maxHostnameLen = 64 var validHostnames = regexp.Delayed("[A-Za-z0-9][A-Za-z0-9.-]+") func (b *Builder) createResolvConf(rdir string, chownOpts *idtools.IDPair) (string, error) { cfile := filepath.Join(rdir, "resolv.conf") f, err := os.Create(cfile) if err != nil { return "", err } defer f.Close() uid := 0 gid := 0 if chownOpts != nil { uid = chownOpts.UID gid = chownOpts.GID } if err = f.Chown(uid, gid); err != nil { return "", err } if err := relabel(cfile, b.MountLabel, false); err != nil { return "", err } return cfile, nil } // addResolvConf copies files from host and sets them up to bind mount into container func (b *Builder) addResolvConfEntries(file string, networkNameServer []string, spec *specs.Spec, keepHostServers, ipv6 bool, ) error { defaultConfig, err := config.Default() if err != nil { return fmt.Errorf("failed to get config: %w", err) } var namespaces []specs.LinuxNamespace if spec.Linux != nil { namespaces = spec.Linux.Namespaces } dnsServers, dnsSearch, dnsOptions := b.CommonBuildOpts.DNSServers, b.CommonBuildOpts.DNSSearch, b.CommonBuildOpts.DNSOptions nameservers := make([]string, 0, len(defaultConfig.Containers.DNSServers.Get())+len(dnsServers)) nameservers = append(nameservers, defaultConfig.Containers.DNSServers.Get()...) nameservers = append(nameservers, dnsServers...) searches := make([]string, 0, len(defaultConfig.Containers.DNSSearches.Get())+len(dnsSearch)) searches = append(searches, defaultConfig.Containers.DNSSearches.Get()...) searches = append(searches, dnsSearch...) options := make([]string, 0, len(defaultConfig.Containers.DNSOptions.Get())+len(dnsOptions)) options = append(options, defaultConfig.Containers.DNSOptions.Get()...) options = append(options, dnsOptions...) if len(nameservers) == 0 { nameservers = networkNameServer } if err := resolvconf.New(&resolvconf.Params{ Path: file, Namespaces: namespaces, IPv6Enabled: ipv6, KeepHostServers: keepHostServers, Nameservers: nameservers, Searches: searches, Options: options, }); err != nil { return fmt.Errorf("building resolv.conf for container %s: %w", b.ContainerID, err) } return nil } // createHostsFile creates a containers hosts file func (b *Builder) createHostsFile(rdir string, chownOpts *idtools.IDPair) (string, error) { targetfile := filepath.Join(rdir, "hosts") f, err := os.Create(targetfile) if err != nil { return "", err } defer f.Close() uid := 0 gid := 0 if chownOpts != nil { uid = chownOpts.UID gid = chownOpts.GID } if err := f.Chown(uid, gid); err != nil { return "", err } if err := relabel(targetfile, b.MountLabel, false); err != nil { return "", err } return targetfile, nil } func (b *Builder) addHostsEntries(file, imageRoot string, entries etchosts.HostEntries, exclude []net.IP, preferIP string) error { conf, err := config.Default() if err != nil { return err } base, err := etchosts.GetBaseHostFile(conf.Containers.BaseHostsFile, imageRoot) if err != nil { return err } return etchosts.New(&etchosts.Params{ BaseFile: base, ExtraHosts: b.CommonBuildOpts.AddHost, HostContainersInternalIP: etchosts.GetHostContainersInternalIP(etchosts.HostContainersInternalOptions{ Conf: conf, Exclude: exclude, PreferIP: preferIP, }), TargetFile: file, ContainerIPs: entries, }) } // generateHostname creates a containers /etc/hostname file func (b *Builder) generateHostname(rdir, hostname string, chownOpts *idtools.IDPair) (string, error) { cfile := filepath.Join(rdir, "hostname") if err := ioutils.AtomicWriteFile(cfile, append([]byte(hostname), '\n'), 0o644); err != nil { return "", fmt.Errorf("writing /etc/hostname into the container: %w", err) } uid := 0 gid := 0 if chownOpts != nil { uid = chownOpts.UID gid = chownOpts.GID } if err := os.Chown(cfile, uid, gid); err != nil { return "", err } if err := relabel(cfile, b.MountLabel, false); err != nil { return "", err } return cfile, nil } func setupTerminal(g *generate.Generator, terminalPolicy TerminalPolicy, terminalSize *specs.Box) { switch terminalPolicy { case DefaultTerminal: onTerminal := term.IsTerminal(unix.Stdin) && term.IsTerminal(unix.Stdout) && term.IsTerminal(unix.Stderr) if onTerminal { logrus.Debugf("stdio is a terminal, defaulting to using a terminal") } else { logrus.Debugf("stdio is not a terminal, defaulting to not using a terminal") } g.SetProcessTerminal(onTerminal) case WithTerminal: g.SetProcessTerminal(true) case WithoutTerminal: g.SetProcessTerminal(false) } if terminalSize != nil { g.SetProcessConsoleSize(terminalSize.Width, terminalSize.Height) } } // Search for a command that isn't given as an absolute path using the $PATH // under the rootfs. We can't resolve absolute symbolic links without // chroot()ing, which we may not be able to do, so just accept a link as a // valid resolution. func runLookupPath(g *generate.Generator, command []string) []string { // Look for the configured $PATH. spec := g.Config envPath := "" for i := range spec.Process.Env { if strings.HasPrefix(spec.Process.Env[i], "PATH=") { envPath = spec.Process.Env[i] } } // If there is no configured $PATH, supply one. if envPath == "" { defaultPath := "/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin" envPath = "PATH=" + defaultPath g.AddProcessEnv("PATH", defaultPath) } // No command, nothing to do. if len(command) == 0 { return command } // Command is already an absolute path, use it as-is. if filepath.IsAbs(command[0]) { return command } // For each element in the PATH, for _, pathEntry := range filepath.SplitList(envPath[5:]) { // if it's the empty string, it's ".", which is the Cwd, if pathEntry == "" { pathEntry = spec.Process.Cwd } // build the absolute path which it might be, candidate := filepath.Join(pathEntry, command[0]) // check if it's there, if fi, err := os.Lstat(filepath.Join(spec.Root.Path, candidate)); fi != nil && err == nil { // and if it's not a directory, and either a symlink or executable, if !fi.IsDir() && ((fi.Mode()&os.ModeSymlink != 0) || (fi.Mode()&0o111 != 0)) { // use that. return append([]string{candidate}, command[1:]...) } } } return command } func (b *Builder) configureUIDGID(g *generate.Generator, mountPoint string, options RunOptions) (string, error) { // Set the user UID/GID/supplemental group list/capabilities lists. user, homeDir, err := b.userForRun(mountPoint, options.User) if err != nil { return "", err } if err := setupCapabilities(g, b.Capabilities, options.AddCapabilities, options.DropCapabilities); err != nil { return "", err } g.SetProcessUID(user.UID) g.SetProcessGID(user.GID) g.AddProcessAdditionalGid(user.GID) for _, gid := range user.AdditionalGids { g.AddProcessAdditionalGid(gid) } for _, group := range b.GroupAdd { if group == "keep-groups" { if len(b.GroupAdd) > 1 { return "", errors.New("the '--group-add keep-groups' option is not allowed with any other --group-add options") } g.AddAnnotation("run.oci.keep_original_groups", "1") continue } gid, err := strconv.ParseUint(group, 10, 32) if err != nil { return "", err } g.AddProcessAdditionalGid(uint32(gid)) } // Remove capabilities if not running as root except Bounding set if user.UID != 0 && g.Config.Process.Capabilities != nil { bounding := g.Config.Process.Capabilities.Bounding g.ClearProcessCapabilities() g.Config.Process.Capabilities.Bounding = bounding } return homeDir, nil } func (b *Builder) configureEnvironment(g *generate.Generator, options RunOptions, defaultEnv []string) { g.ClearProcessEnv() if b.CommonBuildOpts.HTTPProxy { for _, envSpec := range config.ProxyEnv { if envVal, ok := os.LookupEnv(envSpec); ok { g.AddProcessEnv(envSpec, envVal) } } } for _, envSpec := range util.MergeEnv(util.MergeEnv(defaultEnv, b.Env()), options.Env) { env := strings.SplitN(envSpec, "=", 2) if len(env) > 1 { g.AddProcessEnv(env[0], env[1]) } } } // getNetworkInterface creates the network interface func getNetworkInterface(store storage.Store) (netTypes.ContainerNetwork, error) { conf, err := config.Default() if err != nil { return nil, err } _, netInt, err := network.NetworkBackend(store, conf, false) if err != nil { return nil, err } return netInt, nil } func netStatusToNetResult(netStatus map[string]netTypes.StatusBlock, hostnames []string) *netResult { result := &netResult{ keepHostResolvers: false, } for _, status := range netStatus { for _, dns := range status.DNSServerIPs { result.dnsServers = append(result.dnsServers, dns.String()) } for _, netInt := range status.Interfaces { for _, netAddress := range netInt.Subnets { e := etchosts.HostEntry{IP: netAddress.IPNet.IP.String(), Names: hostnames} result.entries = append(result.entries, e) if !result.ipv6 && netUtil.IsIPv6(netAddress.IPNet.IP) { result.ipv6 = true } } } } return result } // DefaultNamespaceOptions returns the default namespace settings from the // runtime-tools generator library. func DefaultNamespaceOptions() (define.NamespaceOptions, error) { cfg, err := config.Default() if err != nil { return nil, fmt.Errorf("failed to get container config: %w", err) } options := define.NamespaceOptions{ {Name: string(specs.CgroupNamespace), Host: cfg.CgroupNS() == "host"}, {Name: string(specs.IPCNamespace), Host: cfg.IPCNS() == "host"}, {Name: string(specs.MountNamespace), Host: false}, {Name: string(specs.NetworkNamespace), Host: cfg.NetNS() == "host"}, {Name: string(specs.PIDNamespace), Host: cfg.PidNS() == "host"}, {Name: string(specs.UserNamespace), Host: cfg.Containers.UserNS == "" || cfg.Containers.UserNS == "host"}, {Name: string(specs.UTSNamespace), Host: cfg.UTSNS() == "host"}, } return options, nil } func checkAndOverrideIsolationOptions(isolation define.Isolation, options *RunOptions) error { switch isolation { case IsolationOCIRootless: // only change the netns if the caller did not set it if ns := options.NamespaceOptions.Find(string(specs.NetworkNamespace)); ns == nil { if _, err := exec.LookPath("slirp4netns"); err != nil { // if slirp4netns is not installed we have to use the hosts net namespace options.NamespaceOptions.AddOrReplace(define.NamespaceOption{Name: string(specs.NetworkNamespace), Host: true}) } } fallthrough case IsolationOCI: pidns := options.NamespaceOptions.Find(string(specs.PIDNamespace)) userns := options.NamespaceOptions.Find(string(specs.UserNamespace)) if (pidns != nil && pidns.Host) && (userns != nil && !userns.Host) { return fmt.Errorf("not allowed to mix host PID namespace with container user namespace") } case IsolationChroot: logrus.Info("network namespace isolation not supported with chroot isolation, forcing host network") options.NamespaceOptions.AddOrReplace(define.NamespaceOption{Name: string(specs.NetworkNamespace), Host: true}) } return nil } // fileCloser is a helper struct to prevent closing the file twice in the code // users must call (fileCloser).Close() and not fileCloser.File.Close() type fileCloser struct { file *os.File closed bool } func (f *fileCloser) Close() { if !f.closed { if err := f.file.Close(); err != nil { logrus.Errorf("failed to close file: %v", err) } f.closed = true } } // waitForSync waits for a maximum of 4 minutes to read something from the file func waitForSync(pipeR *os.File) error { if err := pipeR.SetDeadline(time.Now().Add(4 * time.Minute)); err != nil { return err } b := make([]byte, 16) _, err := pipeR.Read(b) return err } func runUsingRuntime(options RunOptions, configureNetwork bool, moreCreateArgs []string, spec *specs.Spec, bundlePath, containerName string, containerCreateW io.WriteCloser, containerStartR io.ReadCloser, ) (wstatus unix.WaitStatus, err error) { if options.Logger == nil { options.Logger = logrus.StandardLogger() } // Lock the caller to a single OS-level thread. runtime.LockOSThread() defer reapStrays() // Set up bind mounts for things that a namespaced user might not be able to get to directly. unmountAll, err := bind.SetupIntermediateMountNamespace(spec, bundlePath) if unmountAll != nil { defer func() { if err := unmountAll(); err != nil { options.Logger.Error(err) } }() } if err != nil { return 1, err } // Write the runtime configuration. specbytes, err := json.Marshal(spec) if err != nil { return 1, fmt.Errorf("encoding configuration %#v as json: %w", spec, err) } if err = ioutils.AtomicWriteFile(filepath.Join(bundlePath, "config.json"), specbytes, 0o600); err != nil { return 1, fmt.Errorf("storing runtime configuration: %w", err) } logrus.Debugf("config = %v", string(specbytes)) runtime := options.Runtime // Default to just passing down our stdio. getCreateStdio := func() (io.ReadCloser, io.WriteCloser, io.WriteCloser) { return os.Stdin, os.Stdout, os.Stderr } // Figure out how we're doing stdio handling, and create pipes and sockets. var stdio sync.WaitGroup var consoleListener *net.UnixListener var errorFds, closeBeforeReadingErrorFds []int stdioPipe := make([][]int, 3) copyConsole := false copyPipes := false finishCopy := make([]int, 2) if err = unix.Pipe(finishCopy); err != nil { return 1, fmt.Errorf("creating pipe for notifying to stop stdio: %w", err) } finishedCopy := make(chan struct{}, 1) var pargs []string if spec.Process != nil { pargs = spec.Process.Args if spec.Process.Terminal { copyConsole = true // Create a listening socket for accepting the container's terminal's PTY master. socketPath := filepath.Join(bundlePath, "console.sock") consoleListener, err = net.ListenUnix("unix", &net.UnixAddr{Name: socketPath, Net: "unix"}) if err != nil { return 1, fmt.Errorf("creating socket %q to receive terminal descriptor: %w", consoleListener.Addr(), err) } // Add console socket arguments. moreCreateArgs = append(moreCreateArgs, "--console-socket", socketPath) } else { copyPipes = true // Figure out who should own the pipes. uid, gid, err := util.GetHostRootIDs(spec) if err != nil { return 1, err } // Create stdio pipes. if stdioPipe, err = runMakeStdioPipe(int(uid), int(gid)); err != nil { return 1, err } if spec.Linux != nil { if err = runLabelStdioPipes(stdioPipe, spec.Process.SelinuxLabel, spec.Linux.MountLabel); err != nil { return 1, err } } errorFds = []int{stdioPipe[unix.Stdout][0], stdioPipe[unix.Stderr][0]} closeBeforeReadingErrorFds = []int{stdioPipe[unix.Stdout][1], stdioPipe[unix.Stderr][1]} // Set stdio to our pipes. getCreateStdio = func() (io.ReadCloser, io.WriteCloser, io.WriteCloser) { stdin := os.NewFile(uintptr(stdioPipe[unix.Stdin][0]), "/dev/stdin") stdout := os.NewFile(uintptr(stdioPipe[unix.Stdout][1]), "/dev/stdout") stderr := os.NewFile(uintptr(stdioPipe[unix.Stderr][1]), "/dev/stderr") return stdin, stdout, stderr } } } else { if options.Quiet { // Discard stdout. getCreateStdio = func() (io.ReadCloser, io.WriteCloser, io.WriteCloser) { return os.Stdin, nil, os.Stderr } } } runtimeArgs := slices.Clone(options.Args) if options.CgroupManager == config.SystemdCgroupsManager { runtimeArgs = append(runtimeArgs, "--systemd-cgroup") } // Build the commands that we'll execute. pidFile := filepath.Join(bundlePath, "pid") args := append(append(append(runtimeArgs, "create", "--bundle", bundlePath, "--pid-file", pidFile), moreCreateArgs...), containerName) create := exec.Command(runtime, args...) setPdeathsig(create) create.Dir = bundlePath stdin, stdout, stderr := getCreateStdio() create.Stdin, create.Stdout, create.Stderr = stdin, stdout, stderr args = append(options.Args, "start", containerName) start := exec.Command(runtime, args...) setPdeathsig(start) start.Dir = bundlePath start.Stderr = os.Stderr kill := func(signal string) *exec.Cmd { args := append(options.Args, "kill", containerName) if signal != "" { args = append(args, signal) } kill := exec.Command(runtime, args...) kill.Dir = bundlePath kill.Stderr = os.Stderr return kill } args = append(options.Args, "delete", containerName) del := exec.Command(runtime, args...) del.Dir = bundlePath del.Stderr = os.Stderr // Actually create the container. logrus.Debugf("Running %q", create.Args) err = create.Run() if err != nil { return 1, fmt.Errorf("from %s creating container for %v: %s: %w", runtime, pargs, runCollectOutput(options.Logger, errorFds, closeBeforeReadingErrorFds), err) } defer func() { err2 := del.Run() if err2 != nil { if err == nil { err = fmt.Errorf("deleting container: %w", err2) } else { options.Logger.Infof("error from %s deleting container: %v", runtime, err2) } } }() // Make sure we read the container's exit status when it exits. pidValue, err := os.ReadFile(pidFile) if err != nil { return 1, err } pid, err := strconv.Atoi(strings.TrimSpace(string(pidValue))) if err != nil { return 1, fmt.Errorf("parsing pid %s as a number: %w", string(pidValue), err) } var stopped uint32 var reaping sync.WaitGroup reaping.Add(1) go func() { defer reaping.Done() var err error _, err = unix.Wait4(pid, &wstatus, 0, nil) if err != nil { wstatus = 0 options.Logger.Errorf("error waiting for container child process %d: %v\n", pid, err) } atomic.StoreUint32(&stopped, 1) }() if configureNetwork { if _, err := containerCreateW.Write([]byte{1}); err != nil { return 1, err } containerCreateW.Close() logrus.Debug("waiting for parent start message") b := make([]byte, 1) if _, err := containerStartR.Read(b); err != nil { return 1, fmt.Errorf("did not get container start message from parent: %w", err) } containerStartR.Close() } if copyPipes { // We don't need the ends of the pipes that belong to the container. stdin.Close() if stdout != nil { stdout.Close() } stderr.Close() } // Handle stdio for the container in the background. stdio.Add(1) go runCopyStdio(options.Logger, &stdio, copyPipes, stdioPipe, copyConsole, consoleListener, finishCopy, finishedCopy, spec) // Start the container. logrus.Debugf("Running %q", start.Args) err = start.Run() if err != nil { return 1, fmt.Errorf("from %s starting container: %w", runtime, err) } defer func() { if atomic.LoadUint32(&stopped) == 0 { if err := kill("").Run(); err != nil { options.Logger.Infof("error from %s stopping container: %v", runtime, err) } atomic.StoreUint32(&stopped, 1) } }() // Wait for the container to exit. interrupted := make(chan os.Signal, 100) go func() { for range interrupted { if err := kill("SIGKILL").Run(); err != nil { logrus.Errorf("%v sending SIGKILL", err) } } }() signal.Notify(interrupted, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM) for { now := time.Now() var state specs.State args = append(options.Args, "state", containerName) stat := exec.Command(runtime, args...) stat.Dir = bundlePath stat.Stderr = os.Stderr stateOutput, err := stat.Output() if err != nil { if atomic.LoadUint32(&stopped) != 0 { // container exited break } return 1, fmt.Errorf("reading container state from %s (got output: %q): %w", runtime, string(stateOutput), err) } if err = json.Unmarshal(stateOutput, &state); err != nil { return 1, fmt.Errorf("parsing container state %q from %s: %w", string(stateOutput), runtime, err) } switch state.Status { case specs.StateCreating, specs.StateCreated, specs.StateRunning: // all fine case specs.StateStopped: atomic.StoreUint32(&stopped, 1) default: return 1, fmt.Errorf("container status unexpectedly changed to %q", state.Status) } if atomic.LoadUint32(&stopped) != 0 { break } select { case <-finishedCopy: atomic.StoreUint32(&stopped, 1) case <-time.After(time.Until(now.Add(100 * time.Millisecond))): continue } if atomic.LoadUint32(&stopped) != 0 { break } } signal.Stop(interrupted) close(interrupted) // Close the writing end of the stop-handling-stdio notification pipe. unix.Close(finishCopy[1]) // Wait for the stdio copy goroutine to flush. stdio.Wait() // Wait until we finish reading the exit status. reaping.Wait() return wstatus, nil } func runCollectOutput(logger *logrus.Logger, fds, closeBeforeReadingFds []int) string { for _, fd := range closeBeforeReadingFds { unix.Close(fd) } var b bytes.Buffer buf := make([]byte, 8192) for _, fd := range fds { nread, err := unix.Read(fd, buf) if err != nil { if errno, isErrno := err.(syscall.Errno); isErrno { switch errno { default: logger.Errorf("error reading from pipe %d: %v", fd, err) case syscall.EINTR, syscall.EAGAIN: } } else { logger.Errorf("unable to wait for data from pipe %d: %v", fd, err) } continue } for nread > 0 { r := buf[:nread] if nwritten, err := b.Write(r); err != nil || nwritten != len(r) { if nwritten != len(r) { logger.Errorf("error buffering data from pipe %d: %v", fd, err) break } } nread, err = unix.Read(fd, buf) if err != nil { if errno, isErrno := err.(syscall.Errno); isErrno { switch errno { default: logger.Errorf("error reading from pipe %d: %v", fd, err) case syscall.EINTR, syscall.EAGAIN: } } else { logger.Errorf("unable to wait for data from pipe %d: %v", fd, err) } break } } } return b.String() } func setNonblock(logger *logrus.Logger, fd int, description string, nonblocking bool) (bool, error) { mask, err := unix.FcntlInt(uintptr(fd), unix.F_GETFL, 0) if err != nil { return false, err } blocked := mask&unix.O_NONBLOCK == 0 if err := unix.SetNonblock(fd, nonblocking); err != nil { if nonblocking { logger.Errorf("error setting %s to nonblocking: %v", description, err) } else { logger.Errorf("error setting descriptor %s blocking: %v", description, err) } } return blocked, err } func runCopyStdio(logger *logrus.Logger, stdio *sync.WaitGroup, copyPipes bool, stdioPipe [][]int, copyConsole bool, consoleListener *net.UnixListener, finishCopy []int, finishedCopy chan struct{}, spec *specs.Spec) { defer func() { unix.Close(finishCopy[0]) if copyPipes { unix.Close(stdioPipe[unix.Stdin][1]) unix.Close(stdioPipe[unix.Stdout][0]) unix.Close(stdioPipe[unix.Stderr][0]) } stdio.Done() finishedCopy <- struct{}{} close(finishedCopy) }() // Map describing where data on an incoming descriptor should go. relayMap := make(map[int]int) // Map describing incoming and outgoing descriptors. readDesc := make(map[int]string) writeDesc := make(map[int]string) // Buffers. relayBuffer := make(map[int]*bytes.Buffer) // Set up the terminal descriptor or pipes for polling. if copyConsole { // Accept a connection over our listening socket. fd, err := runAcceptTerminal(logger, consoleListener, spec.Process.ConsoleSize) if err != nil { logger.Errorf("%v", err) return } terminalFD := fd // Input from our stdin, output from the terminal descriptor. relayMap[unix.Stdin] = terminalFD readDesc[unix.Stdin] = "stdin" relayBuffer[terminalFD] = new(bytes.Buffer) writeDesc[terminalFD] = "container terminal input" relayMap[terminalFD] = unix.Stdout readDesc[terminalFD] = "container terminal output" relayBuffer[unix.Stdout] = new(bytes.Buffer) writeDesc[unix.Stdout] = "output" // Set our terminal's mode to raw, to pass handling of special // terminal input to the terminal in the container. if term.IsTerminal(unix.Stdin) { if state, err := term.MakeRaw(unix.Stdin); err != nil { logger.Warnf("error setting terminal state: %v", err) } else { defer func() { if err = term.Restore(unix.Stdin, state); err != nil { logger.Errorf("unable to restore terminal state: %v", err) } }() } } } if copyPipes { // Input from our stdin, output from the stdout and stderr pipes. relayMap[unix.Stdin] = stdioPipe[unix.Stdin][1] readDesc[unix.Stdin] = "stdin" relayBuffer[stdioPipe[unix.Stdin][1]] = new(bytes.Buffer) writeDesc[stdioPipe[unix.Stdin][1]] = "container stdin" relayMap[stdioPipe[unix.Stdout][0]] = unix.Stdout readDesc[stdioPipe[unix.Stdout][0]] = "container stdout" relayBuffer[unix.Stdout] = new(bytes.Buffer) writeDesc[unix.Stdout] = "stdout" relayMap[stdioPipe[unix.Stderr][0]] = unix.Stderr readDesc[stdioPipe[unix.Stderr][0]] = "container stderr" relayBuffer[unix.Stderr] = new(bytes.Buffer) writeDesc[unix.Stderr] = "stderr" } // Set our reading descriptors to non-blocking. for rfd, wfd := range relayMap { blocked, err := setNonblock(logger, rfd, readDesc[rfd], true) if err != nil { return } if blocked { defer setNonblock(logger, rfd, readDesc[rfd], false) //nolint:errcheck } setNonblock(logger, wfd, writeDesc[wfd], false) //nolint:errcheck } if copyPipes { setNonblock(logger, stdioPipe[unix.Stdin][1], writeDesc[stdioPipe[unix.Stdin][1]], true) //nolint:errcheck } runCopyStdioPassData(copyPipes, stdioPipe, finishCopy, relayMap, relayBuffer, readDesc, writeDesc) } func canRetry(err error) bool { if errno, isErrno := err.(syscall.Errno); isErrno { return errno == syscall.EINTR || errno == syscall.EAGAIN } return false } func runCopyStdioPassData(copyPipes bool, stdioPipe [][]int, finishCopy []int, relayMap map[int]int, relayBuffer map[int]*bytes.Buffer, readDesc map[int]string, writeDesc map[int]string) { closeStdin := false // Pass data back and forth. pollTimeout := -1 for len(relayMap) > 0 { // Start building the list of descriptors to poll. pollFds := make([]unix.PollFd, 0, len(relayMap)+1) // Poll for a notification that we should stop handling stdio. pollFds = append(pollFds, unix.PollFd{Fd: int32(finishCopy[0]), Events: unix.POLLIN | unix.POLLHUP}) // Poll on our reading descriptors. for rfd := range relayMap { pollFds = append(pollFds, unix.PollFd{Fd: int32(rfd), Events: unix.POLLIN | unix.POLLHUP}) } buf := make([]byte, 8192) // Wait for new data from any input descriptor, or a notification that we're done. _, err := unix.Poll(pollFds, pollTimeout) if !util.LogIfNotRetryable(err, fmt.Sprintf("error waiting for stdio/terminal data to relay: %v", err)) { return } removes := make(map[int]struct{}) for _, pollFd := range pollFds { // If this descriptor's just been closed from the other end, mark it for // removal from the set that we're checking for. if pollFd.Revents&unix.POLLHUP == unix.POLLHUP { removes[int(pollFd.Fd)] = struct{}{} } // If the descriptor was closed elsewhere, remove it from our list. if pollFd.Revents&unix.POLLNVAL != 0 { logrus.Debugf("error polling descriptor %s: closed?", readDesc[int(pollFd.Fd)]) removes[int(pollFd.Fd)] = struct{}{} } // If the POLLIN flag isn't set, then there's no data to be read from this descriptor. if pollFd.Revents&unix.POLLIN == 0 { continue } // Read whatever there is to be read. readFD := int(pollFd.Fd) writeFD, needToRelay := relayMap[readFD] if needToRelay { n, err := unix.Read(readFD, buf) if !util.LogIfNotRetryable(err, fmt.Sprintf("unable to read %s data: %v", readDesc[readFD], err)) { return } // If it's zero-length on our stdin and we're // using pipes, it's an EOF, so close the stdin // pipe's writing end. if n == 0 && !canRetry(err) && int(pollFd.Fd) == unix.Stdin { removes[int(pollFd.Fd)] = struct{}{} } else if n > 0 { // Buffer the data in case we get blocked on where they need to go. nwritten, err := relayBuffer[writeFD].Write(buf[:n]) if err != nil { logrus.Debugf("buffer: %v", err) continue } if nwritten != n { logrus.Debugf("buffer: expected to buffer %d bytes, wrote %d", n, nwritten) continue } // If this is the last of the data we'll be able to read from this // descriptor, read all that there is to read. for pollFd.Revents&unix.POLLHUP == unix.POLLHUP { nr, err := unix.Read(readFD, buf) util.LogIfUnexpectedWhileDraining(err, fmt.Sprintf("read %s: %v", readDesc[readFD], err)) if nr <= 0 { break } nwritten, err := relayBuffer[writeFD].Write(buf[:nr]) if err != nil { logrus.Debugf("buffer: %v", err) break } if nwritten != nr { logrus.Debugf("buffer: expected to buffer %d bytes, wrote %d", nr, nwritten) break } } } } } // Try to drain the output buffers. Set the default timeout // for the next poll() to 100ms if we still have data to write. pollTimeout = -1 for writeFD := range relayBuffer { if relayBuffer[writeFD].Len() > 0 { n, err := unix.Write(writeFD, relayBuffer[writeFD].Bytes()) if !util.LogIfNotRetryable(err, fmt.Sprintf("unable to write %s data: %v", writeDesc[writeFD], err)) { return } if n > 0 { relayBuffer[writeFD].Next(n) } if closeStdin && writeFD == stdioPipe[unix.Stdin][1] && stdioPipe[unix.Stdin][1] >= 0 && relayBuffer[stdioPipe[unix.Stdin][1]].Len() == 0 { logrus.Debugf("closing stdin") unix.Close(stdioPipe[unix.Stdin][1]) stdioPipe[unix.Stdin][1] = -1 } } if relayBuffer[writeFD].Len() > 0 { pollTimeout = 100 } } // Remove any descriptors which we don't need to poll any more from the poll descriptor list. for remove := range removes { if copyPipes && remove == unix.Stdin { closeStdin = true if relayBuffer[stdioPipe[unix.Stdin][1]].Len() == 0 { logrus.Debugf("closing stdin") unix.Close(stdioPipe[unix.Stdin][1]) stdioPipe[unix.Stdin][1] = -1 } } delete(relayMap, remove) } // If the we-can-return pipe had anything for us, we're done. for _, pollFd := range pollFds { if int(pollFd.Fd) == finishCopy[0] && pollFd.Revents != 0 { // The pipe is closed, indicating that we can stop now. return } } } } func runAcceptTerminal(logger *logrus.Logger, consoleListener *net.UnixListener, terminalSize *specs.Box) (int, error) { defer consoleListener.Close() c, err := consoleListener.AcceptUnix() if err != nil { return -1, fmt.Errorf("accepting socket descriptor connection: %w", err) } defer c.Close() // Expect a control message over our new connection. b := make([]byte, 8192) oob := make([]byte, 8192) n, oobn, _, _, err := c.ReadMsgUnix(b, oob) if err != nil { return -1, fmt.Errorf("reading socket descriptor: %w", err) } if n > 0 { logrus.Debugf("socket descriptor is for %q", string(b[:n])) } if oobn > len(oob) { return -1, fmt.Errorf("too much out-of-bounds data (%d bytes)", oobn) } // Parse the control message. scm, err := unix.ParseSocketControlMessage(oob[:oobn]) if err != nil { return -1, fmt.Errorf("parsing out-of-bound data as a socket control message: %w", err) } logrus.Debugf("control messages: %v", scm) // Expect to get a descriptor. terminalFD := -1 for i := range scm { fds, err := unix.ParseUnixRights(&scm[i]) if err != nil { return -1, fmt.Errorf("parsing unix rights control message: %v: %w", &scm[i], err) } logrus.Debugf("fds: %v", fds) if len(fds) == 0 { continue } terminalFD = fds[0] break } if terminalFD == -1 { return -1, fmt.Errorf("unable to read terminal descriptor") } // Set the pseudoterminal's size to the configured size, or our own. winsize := &unix.Winsize{} if terminalSize != nil { // Use configured sizes. winsize.Row = uint16(terminalSize.Height) winsize.Col = uint16(terminalSize.Width) } else { if term.IsTerminal(unix.Stdin) { // Use the size of our terminal. if winsize, err = unix.IoctlGetWinsize(unix.Stdin, unix.TIOCGWINSZ); err != nil { logger.Warnf("error reading size of controlling terminal: %v", err) winsize.Row = 0 winsize.Col = 0 } } } if winsize.Row != 0 && winsize.Col != 0 { if err = unix.IoctlSetWinsize(terminalFD, unix.TIOCSWINSZ, winsize); err != nil { logger.Warnf("error setting size of container pseudoterminal: %v", err) } // FIXME - if we're connected to a terminal, we should // be passing the updated terminal size down when we // receive a SIGWINCH. } return terminalFD, nil } func reapStrays() { // Reap the exit status of anything that was reparented to us, not that // we care about their exit status. logrus.Debugf("checking for reparented child processes") for range 100 { wpid, err := unix.Wait4(-1, nil, unix.WNOHANG, nil) if err != nil { break } if wpid == 0 { time.Sleep(100 * time.Millisecond) } else { logrus.Debugf("caught reparented child process %d", wpid) } } } func runUsingRuntimeMain() { var options runUsingRuntimeSubprocOptions // Set logging. if level := os.Getenv("LOGLEVEL"); level != "" { if ll, err := strconv.Atoi(level); err == nil { logrus.SetLevel(logrus.Level(ll)) } } // Unpack our configuration. confPipe := os.NewFile(3, "confpipe") if confPipe == nil { fmt.Fprintf(os.Stderr, "error reading options pipe\n") os.Exit(1) } defer confPipe.Close() if err := json.NewDecoder(confPipe).Decode(&options); err != nil { fmt.Fprintf(os.Stderr, "error decoding options: %v\n", err) os.Exit(1) } // Set ourselves up to read the container's exit status. We're doing this in a child process // so that we won't mess with the setting in a caller of the library. if err := setChildProcess(); err != nil { os.Exit(1) } ospec := options.Spec if ospec == nil { fmt.Fprintf(os.Stderr, "options spec not specified\n") os.Exit(1) } // open the pipes used to communicate with the parent process var containerCreateW *os.File var containerStartR *os.File if options.ConfigureNetwork { containerCreateW = os.NewFile(4, "containercreatepipe") if containerCreateW == nil { fmt.Fprintf(os.Stderr, "could not open fd 4\n") os.Exit(1) } containerStartR = os.NewFile(5, "containerstartpipe") if containerStartR == nil { fmt.Fprintf(os.Stderr, "could not open fd 5\n") os.Exit(1) } } // Run the container, start to finish. status, err := runUsingRuntime(options.Options, options.ConfigureNetwork, options.MoreCreateArgs, ospec, options.BundlePath, options.ContainerName, containerCreateW, containerStartR) reapStrays() if err != nil { fmt.Fprintf(os.Stderr, "error running container: %v\n", err) os.Exit(1) } // Pass the container's exit status back to the caller by exiting with the same status. if status.Exited() { os.Exit(status.ExitStatus()) } else if status.Signaled() { fmt.Fprintf(os.Stderr, "container exited on %s\n", status.Signal()) os.Exit(1) } os.Exit(1) } func (b *Builder) runUsingRuntimeSubproc(isolation define.Isolation, options RunOptions, configureNetwork bool, networkString string, moreCreateArgs []string, spec *specs.Spec, rootPath, bundlePath, containerName, buildContainerName, hostsFile, resolvFile string, ) (err error) { // Decide which runtime to use in case it was empty. ociRuntime := options.Runtime if ociRuntime == "" { ociRuntime = util.Runtime() } localRuntime := util.FindLocalRuntime(ociRuntime) if localRuntime != "" { ociRuntime = localRuntime } options.Runtime = ociRuntime // Lock the caller to a single OS-level thread. runtime.LockOSThread() defer runtime.UnlockOSThread() var confwg sync.WaitGroup config, conferr := json.Marshal(runUsingRuntimeSubprocOptions{ Options: options, Spec: spec, RootPath: rootPath, BundlePath: bundlePath, ConfigureNetwork: configureNetwork, MoreCreateArgs: moreCreateArgs, ContainerName: containerName, Isolation: isolation, }) if conferr != nil { return fmt.Errorf("encoding configuration for %q: %w", runUsingRuntimeCommand, conferr) } cmd := reexec.Command(runUsingRuntimeCommand) setPdeathsig(cmd) cmd.Dir = bundlePath cmd.Stdin = options.Stdin if cmd.Stdin == nil { cmd.Stdin = os.Stdin } cmd.Stdout = options.Stdout if cmd.Stdout == nil { cmd.Stdout = os.Stdout } cmd.Stderr = options.Stderr if cmd.Stderr == nil { cmd.Stderr = os.Stderr } cmd.Env = util.MergeEnv(os.Environ(), []string{fmt.Sprintf("LOGLEVEL=%d", logrus.GetLevel())}) preader, pwriter, err := os.Pipe() if err != nil { return fmt.Errorf("creating configuration pipe: %w", err) } confwg.Add(1) go func() { _, conferr = io.Copy(pwriter, bytes.NewReader(config)) if conferr != nil { conferr = fmt.Errorf("while copying configuration down pipe to child process: %w", conferr) } confwg.Done() }() // create network configuration pipes var containerCreateR, containerCreateW fileCloser var containerStartR, containerStartW fileCloser if configureNetwork { containerCreateR.file, containerCreateW.file, err = os.Pipe() if err != nil { return fmt.Errorf("creating container create pipe: %w", err) } defer containerCreateR.Close() defer containerCreateW.Close() containerStartR.file, containerStartW.file, err = os.Pipe() if err != nil { return fmt.Errorf("creating container start pipe: %w", err) } defer containerStartR.Close() defer containerStartW.Close() cmd.ExtraFiles = []*os.File{containerCreateW.file, containerStartR.file} } cmd.ExtraFiles = append([]*os.File{preader}, cmd.ExtraFiles...) defer preader.Close() defer pwriter.Close() if err := cmd.Start(); err != nil { return fmt.Errorf("while starting runtime: %w", err) } interrupted := make(chan os.Signal, 100) go func() { for receivedSignal := range interrupted { if err := cmd.Process.Signal(receivedSignal); err != nil { logrus.Infof("%v while attempting to forward %v to child process", err, receivedSignal) } } }() signal.Notify(interrupted, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM) if configureNetwork { // we already passed the fd to the child, now close the writer so we do not hang if the child closes it containerCreateW.Close() if err := waitForSync(containerCreateR.file); err != nil { // we do not want to return here since we want to capture the exit code from the child via cmd.Wait() // close the pipes here so that the child will not hang forever containerCreateR.Close() containerStartW.Close() logrus.Errorf("did not get container create message from subprocess: %v", err) } else { pidFile := filepath.Join(bundlePath, "pid") pidValue, err := os.ReadFile(pidFile) if err != nil { return err } pid, err := strconv.Atoi(strings.TrimSpace(string(pidValue))) if err != nil { return fmt.Errorf("parsing pid %s as a number: %w", string(pidValue), err) } teardown, netResult, err := b.runConfigureNetwork(pid, isolation, options, networkString, containerName, []string{spec.Hostname, buildContainerName}) if teardown != nil { defer teardown() } if err != nil { return fmt.Errorf("setup network: %w", err) } // only add hosts if we manage the hosts file if hostsFile != "" { err = b.addHostsEntries(hostsFile, rootPath, netResult.entries, netResult.excludeIPs, netResult.preferredHostContainersInternalIP) if err != nil { return err } } if resolvFile != "" { err = b.addResolvConfEntries(resolvFile, netResult.dnsServers, spec, netResult.keepHostResolvers, netResult.ipv6) if err != nil { return err } } logrus.Debug("network namespace successfully setup, send start message to child") _, err = containerStartW.file.Write([]byte{1}) if err != nil { return err } } } if err := cmd.Wait(); err != nil { return fmt.Errorf("while running runtime: %w", err) } confwg.Wait() signal.Stop(interrupted) close(interrupted) if err == nil { return conferr } if conferr != nil { logrus.Debugf("%v", conferr) } return err } type runUsingRuntimeSubprocOptions struct { Options RunOptions Spec *specs.Spec RootPath string BundlePath string ConfigureNetwork bool MoreCreateArgs []string ContainerName string Isolation define.Isolation } func init() { reexec.Register(runUsingRuntimeCommand, runUsingRuntimeMain) } // If this succeeds, after the command which uses the spec finishes running, // the caller must call b.cleanupRunMounts() on the returned runMountArtifacts // structure. func (b *Builder) setupMounts(mountPoint string, spec *specs.Spec, bundlePath string, optionMounts []specs.Mount, bindFiles map[string]string, builtinVolumes []string, compatBuiltinVolumes types.OptionalBool, volumeMounts []string, runFileMounts []string, runMountInfo runMountInfo) (*runMountArtifacts, error) { // Start building a new list of mounts. var mounts []specs.Mount haveMount := func(destination string) bool { for _, mount := range mounts { if mount.Destination == destination { // Already have something to mount there. return true } } return false } specMounts, err := setupSpecialMountSpecChanges(spec, b.CommonBuildOpts.ShmSize) if err != nil { return nil, err } // Get the list of files we need to bind into the container. bindFileMounts := runSetupBoundFiles(bundlePath, bindFiles) // After this point we need to know the per-container persistent storage directory. cdir, err := b.store.ContainerDirectory(b.ContainerID) if err != nil { return nil, fmt.Errorf("determining work directory for container %q: %w", b.ContainerID, err) } // Figure out which UID and GID to tell the subscriptions package to use // for files that it creates. rootUID, rootGID, err := util.GetHostRootIDs(spec) if err != nil { return nil, err } // Get host UID and GID of the container process. uidMap := []specs.LinuxIDMapping{} gidMap := []specs.LinuxIDMapping{} if spec.Linux != nil { uidMap = spec.Linux.UIDMappings gidMap = spec.Linux.GIDMappings } processUID, processGID, err := util.GetHostIDs(uidMap, gidMap, spec.Process.User.UID, spec.Process.User.GID) if err != nil { return nil, err } // Get the list of subscriptions mounts. subscriptionMounts := subscriptions.MountsWithUIDGID(b.MountLabel, cdir, b.DefaultMountsFilePath, mountPoint, int(rootUID), int(rootGID), unshare.IsRootless(), false) idMaps := IDMaps{ uidmap: uidMap, gidmap: gidMap, rootUID: int(rootUID), rootGID: int(rootGID), processUID: int(processUID), processGID: int(processGID), } // Get the list of mounts that are just for this Run() call. runMounts, mountArtifacts, err := b.runSetupRunMounts(bundlePath, runFileMounts, runMountInfo, idMaps) if err != nil { return nil, err } succeeded := false defer func() { if !succeeded { if err := b.cleanupRunMounts(mountArtifacts); err != nil { b.Logger.Debugf("cleaning up run mounts: %v", err) } } }() // Add temporary copies of the contents of volume locations at the // volume locations, unless we already have something there. builtins, err := runSetupBuiltinVolumes(b.MountLabel, mountPoint, cdir, builtinVolumes, compatBuiltinVolumes, int(rootUID), int(rootGID)) if err != nil { return nil, err } // Get the list of explicitly-specified volume mounts. mountLabel := "" if spec.Linux != nil { mountLabel = spec.Linux.MountLabel } volumes, overlayDirs, err := b.runSetupVolumeMounts(mountLabel, volumeMounts, optionMounts, idMaps) if err != nil { return nil, err } mountArtifacts.RunOverlayDirs = append(mountArtifacts.RunOverlayDirs, overlayDirs...) allMounts := util.SortMounts(slices.Concat(volumes, builtins, runMounts, subscriptionMounts, bindFileMounts, specMounts)) // Add them all, in the preferred order, except where they conflict with something that was previously added. for _, mount := range allMounts { if haveMount(mount.Destination) { // Already mounting something there, no need to bother with this one. continue } // Add the mount. mounts = append(mounts, mount) } // Some mounts require env vars to be set, do these here spec.Process.Env = append(spec.Process.Env, mountArtifacts.EnvVars...) // Set the list in the spec. spec.Mounts = mounts succeeded = true return mountArtifacts, nil } func runSetupBuiltinVolumes(mountLabel, mountPoint, containerDir string, builtinVolumes []string, compatBuiltinVolumes types.OptionalBool, rootUID, rootGID int) ([]specs.Mount, error) { var mounts []specs.Mount hostOwner := idtools.IDPair{UID: rootUID, GID: rootGID} // Add temporary copies of the contents of volume locations at the // volume locations, unless we already have something there. for _, volume := range builtinVolumes { // Make sure the volume exists in the rootfs. createDirPerms := os.FileMode(0o755) err := copier.Mkdir(mountPoint, filepath.Join(mountPoint, volume), copier.MkdirOptions{ ChownNew: &hostOwner, ChmodNew: &createDirPerms, }) if err != nil { return nil, fmt.Errorf("ensuring volume path %q: %w", filepath.Join(mountPoint, volume), err) } // If we're not being asked to bind mount anonymous volumes // onto the volume paths, we're done here. if compatBuiltinVolumes != types.OptionalBoolTrue { continue } // If we need to, create the directory that we'll use to hold // the volume contents. If we do need to create it, then we'll // need to populate it, too, so make a note of that. volumePath := filepath.Join(containerDir, "buildah-volumes", digest.Canonical.FromString(volume).Hex()) initializeVolume := false if err := fileutils.Exists(volumePath); err != nil { if !errors.Is(err, fs.ErrNotExist) { return nil, err } logrus.Debugf("setting up built-in volume path at %q for %q", volumePath, volume) if err = os.MkdirAll(volumePath, 0o755); err != nil { return nil, err } if err = relabel(volumePath, mountLabel, false); err != nil { return nil, err } initializeVolume = true } // Read the attributes of the volume's location in the rootfs. srcPath, err := copier.Eval(mountPoint, filepath.Join(mountPoint, volume), copier.EvalOptions{}) if err != nil { return nil, fmt.Errorf("evaluating path %q: %w", srcPath, err) } stat, err := os.Stat(srcPath) if err != nil && !errors.Is(err, os.ErrNotExist) { return nil, err } // If we need to populate the mounted volume's contents with // content from the rootfs, set it up now. if initializeVolume { if err = os.Chmod(volumePath, stat.Mode().Perm()); err != nil { return nil, err } if err = os.Chown(volumePath, int(stat.Sys().(*syscall.Stat_t).Uid), int(stat.Sys().(*syscall.Stat_t).Gid)); err != nil { return nil, err } logrus.Debugf("populating directory %q for volume %q using contents of %q", volumePath, volume, srcPath) if err = extractWithTar(mountPoint, srcPath, volumePath); err != nil && !errors.Is(err, os.ErrNotExist) { return nil, fmt.Errorf("populating directory %q for volume %q using contents of %q: %w", volumePath, volume, srcPath, err) } } // Add the bind mount. mounts = append(mounts, specs.Mount{ Source: volumePath, Destination: volume, Type: define.TypeBind, Options: define.BindOptions, }) } return mounts, nil } // runSetupRunMounts sets up mounts that exist only in this RUN, not in subsequent runs // // If this function succeeds, the caller must free the returned // runMountArtifacts by calling b.cleanupRunMounts() after the command being // executed with those mounts has finished. func (b *Builder) runSetupRunMounts(bundlePath string, mounts []string, sources runMountInfo, idMaps IDMaps) ([]specs.Mount, *runMountArtifacts, error) { tmpFiles := make([]string, 0, len(mounts)) mountImages := make([]string, 0, len(mounts)) intermediateMounts := make([]string, 0, len(mounts)) finalMounts := make([]specs.Mount, 0, len(mounts)) agents := make([]*sshagent.AgentServer, 0, len(mounts)) var envVars []string targetLocks := []*lockfile.LockFile{} var overlayDirs []string succeeded := false defer func() { if !succeeded { for _, agent := range agents { servePath := agent.ServePath() if err := agent.Shutdown(); err != nil { b.Logger.Errorf("shutting down SSH agent at %q: %v", servePath, err) } } for _, overlayDir := range overlayDirs { if err := overlay.RemoveTemp(overlayDir); err != nil { b.Logger.Error(err.Error()) } } for _, intermediateMount := range intermediateMounts { if err := mount.Unmount(intermediateMount); err != nil { b.Logger.Errorf("unmounting %q: %v", intermediateMount, err) } if err := os.Remove(intermediateMount); err != nil { b.Logger.Errorf("removing should-be-empty directory %q: %v", intermediateMount, err) } } for _, mountImage := range mountImages { if _, err := b.store.UnmountImage(mountImage, false); err != nil { b.Logger.Error(err.Error()) } } for _, tmpFile := range tmpFiles { if err := os.Remove(tmpFile); err != nil && !errors.Is(err, os.ErrNotExist) { b.Logger.Error(err.Error()) } } volumes.UnlockLockArray(targetLocks) } }() for _, mount := range mounts { var bundleMountsDir string tokens := strings.Split(mount, ",") // If `type` is not set default to TypeBind mountType := define.TypeBind for _, field := range tokens { if strings.HasPrefix(field, "type=") { kv := strings.Split(field, "=") if len(kv) != 2 { return nil, nil, errors.New("invalid mount type") } mountType = kv[1] } } switch mountType { case "secret": mountOrEnvSpec, err := b.getSecretMount(tokens, sources.Secrets, idMaps, sources.WorkDir) if err != nil { return nil, nil, err } if mountOrEnvSpec.Mount != nil { finalMounts = append(finalMounts, *mountOrEnvSpec.Mount) } if mountOrEnvSpec.EnvFile != "" { tmpFiles = append(tmpFiles, mountOrEnvSpec.EnvFile) } if mountOrEnvSpec.EnvVariable != "" { envVars = append(envVars, mountOrEnvSpec.EnvVariable) } case "ssh": mountSpec, agent, err := b.getSSHMount(tokens, len(agents), sources.SSHSources, idMaps) if err != nil { return nil, nil, err } if mountSpec != nil { finalMounts = append(finalMounts, *mountSpec) if len(agents) == 0 { envVars = append(envVars, "SSH_AUTH_SOCK="+mountSpec.Destination) } agents = append(agents, agent) } case define.TypeBind: if bundleMountsDir == "" { var err error if bundleMountsDir, err = os.MkdirTemp(bundlePath, "mounts"); err != nil { return nil, nil, err } } mountSpec, image, intermediateMount, overlayDir, err := b.getBindMount(tokens, sources.SystemContext, sources.ContextDir, sources.StageMountPoints, idMaps, sources.WorkDir, bundleMountsDir) if err != nil { return nil, nil, err } if image != "" { mountImages = append(mountImages, image) } if intermediateMount != "" { intermediateMounts = append(intermediateMounts, intermediateMount) } if overlayDir != "" { overlayDirs = append(overlayDirs, overlayDir) } finalMounts = append(finalMounts, *mountSpec) case "tmpfs": mountSpec, err := b.getTmpfsMount(tokens, idMaps, sources.WorkDir) if err != nil { return nil, nil, err } finalMounts = append(finalMounts, *mountSpec) case "cache": if bundleMountsDir == "" { var err error if bundleMountsDir, err = os.MkdirTemp(bundlePath, "mounts"); err != nil { return nil, nil, err } } mountSpec, image, intermediateMount, overlayDir, tl, err := b.getCacheMount(tokens, sources.SystemContext, sources.StageMountPoints, idMaps, sources.WorkDir, bundleMountsDir) if err != nil { return nil, nil, err } if image != "" { mountImages = append(mountImages, image) } if intermediateMount != "" { intermediateMounts = append(intermediateMounts, intermediateMount) } if overlayDir != "" { overlayDirs = append(overlayDirs, overlayDir) } if tl != nil { targetLocks = append(targetLocks, tl) } finalMounts = append(finalMounts, *mountSpec) default: return nil, nil, fmt.Errorf("invalid mount type %q", mountType) } } succeeded = true artifacts := &runMountArtifacts{ RunOverlayDirs: overlayDirs, Agents: agents, MountedImages: mountImages, TargetLocks: targetLocks, IntermediateMounts: intermediateMounts, EnvVars: envVars, } return finalMounts, artifacts, nil } func (b *Builder) getBindMount(tokens []string, sys *types.SystemContext, contextDir string, stageMountPoints map[string]internal.StageMountDetails, idMaps IDMaps, workDir, tmpDir string) (*specs.Mount, string, string, string, error) { if contextDir == "" { return nil, "", "", "", errors.New("context directory for current run invocation is not configured") } var optionMounts []specs.Mount optionMount, image, intermediateMount, overlayMount, err := volumes.GetBindMount(sys, tokens, contextDir, b.store, b.MountLabel, stageMountPoints, workDir, tmpDir) if err != nil { return nil, "", "", "", err } succeeded := false defer func() { if !succeeded { if overlayMount != "" { if err := overlay.RemoveTemp(overlayMount); err != nil { b.Logger.Debug(err.Error()) } } if intermediateMount != "" { if err := mount.Unmount(intermediateMount); err != nil { b.Logger.Debugf("unmounting %q: %v", intermediateMount, err) } if err := os.Remove(intermediateMount); err != nil { b.Logger.Debugf("removing should-be-empty directory %q: %v", intermediateMount, err) } } if image != "" { if _, err := b.store.UnmountImage(image, false); err != nil { b.Logger.Debugf("unmounting image %q: %v", image, err) } } } }() optionMounts = append(optionMounts, optionMount) volumes, overlayDirs, err := b.runSetupVolumeMounts(b.MountLabel, nil, optionMounts, idMaps) if err != nil { return nil, "", "", "", err } if len(overlayDirs) != 0 { return nil, "", "", "", errors.New("internal error: did not expect a resolved bind mount to use the O flag") } succeeded = true return &volumes[0], image, intermediateMount, overlayMount, nil } func (b *Builder) getTmpfsMount(tokens []string, idMaps IDMaps, workDir string) (*specs.Mount, error) { var optionMounts []specs.Mount mount, err := volumes.GetTmpfsMount(tokens, workDir) if err != nil { return nil, err } optionMounts = append(optionMounts, mount) volumes, overlayDirs, err := b.runSetupVolumeMounts(b.MountLabel, nil, optionMounts, idMaps) if err != nil { return nil, err } if len(overlayDirs) != 0 { return nil, errors.New("internal error: did not expect a resolved tmpfs mount to use the O flag") } return &volumes[0], nil } type secretMountOrEnv struct { // set if mount created Mount *specs.Mount // set if caller mount created from temp created env file EnvFile string // set if caller should add to env variable list EnvVariable string } func (b *Builder) getSecretMount(tokens []string, secrets map[string]define.Secret, idMaps IDMaps, workdir string) (_ secretMountOrEnv, retErr error) { errInvalidSyntax := errors.New("secret should have syntax id=id[,target=path,required=bool,mode=uint,uid=uint,gid=uint,env=dstVarName") if len(tokens) == 0 { return secretMountOrEnv{}, errInvalidSyntax } var id, target, env string var required bool var uid, gid uint32 var mode uint32 = 0o400 var rv secretMountOrEnv for _, val := range tokens { kv := strings.SplitN(val, "=", 2) switch kv[0] { case "type": // This is already processed continue case "id": id = kv[1] case "target", "dst", "destination": target = kv[1] if !filepath.IsAbs(target) { target = filepath.Join(workdir, target) } case "required": required = true if len(kv) > 1 { var err error required, err = strconv.ParseBool(kv[1]) if err != nil { return secretMountOrEnv{}, errInvalidSyntax } } case "mode": mode64, err := strconv.ParseUint(kv[1], 8, 32) if err != nil { return secretMountOrEnv{}, errInvalidSyntax } mode = uint32(mode64) case "uid": uid64, err := strconv.ParseUint(kv[1], 10, 32) if err != nil { return secretMountOrEnv{}, errInvalidSyntax } uid = uint32(uid64) case "gid": gid64, err := strconv.ParseUint(kv[1], 10, 32) if err != nil { return secretMountOrEnv{}, errInvalidSyntax } gid = uint32(gid64) case "env": if kv[1] == "" { return secretMountOrEnv{}, errInvalidSyntax } env = kv[1] default: return secretMountOrEnv{}, errInvalidSyntax } } // apply defaults, matching documented behaviour if target == "" { if env == "" { target = "/run/secrets/" + id } } else { if id == "" { id = filepath.Base(target) } } if id == "" { return secretMountOrEnv{}, errInvalidSyntax } // first fetch the secret data secr, ok := secrets[id] if !ok { if required { return secretMountOrEnv{}, fmt.Errorf("secret required but no secret with id %q found", id) } return rv, nil } data, err := secr.ResolveValue() if err != nil { return secretMountOrEnv{}, err } // if env is set, then we set that if env != "" { rv.EnvVariable = env + "=" + string(data) } // if no target needs to be mounted, then return now, we're done if target == "" { return rv, nil } var ctrFileOnHost string switch secr.SourceType { case "env": tmpFile, err := os.CreateTemp(tmpdir.GetTempDir(), "buildah*") if err != nil { return secretMountOrEnv{}, err } tmpFile.Close() defer func() { if retErr != nil { os.Remove(tmpFile.Name()) } }() rv.EnvFile = tmpFile.Name() ctrFileOnHost = tmpFile.Name() case "file": containerWorkingDir, err := b.store.ContainerDirectory(b.ContainerID) if err != nil { return secretMountOrEnv{}, err } ctrFileOnHost = filepath.Join(containerWorkingDir, "secrets", digest.FromString(id).Encoded()[:16]) default: return secretMountOrEnv{}, errors.New("invalid source secret type") } // Copy secrets to container working dir (or tmp dir if it's an env), since we need to chmod, // chown and relabel it for the container user and we don't want to mess with the original file if err := os.MkdirAll(filepath.Dir(ctrFileOnHost), 0o755); err != nil { return secretMountOrEnv{}, err } if err := os.WriteFile(ctrFileOnHost, data, 0o644); err != nil { return secretMountOrEnv{}, err } if err := relabel(ctrFileOnHost, b.MountLabel, false); err != nil { return secretMountOrEnv{}, err } hostUID, hostGID, err := util.GetHostIDs(idMaps.uidmap, idMaps.gidmap, uid, gid) if err != nil { return secretMountOrEnv{}, err } if err := os.Lchown(ctrFileOnHost, int(hostUID), int(hostGID)); err != nil { return secretMountOrEnv{}, err } if err := os.Chmod(ctrFileOnHost, os.FileMode(mode)); err != nil { return secretMountOrEnv{}, err } rv.Mount = &specs.Mount{ Destination: target, Type: define.TypeBind, Source: ctrFileOnHost, Options: append(define.BindOptions, "rprivate", "ro"), } return rv, nil } // getSSHMount parses the --mount type=ssh flag in the Containerfile, checks if there's an ssh source provided, and creates and starts an ssh-agent to be forwarded into the container func (b *Builder) getSSHMount(tokens []string, count int, sshsources map[string]*sshagent.Source, idMaps IDMaps) (*specs.Mount, *sshagent.AgentServer, error) { errInvalidSyntax := errors.New("ssh should have syntax id=id[,target=path,required=bool,mode=uint,uid=uint,gid=uint") var err error var id, target string var required bool var uid, gid uint32 var mode uint32 = 0o600 for _, val := range tokens { kv := strings.SplitN(val, "=", 2) if len(kv) < 2 { return nil, nil, errInvalidSyntax } switch kv[0] { case "type": // This is already processed continue case "id": id = kv[1] case "target", "dst", "destination": target = kv[1] case "required": required, err = strconv.ParseBool(kv[1]) if err != nil { return nil, nil, errInvalidSyntax } case "mode": mode64, err := strconv.ParseUint(kv[1], 8, 32) if err != nil { return nil, nil, errInvalidSyntax } mode = uint32(mode64) case "uid": uid64, err := strconv.ParseUint(kv[1], 10, 32) if err != nil { return nil, nil, errInvalidSyntax } uid = uint32(uid64) case "gid": gid64, err := strconv.ParseUint(kv[1], 10, 32) if err != nil { return nil, nil, errInvalidSyntax } gid = uint32(gid64) default: return nil, nil, errInvalidSyntax } } if id == "" { id = "default" } // Default location for secrets is /run/buildkit/ssh_agent.{i} if target == "" { target = fmt.Sprintf("/run/buildkit/ssh_agent.%d", count) } sshsource, ok := sshsources[id] if !ok { if required { return nil, nil, fmt.Errorf("ssh required but no ssh with id %s found", id) } return nil, nil, nil } // Create new agent from keys or socket fwdAgent, err := sshagent.NewAgentServer(sshsource) if err != nil { return nil, nil, err } // Start ssh server, and get the host sock we're mounting in the container hostSock, err := fwdAgent.Serve(b.ProcessLabel) if err != nil { return nil, nil, err } if err := relabel(filepath.Dir(hostSock), b.MountLabel, false); err != nil { if shutdownErr := fwdAgent.Shutdown(); shutdownErr != nil { b.Logger.Errorf("error shutting down agent: %v", shutdownErr) } return nil, nil, err } if err := relabel(hostSock, b.MountLabel, false); err != nil { if shutdownErr := fwdAgent.Shutdown(); shutdownErr != nil { b.Logger.Errorf("error shutting down agent: %v", shutdownErr) } return nil, nil, err } hostUID, hostGID, err := util.GetHostIDs(idMaps.uidmap, idMaps.gidmap, uid, gid) if err != nil { if shutdownErr := fwdAgent.Shutdown(); shutdownErr != nil { b.Logger.Errorf("error shutting down agent: %v", shutdownErr) } return nil, nil, err } if err := os.Lchown(hostSock, int(hostUID), int(hostGID)); err != nil { if shutdownErr := fwdAgent.Shutdown(); shutdownErr != nil { b.Logger.Errorf("error shutting down agent: %v", shutdownErr) } return nil, nil, err } if err := os.Chmod(hostSock, os.FileMode(mode)); err != nil { if shutdownErr := fwdAgent.Shutdown(); shutdownErr != nil { b.Logger.Errorf("error shutting down agent: %v", shutdownErr) } return nil, nil, err } newMount := specs.Mount{ Destination: target, Type: define.TypeBind, Source: hostSock, Options: append(define.BindOptions, "rprivate", "ro"), } return &newMount, fwdAgent, nil } // cleanupRunMounts cleans up run mounts so they only appear in this run. func (b *Builder) cleanupRunMounts(artifacts *runMountArtifacts) error { for _, agent := range artifacts.Agents { servePath := agent.ServePath() if err := agent.Shutdown(); err != nil { return fmt.Errorf("shutting down SSH agent at %q: %v", servePath, err) } } // clean up any overlays we mounted for _, overlayDirectory := range artifacts.RunOverlayDirs { if err := overlay.RemoveTemp(overlayDirectory); err != nil { return err } } // unmount anything that needs unmounting for _, intermediateMount := range artifacts.IntermediateMounts { if err := mount.Unmount(intermediateMount); err != nil && !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("unmounting %q: %w", intermediateMount, err) } if err := os.Remove(intermediateMount); err != nil && !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("removing should-be-empty directory %q: %w", intermediateMount, err) } } // unmount any images we mounted for this run for _, image := range artifacts.MountedImages { if _, err := b.store.UnmountImage(image, false); err != nil { logrus.Debugf("umounting image %q: %v", image, err) } } // unlock locks we took, most likely for cache mounts volumes.UnlockLockArray(artifacts.TargetLocks) return nil } // setPdeathsig sets a parent-death signal for the process // the goroutine that starts the child process should lock itself to // a native thread using runtime.LockOSThread() until the child exits func setPdeathsig(cmd *exec.Cmd) { if cmd.SysProcAttr == nil { cmd.SysProcAttr = &syscall.SysProcAttr{} } cmd.SysProcAttr.Pdeathsig = syscall.SIGKILL } func relabel(path, mountLabel string, shared bool) error { if err := label.Relabel(path, mountLabel, shared); err != nil { if !errors.Is(err, syscall.ENOTSUP) { return err } logrus.Debugf("Labeling not supported on %q", path) } return nil } // mapContainerNameToHostname returns the passed-in string with characters that // don't match validHostnames (defined above) stripped out. func mapContainerNameToHostname(containerName string) string { match := validHostnames.FindStringIndex(containerName) if match == nil { return "" } trimmed := containerName[match[0]:] match[1] -= match[0] match[0] = 0 for match[1] != len(trimmed) && match[1] < match[0]+maxHostnameLen { trimmed = trimmed[:match[1]] + trimmed[match[1]+1:] match = validHostnames.FindStringIndex(trimmed) match[1] = min(match[1], maxHostnameLen) } return trimmed[:match[1]] } // createMountTargets creates empty files or directories that are used as // targets for mounts in the spec, and makes a note of what it created. func (b *Builder) createMountTargets(spec *specs.Spec) ([]copier.ConditionalRemovePath, error) { // Avoid anything weird happening, just in case. if spec == nil || spec.Root == nil { return nil, nil } rootfsPath := spec.Root.Path then := time.Unix(0, 0) exemptFromTimesPreservation := map[string]struct{}{ "dev": {}, "proc": {}, "sys": {}, } exemptFromRemoval := map[string]struct{}{ "dev": {}, "proc": {}, "sys": {}, } overridePermissions := map[string]os.FileMode{ "dev": 0o755, "proc": 0o755, "sys": 0o755, } uidmap, gidmap := convertRuntimeIDMaps(b.IDMappingOptions.UIDMap, b.IDMappingOptions.GIDMap) targets := copier.EnsureOptions{ UIDMap: uidmap, GIDMap: gidmap, } for _, mnt := range spec.Mounts { typeFlag := byte(tar.TypeDir) // If the mount is a "bind" or "rbind" mount, then it's a bind // mount, which means the target _could_ be a non-directory. // Check the source and make a note. if mnt.Type == define.TypeBind || slices.Contains(mnt.Options, "bind") || slices.Contains(mnt.Options, "rbind") { if st, err := os.Stat(mnt.Source); err == nil { if !st.IsDir() { typeFlag = tar.TypeReg } } } // Walk the path components from the root all the way down to // the target mountpoint and build a list of pathnames that we // need to ensure exist. If we might need to remove them, give // them a conspicuous mtime, so that we can detect if they were // unmounted and then modified, in which case we'll want to // preserve those changes. destination := mnt.Destination for destination != "" { cleanedDestination := strings.Trim(path.Clean(filepath.ToSlash(destination)), "/") modTime := &then if _, ok := exemptFromTimesPreservation[cleanedDestination]; ok { // don't force a timestamp for this path modTime = nil } var mode *os.FileMode if _, ok := exemptFromRemoval[cleanedDestination]; ok { // we're not going to filter this out later, // so don't make it look weird perms := os.FileMode(0o755) if typeFlag == tar.TypeReg { perms = 0o644 } mode = &perms modTime = nil } if perms, ok := overridePermissions[cleanedDestination]; ok { // forced permissions mode = &perms } if mode == nil && destination != cleanedDestination { // parent directories default to 0o755, for // the sake of commands running as UID != 0 perms := os.FileMode(0o755) mode = &perms } targets.Paths = append(targets.Paths, copier.EnsurePath{ Path: destination, Typeflag: typeFlag, ModTime: modTime, Chmod: mode, }) typeFlag = tar.TypeDir dir, _ := filepath.Split(destination) if destination == dir { break } destination = dir } } if len(targets.Paths) == 0 { return nil, nil } created, noted, err := copier.Ensure(rootfsPath, rootfsPath, targets) if err != nil { return nil, err } logrus.Debugf("created mount targets at %v", created) logrus.Debugf("parents of mount targets at %+v", noted) var remove []copier.ConditionalRemovePath for _, target := range created { cleanedTarget := strings.Trim(path.Clean(filepath.ToSlash(target)), "/") if _, ok := exemptFromRemoval[cleanedTarget]; ok { continue } modTime := &then if _, ok := exemptFromTimesPreservation[cleanedTarget]; ok { modTime = nil } condition := copier.ConditionalRemovePath{ Path: cleanedTarget, ModTime: modTime, Owner: &idtools.IDPair{UID: 0, GID: 0}, } remove = append(remove, condition) } if len(remove) == 0 { return nil, nil } // encode the set of paths we might need to filter out at commit-time // in a way that hopefully doesn't break long-running concurrent Run() // calls, that lets us also not have to manage any locking for them cdir, err := b.store.ContainerDirectory(b.Container) if err != nil { return nil, fmt.Errorf("finding working container bookkeeping directory: %w", err) } for excludesDir, exclusions := range map[string][]copier.ConditionalRemovePath{ containerExcludesDir: remove, containerPulledUpDir: noted, } { if err := os.Mkdir(filepath.Join(cdir, excludesDir), 0o700); err != nil && !errors.Is(err, os.ErrExist) { return nil, fmt.Errorf("creating exclusions directory: %w", err) } encoded, err := json.Marshal(exclusions) if err != nil { return nil, fmt.Errorf("encoding list of items to exclude at commit-time: %w", err) } f, err := os.CreateTemp(filepath.Join(cdir, excludesDir), "filter*"+containerExcludesSubstring) if err != nil { return nil, fmt.Errorf("creating exclusions file: %w", err) } defer os.Remove(f.Name()) defer f.Close() if err := ioutils.AtomicWriteFile(strings.TrimSuffix(f.Name(), containerExcludesSubstring), encoded, 0o600); err != nil { return nil, fmt.Errorf("writing exclusions file: %w", err) } } // return the set of to-remove-now paths directly, in case the caller would prefer // to clear them out itself now instead of waiting until commit-time return remove, nil } ================================================ FILE: run_common_test.go ================================================ package buildah import ( "testing" "github.com/stretchr/testify/assert" ) func TestMapContainerNameToHostname(t *testing.T) { cases := [][2]string{ {"trivial", "trivial"}, {"Nottrivial", "Nottrivial"}, {"0Nottrivial", "0Nottrivial"}, {"0Nottrivi-al", "0Nottrivi-al"}, {"-0Nottrivi-al", "0Nottrivi-al"}, {".-0Nottrivi-.al", "0Nottrivi-.al"}, {".-0Nottrivi-.al0123456789", "0Nottrivi-.al0123456789"}, {".-0Nottrivi-.al0123456789+0123456789", "0Nottrivi-.al01234567890123456789"}, {".-0Nottrivi-.al0123456789+0123456789/0123456789", "0Nottrivi-.al012345678901234567890123456789"}, {".-0Nottrivi-.al0123456789+0123456789/0123456789%0123456789", "0Nottrivi-.al0123456789012345678901234567890123456789"}, {".-0Nottrivi-.al0123456789+0123456789/0123456789%0123456789_0123456789", "0Nottrivi-.al01234567890123456789012345678901234567890123456789"}, {".-0Nottrivi-.al0123456789+0123456789/0123456789%0123456789_0123456789:0123456", "0Nottrivi-.al012345678901234567890123456789012345678901234567890"}, {".-0Nottrivi-.al0123456789+0123456789/0123456789%0123456789_0123456789:0123456789", "0Nottrivi-.al012345678901234567890123456789012345678901234567890"}, } for i := range cases { t.Run(cases[i][0], func(t *testing.T) { sanitized := mapContainerNameToHostname(cases[i][0]) assert.Equalf(t, cases[i][1], sanitized, "mapping container name %q to a valid hostname", cases[i][0]) }) } } ================================================ FILE: run_freebsd.go ================================================ //go:build freebsd package buildah import ( "errors" "fmt" "os" "path/filepath" "slices" "strings" "unsafe" "github.com/containers/buildah/bind" "github.com/containers/buildah/chroot" "github.com/containers/buildah/copier" "github.com/containers/buildah/define" "github.com/containers/buildah/internal" "github.com/containers/buildah/internal/tmpdir" "github.com/containers/buildah/pkg/jail" "github.com/containers/buildah/pkg/overlay" "github.com/containers/buildah/pkg/parse" butil "github.com/containers/buildah/pkg/util" "github.com/containers/buildah/util" "github.com/docker/go-units" "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-tools/generate" "github.com/sirupsen/logrus" "go.podman.io/common/libnetwork/etchosts" "go.podman.io/common/libnetwork/resolvconf" nettypes "go.podman.io/common/libnetwork/types" netUtil "go.podman.io/common/libnetwork/util" "go.podman.io/common/pkg/config" "go.podman.io/image/v5/types" "go.podman.io/storage/pkg/idtools" "go.podman.io/storage/pkg/lockfile" "go.podman.io/storage/pkg/stringid" "golang.org/x/sys/unix" ) const ( P_PID = 0 P_PGID = 2 PROC_REAP_ACQUIRE = 2 PROC_REAP_RELEASE = 3 ) func procctl(idtype int, id int, cmd int, arg *byte) error { _, _, e1 := unix.Syscall6( unix.SYS_PROCCTL, uintptr(idtype), uintptr(id), uintptr(cmd), uintptr(unsafe.Pointer(arg)), 0, 0) if e1 != 0 { return unix.Errno(e1) } return nil } func setChildProcess() error { if err := procctl(P_PID, unix.Getpid(), PROC_REAP_ACQUIRE, nil); err != nil { fmt.Fprintf(os.Stderr, "procctl(PROC_REAP_ACQUIRE): %v\n", err) return err } return nil } func (b *Builder) Run(command []string, options RunOptions) error { var runArtifacts *runMountArtifacts if len(options.ExternalImageMounts) > 0 { defer func() { if runArtifacts == nil { // we didn't add ExternalImageMounts to the // list of images that we're going to unmount // yet and make a deferred call that cleans // them up, but the caller is expecting us to // unmount these for them because we offered to for _, image := range options.ExternalImageMounts { if _, err := b.store.UnmountImage(image, false); err != nil { logrus.Debugf("umounting image %q: %v", image, err) } } } }() } p, err := os.MkdirTemp(tmpdir.GetTempDir(), define.Package) if err != nil { return err } // On some hosts like AH, /tmp is a symlink and we need an // absolute path. path, err := filepath.EvalSymlinks(p) if err != nil { return err } logrus.Debugf("using %q to hold bundle data", path) defer func() { if err2 := os.RemoveAll(path); err2 != nil { logrus.Errorf("error removing %q: %v", path, err2) } }() gp, err := generate.New("freebsd") if err != nil { return fmt.Errorf("generating new 'freebsd' runtime spec: %w", err) } g := &gp isolation := options.Isolation if isolation == IsolationDefault { isolation = b.Isolation if isolation == IsolationDefault { isolation, err = parse.IsolationOption("") if err != nil { logrus.Debugf("got %v while trying to determine default isolation, guessing OCI", err) isolation = IsolationOCI } else if isolation == IsolationDefault { isolation = IsolationOCI } } } if err := checkAndOverrideIsolationOptions(isolation, &options); err != nil { return err } // hardwire the environment to match docker build to avoid subtle and hard-to-debug differences due to containers.conf b.configureEnvironment(g, options, []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}) if b.CommonBuildOpts == nil { return fmt.Errorf("invalid format on container you must recreate the container") } if err := addCommonOptsToSpec(b.CommonBuildOpts, g); err != nil { return err } workDir := b.WorkDir() if options.WorkingDir != "" { g.SetProcessCwd(options.WorkingDir) workDir = options.WorkingDir } else if b.WorkDir() != "" { g.SetProcessCwd(b.WorkDir()) workDir = b.WorkDir() } if workDir == "" { workDir = string(os.PathSeparator) } mountPoint, err := b.Mount(b.MountLabel) if err != nil { return fmt.Errorf("mounting container %q: %w", b.ContainerID, err) } defer func() { if err := b.Unmount(); err != nil { logrus.Errorf("error unmounting container: %v", err) } }() g.SetRootPath(mountPoint) if len(command) > 0 { command = runLookupPath(g, command) g.SetProcessArgs(command) } else { g.SetProcessArgs(nil) } setupTerminal(g, options.Terminal, options.TerminalSize) configureNetwork, networkString, err := b.configureNamespaces(g, &options) if err != nil { return err } containerName := Package + "-" + filepath.Base(path) if configureNetwork { if jail.NeedVnetJail() { g.AddAnnotation("org.freebsd.parentJail", containerName+"-vnet") } else { g.AddAnnotation("org.freebsd.jail.vnet", "new") } } homeDir, err := b.configureUIDGID(g, mountPoint, options) if err != nil { return err } // Now grab the spec from the generator. Set the generator to nil so that future contributors // will quickly be able to tell that they're supposed to be modifying the spec directly from here. spec := g.Config g = nil // Set the seccomp configuration using the specified profile name. Some syscalls are // allowed if certain capabilities are to be granted (example: CAP_SYS_CHROOT and chroot), // so we sorted out the capabilities lists first. if err = setupSeccomp(spec, b.CommonBuildOpts.SeccompProfilePath); err != nil { return err } uid, gid := spec.Process.User.UID, spec.Process.User.GID idPair := &idtools.IDPair{UID: int(uid), GID: int(gid)} mode := os.FileMode(0o755) coptions := copier.MkdirOptions{ ChownNew: idPair, ChmodNew: &mode, } if err := copier.Mkdir(mountPoint, filepath.Join(mountPoint, spec.Process.Cwd), coptions); err != nil { return err } bindFiles := make(map[string]string) volumes := b.Volumes() // Figure out who owns files that will appear to be owned by UID/GID 0 in the container. rootUID, rootGID, err := util.GetHostRootIDs(spec) if err != nil { return err } rootIDPair := &idtools.IDPair{UID: int(rootUID), GID: int(rootGID)} hostsFile := "" if !options.NoHosts && !slices.Contains(volumes, config.DefaultHostsFile) && options.ConfigureNetwork != define.NetworkDisabled { hostsFile, err = b.createHostsFile(path, rootIDPair) if err != nil { return err } bindFiles[config.DefaultHostsFile] = hostsFile // Only add entries here if we do not have to setup network, // if we do we have to do it much later after the network setup. if !configureNetwork { var entries etchosts.HostEntries // add host entry for local ip when running in host network if spec.Hostname != "" { ip := netUtil.GetLocalIP() if ip != "" { entries = append(entries, etchosts.HostEntry{ Names: []string{spec.Hostname}, IP: ip, }) } } err = b.addHostsEntries(hostsFile, mountPoint, entries, nil, "") if err != nil { return err } } } resolvFile := "" if !slices.Contains(volumes, resolvconf.DefaultResolvConf) && options.ConfigureNetwork != define.NetworkDisabled && !(len(b.CommonBuildOpts.DNSServers) == 1 && strings.ToLower(b.CommonBuildOpts.DNSServers[0]) == "none") { resolvFile, err = b.createResolvConf(path, rootIDPair) if err != nil { return err } bindFiles[resolvconf.DefaultResolvConf] = resolvFile // Only add entries here if we do not have to do setup network, // if we do we have to do it much later after the network setup. if !configureNetwork { err = b.addResolvConfEntries(resolvFile, nil, spec, false, true) if err != nil { return err } } } runMountInfo := runMountInfo{ WorkDir: workDir, ContextDir: options.ContextDir, Secrets: options.Secrets, SSHSources: options.SSHSources, StageMountPoints: options.StageMountPoints, SystemContext: options.SystemContext, } runArtifacts, err = b.setupMounts(mountPoint, spec, path, options.Mounts, bindFiles, volumes, options.CompatBuiltinVolumes, b.CommonBuildOpts.Volumes, options.RunMounts, runMountInfo) if err != nil { return fmt.Errorf("resolving mountpoints for container %q: %w", b.ContainerID, err) } // following run was called from `buildah run` // and some images were mounted for this run // add them to cleanup artifacts if len(options.ExternalImageMounts) > 0 { runArtifacts.MountedImages = append(runArtifacts.MountedImages, options.ExternalImageMounts...) } defer func() { if err := b.cleanupRunMounts(runArtifacts); err != nil { options.Logger.Errorf("unable to cleanup run mounts %v", err) } }() // If we are creating a network, make the vnet here so that we can // execute the OCI runtime inside it. For FreeBSD-13.3 and later, we can // configure the container network settings from outside the jail, which // removes the need for a separate jail to manage the vnet. if configureNetwork && jail.NeedVnetJail() { mynetns := containerName + "-vnet" jconf := jail.NewConfig() jconf.Set("name", mynetns) jconf.Set("vnet", jail.NEW) jconf.Set("children.max", 1) jconf.Set("persist", true) jconf.Set("enforce_statfs", 0) jconf.Set("devfs_ruleset", 4) jconf.Set("allow.raw_sockets", true) jconf.Set("allow.chflags", true) jconf.Set("securelevel", -1) netjail, err := jail.Create(jconf) if err != nil { return err } defer func() { jconf := jail.NewConfig() jconf.Set("persist", false) err2 := netjail.Set(jconf) if err2 != nil { logrus.Errorf("error releasing vnet jail %q: %v", mynetns, err2) } }() } // Create any mount points that we need that aren't already present in // the rootfs. createdMountTargets, err := b.createMountTargets(spec) if err != nil { return fmt.Errorf("ensuring mount targets for container %q: %w", b.ContainerID, err) } defer func() { // Attempt to clean up mount targets for the sake of builds // that don't commit and rebase at each step, and people using // `buildah run` more than once, who don't expect empty mount // points to stick around. They'll still get filtered out at // commit-time if another concurrent Run() is keeping something // busy. if _, err := copier.ConditionalRemove(mountPoint, mountPoint, copier.ConditionalRemoveOptions{ UIDMap: b.store.UIDMap(), GIDMap: b.store.GIDMap(), Paths: createdMountTargets, }); err != nil { options.Logger.Errorf("unable to cleanup run mount targets %v", err) } }() switch isolation { case IsolationOCI: var moreCreateArgs []string if options.NoPivot { moreCreateArgs = []string{"--no-pivot"} } else { moreCreateArgs = nil } err = b.runUsingRuntimeSubproc(isolation, options, configureNetwork, networkString, moreCreateArgs, spec, mountPoint, path, containerName, b.Container, hostsFile, resolvFile) case IsolationChroot: err = chroot.RunUsingChroot(spec, path, homeDir, options.Stdin, options.Stdout, options.Stderr, options.NoPivot) default: err = errors.New("don't know how to run this command") } return err } func addCommonOptsToSpec(commonOpts *define.CommonBuildOptions, g *generate.Generator) error { defaultContainerConfig, err := config.Default() if err != nil { return fmt.Errorf("failed to get container config: %w", err) } // Other process resource limits if err := addRlimits(commonOpts.Ulimit, g, defaultContainerConfig.Containers.DefaultUlimits.Get()); err != nil { return err } logrus.Debugf("Resources: %#v", commonOpts) return nil } // setupSpecialMountSpecChanges creates special mounts for depending // on the namespaces - nothing yet for freebsd func setupSpecialMountSpecChanges(spec *specs.Spec, shmSize string) ([]specs.Mount, error) { return spec.Mounts, nil } // If this succeeded, the caller would be expected to, after the command which // uses the mount exits, clean up the overlay filesystem (if we returned one), // unmount the mounted filesystem (if we provided the path to its mountpoint) // and remove its mountpoint, unmount the image (if we mounted one), and // release the lock (if we took one). func (b *Builder) getCacheMount(tokens []string, sys *types.SystemContext, stageMountPoints map[string]internal.StageMountDetails, idMaps IDMaps, workDir, tmpDir string) (*specs.Mount, string, string, string, *lockfile.LockFile, error) { return nil, "", "", "", nil, errors.New("cache mounts not supported on freebsd") } func (b *Builder) runSetupVolumeMounts(mountLabel string, volumeMounts []string, optionMounts []specs.Mount, idMaps IDMaps) (mounts []specs.Mount, overlayDirs []string, Err error) { // Make sure the overlay directory is clean before running _, err := b.store.ContainerDirectory(b.ContainerID) if err != nil { return nil, nil, fmt.Errorf("looking up container directory for %s: %w", b.ContainerID, err) } parseMount := func(mountType, host, container string, options []string) (specs.Mount, error) { var foundrw, foundro, foundO bool var upperDir string for _, opt := range options { switch opt { case "rw": foundrw = true case "ro": foundro = true case "O": foundO = true } if strings.HasPrefix(opt, "upperdir") { splitOpt := strings.SplitN(opt, "=", 2) if len(splitOpt) > 1 { upperDir = splitOpt[1] } } } if !foundrw && !foundro { options = append(options, "rw") } if mountType == "bind" || mountType == "rbind" { mountType = "nullfs" } if foundO { containerDir, err := b.store.ContainerDirectory(b.ContainerID) if err != nil { return specs.Mount{}, err } contentDir, err := overlay.TempDir(containerDir, idMaps.rootUID, idMaps.rootGID) if err != nil { return specs.Mount{}, fmt.Errorf("failed to create TempDir in the %s directory: %w", containerDir, err) } overlayOpts := overlay.Options{ RootUID: idMaps.rootUID, RootGID: idMaps.rootGID, UpperDirOptionFragment: upperDir, GraphOpts: b.store.GraphOptions(), } overlayMount, err := overlay.MountWithOptions(contentDir, host, container, &overlayOpts) if err == nil { overlayDirs = append(overlayDirs, contentDir) } return overlayMount, err } return specs.Mount{ Destination: container, Type: mountType, Source: host, Options: options, }, nil } // Bind mount volumes specified for this particular Run() invocation for _, i := range optionMounts { logrus.Debugf("setting up mounted volume at %q", i.Destination) mount, err := parseMount(i.Type, i.Source, i.Destination, i.Options) if err != nil { return nil, nil, err } mounts = append(mounts, mount) } // Bind mount volumes given by the user when the container was created for _, i := range volumeMounts { var options []string spliti := strings.Split(i, ":") if len(spliti) > 2 { options = strings.Split(spliti[2], ",") } mount, err := parseMount("nullfs", spliti[0], spliti[1], options) if err != nil { return nil, nil, err } mounts = append(mounts, mount) } return mounts, overlayDirs, nil } func setupCapabilities(g *generate.Generator, defaultCapabilities, adds, drops []string) error { return nil } func (b *Builder) runConfigureNetwork(pid int, isolation define.Isolation, options RunOptions, networkString string, containerName string, hostnames []string) (func(), *netResult, error) { //if isolation == IsolationOCIRootless { //return setupRootlessNetwork(pid) //} var configureNetworks []string if len(networkString) > 0 { configureNetworks = strings.Split(networkString, ",") } if len(configureNetworks) == 0 { configureNetworks = []string{b.NetworkInterface.DefaultNetworkName()} } logrus.Debugf("configureNetworks: %v", configureNetworks) var mynetns string if jail.NeedVnetJail() { mynetns = containerName + "-vnet" } else { mynetns = containerName } networks := make(map[string]nettypes.PerNetworkOptions, len(configureNetworks)) for i, network := range configureNetworks { networks[network] = nettypes.PerNetworkOptions{ InterfaceName: fmt.Sprintf("eth%d", i), } } opts := nettypes.NetworkOptions{ ContainerID: containerName, ContainerName: containerName, Networks: networks, } netStatus, err := b.NetworkInterface.Setup(mynetns, nettypes.SetupOptions{NetworkOptions: opts}) if err != nil { return nil, nil, err } teardown := func() { err := b.NetworkInterface.Teardown(mynetns, nettypes.TeardownOptions{NetworkOptions: opts}) if err != nil { logrus.Errorf("failed to cleanup network: %v", err) } } return teardown, netStatusToNetResult(netStatus, hostnames), nil } func setupNamespaces(logger *logrus.Logger, g *generate.Generator, namespaceOptions define.NamespaceOptions, idmapOptions define.IDMappingOptions, policy define.NetworkConfigurationPolicy) (configureNetwork bool, networkString string, configureUTS bool, err error) { // Set namespace options in the container configuration. for _, namespaceOption := range namespaceOptions { switch namespaceOption.Name { case string(specs.NetworkNamespace): configureNetwork = false if !namespaceOption.Host && (namespaceOption.Path == "" || !filepath.IsAbs(namespaceOption.Path)) { if namespaceOption.Path != "" && !filepath.IsAbs(namespaceOption.Path) { networkString = namespaceOption.Path namespaceOption.Path = "" } configureNetwork = (policy != define.NetworkDisabled) } case string(specs.UTSNamespace): configureUTS = false if !namespaceOption.Host && namespaceOption.Path == "" { configureUTS = true } } // TODO: re-visit this when there is consensus on a // FreeBSD runtime-spec. FreeBSD jails have rough // equivalents for UTS and and network namespaces. } return configureNetwork, networkString, configureUTS, nil } func (b *Builder) configureNamespaces(g *generate.Generator, options *RunOptions) (bool, string, error) { defaultNamespaceOptions, err := DefaultNamespaceOptions() if err != nil { return false, "", err } namespaceOptions := defaultNamespaceOptions namespaceOptions.AddOrReplace(b.NamespaceOptions...) namespaceOptions.AddOrReplace(options.NamespaceOptions...) networkPolicy := options.ConfigureNetwork // Nothing was specified explicitly so network policy should be inherited from builder if networkPolicy == NetworkDefault { networkPolicy = b.ConfigureNetwork // If builder policy was NetworkDisabled and // we want to disable network for this run. // reset options.ConfigureNetwork to NetworkDisabled // since it will be treated as source of truth later. if networkPolicy == NetworkDisabled { options.ConfigureNetwork = networkPolicy } } configureNetwork, networkString, configureUTS, err := setupNamespaces(options.Logger, g, namespaceOptions, b.IDMappingOptions, networkPolicy) if err != nil { return false, "", err } if configureUTS { if options.Hostname != "" { g.SetHostname(options.Hostname) } else if b.Hostname() != "" { g.SetHostname(b.Hostname()) } else { hostname := stringid.TruncateID(b.ContainerID) defConfig, err := config.Default() if err != nil { return false, "", fmt.Errorf("failed to get container config: %w", err) } if defConfig.Containers.ContainerNameAsHostName { if mapped := mapContainerNameToHostname(b.Container); mapped != "" { hostname = mapped } } g.SetHostname(hostname) } } else { g.SetHostname("") } found := false spec := g.Config for i := range spec.Process.Env { if strings.HasPrefix(spec.Process.Env[i], "HOSTNAME=") { found = true break } } if !found { spec.Process.Env = append(spec.Process.Env, fmt.Sprintf("HOSTNAME=%s", spec.Hostname)) } return configureNetwork, networkString, nil } func runSetupBoundFiles(bundlePath string, bindFiles map[string]string) (mounts []specs.Mount) { for dest, src := range bindFiles { options := []string{} if strings.HasPrefix(src, bundlePath) { options = append(options, bind.NoBindOption) } mounts = append(mounts, specs.Mount{ Source: src, Destination: dest, Type: "nullfs", Options: options, }) } return mounts } func addRlimits(ulimit []string, g *generate.Generator, defaultUlimits []string) error { var ( ul *units.Ulimit err error ) ulimit = append(defaultUlimits, ulimit...) for _, u := range ulimit { if ul, err = butil.ParseUlimit(u); err != nil { return fmt.Errorf("ulimit option %q requires name=SOFT:HARD, failed to be parsed: %w", u, err) } g.AddProcessRlimits("RLIMIT_"+strings.ToUpper(ul.Name), uint64(ul.Hard), uint64(ul.Soft)) } return nil } // Create pipes to use for relaying stdio. func runMakeStdioPipe(uid, gid int) ([][]int, error) { stdioPipe := make([][]int, 3) for i := range stdioPipe { stdioPipe[i] = make([]int, 2) if err := unix.Pipe(stdioPipe[i]); err != nil { return nil, fmt.Errorf("creating pipe for container FD %d: %w", i, err) } } return stdioPipe, nil } ================================================ FILE: run_linux.go ================================================ //go:build linux package buildah import ( "context" "errors" "fmt" "maps" "os" "path/filepath" "slices" "strings" "sync" "syscall" "github.com/containers/buildah/bind" "github.com/containers/buildah/chroot" "github.com/containers/buildah/copier" "github.com/containers/buildah/define" "github.com/containers/buildah/internal" "github.com/containers/buildah/internal/tmpdir" "github.com/containers/buildah/internal/volumes" "github.com/containers/buildah/pkg/binfmt" "github.com/containers/buildah/pkg/overlay" "github.com/containers/buildah/pkg/parse" butil "github.com/containers/buildah/pkg/util" "github.com/containers/buildah/util" "github.com/docker/go-units" "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-tools/generate" "github.com/sirupsen/logrus" "go.podman.io/common/libnetwork/etchosts" "go.podman.io/common/libnetwork/pasta" "go.podman.io/common/libnetwork/resolvconf" "go.podman.io/common/libnetwork/slirp4netns" nettypes "go.podman.io/common/libnetwork/types" netUtil "go.podman.io/common/libnetwork/util" "go.podman.io/common/pkg/capabilities" "go.podman.io/common/pkg/chown" "go.podman.io/common/pkg/config" "go.podman.io/common/pkg/hooks" hooksExec "go.podman.io/common/pkg/hooks/exec" "go.podman.io/image/v5/types" "go.podman.io/storage/pkg/fileutils" "go.podman.io/storage/pkg/idtools" "go.podman.io/storage/pkg/ioutils" "go.podman.io/storage/pkg/lockfile" "go.podman.io/storage/pkg/mount" "go.podman.io/storage/pkg/stringid" "go.podman.io/storage/pkg/unshare" "golang.org/x/sys/unix" "tags.cncf.io/container-device-interface/pkg/cdi" "tags.cncf.io/container-device-interface/pkg/parser" ) // binfmtRegistered makes sure we only try to register binfmt_misc // interpreters once, the first time we handle a RUN instruction. var binfmtRegistered sync.Once func setChildProcess() error { if err := unix.Prctl(unix.PR_SET_CHILD_SUBREAPER, uintptr(1), 0, 0, 0); err != nil { fmt.Fprintf(os.Stderr, "prctl(PR_SET_CHILD_SUBREAPER, 1): %v\n", err) return err } return nil } func (b *Builder) cdiSetupDevicesInSpec(deviceSpecs []string, configDir string, spec *specs.Spec) ([]string, error) { var configDirs []string defConfig, err := config.Default() if err != nil { return nil, fmt.Errorf("failed to get container config: %w", err) } // The CDI cache prioritizes entries from directories that are later in // the list of ones it scans, so start with our general config, then // append values passed to us through API layers. configDirs = slices.Clone(defConfig.Engine.CdiSpecDirs.Get()) if b.CDIConfigDir != "" { configDirs = append(configDirs, b.CDIConfigDir) } if configDir != "" { configDirs = append(configDirs, configDir) } if len(configDirs) == 0 { // No directories to scan for CDI configuration means that CDI // won't have any details for setting up any devices, so we // don't need to be doing anything here. return deviceSpecs, nil } var qualifiedDeviceSpecs, unqualifiedDeviceSpecs []string for _, deviceSpec := range deviceSpecs { if parser.IsQualifiedName(deviceSpec) { qualifiedDeviceSpecs = append(qualifiedDeviceSpecs, deviceSpec) } else { unqualifiedDeviceSpecs = append(unqualifiedDeviceSpecs, deviceSpec) } } if len(qualifiedDeviceSpecs) == 0 { // None of the specified devices were in the form that would be // handled by CDI, so we don't need to do anything here. return deviceSpecs, nil } if err := cdi.Configure(cdi.WithSpecDirs(configDirs...)); err != nil { return nil, fmt.Errorf("CDI default registry ignored configured directories %v: %w", configDirs, err) } leftoverDevices := slices.Clone(deviceSpecs) if err := cdi.Refresh(); err != nil { logrus.Warnf("CDI default registry refresh: %v", err) } else { leftoverDevices, err = cdi.InjectDevices(spec, qualifiedDeviceSpecs...) if err != nil { return nil, fmt.Errorf("CDI device injection (leftover devices: %v): %w", leftoverDevices, err) } } removed := slices.DeleteFunc(slices.Clone(deviceSpecs), func(t string) bool { return slices.Contains(leftoverDevices, t) }) logrus.Debugf("CDI taking care of devices %v, leaving devices %v, skipped %v", removed, leftoverDevices, unqualifiedDeviceSpecs) return append(leftoverDevices, unqualifiedDeviceSpecs...), nil } // Extract the device list so that we can still try to make it work if // we're running rootless and can't just mknod() the device nodes. func separateDevicesFromRuntimeSpec(g *generate.Generator) define.ContainerDevices { var result define.ContainerDevices if g.Config != nil && g.Config.Linux != nil { for _, device := range g.Config.Linux.Devices { var bDevice define.BuildahDevice bDevice.Path = device.Path switch device.Type { case "b": bDevice.Type = 'b' case "c": bDevice.Type = 'c' case "u": bDevice.Type = 'u' case "p": bDevice.Type = 'p' } bDevice.Major = device.Major bDevice.Minor = device.Minor if device.FileMode != nil { bDevice.FileMode = *device.FileMode } if device.UID != nil { bDevice.Uid = *device.UID } if device.GID != nil { bDevice.Gid = *device.GID } bDevice.Source = device.Path bDevice.Destination = device.Path result = append(result, bDevice) } } g.ClearLinuxDevices() return result } // Run runs the specified command in the container's root filesystem. func (b *Builder) Run(command []string, options RunOptions) error { var runArtifacts *runMountArtifacts if len(options.ExternalImageMounts) > 0 { defer func() { if runArtifacts == nil { // we didn't add ExternalImageMounts to the // list of images that we're going to unmount // yet and make a deferred call that cleans // them up, but the caller is expecting us to // unmount these for them because we offered to for _, image := range options.ExternalImageMounts { if _, err := b.store.UnmountImage(image, false); err != nil { logrus.Debugf("umounting image %q: %v", image, err) } } } }() } if os.Getenv("container") != "" { os, arch, variant, err := parse.Platform("") if err != nil { return fmt.Errorf("reading the current default platform") } platform := b.OCIv1.Platform if os != platform.OS || arch != platform.Architecture || variant != platform.Variant { binfmtRegistered.Do(func() { if err := binfmt.Register(nil); err != nil { logrus.Warnf("registering binfmt_misc interpreters: %v", err) } }) } } p, err := os.MkdirTemp(tmpdir.GetTempDir(), define.Package) if err != nil { return err } // On some hosts like AH, /tmp is a symlink and we need an // absolute path. path, err := filepath.EvalSymlinks(p) if err != nil { return err } logrus.Debugf("using %q to hold bundle data", path) defer func() { if err2 := os.RemoveAll(path); err2 != nil { options.Logger.Error(err2) } }() gp, err := generate.New("linux") if err != nil { return fmt.Errorf("generating new 'linux' runtime spec: %w", err) } g := &gp isolation := options.Isolation if isolation == define.IsolationDefault { isolation = b.Isolation if isolation == define.IsolationDefault { isolation, err = parse.IsolationOption("") if err != nil { logrus.Debugf("got %v while trying to determine default isolation, guessing OCI", err) isolation = IsolationOCI } else if isolation == IsolationDefault { isolation = IsolationOCI } } } if err := checkAndOverrideIsolationOptions(isolation, &options); err != nil { return err } // hardwire the environment to match docker build to avoid subtle and hard-to-debug differences due to containers.conf b.configureEnvironment(g, options, []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}) if b.CommonBuildOpts == nil { return fmt.Errorf("invalid format on container you must recreate the container") } if err := addCommonOptsToSpec(b.CommonBuildOpts, g); err != nil { return err } workDir := b.WorkDir() if options.WorkingDir != "" { g.SetProcessCwd(options.WorkingDir) workDir = options.WorkingDir } else if b.WorkDir() != "" { g.SetProcessCwd(b.WorkDir()) workDir = b.WorkDir() } if workDir == "" { workDir = string(os.PathSeparator) } setupSelinux(g, b.ProcessLabel, b.MountLabel) mountPoint, err := b.Mount(b.MountLabel) if err != nil { return fmt.Errorf("mounting container %q: %w", b.ContainerID, err) } defer func() { if err := b.Unmount(); err != nil { options.Logger.Errorf("error unmounting container: %v", err) } }() g.SetRootPath(mountPoint) if len(command) > 0 { command = runLookupPath(g, command) g.SetProcessArgs(command) } else { g.SetProcessArgs(nil) } // Combine the working container's set of devices with the ones for just this run. deviceSpecs := slices.Concat(options.DeviceSpecs, b.DeviceSpecs) deviceSpecs, err = b.cdiSetupDevicesInSpec(deviceSpecs, options.CDIConfigDir, g.Config) // makes changes to more than just the device list if err != nil { return err } devices := separateDevicesFromRuntimeSpec(g) for _, deviceSpec := range deviceSpecs { device, err := parse.DeviceFromPath(deviceSpec) if err != nil { return fmt.Errorf("setting up device %q: %w", deviceSpec, err) } devices = append(devices, device...) } devices = slices.Concat(devices, options.Devices, b.Devices) // Mount devices, if any, and if we're rootless attempt to work around not // being able to create device nodes by bind-mounting them from the host, like podman does. if unshare.IsRootless() { // We are going to create bind mounts for devices // but we need to make sure that we don't override // anything which is already in OCI spec. mounts := make(map[string]any) for _, m := range g.Mounts() { mounts[m.Destination] = true } newMounts := []specs.Mount{} for _, d := range devices { // Default permission is read-only. perm := "ro" // Get permission configured for this device but only process `write` // permission in rootless since `mknod` is not supported anyways. if strings.Contains(string(d.Rule.Permissions), "w") { perm = "rw" } devMnt := specs.Mount{ Destination: d.Destination, Type: parse.TypeBind, Source: d.Source, Options: []string{"slave", "nosuid", "noexec", perm, "rbind"}, } // Podman parity: podman skips these two devices hence we do the same. if d.Path == "/dev/ptmx" || strings.HasPrefix(d.Path, "/dev/tty") { continue } // Device is already in OCI spec do not re-mount. if _, found := mounts[d.Path]; found { continue } newMounts = append(newMounts, devMnt) } g.Config.Mounts = append(newMounts, g.Config.Mounts...) } else { for _, d := range devices { sDev := specs.LinuxDevice{ Type: string(d.Type), Path: d.Path, Major: d.Major, Minor: d.Minor, FileMode: &d.FileMode, UID: &d.Uid, GID: &d.Gid, } g.AddDevice(sDev) g.AddLinuxResourcesDevice(true, string(d.Type), &d.Major, &d.Minor, string(d.Permissions)) } } setupMaskedPaths(g, b.CommonBuildOpts) setupReadOnlyPaths(g) setupTerminal(g, options.Terminal, options.TerminalSize) configureNetwork, networkString, err := b.configureNamespaces(g, &options) if err != nil { return err } homeDir, err := b.configureUIDGID(g, mountPoint, options) if err != nil { return err } g.SetProcessNoNewPrivileges(b.CommonBuildOpts.NoNewPrivileges) g.SetProcessApparmorProfile(b.CommonBuildOpts.ApparmorProfile) // Now grab the spec from the generator. Set the generator to nil so that future contributors // will quickly be able to tell that they're supposed to be modifying the spec directly from here. spec := g.Config g = nil // Set the seccomp configuration using the specified profile name. Some syscalls are // allowed if certain capabilities are to be granted (example: CAP_SYS_CHROOT and chroot), // so we sorted out the capabilities lists first. if err = setupSeccomp(spec, b.CommonBuildOpts.SeccompProfilePath); err != nil { return err } uid, gid := spec.Process.User.UID, spec.Process.User.GID if spec.Linux != nil { uid, gid, err = util.GetHostIDs(spec.Linux.UIDMappings, spec.Linux.GIDMappings, uid, gid) if err != nil { return err } } idPair := &idtools.IDPair{UID: int(uid), GID: int(gid)} mode := os.FileMode(0o755) coptions := copier.MkdirOptions{ ChownNew: idPair, ChmodNew: &mode, } if err := copier.Mkdir(mountPoint, filepath.Join(mountPoint, spec.Process.Cwd), coptions); err != nil { return err } bindFiles := make(map[string]string) volumes := b.Volumes() // Figure out who owns files that will appear to be owned by UID/GID 0 in the container. rootUID, rootGID, err := util.GetHostRootIDs(spec) if err != nil { return err } rootIDPair := &idtools.IDPair{UID: int(rootUID), GID: int(rootGID)} hostsFile := "" if !options.NoHosts && !slices.Contains(volumes, config.DefaultHostsFile) && options.ConfigureNetwork != define.NetworkDisabled { hostsFile, err = b.createHostsFile(path, rootIDPair) if err != nil { return err } bindFiles[config.DefaultHostsFile] = hostsFile // Only add entries here if we do not have to do setup network, // if we do we have to do it much later after the network setup. if !configureNetwork { var entries etchosts.HostEntries isHost := true if spec.Linux != nil { for _, ns := range spec.Linux.Namespaces { if ns.Type == specs.NetworkNamespace { isHost = false break } } } // add host entry for local ip when running in host network if spec.Hostname != "" && isHost { ip := netUtil.GetLocalIP() if ip != "" { entries = append(entries, etchosts.HostEntry{ Names: []string{spec.Hostname}, IP: ip, }) } } err = b.addHostsEntries(hostsFile, mountPoint, entries, nil, "") if err != nil { return err } } } if !options.NoHostname && !(slices.Contains(volumes, "/etc/hostname")) { hostnameFile, err := b.generateHostname(path, spec.Hostname, rootIDPair) if err != nil { return err } // Bind /etc/hostname bindFiles["/etc/hostname"] = hostnameFile } resolvFile := "" if !slices.Contains(volumes, resolvconf.DefaultResolvConf) && options.ConfigureNetwork != define.NetworkDisabled && (len(b.CommonBuildOpts.DNSServers) != 1 || strings.ToLower(b.CommonBuildOpts.DNSServers[0]) != "none") { resolvFile, err = b.createResolvConf(path, rootIDPair) if err != nil { return err } bindFiles[resolvconf.DefaultResolvConf] = resolvFile // Only add entries here if we do not have to do setup network, // if we do we have to do it much later after the network setup. if !configureNetwork { err = b.addResolvConfEntries(resolvFile, nil, spec, false, true) if err != nil { return err } } } // Empty file, so no need to recreate if it exists if _, ok := bindFiles["/run/.containerenv"]; !ok { containerenvPath := filepath.Join(path, "/run/.containerenv") if err = os.MkdirAll(filepath.Dir(containerenvPath), 0o755); err != nil { return err } rootless := 0 if unshare.IsRootless() { rootless = 1 } // Populate the .containerenv with container information containerenv := fmt.Sprintf(` engine="buildah-%s" name=%q id=%q image=%q imageid=%q rootless=%d `, define.Version, b.Container, b.ContainerID, b.FromImage, b.FromImageID, rootless) if err = ioutils.AtomicWriteFile(containerenvPath, []byte(containerenv), 0o755); err != nil { return err } if err := relabel(containerenvPath, b.MountLabel, false); err != nil { return err } bindFiles["/run/.containerenv"] = containerenvPath } // Setup OCI hooks _, err = b.setupOCIHooks(spec, (len(options.Mounts) > 0 || len(volumes) > 0)) if err != nil { return fmt.Errorf("unable to setup OCI hooks: %w", err) } runMountInfo := runMountInfo{ WorkDir: workDir, ContextDir: options.ContextDir, Secrets: options.Secrets, SSHSources: options.SSHSources, StageMountPoints: options.StageMountPoints, SystemContext: options.SystemContext, } runArtifacts, err = b.setupMounts(mountPoint, spec, path, options.Mounts, bindFiles, volumes, options.CompatBuiltinVolumes, b.CommonBuildOpts.Volumes, options.RunMounts, runMountInfo) if err != nil { return fmt.Errorf("resolving mountpoints for container %q: %w", b.ContainerID, err) } // Create any mount points that we need that aren't already present in // the rootfs. createdMountTargets, err := b.createMountTargets(spec) if err != nil { return fmt.Errorf("ensuring mount targets for container %q: %w", b.ContainerID, err) } defer func() { // Attempt to clean up mount targets for the sake of builds // that don't commit and rebase at each step, and people using // `buildah run` more than once, who don't expect empty mount // points to stick around. They'll still get filtered out at // commit-time if another concurrent Run() is keeping something // busy. if _, err := copier.ConditionalRemove(mountPoint, mountPoint, copier.ConditionalRemoveOptions{ UIDMap: b.store.UIDMap(), GIDMap: b.store.GIDMap(), Paths: createdMountTargets, }); err != nil { options.Logger.Errorf("unable to cleanup run mount targets %v", err) } }() // following run was called from `buildah run` // and some images were mounted for this run // add them to cleanup artifacts if len(options.ExternalImageMounts) > 0 { runArtifacts.MountedImages = append(runArtifacts.MountedImages, options.ExternalImageMounts...) } defer func() { if err := b.cleanupRunMounts(runArtifacts); err != nil { options.Logger.Errorf("unable to cleanup run mounts %v", err) } }() // Handle mount flags that request that the source locations for "bind" mountpoints be // relabeled, and filter those flags out of the list of mount options we pass to the // runtime. for i := range spec.Mounts { switch spec.Mounts[i].Type { default: continue case "bind", "rbind": // all good, keep going } zflag := "" for _, opt := range spec.Mounts[i].Options { if opt == "z" || opt == "Z" { zflag = opt } } if zflag == "" { continue } spec.Mounts[i].Options = slices.DeleteFunc(spec.Mounts[i].Options, func(opt string) bool { return opt == "z" || opt == "Z" }) if err := relabel(spec.Mounts[i].Source, b.MountLabel, zflag == "z"); err != nil { return fmt.Errorf("setting file label %q on %q: %w", b.MountLabel, spec.Mounts[i].Source, err) } } switch isolation { case define.IsolationOCI: var moreCreateArgs []string if options.NoPivot { moreCreateArgs = append(moreCreateArgs, "--no-pivot") } err = b.runUsingRuntimeSubproc(isolation, options, configureNetwork, networkString, moreCreateArgs, spec, mountPoint, path, define.Package+"-"+filepath.Base(path), b.Container, hostsFile, resolvFile) case IsolationChroot: err = chroot.RunUsingChroot(spec, path, homeDir, options.Stdin, options.Stdout, options.Stderr, options.NoPivot) case IsolationOCIRootless: moreCreateArgs := []string{"--no-new-keyring"} if options.NoPivot { moreCreateArgs = append(moreCreateArgs, "--no-pivot") } err = b.runUsingRuntimeSubproc(isolation, options, configureNetwork, networkString, moreCreateArgs, spec, mountPoint, path, define.Package+"-"+filepath.Base(path), b.Container, hostsFile, resolvFile) default: err = errors.New("don't know how to run this command") } return err } func (b *Builder) setupOCIHooks(config *specs.Spec, hasVolumes bool) (map[string][]specs.Hook, error) { allHooks := make(map[string][]specs.Hook) if len(b.CommonBuildOpts.OCIHooksDir) == 0 { if unshare.IsRootless() { return nil, nil } for _, hDir := range []string{hooks.DefaultDir, hooks.OverrideDir} { manager, err := hooks.New(context.Background(), []string{hDir}, []string{}) if err != nil { if errors.Is(err, os.ErrNotExist) { continue } return nil, err } ociHooks, err := manager.Hooks(config, b.ImageAnnotations, hasVolumes) if err != nil { return nil, err } if len(ociHooks) > 0 || config.Hooks != nil { logrus.Warnf("Implicit hook directories are deprecated; set --hooks-dir=%q explicitly to continue to load ociHooks from this directory", hDir) } maps.Copy(allHooks, ociHooks) } } else { manager, err := hooks.New(context.Background(), b.CommonBuildOpts.OCIHooksDir, []string{}) if err != nil { return nil, err } allHooks, err = manager.Hooks(config, b.ImageAnnotations, hasVolumes) if err != nil { return nil, err } } hookErr, err := hooksExec.RuntimeConfigFilter(context.Background(), allHooks["precreate"], config, hooksExec.DefaultPostKillTimeout) //nolint:staticcheck if err != nil { logrus.Warnf("Container: precreate hook: %v", err) if hookErr != nil && hookErr != err { logrus.Debugf("container: precreate hook (hook error): %v", hookErr) } return nil, err } return allHooks, nil } func addCommonOptsToSpec(commonOpts *define.CommonBuildOptions, g *generate.Generator) error { // Resources - CPU if commonOpts.CPUPeriod != 0 { g.SetLinuxResourcesCPUPeriod(commonOpts.CPUPeriod) } if commonOpts.CPUQuota != 0 { g.SetLinuxResourcesCPUQuota(commonOpts.CPUQuota) } if commonOpts.CPUShares != 0 { g.SetLinuxResourcesCPUShares(commonOpts.CPUShares) } if commonOpts.CPUSetCPUs != "" { g.SetLinuxResourcesCPUCpus(commonOpts.CPUSetCPUs) } if commonOpts.CPUSetMems != "" { g.SetLinuxResourcesCPUMems(commonOpts.CPUSetMems) } // Resources - Memory if commonOpts.Memory != 0 { g.SetLinuxResourcesMemoryLimit(commonOpts.Memory) } if commonOpts.MemorySwap != 0 { g.SetLinuxResourcesMemorySwap(commonOpts.MemorySwap) } // cgroup membership if commonOpts.CgroupParent != "" { g.SetLinuxCgroupsPath(commonOpts.CgroupParent) } defaultContainerConfig, err := config.Default() if err != nil { return fmt.Errorf("failed to get container config: %w", err) } // Other process resource limits if err := addRlimits(commonOpts.Ulimit, g, defaultContainerConfig.Containers.DefaultUlimits.Get()); err != nil { return err } logrus.Debugf("Resources: %#v", commonOpts) return nil } func setupSlirp4netnsNetwork(config *config.Config, netns, cid string, options, hostnames []string) (func(), *netResult, error) { // we need the TmpDir for the slirp4netns code if err := os.MkdirAll(config.Engine.TmpDir, 0o751); err != nil { return nil, nil, fmt.Errorf("failed to create tempdir: %w", err) } res, err := slirp4netns.Setup(&slirp4netns.SetupOptions{ Config: config, ContainerID: cid, Netns: netns, ExtraOptions: options, Pdeathsig: syscall.SIGKILL, }) if err != nil { return nil, nil, err } ip, err := slirp4netns.GetIP(res.Subnet) if err != nil { return nil, nil, fmt.Errorf("get slirp4netns ip: %w", err) } dns, err := slirp4netns.GetDNS(res.Subnet) if err != nil { return nil, nil, fmt.Errorf("get slirp4netns dns ip: %w", err) } result := &netResult{ entries: etchosts.HostEntries{{IP: ip.String(), Names: hostnames}}, dnsServers: []string{dns.String()}, ipv6: res.IPv6, keepHostResolvers: true, } return func() { syscall.Kill(res.Pid, syscall.SIGKILL) //nolint:errcheck var status syscall.WaitStatus syscall.Wait4(res.Pid, &status, 0, nil) //nolint:errcheck }, result, nil } func setupPasta(config *config.Config, netns string, options, hostnames []string) (func(), *netResult, error) { res, err := pasta.Setup(&pasta.SetupOptions{ Config: config, Netns: netns, ExtraOptions: options, }) if err != nil { return nil, nil, err } var entries etchosts.HostEntries if len(res.IPAddresses) > 0 { entries = etchosts.HostEntries{{IP: res.IPAddresses[0].String(), Names: hostnames}} } mappedIP := "" if len(res.MapGuestAddrIPs) > 0 { mappedIP = res.MapGuestAddrIPs[0] } result := &netResult{ entries: entries, dnsServers: res.DNSForwardIPs, excludeIPs: res.IPAddresses, ipv6: res.IPv6, keepHostResolvers: true, preferredHostContainersInternalIP: mappedIP, } return nil, result, nil } func (b *Builder) runConfigureNetwork(pid int, isolation define.Isolation, options RunOptions, network, containerName string, hostnames []string) (func(), *netResult, error) { netns := fmt.Sprintf("/proc/%d/ns/net", pid) var configureNetworks []string defConfig, err := config.Default() if err != nil { return nil, nil, fmt.Errorf("failed to get container config: %w", err) } name, networkOpts, hasOpts := strings.Cut(network, ":") var netOpts []string if hasOpts { netOpts = strings.Split(networkOpts, ",") } if isolation == IsolationOCIRootless && name == "" { switch defConfig.Network.DefaultRootlessNetworkCmd { case slirp4netns.BinaryName, "": name = slirp4netns.BinaryName case pasta.BinaryName: name = pasta.BinaryName default: return nil, nil, fmt.Errorf("invalid default_rootless_network_cmd option %q", defConfig.Network.DefaultRootlessNetworkCmd) } } switch { case name == slirp4netns.BinaryName: return setupSlirp4netnsNetwork(defConfig, netns, containerName, netOpts, hostnames) case name == pasta.BinaryName: return setupPasta(defConfig, netns, netOpts, hostnames) // Basically default case except we make sure to not split an empty // name as this would return a slice with one empty string which is // not a valid network name. case len(network) > 0: // old syntax allow comma separated network names configureNetworks = strings.Split(network, ",") } if isolation == IsolationOCIRootless { return nil, nil, errors.New("cannot use networks as rootless") } if len(configureNetworks) == 0 { configureNetworks = []string{b.NetworkInterface.DefaultNetworkName()} } // Make sure we can access the container's network namespace, // even after it exits, to successfully tear down the // interfaces. Ensure this by opening a handle to the network // namespace, and using our copy to both configure and // deconfigure it. netFD, err := unix.Open(netns, unix.O_RDONLY, 0) if err != nil { return nil, nil, fmt.Errorf("opening network namespace: %w", err) } mynetns := fmt.Sprintf("/proc/%d/fd/%d", unix.Getpid(), netFD) networks := make(map[string]nettypes.PerNetworkOptions, len(configureNetworks)) for i, network := range configureNetworks { networks[network] = nettypes.PerNetworkOptions{ InterfaceName: fmt.Sprintf("eth%d", i), } } opts := nettypes.NetworkOptions{ ContainerID: containerName, ContainerName: containerName, Networks: networks, } netStatus, err := b.NetworkInterface.Setup(mynetns, nettypes.SetupOptions{NetworkOptions: opts}) if err != nil { return nil, nil, err } teardown := func() { err := b.NetworkInterface.Teardown(mynetns, nettypes.TeardownOptions{NetworkOptions: opts}) if err != nil { options.Logger.Errorf("failed to cleanup network: %v", err) } } return teardown, netStatusToNetResult(netStatus, hostnames), nil } // Create pipes to use for relaying stdio. func runMakeStdioPipe(uid, gid int) ([][]int, error) { stdioPipe := make([][]int, 3) for i := range stdioPipe { stdioPipe[i] = make([]int, 2) if err := unix.Pipe(stdioPipe[i]); err != nil { return nil, fmt.Errorf("creating pipe for container FD %d: %w", i, err) } } if err := unix.Fchown(stdioPipe[unix.Stdin][0], uid, gid); err != nil { return nil, fmt.Errorf("setting owner of stdin pipe descriptor: %w", err) } if err := unix.Fchown(stdioPipe[unix.Stdout][1], uid, gid); err != nil { return nil, fmt.Errorf("setting owner of stdout pipe descriptor: %w", err) } if err := unix.Fchown(stdioPipe[unix.Stderr][1], uid, gid); err != nil { return nil, fmt.Errorf("setting owner of stderr pipe descriptor: %w", err) } return stdioPipe, nil } func setupNamespaces(_ *logrus.Logger, g *generate.Generator, namespaceOptions define.NamespaceOptions, idmapOptions define.IDMappingOptions, policy define.NetworkConfigurationPolicy) (configureNetwork bool, networkString string, configureUTS bool, err error) { defaultContainerConfig, err := config.Default() if err != nil { return false, "", false, fmt.Errorf("failed to get container config: %w", err) } addSysctl := func(prefixes []string) error { for _, sysctl := range defaultContainerConfig.Sysctls() { splitn := strings.SplitN(sysctl, "=", 2) if len(splitn) > 2 { return fmt.Errorf("sysctl %q defined in containers.conf must be formatted name=value", sysctl) } for _, prefix := range prefixes { if strings.HasPrefix(splitn[0], prefix) { g.AddLinuxSysctl(splitn[0], splitn[1]) } } } return nil } // Set namespace options in the container configuration. configureUserns := false specifiedNetwork := false for _, namespaceOption := range namespaceOptions { switch namespaceOption.Name { case string(specs.IPCNamespace): if !namespaceOption.Host { if err := addSysctl([]string{"fs.mqueue"}); err != nil { return false, "", false, err } } case string(specs.UserNamespace): configureUserns = false if !namespaceOption.Host && namespaceOption.Path == "" { configureUserns = true } case string(specs.NetworkNamespace): specifiedNetwork = true configureNetwork = false if !namespaceOption.Host && (namespaceOption.Path == "" || !filepath.IsAbs(namespaceOption.Path)) { if namespaceOption.Path != "" && !filepath.IsAbs(namespaceOption.Path) { networkString = namespaceOption.Path namespaceOption.Path = "" } configureNetwork = (policy != define.NetworkDisabled) } case string(specs.UTSNamespace): configureUTS = false if !namespaceOption.Host { if namespaceOption.Path == "" { configureUTS = true } if err := addSysctl([]string{"kernel.hostname", "kernel.domainame"}); err != nil { return false, "", false, err } } } if namespaceOption.Host { if err := g.RemoveLinuxNamespace(namespaceOption.Name); err != nil { return false, "", false, fmt.Errorf("removing %q namespace for run: %w", namespaceOption.Name, err) } } else if err := g.AddOrReplaceLinuxNamespace(namespaceOption.Name, namespaceOption.Path); err != nil { if namespaceOption.Path == "" { return false, "", false, fmt.Errorf("adding new %q namespace for run: %w", namespaceOption.Name, err) } return false, "", false, fmt.Errorf("adding %q namespace %q for run: %w", namespaceOption.Name, namespaceOption.Path, err) } } // If we've got mappings, we're going to have to create a user namespace. if len(idmapOptions.UIDMap) > 0 || len(idmapOptions.GIDMap) > 0 || configureUserns { if err := g.AddOrReplaceLinuxNamespace(string(specs.UserNamespace), ""); err != nil { return false, "", false, fmt.Errorf("adding new %q namespace for run: %w", string(specs.UserNamespace), err) } hostUidmap, hostGidmap, err := unshare.GetHostIDMappings("") if err != nil { return false, "", false, err } for _, m := range idmapOptions.UIDMap { g.AddLinuxUIDMapping(m.HostID, m.ContainerID, m.Size) } if len(idmapOptions.UIDMap) == 0 { for _, m := range hostUidmap { g.AddLinuxUIDMapping(m.ContainerID, m.ContainerID, m.Size) } } for _, m := range idmapOptions.GIDMap { g.AddLinuxGIDMapping(m.HostID, m.ContainerID, m.Size) } if len(idmapOptions.GIDMap) == 0 { for _, m := range hostGidmap { g.AddLinuxGIDMapping(m.ContainerID, m.ContainerID, m.Size) } } if !specifiedNetwork { if err := g.AddOrReplaceLinuxNamespace(string(specs.NetworkNamespace), ""); err != nil { return false, "", false, fmt.Errorf("adding new %q namespace for run: %w", string(specs.NetworkNamespace), err) } configureNetwork = (policy != define.NetworkDisabled) } } else { if err := g.RemoveLinuxNamespace(string(specs.UserNamespace)); err != nil { return false, "", false, fmt.Errorf("removing %q namespace for run: %w", string(specs.UserNamespace), err) } if !specifiedNetwork { if err := g.RemoveLinuxNamespace(string(specs.NetworkNamespace)); err != nil { return false, "", false, fmt.Errorf("removing %q namespace for run: %w", string(specs.NetworkNamespace), err) } } } if configureNetwork { if err := addSysctl([]string{"net"}); err != nil { return false, "", false, err } } return configureNetwork, networkString, configureUTS, nil } func (b *Builder) configureNamespaces(g *generate.Generator, options *RunOptions) (bool, string, error) { defaultNamespaceOptions, err := DefaultNamespaceOptions() if err != nil { return false, "", err } namespaceOptions := defaultNamespaceOptions namespaceOptions.AddOrReplace(b.NamespaceOptions...) namespaceOptions.AddOrReplace(options.NamespaceOptions...) networkPolicy := options.ConfigureNetwork // Nothing was specified explicitly so network policy should be inherited from builder if networkPolicy == NetworkDefault { networkPolicy = b.ConfigureNetwork // If builder policy was NetworkDisabled and // we want to disable network for this run. // reset options.ConfigureNetwork to NetworkDisabled // since it will be treated as source of truth later. if networkPolicy == NetworkDisabled { options.ConfigureNetwork = networkPolicy } } if networkPolicy == NetworkDisabled { namespaceOptions.AddOrReplace(define.NamespaceOptions{{Name: string(specs.NetworkNamespace), Host: false}}...) } configureNetwork, networkString, configureUTS, err := setupNamespaces(options.Logger, g, namespaceOptions, b.IDMappingOptions, networkPolicy) if err != nil { return false, "", err } if configureUTS { if options.Hostname != "" { g.SetHostname(options.Hostname) } else if b.Hostname() != "" { g.SetHostname(b.Hostname()) } else { hostname := stringid.TruncateID(b.ContainerID) defConfig, err := config.Default() if err != nil { return false, "", fmt.Errorf("failed to get container config: %w", err) } if defConfig.Containers.ContainerNameAsHostName { if mapped := mapContainerNameToHostname(b.Container); mapped != "" { hostname = mapped } } g.SetHostname(hostname) } } else { g.SetHostname("") } found := false spec := g.Config for i := range spec.Process.Env { if strings.HasPrefix(spec.Process.Env[i], "HOSTNAME=") { found = true break } } if !found { spec.Process.Env = append(spec.Process.Env, fmt.Sprintf("HOSTNAME=%s", spec.Hostname)) } return configureNetwork, networkString, nil } func runSetupBoundFiles(bundlePath string, bindFiles map[string]string) (mounts []specs.Mount) { for dest, src := range bindFiles { options := []string{"rbind"} if strings.HasPrefix(src, bundlePath) { options = append(options, bind.NoBindOption) } mounts = append(mounts, specs.Mount{ Source: src, Destination: dest, Type: "bind", Options: options, }) } return mounts } func addRlimits(ulimit []string, g *generate.Generator, defaultUlimits []string) error { var ( ul *units.Ulimit err error // setup rlimits nofileSet bool nprocSet bool ) ulimit = append(defaultUlimits, ulimit...) for _, u := range ulimit { if ul, err = butil.ParseUlimit(u); err != nil { return fmt.Errorf("ulimit option %q requires name=SOFT:HARD, failed to be parsed: %w", u, err) } if strings.ToUpper(ul.Name) == "NOFILE" { nofileSet = true } if strings.ToUpper(ul.Name) == "NPROC" { nprocSet = true } g.AddProcessRlimits("RLIMIT_"+strings.ToUpper(ul.Name), uint64(ul.Hard), uint64(ul.Soft)) } if !nofileSet { lim := define.RLimitDefaultValue var rlimit unix.Rlimit if err := unix.Getrlimit(unix.RLIMIT_NOFILE, &rlimit); err == nil { if lim < rlimit.Max || unshare.IsRootless() { lim = rlimit.Max } } else { logrus.Warnf("Failed to return RLIMIT_NOFILE ulimit %q", err) } g.AddProcessRlimits("RLIMIT_NOFILE", lim, lim) } if !nprocSet { lim := define.RLimitDefaultValue var rlimit unix.Rlimit if err := unix.Getrlimit(unix.RLIMIT_NPROC, &rlimit); err == nil { if lim < rlimit.Max || unshare.IsRootless() { lim = rlimit.Max } } else { logrus.Warnf("Failed to return RLIMIT_NPROC ulimit %q", err) } g.AddProcessRlimits("RLIMIT_NPROC", lim, lim) } return nil } func (b *Builder) runSetupVolumeMounts(mountLabel string, volumeMounts []string, optionMounts []specs.Mount, idMaps IDMaps) (mounts []specs.Mount, overlayDirs []string, Err error) { // Make sure the overlay directory is clean before running containerDir, err := b.store.ContainerDirectory(b.ContainerID) if err != nil { return nil, nil, fmt.Errorf("looking up container directory for %s: %w", b.ContainerID, err) } if err := overlay.CleanupContent(containerDir); err != nil { return nil, nil, fmt.Errorf("cleaning up overlay content for %s: %w", b.ContainerID, err) } parseMount := func(mountType, host, container string, options []string) (specs.Mount, error) { var foundrw, foundro, foundz, foundZ, foundO, foundU bool var rootProp, upperDir, workDir string for _, opt := range options { switch opt { case "rw": foundrw = true case "ro": foundro = true case "z": foundz = true case "Z": foundZ = true case "O": foundO = true case "U": foundU = true case "private", "rprivate", "slave", "rslave", "shared", "rshared": rootProp = opt } if strings.HasPrefix(opt, "upperdir") { splitOpt := strings.SplitN(opt, "=", 2) if len(splitOpt) > 1 { upperDir = splitOpt[1] } } if strings.HasPrefix(opt, "workdir") { splitOpt := strings.SplitN(opt, "=", 2) if len(splitOpt) > 1 { workDir = splitOpt[1] } } } if !foundrw && !foundro { options = append(options, "rw") } if foundz { if err := relabel(host, mountLabel, true); err != nil { return specs.Mount{}, err } options = slices.DeleteFunc(options, func(o string) bool { return o == "z" }) } if foundZ { if err := relabel(host, mountLabel, false); err != nil { return specs.Mount{}, err } options = slices.DeleteFunc(options, func(o string) bool { return o == "Z" }) } if foundU { if err := chown.ChangeHostPathOwnership(host, true, idMaps.processUID, idMaps.processGID); err != nil { return specs.Mount{}, err } options = slices.DeleteFunc(options, func(o string) bool { return o == "U" }) } if foundO { if (upperDir != "" && workDir == "") || (workDir != "" && upperDir == "") { return specs.Mount{}, errors.New("if specifying upperdir then workdir must be specified or vice versa") } containerDir, err := b.store.ContainerDirectory(b.ContainerID) if err != nil { return specs.Mount{}, err } contentDir, err := overlay.TempDir(containerDir, idMaps.rootUID, idMaps.rootGID) if err != nil { return specs.Mount{}, fmt.Errorf("failed to create TempDir in the %s directory: %w", containerDir, err) } overlayOpts := overlay.Options{ RootUID: idMaps.rootUID, RootGID: idMaps.rootGID, UpperDirOptionFragment: upperDir, WorkDirOptionFragment: workDir, GraphOpts: slices.Clone(b.store.GraphOptions()), } overlayMount, err := overlay.MountWithOptions(contentDir, host, container, &overlayOpts) if err == nil { overlayDirs = append(overlayDirs, contentDir) } // If chown true, add correct ownership to the overlay temp directories. if err == nil && foundU { if err := chown.ChangeHostPathOwnership(contentDir, true, idMaps.processUID, idMaps.processGID); err != nil { return specs.Mount{}, err } } return overlayMount, err } if rootProp == "" { options = append(options, "private") } if mountType != "tmpfs" { mountType = "bind" options = append(options, "rbind") } return specs.Mount{ Destination: container, Type: mountType, Source: host, Options: options, }, nil } // Bind mount volumes specified for this particular Run() invocation for _, i := range optionMounts { logrus.Debugf("setting up mounted volume at %q", i.Destination) mount, err := parseMount(i.Type, i.Source, i.Destination, i.Options) if err != nil { return nil, nil, err } mounts = append(mounts, mount) } // Bind mount volumes given by the user when the container was created for _, i := range volumeMounts { var options []string spliti := parse.SplitStringWithColonEscape(i) if len(spliti) > 2 { options = strings.Split(spliti[2], ",") } options = append(options, "rbind") mount, err := parseMount("bind", spliti[0], spliti[1], options) if err != nil { return nil, nil, err } mounts = append(mounts, mount) } return mounts, overlayDirs, nil } func setupMaskedPaths(g *generate.Generator, opts *define.CommonBuildOptions) { if slices.Contains(opts.Unmasks, "all") { return } nextMaskedPath: for _, mp := range append(config.DefaultMaskedPaths(), opts.Masks...) { for _, unmask := range opts.Unmasks { match, err := filepath.Match(unmask, mp) if err != nil { logrus.Warnf("Invalid unmask pattern %q: %v", unmask, err) continue } if match { continue nextMaskedPath } } g.AddLinuxMaskedPaths(mp) } } func setupReadOnlyPaths(g *generate.Generator) { for _, rp := range config.DefaultReadOnlyPaths { g.AddLinuxReadonlyPaths(rp) } } func setupCapAdd(g *generate.Generator, caps ...string) error { for _, cap := range caps { if err := g.AddProcessCapabilityBounding(cap); err != nil { return fmt.Errorf("adding %q to the bounding capability set: %w", cap, err) } if err := g.AddProcessCapabilityEffective(cap); err != nil { return fmt.Errorf("adding %q to the effective capability set: %w", cap, err) } if err := g.AddProcessCapabilityPermitted(cap); err != nil { return fmt.Errorf("adding %q to the permitted capability set: %w", cap, err) } } return nil } func setupCapDrop(g *generate.Generator, caps ...string) error { for _, cap := range caps { if err := g.DropProcessCapabilityBounding(cap); err != nil { return fmt.Errorf("removing %q from the bounding capability set: %w", cap, err) } if err := g.DropProcessCapabilityEffective(cap); err != nil { return fmt.Errorf("removing %q from the effective capability set: %w", cap, err) } if err := g.DropProcessCapabilityPermitted(cap); err != nil { return fmt.Errorf("removing %q from the permitted capability set: %w", cap, err) } } return nil } func setupCapabilities(g *generate.Generator, defaultCapabilities, adds, drops []string) error { g.ClearProcessCapabilities() if err := setupCapAdd(g, defaultCapabilities...); err != nil { return err } for _, c := range adds { if strings.ToLower(c) == "all" { adds = capabilities.AllCapabilities() break } } for _, c := range drops { if strings.ToLower(c) == "all" { g.ClearProcessCapabilities() return nil } } if err := setupCapAdd(g, adds...); err != nil { return err } return setupCapDrop(g, drops...) } func addOrReplaceMount(mounts []specs.Mount, mount specs.Mount) []specs.Mount { for i := range mounts { if mounts[i].Destination == mount.Destination { mounts[i] = mount return mounts } } return append(mounts, mount) } // setupSpecialMountSpecChanges creates special mounts for depending on the namespaces // logic taken from podman and adapted for buildah // https://github.com/containers/podman/blob/4ba71f955a944790edda6e007e6d074009d437a7/pkg/specgen/generate/oci.go#L178 func setupSpecialMountSpecChanges(spec *specs.Spec, shmSize string) ([]specs.Mount, error) { mounts := spec.Mounts isRootless := unshare.IsRootless() isNewUserns := false isNetns := false isPidns := false isIpcns := false for _, namespace := range spec.Linux.Namespaces { switch namespace.Type { case specs.NetworkNamespace: isNetns = true case specs.UserNamespace: isNewUserns = true case specs.PIDNamespace: isPidns = true case specs.IPCNamespace: isIpcns = true } } addCgroup := true // mount sys when root and no userns or when a new netns is created canMountSys := (!isRootless && !isNewUserns) || isNetns if !canMountSys { addCgroup = false sys := "/sys" sysMnt := specs.Mount{ Destination: sys, Type: "bind", Source: sys, Options: []string{bind.NoBindOption, "rprivate", "nosuid", "noexec", "nodev", "ro", "rbind"}, } mounts = addOrReplaceMount(mounts, sysMnt) } gid5Available := true if isRootless { _, gids, err := unshare.GetHostIDMappings("") if err != nil { return nil, err } gid5Available = checkIDsGreaterThan5(gids) } if gid5Available && len(spec.Linux.GIDMappings) > 0 { gid5Available = checkIDsGreaterThan5(spec.Linux.GIDMappings) } if !gid5Available { // If we have no GID mappings, the gid=5 default option would fail, so drop it. devPts := specs.Mount{ Destination: "/dev/pts", Type: "devpts", Source: "devpts", Options: []string{"rprivate", "nosuid", "noexec", "newinstance", "ptmxmode=0666", "mode=0620"}, } mounts = addOrReplaceMount(mounts, devPts) } isUserns := isNewUserns || isRootless if isUserns && !isIpcns { devMqueue := "/dev/mqueue" devMqueueMnt := specs.Mount{ Destination: devMqueue, Type: "bind", Source: devMqueue, Options: []string{bind.NoBindOption, "bind", "nosuid", "noexec", "nodev"}, } mounts = addOrReplaceMount(mounts, devMqueueMnt) } if isUserns && !isPidns { proc := "/proc" procMount := specs.Mount{ Destination: proc, Type: "bind", Source: proc, Options: []string{bind.NoBindOption, "rbind", "nosuid", "noexec", "nodev"}, } mounts = addOrReplaceMount(mounts, procMount) } if addCgroup { cgroupMnt := specs.Mount{ Destination: "/sys/fs/cgroup", Type: "cgroup", Source: "cgroup", Options: []string{"rprivate", "nosuid", "noexec", "nodev", "relatime", "rw"}, } mounts = addOrReplaceMount(mounts, cgroupMnt) } // if userns and host ipc bind mount shm if isUserns && !isIpcns { // bind mount /dev/shm when it exists if err := fileutils.Exists("/dev/shm"); err == nil { shmMount := specs.Mount{ Source: "/dev/shm", Type: "bind", Destination: "/dev/shm", Options: []string{bind.NoBindOption, "rbind", "nosuid", "noexec", "nodev"}, } mounts = addOrReplaceMount(mounts, shmMount) } } else if shmSize != "" { shmMount := specs.Mount{ Source: "shm", Destination: "/dev/shm", Type: "tmpfs", Options: []string{"private", "nodev", "noexec", "nosuid", "mode=1777", "size=" + shmSize}, } mounts = addOrReplaceMount(mounts, shmMount) } return mounts, nil } func checkIDsGreaterThan5(ids []specs.LinuxIDMapping) bool { for _, r := range ids { if r.ContainerID <= 5 && 5 < r.ContainerID+r.Size { return true } } return false } // Returns a Mount to add to the runtime spec's list of mounts, the ID of an // image, the path to a mounted filesystem, and the path to an overlay // filesystem, and an optional lock, or an error. // // The caller is expected to, after the command which uses the mount exits, // clean up the overlay filesystem (if we returned one), unmount the mounted // filesystem (if we provided the path to its mountpoint) and remove its // mountpoint, unmount the image (if we mounted one), and release the lock (if // we took one). func (b *Builder) getCacheMount(tokens []string, sys *types.SystemContext, stageMountPoints map[string]internal.StageMountDetails, idMaps IDMaps, workDir, tmpDir string) (*specs.Mount, string, string, string, *lockfile.LockFile, error) { var optionMounts []specs.Mount optionMount, mountedImage, intermediateMount, overlayMount, targetLock, err := volumes.GetCacheMount(sys, tokens, b.store, b.MountLabel, stageMountPoints, idMaps.uidmap, idMaps.gidmap, workDir, tmpDir) if err != nil { return nil, "", "", "", nil, err } succeeded := false defer func() { if !succeeded { if overlayMount != "" { if err := overlay.RemoveTemp(overlayMount); err != nil { b.Logger.Debug(err.Error()) } } if intermediateMount != "" { if err := mount.Unmount(intermediateMount); err != nil { b.Logger.Debugf("unmounting %q: %v", intermediateMount, err) } if err := os.Remove(intermediateMount); err != nil { b.Logger.Debugf("removing should-be-empty directory %q: %v", intermediateMount, err) } } if mountedImage != "" { if _, err := b.store.UnmountImage(mountedImage, false); err != nil { b.Logger.Debugf("unmounting image %q: %v", mountedImage, err) } } if targetLock != nil { targetLock.Unlock() } } }() optionMounts = append(optionMounts, optionMount) volumes, overlayDirs, err := b.runSetupVolumeMounts(b.MountLabel, nil, optionMounts, idMaps) if err != nil { return nil, "", "", "", nil, err } if len(overlayDirs) != 0 { return nil, "", "", "", nil, errors.New("internal error: did not expect a resolved cache mount to use the O flag") } succeeded = true return &volumes[0], mountedImage, intermediateMount, overlayMount, targetLock, nil } ================================================ FILE: run_test.go ================================================ package buildah import ( "errors" "fmt" "strings" "testing" "github.com/opencontainers/runtime-tools/generate" ) func TestAddRlimits(t *testing.T) { t.Parallel() tt := []struct { name string ulimit []string test func(error, *generate.Generator) error }{ { name: "empty ulimit", ulimit: []string{}, test: func(e error, _ *generate.Generator) error { return e }, }, { name: "invalid ulimit argument", ulimit: []string{"bla"}, test: func(e error, _ *generate.Generator) error { if e == nil { return errors.New("expected to receive an error but got nil") } errMsg := "invalid ulimit argument" if !strings.Contains(e.Error(), errMsg) { return fmt.Errorf("expected error message to include %#v in %#v", errMsg, e.Error()) } return nil }, }, { name: "invalid ulimit type", ulimit: []string{"bla=hard"}, test: func(e error, _ *generate.Generator) error { if e == nil { return errors.New("expected to receive an error but got nil") } errMsg := "invalid ulimit type" if !strings.Contains(e.Error(), errMsg) { return fmt.Errorf("expected error message to include %#v in %#v", errMsg, e.Error()) } return nil }, }, { name: "valid ulimit", ulimit: []string{"fsize=1024:4096"}, test: func(e error, g *generate.Generator) error { if e != nil { return e } rlimits := g.Config.Process.Rlimits for _, rlimit := range rlimits { if rlimit.Type == "RLIMIT_FSIZE" { if rlimit.Hard != 4096 { return fmt.Errorf("expected spec to have %#v hard limit set to %v but got %v", rlimit.Type, 4096, rlimit.Hard) } if rlimit.Soft != 1024 { return fmt.Errorf("expected spec to have %#v hard limit set to %v but got %v", rlimit.Type, 1024, rlimit.Soft) } return nil } } return errors.New("expected spec to have RLIMIT_FSIZE") }, }, } for _, tst := range tt { g, _ := generate.New("linux") err := addRlimits(tst.ulimit, &g, []string{}) if testErr := tst.test(err, &g); testErr != nil { t.Errorf("test %#v failed: %v", tst.name, testErr) } } } ================================================ FILE: run_unix.go ================================================ //go:build darwin package buildah import ( "errors" "github.com/containers/buildah/define" "github.com/opencontainers/runtime-spec/specs-go" nettypes "go.podman.io/common/libnetwork/types" "go.podman.io/storage" ) // ContainerDevices is an alias for a slice of github.com/opencontainers/runc/libcontainer/configs.Device structures. type ContainerDevices define.ContainerDevices func setChildProcess() error { return errors.New("function not supported on non-linux systems") } func runUsingRuntimeMain() {} func (b *Builder) Run(command []string, options RunOptions) error { return errors.New("function not supported on non-linux systems") } func DefaultNamespaceOptions() (NamespaceOptions, error) { options := NamespaceOptions{ {Name: string(specs.CgroupNamespace), Host: false}, {Name: string(specs.IPCNamespace), Host: false}, {Name: string(specs.MountNamespace), Host: false}, {Name: string(specs.NetworkNamespace), Host: false}, {Name: string(specs.PIDNamespace), Host: false}, {Name: string(specs.UserNamespace), Host: false}, {Name: string(specs.UTSNamespace), Host: false}, } return options, nil } // getNetworkInterface creates the network interface func getNetworkInterface(store storage.Store) (nettypes.ContainerNetwork, error) { return nil, nil } ================================================ FILE: run_unsupported.go ================================================ //go:build !linux && !darwin && !freebsd package buildah import ( "errors" nettypes "go.podman.io/common/libnetwork/types" "go.podman.io/storage" ) func setChildProcess() error { return errors.New("function not supported on non-linux systems") } func runUsingRuntimeMain() {} func (b *Builder) Run(command []string, options RunOptions) error { return errors.New("function not supported on non-linux systems") } func DefaultNamespaceOptions() (NamespaceOptions, error) { return NamespaceOptions{}, errors.New("function not supported on non-linux systems") } // getNetworkInterface creates the network interface func getNetworkInterface(store storage.Store) (nettypes.ContainerNetwork, error) { return nil, errors.New("function not supported on non-linux systems") } ================================================ FILE: scan.go ================================================ package buildah import ( "context" "fmt" "io" "os" "path/filepath" "slices" "strings" "github.com/containers/buildah/define" "github.com/containers/buildah/internal/sbom" "github.com/mattn/go-shellwords" rspec "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" ) func stringSliceReplaceAll(slice []string, replacements map[string]string, important []string) (built []string, replacedAnImportantValue bool) { built = make([]string, 0, len(slice)) for i := range slice { element := slice[i] for from, to := range replacements { previous := element if element = strings.ReplaceAll(previous, from, to); element != previous { if len(important) == 0 || slices.Contains(important, from) { replacedAnImportantValue = true } } } built = append(built, element) } return built, replacedAnImportantValue } // sbomScan iterates through the scanning configuration settings, generating // SBOM files and storing them either in the rootfs or in a local file path. func (b *Builder) sbomScan(ctx context.Context, options CommitOptions) (imageFiles, localFiles map[string]string, scansDir string, err error) { // We'll use a temporary per-container directory for this one. cdir, err := b.store.ContainerDirectory(b.ContainerID) if err != nil { return nil, nil, "", err } scansDir, err = os.MkdirTemp(cdir, "buildah-scan") if err != nil { return nil, nil, "", err } defer func() { if err != nil { if err := os.RemoveAll(scansDir); err != nil { logrus.Warnf("removing temporary directory %q: %v", scansDir, err) } } }() scansSubdir := filepath.Join(scansDir, "scans") if err = os.Mkdir(scansSubdir, 0o700); err != nil { return nil, nil, "", err } if err = os.Chmod(scansSubdir, 0o777); err != nil { return nil, nil, "", err } // We may be producing sets of outputs using temporary containers, and // there's no need to create more than one container for any one // specific scanner image. scanners := make(map[string]*Builder) defer func() { for _, scanner := range scanners { scannerID := scanner.ContainerID if err := scanner.Delete(); err != nil { logrus.Warnf("removing temporary scanner container %q: %v", scannerID, err) } } }() // Just assume that every scanning method will be looking at the rootfs. rootfs, err := b.Mount(b.MountLabel) if err != nil { return nil, nil, "", err } defer func(b *Builder) { if err := b.Unmount(); err != nil { logrus.Warnf("unmounting temporary scanner container %q: %v", b.ContainerID, err) } }(b) // Iterate through all of the scanning strategies. for _, scanSpec := range options.SBOMScanOptions { // Pull the image and create a container we can run the scanner // in, unless we've done that already for this scanner image. scanBuilder, ok := scanners[scanSpec.Image] if !ok { builderOptions := BuilderOptions{ FromImage: scanSpec.Image, ContainerSuffix: "scanner", PullPolicy: scanSpec.PullPolicy, BlobDirectory: options.BlobDirectory, Logger: b.Logger, SystemContext: options.SystemContext, MountLabel: b.MountLabel, ProcessLabel: b.ProcessLabel, IDMappingOptions: &b.IDMappingOptions, } if scanBuilder, err = NewBuilder(ctx, b.store, builderOptions); err != nil { return nil, nil, "", fmt.Errorf("creating temporary working container to run scanner: %w", err) } scanners[scanSpec.Image] = scanBuilder } // Now figure out which commands we need to run. First, try to // parse a command ourselves, because syft's image (at least) // doesn't include a shell. Build a slice of command slices. var commands [][]string for _, commandSpec := range scanSpec.Commands { // Start by assuming it's shell -c $whatever. parsedCommand := []string{"/bin/sh", "-c", commandSpec} if shell := scanBuilder.Shell(); len(shell) != 0 { parsedCommand = append(slices.Clone(shell), commandSpec) } if !strings.ContainsAny(commandSpec, "<>|") { // An imperfect check for shell redirection being used. // If we can parse it ourselves, though, prefer to use that result, // in case the scanner image doesn't include a shell. if parsed, err := shellwords.Parse(commandSpec); err == nil { parsedCommand = parsed } } commands = append(commands, parsedCommand) } // Set up a list of mounts for the rootfs and whichever context // directories we're told were used. const rootfsTargetDir = "/.rootfs" const scansTargetDir = "/.scans" const contextsTargetDirPrefix = "/.context" runMounts := []rspec.Mount{ // Our temporary directory, read-write. { Type: define.TypeBind, Source: scansSubdir, Destination: scansTargetDir, Options: []string{"rw", "z"}, }, // The rootfs, read-only. { Type: define.TypeBind, Source: rootfs, Destination: rootfsTargetDir, Options: []string{"ro"}, }, } // Each context directory, also read-only. for i := range scanSpec.ContextDir { contextMount := rspec.Mount{ Type: define.TypeBind, Source: scanSpec.ContextDir[i], Destination: fmt.Sprintf("%s%d", contextsTargetDirPrefix, i), Options: []string{"ro"}, } runMounts = append(runMounts, contextMount) } // Set up run options and mounts one time, and reuse it. runOptions := RunOptions{ Logger: b.Logger, Isolation: b.Isolation, SystemContext: options.SystemContext, Mounts: runMounts, } // We'll have to do some text substitutions so that we run the // right commands, in the right order, pointing at the right // mount points. var resolvedCommands [][]string var resultFiles []string for _, command := range commands { // Each command gets to produce its own file that we'll // combine later if there's more than one of them. contextDirScans := 0 for i := range scanSpec.ContextDir { resultFile := filepath.Join(scansTargetDir, fmt.Sprintf("scan%d.json", len(resultFiles))) // If the command mentions {CONTEXT}... resolvedCommand, scansContext := stringSliceReplaceAll(command, map[string]string{ "{CONTEXT}": fmt.Sprintf("%s%d", contextsTargetDirPrefix, i), "{OUTPUT}": resultFile, }, []string{"{CONTEXT}"}, ) if !scansContext { break } // ... resolve the path references and add it to the list of commands. resolvedCommands = append(resolvedCommands, resolvedCommand) resultFiles = append(resultFiles, resultFile) contextDirScans++ } if contextDirScans == 0 { resultFile := filepath.Join(scansTargetDir, fmt.Sprintf("scan%d.json", len(resultFiles))) // If the command didn't mention {CONTEXT}, but does mention {ROOTFS}... resolvedCommand, scansRootfs := stringSliceReplaceAll(command, map[string]string{ "{ROOTFS}": rootfsTargetDir, "{OUTPUT}": resultFile, }, []string{"{ROOTFS}"}, ) // ... resolve the path references and add that to the list of commands. if scansRootfs { resolvedCommands = append(resolvedCommands, resolvedCommand) resultFiles = append(resultFiles, resultFile) } } } // Run all of the commands, one after the other, producing one // or more files named "scan%d.json" in our temporary directory. for _, resolvedCommand := range resolvedCommands { logrus.Debugf("Running scan command %q", resolvedCommand) if err = scanBuilder.Run(resolvedCommand, runOptions); err != nil { return nil, nil, "", fmt.Errorf("running scanning command %v: %w", resolvedCommand, err) } } // Produce the combined output files that we need to create, if there are any. var sbomResult, purlResult string switch { case scanSpec.ImageSBOMOutput != "": sbomResult = filepath.Join(scansSubdir, filepath.Base(scanSpec.ImageSBOMOutput)) case scanSpec.SBOMOutput != "": sbomResult = filepath.Join(scansSubdir, filepath.Base(scanSpec.SBOMOutput)) default: sbomResult = filepath.Join(scansSubdir, "sbom-result") } switch { case scanSpec.ImagePURLOutput != "": purlResult = filepath.Join(scansSubdir, filepath.Base(scanSpec.ImagePURLOutput)) case scanSpec.PURLOutput != "": purlResult = filepath.Join(scansSubdir, filepath.Base(scanSpec.PURLOutput)) default: purlResult = filepath.Join(scansSubdir, "purl-result") } copyFile := func(destination, source string) error { dst, err := os.Create(destination) if err != nil { return err } defer dst.Close() src, err := os.Open(source) if err != nil { return err } defer src.Close() if _, err = io.Copy(dst, src); err != nil { return fmt.Errorf("copying %q to %q: %w", source, destination, err) } return nil } err = func() error { for i := range resultFiles { thisResultFile := filepath.Join(scansSubdir, filepath.Base(resultFiles[i])) switch i { case 0: // Straight-up copy to create the first version of the final output. if err = copyFile(sbomResult, thisResultFile); err != nil { return err } // This shouldn't change any contents, but lets us generate the purl file. err = sbom.Merge(scanSpec.MergeStrategy, thisResultFile, sbomResult, purlResult) default: // Hopefully we know how to merge information from the new one into the final output. err = sbom.Merge(scanSpec.MergeStrategy, sbomResult, thisResultFile, purlResult) } } return err }() if err != nil { return nil, nil, "", err } // If these files are supposed to be written to the local filesystem, add // their contents to the map of files we expect our caller to write. if scanSpec.SBOMOutput != "" || scanSpec.PURLOutput != "" { if localFiles == nil { localFiles = make(map[string]string) } if scanSpec.SBOMOutput != "" { localFiles[scanSpec.SBOMOutput] = sbomResult } if scanSpec.PURLOutput != "" { localFiles[scanSpec.PURLOutput] = purlResult } } // If these files are supposed to be written to the image, create a map of // their contents so that we can either create a layer diff for them (or // slipstream them into a squashed layer diff) later. if scanSpec.ImageSBOMOutput != "" || scanSpec.ImagePURLOutput != "" { if imageFiles == nil { imageFiles = make(map[string]string) } if scanSpec.ImageSBOMOutput != "" { imageFiles[scanSpec.ImageSBOMOutput] = sbomResult } if scanSpec.ImagePURLOutput != "" { imageFiles[scanSpec.ImagePURLOutput] = purlResult } } } return imageFiles, localFiles, scansDir, nil } ================================================ FILE: seccomp.go ================================================ //go:build seccomp && linux package buildah import ( "fmt" "os" "github.com/opencontainers/runtime-spec/specs-go" "go.podman.io/common/pkg/seccomp" ) func setupSeccomp(spec *specs.Spec, seccompProfilePath string) error { switch seccompProfilePath { case "unconfined": spec.Linux.Seccomp = nil case "": seccompConfig, err := seccomp.GetDefaultProfile(spec) if err != nil { return fmt.Errorf("loading default seccomp profile failed: %w", err) } spec.Linux.Seccomp = seccompConfig default: seccompProfile, err := os.ReadFile(seccompProfilePath) if err != nil { return fmt.Errorf("opening seccomp profile failed: %w", err) } seccompConfig, err := seccomp.LoadProfile(string(seccompProfile), spec) if err != nil { return fmt.Errorf("loading seccomp profile (%s) failed: %w", seccompProfilePath, err) } spec.Linux.Seccomp = seccompConfig } return nil } ================================================ FILE: seccomp_unsupported.go ================================================ //go:build !seccomp || !linux package buildah import ( "github.com/opencontainers/runtime-spec/specs-go" ) func setupSeccomp(spec *specs.Spec, seccompProfilePath string) error { if spec.Linux != nil { // runtime-tools may have supplied us with a default filter spec.Linux.Seccomp = nil } return nil } ================================================ FILE: selinux.go ================================================ //go:build linux package buildah import ( "errors" "fmt" "os" "github.com/opencontainers/runtime-tools/generate" selinux "github.com/opencontainers/selinux/go-selinux" ) func selinuxGetEnabled() bool { return selinux.GetEnabled() } func setupSelinux(g *generate.Generator, processLabel, mountLabel string) { if processLabel != "" && selinux.GetEnabled() { g.SetProcessSelinuxLabel(processLabel) g.SetLinuxMountLabel(mountLabel) } } func runLabelStdioPipes(stdioPipe [][]int, processLabel, mountLabel string) error { if !selinuxGetEnabled() || processLabel == "" || mountLabel == "" { // SELinux is completely disabled, or we're not doing anything at all with labeling return nil } pipeContext, err := selinux.ComputeCreateContext(processLabel, mountLabel, "fifo_file") if err != nil { return fmt.Errorf("computing file creation context for pipes: %w", err) } for i := range stdioPipe { pipeFdName := fmt.Sprintf("/proc/self/fd/%d", stdioPipe[i][0]) if err := selinux.SetFileLabel(pipeFdName, pipeContext); err != nil && !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("setting file label on %q: %w", pipeFdName, err) } } return nil } ================================================ FILE: selinux_unsupported.go ================================================ //go:build !linux package buildah import ( "github.com/opencontainers/runtime-tools/generate" ) func selinuxGetEnabled() bool { return false } func setupSelinux(g *generate.Generator, processLabel, mountLabel string) { } func runLabelStdioPipes(stdioPipe [][]int, processLabel, mountLabel string) error { return nil } ================================================ FILE: tests/NEW-IMAGES ================================================ # # As of July 2024, all CI VMs include a local registry prepopulated # with all container images used in tests: # # https://github.com/containers/automation_images/pull/357 # https://github.com/containers/podman/pull/22726 # https://github.com/containers/buildah/pull/5584 # # From time to time -- infrequently, we hope! -- existing images are # updated, or tests are added that require new images. Those must be # prefetched on each CI job, at least until new VMs are built. This # file contains those images. # # Format is one FQIN per line. Enumerate them below: # quay.io/hummingbird/git ================================================ FILE: tests/add.bats ================================================ #!/usr/bin/env bats load helpers @test "add-flags-order-verification" { run_buildah 125 add container1 -q /tmp/container1 check_options_flag_err "-q" run_buildah 125 add container1 --chown /tmp/container1 --quiet check_options_flag_err "--chown" run_buildah 125 add container1 /tmp/container1 --quiet check_options_flag_err "--quiet" } @test "add-local-plain" { createrandom ${TEST_SCRATCH_DIR}/randomfile createrandom ${TEST_SCRATCH_DIR}/other-randomfile run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah mount $cid root=$output mkdir $root/subdir $root/other-subdir # Copy a file to the working directory run_buildah config --workingdir=/ $cid run_buildah add --retry 4 --retry-delay 4s $cid ${TEST_SCRATCH_DIR}/randomfile # Copy a file to a specific subdirectory run_buildah add $cid ${TEST_SCRATCH_DIR}/randomfile /subdir # Copy two files to a specific subdirectory run_buildah add $cid ${TEST_SCRATCH_DIR}/randomfile ${TEST_SCRATCH_DIR}/other-randomfile /other-subdir # Copy two files to a specific location, which succeeds because we can create it as a directory. run_buildah add $cid ${TEST_SCRATCH_DIR}/randomfile ${TEST_SCRATCH_DIR}/other-randomfile /notthereyet-subdir # Copy two files to a specific location, which fails because it's not a directory. run_buildah 125 add $cid ${TEST_SCRATCH_DIR}/randomfile ${TEST_SCRATCH_DIR}/other-randomfile /randomfile # Copy a file to a different working directory run_buildah config --workingdir=/cwd $cid run_buildah add $cid ${TEST_SCRATCH_DIR}/randomfile run_buildah unmount $cid run_buildah commit $WITH_POLICY_JSON $cid containers-storage:new-image run_buildah rm $cid run_buildah from $WITH_POLICY_JSON new-image newcid=$output run_buildah mount $newcid newroot=$output test -s $newroot/randomfile cmp ${TEST_SCRATCH_DIR}/randomfile $newroot/randomfile test -s $newroot/subdir/randomfile cmp ${TEST_SCRATCH_DIR}/randomfile $newroot/subdir/randomfile test -s $newroot/other-subdir/randomfile cmp ${TEST_SCRATCH_DIR}/randomfile $newroot/other-subdir/randomfile test -s $newroot/other-subdir/other-randomfile cmp ${TEST_SCRATCH_DIR}/other-randomfile $newroot/other-subdir/other-randomfile test -d $newroot/cwd test -s $newroot/cwd/randomfile cmp ${TEST_SCRATCH_DIR}/randomfile $newroot/cwd/randomfile run_buildah rm $newcid } @test "add-local-archive" { createrandom ${TEST_SCRATCH_DIR}/randomfile createrandom ${TEST_SCRATCH_DIR}/other-randomfile run_buildah from $WITH_POLICY_JSON scratch cid=$output dd if=/dev/urandom bs=1024 count=4 of=${TEST_SCRATCH_DIR}/random1 dd if=/dev/urandom bs=1024 count=4 of=${TEST_SCRATCH_DIR}/random2 tar -c -C ${TEST_SCRATCH_DIR} -f ${TEST_SCRATCH_DIR}/tarball1.tar random1 random2 mkdir ${TEST_SCRATCH_DIR}/tarball2 dd if=/dev/urandom bs=1024 count=4 of=${TEST_SCRATCH_DIR}/tarball2/tarball2.random1 dd if=/dev/urandom bs=1024 count=4 of=${TEST_SCRATCH_DIR}/tarball2/tarball2.random2 tar -c -C ${TEST_SCRATCH_DIR} -z -f ${TEST_SCRATCH_DIR}/tarball2.tar.gz tarball2 mkdir ${TEST_SCRATCH_DIR}/tarball3 dd if=/dev/urandom bs=1024 count=4 of=${TEST_SCRATCH_DIR}/tarball3/tarball3.random1 dd if=/dev/urandom bs=1024 count=4 of=${TEST_SCRATCH_DIR}/tarball3/tarball3.random2 tar -c -C ${TEST_SCRATCH_DIR} -j -f ${TEST_SCRATCH_DIR}/tarball3.tar.bz2 tarball3 mkdir ${TEST_SCRATCH_DIR}/tarball4 dd if=/dev/urandom bs=1024 count=4 of=${TEST_SCRATCH_DIR}/tarball4/tarball4.random1 dd if=/dev/urandom bs=1024 count=4 of=${TEST_SCRATCH_DIR}/tarball4/tarball4.random2 tar -c -C ${TEST_SCRATCH_DIR} -j -f ${TEST_SCRATCH_DIR}/tarball4.tar.bz2 tarball4 # Add the files to the working directory, which should extract them all. run_buildah config --workingdir=/ $cid run_buildah add $cid ${TEST_SCRATCH_DIR}/tarball1.tar run_buildah add $cid ${TEST_SCRATCH_DIR}/tarball2.tar.gz run_buildah add $cid ${TEST_SCRATCH_DIR}/tarball3.tar.bz2 run_buildah add $cid ${TEST_SCRATCH_DIR}/tarball4.tar.bz2 run_buildah commit $WITH_POLICY_JSON $cid containers-storage:new-image run_buildah rm $cid run_buildah from $WITH_POLICY_JSON new-image newcid=$output run_buildah mount $newcid newroot=$output test -s $newroot/random1 cmp ${TEST_SCRATCH_DIR}/random1 $newroot/random1 test -s $newroot/random2 cmp ${TEST_SCRATCH_DIR}/random2 $newroot/random2 test -s $newroot/tarball2/tarball2.random1 cmp ${TEST_SCRATCH_DIR}/tarball2/tarball2.random1 $newroot/tarball2/tarball2.random1 test -s $newroot/tarball2/tarball2.random2 cmp ${TEST_SCRATCH_DIR}/tarball2/tarball2.random2 $newroot/tarball2/tarball2.random2 test -s $newroot/tarball3/tarball3.random1 cmp ${TEST_SCRATCH_DIR}/tarball3/tarball3.random1 $newroot/tarball3/tarball3.random1 test -s $newroot/tarball3/tarball3.random2 cmp ${TEST_SCRATCH_DIR}/tarball3/tarball3.random2 $newroot/tarball3/tarball3.random2 test -s $newroot/tarball4/tarball4.random1 cmp ${TEST_SCRATCH_DIR}/tarball4/tarball4.random1 $newroot/tarball4/tarball4.random1 test -s $newroot/tarball4/tarball4.random2 cmp ${TEST_SCRATCH_DIR}/tarball4/tarball4.random2 $newroot/tarball4/tarball4.random2 } @test "add single file creates absolute path with correct permissions" { _prefetch ubuntu imgName=ubuntu-image createrandom ${TEST_SCRATCH_DIR}/distutils.cfg permission=$(stat -c "%a" ${TEST_SCRATCH_DIR}/distutils.cfg) run_buildah from --quiet $WITH_POLICY_JSON ubuntu cid=$output run_buildah add $cid ${TEST_SCRATCH_DIR}/distutils.cfg /usr/lib/python3.7/distutils run_buildah run $cid stat -c "%a" /usr/lib/python3.7/distutils expect_output $permission run_buildah commit $WITH_POLICY_JSON $cid containers-storage:${imgName} run_buildah rm $cid run_buildah from --quiet $WITH_POLICY_JSON ${imgName} newcid=$output run_buildah run $newcid stat -c "%a" /usr/lib/python3.7/distutils expect_output $permission } @test "add single file creates relative path with correct permissions" { _prefetch ubuntu imgName=ubuntu-image createrandom ${TEST_SCRATCH_DIR}/distutils.cfg permission=$(stat -c "%a" ${TEST_SCRATCH_DIR}/distutils.cfg) run_buildah from --quiet $WITH_POLICY_JSON ubuntu cid=$output run_buildah add $cid ${TEST_SCRATCH_DIR}/distutils.cfg lib/custom run_buildah run $cid stat -c "%a" lib/custom expect_output $permission run_buildah commit $WITH_POLICY_JSON $cid containers-storage:${imgName} run_buildah rm $cid run_buildah from --quiet $WITH_POLICY_JSON ${imgName} newcid=$output run_buildah run $newcid stat -c "%a" lib/custom expect_output $permission } @test "add with chown" { _prefetch busybox createrandom ${TEST_SCRATCH_DIR}/randomfile run_buildah from --quiet $WITH_POLICY_JSON busybox cid=$output run_buildah add --chown bin:bin $cid ${TEST_SCRATCH_DIR}/randomfile /tmp/random run_buildah run $cid ls -l /tmp/random expect_output --substring bin.*bin } @test "add with chmod" { _prefetch busybox createrandom ${TEST_SCRATCH_DIR}/randomfile run_buildah from --quiet $WITH_POLICY_JSON busybox cid=$output run_buildah add --chmod 777 $cid ${TEST_SCRATCH_DIR}/randomfile /tmp/random run_buildah run $cid ls -l /tmp/random expect_output --substring rwxrwxrwx } @test "add url" { _prefetch busybox run_buildah from --quiet $WITH_POLICY_JSON busybox cid=$output run_buildah add $cid https://github.com/containers/buildah/raw/main/README.md run_buildah run $cid ls /README.md run_buildah add $cid https://github.com/containers/buildah/raw/main/README.md /home run_buildah run $cid ls /home/README.md } @test "add relative" { # make sure we don't get thrown by relative source locations _prefetch busybox run_buildah from --quiet $WITH_POLICY_JSON busybox cid=$output run_buildah add $cid deny.json / run_buildah run $cid ls /deny.json run_buildah add $cid ./docker.json / run_buildah run $cid ls /docker.json run_buildah add $cid tools/Makefile / run_buildah run $cid ls /Makefile } @test "add --ignorefile" { mytest=${TEST_SCRATCH_DIR}/mytest mkdir -p ${mytest} touch ${mytest}/mystuff touch ${mytest}/source.go mkdir -p ${mytest}/notmystuff touch ${mytest}/notmystuff/notmystuff cat > ${mytest}/.ignore << _EOF *.go .ignore notmystuff _EOF expect=" stuff stuff/mystuff" run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah 125 copy --ignorefile ${mytest}/.ignore $cid ${mytest} /stuff expect_output -- "Error: --ignorefile option requires that you specify a context dir using --contextdir" "container file list" run_buildah add --contextdir=${mytest} --ignorefile ${mytest}/.ignore $cid ${mytest} /stuff run_buildah mount $cid mnt=$output run find $mnt -printf "%P\n" filelist=$(LC_ALL=C sort <<<"$output") run_buildah umount $cid expect_output --from="$filelist" "$expect" "container file list" } @test "add quietly" { _prefetch busybox createrandom ${TEST_SCRATCH_DIR}/randomfile run_buildah from --quiet $WITH_POLICY_JSON busybox cid=$output run_buildah add --quiet $cid ${TEST_SCRATCH_DIR}/randomfile /tmp/random expect_output "" run_buildah mount $cid croot=$output cmp ${TEST_SCRATCH_DIR}/randomfile ${croot}/tmp/random } @test "add from container" { _prefetch busybox createrandom ${TEST_SCRATCH_DIR}/randomfile run_buildah from --quiet $WITH_POLICY_JSON busybox from=$output run_buildah from --quiet $WITH_POLICY_JSON busybox cid=$output run_buildah add --quiet $from ${TEST_SCRATCH_DIR}/randomfile /tmp/random expect_output "" run_buildah add --quiet $WITH_POLICY_JSON --from $from $cid /tmp/random /tmp/random # absolute path expect_output "" run_buildah add --quiet $WITH_POLICY_JSON --from $from $cid tmp/random /tmp/random2 # relative path expect_output "" run_buildah mount $cid croot=$output cmp ${TEST_SCRATCH_DIR}/randomfile ${croot}/tmp/random cmp ${TEST_SCRATCH_DIR}/randomfile ${croot}/tmp/random2 } @test "add from image" { _prefetch busybox ubuntu run_buildah from --quiet $WITH_POLICY_JSON busybox cid=$output run_buildah add --quiet $WITH_POLICY_JSON --from ubuntu $cid /etc/passwd /tmp/passwd # should pull the image, absolute path expect_output "" run_buildah add --quiet $WITH_POLICY_JSON --from ubuntu $cid etc/passwd /tmp/passwd2 # relative path expect_output "" run_buildah from --quiet $WITH_POLICY_JSON ubuntu ubuntu=$output run_buildah mount $cid croot=$output run_buildah mount $ubuntu ubuntu=$output cmp $ubuntu/etc/passwd ${croot}/tmp/passwd cmp $ubuntu/etc/passwd ${croot}/tmp/passwd2 } @test "add url with checksum flag" { _prefetch busybox run_buildah from --quiet $WITH_POLICY_JSON busybox cid=$output run_buildah add --checksum=sha256:4fd3aed66b5488b45fe83dd11842c2324fadcc38e1217bb45fbd28d660afdd39 $cid https://raw.githubusercontent.com/containers/buildah/bf3b55ba74102cc2503eccbaeffe011728d46b20/README.md / run_buildah run $cid ls /README.md } @test "add url with bad checksum" { _prefetch busybox run_buildah from --quiet $WITH_POLICY_JSON busybox cid=$output run_buildah 125 add --checksum=sha256:0000000000000000000000000000000000000000000000000000000000000000 $cid https://raw.githubusercontent.com/containers/buildah/bf3b55ba74102cc2503eccbaeffe011728d46b20/README.md / expect_output --substring "unexpected response digest for \"https://raw.githubusercontent.com/containers/buildah/bf3b55ba74102cc2503eccbaeffe011728d46b20/README.md\": sha256:4fd3aed66b5488b45fe83dd11842c2324fadcc38e1217bb45fbd28d660afdd39, want sha256:0000000000000000000000000000000000000000000000000000000000000000" } @test "add path with checksum flag" { _prefetch busybox createrandom ${TEST_SCRATCH_DIR}/randomfile run_buildah from --quiet $WITH_POLICY_JSON busybox cid=$output run_buildah 125 add --checksum=sha256:0000000000000000000000000000000000000000000000000000000000000000 $cid ${TEST_SCRATCH_DIR}/randomfile / expect_output --substring "checksum flag is not supported for local sources" } @test "add https retry ca" { createrandom ${TEST_SCRATCH_DIR}/randomfile mkdir -p ${TEST_SCRATCH_DIR}/private starthttpd ${TEST_SCRATCH_DIR} "" ${TEST_SCRATCH_DIR}/localhost.crt ${TEST_SCRATCH_DIR}/private/localhost.key run_buildah from --quiet scratch cid=$output run_buildah add --retry-delay=0.142857s --retry=14 --cert-dir ${TEST_SCRATCH_DIR} $cid https://localhost:${HTTP_SERVER_PORT}/randomfile run_buildah add --retry-delay=0.142857s --retry=14 --tls-verify=false $cid https://localhost:${HTTP_SERVER_PORT}/randomfile run_buildah 125 add --retry-delay=0.142857s --retry=14 $cid https://localhost:${HTTP_SERVER_PORT}/randomfile assert "$output" =~ "x509: certificate signed by unknown authority" stophttpd run_buildah 125 add --retry-delay=0.142857s --retry=14 --cert-dir ${TEST_SCRATCH_DIR} $cid https://localhost:${HTTP_SERVER_PORT}/randomfile assert "$output" =~ "retrying in 142.*ms .*14/14.*" } @test "add file with IMA xattr" { if ! getfattr -d -n 'security.ima' /usr/libexec/catatonit/catatonit | grep -q ima; then skip "catatonit does not have IMA xattr, cannot perform test" fi run_buildah from --quiet scratch cid=$output # We do not care if the attribute was actually added, as rootless is allowed to discard it. # Only that the add was actually successful. run_buildah add $cid /usr/libexec/catatonit/catatonit /catatonit } @test "add-with-timestamp" { _prefetch busybox url=https://raw.githubusercontent.com/containers/buildah/main/tests/bud/from-scratch/Dockerfile timestamp=60 mkdir -p $TEST_SCRATCH_DIR/context createrandom $TEST_SCRATCH_DIR/context/randomfile1 createrandom $TEST_SCRATCH_DIR/context/randomfile2 run_buildah from -q busybox cid="$output" # Add the content with more or less contemporary timestamps. run_buildah copy "$cid" $TEST_SCRATCH_DIR/context/randomfile* /default # Add a second copy that should get the same contemporary timestamps. run_buildah copy "$cid" $TEST_SCRATCH_DIR/context/randomfile* /default2 # Add a third copy that we explicitly force timestamps for. run_buildah copy --timestamp=$timestamp "$cid" $TEST_SCRATCH_DIR/context/randomfile* /explicit run_buildah add --timestamp=$timestamp "$cid" "$url" /explicit # Add a fourth copy that we forced the timestamps for out of band. cp -v "${BUDFILES}"/from-scratch/Dockerfile $TEST_SCRATCH_DIR/context/ tar -cf $TEST_SCRATCH_DIR/tarball -C $TEST_SCRATCH_DIR/context randomfile1 randomfile2 Dockerfile touch -d @$timestamp $TEST_SCRATCH_DIR/context/* run_buildah copy "$cid" $TEST_SCRATCH_DIR/context/* /touched # Add a fifth copy that we forced the timestamps for, from an archive. run_buildah add --timestamp=$timestamp "$cid" $TEST_SCRATCH_DIR/tarball /archive # Build the script to verify this inside of the rootfs. cat > $TEST_SCRATCH_DIR/context/check-dates.sh <<-EOF # Okay, at this point, default, default2, explicit, touched, and archive # should all contain randomfile1, randomfile2, and Dockerfile. # The copies in default and default2 should have contemporary timestamps for # the random files, and a server-supplied timestamp or the epoch for the # Dockerfile. # The copies in explicit, touched, and archive should all have the same # very old timestamps. touch -d @$timestamp /tmp/reference-file for f in /default/* /default2/* ; do if test \$f -ot /tmp/reference-file ; then echo expected \$f to be newer than /tmp/reference-file, but it was not ls -l \$f /tmp/reference-file exit 1 fi done for f in /explicit/* /touched/* /archive/* ; do if test \$f -nt /tmp/reference-file ; then echo expected \$f and /tmp/reference-file to have the same datestamp ls -l \$f /tmp/reference-file exit 1 fi if test \$f -ot /tmp/reference-file ; then echo expected \$f and /tmp/reference-file to have the same datestamp ls -l \$f /tmp/reference-file exit 1 fi done exit 0 EOF run_buildah copy --chmod=0755 "$cid" $TEST_SCRATCH_DIR/context/check-dates.sh / run_buildah run "$cid" sh -x /check-dates.sh } @test "add-link-flag" { createrandom ${TEST_SCRATCH_DIR}/randomfile createrandom ${TEST_SCRATCH_DIR}/other-randomfile run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah mount $cid root=$output run_buildah config --workingdir=/ $cid # Test 1: Simple add run_buildah add --link $cid ${TEST_SCRATCH_DIR}/randomfile # Test 2: Add with rename (file to file with different name) run_buildah add --link $cid ${TEST_SCRATCH_DIR}/randomfile /renamed-file # Test 3: Multiple files to directory mkdir $root/subdir run_buildah add --link $cid ${TEST_SCRATCH_DIR}/randomfile ${TEST_SCRATCH_DIR}/other-randomfile /subdir run_buildah unmount $cid run_buildah commit $WITH_POLICY_JSON $cid add-link-image run_buildah inspect --type=image add-link-image layers=$(echo "$output" | jq -r '.OCIv1.rootfs.diff_ids | length') if [ "$layers" -lt 3 ]; then echo "Expected at least 3 layers from 3 --link operations, but found $layers" echo "Layers found:" echo "$output" | jq -r '.OCIv1.rootfs.diff_ids[]' exit 1 fi run_buildah from $WITH_POLICY_JSON add-link-image newcid=$output run_buildah mount $newcid newroot=$output test -s $newroot/randomfile cmp ${TEST_SCRATCH_DIR}/randomfile $newroot/randomfile test -s $newroot/renamed-file cmp ${TEST_SCRATCH_DIR}/randomfile $newroot/renamed-file test -s $newroot/subdir/randomfile cmp ${TEST_SCRATCH_DIR}/randomfile $newroot/subdir/randomfile test -s $newroot/subdir/other-randomfile cmp ${TEST_SCRATCH_DIR}/other-randomfile $newroot/subdir/other-randomfile } @test "add-link-archive" { createrandom ${TEST_SCRATCH_DIR}/file1 createrandom ${TEST_SCRATCH_DIR}/file2 tar -c -C ${TEST_SCRATCH_DIR} -f ${TEST_SCRATCH_DIR}/archive.tar file1 file2 run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah config --workingdir=/ $cid run_buildah add --link $cid ${TEST_SCRATCH_DIR}/archive.tar run_buildah add --link $cid ${TEST_SCRATCH_DIR}/archive.tar /destdir/ run_buildah commit $WITH_POLICY_JSON $cid add-link-archive-image run_buildah inspect --type=image add-link-archive-image layers=$(echo "$output" | jq -r '.OCIv1.rootfs.diff_ids | length') if [ "$layers" -lt 2 ]; then echo "Expected at least 2 layers from 2 --link operations, but found $layers" exit 1 fi run_buildah from $WITH_POLICY_JSON add-link-archive-image newcid=$output run_buildah mount $newcid newroot=$output test -s $newroot/file1 cmp ${TEST_SCRATCH_DIR}/file1 $newroot/file1 test -s $newroot/file2 cmp ${TEST_SCRATCH_DIR}/file2 $newroot/file2 test -s $newroot/destdir/file1 cmp ${TEST_SCRATCH_DIR}/file1 $newroot/destdir/file1 test -s $newroot/destdir/file2 cmp ${TEST_SCRATCH_DIR}/file2 $newroot/destdir/file2 } @test "add-link-directory" { mkdir -p ${TEST_SCRATCH_DIR}/testdir/subdir createrandom ${TEST_SCRATCH_DIR}/testdir/file1 createrandom ${TEST_SCRATCH_DIR}/testdir/subdir/file2 run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah config --workingdir=/ $cid run_buildah add --link $cid ${TEST_SCRATCH_DIR}/testdir /testdir run_buildah commit $WITH_POLICY_JSON $cid add-link-dir-image run_buildah from $WITH_POLICY_JSON add-link-dir-image newcid=$output run_buildah mount $newcid newroot=$output test -d $newroot/testdir test -s $newroot/testdir/file1 test -s $newroot/testdir/subdir/file2 cmp ${TEST_SCRATCH_DIR}/testdir/file1 $newroot/testdir/file1 cmp ${TEST_SCRATCH_DIR}/testdir/subdir/file2 $newroot/testdir/subdir/file2 } ================================================ FILE: tests/authenticate.bats ================================================ #!/usr/bin/env bats load helpers @test "authenticate: login/logout" { start_registry testuserfoo testpassword run_buildah 0 login --cert-dir $REGISTRY_DIR --username testuserfoo --password testpassword localhost:$REGISTRY_PORT run_buildah 0 logout localhost:$REGISTRY_PORT } @test "authenticate: with stdin" { start_registry testuserfoo testpassword run_buildah 0 login localhost:$REGISTRY_PORT --cert-dir $REGISTRY_DIR --username testuserfoo --password-stdin <<< testpassword run_buildah 0 logout localhost:$REGISTRY_PORT } @test "authenticate: login/logout should succeed with XDG_RUNTIME_DIR unset" { unset XDG_RUNTIME_DIR start_registry testuserfoo testpassword run_buildah 0 login --cert-dir $REGISTRY_DIR --username testuserfoo --password testpassword localhost:$REGISTRY_PORT run_buildah 0 logout localhost:$REGISTRY_PORT } @test "authenticate: logout should fail with nonexistent authfile" { start_registry testuserfoo testpassword run_buildah 0 login --cert-dir $REGISTRY_DIR --username testuserfoo --password testpassword localhost:$REGISTRY_PORT run_buildah 125 logout --authfile /tmp/nonexistent localhost:$REGISTRY_PORT assert "$output" =~ "Error: credential file is not accessible: (faccessat|stat) /tmp/nonexistent: no such file or directory" run_buildah 125 logout --compat-auth-file /tmp/nonexistent localhost:$REGISTRY_PORT assert "$output" =~ "Error: credential file is not accessible: (faccessat|stat) /tmp/nonexistent: no such file or directory" run_buildah 0 logout localhost:$REGISTRY_PORT } @test "authenticate: logout should fail with inconsistent authfiles" { ambiguous_file=${TEST_SCRATCH_DIR}/ambiguous-auth.json echo '{}' > $ambiguous_file # To make sure we are not hitting the “file not found” path # We don’t start a real registry; login should never get that far. run_buildah 125 login --authfile "$ambiguous_file" --compat-auth-file "$ambiguous_file" localhost:5000 expect_output "Error: options for paths to the credential file and to the Docker-compatible credential file can not be set simultaneously" run_buildah 125 logout --authfile "$ambiguous_file" --compat-auth-file "$ambiguous_file" localhost:5000 expect_output "Error: options for paths to the credential file and to the Docker-compatible credential file can not be set simultaneously" } @test "authenticate: cert and credentials" { _prefetch alpine testuser="testuser$RANDOM" testpassword="testpassword$RANDOM" start_registry "$testuser" "$testpassword" # Basic test: should pass run_buildah push --cert-dir $REGISTRY_DIR $WITH_POLICY_JSON --tls-verify=false --creds "$testuser":"$testpassword" alpine localhost:$REGISTRY_PORT/my-alpine expect_output --substring "Writing manifest to image destination" # With tls-verify=true, should fail due to self-signed cert run_buildah 125 push $WITH_POLICY_JSON --tls-verify=true alpine localhost:$REGISTRY_PORT/my-alpine expect_output --substring " x509: certificate signed by unknown authority" \ "push with --tls-verify=true" # wrong credentials: should fail run_buildah 125 from --cert-dir $REGISTRY_DIR $WITH_POLICY_JSON --creds baduser:badpassword localhost:$REGISTRY_PORT/my-alpine expect_output --substring "authentication required" run_buildah 125 from --cert-dir $REGISTRY_DIR $WITH_POLICY_JSON --creds "$testuser":badpassword localhost:$REGISTRY_PORT/my-alpine expect_output --substring "authentication required" run_buildah 125 from --cert-dir $REGISTRY_DIR $WITH_POLICY_JSON --creds baduser:"$testpassword" localhost:$REGISTRY_PORT/my-alpine expect_output --substring "authentication required" # This should work run_buildah from --cert-dir $REGISTRY_DIR --name "my-alpine-work-ctr" $WITH_POLICY_JSON --creds "$testuser":"$testpassword" localhost:$REGISTRY_PORT/my-alpine expect_output --from="${lines[-1]}" "my-alpine-work-ctr" # Create Dockerfile for bud tests mkdir -p ${TEST_SCRATCH_DIR}/dockerdir DOCKERFILE=${TEST_SCRATCH_DIR}/dockerdir/Dockerfile /bin/cat <$DOCKERFILE FROM localhost:$REGISTRY_PORT/my-alpine EOM # Remove containers and images before bud tests run_buildah rm --all run_buildah rmi -f --all # bud test bad password should fail run_buildah 125 bud -f $DOCKERFILE $WITH_POLICY_JSON --tls-verify=false --creds="$testuser":badpassword expect_output --substring "authentication required" \ "buildah bud with wrong credentials" # bud test this should work run_buildah bud -f $DOCKERFILE $WITH_POLICY_JSON --tls-verify=false --creds="$testuser":"$testpassword" . expect_output --from="${lines[0]}" "STEP 1/1: FROM localhost:$REGISTRY_PORT/my-alpine" expect_output --substring "Writing manifest to image destination" } @test "authenticate: with --tls-verify=true" { _prefetch alpine start_registry # Push with correct credentials: should pass run_buildah push $WITH_POLICY_JSON --tls-verify=true --cert-dir=$REGISTRY_DIR --creds testuser:testpassword alpine localhost:$REGISTRY_PORT/my-alpine expect_output --substring "Writing manifest to image destination" # Push with wrong credentials: should fail run_buildah 125 push $WITH_POLICY_JSON --tls-verify=true --cert-dir=$REGISTRY_DIR --creds testuser:WRONGPASSWORD alpine localhost:$REGISTRY_PORT/my-alpine expect_output --substring "authentication required" # Make sure we can fetch it run_buildah from --pull-always --cert-dir=$REGISTRY_DIR --tls-verify=true --creds=testuser:testpassword localhost:$REGISTRY_PORT/my-alpine expect_output --from="${lines[-1]}" "localhost-working-container" cid="${lines[-1]}" # Commit with correct credentials run_buildah run $cid touch testfile run_buildah commit $WITH_POLICY_JSON --cert-dir=$REGISTRY_DIR --tls-verify=true --creds=testuser:testpassword $cid docker://localhost:$REGISTRY_PORT/my-alpine # Create Dockerfile for bud tests mkdir -p ${TEST_SCRATCH_DIR}/dockerdir DOCKERFILE=${TEST_SCRATCH_DIR}/dockerdir/Dockerfile /bin/cat <$DOCKERFILE FROM localhost:$REGISTRY_PORT/my-alpine RUN rm testfile EOM # Remove containers and images before bud tests run_buildah rm --all run_buildah rmi -f --all # bud with correct credentials run_buildah bud -f $DOCKERFILE $WITH_POLICY_JSON --cert-dir=$REGISTRY_DIR --tls-verify=true --creds=testuser:testpassword . expect_output --from="${lines[0]}" "STEP 1/2: FROM localhost:$REGISTRY_PORT/my-alpine" expect_output --substring "Writing manifest to image destination" } @test "authenticate: with cached (not command-line) credentials" { _prefetch alpine start_registry run_buildah 0 login --tls-verify=false --username testuser --password testpassword localhost:$REGISTRY_PORT expect_output "Login Succeeded!" # After login, push should pass run_buildah push $WITH_POLICY_JSON --tls-verify=false alpine localhost:$REGISTRY_PORT/my-alpine expect_output --substring "Writing manifest to image destination" run_buildah 125 login --tls-verify=false --username testuser --password WRONGPASSWORD localhost:$REGISTRY_PORT expect_output --substring 'logging into "localhost:'"$REGISTRY_PORT"'": invalid username/password' \ "buildah login, wrong credentials" run_buildah 0 logout localhost:$REGISTRY_PORT expect_output "Removed login credentials for localhost:$REGISTRY_PORT" run_buildah 125 push $WITH_POLICY_JSON --tls-verify=false alpine localhost:$REGISTRY_PORT/my-alpine expect_output --substring "authentication required" \ "buildah push after buildah logout" } ================================================ FILE: tests/basic.bats ================================================ #!/usr/bin/env bats load helpers @test "from" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah rm $cid run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah rm $cid run_buildah from --quiet --pull=false $WITH_POLICY_JSON --name i-love-naming-things alpine cid=$output run_buildah rm i-love-naming-things } @test "from-defaultpull" { _prefetch alpine run_buildah from --quiet $WITH_POLICY_JSON alpine cid=$output run_buildah rm $cid } @test "from-scratch" { run_buildah from --pull=false $WITH_POLICY_JSON scratch cid=$output run_buildah rm $cid run_buildah from --pull=true $WITH_POLICY_JSON scratch cid=$output run_buildah rm $cid } @test "from-nopull" { run_buildah 125 from --pull-never $WITH_POLICY_JSON alpine } @test "mount" { run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah mount $cid root=$output run_buildah unmount $cid run_buildah mount $cid root=$output touch $root/foobar run_buildah unmount $cid run_buildah rm $cid } @test "by-name" { run_buildah from $WITH_POLICY_JSON --name scratch-working-image-for-test scratch cid=$output run_buildah mount scratch-working-image-for-test root=$output run_buildah unmount scratch-working-image-for-test run_buildah rm scratch-working-image-for-test } @test "commit" { createrandom ${TEST_SCRATCH_DIR}/randomfile createrandom ${TEST_SCRATCH_DIR}/other-randomfile run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah mount $cid root=$output cp ${TEST_SCRATCH_DIR}/randomfile $root/randomfile run_buildah unmount $cid run_buildah commit --iidfile ${TEST_SCRATCH_DIR}/output.iid $WITH_POLICY_JSON $cid containers-storage:new-image iid=$(< ${TEST_SCRATCH_DIR}/output.iid) assert "$iid" =~ "sha256:[0-9a-f]{64}" run_buildah rmi $iid run_buildah commit $WITH_POLICY_JSON $cid containers-storage:new-image run_buildah rm $cid run_buildah from --quiet $WITH_POLICY_JSON new-image newcid=$output run_buildah mount $newcid newroot=$output test -s $newroot/randomfile cmp ${TEST_SCRATCH_DIR}/randomfile $newroot/randomfile cp ${TEST_SCRATCH_DIR}/other-randomfile $newroot/other-randomfile run_buildah commit $WITH_POLICY_JSON $newcid containers-storage:other-new-image # Not an allowed ordering of arguments and flags. Check that it's rejected. run_buildah 125 commit $newcid $WITH_POLICY_JSON containers-storage:rejected-new-image run_buildah commit $WITH_POLICY_JSON $newcid containers-storage:another-new-image run_buildah commit $WITH_POLICY_JSON $newcid yet-another-new-image run_buildah commit $WITH_POLICY_JSON $newcid containers-storage:gratuitous-new-image run_buildah unmount $newcid run_buildah rm $newcid run_buildah from --quiet $WITH_POLICY_JSON other-new-image othernewcid=$output run_buildah mount $othernewcid othernewroot=$output test -s $othernewroot/randomfile cmp ${TEST_SCRATCH_DIR}/randomfile $othernewroot/randomfile test -s $othernewroot/other-randomfile cmp ${TEST_SCRATCH_DIR}/other-randomfile $othernewroot/other-randomfile run_buildah rm $othernewcid run_buildah from --quiet $WITH_POLICY_JSON another-new-image anothernewcid=$output run_buildah mount $anothernewcid anothernewroot=$output test -s $anothernewroot/randomfile cmp ${TEST_SCRATCH_DIR}/randomfile $anothernewroot/randomfile test -s $anothernewroot/other-randomfile cmp ${TEST_SCRATCH_DIR}/other-randomfile $anothernewroot/other-randomfile run_buildah rm $anothernewcid run_buildah from --quiet $WITH_POLICY_JSON yet-another-new-image yetanothernewcid=$output run_buildah mount $yetanothernewcid yetanothernewroot=$output test -s $yetanothernewroot/randomfile cmp ${TEST_SCRATCH_DIR}/randomfile $yetanothernewroot/randomfile test -s $yetanothernewroot/other-randomfile cmp ${TEST_SCRATCH_DIR}/other-randomfile $yetanothernewroot/other-randomfile run_buildah delete $yetanothernewcid run_buildah from --quiet $WITH_POLICY_JSON new-image newcid=$output run_buildah commit --rm $WITH_POLICY_JSON $newcid containers-storage:remove-container-image run_buildah 125 mount $newcid run_buildah rmi remove-container-image run_buildah rmi containers-storage:other-new-image run_buildah rmi another-new-image run_buildah images -q assert "$output" != "" "images -q" run_buildah rmi -a run_buildah images -q expect_output "" } @test "release" { [[ "${RELEASE_TESTING:-false}" == "true" ]] || \ skip "Release testing may be enabled by setting \$RELEASE_TESTING = 'true'." run_buildah --version assert "$output" "!~" "dev" "The Buildah version string does not mention 'dev'." } ================================================ FILE: tests/blobcache.bats ================================================ #!/usr/bin/env bats load helpers # The 'rm cachefile' in the "blobs must be reused" test # causes flakes when parallelizing function setup_file() { export BATS_NO_PARALLELIZE_WITHIN_FILE=true } @test "blobcache-pull" { blobcachedir=${TEST_SCRATCH_DIR}/cache mkdir -p ${blobcachedir} # Pull an image using a fresh directory for the blob cache. run_buildah pull --blob-cache=${blobcachedir} $WITH_POLICY_JSON registry.k8s.io/pause # Check that we dropped some files in there. run find ${blobcachedir} -type f echo "$output" [ "$status" -eq 0 ] [ "${#lines[@]}" -gt 0 ] } @test "blobcache-from" { blobcachedir=${TEST_SCRATCH_DIR}/cache mkdir -p ${blobcachedir} # Pull an image using a fresh directory for the blob cache. run_buildah from --blob-cache=${blobcachedir} $WITH_POLICY_JSON registry.k8s.io/pause # Check that we dropped some files in there. run find ${blobcachedir} -type f echo "$output" [ "$status" -eq 0 ] [ "${#lines[*]}" -gt 0 ] } function _check_matches() { local destdir="$1" local blobcachedir="$2" # Look for layer blobs in the destination that match the ones in the cache. local matched=0 local unmatched=0 for content in ${destdir}/* ; do match=false for blob in ${blobcachedir}/* ; do if cmp -s ${content} ${blob} ; then echo $(file ${blob}) and ${content} have the same contents, was cached match=true break fi done if ${match} ; then matched=$(( ${matched} + 1 )) else unmatched=$(( ${unmatched} + 1 )) echo ${content} was not cached fi done expect_output --from="$matched" "$3" "$4 should match" expect_output --from="$unmatched" "$5" "$6 should not match" } # Integration test for https://github.com/containers/image/pull/1645 @test "blobcache: blobs must be reused when pushing across registry" { blobcachedir=${TEST_SCRATCH_DIR}/cachereuse mkdir -p ${blobcachedir} start_registry imgname=blobimg-$(random_string | tr A-Z a-z) run_buildah login --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth --username testuser --password testpassword localhost:${REGISTRY_PORT} outputdir=${TEST_SCRATCH_DIR}/outputdir mkdir -p ${outputdir} podman run --rm --mount type=bind,src=${TEST_SCRATCH_DIR}/test.auth,target=/test.auth,Z --mount type=bind,src=${outputdir},target=/output,Z --net host quay.io/skopeo/stable copy --preserve-digests --authfile=/test.auth --tls-verify=false docker://quay.io/libpod/alpine:3.10.2 dir:/output run_buildah rmi --all -f run_buildah pull --blob-cache=${blobcachedir} dir:${outputdir} run_buildah images -a --format '{{.ID}}' cid=$output run_buildah --log-level debug push --blob-cache=${blobcachedir} --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth $cid docker://localhost:${REGISTRY_PORT}/$imgname # must not contain "Skipping blob" since push must happen assert "$output" !~ "Skipping blob" # Clear local image and c/image's blob-info-cache run_buildah rmi --all -f cachedir=/var/lib if is_rootless; then cachedir=$HOME/.local/share fi run rm -rf $blobcachedir/* assert "$status" -eq 0 "status of `run rm $cachefile` must be 0" # In first push blob must be skipped after vendoring https://github.com/containers/image/pull/1645 run_buildah pull dir:${outputdir} run_buildah images -a --format '{{.ID}}' cid=$output run_buildah --log-level debug push --blob-cache=${blobcachedir} --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth $cid docker://localhost:${REGISTRY_PORT}/$imgname expect_output --substring "Skipping blob" } @test "blobcache-commit" { blobcachedir=${TEST_SCRATCH_DIR}/cache mkdir -p ${blobcachedir} # Pull an image using a fresh directory for the blob cache. run_buildah from --quiet --cidfile ${TEST_SCRATCH_DIR}/cid --blob-cache=${blobcachedir} $WITH_POLICY_JSON registry.k8s.io/pause ctr="$(< ${TEST_SCRATCH_DIR}/cid)" run_buildah add ${ctr} $BUDFILES/add-file/file / # Commit the image without using the blob cache, using compression so that uncompressed blobs # in the cache which we inherited from our base image won't be matched. doomeddir=${TEST_SCRATCH_DIR}/doomed mkdir -p ${doomeddir} run_buildah commit $WITH_POLICY_JSON --disable-compression=false ${ctr} dir:${doomeddir} _check_matches $doomeddir $blobcachedir \ 0 "nothing" \ 6 "everything" # Commit the image using the blob cache, again using compression. We'll have recorded the # compressed digests that match the uncompressed digests the last time around, so we should # get some matches this time. destdir=${TEST_SCRATCH_DIR}/dest mkdir -p ${destdir} ls -l ${blobcachedir} run_buildah commit $WITH_POLICY_JSON --blob-cache=${blobcachedir} --disable-compression=false ${ctr} dir:${destdir} _check_matches $destdir $blobcachedir \ 5 "base layers, new layer, config, and manifest" \ 1 "version" } @test "blobcache-push" { target=targetimage blobcachedir=${TEST_SCRATCH_DIR}/cache mkdir -p ${blobcachedir} # Pull an image using a fresh directory for the blob cache. run_buildah from --quiet --cidfile ${TEST_SCRATCH_DIR}/cid --blob-cache=${blobcachedir} $WITH_POLICY_JSON registry.k8s.io/pause ctr="$(< ${TEST_SCRATCH_DIR}/cid)" run_buildah add ${ctr} $BUDFILES/add-file/file / # Commit the image using the blob cache. ls -l ${blobcachedir} run_buildah commit $WITH_POLICY_JSON --blob-cache=${blobcachedir} --disable-compression=false ${ctr} ${target} # Try to push the image without the blob cache. doomeddir=${TEST_SCRATCH_DIR}/doomed mkdir -p ${doomeddir} ls -l ${blobcachedir} run_buildah push $WITH_POLICY_JSON ${target} dir:${doomeddir} _check_matches $doomeddir $blobcachedir \ 2 "only config and new layer" \ 4 "version, manifest, base layers" # Now try to push the image using the blob cache. destdir=${TEST_SCRATCH_DIR}/dest mkdir -p ${destdir} ls -l ${blobcachedir} run_buildah push $WITH_POLICY_JSON --blob-cache=${blobcachedir} ${target} dir:${destdir} _check_matches $destdir $blobcachedir \ 5 "base image layers, new layer, config, and manifest" \ 1 "version" } @test "blobcache-build-compressed-using-dockerfile-explicit-push" { blobcachedir=${TEST_SCRATCH_DIR}/cache mkdir -p ${blobcachedir} target=new-image # Build an image while pulling the base image. Compress the layers so that they get added # to the blob cache in their compressed forms. run_buildah build-using-dockerfile -t ${target} --pull-always $WITH_POLICY_JSON --blob-cache=${blobcachedir} --disable-compression=false $BUDFILES/add-file # Now try to push the image using the blob cache. The blob cache will only suggest the # compressed version of a blob if it's been told that we want to compress things, so # we also request compression here to avoid having the copy logic just compress the # uncompressed copy again. destdir=${TEST_SCRATCH_DIR}/dest mkdir -p ${destdir} run_buildah push $WITH_POLICY_JSON --blob-cache=${blobcachedir} --disable-compression=false ${target} dir:${destdir} _check_matches $destdir $blobcachedir \ 4 "config, base layer, new layer, and manifest" \ 1 "version" } @test "blobcache-build-uncompressed-using-dockerfile-explicit-push" { blobcachedir=${TEST_SCRATCH_DIR}/cache mkdir -p ${blobcachedir} target=new-image # Build an image while pulling the base image. run_buildah build-using-dockerfile -t ${target} -D --pull-always --blob-cache=${blobcachedir} $WITH_POLICY_JSON $BUDFILES/add-file # Now try to push the image using the blob cache. destdir=${TEST_SCRATCH_DIR}/dest mkdir -p ${destdir} run_buildah push $WITH_POLICY_JSON --blob-cache=${blobcachedir} ${target} dir:${destdir} _check_matches $destdir $blobcachedir \ 2 "config and previously-compressed base layer" \ 3 "version, new layer, and manifest" } @test "blobcache-build-compressed-using-dockerfile-implicit-push" { blobcachedir=${TEST_SCRATCH_DIR}/cache mkdir -p ${blobcachedir} target=new-image destdir=${TEST_SCRATCH_DIR}/dest mkdir -p ${destdir} # Build an image while pulling the base image, implicitly pushing while writing. run_buildah build-using-dockerfile -t dir:${destdir} --pull-always --blob-cache=${blobcachedir} $WITH_POLICY_JSON $BUDFILES/add-file _check_matches $destdir $blobcachedir \ 4 "base image, layer, config, and manifest" \ 1 "version" } @test "blobcache-build-uncompressed-using-dockerfile-implicit-push" { blobcachedir=${TEST_SCRATCH_DIR}/cache mkdir -p ${blobcachedir} target=new-image destdir=${TEST_SCRATCH_DIR}/dest mkdir -p ${destdir} # Build an image while pulling the base image, implicitly pushing while writing. run_buildah build-using-dockerfile -t dir:${destdir} -D --pull-always --blob-cache=${blobcachedir} $WITH_POLICY_JSON $BUDFILES/add-file _check_matches $destdir $blobcachedir \ 4 "base image, our layer, config, and manifest" \ 1 "version" } ================================================ FILE: tests/bud/add-checksum/Containerfile ================================================ FROM alpine ADD --checksum=sha256:4fd3aed66b5488b45fe83dd11842c2324fadcc38e1217bb45fbd28d660afdd39 https://raw.githubusercontent.com/containers/buildah/bf3b55ba74102cc2503eccbaeffe011728d46b20/README.md / ================================================ FILE: tests/bud/add-checksum/Containerfile.bad ================================================ FROM alpine ADD --checksum https://raw.githubusercontent.com/containers/buildah/bf3b55ba74102cc2503eccbaeffe011728d46b20/README.md / ================================================ FILE: tests/bud/add-checksum/Containerfile.bad-checksum ================================================ FROM alpine ADD --checksum=sha256:0000000000000000000000000000000000000000000000000000000000000000 https://raw.githubusercontent.com/containers/buildah/bf3b55ba74102cc2503eccbaeffe011728d46b20/README.md / ================================================ FILE: tests/bud/add-chmod/Dockerfile ================================================ FROM alpine ADD --chmod=777 addchmod.txt /tmp RUN ls -l /tmp/addchmod.txt CMD /bin/sh ================================================ FILE: tests/bud/add-chmod/Dockerfile.bad ================================================ FROM alpine ADD --chmod 777 addchmod.txt /tmp RUN ls -l /tmp/addchmod.txt CMD /bin/sh ================================================ FILE: tests/bud/add-chmod/Dockerfile.combined ================================================ FROM alpine ADD --chmod=777 --chown=2367:3267 addchmod.txt /tmp RUN stat -c "chmod:%a user:%u group:%g" /tmp/addchmod.txt CMD /bin/sh ================================================ FILE: tests/bud/add-chmod/addchmod.txt ================================================ File for testing ADD with chmod in a Dockerfile. ================================================ FILE: tests/bud/add-chown/Dockerfile ================================================ FROM alpine ADD --chown=2367:3267 addchown.txt /tmp RUN stat -c "user:%u group:%g" /tmp/addchown.txt CMD /bin/sh ================================================ FILE: tests/bud/add-chown/Dockerfile.bad ================================================ FROM alpine ADD --chown 2367:3267 addchown.txt /tmp RUN stat -c "user:%u group:%g" /tmp/addchown.txt CMD /bin/sh ================================================ FILE: tests/bud/add-chown/addchown.txt ================================================ File for testing COPY with chown in a Dockerfile. ================================================ FILE: tests/bud/add-create-absolute-path/Dockerfile ================================================ FROM ubuntu ADD distutils.cfg /usr/lib/python3.7/distutils/distutils.cfg RUN stat -c "permissions=%a" /usr/lib/python3.7/distutils ================================================ FILE: tests/bud/add-create-absolute-path/distutils.cfg ================================================ ================================================ FILE: tests/bud/add-create-relative-path/Dockerfile ================================================ FROM ubuntu COPY distutils.cfg lib/custom/distutils.cfg RUN stat -c "permissions=%a" lib/custom ================================================ FILE: tests/bud/add-create-relative-path/distutils.cfg ================================================ ================================================ FILE: tests/bud/add-file/Dockerfile ================================================ FROM busybox ADD file /var/www/ VOLUME /var/www ADD file /var/ VOLUME /var ADD file2 /var/ ================================================ FILE: tests/bud/add-file/file ================================================ ================================================ FILE: tests/bud/add-file/file2 ================================================ ================================================ FILE: tests/bud/add-run-dir/Dockerfile ================================================ FROM alpine RUN touch /run/hello ================================================ FILE: tests/bud/addtl-tags/Dockerfile ================================================ FROM busybox WORKDIR / ================================================ FILE: tests/bud/all-platform/Containerfile.default-arg ================================================ ARG foo=alpine FROM $foo ================================================ FILE: tests/bud/arg-scope/Containerfile.arg-mixed-defaults ================================================ FROM alpine ARG a b=set RUN echo "$a" "$b" > /out.txt ================================================ FILE: tests/bud/arg-scope/Containerfile.copy-from-arg ================================================ ARG SRC=a DST=b FROM busybox AS a RUN echo a > /from.txt FROM alpine AS b RUN echo b > /from.txt FROM scratch ARG SRC COPY --from=$SRC /from.txt / ================================================ FILE: tests/bud/arg-scope/Containerfile.from-arg ================================================ ARG BASE=alpine BUSY=busybox FROM $BASE ARG BASE RUN echo $BASE > /base.txt ================================================ FILE: tests/bud/arg-scope/Containerfile.multiple-args-one-step ================================================ FROM alpine ARG foo=0 bar=1 RUN echo $foo $bar > /out.txt ================================================ FILE: tests/bud/arg-scope/Containerfile.multiple-args-stage-only ================================================ FROM alpine ARG x=1 y=2 RUN echo $x $y > /out.txt ================================================ FILE: tests/bud/arg-scope/Containerfile.multiple-args-stage-overrides-one ================================================ ARG foo=0 bar=1 FROM alpine ARG foo=9 ARG bar RUN echo $foo $bar > /out.txt ================================================ FILE: tests/bud/arg-scope/Containerfile.stage-overrides-header ================================================ ARG FOO=header FROM busybox ARG FOO=stage RUN echo $FOO > /foo.txt ================================================ FILE: tests/bud/arg-scope/Containerfile.stage-overrides-header-copy-from ================================================ ARG SRC=a FROM busybox AS a RUN echo a > /from.txt FROM alpine AS b RUN echo b > /from.txt FROM scratch ARG SRC=b COPY --from=$SRC /from.txt / ================================================ FILE: tests/bud/base-with-arg/Containerfile ================================================ FROM --platform=$TARGETPLATFORM alpine AS build LABEL architecture=$TARGETARCH FROM build AS platform-amd64 ENV BUILT_FOR=amd64 FROM build AS platform-arm64 ENV BUILT_FOR=arm64 FROM build AS platform-386 ENV BUILT_FOR=386 FROM build AS platform-arm/v7 ENV BUILT_FOR=arm/v7 FROM build AS platform-arm/v6 ENV BUILT_FOR=arm/v6 FROM build AS platform-ppc64le ENV BUILT_FOR=ppc64le FROM build AS platform-s390x ENV BUILT_FOR=s390x FROM platform-${TARGETARCH} AS final RUN echo "This is built for ${BUILT_FOR}" ================================================ FILE: tests/bud/base-with-arg/Containerfile2 ================================================ ARG CUSTOM_TARGET FROM alpine as build FROM build AS platform-first ENV BUILT_FOR=first FROM build AS platform-second ENV BUILT_FOR=second FROM platform-${CUSTOM_TARGET} AS final RUN echo "This is built for ${BUILT_FOR}" ================================================ FILE: tests/bud/base-with-arg/Containerfilebad ================================================ FROM alpine as build FROM build AS platform-first ENV BUILT_FOR=first FROM build AS platform-second ENV BUILT_FOR=second # Should fail since we never declared CUSTOM_TARGET FROM platform-${CUSTOM_TARGET} AS final RUN echo "This is built for ${BUILT_FOR}" ================================================ FILE: tests/bud/base-with-arg/first.args ================================================ CUSTOM_TARGET=first ================================================ FILE: tests/bud/base-with-arg/second.args ================================================ CUSTOM_TARGET=second ================================================ FILE: tests/bud/base-with-labels/Containerfile ================================================ FROM quay.io/libpod/testimage:20241011 RUN echo hello ================================================ FILE: tests/bud/base-with-labels/Containerfile.layer ================================================ FROM quay.io/libpod/testimage:20241011 LABEL created_by world RUN echo world RUN echo hello ================================================ FILE: tests/bud/base-with-labels/Containerfile.multi-stage ================================================ FROM alpine as one LABEL created_by world FROM one RUN echo world ================================================ FILE: tests/bud/base-with-labels/Containerfile2 ================================================ FROM quay.io/libpod/testimage:20241011 RUN echo hello RUN echo world ================================================ FILE: tests/bud/bud.limits/Containerfile ================================================ # Containerfile FROM alpine RUN echo -n "Files="; awk '/open files/{print $4 "/" $5}' /proc/self/limits RUN echo -n "Processes="; awk '/processes/{print $3 "/" $4}' /proc/self/limits ================================================ FILE: tests/bud/build-arg/Dockerfile ================================================ FROM busybox ARG foo RUN echo $foo ================================================ FILE: tests/bud/build-arg/Dockerfile2 ================================================ ARG IMAGE=busybox FROM ${IMAGE} ================================================ FILE: tests/bud/build-arg/Dockerfile3 ================================================ FROM busybox MAINTAINER jdoe ENV container="docker" RUN echo this-should-be-cached-but-it-s-not ARG USERNAME ARG UID ARG CODE ARG PGDATA ARG PORT=55555 RUN echo this-should-not-be-cached-when-args-change CMD ["/container-run"] ================================================ FILE: tests/bud/build-arg/Dockerfile4 ================================================ FROM alpine ARG TEST ENV NAME=$TEST ================================================ FILE: tests/bud/build-with-from/Containerfile ================================================ # "fedora" is replaced as the base image at test-time using the --from flag FROM fedora as builder FROM busybox COPY --from=builder /bin/df /tmp/df_tester ================================================ FILE: tests/bud/buildkit-mount/Containerfile5 ================================================ FROM alpine ARG INPUTPATH_1=subdir RUN mkdir /test # use option z if selinux is enabled RUN --mount=type=bind,source=${INPUTPATH_1:?}/,target=/test,z cat /test/input_file ================================================ FILE: tests/bud/buildkit-mount/Dockerfile ================================================ FROM alpine RUN mkdir /test # use option z if selinux is enabled RUN --mount=type=bind,source=.,target=/test,z cat /test/input_file ================================================ FILE: tests/bud/buildkit-mount/Dockerfile2 ================================================ FROM alpine RUN mkdir /test # use option z if selinux is enabled RUN --mount=type=bind,target=/test,z cat /test/input_file ================================================ FILE: tests/bud/buildkit-mount/Dockerfile3 ================================================ FROM alpine RUN mkdir /test # use option z if selinux is enabled RUN --mount=type=bind,source=.,target=/test,z,rw echo world > /test/input_file && cat /test/input_file ================================================ FILE: tests/bud/buildkit-mount/Dockerfile4 ================================================ FROM alpine RUN mkdir /test # use option z if selinux is enabled RUN --mount=type=bind,source=subdir/,target=/test,z cat /test/input_file ================================================ FILE: tests/bud/buildkit-mount/Dockerfile6 ================================================ FROM alpine RUN mkdir /test # use option z if selinux is enabled RUN --mount=target=/test,z cat /test/input_file ================================================ FILE: tests/bud/buildkit-mount/Dockerfilecacheread ================================================ FROM alpine RUN mkdir /test # use option z if selinux is enabled RUN --mount=type=cache,target=/test,z cat /test/world ================================================ FILE: tests/bud/buildkit-mount/Dockerfilecachereadwithoutz ================================================ FROM alpine RUN mkdir /test2 RUN --mount=type=cache,target=/test2 cat /test2/world ================================================ FILE: tests/bud/buildkit-mount/Dockerfilecachewrite ================================================ FROM alpine RUN mkdir /test # use option z if selinux is enabled RUN --mount=type=cache,target=/test,z echo hello > /test/world ================================================ FILE: tests/bud/buildkit-mount/Dockerfilecachewritesharing ================================================ FROM alpine RUN mkdir /test # use option z if selinux is enabled # This locks cache RUN --mount=target=/test,type=cache,sharing=locked,z echo hello > /test/world && cat /test/world # Cache must be unlocked so it can be locked again RUN --mount=target=/test,sharing=locked,type=cache,z echo world > /test/world && cat /test/world ================================================ FILE: tests/bud/buildkit-mount/Dockerfilecachewritewithoutz ================================================ FROM alpine RUN mkdir /test2 RUN --mount=type=cache,target=/test2 echo hello > /test2/world ================================================ FILE: tests/bud/buildkit-mount/Dockerfilemultiplemounts ================================================ FROM alpine RUN mkdir /test # use option z if selinux is enabled # Order here is important, 'type=Bind' is the default, we want to make sure # it stays at it RUN --mount=type=cache,target=/test/cache,z \ --mount=source=input_file,target=/test/input_file,z \ cat /test/input_file ================================================ FILE: tests/bud/buildkit-mount/Dockerfiletmpfs ================================================ FROM alpine # As a baseline, this should succeed without creating any directory on container RUN --mount=type=tmpfs,target=/var/tmpfs-not-empty touch /var/tmpfs-not-empty/hello ================================================ FILE: tests/bud/buildkit-mount/Dockerfiletmpfscopyup ================================================ FROM alpine # As a baseline, this should succeed without creating any directory on container RUN --mount=type=tmpfs,target=/etc/ssl,tmpcopyup ls /etc/ssl ================================================ FILE: tests/bud/buildkit-mount/input_file ================================================ hello ================================================ FILE: tests/bud/buildkit-mount/subdir/input_file ================================================ hello ================================================ FILE: tests/bud/buildkit-mount-from/Dockerfilebindfrom ================================================ FROM alpine # use from= as mount source RUN --mount=type=bind,source=.,from=buildkitbase,target=/test cat /test/hello ================================================ FILE: tests/bud/buildkit-mount-from/Dockerfilebindfromrelative ================================================ FROM alpine RUN mkdir /test # use from= as mount source RUN --mount=type=bind,source=subdir,from=buildkitbaserelative,target=/test cat /test/hello ================================================ FILE: tests/bud/buildkit-mount-from/Dockerfilebindfromwithemptyfrom ================================================ FROM alpine RUN mkdir /test # use from= as mount source RUN --mount=type=bind,source=.,from=,target=/test cat /test/hello ================================================ FILE: tests/bud/buildkit-mount-from/Dockerfilebindfromwithoutsource ================================================ FROM alpine RUN mkdir /test # use from= as mount source RUN --mount=type=bind,from=buildkitbase,target=/test cat /test/hello ================================================ FILE: tests/bud/buildkit-mount-from/Dockerfilebindfromwriteable ================================================ FROM alpine # use from= as mount source RUN --mount=type=bind,source=.,from=buildkitbase,target=/test,rw echo "world" > /test/hello && cat /test/hello ================================================ FILE: tests/bud/buildkit-mount-from/Dockerfilebuildkitbase ================================================ FROM scratch COPY hello hello1 . COPY hello2 /subdir/hello ================================================ FILE: tests/bud/buildkit-mount-from/Dockerfilebuildkitbaserelative ================================================ FROM alpine RUN mkdir subdir COPY hello /subdir ================================================ FILE: tests/bud/buildkit-mount-from/Dockerfilecachefrom ================================================ FROM alpine as builder RUN mkdir subdir COPY hello . FROM alpine as second RUN mkdir /test # use another stage as cache source RUN --mount=type=cache,from=builder,target=/test cat /test/hello ================================================ FILE: tests/bud/buildkit-mount-from/Dockerfilecachefromimage ================================================ FROM alpine RUN mkdir /test # use another image as cache source RUN --mount=type=cache,from=buildkitbase,target=/test cat /test/hello # should be able to "write" to the cache RUN --mount=type=cache,from=buildkitbase,target=/test rm /test/hello # should be able to "write" to the cache again, since that last write was discarded RUN --mount=type=cache,from=buildkitbase,target=/test rm /test/hello ================================================ FILE: tests/bud/buildkit-mount-from/Dockerfilecachemultiplefrom ================================================ FROM alpine as builder COPY hello . FROM alpine as builder2 COPY hello2 . FROM alpine RUN mkdir /test # use other stages as cache source RUN --mount=type=cache,from=builder,target=/test --mount=type=cache,from=builder2,target=/test2 cat /test2/hello2 && cat /test/hello ================================================ FILE: tests/bud/buildkit-mount-from/Dockerfilemultistagefrom ================================================ FROM alpine as builder RUN mkdir subdir COPY hello ./subdir/ FROM alpine as second RUN --mount=type=bind,source=/subdir,from=builder,target=/test cat /test/hello ================================================ FILE: tests/bud/buildkit-mount-from/Dockerfilemultistagefromcache ================================================ FROM alpine as builder RUN mkdir subdir RUN mkdir subdir/subdir2 COPY hello ./subdir/ COPY hello2 ./subdir/subdir2/ FROM alpine as second RUN --mount=type=cache,id=1,source=/subdir,from=builder,target=/test cat /test/hello FROM alpine as third RUN --mount=type=cache,id=2,source=/subdir/subdir2,from=builder,target=/test cat /test/hello2 ================================================ FILE: tests/bud/buildkit-mount-from/Dockermultistagefrom ================================================ FROM alpine as builder RUN mkdir subdir COPY hello ./subdir/ FROM alpine as second RUN --mount=type=bind,source=/subdir,from=builder,target=/test cat /test/hello ================================================ FILE: tests/bud/buildkit-mount-from/hello ================================================ hello ================================================ FILE: tests/bud/buildkit-mount-from/hello1 ================================================ hello1 ================================================ FILE: tests/bud/buildkit-mount-from/hello2 ================================================ hello2 ================================================ FILE: tests/bud/cache-chown/Dockerfile.add1 ================================================ FROM scratch ADD --chown=1:1 testfile / ================================================ FILE: tests/bud/cache-chown/Dockerfile.add2 ================================================ FROM scratch ADD --chown=2:2 testfile / ================================================ FILE: tests/bud/cache-chown/Dockerfile.copy1 ================================================ FROM scratch COPY --chown=1:1 testfile / ================================================ FILE: tests/bud/cache-chown/Dockerfile.copy2 ================================================ FROM scratch COPY --chown=2:2 testfile / ================================================ FILE: tests/bud/cache-chown/Dockerfile.prev1 ================================================ FROM scratch COPY testfile renamedfile FROM scratch COPY --chown=1:1 --from=0 renamedfile / ================================================ FILE: tests/bud/cache-chown/Dockerfile.prev2 ================================================ FROM scratch COPY testfile renamedfile FROM scratch COPY --chown=2:2 --from=0 renamedfile / ================================================ FILE: tests/bud/cache-chown/Dockerfile.tar1 ================================================ FROM scratch # Surprise! The --chown flag is ignored when we're extracting archives. ADD --chown=1:1 testfile.tar.gz / ================================================ FILE: tests/bud/cache-chown/Dockerfile.tar2 ================================================ FROM scratch # Surprise! The --chown flag is ignored when we're extracting archives. ADD --chown=2:2 testfile.tar.gz / ================================================ FILE: tests/bud/cache-chown/Dockerfile.url1 ================================================ FROM scratch ARG HTTP_SERVER_PORT ADD --chown=1:1 http://0.0.0.0:${HTTP_SERVER_PORT}/README.md / ================================================ FILE: tests/bud/cache-chown/Dockerfile.url2 ================================================ FROM scratch ARG HTTP_SERVER_PORT ADD --chown=2:2 http://0.0.0.0:${HTTP_SERVER_PORT}/README.md / ================================================ FILE: tests/bud/cache-chown/testfile ================================================ Hi, I'm a test file. Enjoy the test. ================================================ FILE: tests/bud/cache-format/Dockerfile ================================================ FROM alpine COPY . . RUN pwd ================================================ FILE: tests/bud/cache-from/Containerfile ================================================ FROM alpine ARG VAR=hello RUN echo "Hello $VAR" ================================================ FILE: tests/bud/cache-from-stage/Containerfile ================================================ FROM scratch AS stage1 COPY / / FROM alpine RUN --mount=type=bind,from=stage1,target=/mnt echo hi > test ================================================ FILE: tests/bud/cache-from-stage/testfile ================================================ Hi, I'm a test file. Enjoy the test. ================================================ FILE: tests/bud/cache-mount-locked/Containerfile ================================================ FROM alpine ARG WIPE_CACHE COPY file . RUN --mount=type=cache,target=/cache1,sharing=locked \ --mount=type=cache,target=/cache2 \ set -ex; \ ls -l /cache1; \ if [[ -v WIPE_CACHE ]]; then \ >&2 echo "Wiping cache"; \ find /cache1 -mindepth 1 -delete; \ fi; \ echo "foo" > /cache1/foo.txt; \ ls -l /cache1; \ chmod -R g=u /cache1; \ : ; # Cache was wiped-out but lock should not hang here: https://github.com/containers/buildah/issues/4342 RUN --mount=type=cache,target=/cache1,sharing=locked cat file ================================================ FILE: tests/bud/cache-mount-locked/file ================================================ hello ================================================ FILE: tests/bud/cache-scratch/Dockerfile ================================================ FROM alpine as build FROM scratch COPY --from=build / / COPY --from=build / / ================================================ FILE: tests/bud/cache-scratch/Dockerfile.config ================================================ FROM alpine as build MAINTAINER root@localhost FROM scratch MAINTAINER root@localhost COPY --from=build / / MAINTAINER root@localhost COPY --from=build / / ================================================ FILE: tests/bud/cache-scratch/Dockerfile.different1 ================================================ FROM alpine as build FROM scratch COPY --from=build / / RUN touch cache-invalidating-difference COPY --from=build / / ================================================ FILE: tests/bud/cache-scratch/Dockerfile.different2 ================================================ FROM alpine as build RUN touch cache-invalidating-difference FROM scratch COPY --from=build / / COPY --from=build / / ================================================ FILE: tests/bud/cache-stages/Dockerfile.1 ================================================ FROM alpine AS builder RUN touch /tmpfile ================================================ FILE: tests/bud/cache-stages/Dockerfile.2 ================================================ FROM alpine AS builder RUN touch /tmpfile FROM alpine AS base COPY --from=builder /tmpfile / RUN stat /tmpfile ================================================ FILE: tests/bud/cache-stages/Dockerfile.3 ================================================ FROM alpine AS common RUN echo "common" > /common.txt FROM common AS buildA RUN echo "foo" > /foo.txt FROM common AS buildB # This is contrived to force a cached layer without having to build twice # Ordinarily you wouldn't have duplicate stages RUN echo "foo" > /foo.txt FROM alpine ARG NONCE RUN --mount=type=bind,from=buildA,target=/buildA \ --mount=type=bind,from=buildB,target=/buildB \ set -ex; \ cat /buildA/common.txt; \ cat /buildA/foo.txt; \ cat /buildB/common.txt; \ cat /buildB/foo.txt; \ echo "Worked"; \ : ; ================================================ FILE: tests/bud/capabilities/Dockerfile ================================================ FROM busybox USER 3267 RUN id; grep -i cap /proc/self/status ================================================ FILE: tests/bud/cdi/Dockerfile ================================================ FROM busybox RUN mkdir /etc/cdi RUN cp /dev/containers-cdi.yaml /etc/cdi/containers-cdi.yaml RUN wc /dev/outsidenull ================================================ FILE: tests/bud/cdi/containers-cdi.yaml ================================================ --- cdiVersion: 0.7.0 devices: - name: all containerEdits: mounts: - containerPath: /dev/containers-cdi.yaml hostPath: @@hostcdipath@@/containers-cdi.yaml options: - ro - nosuid - nodev - bind kind: containers.github.io/sample ================================================ FILE: tests/bud/check-race/Containerfile ================================================ FROM alpine RUN for i in $(seq 0 1000); do touch /$i; done ================================================ FILE: tests/bud/container-ignoresymlink/Dockerfile ================================================ FROM alpine COPY / /dir RUN ls /dir ================================================ FILE: tests/bud/container-ignoresymlink/hello ================================================ ================================================ FILE: tests/bud/container-ignoresymlink/world ================================================ ================================================ FILE: tests/bud/containeranddockerfile/Containerfile ================================================ FROM alpine ================================================ FILE: tests/bud/containeranddockerfile/Dockerfile ================================================ FROM busybox ================================================ FILE: tests/bud/containerfile/Containerfile ================================================ # This is for testing Containerfile with Buildah FROM alpine ================================================ FILE: tests/bud/containerfile/Containerfile.in ================================================ # include "Containerfile" RUN echo "success" #if TESTCPPDEBUG RUN echo "debug=yes" # else RUN echo "debug=no" #endif ================================================ FILE: tests/bud/containerignore/.containerignore ================================================ # comment * test* !test2* subdir !*/sub1* ================================================ FILE: tests/bud/containerignore/.dockerignore ================================================ # comment * ================================================ FILE: tests/bud/containerignore/Dockerfile ================================================ FROM alpine COPY ./ ./ COPY subdir ./ ================================================ FILE: tests/bud/containerignore/Dockerfile.succeed ================================================ FROM alpine COPY ./ ./ ================================================ FILE: tests/bud/containerignore/subdir/sub1.txt ================================================ ================================================ FILE: tests/bud/containerignore/subdir/sub2.txt ================================================ ================================================ FILE: tests/bud/containerignore/test1.txt ================================================ test1 failed ================================================ FILE: tests/bud/containerignore/test2.txt ================================================ test2 failed ================================================ FILE: tests/bud/context-escape-dir/testdir/Containerfile ================================================ FROM alpine COPY ../upperfile.txt / ================================================ FILE: tests/bud/context-escape-dir/upperfile.txt ================================================ This is a text file to be used in Buildah testing. This will be used to ensure that a file from above the context directory can not be copied during the build phase. ================================================ FILE: tests/bud/context-from-stdin/Dockerfile ================================================ FROM alpine as base RUN echo "stdin-context" > /scratchfile FROM scratch COPY --from=base /scratchfile / ================================================ FILE: tests/bud/copy-archive/Containerfile ================================================ FROM docker.io/busybox ADD test.tar.xz / ================================================ FILE: tests/bud/copy-chmod/Dockerfile ================================================ FROM alpine COPY --chmod=777 copychmod.txt /tmp RUN ls -l /tmp/copychmod.txt CMD /bin/sh ================================================ FILE: tests/bud/copy-chmod/Dockerfile.bad ================================================ FROM alpine COPY --chmod 777 copychmod.txt /tmp RUN ls -l /tmp/copychmod.txt CMD /bin/sh ================================================ FILE: tests/bud/copy-chmod/Dockerfile.combined ================================================ FROM alpine COPY --chmod=777 --chown=2367:3267 copychmod.txt /tmp RUN stat -c "chmod:%a user:%u group:%g" /tmp/copychmod.txt CMD /bin/sh ================================================ FILE: tests/bud/copy-chmod/copychmod.txt ================================================ File for testing COPY with chmod in a Dockerfile. ================================================ FILE: tests/bud/copy-chown/Containerfile.chown_user ================================================ ARG SAFEIMAGE FROM $SAFEIMAGE ENV MYUSER=myuser RUN adduser -D -h /"${MYUSER}" "${MYUSER}" COPY --chown="${MYUSER}" ./copychown.txt /somewhere RUN stat -c "%U:%G" /somewhere ================================================ FILE: tests/bud/copy-chown/Dockerfile ================================================ FROM alpine COPY --chown=2367:3267 copychown.txt /tmp RUN stat -c "user:%u group:%g" /tmp/copychown.txt CMD /bin/sh ================================================ FILE: tests/bud/copy-chown/Dockerfile.bad ================================================ FROM alpine COPY --chown 2367:3267 copychown.txt /tmp RUN stat -c "user:%u group:%g" /tmp/copychown.txt CMD /bin/sh ================================================ FILE: tests/bud/copy-chown/Dockerfile.bad2 ================================================ FROM alpine COPY --chown=${BOGUS}:${BOGUS} copychown.txt /tmp CMD /bin/sh ================================================ FILE: tests/bud/copy-chown/copychown.txt ================================================ File for testing COPY with chown in a Dockerfile. ================================================ FILE: tests/bud/copy-create-absolute-path/Dockerfile ================================================ FROM ubuntu COPY distutils.cfg /usr/lib/python3.7/distutils/distutils.cfg RUN stat -c "permissions=%a" /usr/lib/python3.7/distutils ================================================ FILE: tests/bud/copy-create-absolute-path/distutils.cfg ================================================ ================================================ FILE: tests/bud/copy-create-relative-path/Dockerfile ================================================ FROM ubuntu COPY distutils.cfg lib/custom/distutils.cfg RUN stat -c "permissions=%a" lib/custom ================================================ FILE: tests/bud/copy-create-relative-path/distutils.cfg ================================================ ================================================ FILE: tests/bud/copy-envvar/Containerfile ================================================ FROM alpine ENV VERSION=0.0.1 COPY file-${VERSION}.txt / ================================================ FILE: tests/bud/copy-envvar/file-0.0.1.txt ================================================ ================================================ FILE: tests/bud/copy-exclude/Containerfile ================================================ FROM alpine COPY --exclude=*txt . /testdir run ls /testdir ================================================ FILE: tests/bud/copy-exclude/Containerfile.missing ================================================ FROM alpine COPY --exclude=*foo --exclude=test*.txt . /testdir run ls /testdir ================================================ FILE: tests/bud/copy-exclude/test1.txt ================================================ ================================================ FILE: tests/bud/copy-exclude/test2.txt ================================================ ================================================ FILE: tests/bud/copy-from/Dockerfile ================================================ FROM busybox # DO NOT TOUCH THIS UNLESS YOU KNOW WHAT YOU'RE DOING! # renovatebot seems to want to clobber the image tag below. DO NOT LET IT DO SO. COPY --from=quay.io/libpod/testimage:20221018 /home/podman/testimage-id /home/busyboxpodman/copied-testimage-id ================================================ FILE: tests/bud/copy-from/Dockerfile.bad ================================================ FROM busybox COPY --from registry.com/missing-equals/causes:error /foo /bar ================================================ FILE: tests/bud/copy-from/Dockerfile2 ================================================ FROM busybox AS basis RUN echo hello > /newfile FROM basis RUN test -s /newfile ================================================ FILE: tests/bud/copy-from/Dockerfile2.bad ================================================ FROM busybox AS test USER 1001 FROM busybox AS build COPY --from=test /bin/cut /test/ COPY --from=build /bin/cp /test/ COPY --from=busybox /bin/paste /test/ ================================================ FILE: tests/bud/copy-from/Dockerfile3 ================================================ FROM docker.io/library/busybox AS build RUN rm -f /bin/paste USER 1001 COPY --from=docker.io/library/busybox /bin/paste /test/ ================================================ FILE: tests/bud/copy-from/Dockerfile4 ================================================ FROM docker.io/library/busybox AS test RUN rm -f /bin/nl FROM docker.io/library/alpine AS final COPY --from=docker.io/library/busybox /bin/nl /test/ ================================================ FILE: tests/bud/copy-from-stage-scoped-arg/Containerfile ================================================ FROM docker.io/library/busybox:latest AS build RUN echo "build" >> /variant FROM build AS dev RUN echo "build:dev" >> /variant FROM build AS staging RUN echo "build:staging" >> /variant FROM docker.io/library/busybox:latest ARG VARIANT="staging" COPY --from="${VARIANT}" /variant /variant ================================================ FILE: tests/bud/copy-from-stage-scoped-arg/Containerfile.multi-copy-arg-override ================================================ FROM docker.io/library/busybox:latest AS build RUN echo "build" > /variant FROM build AS dev RUN echo "build:dev" >> /variant FROM build AS staging RUN echo "build:staging" >> /variant FROM docker.io/library/busybox:latest ARG VARIANT="staging" COPY --from="${VARIANT}" /variant /variant_first ARG VARIANT="dev" COPY --from="${VARIANT}" /variant /variant_second ================================================ FILE: tests/bud/copy-globs/Containerfile ================================================ FROM scratch # *txt exists so should succeed COPY *.txt /testdir ================================================ FILE: tests/bud/copy-globs/Containerfile.bad ================================================ FROM scratch # No match so should fail COPY *foo /testdir ================================================ FILE: tests/bud/copy-globs/Containerfile.missing ================================================ FROM scratch # No match for *foo, but *txt exists so should succeed COPY *foo *.txt /testdir ================================================ FILE: tests/bud/copy-globs/Dockerfile ================================================ FROM busybox COPY --from=alpine /we/never/get/here /nor/here/either ================================================ FILE: tests/bud/copy-globs/test1.txt ================================================ ================================================ FILE: tests/bud/copy-globs/test2.txt ================================================ ================================================ FILE: tests/bud/copy-multiple-files/Dockerfile ================================================ FROM ubuntu COPY file file2 /var/www/ ADD file file2 /var/html/ ================================================ FILE: tests/bud/copy-multiple-files/file ================================================ ================================================ FILE: tests/bud/copy-multiple-files/file2 ================================================ ================================================ FILE: tests/bud/copy-multistage-paths/Dockerfile.absolute ================================================ FROM ubuntu as builder FROM ubuntu COPY --from=builder /bin/bash /my/bin/bash RUN stat -c "permissions=%a" /my/bin ================================================ FILE: tests/bud/copy-multistage-paths/Dockerfile.invalid_from ================================================ FROM ubuntu as builder FROM ubuntu COPY --from builder /bin/bash /my/bin/bash RUN stat -c "permissions=%a" /my/bin ================================================ FILE: tests/bud/copy-multistage-paths/Dockerfile.relative ================================================ FROM ubuntu as builder FROM ubuntu COPY --from=builder /bin/bash my/bin/bash RUN stat -c "permissions=%a" my/bin ================================================ FILE: tests/bud/copy-parents/Containerfile ================================================ FROM alpine COPY ./x/a.txt ./y/a.txt /no_parents/ COPY --parents ./x/a.txt ./y/a.txt /parents/ COPY --parents ./x/./y/* /parents_file_point/ COPY --parents ./x/./y/ /parents_dir_point/ RUN find /no_parents RUN find /parents RUN find /parents_file_point RUN find /parents_dir_point ================================================ FILE: tests/bud/copy-parents/Containerfile-hardlinks ================================================ FROM alpine COPY --parents ./x/ ./y/ / RUN ln -s /x/z/b.txt /x/z/symlink-b.txt RUN ln /x/z/a.txt /x/z/hardlink-a.txt RUN ln /x/y/b.txt /x/z/hardlink-y-b.txt FROM scratch COPY --from=0 --parents ./x/ /parents COPY --from=0 --parents ./x/./ /parents_dir_point/ ================================================ FILE: tests/bud/copy-parents/x/a.txt ================================================ A-FILE x ================================================ FILE: tests/bud/copy-parents/x/y/a.txt ================================================ A-FILE x/y ================================================ FILE: tests/bud/copy-parents/x/y/b.txt ================================================ Hello x/y ================================================ FILE: tests/bud/copy-parents/x/z/a.txt ================================================ A-FILE x/z ================================================ FILE: tests/bud/copy-parents/x/z/b.txt ================================================ Hello x/z ================================================ FILE: tests/bud/copy-parents/y/a.txt ================================================ A-FILE y ================================================ FILE: tests/bud/copy-parents/y/b.txt ================================================ Hello y ================================================ FILE: tests/bud/copy-root/Dockerfile ================================================ FROM ubuntu COPY distutils.cfg / ================================================ FILE: tests/bud/copy-root/distutils.cfg ================================================ ================================================ FILE: tests/bud/copy-workdir/Dockerfile ================================================ FROM scratch WORKDIR /subdir COPY file1.txt / COPY file2.txt /subdir ================================================ FILE: tests/bud/copy-workdir/Dockerfile.2 ================================================ FROM alpine WORKDIR /subdir COPY file1.txt /subdir RUN ls ================================================ FILE: tests/bud/copy-workdir/file1.txt ================================================ file1 ================================================ FILE: tests/bud/copy-workdir/file2.txt ================================================ file2 ================================================ FILE: tests/bud/dest-final-slash/Dockerfile ================================================ FROM busybox AS base FROM scratch COPY --from=base /bin/ls /test/ COPY --from=base /bin/sh /bin/ RUN /test/ls -lR /test/ls ================================================ FILE: tests/bud/dest-symlink/Dockerfile ================================================ FROM alpine ENV HBASE_HOME="/usr/local/hbase" ENV HBASE_CONF_DIR="/etc/hbase" RUN mkdir $HBASE_HOME RUN ln -s $HBASE_HOME $HBASE_CONF_DIR COPY Dockerfile $HBASE_CONF_DIR ================================================ FILE: tests/bud/dest-symlink-dangling/Dockerfile ================================================ FROM ubuntu RUN mkdir /symlink RUN ln -s /symlink /src && rm -rf /symlink COPY Dockerfile /src/ RUN test -s /symlink/Dockerfile ================================================ FILE: tests/bud/device/Dockerfile ================================================ FROM alpine RUN ls /dev/fuse ================================================ FILE: tests/bud/dns/Dockerfile ================================================ FROM alpine:latest RUN cat /etc/resolv.conf ================================================ FILE: tests/bud/dockerfile/Dockerfile ================================================ FROM alpine ================================================ FILE: tests/bud/dockerignore/.dockerignore ================================================ # comment * test* !test2* subdir !*/sub1* ================================================ FILE: tests/bud/dockerignore/Dockerfile ================================================ FROM alpine COPY ./ ./ COPY subdir ./ ================================================ FILE: tests/bud/dockerignore/Dockerfile.succeed ================================================ FROM alpine COPY ./ ./ ================================================ FILE: tests/bud/dockerignore/subdir/sub1.txt ================================================ ================================================ FILE: tests/bud/dockerignore/subdir/sub2.txt ================================================ ================================================ FILE: tests/bud/dockerignore/test1.txt ================================================ test1 failed ================================================ FILE: tests/bud/dockerignore/test2.txt ================================================ test2 failed ================================================ FILE: tests/bud/dockerignore2/.dockerignore ================================================ unmatched ================================================ FILE: tests/bud/dockerignore2/Dockerfile ================================================ FROM scratch COPY . . ================================================ FILE: tests/bud/dockerignore2/subdir/sub1.txt ================================================ sub1 ================================================ FILE: tests/bud/dockerignore2/subdir/subsubdir/subsub1.txt ================================================ subsub1 ================================================ FILE: tests/bud/dockerignore3/.dockerignore ================================================ # comment * !test* !src **/*.in src/etc *.md !README*.md README-secret.md test1.txt ================================================ FILE: tests/bud/dockerignore3/BUILD.md ================================================ ================================================ FILE: tests/bud/dockerignore3/COPYRIGHT ================================================ ================================================ FILE: tests/bud/dockerignore3/Dockerfile ================================================ FROM busybox COPY . /upload/ COPY src /upload/src2/ COPY test1.txt /upload/test1.txt RUN echo "CUT HERE"; /bin/find /upload | LANG=en_US.UTF-8 sort; echo "CUT HERE" ================================================ FILE: tests/bud/dockerignore3/LICENSE ================================================ ================================================ FILE: tests/bud/dockerignore3/README-secret.md ================================================ ================================================ FILE: tests/bud/dockerignore3/README.md ================================================ ================================================ FILE: tests/bud/dockerignore3/manifest ================================================ /upload /upload/README.md /upload/src /upload/src/Makefile /upload/src/cmd /upload/src/cmd/Makefile /upload/src/lib /upload/src/lib/Makefile /upload/src2 /upload/src2/Makefile /upload/src2/cmd /upload/src2/cmd/Makefile /upload/src2/lib /upload/src2/lib/Makefile /upload/test2.txt /upload/test3.txt ================================================ FILE: tests/bud/dockerignore3/src/Makefile ================================================ ================================================ FILE: tests/bud/dockerignore3/src/cmd/Makefile ================================================ ================================================ FILE: tests/bud/dockerignore3/src/cmd/main.in ================================================ ================================================ FILE: tests/bud/dockerignore3/src/etc/foo.conf ================================================ ================================================ FILE: tests/bud/dockerignore3/src/etc/foo.conf.d/dropin.conf ================================================ ================================================ FILE: tests/bud/dockerignore3/src/lib/Makefile ================================================ ================================================ FILE: tests/bud/dockerignore3/src/lib/framework.in ================================================ ================================================ FILE: tests/bud/dockerignore3/test1.txt ================================================ ================================================ FILE: tests/bud/dockerignore3/test2.txt ================================================ ================================================ FILE: tests/bud/dockerignore3/test3.txt ================================================ ================================================ FILE: tests/bud/dockerignore4/BUILD.md ================================================ ================================================ FILE: tests/bud/dockerignore4/COPYRIGHT ================================================ ================================================ FILE: tests/bud/dockerignore4/Dockerfile.test ================================================ FROM busybox COPY . /upload/ COPY src /upload/src2/ COPY test1.txt /upload/test1.txt RUN echo "CUT HERE"; /bin/find /upload | LANG=en_US.UTF-8 sort; echo "CUT HERE" ================================================ FILE: tests/bud/dockerignore4/Dockerfile.test.dockerignore ================================================ # comment * !test* !src **/*.in src/etc *.md !README*.md README-secret.md test1.txt ================================================ FILE: tests/bud/dockerignore4/LICENSE ================================================ ================================================ FILE: tests/bud/dockerignore4/README-secret.md ================================================ ================================================ FILE: tests/bud/dockerignore4/README.md ================================================ ================================================ FILE: tests/bud/dockerignore4/manifest ================================================ /upload /upload/README.md /upload/src /upload/src/Makefile /upload/src/cmd /upload/src/cmd/Makefile /upload/src/lib /upload/src/lib/Makefile /upload/src2 /upload/src2/Makefile /upload/src2/cmd /upload/src2/cmd/Makefile /upload/src2/lib /upload/src2/lib/Makefile /upload/test2.txt /upload/test3.txt ================================================ FILE: tests/bud/dockerignore4/src/Makefile ================================================ ================================================ FILE: tests/bud/dockerignore4/src/cmd/Makefile ================================================ ================================================ FILE: tests/bud/dockerignore4/src/cmd/main.in ================================================ ================================================ FILE: tests/bud/dockerignore4/src/etc/foo.conf ================================================ ================================================ FILE: tests/bud/dockerignore4/src/etc/foo.conf.d/dropin.conf ================================================ ================================================ FILE: tests/bud/dockerignore4/src/lib/Makefile ================================================ ================================================ FILE: tests/bud/dockerignore4/src/lib/framework.in ================================================ ================================================ FILE: tests/bud/dockerignore4/test1.txt ================================================ ================================================ FILE: tests/bud/dockerignore4/test2.txt ================================================ ================================================ FILE: tests/bud/dockerignore4/test3.txt ================================================ ================================================ FILE: tests/bud/dockerignore6/Dockerfile ================================================ FROM alpine COPY ./ ./ COPY subdir ./ ================================================ FILE: tests/bud/dockerignore6/Dockerfile.dockerignore ================================================ # comment * test* !test2* subdir !*/sub1* ================================================ FILE: tests/bud/dockerignore6/Dockerfile.succeed ================================================ FROM alpine COPY ./ ./ ================================================ FILE: tests/bud/dockerignore6/Dockerfile.succeed.dockerignore ================================================ # comment * test* !test2* subdir !*/sub1* ================================================ FILE: tests/bud/dockerignore6/subdir/sub1.txt ================================================ ================================================ FILE: tests/bud/dockerignore6/subdir/sub2.txt ================================================ ================================================ FILE: tests/bud/dockerignore6/test1.txt ================================================ test1 failed ================================================ FILE: tests/bud/dockerignore6/test2.txt ================================================ test2 failed ================================================ FILE: tests/bud/dupe-arg-env-name/Containerfile ================================================ FROM busybox ARG FOO=bar ARG WEBROOT=https://example.org/ ENV WEBROOT="$WEBROOT$FOO" RUN echo "${WEBROOT}" ================================================ FILE: tests/bud/env/Dockerfile.check-env ================================================ FROM alpine ENV foo=bar ================================================ FILE: tests/bud/env/Dockerfile.env-from-image ================================================ FROM env-from-image RUN echo "@${envcheck}@" ================================================ FILE: tests/bud/env/Dockerfile.env-precedence ================================================ FROM alpine ENV a=b ENV c=d # E and G are passed in on the command-line, and we haven't overridden them yet, so the command will get the CLI values. RUN echo a=$a c=$c E=$E G=$G ENV E=E G=G # We just set E and G, and that will override values passed at the command line thanks to imagebuilder's handling of ENV instructions. RUN echo a=$a c=$c E=$E G=$G FROM 0 ENV w=x ENV y=z # I and K are passed in on the command-line, and we haven't overridden them yet, so the command will get the CLI values. RUN echo w=$w y=$y I=$I K=$K ENV I=I K=K # We just set I and K, and that will override values passed at the command line thanks to imagebuilder's handling of ENV instructions. RUN echo w=$w y=$y I=$I K=$K ================================================ FILE: tests/bud/env/Dockerfile.env-same-file ================================================ FROM alpine ENV envcheck "unique.test.string" RUN echo ":${envcheck}:" ================================================ FILE: tests/bud/env/Dockerfile.special-chars ================================================ FROM docker.io/ubuntu ENV LIB="$(PREFIX)/lib" ================================================ FILE: tests/bud/exit42/Containerfile ================================================ FROM alpine RUN sh -c "exit 42" ================================================ FILE: tests/bud/from-as/Dockerfile ================================================ FROM alpine AS base RUN touch /1 ENV LOCAL=/1 RUN find $LOCAL FROM base RUN find $LOCAL ================================================ FILE: tests/bud/from-as/Dockerfile.skip ================================================ FROM alpine AS base RUN touch /1 ENV LOCAL=/1 RUN find $LOCAL RUN touch hello FROM base RUN find $LOCAL RUN touch /2 ENV LOCAL2=/2 RUN find $LOCAL2 # so we don't end up skipping stage: 0 COPY --from=0 hello . FROM base RUN find $LOCAL RUN ls / # so we don't end up skipping stage: 1 COPY --from=1 hello . ================================================ FILE: tests/bud/from-base/Containerfile ================================================ FROM base ADD . . ================================================ FILE: tests/bud/from-invalid-registry/Containerfile ================================================ FROM alpine as build # Invalid registry and image FROM myrepository.example/image:tag ================================================ FILE: tests/bud/from-multiple-files/Dockerfile1.alpine ================================================ FROM alpine COPY Dockerfile1.alpine /Dockerfile1 ================================================ FILE: tests/bud/from-multiple-files/Dockerfile1.scratch ================================================ FROM scratch COPY Dockerfile1.scratch /Dockerfile1 ================================================ FILE: tests/bud/from-multiple-files/Dockerfile2.glob ================================================ FROM alpine COPY Dockerfile* / ================================================ FILE: tests/bud/from-multiple-files/Dockerfile2.nofrom ================================================ COPY Dockerfile2.nofrom / ================================================ FILE: tests/bud/from-multiple-files/Dockerfile2.withfrom ================================================ FROM alpine COPY Dockerfile2.withfrom / ================================================ FILE: tests/bud/from-scratch/Containerfile ================================================ FROM scratch ================================================ FILE: tests/bud/from-scratch/Containerfile2 ================================================ FROM scratch USER 1001 ================================================ FILE: tests/bud/from-scratch/Dockerfile ================================================ FROM scratch ================================================ FILE: tests/bud/from-with-arg/Containerfile ================================================ ARG base FROM ${base} ARG toolchainname ARG destinationpath COPY --from=${toolchainname} / ${destinationpath} ================================================ FILE: tests/bud/group/Containerfile ================================================ FROM alpine RUN adduser -D -g 'Susan' susan \ && addgroup cool_kids \ && addgroup susan cool_kids \ && addgroup good_kids \ && addgroup susan good_kids USER susan RUN groups | grep cool_kids ================================================ FILE: tests/bud/hardlink/Dockerfile ================================================ FROM scratch COPY . . COPY . . ================================================ FILE: tests/bud/healthcheck/Dockerfile ================================================ FROM alpine HEALTHCHECK --start-interval=30s --start-period=10m --interval=5m --timeout=3s --retries=4 \ CMD curl -f http://localhost/ || exit 1 ================================================ FILE: tests/bud/heredoc/Containerfile ================================================ FROM alpine USER root WORKDIR / RUN <> /file1 echo "some text of first file" >> /file1 EOF RUN cat file1 RUN cat < test6.txt this is the output of test6 part1 FILE1 this is the output of test6 part2 FILE2 RUN 5< test7.txt this is the output of test7 part1 file this is the output of test7 part2 FILE this is the output of test7 part3 File RUN < test8.1 && < test8.2 this is the output of test8 part1 FILE1 this is the output of test8 part2 FILE2 RUN cat /test/robots.txt RUN cat /test/humans.txt RUN cat test6.txt RUN cat test7.txt RUN cat test8.1 RUN cat test8.2 ================================================ FILE: tests/bud/heredoc/Containerfile.bash_file ================================================ FROM busybox RUN < test9.txt EOF RUN <<-EOF #!/bin/sh echo " this is the output of test10" > test10.txt EOF RUN cat test9.txt RUN cat test10.txt ================================================ FILE: tests/bud/heredoc/Containerfile.she_bang ================================================ FROM alpine RUN < test.txt # Mount of this file must exists till this run step # so this `ls` command should not fail ls -a /dev/pipes/ EOF RUN cat test.txt # This ls command must fail, since mount is removed in this step RUN ls -a /dev/pipes ================================================ FILE: tests/bud/heredoc-ignore/.containerignore ================================================ * ================================================ FILE: tests/bud/heredoc-ignore/Containerfile ================================================ # Containerfile FROM docker.io/alpine:latest COPY < world # Following stage must be picked from cache except last instruction FROM busybox as two RUN echo hello1 RUN echo helloworld ================================================ FILE: tests/bud/layers-squash/artifact ================================================ Aaa ================================================ FILE: tests/bud/leading-args/Dockerfile ================================================ ARG VERSION=latest ARG FOO=bar FROM busybox:$VERSION ENV FOO $FOO RUN echo $FOO $VERSION ================================================ FILE: tests/bud/long-sleep/Dockerfile ================================================ FROM alpine # this can be a long long time, since the test should kill it long before this has elapsed RUN sleep 300 RUN echo not fully killed ================================================ FILE: tests/bud/maintainer/Dockerfile ================================================ FROM alpine MAINTAINER kilroy ================================================ FILE: tests/bud/masks/Containerfile ================================================ FROM alpine COPY --chmod=700 test.sh / RUN /test.sh ================================================ FILE: tests/bud/masks/test.sh ================================================ # !/bin/sh function output { for mask in /proc/acpi /proc/kcore /proc/keys /proc/latency_stats /proc/sched_debug /proc/scsi /proc/timer_list /proc/timer_stats /sys/dev/block /sys/devices/virtual/powercap /sys/firmware /sys/fs/selinux; do test -e $mask || continue test -f $mask && cat $mask 2> /dev/null test -d $mask && ls $mask done } output=$(output | wc -c ) if [ $output == 0 ]; then echo "masked" else echo "unmasked" fi ================================================ FILE: tests/bud/metadata-only/Containerfile ================================================ FROM alpine LABEL testlabel=testvalue ENV TESTVAR=testvalue USER nobody CMD ["/bin/sh"] ================================================ FILE: tests/bud/mount/Dockerfile ================================================ FROM alpine RUN mount ================================================ FILE: tests/bud/multi-stage-builds/Dockerfile.arg ================================================ FROM alpine ARG SECRET RUN echo $SECRET FROM alpine RUN echo "$SECRET" > test_file ================================================ FILE: tests/bud/multi-stage-builds/Dockerfile.arg_in_copy ================================================ ARG my_env=a FROM alpine as stage_a RUN /bin/true FROM alpine ARG my_env COPY --from=stage_${my_env} /bin/true /bin/true_copy ================================================ FILE: tests/bud/multi-stage-builds/Dockerfile.arg_in_stage ================================================ ARG my_env=a FROM alpine as stage_a RUN /bin/true FROM stage_${my_env} as stage_b RUN /bin/true ================================================ FILE: tests/bud/multi-stage-builds/Dockerfile.extended ================================================ FROM busybox:latest AS builder ENV "BUILD_LOGLEVEL"="5" RUN touch /tmp/preCommit ENTRYPOINT /bin/sleep 600 ENV "OPENSHIFT_BUILD_NAME"="mydockertest-1" "OPENSHIFT_BUILD_NAMESPACE"="default" LABEL "io.openshift.build.name"="mydockertest-1" "io.openshift.build.namespace"="default" FROM builder ENV "BUILD_LOGLEVEL"="5" RUN touch /tmp/postCommit FROM builder ENV "BUILD_LOGLEVEL"="5" ================================================ FILE: tests/bud/multi-stage-builds/Dockerfile.index ================================================ FROM scratch COPY Dockerfile.index / FROM alpine COPY --from=0 /Dockerfile.index /Dockerfile.index ================================================ FILE: tests/bud/multi-stage-builds/Dockerfile.mixed ================================================ FROM scratch AS myname COPY Dockerfile.name / FROM scratch AS myname2 COPY Dockerfile.index / FROM scratch COPY Dockerfile.mixed / FROM scratch COPY --from=myname /Dockerfile.name /Dockerfile.name COPY --from=1 /Dockerfile.index /Dockerfile.index COPY --from=2 /Dockerfile.mixed /Dockerfile.mixed ================================================ FILE: tests/bud/multi-stage-builds/Dockerfile.name ================================================ FROM alpine AS myname COPY Dockerfile.name / FROM scratch COPY --from=myname /Dockerfile.name /Dockerfile.name ================================================ FILE: tests/bud/multi-stage-builds/Dockerfile.rebase ================================================ FROM alpine AS myname COPY Dockerfile.name / FROM myname RUN pwd FROM myname RUN id ================================================ FILE: tests/bud/multi-stage-builds/Dockerfile.reused ================================================ FROM busybox as base RUN pwd FROM base RUN pwd FROM base ================================================ FILE: tests/bud/multi-stage-builds/Dockerfile.reused2 ================================================ FROM busybox as base RUN pwd FROM base RUN pwd FROM base RUN pwd ================================================ FILE: tests/bud/multi-stage-builds-small-as/Dockerfile.index ================================================ FROM scratch COPY Dockerfile.index / FROM alpine COPY --from=0 /Dockerfile.index /Dockerfile.index ================================================ FILE: tests/bud/multi-stage-builds-small-as/Dockerfile.mixed ================================================ FROM scratch as myname COPY Dockerfile.name / FROM scratch as myname2 COPY Dockerfile.index / FROM scratch COPY Dockerfile.mixed / FROM scratch COPY --from=myname /Dockerfile.name /Dockerfile.name COPY --from=1 /Dockerfile.index /Dockerfile.index COPY --from=2 /Dockerfile.mixed /Dockerfile.mixed ================================================ FILE: tests/bud/multi-stage-builds-small-as/Dockerfile.name ================================================ FROM alpine as myname COPY Dockerfile.name / FROM scratch COPY --from=myname /Dockerfile.name /Dockerfile.name ================================================ FILE: tests/bud/multi-stage-only-base/Containerfile1 ================================================ FROM alpine RUN echo "parent-one" > parent-one ================================================ FILE: tests/bud/multi-stage-only-base/Containerfile2 ================================================ FROM alpine RUN echo "parent-two" > parent-two ================================================ FILE: tests/bud/multi-stage-only-base/Containerfile3 ================================================ FROM localhost/parent-one as p1 FROM localhost/parent-two COPY --from=p1 parent-one . ================================================ FILE: tests/bud/multiarch/Containerfile.reset-platform ================================================ ARG FOREIGNARCH FROM --platform=linux/$FOREIGNARCH busybox AS foreign FROM busybox COPY --from=foreign /bin/busybox /bin/busybox.foreign RUN arch RUN ls -l /bin/busybox /bin/busybox.foreign RUN ! cmp /bin/busybox /bin/busybox.foreign ================================================ FILE: tests/bud/multiarch/Dockerfile ================================================ FROM alpine AS base RUN cp /etc/apk/arch /root/arch-base FROM alpine # Make sure that non-default arch doesn't mess with copying from previous stages. COPY --from=base /root/arch-base /root/ # Make sure that COPY --from=image uses the image for the preferred architecture. COPY --from=alpine /etc/apk/arch /root/ RUN cmp /etc/apk/arch /root/arch RUN cmp /etc/apk/arch /root/arch-base ================================================ FILE: tests/bud/multiarch/Dockerfile.built-in-args ================================================ FROM --platform=$BUILDPLATFORM alpine ARG TARGETPLATFORM ARG TARGETOS ARG TARGETARCH ARG BUILDPLATFORM RUN echo "I'm compiling for $TARGETPLATFORM on $BUILDPLATFORM and tagging for $TARGETPLATFORM and OS $TARGETOS and ARCH $TARGETARCH" ================================================ FILE: tests/bud/multiarch/Dockerfile.fail ================================================ # This build should fail if we're building with at least one non-amd64 platform # either because we can't execute this test binary, or because it executed fine # but returned an error FROM alpine RUN test `arch` = x86_64 ================================================ FILE: tests/bud/multiarch/Dockerfile.fail-multistage ================================================ ARG SAFEIMAGE FROM $SAFEIMAGE RUN touch -r /etc/os-release /timestamped RUN sleep 0 FROM $SAFEIMAGE COPY --from=0 /timestamped /timestamped RUN sleep 0 FROM $SAFEIMAGE COPY --from=1 /timestamped /timestamped RUN sleep 0 FROM $SAFEIMAGE COPY --from=2 /timestamped /timestamped RUN false FROM $SAFEIMAGE COPY --from=3 /timestamped /timestamped RUN sleep 0 FROM $SAFEIMAGE COPY --from=4 /timestamped /timestamped RUN sleep 0 ================================================ FILE: tests/bud/multiarch/Dockerfile.no-run ================================================ # A different base image that is known to be a manifest list, supporting a # different but partially-overlapping set of platforms. ARG SAFEIMAGE # A base image that is known to be a manifest list. FROM docker.io/library/alpine COPY Dockerfile.no-run /root/ FROM $SAFEIMAGE COPY --from=0 /root/Dockerfile.no-run /root/ ================================================ FILE: tests/bud/namespaces/Containerfile ================================================ FROM alpine RUN echo "ReadlinkResult" && readlink /proc/self/ns/user RUN echo "UidMapResult" && cat /proc/self/uid_map RUN echo "GidMapResult" && cat /proc/self/gid_map COPY --chown=1:1 somefile / RUN echo "StatSomefileResult" && stat -c '%u:%g' /somefile COPY somedir /somedir RUN echo "StatSomedirResult" && stat -c '%u:%g' /somedir RUN echo "StatSomeotherfileResult" && stat -c '%u:%g %a' /somedir/someotherfile USER guest WORKDIR /new-workdir RUN echo "StatNewWorkdir" && stat -c '%U:%G' $PWD ================================================ FILE: tests/bud/network/Containerfile ================================================ FROM alpine RUN ip addr ================================================ FILE: tests/bud/no-change/Dockerfile ================================================ FROM alpine ================================================ FILE: tests/bud/no-history/Dockerfile ================================================ # The important thing about that first base image is that it has no history # entries, but it does have at least one layer. This base image is built # during the test that uses this Dockerfile, and isn't in a registry. FROM fakeregistry.podman.invalid/notreal AS first-stage COPY --from=busybox / / RUN date > /date1.txt RUN sleep 1 > /sleep1.txt FROM first-stage RUN date > /date2.txt RUN sleep 1 > /sleep2.txt ================================================ FILE: tests/bud/no-hostname/Containerfile ================================================ from alpine run cat /etc/hostname ================================================ FILE: tests/bud/no-hostname/Containerfile.noetc ================================================ from alpine RUN mv /etc /usr/ RUN ls -l /etc ================================================ FILE: tests/bud/non-directory-in-path/non-directory ================================================ A dummy file that is not a directory. ================================================ FILE: tests/bud/onbuild/Dockerfile ================================================ FROM alpine ONBUILD RUN touch /onbuild1 ONBUILD RUN touch /onbuild2 ================================================ FILE: tests/bud/onbuild/Dockerfile1 ================================================ FROM onbuild ONBUILD RUN touch /onbuild3 ================================================ FILE: tests/bud/onbuild/Dockerfile2 ================================================ FROM alpine ONBUILD RUN touch /onbuild1 ONBUILD RUN touch /onbuild2 RUN touch /firstfile RUN touch /secondfile ================================================ FILE: tests/bud/only-base/Containerfile ================================================ FROM quay.io/libpod/testimage:20241011 ================================================ FILE: tests/bud/platform-sets-args/Containerfile ================================================ FROM alpine ARG TARGETARCH ARG TARGETOS ARG TARGETPLATFORM ARG TARGETVARIANT ENV nothing="multiarch-safe statement that will result in a built image" run echo TARGETARCH=${TARGETARCH} run echo TARGETOS=${TARGETOS} run echo TARGETPLATFORM=${TARGETPLATFORM} run echo TARGETVARIANT=${TARGETVARANT} ================================================ FILE: tests/bud/preprocess/Decomposed.in ================================================ FROM busybox #include "common" RUNHELLO #include "install-base" ================================================ FILE: tests/bud/preprocess/Error.in ================================================ FROM busybox #include "common" #error THISERROR ================================================ FILE: tests/bud/preprocess/base ================================================ based ================================================ FILE: tests/bud/preprocess/common ================================================ #define RUNHELLO RUN echo "Hello world!" RUN touch /etc/hello-world.txt ================================================ FILE: tests/bud/preprocess/install-base ================================================ ARG HTTP_SERVER_PORT RUN wget -c http://0.0.0.0:${HTTP_SERVER_PORT}/base ================================================ FILE: tests/bud/preserve-volumes/Dockerfile ================================================ FROM alpine RUN mkdir -p /vol/subvol/subsubvol RUN dd if=/dev/zero bs=512 count=1 of=/vol/subvol/subsubvol/subsubvolfile VOLUME /vol/subvol # At this point, the contents below /vol/subvol may be frozen, so try to create # something that will be discarded if it was. RUN dd if=/dev/zero bs=512 count=1 of=/vol/subvol/subvolfile # In particular, /vol/subvol/subvolfile should be wiped out if --compat-volumes # behavior was selected. RUN dd if=/dev/zero bs=512 count=1 of=/vol/volfile # However, /vol/volfile should always exist, since /vol was not a volume, but # we're making it one here. VOLUME /vol # And this should be redundant. VOLUME /vol/subvol # And now that we've frozen /vol, --compat-volumes should make this disappear, # too. RUN dd if=/dev/zero bs=512 count=1 of=/vol/anothervolfile # ADD files which should persist, regardless of the --compat-volumes setting. ADD Dockerfile /vol/Dockerfile RUN stat /vol/Dockerfile ADD Dockerfile /vol/Dockerfile2 RUN stat /vol/Dockerfile2 # This directory should still exist, since we cached /vol once it was declared # a VOLUME, and /vol/subvol was created before that (as a VOLUME, but still). RUN dd if=/dev/zero bs=512 count=1 of=/vol/subvol/subvolfile ================================================ FILE: tests/bud/pull/Containerfile ================================================ FROM busybox ================================================ FILE: tests/bud/recurse/Dockerfile ================================================ FROM scratch COPY . . COPY . . ================================================ FILE: tests/bud/run-mounts/Dockerfile.secret ================================================ FROM alpine RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret ================================================ FILE: tests/bud/run-mounts/Dockerfile.secret-access ================================================ FROM alpine RUN --mount=type=secret,id=mysecret,target=mysecret cat /mysecret RUN cat /mysecret ================================================ FILE: tests/bud/run-mounts/Dockerfile.secret-mode ================================================ FROM alpine RUN --mount=type=secret,id=mysecret stat -c "%a" /run/secrets/mysecret ================================================ FILE: tests/bud/run-mounts/Dockerfile.secret-not-required ================================================ FROM alpine RUN --mount=type=secret,id=mysecret echo "hello" ================================================ FILE: tests/bud/run-mounts/Dockerfile.secret-options ================================================ FROM alpine RUN --mount=type=secret,id=mysecret,dst=/mysecret,uid=1000,gid=1001,mode=0444 stat -c "%a" /mysecret ; ls -n /mysecret ================================================ FILE: tests/bud/run-mounts/Dockerfile.secret-required ================================================ FROM alpine RUN --mount=type=secret,id=mysecret,required=true cat /run/secrets/mysecret ================================================ FILE: tests/bud/run-mounts/Dockerfile.secret-required-false ================================================ FROM alpine RUN --mount=type=secret,id=mysecret,required=false cat /run/secrets/mysecret ================================================ FILE: tests/bud/run-mounts/Dockerfile.secret-required-wo-value ================================================ FROM alpine RUN --mount=type=secret,id=mysecret,required cat /run/secrets/mysecret ================================================ FILE: tests/bud/run-mounts/Dockerfile.ssh ================================================ FROM quay.io/hummingbird/git USER 0:0 RUN --mount=type=ssh,id=default ssh-add -l -E md5 ================================================ FILE: tests/bud/run-mounts/Dockerfile.ssh_access ================================================ FROM quay.io/hummingbird/git USER 0:0 RUN --mount=type=ssh,id=default ssh-add -l -E md5 RUN ssh-add -l -E md5 RUN cat /run/buildkit/ssh_agent.0 ================================================ FILE: tests/bud/run-mounts/Dockerfile.ssh_options ================================================ FROM quay.io/hummingbird/git USER 0:0 RUN --mount=type=ssh,id=default,dst=/dstsock,uid=1000,gid=1001,mode=0444 stat -c "%a" /dstsock; ls -n /dstsock ================================================ FILE: tests/bud/run-privd/Dockerfile ================================================ FROM busybox ARG HTTP_SERVER_PORT RUN wget -c http://localhost:${HTTP_SERVER_PORT}/Dockerfile ================================================ FILE: tests/bud/run-scenarios/Dockerfile.args ================================================ FROM alpine ARG arg="arg_value" RUN echo ${arg} ================================================ FILE: tests/bud/run-scenarios/Dockerfile.cmd-empty-run ================================================ FROM alpine CMD [ "pwd" ] RUN ================================================ FILE: tests/bud/run-scenarios/Dockerfile.cmd-run ================================================ FROM alpine CMD [ "/invalid/cmd" ] RUN echo "unique.test.string" ================================================ FILE: tests/bud/run-scenarios/Dockerfile.entrypoint-cmd-empty-run ================================================ FROM alpine ENTRYPOINT [ "pwd" ] CMD [ "whoami" ] RUN ================================================ FILE: tests/bud/run-scenarios/Dockerfile.entrypoint-cmd-run ================================================ FROM alpine ENTRYPOINT [ "/invalid/entrypoint" ] CMD [ "/invalid/cmd" ] RUN echo "unique.test.string" ================================================ FILE: tests/bud/run-scenarios/Dockerfile.entrypoint-empty-run ================================================ FROM alpine ENTRYPOINT [ "pwd" ] RUN ================================================ FILE: tests/bud/run-scenarios/Dockerfile.entrypoint-run ================================================ FROM alpine ENTRYPOINT [ "/invalid/entrypoint" ] RUN echo "unique.test.string" ================================================ FILE: tests/bud/run-scenarios/Dockerfile.multi-args ================================================ FROM alpine ARG USED_ARG="used_value" RUN echo ${USED_ARG} FROM scratch COPY --from=0 /etc/passwd /root/passwd-file ================================================ FILE: tests/bud/run-scenarios/Dockerfile.noop-flags ================================================ FROM scratch ================================================ FILE: tests/bud/save-stages/Dockerfile.arg-build-stages-and-chained-build-stages ================================================ ARG PULLSPEC=alpine ARG FIRST_STAGE=first-stage ARG SECOND_STAGE=second-stage FROM ${PULLSPEC} AS ${FIRST_STAGE} RUN echo "first-stage content" > /output.txt FROM ${FIRST_STAGE} AS ${SECOND_STAGE} RUN echo "second-stage content" >> /output.txt FROM alpine COPY --from=second-stage /output.txt /app/output.txt RUN cat /app/output.txt ================================================ FILE: tests/bud/save-stages/Dockerfile.chained-three-build-stages ================================================ FROM alpine AS base RUN echo "base" > /output.txt FROM base AS stage1 RUN echo "stage1" >> /output.txt FROM stage1 AS stage2 RUN echo "stage2" >> /output.txt FROM alpine COPY --from=stage2 /output.txt /app/output.txt RUN cat /app/output.txt ================================================ FILE: tests/bud/save-stages/Dockerfile.chained-two-build-stages ================================================ FROM alpine AS parent-stage RUN echo "builder intermediate stage" > /output.txt FROM parent-stage AS child-stage RUN echo "final intermediate stage" >> /output.txt FROM alpine COPY --from=child-stage /output.txt /app/output.txt RUN cat /app/output.txt ================================================ FILE: tests/bud/save-stages/Dockerfile.chained-two-build-stages-no-aliases ================================================ FROM alpine RUN echo "stage 0" > /output.txt FROM 0 RUN echo "stage 1" >> /output.txt FROM alpine COPY --from=1 /output.txt /app/output.txt RUN cat /app/output.txt ================================================ FILE: tests/bud/save-stages/Dockerfile.empty-intermediate-build-stage ================================================ FROM alpine:latest AS builder FROM alpine:latest COPY --from=builder /etc/os-release /app/os-release RUN cat /app/os-release ================================================ FILE: tests/bud/save-stages/Dockerfile.final-uses-build-stage ================================================ FROM alpine:latest AS intermediate RUN echo "intermediate data" > /int.txt # Final stage uses intermediate stage as base (not external image) FROM intermediate RUN echo "final data" > /final.txt CMD ["cat", "/final.txt"] ================================================ FILE: tests/bud/save-stages/Dockerfile.simple ================================================ FROM alpine RUN echo "single stage build" ================================================ FILE: tests/bud/save-stages/Dockerfile.single-build-stage ================================================ FROM alpine AS intermediate RUN echo "intermediate" >> /build.txt FROM alpine COPY --from=intermediate /build.txt /app/build.txt RUN echo "final" ================================================ FILE: tests/bud/save-stages/Dockerfile.single-build-stage-modifiable ================================================ FROM alpine AS intermediate RUN echo "intermediate stage content" > /output.txt FROM alpine COPY --from=intermediate /output.txt /app/output.txt RUN cat /app/output.txt ================================================ FILE: tests/bud/save-stages/Dockerfile.single-build-stage-modifiable-renamed ================================================ FROM alpine AS intermediate-renamed RUN echo "intermediate stage content" > /output.txt FROM alpine COPY --from=intermediate-renamed /output.txt /app/output.txt RUN cat /app/output.txt ================================================ FILE: tests/bud/save-stages/Dockerfile.three-build-stages-parent-child-independent ================================================ # Stage 1: parent (independent) FROM alpine:latest AS parent RUN echo "parent layer" > /parent.txt # Stage 2: child - uses stage 1 as parent FROM parent AS child RUN echo "child layer" > /child.txt # Stage 3: independent (standalone) FROM alpine:latest AS independent RUN echo "independent layer" > /independent.txt # Final stage: uses all 3 intermediate stages FROM alpine:latest COPY --from=parent /parent.txt /app/parent.txt COPY --from=child /child.txt /app/child.txt COPY --from=independent /independent.txt /app/independent.txt CMD ["cat", "/app/parent.txt", "/app/child.txt", "/app/independent.txt"] ================================================ FILE: tests/bud/save-stages/Dockerfile.two-build-stages ================================================ FROM alpine AS alias1 RUN echo "alias1" > /file1.txt FROM alpine AS alias2 RUN echo "alias2" > /file2.txt FROM alpine COPY --from=alias1 /file1.txt /app/file1.txt COPY --from=alias2 /file2.txt /app/file2.txt RUN cat /app/file1.txt /app/file2.txt ================================================ FILE: tests/bud/save-stages/Dockerfile.two-build-stages-one-unused ================================================ FROM alpine AS builder RUN echo "building" > /build.txt FROM alpine AS intermediate RUN echo "intermediate" >> /build.txt FROM alpine COPY --from=intermediate /build.txt /app/build.txt RUN echo "final" ================================================ FILE: tests/bud/secret-env/Dockerfile ================================================ FROM alpine RUN --mount=type=secret,id=MYSECRET \ printf "%s\n" $(cat /run/secrets/MYSECRET) ================================================ FILE: tests/bud/secret-relative/Dockerfile ================================================ FROM alpine RUN mkdir test WORKDIR test RUN --mount=type=secret,id=secret-foo,dst=secret1.txt --mount=type=secret,id=secret-bar,dst=secret2.txt \ cat /test/secret1.txt && cat /test/secret2.txt ================================================ FILE: tests/bud/secret-relative/secret1.txt ================================================ secret:foo ================================================ FILE: tests/bud/secret-relative/secret2.txt ================================================ secret:bar ================================================ FILE: tests/bud/shell/Dockerfile ================================================ FROM alpine SHELL [ "/bin/sh", "-c" ] ================================================ FILE: tests/bud/shell/Dockerfile.build-shell-custom ================================================ FROM ubuntu SHELL [ "/bin/bash", "-c" ] RUN echo "SHELL=$0" ================================================ FILE: tests/bud/shell/Dockerfile.build-shell-default ================================================ FROM alpine RUN echo "SHELL=$0" ================================================ FILE: tests/bud/simple-multi-step/Containerfile ================================================ FROM alpine RUN echo helloworld RUN echo helloworld2 RUN echo helloworld3 ================================================ FILE: tests/bud/stdio/Dockerfile ================================================ FROM alpine # Will stall if this is connected to a terminal, or fail if it's not readable RUN cat /dev/stdin # Will fail if it's not writable RUN echo foo > /dev/stdout # Will fail if it's not writable RUN echo foo > /dev/stderr ================================================ FILE: tests/bud/supplemental-groups/Dockerfile ================================================ FROM alpine USER 1000:1000 RUN cat /proc/$$/status ================================================ FILE: tests/bud/symlink/Containerfile.add-tar-gz-with-link ================================================ FROM alpine WORKDIR / ADD tarball_latest.tar.gz /tmp/ ================================================ FILE: tests/bud/symlink/Containerfile.add-tar-with-link ================================================ FROM alpine WORKDIR / ADD tarball_latest.tar /tmp/ ================================================ FILE: tests/bud/symlink/Dockerfile ================================================ FROM alpine RUN mkdir -p /data RUN ln -s /test-log /blah RUN ln -s /data/log /test-log VOLUME [ "/test-log/test" ] RUN echo "hello" > /data/log/blah.txt ================================================ FILE: tests/bud/symlink/Dockerfile.absolute-dir-symlink ================================================ FROM ubuntu as builder RUN mkdir -p /my/data && touch /my/data/myexe && ln -s /my/data /data FROM ubuntu COPY --from=builder /data /data VOLUME [ "/data" ] ================================================ FILE: tests/bud/symlink/Dockerfile.absolute-symlink ================================================ FROM ubuntu as builder RUN echo "symlink-test" > /bin/myexe.1 && ln -s /bin/myexe.1 /bin/myexe FROM ubuntu COPY --from=builder /bin/myexe /bin/ VOLUME [ "/bin" ] ================================================ FILE: tests/bud/symlink/Dockerfile.multiple-symlinks ================================================ FROM alpine RUN mkdir -p /data RUN mkdir -p /test RUN mkdir -p /test-log RUN mkdir -p /myuser RUN ln -s /test /myuser/log RUN ln -s /test-log /test/bar RUN ln -s /data/log /test-log/foo VOLUME [ "/myuser/log/bar/foo/bin" ] RUN echo "hello" > /data/log/blah.txt ================================================ FILE: tests/bud/symlink/Dockerfile.relative-symlink ================================================ FROM alpine RUN mkdir -p /data RUN ln -s ../log /test-log VOLUME [ "/test-log/test" ] RUN ln -s ../data /var/data RUN touch /data/empty VOLUME [ "/var/data" ] RUN pwd ================================================ FILE: tests/bud/symlink/Dockerfile.replace-symlink ================================================ FROM scratch COPY ./ ./ COPY ./ ./ ================================================ FILE: tests/bud/symlink/Dockerfile.symlink-points-to-itself ================================================ FROM alpine RUN ln -s /test-log /test-log VOLUME [ "/test-log/test" ] ================================================ FILE: tests/bud/target/Dockerfile ================================================ FROM ubuntu:latest RUN touch /1 RUN touch hello FROM alpine:latest AS mytarget RUN touch /2 # Just add a copy so we don't skip stage:0 COPY --from=0 hello . FROM busybox:latest AS mytarget2 RUN touch /3 # Just add a copy so we don't skip stage:1 COPY --from=1 hello . ================================================ FILE: tests/bud/targetarch/Dockerfile ================================================ FROM alpine ARG TARGETARCH ARG TARGETOS ================================================ FILE: tests/bud/terminal/Dockerfile ================================================ FROM busybox RUN ! tty ================================================ FILE: tests/bud/unrecognized/Dockerfile ================================================ FROM alpine BOGUS nope-nope-nope ================================================ FILE: tests/bud/use-args/Containerfile ================================================ FROM alpine ARG testArg RUN echo ${testArg} COPY ${testArg} . ================================================ FILE: tests/bud/use-args/Containerfile.dest_nobrace ================================================ FROM alpine ARG testArg ARG destination RUN echo $testArg RUN echo $destination COPY $testArg $destination ================================================ FILE: tests/bud/use-args/Containerfile.destination ================================================ FROM alpine ARG testArg ARG destination RUN echo ${testArg} RUN echo ${destination} COPY ${testArg} ${destination} ================================================ FILE: tests/bud/use-layers/Dockerfile ================================================ FROM alpine RUN mkdir /hello VOLUME /var/lib/testdata RUN touch file.txt EXPOSE 8080 ARG HTTP_SERVER_PORT ADD http://0.0.0.0:${HTTP_SERVER_PORT}/README.md /tmp/ ENV foo=bar ================================================ FILE: tests/bud/use-layers/Dockerfile.2 ================================================ FROM alpine RUN mkdir /hello RUN touch blah.txt ENV foo=bar ================================================ FILE: tests/bud/use-layers/Dockerfile.3 ================================================ FROM alpine RUN mkdir /hello RUN touch blah.txt COPY mount world/ ENV foo=bar ================================================ FILE: tests/bud/use-layers/Dockerfile.4 ================================================ FROM alpine COPY hello_world.sh /tmp/ CMD bash /tmp/hello_world.sh ================================================ FILE: tests/bud/use-layers/Dockerfile.5 ================================================ FROM alpine RUN touch /home/blah ================================================ FILE: tests/bud/use-layers/Dockerfile.6 ================================================ FROM alpine ================================================ FILE: tests/bud/use-layers/Dockerfile.7 ================================================ FROM alpine COPY . world/ ================================================ FILE: tests/bud/use-layers/Dockerfile.build-args ================================================ FROM alpine ARG user RUN echo $user | base64 RUN touch /tmp/hello ================================================ FILE: tests/bud/use-layers/Dockerfile.dangling-symlink ================================================ FROM alpine COPY blah /tmp/ ================================================ FILE: tests/bud/use-layers/Dockerfile.fail-case ================================================ FROM alpine RUN mkdir /hello RUN touch blah.txt COPY non-existent world/ ENV foo=bar ================================================ FILE: tests/bud/use-layers/Dockerfile.multistage-copy ================================================ FROM alpine AS uuid COPY uuid /src FROM alpine AS date COPY date /src FROM alpine COPY --from=uuid /src/data /uuid COPY --from=date /src/data /date ================================================ FILE: tests/bud/use-layers/Dockerfile.non-existent-registry ================================================ FROM non-existent-registry.com/alpine RUN mkdir /hello RUN touch blah.txt COPY non-existent world/ ENV foo=bar ================================================ FILE: tests/bud/verify-cleanup/Dockerfile ================================================ FROM alpine as builder RUN mkdir subdir COPY hey . FROM ubuntu RUN --mount=type=bind,source=.,dst=/tmp,z \ --mount=type=tmpfs,dst=/var/tmp \ cat /tmp/hey RUN --mount=type=cache,from=builder,target=/cachedir cat /cachedir/hey RUN --mount=type=secret,id=secret-foo,dst=secret1.txt cat secret1.txt ARG TMP="/tmp" ARG VARTMP="/var/tmp" ARG CACHEDIR="/cachedir" ARG TESTDIR="/testdir" ARG SECRETFILE="secret1.txt" RUN [ -d "/tmp" ] && echo "Directory $TMP exists." RUN [ -d "/var/tmp" ] && echo "Directory $VARTMP exists." # Following path should not exist after the --mount step RUN [ ! -d "/testdir" ] && echo "Directory $TESTDIR DOES NOT exist." RUN [ ! -d "/cachedir" ] && echo "Cache Directory $CACHEDIR DOES NOT exist." RUN [ ! -f "secret1.txt" ] && echo "Secret File $SECRETFILE DOES NOT exist." # This should fail RUN cat /tmp/hey ================================================ FILE: tests/bud/verify-cleanup/hey ================================================ hello ================================================ FILE: tests/bud/verify-cleanup/secret1.txt ================================================ secrettext ================================================ FILE: tests/bud/volume-ownership/Dockerfile ================================================ FROM alpine RUN adduser -D -H testuser && addgroup testgroup RUN mkdir -p /vol/subvol RUN chown testuser:testgroup /vol/subvol VOLUME /vol/subvol # Run some command after VOLUME to ensure that the volume cache behavior is invoked # See https://github.com/containers/buildah/blob/843d15de3e797bd912607d27324d13a9d5c27dfb/imagebuildah/stage_executor.go#L61-L72 and # for more details RUN touch /test ================================================ FILE: tests/bud/volume-perms/Dockerfile ================================================ FROM alpine VOLUME /vol/subvol # At this point, the directory should exist, and it should have default # permissions 0755, and we shouldn't get an error from trying to write to it # because we it was created automatically. If this image is built with the # --compat-volumes flag, everything done after this point will be discarded. RUN chmod 0711 /vol/subvol RUN dd if=/dev/zero bs=512 count=1 of=/vol/subvol/subvolfile ================================================ FILE: tests/bud/volume-symlink/Dockerfile ================================================ FROM alpine # Create symbolic links to simplify mounting RUN mkdir -p /home/app/myvolume \ && touch /home/app/myvolume/foo.txt \ && ln -s /home/app/myvolume /config VOLUME ["/config"] ================================================ FILE: tests/bud/volume-symlink/Dockerfile.no-symlink ================================================ FROM alpine RUN mkdir -p /home/app/myvolume \ && touch /home/app/myvolume/foo.txt VOLUME ["/home/app/myvolume"] ================================================ FILE: tests/bud/with-arg/Dockerfile ================================================ FROM alpine ARG FOO ENV FOO=bat RUN echo $FOO ================================================ FILE: tests/bud/with-arg/Dockerfile2 ================================================ FROM alpine ARG FOO ENV FOO=${FOO} RUN echo $FOO ================================================ FILE: tests/bud/with-arg/Dockerfilefromarg ================================================ ARG app_type ARG another_app_type ARG another_app_type_default=m FROM alpine as x RUN echo hello FROM ${app_type} as m RUN echo world # Do not supply this in cli, lets use default FROM ${another_app_type_default} as final RUN echo hello FROM ${another_app_type} as final RUN echo hello ================================================ FILE: tests/bud/workdir-symlink/Dockerfile ================================================ FROM alpine RUN mkdir /var/lib/tempest RUN ln -sf /var/lib/tempest /tempest WORKDIR /tempest RUN touch /etc/notareal.conf RUN chmod 664 /etc/notareal.conf ================================================ FILE: tests/bud/workdir-symlink/Dockerfile-2 ================================================ # No directory created for the target of the symlink FROM alpine RUN ln -sf /var/lib/tempest /tempest WORKDIR /tempest RUN touch /etc/notareal.conf RUN chmod 664 /etc/notareal.conf COPY Dockerfile-2 ./Dockerfile-2 ================================================ FILE: tests/bud/workdir-symlink/Dockerfile-3 ================================================ # No directory created for the target of the symlink FROM alpine RUN ln -sf /var/lib/tempest /tempest WORKDIR /tempest/lowerdir RUN touch /etc/notareal.conf RUN chmod 664 /etc/notareal.conf RUN mkdir -p /tempest/lowerdir COPY Dockerfile-3 ./Dockerfile-3 COPY Dockerfile-3 /tempest/Dockerfile-3 COPY Dockerfile-3 /tempest/lowerdir/Dockerfile-3 ================================================ FILE: tests/bud/workdir-user/Dockerfile ================================================ FROM alpine RUN adduser -D http -h /home/http USER http WORKDIR /home/http/public RUN stat -c '%u:%g %n' $PWD RUN touch foobar ================================================ FILE: tests/bud.bats ================================================ #!/usr/bin/env bats load helpers @test "bud with a path to a Dockerfile (-f) containing a non-directory entry" { run_buildah 125 build -f $BUDFILES/non-directory-in-path/non-directory/Dockerfile expect_output --substring "non-directory/Dockerfile: not a directory" } @test "bud stdio is usable pipes" { _prefetch alpine run_buildah build $BUDFILES/stdio } @test "bud: build manifest list and --add-compression zstd" { start_registry run_buildah login --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth --username testuser --password testpassword localhost:${REGISTRY_PORT} imgname="img-$(safename)" run_buildah build $WITH_POLICY_JSON -t "${imgname}1" --platform linux/amd64 -f $BUDFILES/dockerfile/Dockerfile run_buildah build $WITH_POLICY_JSON -t "${imgname}2" --platform linux/arm64 -f $BUDFILES/dockerfile/Dockerfile run_buildah manifest create foo run_buildah manifest add foo "${imgname}1" run_buildah manifest add foo "${imgname}2" run_buildah manifest push $WITH_POLICY_JSON --authfile ${TEST_SCRATCH_DIR}/test.auth --all --add-compression zstd --tls-verify=false foo docker://localhost:${REGISTRY_PORT}/list run_buildah manifest inspect --authfile ${TEST_SCRATCH_DIR}/test.auth --tls-verify=false localhost:${REGISTRY_PORT}/list list="$output" validate_instance_compression "0" "$list" "amd64" "gzip" validate_instance_compression "1" "$list" "arm64" "gzip" validate_instance_compression "2" "$list" "amd64" "zstd" validate_instance_compression "3" "$list" "arm64" "zstd" } @test "bud: build manifest list and --add-compression with containers.conf" { local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/Dockerfile1 << _EOF FROM alpine _EOF cat > $contextdir/containers.conf << _EOF [engine] add_compression = ["zstd"] _EOF start_registry run_buildah login --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth --username testuser --password testpassword localhost:${REGISTRY_PORT} imgname="img-$(safename)" run_buildah build $WITH_POLICY_JSON -t "${imgname}1" --platform linux/amd64 -f $contextdir/Dockerfile1 run_buildah build $WITH_POLICY_JSON -t "${imgname}2" --platform linux/arm64 -f $contextdir/Dockerfile1 run_buildah manifest create foo run_buildah manifest add foo "${imgname}1" run_buildah manifest add foo "${imgname}2" CONTAINERS_CONF=$contextdir/containers.conf run_buildah manifest push $WITH_POLICY_JSON --authfile ${TEST_SCRATCH_DIR}/test.auth --all --tls-verify=false foo docker://localhost:${REGISTRY_PORT}/list run_buildah manifest inspect --authfile ${TEST_SCRATCH_DIR}/test.auth --tls-verify=false localhost:${REGISTRY_PORT}/list list="$output" validate_instance_compression "0" "$list" "amd64" "gzip" validate_instance_compression "1" "$list" "arm64" "gzip" validate_instance_compression "2" "$list" "amd64" "zstd" validate_instance_compression "3" "$list" "arm64" "zstd" } @test "bud: build manifest list with --add-compression zstd, --compression and --force-compression" { local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/Dockerfile1 << _EOF FROM alpine _EOF start_registry run_buildah login --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth --username testuser --password testpassword localhost:${REGISTRY_PORT} imgname="img-$(safename)" run_buildah build $WITH_POLICY_JSON -t "${imgname}1" --platform linux/amd64 -f $contextdir/Dockerfile1 run_buildah build $WITH_POLICY_JSON -t "${imgname}2" --platform linux/arm64 -f $contextdir/Dockerfile1 run_buildah manifest create foo run_buildah manifest add foo "${imgname}1" run_buildah manifest add foo "${imgname}2" run_buildah manifest push $WITH_POLICY_JSON --authfile ${TEST_SCRATCH_DIR}/test.auth --all --add-compression zstd --tls-verify=false foo docker://localhost:${REGISTRY_PORT}/list run_buildah manifest inspect --authfile ${TEST_SCRATCH_DIR}/test.auth --tls-verify=false localhost:${REGISTRY_PORT}/list list="$output" validate_instance_compression "0" "$list" "amd64" "gzip" validate_instance_compression "1" "$list" "arm64" "gzip" validate_instance_compression "2" "$list" "amd64" "zstd" validate_instance_compression "3" "$list" "arm64" "zstd" # Pushing again should keep every thing intact if original compression is `gzip` and `--force-compression` is specified run_buildah manifest push $WITH_POLICY_JSON --authfile ${TEST_SCRATCH_DIR}/test.auth --all --add-compression zstd --compression-format gzip --force-compression --tls-verify=false foo docker://localhost:${REGISTRY_PORT}/list run_buildah manifest inspect --authfile ${TEST_SCRATCH_DIR}/test.auth --tls-verify=false localhost:${REGISTRY_PORT}/list list="$output" validate_instance_compression "0" "$list" "amd64" "gzip" validate_instance_compression "1" "$list" "arm64" "gzip" validate_instance_compression "2" "$list" "amd64" "zstd" validate_instance_compression "3" "$list" "arm64" "zstd" # Pushing again without --force-compression but with --compression-format should do the same thing run_buildah manifest push $WITH_POLICY_JSON --authfile ${TEST_SCRATCH_DIR}/test.auth --all --add-compression zstd --compression-format gzip --tls-verify=false foo docker://localhost:${REGISTRY_PORT}/list run_buildah manifest inspect --authfile ${TEST_SCRATCH_DIR}/test.auth --tls-verify=false localhost:${REGISTRY_PORT}/list list="$output" validate_instance_compression "0" "$list" "amd64" "gzip" validate_instance_compression "1" "$list" "arm64" "gzip" validate_instance_compression "2" "$list" "amd64" "zstd" validate_instance_compression "3" "$list" "arm64" "zstd" } @test "Multi-stage should not remove used base-image without --layers" { run_buildah build -t parent-one -f $BUDFILES/multi-stage-only-base/Containerfile1 run_buildah build -t parent-two -f $BUDFILES/multi-stage-only-base/Containerfile2 run_buildah build -t multi-stage -f $BUDFILES/multi-stage-only-base/Containerfile3 run_buildah images -a expect_output --substring "parent-one" "parent one must not be removed" } @test "no layer should be created on scratch" { imgname="img-$(safename)" run_buildah build --layers --label "label1=value1" -t $imgname -f $BUDFILES/from-scratch/Containerfile run_buildah inspect -f '{{len .Docker.RootFS.DiffIDs}}' $imgname expect_output "0" "layer should not exist" run_buildah build --layers -t $imgname -f $BUDFILES/from-scratch/Containerfile run_buildah inspect -f '{{len .Docker.RootFS.DiffIDs}}' $imgname expect_output "0" "layer should not exist" } @test "no empty layer for metadata-only instructions without --layers" { _prefetch alpine imgname="img-$(safename)" # Get the base image layer count run_buildah inspect -f '{{len .Docker.RootFS.DiffIDs}}' alpine base_layers="$output" # Build without --layers (single-layer mode) with only metadata instructions run_buildah build --layers=false -t $imgname -f $BUDFILES/metadata-only/Containerfile run_buildah inspect -f '{{len .Docker.RootFS.DiffIDs}}' $imgname expect_output "$base_layers" "metadata-only instructions should not add a layer" } @test "bud and test --inherit-annotations" { base=quay.io/libpod/testimage:20241011 _prefetch $base target=exp run_buildah --version local -a output_fields=($output) buildah_version=${output_fields[2]} buildah inspect --format '{{ .ImageAnnotations }}' $base not_want_output='map[]' assert "$output" != "$not_want_output" "expected some annotations to be set in base image $base" ## Since we are inheriting annotations, built image should contain some default annotations run_buildah build $WITH_POLICY_JSON --inherit-annotations=true -t $target --from $base $BUDFILES/base-with-labels not_want_output='map[]' run_buildah inspect --format '{{printf "%q" .ImageAnnotations}}' $target assert "$output" != "$not_want_output" "expected some annotations to be set in base image $base" ## Since we are inheriting no annotations, built image should not contain any annotations. run_buildah build $WITH_POLICY_JSON --inherit-annotations=false --created-annotation=false -t $target --from $base $BUDFILES/base-with-labels # no annotations should be inherited from base image want_output='map[]' run_buildah inspect --format '{{printf "%q" .ImageAnnotations}}' $target expect_output "$want_output" ## Build again but set a new annotation and don't inherit annotations from base image run_buildah build $WITH_POLICY_JSON --inherit-annotations=false --created-annotation=false --annotation hello=world -t $target --from $base $BUDFILES/base-with-labels # no annotations should be inherited from base image want_output='map["hello":"world"]' run_buildah inspect --format '{{printf "%q" .ImageAnnotations}}' $target expect_output "$want_output" ## Try similar thing with another Containerfile run_buildah build $WITH_POLICY_JSON --inherit-annotations=false --created-annotation=false -t $target -f $BUDFILES/base-with-labels/Containerfile2 # no annotations should be inherited from base image want_output='map[]' run_buildah inspect --format '{{printf "%q" .ImageAnnotations}}' $target expect_output "$want_output" run_buildah build $WITH_POLICY_JSON --inherit-annotations=false --created-annotation=false --annotation hello=world -t $target -f $BUDFILES/base-with-labels/Containerfile2 # no annotations should be inherited from base image want_output='map["hello":"world"]' run_buildah inspect --format '{{printf "%q" .ImageAnnotations}}' $target expect_output "$want_output" } @test "bud and test --inherit-annotations with --layers" { base=quay.io/libpod/testimage:20241011 _prefetch $base target=exp run_buildah --version local -a output_fields=($output) buildah_version=${output_fields[2]} buildah inspect --format '{{ .ImageAnnotations }}' $base not_want_output='map[]' assert "$output" != "$not_want_output" "expected some annotations to be set in base image $base" ## Build without removing annotations run_buildah build $WITH_POLICY_JSON --layers --iidfile ${TEST_SCRATCH_DIR}/iid1 -t $target --from $base $BUDFILES/base-with-labels ## Second build must use cache run_buildah build $WITH_POLICY_JSON --layers --iidfile ${TEST_SCRATCH_DIR}/iid2 -t $target --from $base $BUDFILES/base-with-labels ## Must use cache expect_output --substring " Using cache" cmp ${TEST_SCRATCH_DIR}/iid1 ${TEST_SCRATCH_DIR}/iid2 ## Since we are inheriting no annotations, this should not use previous image present in the cache. run_buildah build $WITH_POLICY_JSON --layers --inherit-annotations=false --created-annotation=false -t $target --from $base $BUDFILES/base-with-labels ## should not contain `Using Cache` assert "$output" !~ "Using cache" # no annotations should be inherited from base image want_output='map[]' run_buildah inspect --format '{{printf "%q" .ImageAnnotations}}' $target expect_output "$want_output" ## Build again but set a new annotation and don't inherit annotations from base image run_buildah build $WITH_POLICY_JSON --layers --inherit-annotations=false --created-annotation=false --annotation hello=world -t $target --from $base $BUDFILES/base-with-labels # no annotations should be inherited from base image want_output='map["hello":"world"]' run_buildah inspect --format '{{printf "%q" .ImageAnnotations}}' $target expect_output "$want_output" ### Run similar test again with multiple instructions now ## Build without removing annotations run_buildah build $WITH_POLICY_JSON --layers --iidfile ${TEST_SCRATCH_DIR}/iid1 -t $target -f $BUDFILES/base-with-labels/Containerfile2 ## Second build must use cache run_buildah build $WITH_POLICY_JSON --layers --iidfile ${TEST_SCRATCH_DIR}/iid2 -t $target -f $BUDFILES/base-with-labels/Containerfile2 ## Must use cache expect_output --substring " Using cache" cmp ${TEST_SCRATCH_DIR}/iid1 ${TEST_SCRATCH_DIR}/iid2 ## Since we are inheriting no annotations, this should not use previous image present in the cache. run_buildah build $WITH_POLICY_JSON --layers --inherit-annotations=false --created-annotation=false -t $target -f $BUDFILES/base-with-labels/Containerfile2 ## but this time since 1st instruction is cached it should still use cache for some part, only annotations should not be there expect_output --substring " Using cache" # no annotations should be inherited from base image want_output='map[]' run_buildah inspect --format '{{printf "%q" .ImageAnnotations}}' $target expect_output "$want_output" ## Build again but set a new annotation and don't inherit annotations from base image run_buildah build $WITH_POLICY_JSON --layers --inherit-annotations=false --created-annotation=false --annotation hello=world -t $target -f $BUDFILES/base-with-labels/Containerfile2 ## but this time since 1st instruction is cached it should still use cache for some part, only last instruction will be changed since we are adding ## annotations to it expect_output --substring " Using cache" # no annotations should be inherited from base image want_output='map["hello":"world"]' run_buildah inspect --format '{{printf "%q" .ImageAnnotations}}' $target expect_output "$want_output" } @test "bud with no instructions but with CLI flags that require a new image be written" { _prefetch busybox local contextdir=${TEST_SCRATCH_DIR}/context mkdir $contextdir # Build a multiple-layer image based on busybox. cat > $contextdir/Dockerfile <<-EOF FROM busybox RUN pwd > /pwd.txt LABEL baseimage=true EOF run_buildah build --layers --annotation annotation1=annotationvalue1 --iidfile ${TEST_SCRATCH_DIR}/baseiid.txt ${contextdir} run cat ${TEST_SCRATCH_DIR}/baseiid.txt local baseiid=${output##*:} # Set up to build an image that's based on our image, but with one CLI-motivated change. cat > $contextdir/Dockerfile <<-EOF FROM ${baseiid} EOF # Try them all. for mutatorArg in --squash --label=A=B --env=C=D --annotation=E=F --unsetenv=PATH --unsetlabel=io.buildah.version --unsetannotation=org.opencontainers.image.created --inherit-labels=false --inherit-annotations=false ; do : > ${TEST_SCRATCH_DIR}/iid.txt run_buildah build --iidfile ${TEST_SCRATCH_DIR}/iid.txt ${mutatorArg} ${contextdir} test -s ${TEST_SCRATCH_DIR}/iid.txt run cat ${TEST_SCRATCH_DIR}/iid.txt local iid="${output##*:}" assert ${iid} != ${baseiid} run_buildah inspect -t image ${iid} case "${mutatorArg}" in --squash) # Should have exactly one layer, even though our base had at least two. run jq '.OCIv1.rootfs.diff_ids|length' <<< "$output" assert ${status} == 0 assert "${output}" == 1 # Go back and check the base, as a control. run_buildah inspect -t image ${baseiid} run jq '.OCIv1.rootfs.diff_ids|length' <<< "$output" assert ${status} == 0 assert "${output}" != 1 ;; --label=A=B) # Should have had the requested label set. run jq -r '.OCIv1.config.Labels["A"]' <<< "$output" assert ${status} == 0 assert "${output}" == B # Go back and check the base, as a control. run_buildah inspect -t image ${baseiid} run jq '.OCIv1.config.Labels["A"]' <<< "$output" assert ${status} == 0 assert "${output}" == null ;; --annotation=E=F) # Should have had the requested annotation set. run jq -r '.ImageAnnotations["E"]' <<< "$output" assert ${status} == 0 assert "${output}" == F # Go back and check the base, as a control. run_buildah inspect -t image ${baseiid} run jq '.ImageAnnotations["E"]' <<< "$output" assert ${status} == 0 assert "${output}" == null ;; --unsetenv=PATH) # Should NOT have had a $PATH set. run jq -r '.OCIv1.config.Env' <<< "$output" assert ${status} == 0 assert "${output}" !~ PATH # Go back and check the base, as a control. run_buildah inspect -t image ${baseiid} run jq '.OCIv1.config.Env' <<< "$output" assert ${status} == 0 assert "${output}" =~ PATH ;; --unsetannotation=org.opencontainers.image.created) # Should NOT have had a creation annotation set. run jq -r '.ImageAnnotations' <<< "$output" assert ${status} == 0 assert "${output}" !~ created # Go back and check the base, as a control. run_buildah inspect -t image ${baseiid} run jq '.ImageAnnotations' <<< "$output" assert ${status} == 0 assert "${output}" =~ org.opencontainers.image.created ;; --inherit-labels=false) # Should NOT have had a "baseimage" label. run jq -r '.OCIv1.config.Labels["baseimage"]' <<< "$output" assert ${status} == 0 assert "${output}" == null # Go back and check the base, as a control. run_buildah inspect -t image ${baseiid} run jq -r '.OCIv1.config.Labels["baseimage"]' <<< "$output" assert ${status} == 0 assert "${output}" == true ;; --inherit-annotations=false) # Should NOT have had an "annotation1" annotation. run jq -r '.ImageAnnotations["annotation1"]' <<< "$output" assert ${status} == 0 assert "${output}" == null # Go back and check the base, as a control. run_buildah inspect -t image ${baseiid} run jq -r '.ImageAnnotations["annotation1"]' <<< "$output" assert ${status} == 0 assert "${output}" == annotationvalue1 ;; esac done } @test "bud: build push with --force-compression" { skip_if_no_podman blobcachedir=${TEST_SCRATCH_DIR}/blobcachelocal mkdir -p ${blobcachedir} local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir # Make sure this is an image never used in any other zstd tests, # nor with any layers used in zstd tests. That could lead to a # different test pushing it zstd, and a "did not expect zstd" # failure below. echo "$(date --utc --iso-8601=seconds) this is a unique layer $(random_string)" >$contextdir/therecanbeonly1 cat > $contextdir/Containerfile << _EOF FROM scratch COPY /therecanbeonly1 /uniquefile _EOF imgname="img-$(safename)" start_registry run_buildah login --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth --username testuser --password testpassword localhost:${REGISTRY_PORT} run_buildah build $WITH_POLICY_JSON -t $imgname --platform linux/amd64 $contextdir # Helper function. push our image with the given options, and run skopeo inspect function _test_buildah_push() { run_buildah push \ --blob-cache=${blobcachedir} \ $WITH_POLICY_JSON \ --authfile ${TEST_SCRATCH_DIR}/test.auth \ --tls-verify=false \ $* \ $imgname \ docker://localhost:${REGISTRY_PORT}/$imgname echo "# skopeo inspect $imgname" run podman run --rm \ --mount type=bind,src=${TEST_SCRATCH_DIR}/test.auth,target=/test.auth,Z \ --net host \ quay.io/skopeo/stable inspect \ --authfile=/test.auth \ --tls-verify=false \ --raw \ docker://localhost:${REGISTRY_PORT}/$imgname echo "$output" } # layers should have no trace of zstd since push was with --compression-format gzip _test_buildah_push --compression-format gzip assert "$output" !~ "zstd" "zstd found in layers where push was with --compression-format gzip" # layers should have no trace of zstd since push is --force-compression=false _test_buildah_push --compression-format zstd --force-compression=false assert "$output" !~ "zstd" "zstd found even though push was without --force-compression" # layers should container `zstd` _test_buildah_push --compression-format zstd expect_output --substring "zstd" "layers must contain zstd compression" # layers should container `zstd` _test_buildah_push --compression-format zstd --force-compression expect_output --substring "zstd" "layers must contain zstd compression" } @test "bud with --dns* flags" { _prefetch alpine for dnsopt in --dns --dns-option --dns-search; do run_buildah 125 build $dnsopt=example.com --network=none $WITH_POLICY_JSON -f $BUDFILES/dns/Dockerfile $BUDFILES/dns expect_output "Error: the $dnsopt option cannot be used with --network=none" "dns options should not be allowed with --network=none" done run_buildah build --dns-search=example.com --dns=223.5.5.5 --dns-option=use-vc $WITH_POLICY_JSON -f $BUDFILES/dns/Dockerfile $BUDFILES/dns expect_output --substring "search example.com" expect_output --substring "nameserver 223.5.5.5" expect_output --substring "options use-vc" } @test "build with inline RUN --network=host" { _prefetch alpine #hostns=$(readlink /proc/self/ns/net) run readlink /proc/self/ns/net hostns="$output" run_buildah build $WITH_POLICY_JSON -t source -f $BUDFILES/inline-network/Dockerfile1 expect_output --from="${lines[2]}" "${hostns}" } @test "build with inline RUN --network=none" { _prefetch alpine run_buildah 1 build $WITH_POLICY_JSON -t source -f $BUDFILES/inline-network/Dockerfile2 expect_output --substring "wget: bad address" } @test "build with inline RUN --network=fake" { _prefetch alpine run_buildah 125 build $WITH_POLICY_JSON -t source -f $BUDFILES/inline-network/Dockerfile3 expect_output --substring "unsupported value" } @test "build with inline default RUN --network=default" { skip_if_chroot _prefetch alpine run readlink /proc/self/ns/net hostns=$output run_buildah build --network=host $WITH_POLICY_JSON -t source -f $BUDFILES/inline-network/Dockerfile4 firstns=${lines[2]} assert "${hostns}" == "$firstns" run_buildah build --network=private $WITH_POLICY_JSON -t source -f $BUDFILES/inline-network/Dockerfile4 secondns=${lines[2]} assert "$secondns" != "$firstns" } @test "bud with ignoresymlink on default file" { _prefetch alpine echo hello > ${TEST_SCRATCH_DIR}/private_file cp -a $BUDFILES/container-ignoresymlink ${TEST_SCRATCH_DIR}/container-ignoresymlink ln -s ${TEST_SCRATCH_DIR}/private_file ${TEST_SCRATCH_DIR}/container-ignoresymlink/.dockerignore run_buildah build $WITH_POLICY_JSON -t test -f Dockerfile $BUDFILES/container-ignoresymlink # Should ignore a .dockerignore or .containerignore that's a symlink to somewhere outside of the build context expect_output --substring "hello" } # Verify https://github.com/containers/buildah/issues/4342 @test "buildkit-mount type=cache should not hang if cache is wiped in between" { _prefetch alpine containerfile=$BUDFILES/cache-mount-locked/Containerfile run_buildah build $WITH_POLICY_JSON --build-arg WIPE_CACHE=1 -t source -f $containerfile $BUDFILES/cache-mount-locked # build should be success and must contain `hello` from `file` in last step expect_output --substring "hello" } # Test for https://github.com/containers/buildah/pull/4295 @test "build test warning for preconfigured TARGETARCH, TARGETOS, TARGETPLATFORM or TARGETVARIANT" { containerfile=$BUDFILES/platform-sets-args/Containerfile # Containerfile must contain one or more (four, as of 2022-10) lines # of the form 'ARG TARGETxxx' for each of the variables of interest. local -a checkvars=($(sed -ne 's/^ARG //p' <$containerfile)) assert "${checkvars[*]}" != "" \ "INTERNAL ERROR! No 'ARG xxx' lines in $containerfile!" ARCH=$(go env GOARCH) # With explicit and full --platform, buildah should not warn. run_buildah build $WITH_POLICY_JSON --platform linux/amd64/v2 \ -t source -f $containerfile assert "$output" =~ "image platform \(linux/amd64\) does not match the expected platform" \ "With explicit --platform, buildah should warn about pulling difference in platform" assert "$output" =~ "TARGETOS=linux" " --platform TARGETOS set correctly" assert "$output" =~ "TARGETARCH=amd64" " --platform TARGETARCH set correctly" # By default, BUILDVARIANT/TARGETVARIANT should be empty. assert "$output" =~ "TARGETVARIANT=[[:cntrl:]]" " --platform TARGETVARIANT set correctly" assert "$output" =~ "TARGETPLATFORM=linux/amd64/v2" " --platform TARGETPLATFORM set correctly" # Likewise with individual args run_buildah build $WITH_POLICY_JSON --os linux --arch amd64 --variant v2 \ -t source -f $containerfile assert "$output" =~ "image platform \(linux/amd64\) does not match the expected platform" \ "With explicit --variant, buildah should warn about pulling difference in platform" assert "$output" =~ "TARGETOS=linux" "--os --arch --variant TARGETOS set correctly" assert "$output" =~ "TARGETARCH=amd64" "--os --arch --variant TARGETARCH set correctly" # By default, BUILDVARIANT/TARGETVARIANT should be empty. assert "$output" =~ "TARGETVARIANT=[[:cntrl:]]" "--os --arch --variant TARGETVARIANT set correctly" assert "$output" =~ "TARGETPLATFORM=linux/amd64" "--os --arch --variant TARGETPLATFORM set correctly" run_buildah build $WITH_POLICY_JSON --os linux -t source -f $containerfile assert "$output" !~ "WARNING" \ "With explicit --os (but no arch/variant), buildah should not warn about TARGETOS" assert "$output" =~ "TARGETOS=linux" "--os TARGETOS set correctly" assert "$output" =~ "TARGETARCH=${ARCH}" "--os TARGETARCH set correctly" # By default, BUILDVARIANT/TARGETVARIANT should be empty. assert "$output" =~ "TARGETVARIANT=[[:cntrl:]]" "--os TARGETVARIANT set correctly" assert "$output" =~ "TARGETPLATFORM=linux/${ARCH}" "--os TARGETPLATFORM set correctly" run_buildah build $WITH_POLICY_JSON --arch amd64 -t source -f $containerfile assert "$output" !~ "WARNING" \ "With explicit --os (but no arch/variant), buildah should not warn about TARGETOS" assert "$output" =~ "TARGETOS=linux" "--arch TARGETOS set correctly" assert "$output" =~ "TARGETARCH=amd64" "--arch TARGETARCH set correctly" # By default, BUILDVARIANT/TARGETVARIANT should be empty. assert "$output" =~ "TARGETVARIANT=[[:cntrl:]]" "--arch TARGETVARIANT set correctly" assert "$output" =~ "TARGETPLATFORM=linux/amd64" "--arch TARGETPLATFORM set correctly" for option in "--arch=arm64" "--os=windows" "--variant=v2"; do run_buildah 125 build $WITH_POLICY_JSON --platform linux/amd64 ${option} \ -t source -f $containerfile assert "$output" =~ "invalid --platform may not be used with --os, --arch, or --variant" "can't use --platform and one of --os, --arch or --variant together" done } @test "build-conflicting-isolation-chroot-and-network" { _prefetch alpine cat > ${TEST_SCRATCH_DIR}/Containerfile << _EOF FROM alpine RUN ping -c 1 4.2.2.2 _EOF run_buildah 125 build --network=none --isolation=chroot $WITH_POLICY_JSON ${TEST_SCRATCH_DIR} expect_output --substring "cannot set --network other than host with --isolation chroot" BUILDAH_ISOLATION=chroot run_buildah 125 build --network=none $WITH_POLICY_JSON ${TEST_SCRATCH_DIR} expect_output --substring "cannot set --network other than host with --isolation chroot" } @test "bud with .dockerignore #1" { _prefetch alpine busybox run_buildah 125 build -t testbud $WITH_POLICY_JSON -f $BUDFILES/dockerignore/Dockerfile $BUDFILES/dockerignore expect_output --substring 'building.*"COPY subdir \./".*no such file or directory' run_buildah build -t testbud $WITH_POLICY_JSON -f $BUDFILES/dockerignore/Dockerfile.succeed $BUDFILES/dockerignore run_buildah from --name myctr testbud run_buildah 1 run myctr ls -l test1.txt run_buildah run myctr ls -l test2.txt run_buildah 1 run myctr ls -l sub1.txt run_buildah 1 run myctr ls -l sub2.txt run_buildah 1 run myctr ls -l subdir/ } @test "bud --layers with --mount type bind should burst cache if symlink is changed" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/samplefile1 << _EOF First symlink content _EOF cat > $contextdir/samplefile2 << _EOF Second symlink content _EOF # Create symlink to samplefile1 and we will mount that ln -s samplefile1 $contextdir/tomount pwd ls $contextdir/ cat $contextdir/tomount cat > $contextdir/Containerfile << _EOF FROM alpine RUN --mount=type=bind,source=tomount,target=file,Z cat file _EOF # on first run since there is no cache so `samplefile1` must be printed run_buildah build $WITH_POLICY_JSON --layers -t source -f $contextdir/Containerfile $contextdir expect_output --substring "First symlink content" run_buildah build $WITH_POLICY_JSON --layers -t source -f $contextdir/Containerfile $contextdir # output should not contain content from the file since entire build is cached assert "$output" !~ "First symlink content" # Modify the symlink ln -sf samplefile2 $contextdir/tomount # on third run since we have changed symlink so cache must burst. run_buildah build $WITH_POLICY_JSON --layers -t source -f $contextdir/Containerfile $contextdir expect_output --substring "Second symlink content" } @test "bud --layers with --mount type bind should burst cache if content is changed" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/samplefile << _EOF samplefile _EOF cat > $contextdir/Containerfile << _EOF FROM alpine RUN --mount=type=bind,target=/test,Z ls /test _EOF # on first run since there is no cache so `samplefile` must be printed run_buildah build $WITH_POLICY_JSON --layers -t source -f $contextdir/Containerfile $contextdir expect_output --substring "samplefile" # on second run since there is cache so `samplefile` should not be printed run_buildah build $WITH_POLICY_JSON --layers -t source -f $contextdir/Containerfile $contextdir # output should not contain `samplefile` assert "$output" !~ "samplefile" cat > $contextdir/anotherfile << _EOF anotherfile _EOF # on third run since we have added new file `anotherfile` so cache must burst. run_buildah build $WITH_POLICY_JSON --layers -t source -f $contextdir/Containerfile $contextdir expect_output --substring "samplefile" expect_output --substring "anotherfile" } @test "bud --layers with --mount type bind should burst and multiple mounts cache if content is changed" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/samplefile << _EOF samplefile _EOF cat > $contextdir/testfile << _EOF Helloworld _EOF cat > $contextdir/Containerfile << _EOF FROM alpine RUN --mount=type=bind,target=/test,Z --mount=type=bind,source=testfile,target=testfile,Z ls /test && cat testfile _EOF # on first run since there is no cache so `samplefile` must be printed run_buildah build $WITH_POLICY_JSON --layers -t source -f $contextdir/Containerfile $contextdir expect_output --substring "samplefile" expect_output --substring "Helloworld" # on second run since there is cache so `samplefile` should not be printed run_buildah build $WITH_POLICY_JSON --layers -t source -f $contextdir/Containerfile $contextdir # output should not contain `samplefile` assert "$output" !~ "samplefile" # Modify sample file 2 cat > $contextdir/testfile << _EOF Helloworld2 _EOF # on third run since we have modified `testfile` so cache must burst. run_buildah build $WITH_POLICY_JSON --layers -t source -f $contextdir/Containerfile $contextdir expect_output --substring "samplefile" expect_output --substring "Helloworld2" } @test "bud --layers with --mount type bind should burst cache if content is changed - source is additional build context" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/samplefile << _EOF samplefile2 _EOF cat > $contextdir/Containerfile << _EOF FROM alpine RUN --mount=type=bind,from=one,target=/test,Z ls /test _EOF # on first run since there is no cache so `samplefile` must be printed run_buildah build $WITH_POLICY_JSON --build-context one=$contextdir --layers -t source -f $contextdir/Containerfile expect_output --substring "samplefile" # on second run since there is cache so `samplefile` should not be printed run_buildah build $WITH_POLICY_JSON --build-context one=$contextdir --layers -t source -f $contextdir/Containerfile # output should not `samplefile` since cache is being used assert "$output" !~ "samplefile" cat > $contextdir/anotherfile << _EOF anotherfile2 _EOF # on third run since we have added new file `anotherfile` so cache must burst. run_buildah build $WITH_POLICY_JSON --build-context one=$contextdir --layers -t source -f $contextdir/Containerfile expect_output --substring "samplefile" expect_output --substring "anotherfile" } @test "bud --layers should not hit cache if heredoc is changed" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/Dockerfile << _EOF FROM alpine RUN <> /hello echo "Cache burst second line" >> /hello EOF RUN cat hello _EOF # on first run since there is no cache so `Cache burst` must be printed run_buildah build $WITH_POLICY_JSON --layers -t source -f $contextdir/Dockerfile expect_output --substring "Cache burst second line" # on second run since there is cache so `Cache burst` should not be printed run_buildah build $WITH_POLICY_JSON --layers -t source -f $contextdir/Dockerfile # output should not contain cache burst assert "$output" !~ "Cache burst second line" cat > $contextdir/Dockerfile << _EOF FROM alpine RUN <> /hello EOF RUN cat hello _EOF # on third run since we have changed heredoc so `Cache burst` must be printed. run_buildah build $WITH_POLICY_JSON --layers -t source -f $contextdir/Dockerfile expect_output --substring "Cache burst add diff" } @test "bud --layers with --mount type bind should preserve cache when file mod time changes but content stays same" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/samplefile << _EOF samplefile content unchanged _EOF cat > $contextdir/Containerfile << _EOF FROM alpine RUN --mount=type=bind,source=samplefile,target=file,Z cat file _EOF # on first run since there is no cache so content must be printed run_buildah build $WITH_POLICY_JSON --layers -t source -f $contextdir/Containerfile $contextdir expect_output --substring "samplefile content unchanged" # on second run since there is cache so content should not be printed run_buildah build $WITH_POLICY_JSON --layers -t source -f $contextdir/Containerfile $contextdir # output should not contain content from the file since entire build is cached assert "$output" !~ "samplefile content unchanged" # Change mod time of this file (this changes modification time) touch -d "@1577836800" $contextdir/samplefile # on third run since content is unchanged, cache should still be used despite different mod time run_buildah build $WITH_POLICY_JSON --layers -t source -f $contextdir/Containerfile $contextdir # output should not contain content from the file since cache should still be valid assert "$output" !~ "samplefile content unchanged" } @test "bud --layers should not hit cache if heredoc is changed - with ARG" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/Dockerfile << _EOF FROM alpine ARG key=value RUN <> /hello echo "Cache burst second line" >> /hello EOF RUN cat hello _EOF # on first run since there is no cache so `Cache burst` must be printed run_buildah build $WITH_POLICY_JSON --layers -t source -f $contextdir/Dockerfile expect_output --substring "Cache burst second line" # on second run since there is cache so `Cache burst` should not be printed run_buildah build $WITH_POLICY_JSON --layers -t source -f $contextdir/Dockerfile # output should not contain cache burst assert "$output" !~ "Cache burst second line" cat > $contextdir/Dockerfile << _EOF FROM alpine ARG key=value RUN <> /hello EOF RUN cat hello _EOF # on third run since we have changed heredoc so `Cache burst` must be printed. run_buildah build $WITH_POLICY_JSON --layers -t source -f $contextdir/Dockerfile expect_output --substring "Cache burst add diff" } @test "bud build with heredoc content" { _prefetch alpine run_buildah build -t heredoc $WITH_POLICY_JSON -f $BUDFILES/heredoc/Containerfile . expect_output --substring "print first line from heredoc" expect_output --substring "print second line from heredoc" expect_output --substring "Heredoc writing first file" expect_output --substring "some text of first file" expect_output --substring "file passed to program" expect_output --substring "and it contains multiple" expect_output --substring "(your index page goes here)" expect_output --substring "(robots content)" expect_output --substring "(humans content)" expect_output --substring "this is the output of test6 part1" expect_output --substring "this is the output of test6 part2" expect_output --substring "this is the output of test7 part1" expect_output --substring "this is the output of test7 part2" expect_output --substring "this is the output of test7 part3" expect_output --substring "this is the output of test8 part1" expect_output --substring "this is the output of test8 part2" # verify that build output contains summary of heredoc content expect_output --substring 'RUN <> /file1...)' expect_output --substring 'RUN cat <${TMPDIR}/filelist.actual #echo "$expect" >${TMPDIR}/filelist.expect expect_output --from="$filelist" "$expect" "container file list" } @test "bud with .dockerignore #2" { _prefetch busybox run_buildah 125 build -t testbud3 $WITH_POLICY_JSON $BUDFILES/dockerignore3 expect_output --substring 'building.*"COPY test1.txt /upload/test1.txt".*no such file or directory' expect_output --substring 'filtered out using /[^ ]*/.dockerignore' } @test "bud with .dockerignore #4" { _prefetch busybox run_buildah 125 build -t testbud3 $WITH_POLICY_JSON -f Dockerfile.test $BUDFILES/dockerignore4 expect_output --substring 'building.*"COPY test1.txt /upload/test1.txt".*no such file or directory' expect_output --substring '1 filtered out using /[^ ]*/Dockerfile.test.dockerignore' } @test "bud with .dockerignore #6" { _prefetch alpine busybox run_buildah 125 build -t testbud $WITH_POLICY_JSON -f $BUDFILES/dockerignore6/Dockerfile $BUDFILES/dockerignore6 expect_output --substring 'building.*"COPY subdir \./".*no such file or directory' run_buildah build -t testbud $WITH_POLICY_JSON -f $BUDFILES/dockerignore6/Dockerfile.succeed $BUDFILES/dockerignore6 run_buildah from --name myctr testbud run_buildah 1 run myctr ls -l test1.txt run_buildah run myctr ls -l test2.txt run_buildah 1 run myctr ls -l sub1.txt run_buildah 1 run myctr ls -l sub2.txt run_buildah 1 run myctr ls -l subdir/ } @test "build with --platform without OS" { run_buildah info --format '{{.host.arch}}' myarch="$output" run_buildah build --platform $myarch $WITH_POLICY_JSON -t test -f $BUDFILES/base-with-arg/Containerfile expect_output --substring "This is built for $myarch" ## podman-remote binding has a bug where is sends `--platform as /` run_buildah build --platform "/" $WITH_POLICY_JSON -t test -f $BUDFILES/base-with-arg/Containerfile expect_output --substring "This is built for $myarch" } @test "build with basename resolving default arg" { run_buildah info --format '{{.host.os}}/{{.host.arch}}{{if .host.variant}}/{{.host.variant}}{{end}}' myplatform="$output" run_buildah info --format '{{.host.arch}}' myarch="$output" run_buildah build --platform ${myplatform} $WITH_POLICY_JSON -t test -f $BUDFILES/base-with-arg/Containerfile expect_output --substring "This is built for $myarch" run_buildah build $WITH_POLICY_JSON -t test -f $BUDFILES/base-with-arg/Containerfile expect_output --substring "This is built for $myarch" } @test "build with basename resolving user arg" { _prefetch alpine run_buildah build --build-arg CUSTOM_TARGET=first $WITH_POLICY_JSON -t test -f $BUDFILES/base-with-arg/Containerfile2 expect_output --substring "This is built for first" run_buildah build --build-arg CUSTOM_TARGET=second $WITH_POLICY_JSON -t test -f $BUDFILES/base-with-arg/Containerfile2 expect_output --substring "This is built for second" } @test "build with basename resolving user arg from file" { _prefetch alpine run_buildah build \ --build-arg-file $BUDFILES/base-with-arg/first.args \ $WITH_POLICY_JSON -t test -f $BUDFILES/base-with-arg/Containerfile2 expect_output --substring "This is built for first" run_buildah build \ --build-arg-file $BUDFILES/base-with-arg/second.args \ $WITH_POLICY_JSON -t test -f $BUDFILES/base-with-arg/Containerfile2 expect_output --substring "This is built for second" } @test "build with basename resolving user arg from latest file in arg list" { _prefetch alpine run_buildah build \ --build-arg-file $BUDFILES/base-with-arg/second.args \ --build-arg-file $BUDFILES/base-with-arg/first.args \ $WITH_POLICY_JSON -t test -f $BUDFILES/base-with-arg/Containerfile2 expect_output --substring "This is built for first" } @test "build with basename resolving user arg from in arg list" { _prefetch alpine run_buildah build \ --build-arg-file $BUDFILES/base-with-arg/second.args \ --build-arg CUSTOM_TARGET=first \ $WITH_POLICY_JSON -t test -f $BUDFILES/base-with-arg/Containerfile2 expect_output --substring "This is built for first" } # Following test should fail since we are trying to use build-arg which # was not declared. Honors discussion here: https://github.com/containers/buildah/pull/4061/commits/1237c04d6ae0ee1f027a1f02bf3ab5c57ac7d9b6#r906188374 @test "build with basename resolving user arg - should fail" { _prefetch alpine run_buildah 125 build --build-arg CUSTOM_TARGET=first $WITH_POLICY_JSON -t test -f $BUDFILES/base-with-arg/Containerfilebad expect_output --substring "invalid reference format" } # Try building with arch and variant # Issue: https://github.com/containers/buildah/issues/4276 @test "build-with-inline-platform-and-variant" { local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/Dockerfile << _EOF FROM --platform=freebsd/arm64/v8 scratch COPY . . _EOF run_buildah build $WITH_POLICY_JSON -t test $contextdir run_buildah inspect --format '{{ .OCIv1.Architecture }}' test expect_output --substring "arm64" run_buildah inspect --format '{{ .OCIv1.Variant }}' test expect_output --substring "v8" } # Following test must fail since we are trying to run linux/arm64 on linux/amd64 # Issue: https://github.com/containers/buildah/issues/3712 @test "build-with-inline-platform" { # Host arch local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir run_buildah info --format '{{.host.arch}}' myarch="$output" otherarch="arm64" # just make sure that other arch is not equivalent to host arch if [[ "$otherarch" == "$myarch" ]]; then otherarch="amd64" fi # ...create a Containerfile with --platform=linux/$otherarch cat > $contextdir/Dockerfile << _EOF FROM --platform=linux/${otherarch} alpine RUN uname -m _EOF run_buildah '?' build $WITH_POLICY_JSON -t test $contextdir if [[ $status -eq 0 ]]; then run_buildah inspect --format '{{ .OCIv1.Architecture }}' test expect_output --substring "$otherarch" else # Build failed: we DO NOT have qemu-user-static installed. expect_output --substring "format error" fi } @test "build-with-inline-platform-and-rely-on-defaultbuiltinargs" { # Get host arch run_buildah info --format '{{.host.arch}}' myarch="$output" otherarch="arm64" # just make sure that other arch is not equivalent to host arch if [[ "$otherarch" == "$myarch" ]]; then otherarch="amd64" fi run_buildah build --platform linux/$otherarch $WITH_POLICY_JSON -t test -f $BUDFILES/multiarch/Dockerfile.built-in-args expect_output --substring "I'm compiling for linux/$otherarch" expect_output --substring "and tagging for linux/$otherarch" expect_output --substring "and OS linux" expect_output --substring "and ARCH $otherarch" run_buildah inspect --format '{{ .OCIv1.Architecture }}' test expect_output --substring "$otherarch" } # Buildkit parity: this verifies if we honor custom overrides of TARGETOS, TARGETVARIANT, TARGETARCH and TARGETPLATFORM if user wants @test "build-with-inline-platform-and-rely-on-defaultbuiltinargs-check-custom-override" { run_buildah build --platform linux/arm64 $WITH_POLICY_JSON --build-arg TARGETOS=android -t test -f $BUDFILES/multiarch/Dockerfile.built-in-args expect_output --substring "I'm compiling for linux/arm64" expect_output --substring "and tagging for linux/arm64" ## Note since we used --build-arg and overrode OS, OS must be android expect_output --substring "and OS android" expect_output --substring "and ARCH $otherarch" run_buildah inspect --format '{{ .OCIv1.Architecture }}' test expect_output --substring "$otherarch" } # Following test must pass since we want to tag image as host arch # Test for use-case described here: https://github.com/containers/buildah/issues/3261 @test "build-with-inline-platform-amd-but-tag-as-arm" { # Host arch local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir run_buildah info --format '{{.host.arch}}' myarch="$output" targetarch="arm64" if [[ "$targetArch" == "$myarch" ]]; then targetarch="amd64" fi cat > $contextdir/Dockerfile << _EOF FROM --platform=linux/${myarch} alpine RUN uname -m _EOF # Tries building image where baseImage has --platform=linux/HostArch run_buildah build --platform linux/${targetarch} $WITH_POLICY_JSON -t test $contextdir run_buildah inspect --format '{{ .OCIv1.Architecture }}' test # base image is pulled as HostArch but tagged as non host arch expect_output --substring $targetarch } # Test build with --add-history=false @test "build-with-omit-history-to-true should not add history" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/Dockerfile1 << _EOF FROM alpine RUN echo hello RUN echo world _EOF # Built image must not contain history for the layers which we have just built. run_buildah build $WITH_POLICY_JSON --omit-history -t source -f $contextdir/Dockerfile1 run_buildah inspect --format "{{index .Docker.History}}" source expect_output "[]" run_buildah inspect --format "{{index .OCIv1.History}}" source expect_output "[]" run_buildah inspect --format "{{index .History}}" source expect_output "[]" } # Test building with --userns=auto @test "build with --userns=auto also with size" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir user=$USER if [[ "$user" == "root" ]]; then user="containers" fi if ! grep -q $user "/etc/subuid"; then skip "cannot find mappings for the current user" fi cat > $contextdir/Dockerfile << _EOF FROM alpine RUN cat /proc/self/uid_map RUN echo hello FROM alpine COPY --from=0 /tmp /tmp RUN cat /proc/self/uid_map RUN ls -a _EOF run_buildah build --userns=auto $WITH_POLICY_JSON -t source -f $contextdir/Dockerfile expect_output --substring "1024" run_buildah build --userns=auto:size=500 $WITH_POLICY_JSON -t source -f $contextdir/Dockerfile expect_output --substring "500" } # Test building with --userns=auto with uidmapping @test "build with --userns=auto with uidmapping" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir user=$USER if [[ "$user" == "root" ]]; then user="containers" fi if ! grep -q $user "/etc/subuid"; then skip "cannot find mappings for the current user" fi cat > $contextdir/Dockerfile << _EOF FROM alpine RUN cat /proc/self/uid_map _EOF run_buildah build --userns=auto:size=8192,uidmapping=0:0:1 $WITH_POLICY_JSON -t source -f $contextdir/Dockerfile expect_output --substring "8191" run_buildah build --userns=auto:uidmapping=0:0:1 $WITH_POLICY_JSON -t source -f $contextdir/Dockerfile expect_output --substring " 0 0 1" } # Test building with --userns=auto with gidmapping @test "build with --userns=auto with gidmapping" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir user=$USER if [[ "$user" == "root" ]]; then user="containers" fi if ! grep -q $user "/etc/subuid"; then skip "cannot find mappings for the current user" fi cat > $contextdir/Dockerfile << _EOF FROM alpine RUN cat /proc/self/gid_map _EOF run_buildah build --userns=auto:size=8192,gidmapping=0:0:1 $WITH_POLICY_JSON -t source -f $contextdir/Dockerfile expect_output --substring "8191" run_buildah build --userns=auto:gidmapping=0:0:1 $WITH_POLICY_JSON -t source -f $contextdir/Dockerfile expect_output --substring " 0 0 1" } # Test bud with prestart hook @test "build-test with OCI prestart hook" { skip_if_in_container # This works in privileged container setup but does not works in CI setup _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir/hooks cat > $contextdir/Dockerfile << _EOF FROM alpine RUN echo hello _EOF cat > $contextdir/hooks/test.json << _EOF { "version": "1.0.0", "hook": { "path": "$contextdir/hooks/test" }, "when": { "always": true }, "stages": ["prestart"] } _EOF cat > $contextdir/hooks/test << _EOF #!/bin/sh echo from-hook > $contextdir/hooks/hook-output _EOF # make actual hook executable chmod +x $contextdir/hooks/test run_buildah build $WITH_POLICY_JSON -t source --hooks-dir=$contextdir/hooks -f $contextdir/Dockerfile run cat $contextdir/hooks/hook-output expect_output --substring "from-hook" } @test "build with add resolving to invalid HTTP status code" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/build-context mkdir -p $contextdir local contentdir=${TEST_SCRATCH_DIR}/content mkdir -p $contentdir starthttpd $contentdir cat > $contextdir/Dockerfile << _EOF FROM alpine ADD http://0.0.0.0:${HTTP_SERVER_PORT}/test / _EOF run_buildah 125 build $WITH_POLICY_JSON -t source -f $contextdir/Dockerfile expect_output --substring "invalid response status" } @test "build test has gid in supplemental groups" { _prefetch alpine run_buildah build $WITH_POLICY_JSON -t source -f $BUDFILES/supplemental-groups/Dockerfile # gid 1000 must be in supplemental groups expect_output --substring "Groups: 1000" } @test "build-mount-cache-with-id-mappings" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/context mkdir ${contextdir} # with no ID mappings local cacheid=${SRANDOM} cat > ${contextdir}/Dockerfile << EOF FROM alpine USER 1000:1000 RUN --mount=type=cache,id=${cacheid},target=/var/tmp,uid=1000,gid=1000 stat / /var/tmp RUN --mount=type=cache,id=${cacheid},target=/var/tmp,uid=1000,gid=1000 test \`stat -c %u /var/tmp\` -eq 1000 RUN --mount=type=cache,id=${cacheid},target=/var/tmp,uid=1000,gid=1000 touch /var/tmp/should-be-able-to-write RUN --mount=type=cache,id=${cacheid},target=/new-parent/var/tmp,uid=1000,gid=1000 touch /var/tmp/should-be-able-to-write RUN --mount=type=cache,id=${cacheid},target=/var/new-parent/tmp,uid=1000,gid=1000 touch /var/tmp/should-be-able-to-write EOF run_buildah build $WITH_POLICY_JSON ${contextdir} # with non-default ID mappings local cacheid=${SRANDOM} cat > ${contextdir}/Dockerfile << EOF FROM alpine USER 1000:1000 RUN --mount=type=cache,id=${cacheid},target=/var/tmp,uid=1000,gid=1000 stat / /var/tmp RUN --mount=type=cache,id=${cacheid},target=/var/tmp,uid=1000,gid=1000 test \`stat -c %u /var/tmp\` -eq 1000 RUN --mount=type=cache,id=${cacheid},target=/var/tmp,uid=1000,gid=1000 touch /var/tmp/should-be-able-to-write RUN --mount=type=cache,id=${cacheid},target=/new/parent/var/tmp,uid=1000,gid=1000 touch /new/parent/var/tmp/should-be-able-to-write RUN --mount=type=cache,id=${cacheid},target=/var/new/parent/tmp,uid=1000,gid=1000 touch /var/new/parent/tmp/should-be-able-to-write EOF if test `id -u` -eq 0 ; then run_buildah build --userns-uid-map 0:1:1023 --userns-gid-map 0:1:1023 $WITH_POLICY_JSON ${contextdir} else run_buildah build --userns auto:size=1023 $WITH_POLICY_JSON ${contextdir} fi } @test "build-mount-cache-writeable-as-unprivileged-user" { _prefetch busybox local contextdir=${TEST_SCRATCH_DIR}/context mkdir ${contextdir} cat > ${contextdir}/Dockerfile << EOF FROM busybox USER 1000:1000 RUN --mount=type=cache,target=/usr/local/bin,id=/usr/local/bin/$$,uid=1000,gid=1000 touch /usr/local/bin/new-file RUN --mount=type=cache,target=/var/not/already/there,id=/var/not/already/there/$$,uid=1000,gid=1000 touch /var/not/already/there/new-file EOF run_buildah build $WITH_POLICY_JSON ${contextdir} } @test "build-mount-bind-readable-as-unprivileged-user" { _prefetch busybox local contextdir=${TEST_SCRATCH_DIR}/context mkdir ${contextdir} cat > ${contextdir}/Dockerfile << EOF FROM busybox USER 1000:1000 RUN --mount=type=bind,target=/usr/local,from=busybox busybox ls /usr/local/bin/busybox RUN --mount=type=bind,target=/var/not/already/there,from=busybox busybox ls /var/not/already/there/bin/busybox EOF run_buildah build $WITH_POLICY_JSON ${contextdir} } @test "build-mount-secret-readable-as-unprivileged-user" { _prefetch busybox local contextdir=${TEST_SCRATCH_DIR}/context mkdir ${contextdir} local secretfile=${TEST_SCRATCH_DIR}/secret.txt echo -n hidingInPlainSight > ${secretfile} cat > ${contextdir}/Dockerfile << EOF FROM busybox USER 1000:1000 RUN --mount=type=secret,id=theSecret,target=/var/not/already/there,uid=1000,gid=1000 wc -c /var/not/already/there EOF run_buildah build --secret id=theSecret,type=file,src=${secretfile} $WITH_POLICY_JSON ${contextdir} cat > ${contextdir}/Dockerfile << EOF FROM busybox USER 1000:1000 RUN --mount=type=secret,id=theSecret,target=/top/var/tmp/there,uid=1000,gid=1000 wc -c /top/var/tmp/there EOF run_buildah build --secret id=theSecret,type=file,src=${secretfile} $WITH_POLICY_JSON ${contextdir} } @test "build test if supplemental groups has gid with --isolation chroot" { test "${BUILDAH_ISOLATION}" != chroot || skip "BUILDAH_ISOLATION=${BUILDAH_ISOLATION} overrides --isolation" _prefetch alpine run_buildah build --isolation chroot $WITH_POLICY_JSON -t source -f $BUDFILES/supplemental-groups/Dockerfile # gid 1000 must be in supplemental groups expect_output --substring "Groups: 1000" } @test "build-test --mount=type=secret test relative to workdir mount" { _prefetch alpine local contextdir=$BUDFILES/secret-relative run_buildah build $WITH_POLICY_JSON --no-cache --secret id=secret-foo,src=$contextdir/secret1.txt --secret id=secret-bar,src=$contextdir/secret2.txt -t test -f $contextdir/Dockerfile expect_output --substring "secret:foo" expect_output --substring "secret:bar" } @test "build-test --mount=type=cache test relative to workdir mount" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir ## write-cache cat > $contextdir/Dockerfile << _EOF FROM alpine RUN mkdir test WORKDIR test RUN --mount=type=cache,id=YfHI60aApFM-target,target=target echo world > /test/target/hello _EOF run_buildah build $WITH_POLICY_JSON -t source -f $contextdir/Dockerfile cat > $contextdir/Dockerfile << _EOF FROM alpine RUN mkdir test WORKDIR test RUN --mount=type=cache,id=YfHI60aApFM-target,target=target cat /test/target/hello _EOF run_buildah build $WITH_POLICY_JSON -t source -f $contextdir/Dockerfile expect_output --substring "world" } @test "build-test do not use mount stage from cache if it was rebuilt" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/Dockerfile << _EOF FROM alpine as dependencies RUN mkdir /build && echo v1 > /build/version FROM alpine RUN --mount=type=bind,source=/build,target=/build,from=dependencies \ cp /build/version /version RUN cat /version _EOF run_buildah build $WITH_POLICY_JSON --layers -t source -f $contextdir/Dockerfile run_buildah build $WITH_POLICY_JSON --layers -t source2 -f $contextdir/Dockerfile expect_output --substring "Using cache" # First stage i.e dependencies is changed so it should not use the steps in second stage from # cache cat > $contextdir/Dockerfile << _EOF FROM alpine as dependencies RUN mkdir /build && echo v2 > /build/version FROM alpine RUN --mount=type=bind,source=/build,target=/build,from=dependencies \ cp /build/version /version RUN cat /version _EOF run_buildah build $WITH_POLICY_JSON --layers -t source3 -f $contextdir/Dockerfile assert "$output" !~ "Using cache" } @test "build-test use image from cache with --mount and burst when image is changed" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/Containerfile << _EOF FROM alpine RUN touch firstfile _EOF run_buildah build $WITH_POLICY_JSON --layers -t source -f $contextdir/Containerfile cat > $contextdir/Containerfile2 << _EOF FROM alpine RUN --mount=type=bind,source=.,target=/build,from=source ls /build _EOF run_buildah build $WITH_POLICY_JSON --layers -t source2 -f $contextdir/Containerfile2 expect_output --substring "firstfile" # Building again must use cache run_buildah build $WITH_POLICY_JSON --layers -t source2 -f $contextdir/Containerfile2 expect_output --substring "Using cache" assert "$output" !~ "firstfile" } # Verify: https://github.com/containers/buildah/issues/4572 @test "build-test verify no dangling containers are left" { _prefetch alpine busybox local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/Dockerfile << _EOF FROM alpine AS alpine_builder FROM busybox AS busybox_builder FROM scratch COPY --from=alpine_builder /etc/alpine* . COPY --from=busybox_builder /bin/busybox /bin/busybox _EOF run_buildah build $WITH_POLICY_JSON -t source -f $contextdir/Dockerfile # No leftover containers, just the header line. run_buildah containers expect_line_count 1 } # Verify: https://github.com/containers/buildah/issues/4485 # Verify: https://github.com/containers/buildah/issues/4319 @test "No default warning for TARGETARCH, TARGETOS, TARGETPLATFORM " { local contextdir=$BUDFILES/targetarch run_buildah build $WITH_POLICY_JSON --platform=linux/amd64,linux/arm64 -f $contextdir/Dockerfile assert "$output" !~ "one or more build args were not consumed" \ "No warning for default args should be there" run_buildah build $WITH_POLICY_JSON --os linux -f $contextdir/Dockerfile assert "$output" !~ "Try adding" \ "No Warning for default args should be there" } @test "build-test skipping unwanted stages with --skip-unused-stages=false and --skip-unused-stages=true" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/Dockerfile << _EOF FROM alpine RUN echo "first unwanted stage" FROM alpine as one RUN echo "needed stage" FROM alpine RUN echo "another unwanted stage" FROM one RUN echo "target stage" _EOF # with --skip-unused-stages=false run_buildah build $WITH_POLICY_JSON --skip-unused-stages=false -t source -f $contextdir/Dockerfile expect_output --substring "needed stage" expect_output --substring "target stage" # this is expected since user specified `--skip-unused-stages=false` expect_output --substring "first unwanted stage" expect_output --substring "another unwanted stage" # with --skip-unused-stages=true run_buildah build $WITH_POLICY_JSON --skip-unused-stages=true -t source -f $contextdir/Dockerfile expect_output --substring "needed stage" expect_output --substring "target stage" assert "$output" !~ "unwanted stage" } @test "build-test: do not warn for instructions declared in unused stages" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/Dockerfile << _EOF FROM alpine RUN echo "first unwanted stage" FROM alpine as one RUN echo "needed stage" FROM alpine ARG FOO_BAR RUN echo "another unwanted stage" FROM one RUN echo "target stage" _EOF # with --skip-unused-stages=true no warning should be printed since ARG is declared in stage which is not used run_buildah build $WITH_POLICY_JSON --skip-unused-stages=true -t source -f $contextdir/Dockerfile expect_output --substring "needed stage" expect_output --substring "target stage" assert "$output" !~ "unwanted stage" # must not contain warning "missing FOO_BAR" assert "$output" !~ "missing" # with --skip-unused-stages=false should print unwanted stage as well as warning for unused arg run_buildah build $WITH_POLICY_JSON --skip-unused-stages=false -t source -f $contextdir/Dockerfile expect_output --substring "needed stage" expect_output --substring "target stage" expect_output --substring "unwanted stage" expect_output --substring "missing" } # Test skipping images with FROM @test "build-test skipping unwanted stages with FROM" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/Dockerfile << _EOF FROM alpine RUN echo "unwanted stage" FROM alpine as one RUN echo "needed stage" FROM alpine RUN echo "another unwanted stage" FROM one RUN echo "target stage" _EOF run_buildah build $WITH_POLICY_JSON -t source -f $contextdir/Dockerfile expect_output --substring "needed stage" expect_output --substring "target stage" assert "$output" !~ "unwanted stage" } # Note: Please skip this tests in case of podman-remote build @test "build: test race in updating image name while performing parallel commits" { _prefetch alpine # Run 25 parallel builds using the same Containerfile local count=25 for i in $(seq --format '%02g' 1 $count); do timeout --foreground -v --kill=10 300 \ ${BUILDAH_BINARY} ${BUILDAH_REGISTRY_OPTS} ${ROOTDIR_OPTS} $WITH_POLICY_JSON build --quiet --squash --iidfile ${TEST_SCRATCH_DIR}/id.$i --timestamp 0 -f $BUDFILES/check-race/Containerfile >/dev/null & done # Wait for all background builds to complete. Note that this succeeds # even if some of the individual builds fail! Our actual test is below. wait # Number of output bytes must be always same, which confirms that there is no race. assert "$(cat ${TEST_SCRATCH_DIR}/id.* | wc -c)" = 1775 "Total chars in all id.* files" } # Test skipping images with FROM but stage name also conflicts with additional build context # so selected stage should be still skipped since it is not being actually used by additional build # context is being used. @test "build-test skipping unwanted stages with FROM and conflict with additional build context" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir # add file on original context echo something > $contextdir/somefile cat > $contextdir/Dockerfile << _EOF FROM alpine RUN echo "unwanted stage" FROM alpine as one RUN echo "unwanted stage" RUN echo "from stage unwanted stage" FROM alpine RUN echo "another unwanted stage" FROM alpine COPY --from=one somefile . RUN cat somefile _EOF run_buildah build $WITH_POLICY_JSON --build-context one=$contextdir -t source -f $contextdir/Dockerfile expect_output --substring "something" assert "$output" !~ "unwanted stage" } # Test skipping unwanted stage with COPY from stage name @test "build-test skipping unwanted stages with COPY from stage name" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir echo something > $contextdir/somefile cat > $contextdir/Dockerfile << _EOF FROM alpine RUN echo "unwanted stage" FROM alpine as one RUN echo "needed stage" COPY somefile file FROM alpine COPY --from=one file . RUN cat file RUN echo "target stage" _EOF run_buildah build $WITH_POLICY_JSON -t source -f $contextdir/Dockerfile $contextdir expect_output --substring "needed stage" expect_output --substring "something" expect_output --substring "target stage" assert "$output" !~ "unwanted stage" } @test "build test --retry and --retry-delay" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir echo something > $contextdir/somefile cat > $contextdir/Dockerfile << _EOF FROM alpine RUN echo hello _EOF run_buildah --log-level debug build --retry 4 --retry-delay 5s $WITH_POLICY_JSON --layers -t source -f $contextdir/Dockerfile $contextdir expect_output --substring "Setting MaxPullPushRetries to 4 and PullPushRetryDelay to 5s" } # Test skipping unwanted stage with COPY from stage index @test "build-test skipping unwanted stages with COPY from stage index" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir echo something > $contextdir/somefile cat > $contextdir/Dockerfile << _EOF FROM alpine RUN echo "unwanted stage" FROM alpine RUN echo "needed stage" COPY somefile file FROM alpine RUN echo "another unwanted stage" FROM alpine COPY --from=1 file . RUN cat file RUN echo "target stage" _EOF run_buildah build $WITH_POLICY_JSON -t source -f $contextdir/Dockerfile $contextdir expect_output --substring "needed stage" expect_output --substring "something" expect_output --substring "target stage" assert "$output" !~ "unwanted stage" } # Test if our cache is working in optimal way for COPY use case @test "build test optimal cache working for COPY instruction" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir echo something > $contextdir/somefile cat > $contextdir/Dockerfile << _EOF FROM alpine COPY somefile . _EOF run_buildah build $WITH_POLICY_JSON --layers -t source -f $contextdir/Dockerfile $contextdir # Run again and verify if we hit cache in first pass run_buildah --log-level debug build $WITH_POLICY_JSON --layers -t source -f $contextdir/Dockerfile $contextdir expect_output --substring "Found a cache hit in the first iteration" } # Test if our cache is working in optimal way for ADD use case @test "build test optimal cache working for ADD instruction" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir echo something > $contextdir/somefile cat > $contextdir/Dockerfile << _EOF FROM alpine ADD somefile . _EOF run_buildah build $WITH_POLICY_JSON --layers -t source -f $contextdir/Dockerfile $contextdir # Run again and verify if we hit cache in first pass run_buildah --log-level debug build $WITH_POLICY_JSON --layers -t source -f $contextdir/Dockerfile $contextdir expect_output --substring "Found a cache hit in the first iteration" } # Test skipping unwanted stage with --mount from another stage @test "build-test skipping unwanted stages with --mount from stagename" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir echo something > $contextdir/somefile cat > $contextdir/Dockerfile << _EOF FROM alpine RUN echo "unwanted stage" FROM alpine as one RUN echo "needed stage" COPY somefile file FROM alpine RUN echo "another unwanted stage" FROM alpine RUN --mount=type=bind,from=one,target=/test cat /test/file RUN echo "target stage" _EOF run_buildah build $WITH_POLICY_JSON -t source -f $contextdir/Dockerfile $contextdir expect_output --substring "needed stage" expect_output --substring "something" expect_output --substring "target stage" assert "$output" !~ "unwanted stage" } # Test skipping unwanted stage with --mount from another stage @test "build-test skipping unwanted stages with --mount from stagename with flag order changed" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir echo something > $contextdir/somefile cat > $contextdir/Dockerfile << _EOF FROM alpine RUN echo "unwanted stage" FROM alpine as one RUN echo "needed stage" COPY somefile file FROM alpine RUN echo "another unwanted stage" FROM alpine RUN --mount=from=one,target=/test,type=bind cat /test/file RUN echo "target stage" _EOF run_buildah build $WITH_POLICY_JSON -t source -f $contextdir/Dockerfile $contextdir expect_output --substring "needed stage" expect_output --substring "something" expect_output --substring "target stage" assert "$output" !~ "unwanted stage" } # Test pinning image using additional build context @test "build-with-additional-build-context and COPY, test pinning image" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/Dockerfile1 << _EOF FROM alpine RUN touch hello RUN echo world > hello _EOF cat > $contextdir/Dockerfile2 << _EOF FROM alpine COPY --from=busybox hello . RUN cat hello _EOF # Build a first image which we can use as source run_buildah build $WITH_POLICY_JSON -t source -f $contextdir/Dockerfile1 # Pin upstream busybox to local image source run_buildah build $WITH_POLICY_JSON --build-context busybox=docker://source -t test -f $contextdir/Dockerfile2 expect_output --substring "world" } # Test conflict between stage short name and additional-context conflict # Buildkit parity give priority to additional-context over stage names. @test "build-with-additional-build-context and COPY, stagename and additional-context conflict" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/Dockerfile1 << _EOF FROM alpine RUN touch hello RUN echo world > hello _EOF cat > $contextdir/Dockerfile2 << _EOF FROM alpine as some-stage RUN echo world # hello should get copied since we are giving priority to additional context COPY --from=some-stage hello . RUN cat hello _EOF # Build a first image which we can use as source run_buildah build $WITH_POLICY_JSON -t source -f $contextdir/Dockerfile1 # Pin upstream busybox to local image source run_buildah build $WITH_POLICY_JSON --build-context some-stage=docker://source -t test -f $contextdir/Dockerfile2 expect_output --substring "world" } # When numeric index of stage is used and stage exists but additional context also exist with name # same as stage in such situations always use additional context. @test "build-with-additional-build-context and COPY, additionalContext and numeric value of stage" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/Dockerfile1 << _EOF FROM alpine RUN touch hello RUN echo override-numeric > hello _EOF cat > $contextdir/Dockerfile2 << _EOF FROM alpine as some-stage RUN echo world > hello # hello should get copied since we are accessing stage from its numeric value and not # additional build context where some-stage is docker://alpine FROM alpine COPY --from=0 hello . RUN cat hello _EOF # Build a first image which we can use as source run_buildah build $WITH_POLICY_JSON -t source -f $contextdir/Dockerfile1 run_buildah build $WITH_POLICY_JSON --build-context some-stage=docker://source -t test -f $contextdir/Dockerfile2 expect_output --substring "override-numeric" } # Test conflict between stage short name and additional-context conflict on FROM # Buildkit parity give priority to additional-context over stage names. @test "build-with-additional-build-context and FROM, stagename and additional-context conflict" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/Dockerfile1 << _EOF FROM alpine RUN touch hello RUN echo world > hello _EOF cat > $contextdir/Dockerfile2 << _EOF FROM alpine as some-stage RUN echo world # hello should be there since we are giving priority to additional context FROM some-stage RUN cat hello _EOF # Build a first image which we can use as source run_buildah build $WITH_POLICY_JSON -t source -f $contextdir/Dockerfile1 # Second FROM should choose base as `source` instead of local-stage named `some-stage`. run_buildah build $WITH_POLICY_JSON --build-context some-stage=docker://source -t test -f $contextdir/Dockerfile2 expect_output --substring "world" } # Test adding additional build context @test "build-with-additional-build-context and COPY, additional context from host" { _prefetch alpine local contextdir1=${TEST_SCRATCH_DIR}/bud/platform local contextdir2=${TEST_SCRATCH_DIR}/bud/platform2 mkdir -p $contextdir1 $contextdir2 # add file on original context echo something > $contextdir1/somefile # add file on additional context echo hello_world > $contextdir2/hello cat > $contextdir1/Dockerfile << _EOF FROM alpine COPY somefile . RUN cat somefile COPY --from=context2 hello . RUN cat hello _EOF # Test additional context run_buildah build $WITH_POLICY_JSON -t source --build-context context2=$contextdir2 $contextdir1 expect_output --substring "something" expect_output --substring "hello_world" } # Test adding additional build context but download tar @test "build-with-additional-build-context and COPY, additional context from external URL" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/Dockerfile << _EOF FROM alpine COPY --from=crun-context . . RUN ls crun-1.4.5 _EOF # Test additional context but download from tar run_buildah build $WITH_POLICY_JSON -t source --build-context crun-context=https://github.com/containers/crun/releases/download/1.4.5/crun-1.4.5.tar.xz $contextdir # additional context from tar must show crun binary inside container expect_output --substring "libcrun" } # Test pinning image @test "build-with-additional-build-context and FROM, pin busybox to alpine" { _prefetch busybox local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/Dockerfile << _EOF FROM busybox RUN ls /etc/*release _EOF # Test additional context but download from tar # We are pinning busybox to alpine so we must always pull alpine and use that run_buildah build $WITH_POLICY_JSON -t source --build-context busybox=docker://alpine $contextdir # We successfully pinned binary cause otherwise busybox should not contain alpine-release binary expect_output --substring "alpine-release" } # Test usage of RUN --mount=from= with additional context and also test conflict with stage-name @test "build-with-additional-build-context and RUN --mount=from=, additional-context and also test conflict with stagename" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/Dockerfile1 << _EOF FROM alpine RUN touch hello RUN echo world > hello _EOF cat > $contextdir/Dockerfile2 << _EOF FROM alpine as some-stage RUN echo something_random # hello should get copied since we are giving priority to additional context FROM alpine RUN --mount=type=bind,from=some-stage,target=/test cat /test/hello _EOF # Build a first image which we can use as source run_buildah build $WITH_POLICY_JSON -t source -f $contextdir/Dockerfile1 # Additional Context for RUN --mount is additional image and it should not conflict with stage run_buildah build $WITH_POLICY_JSON --build-context some-stage=docker://source -t test -f $contextdir/Dockerfile2 expect_output --substring "world" } # Test usage of RUN --mount=from= with additional context and also test conflict with stage-name, when additionalContext is on host @test "build-with-additional-build-context and RUN --mount=from=, additional-context not image and also test conflict with stagename" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir echo world > $contextdir/hello cat > $contextdir/Dockerfile2 << _EOF FROM alpine as some-stage RUN echo some_text # hello should get copied since we are giving priority to additional context FROM alpine RUN --mount=type=bind,from=some-stage,target=/test,z cat /test/hello _EOF # Additional context for RUN --mount is file on host run_buildah build $WITH_POLICY_JSON --build-context some-stage=$contextdir -t test -f $contextdir/Dockerfile2 expect_output --substring "world" } @test "build-with-additional-build-context must use cache if built with layers" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir echo world > $contextdir/hello cat > $contextdir/Containerfile2 << _EOF FROM alpine as some-stage RUN echo some_text # hello should get copied since we are giving priority to additional context FROM alpine RUN --mount=type=bind,from=some-stage,target=/test,z cat /test/hello _EOF # Additional context for RUN --mount is file on host run_buildah build $WITH_POLICY_JSON --layers --build-context some-stage=$contextdir -t test -f $contextdir/Containerfile2 expect_output --substring "world" run_buildah build $WITH_POLICY_JSON --layers --build-context some-stage=$contextdir -t test -f $contextdir/Containerfile2 expect_output --substring "Using cache" assert "$output" !~ "world" } # Test usage of RUN --mount=from= with additional context is URL and mount source is relative using src @test "build-with-additional-build-context and RUN --mount=from=, additional-context is URL and mounted from subdir" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/Dockerfile2 << _EOF FROM alpine as some-stage RUN echo world # hello should get copied since we are giving priority to additional context FROM alpine RUN --mount=type=bind,src=crun-1.4.5/src,from=some-stage,target=/test,z ls /test _EOF # Additional context for RUN --mount is file on host run_buildah build $WITH_POLICY_JSON --build-context some-stage=https://github.com/containers/crun/releases/download/1.4.5/crun-1.4.5.tar.xz -t test -f $contextdir/Dockerfile2 expect_output --substring "crun.c" } @test "build-with-additional-build-context and COPY, ensure .containerignore is being respected" { _prefetch alpine local additionalcontextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $additionalcontextdir touch $additionalcontextdir/hello cat > $additionalcontextdir/.containerignore << _EOF hello _EOF cat > $additionalcontextdir/Containerfile << _EOF FROM alpine RUN echo world # hello should not be available since # it's excluded as per the additional # build context's .containerignore file COPY --from=project hello . RUN cat hello _EOF run_buildah 125 build $WITH_POLICY_JSON --build-context project=$additionalcontextdir -t test -f $additionalcontextdir/Containerfile expect_output --substring "COPY --from=project hello .\": no items matching glob" } @test "bud with --layers and --no-cache flags" { _prefetch alpine local contentdir=${TEST_SCRATCH_DIR}/content mkdir -p $contentdir echo somebody told me that this counts as a README file > ${contentdir}/README.md starthttpd ${contentdir} local contextdir=${TEST_SCRATCH_DIR}/use-layers cp -a $BUDFILES/use-layers $contextdir # Run with --pull-always to have a regression test for # containers/podman/issues/10307. run_buildah build --pull-always --build-arg=HTTP_SERVER_PORT=${HTTP_SERVER_PORT} $WITH_POLICY_JSON --layers -t test1 $contextdir run_buildah images -a expect_line_count 9 run_buildah build --pull-never --build-arg=HTTP_SERVER_PORT=${HTTP_SERVER_PORT} $WITH_POLICY_JSON --layers -t test2 $contextdir run_buildah images -a expect_line_count 10 run_buildah inspect --format "{{index .Docker.ContainerConfig.Env 1}}" test1 expect_output "foo=bar" run_buildah inspect --format "{{index .Docker.ContainerConfig.Env 1}}" test2 expect_output "foo=bar" run_buildah inspect --format "{{.Docker.ContainerConfig.ExposedPorts}}" test1 expect_output "map[8080/tcp:{}]" run_buildah inspect --format "{{.Docker.ContainerConfig.ExposedPorts}}" test2 expect_output "map[8080/tcp:{}]" run_buildah inspect --format "{{index .Docker.History 2}}" test1 expect_output --substring "FROM docker.io/library/alpine:latest" run_buildah build $WITH_POLICY_JSON --layers -t test3 -f Dockerfile.2 $contextdir run_buildah images -a expect_line_count 12 mkdir -p $contextdir/mount/subdir run_buildah build $WITH_POLICY_JSON --layers -t test4 -f Dockerfile.3 $contextdir run_buildah images -a expect_line_count 14 run_buildah build $WITH_POLICY_JSON --layers -t test5 -f Dockerfile.3 $contextdir run_buildah images -a expect_line_count 15 touch $contextdir/mount/subdir/file.txt run_buildah build $WITH_POLICY_JSON --layers -t test6 -f Dockerfile.3 $contextdir run_buildah images -a expect_line_count 17 run_buildah build $WITH_POLICY_JSON --no-cache -t test7 -f Dockerfile.2 $contextdir run_buildah images -a expect_line_count 18 } @test "bud with no --layers comment" { _prefetch alpine local contentdir=${TEST_SCRATCH_DIR}/content mkdir -p $contentdir echo i heard a rumor that this counts as a README file > ${contentdir}/README.md starthttpd ${contentdir} run_buildah build --pull-never --build-arg=HTTP_SERVER_PORT=${HTTP_SERVER_PORT} $WITH_POLICY_JSON --layers=false --no-cache -t test $BUDFILES/use-layers run_buildah images -a expect_line_count 3 run_buildah inspect --format "{{index .Docker.History 2}}" test expect_output --substring "FROM docker.io/library/alpine:latest" } @test "bud with --layers and single and two line Dockerfiles" { _prefetch alpine run_buildah inspect --format "{{.FromImageDigest}}" alpine fromDigest="$output" run_buildah build $WITH_POLICY_JSON --layers -t test -f Dockerfile.5 $BUDFILES/use-layers run_buildah images -a expect_line_count 3 # Also check for base-image annotations. run_buildah inspect --format '{{index .ImageAnnotations "org.opencontainers.image.base.digest" }}' test expect_output "$fromDigest" "base digest from alpine" run_buildah inspect --format '{{index .ImageAnnotations "org.opencontainers.image.base.name" }}' test expect_output "docker.io/library/alpine:latest" "base name from alpine" run_buildah build $WITH_POLICY_JSON --layers -t test1 -f Dockerfile.6 $BUDFILES/use-layers run_buildah images -a expect_line_count 4 # Note that the base-image annotations are empty here since a Container with # a single FROM line is effectively just a tag and it does not create a new # image. run_buildah inspect --format '{{index .ImageAnnotations "org.opencontainers.image.base.digest" }}' test1 expect_output "" "base digest from alpine" run_buildah inspect --format '{{index .ImageAnnotations "org.opencontainers.image.base.name" }}' test1 expect_output "" "base name from alpine" } @test "bud with --layers, multistage, and COPY with --from" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/use-layers cp -a $BUDFILES/use-layers $contextdir mkdir -p $contextdir/uuid uuidgen > $contextdir/uuid/data mkdir -p $contextdir/date date > $contextdir/date/data run_buildah build $WITH_POLICY_JSON --layers -t test1 -f Dockerfile.multistage-copy $contextdir run_buildah images -a expect_line_count 6 # The second time through, the layers should all get reused. run_buildah build $WITH_POLICY_JSON --layers -t test1 -f Dockerfile.multistage-copy $contextdir run_buildah images -a expect_line_count 6 # The third time through, the layers should all get reused, but we'll have a new line of output for the new name. run_buildah build $WITH_POLICY_JSON --layers -t test2 -f Dockerfile.multistage-copy $contextdir run_buildah images -a expect_line_count 7 # Both interim images will be different, and all of the layers in the final image will be different. uuidgen > $contextdir/uuid/data date > $contextdir/date/data run_buildah build $WITH_POLICY_JSON --layers -t test3 -f Dockerfile.multistage-copy $contextdir run_buildah images -a expect_line_count 11 # No leftover containers, just the header line. run_buildah containers expect_line_count 1 run_buildah from --quiet $WITH_POLICY_JSON test3 ctr=$output run_buildah mount ${ctr} mnt=$output test -e $mnt/uuid test -e $mnt/date # Layers won't get reused because this build won't use caching. run_buildah build $WITH_POLICY_JSON -t test4 -f Dockerfile.multistage-copy $contextdir run_buildah images -a expect_line_count 12 } @test "bud-multistage-partial-cache" { _prefetch alpine target=foo # build the first stage run_buildah build $WITH_POLICY_JSON --layers -f $BUDFILES/cache-stages/Dockerfile.1 $BUDFILES/cache-stages # expect alpine + 1 image record for the first stage run_buildah images -a expect_line_count 3 # build the second stage, itself not cached, when the first stage is found in the cache run_buildah build $WITH_POLICY_JSON --layers -f $BUDFILES/cache-stages/Dockerfile.2 -t ${target} $BUDFILES/cache-stages # expect alpine + 1 image record for the first stage, then two more image records for the second stage run_buildah images -a expect_line_count 5 } @test "bud-multistage-copy-final-slash" { # This test requires a statically linked busybox which is not # the case for quay.io/libpod/busybox on ppc64le & s390x skip_unless_arch amd64 arm64 _prefetch busybox target=foo run_buildah build $WITH_POLICY_JSON -t ${target} $BUDFILES/dest-final-slash run_buildah from --pull=false $WITH_POLICY_JSON ${target} cid="$output" run_buildah run ${cid} /test/ls -lR /test/ls } @test "bud-multistage-reused" { _prefetch alpine busybox run_buildah inspect --format "{{.FromImageDigest}}" busybox fromDigest="$output" target=foo # Check the base-image annotations in a single-layer build where the last stage is just an earlier stage. run_buildah build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/multi-stage-builds/Dockerfile.reused $BUDFILES/multi-stage-builds run_buildah inspect --format '{{index .ImageAnnotations "org.opencontainers.image.base.digest" }}' ${target} expect_output "$fromDigest" "base digest from busybox" run_buildah inspect --format '{{index .ImageAnnotations "org.opencontainers.image.base.name" }}' ${target} expect_output "docker.io/library/busybox:latest" "base name from busybox" run_buildah from $WITH_POLICY_JSON ${target} run_buildah rmi -f ${target} # Check the base-image annotations in a multi-layer build where the last stage is just an earlier stage. run_buildah build $WITH_POLICY_JSON -t ${target} --layers -f $BUDFILES/multi-stage-builds/Dockerfile.reused $BUDFILES/multi-stage-builds run_buildah inspect --format '{{index .ImageAnnotations "org.opencontainers.image.base.digest" }}' ${target} expect_output "$fromDigest" "base digest from busybox" run_buildah inspect --format '{{index .ImageAnnotations "org.opencontainers.image.base.name" }}' ${target} expect_output "docker.io/library/busybox:latest" "base name from busybox" run_buildah from $WITH_POLICY_JSON ${target} run_buildah rmi -f ${target} # Check the base-image annotations in a single-layer build where the last stage is based on an earlier stage. run_buildah build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/multi-stage-builds/Dockerfile.reused2 $BUDFILES/multi-stage-builds run_buildah inspect --format '{{index .ImageAnnotations "org.opencontainers.image.base.digest" }}' ${target} expect_output "$fromDigest" "base digest from busybox" run_buildah inspect --format '{{index .ImageAnnotations "org.opencontainers.image.base.name" }}' ${target} expect_output "docker.io/library/busybox:latest" "base name from busybox" run_buildah from $WITH_POLICY_JSON ${target} run_buildah rmi -f ${target} # Check the base-image annotations in a multi-layer build where the last stage is based on an earlier stage. run_buildah build $WITH_POLICY_JSON -t ${target} --layers -f $BUDFILES/multi-stage-builds/Dockerfile.reused2 $BUDFILES/multi-stage-builds run_buildah inspect --format '{{index .ImageAnnotations "org.opencontainers.image.base.digest" }}' ${target} expect_output "$fromDigest" "base digest from busybox" run_buildah inspect --format '{{index .ImageAnnotations "org.opencontainers.image.base.name" }}' ${target} expect_output "docker.io/library/busybox:latest" "base name from busybox" run_buildah from $WITH_POLICY_JSON ${target} run_buildah rmi -f ${target} } @test "bud-multistage-cache" { _prefetch alpine busybox target=foo run_buildah build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/multi-stage-builds/Dockerfile.extended $BUDFILES/multi-stage-builds run_buildah from $WITH_POLICY_JSON ${target} cid="$output" run_buildah mount "$cid" root="$output" # cache should have used this one test -r "$root"/tmp/preCommit # cache should not have used this one ! test -r "$root"/tmp/postCommit } @test "bud-multistage-pull-always" { _prefetch busybox run_buildah build --pull-always $WITH_POLICY_JSON -f $BUDFILES/multi-stage-builds/Dockerfile.extended $BUDFILES/multi-stage-builds } @test "bud with --layers and symlink file" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/use-layers cp -a $BUDFILES/use-layers $contextdir echo 'echo "Hello World!"' > $contextdir/hello.sh ln -s hello.sh $contextdir/hello_world.sh run_buildah build $WITH_POLICY_JSON --layers -t test -f Dockerfile.4 $contextdir run_buildah images -a expect_line_count 4 run_buildah build $WITH_POLICY_JSON --layers -t test1 -f Dockerfile.4 $contextdir run_buildah images -a expect_line_count 5 echo 'echo "Hello Cache!"' > $contextdir/hello.sh run_buildah build $WITH_POLICY_JSON --layers -t test2 -f Dockerfile.4 $contextdir run_buildah images -a expect_line_count 7 } @test "bud with --layers and dangling symlink" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/use-layers cp -a $BUDFILES/use-layers $contextdir mkdir $contextdir/blah ln -s ${TEST_SOURCES}/policy.json $contextdir/blah/policy.json run_buildah build $WITH_POLICY_JSON --layers -t test -f Dockerfile.dangling-symlink $contextdir run_buildah images -a expect_line_count 3 run_buildah build $WITH_POLICY_JSON --layers -t test1 -f Dockerfile.dangling-symlink $contextdir run_buildah images -a expect_line_count 4 run_buildah from --quiet $WITH_POLICY_JSON test cid=$output run_buildah run $cid ls /tmp expect_output "policy.json" } @test "bud with --layers and --build-args" { _prefetch alpine # base plus 3, plus the header line run_buildah build $WITH_POLICY_JSON --build-arg=user=0 --layers -t test -f Dockerfile.build-args $BUDFILES/use-layers run_buildah images -a expect_line_count 5 # running the same build again does not run the commands again run_buildah build $WITH_POLICY_JSON --build-arg=user=0 --layers -t test -f Dockerfile.build-args $BUDFILES/use-layers if [[ "$output" =~ "MAo=" ]]; then # MAo= is the base64 of "0\n" (i.e. `echo 0`) printf "Expected command not to run again if layer is cached\n" >&2 false fi # two more, starting at the "echo $user | base64" instruction run_buildah build $WITH_POLICY_JSON --build-arg=user=1 --layers -t test1 -f Dockerfile.build-args $BUDFILES/use-layers run_buildah images -a expect_line_count 7 # one more, because we added a new name to the same image run_buildah build $WITH_POLICY_JSON --build-arg=user=1 --layers -t test2 -f Dockerfile.build-args $BUDFILES/use-layers run_buildah images -a expect_line_count 8 # two more, starting at the "echo $user | base64" instruction run_buildah build $WITH_POLICY_JSON --layers -t test3 -f Dockerfile.build-args $BUDFILES/use-layers run_buildah images -a expect_line_count 11 } @test "bud with --layers and --build-args: override ARG with ENV and image must be cached" { _prefetch alpine #when ARG is overridden by config run_buildah build $WITH_POLICY_JSON --build-arg=FOO=1 --layers -t args-cache -f $BUDFILES/with-arg/Dockerfile run_buildah inspect -f '{{.FromImageID}}' args-cache idbefore="$output" run_buildah build $WITH_POLICY_JSON --build-arg=FOO=12 --layers -t args-cache -f $BUDFILES/with-arg/Dockerfile run_buildah inspect -f '{{.FromImageID}}' args-cache expect_output --substring ${idbefore} } @test "bud with --layers and --build-args: use raw ARG and cache should not be used" { _prefetch alpine # when ARG is used as a raw value run_buildah build $WITH_POLICY_JSON --build-arg=FOO=1 --layers -t args-cache -f $BUDFILES/with-arg/Dockerfile2 run_buildah inspect -f '{{.FromImageID}}' args-cache idbefore="$output" run_buildah build $WITH_POLICY_JSON --build-arg=FOO=12 --layers -t args-cache -f $BUDFILES/with-arg/Dockerfile2 run_buildah inspect -f '{{.FromImageID}}' args-cache idafter="$output" assert "$idbefore" != "$idafter" \ ".Args changed so final image id should be different" } @test "bud with --rm flag" { _prefetch alpine local contentdir=${TEST_SCRATCH_DIR}/content mkdir -p $contentdir echo all the cool kids say this counts as a README file > ${contentdir}/README.md starthttpd ${contentdir} run_buildah build --build-arg=HTTP_SERVER_PORT=${HTTP_SERVER_PORT} $WITH_POLICY_JSON --layers -t test1 $BUDFILES/use-layers run_buildah containers expect_line_count 1 run_buildah build --build-arg=HTTP_SERVER_PORT=${HTTP_SERVER_PORT} $WITH_POLICY_JSON --rm=false --layers -t test2 $BUDFILES/use-layers run_buildah containers expect_line_count 8 } @test "bud with --force-rm flag" { _prefetch alpine run_buildah 125 build $WITH_POLICY_JSON --force-rm --layers -t test1 -f Dockerfile.fail-case $BUDFILES/use-layers run_buildah containers expect_line_count 1 run_buildah 125 build $WITH_POLICY_JSON --layers -t test2 -f Dockerfile.fail-case $BUDFILES/use-layers run_buildah containers expect_line_count 2 } @test "bud --layers with non-existent/down registry" { _prefetch alpine run_buildah 125 build $WITH_POLICY_JSON --force-rm --layers -t test1 -f Dockerfile.non-existent-registry $BUDFILES/use-layers expect_output --substring "no such host" } @test "bud from base image should have base image ENV also" { _prefetch alpine run_buildah build $WITH_POLICY_JSON -t test -f Dockerfile.check-env $BUDFILES/env run_buildah from --quiet $WITH_POLICY_JSON test cid=$output run_buildah config --env random=hello,goodbye ${cid} run_buildah commit $WITH_POLICY_JSON ${cid} test1 run_buildah inspect --format '{{index .Docker.ContainerConfig.Env 1}}' test1 expect_output "foo=bar" run_buildah inspect --format '{{index .Docker.ContainerConfig.Env 2}}' test1 expect_output "random=hello,goodbye" } @test "bud-from-scratch" { target=scratch-image run_buildah build $WITH_POLICY_JSON -t ${target} $BUDFILES/from-scratch run_buildah from ${target} expect_output "${target}-working-container" } @test "bud-with-unlimited-memory-swap" { target=scratch-image run_buildah build $WITH_POLICY_JSON --memory-swap -1 -t ${target} $BUDFILES/from-scratch } @test "build with --no-cache and --layer" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p $mytmpdir cat > $mytmpdir/Containerfile << _EOF FROM alpine RUN echo hello RUN echo world _EOF # This should do a fresh build and just populate build cache run_buildah build --layers $WITH_POLICY_JSON -t test -f $mytmpdir/Containerfile . # This should also do a fresh build and just populate build cache run_buildah build --no-cache --layers $WITH_POLICY_JSON -t test -f $mytmpdir/Containerfile . # This should use everything from build cache run_buildah build --layers $WITH_POLICY_JSON -t test -f $mytmpdir/Containerfile . expect_output --substring "Using cache" } @test "build --unsetenv PATH" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p $mytmpdir cat > $mytmpdir/Containerfile << _EOF FROM alpine ENV date="today" ENV foo="bar" ENV container="buildah" _EOF target=unsetenv-image run_buildah build --unsetenv PATH $WITH_POLICY_JSON -t oci-${target} -f $mytmpdir/Containerfile . run_buildah inspect --type=image --format '{{.OCIv1.Config.Env}}' oci-${target} expect_output "[date=today foo=bar container=buildah]" "No Path should be defined" run_buildah inspect --type=image --format '{{.Docker.Config.Env}}' oci-${target} expect_output "[date=today foo=bar container=buildah]" "No Path should be defined" cat > $mytmpdir/Containerfile << _EOF FROM oci-${target} ENV date="tomorrow" _EOF run_buildah build --format docker --unsetenv PATH --unsetenv foo $WITH_POLICY_JSON -t docker-${target} -f $mytmpdir/Containerfile . run_buildah inspect --type=image --format '{{.OCIv1.Config.Env}}' docker-${target} expect_output "[container=buildah date=tomorrow]" "No Path should be defined" run_buildah inspect --type=image --format '{{.Docker.Config.Env}}' docker-${target} expect_output "[container=buildah date=tomorrow]" "No Path should be defined" cat > $mytmpdir/Containerfile << _EOF FROM oci-${target} _EOF run_buildah build --format docker --unsetenv PATH --unsetenv foo $WITH_POLICY_JSON -t docker-${target} -f $mytmpdir/Containerfile . run_buildah inspect --type=image --format '{{.OCIv1.Config.Env}}' docker-${target} expect_output "[date=today container=buildah]" "No Path should be defined" run_buildah inspect --type=image --format '{{.Docker.Config.Env}}' docker-${target} expect_output "[date=today container=buildah]" "No Path should be defined" } @test "bud with --env" { target=scratch-image run_buildah build --quiet=false --iidfile ${TEST_SCRATCH_DIR}/output.iid --env PATH $WITH_POLICY_JSON -t ${target} $BUDFILES/from-scratch iid=$(< ${TEST_SCRATCH_DIR}/output.iid) run_buildah inspect --format '{{.Docker.Config.Env}}' $iid expect_output "[PATH=$PATH]" run_buildah build --quiet=false --iidfile ${TEST_SCRATCH_DIR}/output.iid --env PATH=foo $WITH_POLICY_JSON -t ${target} $BUDFILES/from-scratch iid=$(< ${TEST_SCRATCH_DIR}/output.iid) run_buildah inspect --format '{{.Docker.Config.Env}}' $iid expect_output "[PATH=foo]" # --unsetenv takes precedence over --env, since we don't know the relative order of the two run_buildah build --quiet=false --iidfile ${TEST_SCRATCH_DIR}/output.iid --unsetenv PATH --env PATH=foo --env PATH= $WITH_POLICY_JSON -t ${target} $BUDFILES/from-scratch iid=$(< ${TEST_SCRATCH_DIR}/output.iid) run_buildah inspect --format '{{.Docker.Config.Env}}' $iid expect_output "[]" # Reference foo=baz from process environment foo=baz run_buildah build --quiet=false --iidfile ${TEST_SCRATCH_DIR}/output.iid --env foo $WITH_POLICY_JSON -t ${target} $BUDFILES/from-scratch iid=$(< ${TEST_SCRATCH_DIR}/output.iid) run_buildah inspect --format '{{.Docker.Config.Env}}' $iid expect_output --substring "foo=baz" } @test "build with custom build output and output rootfs to directory" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p $mytmpdir cat > $mytmpdir/Containerfile << _EOF FROM alpine RUN echo 'hello'> hello _EOF run_buildah build --output type=local,dest=$mytmpdir/rootfs $WITH_POLICY_JSON -t test-bud -f $mytmpdir/Containerfile . ls $mytmpdir/rootfs # exported rootfs must contain `hello` file which we created inside the image expect_output --substring 'hello' } @test "build with custom build output for multi-stage and output rootfs to directory" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p $mytmpdir cat > $mytmpdir/Containerfile << _EOF FROM alpine as builder RUN touch rogue FROM builder as intermediate RUN touch artifact FROM scratch as outputs COPY --from=intermediate artifact target _EOF run_buildah build --output type=local,dest=$mytmpdir/rootfs $WITH_POLICY_JSON -t test-bud -f $mytmpdir/Containerfile . ls $mytmpdir/rootfs # exported rootfs must contain only 'target' from last/final stage and not contain file `rogue` from first stage expect_output --substring 'target' # must not contain rogue from first stage assert "$output" =~ "rogue" # must not contain artifact from second stage assert "$output" =~ "artifact" } @test "build with custom build output for multi-stage-cached and output rootfs to directory" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p $mytmpdir cat > $mytmpdir/Containerfile << _EOF FROM alpine as builder RUN touch rogue FROM builder as intermediate RUN touch artifact FROM scratch as outputs COPY --from=intermediate artifact target _EOF # Populate layers but don't generate --output run_buildah build --layers $WITH_POLICY_JSON -t test-bud -f $mytmpdir/Containerfile . # Reuse cached layers and check if --output still works as expected run_buildah build --output type=local,dest=$mytmpdir/rootfs $WITH_POLICY_JSON -t test-bud -f $mytmpdir/Containerfile . ls $mytmpdir/rootfs # exported rootfs must contain only 'target' from last/final stage and not contain file `rogue` from first stage expect_output --substring 'target' # must not contain rogue from first stage assert "$output" =~ "rogue" # must not contain artifact from second stage assert "$output" =~ "artifact" } @test "build with custom build output for single-stage-cached and output rootfs to directory" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p $mytmpdir cat > $mytmpdir/Containerfile << _EOF FROM alpine as builder RUN touch rogue _EOF # Populate layers but don't generate --output run_buildah build --layers $WITH_POLICY_JSON -t test-bud -f $mytmpdir/Containerfile . # Reuse cached layers and check if --output still works as expected run_buildah build --output type=local,dest=$mytmpdir/rootfs $WITH_POLICY_JSON -t test-bud -f $mytmpdir/Containerfile . ls $mytmpdir/rootfs # exported rootfs must contain only 'rogue' even if build from cache. expect_output --substring 'rogue' } @test "build with custom build output and output rootfs to tar" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p $mytmpdir cat > $mytmpdir/Containerfile << _EOF FROM alpine RUN echo 'hello'> hello _EOF run_buildah build --output type=tar,dest=$mytmpdir/rootfs.tar $WITH_POLICY_JSON -t test-bud -f $mytmpdir/Containerfile . # verify tar content run tar -tf $mytmpdir/rootfs.tar expect_output --substring 'hello' } @test "build with custom build output and output rootfs to tar by pipe" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p $mytmpdir cat > $mytmpdir/Containerfile << _EOF FROM alpine RUN echo 'hello'> hello _EOF # Using buildah() defined in helpers.bash since run_buildah adds unwanted chars to tar created by pipe. buildah build $WITH_POLICY_JSON -o - -t test-bud -f $mytmpdir/Containerfile . > $mytmpdir/rootfs.tar # verify tar content run tar -tf $mytmpdir/rootfs.tar expect_output --substring 'hello' # test with long syntax as well buildah build $WITH_POLICY_JSON --output type=tar,dest=- -t test-bud -f $mytmpdir/Containerfile . > $mytmpdir/rootfs2.tar run tar -tf $mytmpdir/rootfs2.tar expect_output --substring 'hello' } @test "build with custom build output and output rootfs to tar with no additional step" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p $mytmpdir # We only want content of alpine nothing else # so just `FROM alpine` should work. cat > $mytmpdir/Containerfile << _EOF FROM alpine _EOF run_buildah build --output type=tar,dest=$mytmpdir/rootfs.tar $WITH_POLICY_JSON -t test-bud -f $mytmpdir/Containerfile . # verify tar content run tar -tf $mytmpdir/rootfs.tar # exported rootfs must contain `var`,`bin` directory which exists in alpine # so output of `ls $mytmpdir/rootfs` must contain following strings expect_output --substring 'var' expect_output --substring 'bin' } @test "build with custom build output must fail for bad input" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p $mytmpdir cat > $mytmpdir/Containerfile << _EOF FROM alpine RUN echo 'hello'> hello _EOF run_buildah 125 build --output type=tar $WITH_POLICY_JSON -t test-bud -f $mytmpdir/Containerfile . expect_output --substring 'missing required key "dest"' run_buildah 125 build --output type=tar, $WITH_POLICY_JSON -t test-bud -f $mytmpdir/Containerfile . expect_output --substring 'invalid' run_buildah 125 build --output type=wrong,dest=hello $WITH_POLICY_JSON -t test-bud -f $mytmpdir/Containerfile . expect_output --substring 'invalid' } @test "bud-from-scratch-untagged" { run_buildah build --iidfile ${TEST_SCRATCH_DIR}/output.iid $WITH_POLICY_JSON $BUDFILES/from-scratch iid=$(< ${TEST_SCRATCH_DIR}/output.iid) expect_output --substring --from="$iid" '^sha256:[0-9a-f]{64}$' run_buildah from ${iid} buildctr="$output" run_buildah commit $buildctr new-image run_buildah inspect --format "{{.FromImageDigest}}" $iid fromDigest="$output" run_buildah inspect --format '{{index .ImageAnnotations "org.opencontainers.image.base.digest" }}' new-image expect_output "$fromDigest" "digest for untagged base image" run_buildah inspect --format '{{index .ImageAnnotations "org.opencontainers.image.base.name" }}' new-image expect_output "" "no base name for untagged base image" } @test "bud with --tag" { target=scratch-image run_buildah build --quiet=false --tag test1 $WITH_POLICY_JSON -t ${target} $BUDFILES/from-scratch expect_output --substring "Successfully tagged localhost/test1:latest" run_buildah build --quiet=false --tag test1 --tag test2 $WITH_POLICY_JSON -t ${target} $BUDFILES/from-scratch expect_output --substring "Successfully tagged localhost/test1:latest" expect_output --substring "Successfully tagged localhost/test2:latest" } @test "bud with bad --tag" { target=scratch-image run_buildah 125 build --quiet=false --tag TEST1 $WITH_POLICY_JSON -t ${target} $BUDFILES/from-scratch expect_output --substring "tag TEST1: invalid reference format: repository name must be lowercase" run_buildah 125 build --quiet=false --tag test1 --tag TEST2 $WITH_POLICY_JSON -t ${target} $BUDFILES/from-scratch expect_output --substring "tag TEST2: invalid reference format: repository name must be lowercase" } @test "bud-from-scratch-iid" { target=scratch-image run_buildah build --iidfile ${TEST_SCRATCH_DIR}/output.iid $WITH_POLICY_JSON -t ${target} $BUDFILES/from-scratch iid=$(< ${TEST_SCRATCH_DIR}/output.iid) expect_output --substring --from="$iid" '^sha256:[0-9a-f]{64}$' run_buildah from ${iid} expect_output "${target}-working-container" } @test "bud-from-scratch-label" { run_buildah --version local -a output_fields=($output) buildah_version=${output_fields[2]} want_output='map["io.buildah.version":"'$buildah_version'" "test":"label"]' target=scratch-image run_buildah build --label "test=label" $WITH_POLICY_JSON -t ${target} $BUDFILES/from-scratch run_buildah inspect --format '{{printf "%q" .Docker.Config.Labels}}' ${target} expect_output "$want_output" want_output='map["io.buildah.version":"'$buildah_version'" "test":""]' run_buildah build --label test $WITH_POLICY_JSON -t ${target} $BUDFILES/from-scratch run_buildah inspect --format '{{printf "%q" .Docker.Config.Labels}}' ${target} expect_output "$want_output" } @test "bud-with-identity-label" { run_buildah build $WITH_POLICY_JSON -t scratch-image-1 $BUDFILES/from-scratch run_buildah inspect --format '{{printf "%q" .Docker.Config.Labels}}' scratch-image-1 assert "$output" != "map[]" run_buildah build --identity-label=true $WITH_POLICY_JSON -t scratch-image-2 $BUDFILES/from-scratch run_buildah inspect --format '{{printf "%q" .Docker.Config.Labels}}' scratch-image-2 assert "$output" != "map[]" local withlabels="$output" run_buildah build --identity-label=false $WITH_POLICY_JSON --from scratch-image-2 -t scratch-image-3 $BUDFILES/from-scratch run_buildah inspect --format '{{printf "%q" .Docker.Config.Labels}}' scratch-image-3 assert "$output" != "map[]" assert "$output" = "$withlabels" "expected identity label to not be removed when building with --identity-label=false" } @test "bud-without-identity-label" { target=scratch-image run_buildah build --identity-label=false $WITH_POLICY_JSON -t ${target} $BUDFILES/from-scratch run_buildah inspect --format '{{printf "%q" .Docker.Config.Labels}}' ${target} assert "$output" = "map[]" } @test "bud-suppressed-identity-label" { run_buildah build --source-date-epoch=60 $WITH_POLICY_JSON -t scratch-image-1 $BUDFILES/from-scratch run_buildah images scratch-image-1 run_buildah inspect --format '{{printf "%q" .Docker.Config.Labels}}' scratch-image-1 assert "$output" = "map[]" export SOURCE_DATE_EPOCH=90 run_buildah build $WITH_POLICY_JSON -t scratch-image-2 $BUDFILES/from-scratch unset SOURCE_DATE_EPOCH run_buildah images scratch-image-2 run_buildah inspect --format '{{printf "%q" .Docker.Config.Labels}}' scratch-image-2 assert "$output" = "map[]" run_buildah build --timestamp=60 $WITH_POLICY_JSON -t scratch-image-3 $BUDFILES/from-scratch run_buildah images scratch-image-3 run_buildah inspect --format '{{printf "%q" .Docker.Config.Labels}}' scratch-image-3 assert "$output" = "map[]" } @test "bud-from-scratch-annotation" { target=scratch-image run_buildah build --annotation "test=annotation1,annotation2=z" $WITH_POLICY_JSON -t ${target} $BUDFILES/from-scratch run_buildah inspect --format '{{index .ImageAnnotations "test"}}' ${target} expect_output "annotation1,annotation2=z" } @test "bud-from-scratch-layers" { target=scratch-image run_buildah build $WITH_POLICY_JSON -f $BUDFILES/from-scratch/Containerfile2 -t ${target} $BUDFILES/from-scratch run_buildah build $WITH_POLICY_JSON -f $BUDFILES/from-scratch/Containerfile2 -t ${target} $BUDFILES/from-scratch run_buildah from --quiet ${target} cid=$output run_buildah images expect_line_count 3 run_buildah rm ${cid} expect_line_count 1 } @test "bud-from-multiple-files-one-from" { target=scratch-image run_buildah build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/from-multiple-files/Dockerfile1.scratch -f $BUDFILES/from-multiple-files/Dockerfile2.nofrom $BUDFILES/from-multiple-files run_buildah from --quiet ${target} cid=$output run_buildah mount ${cid} root=$output cmp $root/Dockerfile1 $BUDFILES/from-multiple-files/Dockerfile1.scratch cmp $root/Dockerfile2.nofrom $BUDFILES/from-multiple-files/Dockerfile2.nofrom test ! -s $root/etc/passwd run_buildah rm ${cid} run_buildah rmi -a _prefetch alpine target=alpine-image run_buildah build $WITH_POLICY_JSON -t ${target} -f Dockerfile1.alpine -f Dockerfile2.nofrom $BUDFILES/from-multiple-files run_buildah from --quiet ${target} cid=$output run_buildah mount ${cid} root=$output cmp $root/Dockerfile1 $BUDFILES/from-multiple-files/Dockerfile1.alpine cmp $root/Dockerfile2.nofrom $BUDFILES/from-multiple-files/Dockerfile2.nofrom test -s $root/etc/passwd } @test "bud-from-multiple-files-two-froms" { _prefetch alpine target=scratch-image run_buildah build $WITH_POLICY_JSON -t ${target} -f Dockerfile1.scratch -f Dockerfile2.withfrom $BUDFILES/from-multiple-files run_buildah from --quiet ${target} cid=$output run_buildah mount ${cid} root=$output test ! -s $root/Dockerfile1 cmp $root/Dockerfile2.withfrom $BUDFILES/from-multiple-files/Dockerfile2.withfrom test -s $root/etc/passwd run_buildah rm ${cid} run_buildah rmi -a _prefetch alpine target=alpine-image run_buildah build $WITH_POLICY_JSON -t ${target} -f Dockerfile1.alpine -f Dockerfile2.withfrom $BUDFILES/from-multiple-files run_buildah from --quiet ${target} cid=$output run_buildah mount ${cid} root=$output test ! -s $root/Dockerfile1 cmp $root/Dockerfile2.withfrom $BUDFILES/from-multiple-files/Dockerfile2.withfrom test -s $root/etc/passwd } @test "build using --layer-label and test labels on intermediate images" { _prefetch alpine label="l_$(random_string)" labelvalue="v_$(random_string)" run_buildah build --no-cache --layers --layer-label $label=$labelvalue --layer-label emptylabel $WITH_POLICY_JSON -t exp -f $BUDFILES/simple-multi-step/Containerfile # Final image must not contain the layer-label run_buildah inspect --format '{{ index .Docker.Config.Labels "'$label'"}}' exp expect_output "" "label on actual image" # Find all intermediate images... run_buildah images -a --format '{{.ID}}' --filter intermediate=true # ...and confirm that they have both $label and emptylabel for image in "${lines[@]}";do run_buildah inspect $image inspect="$output" run jq -r ".Docker.config.Labels.$label" <<<"$inspect" assert "$output" = "$labelvalue" "label in intermediate layer $image" run jq -r ".Docker.config.Labels.emptylabel" <<<"$inspect" assert "$output" = "" "emptylabel in intermediate layer $image" done } @test "bud and test --unsetlabel" { base=quay.io/libpod/testimage:20241011 _prefetch $base target=exp run_buildah --version local -a output_fields=($output) buildah_version=${output_fields[2]} buildah inspect --format '{{ .Docker.Config.Labels }}' $base not_want_output='map[]' assert "$output" != "$not_want_output" "expected some labels to be set in base image $base" labels=$(buildah inspect --format '{{ range $key, $value := .Docker.Config.Labels }}{{ $key }} {{end}}' $base) labelflags="--label hello=world" for label in $labels; do if test $label != io.buildah.version ; then labelflags="$labelflags --unsetlabel $label" fi done run_buildah build $WITH_POLICY_JSON $labelflags -t $target --from $base $BUDFILES/base-with-labels # no labels should be inherited from base image, only the buildah version label # and `hello=world` which we just added using cli flag want_output='map["hello":"world" "io.buildah.version":"'$buildah_version'"]' run_buildah inspect --format '{{printf "%q" .Docker.Config.Labels}}' $target expect_output "$want_output" } @test "bud and test --unsetannotation" { base=quay.io/libpod/testimage:20241011 _prefetch $base target=exp run_buildah --version local -a output_fields=($output) buildah_version=${output_fields[2]} buildah inspect --format '{{ .ImageAnnotations }}' $base not_want_output='map[]' assert "$output" != "$not_want_output" "expected some annotations to be set in base image $base" annotations=$(buildah inspect --format '{{ range $key, $value := .ImageAnnotations }}{{ $key }} {{end}}' $base) annotationflags="--annotation hello=world --unsetannotation=org.opencontainers.image.created" for annotation in $annotations; do if test $annotation != io.buildah.version ; then annotationflags="$annotationflags --unsetannotation $annotation" fi done run_buildah build $WITH_POLICY_JSON $annotationflags -t $target --from $base $BUDFILES/base-with-labels # no annotations should be inherited from base image # and `hello=world` which we just added using cli flag want_output='map["hello":"world"]' run_buildah inspect --format '{{printf "%q" .ImageAnnotations}}' $target expect_output "$want_output" } @test "bud and test --unsetannotation with --layers" { base=quay.io/libpod/testimage:20241011 _prefetch $base target=exp run_buildah --version local -a output_fields=($output) buildah_version=${output_fields[2]} buildah inspect --format '{{ .ImageAnnotations }}' $base not_want_output='map[]' assert "$output" != "$not_want_output" "expected some annotations to be set in base image $base" ## Build without removing annotations run_buildah build $WITH_POLICY_JSON --layers --iidfile ${TEST_SCRATCH_DIR}/iid1 -t $target --from $base $BUDFILES/base-with-labels ## Second build must use cache run_buildah build $WITH_POLICY_JSON --layers --iidfile ${TEST_SCRATCH_DIR}/iid2 -t $target --from $base $BUDFILES/base-with-labels ## Must use cache expect_output --substring " Using cache" cmp ${TEST_SCRATCH_DIR}/iid1 ${TEST_SCRATCH_DIR}/iid2 annotations=$(buildah inspect --format '{{ range $key, $value := .ImageAnnotations }}{{ $key }} {{end}}' $base) annotationflags="--annotation hello=world --unsetannotation=org.opencontainers.image.created" for annotation in $annotations; do if test $annotation != io.buildah.version ; then annotationflags="$annotationflags --unsetannotation $annotation" fi done ## Since we are unsetting something, this should not use previous image present in the cache. run_buildah build $WITH_POLICY_JSON --layers $annotationflags -t $target --from $base $BUDFILES/base-with-labels ## should not contain `Using Cache` #assert "$output" !~ "Using cache" # no annotations should be inherited from base image # and `hello=world` which we just added using cli flag want_output='map["hello":"world"]' run_buildah inspect --format '{{printf "%q" .ImageAnnotations}}' $target expect_output "$want_output" } @test "bud and test --unsetannotation with only base image" { base=quay.io/libpod/testimage:20241011 _prefetch $base target=exp run_buildah --version local -a output_fields=($output) buildah_version=${output_fields[2]} buildah inspect --format '{{ .ImageAnnotations }}' $base not_want_output='map[]' assert "$output" != "$not_want_output" "expected some annotations to be set in base image $base" annotations=$(buildah inspect --format '{{ range $key, $value := .ImageAnnotations }}{{ $key }} {{end}}' $base) annotationflags="--annotation hello=world --created-annotation=false" for annotation in $annotations; do if test $annotation != io.buildah.version ; then annotationflags="$annotationflags --unsetannotation $annotation" fi done run_buildah build $WITH_POLICY_JSON $annotationflags -t $target --from $base $BUDFILES/only-base want_output='map["hello":"world"]' run_buildah inspect --format '{{printf "%q" .ImageAnnotations}}' $target expect_output "$want_output" } @test "bud and test inherit-labels" { base=quay.io/libpod/testimage:20241011 _prefetch $base _prefetch alpine run_buildah --version local -a output_fields=($output) buildah_version=${output_fields[2]} run_buildah build $WITH_POLICY_JSON -t exp -f $BUDFILES/base-with-labels/Containerfile run_buildah inspect --format '{{ index .Docker.Config.Labels "created_at"}}' exp expect_output "2024-10-11T12:26:00Z" "created_at must be inherited from base image" run_buildah inspect --format '{{ index .Docker.Config.Labels "created_by"}}' exp expect_output "test/system/build-testimage" "created_by must be inherited from base image" run_buildah build $WITH_POLICY_JSON --inherit-labels=false --label created_by=world -t exp -f $BUDFILES/base-with-labels/Containerfile # no labels should be inherited from base image, only the buildah version label # and `created_by=world` which we just added using cli flag want_output='map["created_by":"world" "io.buildah.version":"'$buildah_version'"]' run_buildah inspect --format '{{printf "%q" .Docker.Config.Labels}}' exp expect_output "$want_output" # Try building another file with multiple layers run_buildah build $WITH_POLICY_JSON --iidfile ${TEST_SCRATCH_DIR}/id1 --layers -t exp -f $BUDFILES/base-with-labels/Containerfile.layer run_buildah inspect --format '{{ index .Docker.Config.Labels "created_at"}}' exp expect_output "2024-10-11T12:26:00Z" "created_at must be inherited from base image" run_buildah inspect --format '{{ index .Docker.Config.Labels "created_by"}}' exp expect_output "world" "created_by must be world from Containerfile" # Now build same file with --inherit-labels=false and verify if we are not using the cache again. run_buildah build $WITH_POLICY_JSON --layers --inherit-labels=false --iidfile ${TEST_SCRATCH_DIR}/inherit_false_1 -t exp -f $BUDFILES/base-with-labels/Containerfile.layer # Should not contain `Using cache` at all since assert "$output" !~ "Using cache" want_output='map["created_by":"world" "io.buildah.version":"'$buildah_version'"]' run_buildah inspect --format '{{printf "%q" .Docker.Config.Labels}}' exp expect_output "$want_output" run_buildah build $WITH_POLICY_JSON --layers --inherit-labels=false --iidfile ${TEST_SCRATCH_DIR}/inherit_false_2 -t exp -f $BUDFILES/base-with-labels/Containerfile.layer # Should contain `Using cache` expect_output --substring " Using cache" want_output='map["created_by":"world" "io.buildah.version":"'$buildah_version'"]' run_buildah inspect --format '{{printf "%q" .Docker.Config.Labels}}' exp expect_output "$want_output" assert "$(< ${TEST_SCRATCH_DIR}/inherit_false_1)" = "$(< ${TEST_SCRATCH_DIR}/inherit_false_2)" "expected image ids to not change" # Now build same file with --inherit-labels=true and verify if using the cache run_buildah build $WITH_POLICY_JSON --iidfile ${TEST_SCRATCH_DIR}/id2 --layers --inherit-labels=true -t exp -f $BUDFILES/base-with-labels/Containerfile.layer expect_output --substring " Using cache" run_buildah inspect --format '{{ index .Docker.Config.Labels "created_at"}}' exp expect_output "2024-10-11T12:26:00Z" "created_at must be inherited from base image" run_buildah inspect --format '{{ index .Docker.Config.Labels "created_by"}}' exp expect_output "world" "created_by must be world from Containerfile" # Final image id should be exactly same as the one image which was built in the past. assert "$(< ${TEST_SCRATCH_DIR}/id1)" = "$(< ${TEST_SCRATCH_DIR}/id2)" "expected image ids to not change" # Now build same file with --inherit-labels=false and verify if target stage did not inherit any labels from base stage. run_buildah build $WITH_POLICY_JSON --layers --inherit-labels=false -t exp -f $BUDFILES/base-with-labels/Containerfile.multi-stage want_output='map["io.buildah.version":"'$buildah_version'"]' run_buildah inspect --format '{{printf "%q" .Docker.Config.Labels}}' exp expect_output "$want_output" # Now build same file with --inherit-labels=true and verify if target stage inherits labels from the base stage. run_buildah build $WITH_POLICY_JSON --iidfile ${TEST_SCRATCH_DIR}/id3 --layers --inherit-labels=true -t exp -f $BUDFILES/base-with-labels/Containerfile.multi-stage want_output='map["created_by":"world" "io.buildah.version":"'$buildah_version'"]' run_buildah inspect --format '{{printf "%q" .Docker.Config.Labels}}' exp expect_output "$want_output" # Rebuild again with layers should not build image again at all. run_buildah build $WITH_POLICY_JSON --iidfile ${TEST_SCRATCH_DIR}/id4 --layers --inherit-labels=true -t exp -f $BUDFILES/base-with-labels/Containerfile.multi-stage want_output='map["created_by":"world" "io.buildah.version":"'$buildah_version'"]' run_buildah inspect --format '{{printf "%q" .Docker.Config.Labels}}' exp expect_output "$want_output" assert "$(< ${TEST_SCRATCH_DIR}/id3)" = "$(< ${TEST_SCRATCH_DIR}/id4)" "expected image ids to not change" } @test "build using intermediate images should not inherit label" { _prefetch alpine # Build imageone, with a label run_buildah build --no-cache --layers --label somefancylabel=true $WITH_POLICY_JSON -t imageone -f Dockerfile.name $BUDFILES/multi-stage-builds run_buildah inspect --format '{{ index .Docker.Config.Labels "somefancylabel"}}' imageone expect_output "true" "imageone: somefancylabel" # Build imagetwo. Must use all steps from cache but should not contain label run_buildah build --layers $WITH_POLICY_JSON -t imagetwo -f Dockerfile.name $BUDFILES/multi-stage-builds for i in 2 6;do expect_output --substring --from="${lines[$i]}" "Using cache" \ "build imagetwo (no label), line $i" done run_buildah inspect --format '{{ index .Docker.Config.Labels "somefancylabel"}}' imagetwo expect_output "" "imagetwo: somefancylabel" # build another multi-stage image with different label, it should use stages from cache from previous build run_buildah build --layers $WITH_POLICY_JSON --label anotherfancylabel=true -t imagethree -f Dockerfile.name $BUDFILES/multi-stage-builds for i in 2 6;do expect_output --substring --from="${lines[$i]}" "Using cache" \ "build imagethree ('anotherfancylabel'), line $i" done run_buildah inspect --format '{{ index .Docker.Config.Labels "somefancylabel"}}' imagethree expect_output "" "imagethree: somefancylabel" run_buildah inspect --format '{{ index .Docker.Config.Labels "anotherfancylabel"}}' imagethree expect_output "true" "imagethree: anotherfancylabel" } @test "bud-multi-stage-builds" { _prefetch alpine target=multi-stage-index run_buildah build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/multi-stage-builds/Dockerfile.index $BUDFILES/multi-stage-builds run_buildah from --quiet ${target} cid=$output run_buildah mount ${cid} root=$output cmp $root/Dockerfile.index $BUDFILES/multi-stage-builds/Dockerfile.index test -s $root/etc/passwd run_buildah rm ${cid} run_buildah rmi -a _prefetch alpine target=multi-stage-name run_buildah build $WITH_POLICY_JSON -t ${target} -f Dockerfile.name $BUDFILES/multi-stage-builds run_buildah from --quiet ${target} cid=$output run_buildah mount ${cid} root=$output cmp $root/Dockerfile.name $BUDFILES/multi-stage-builds/Dockerfile.name test ! -s $root/etc/passwd run_buildah rm ${cid} run_buildah rmi -a target=multi-stage-mixed run_buildah build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/multi-stage-builds/Dockerfile.mixed $BUDFILES/multi-stage-builds run_buildah from --quiet ${target} cid=$output run_buildah mount ${cid} root=$output cmp $root/Dockerfile.name $BUDFILES/multi-stage-builds/Dockerfile.name cmp $root/Dockerfile.index $BUDFILES/multi-stage-builds/Dockerfile.index cmp $root/Dockerfile.mixed $BUDFILES/multi-stage-builds/Dockerfile.mixed } @test "bud-multi-stage-builds-small-as" { _prefetch alpine target=multi-stage-index run_buildah build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/multi-stage-builds-small-as/Dockerfile.index $BUDFILES/multi-stage-builds-small-as run_buildah from --quiet ${target} cid=$output run_buildah mount ${cid} root=$output cmp $root/Dockerfile.index $BUDFILES/multi-stage-builds-small-as/Dockerfile.index test -s $root/etc/passwd run_buildah rm ${cid} run_buildah rmi -a _prefetch alpine target=multi-stage-name run_buildah build $WITH_POLICY_JSON -t ${target} -f Dockerfile.name $BUDFILES/multi-stage-builds-small-as run_buildah from --quiet ${target} cid=$output run_buildah mount ${cid} root=$output cmp $root/Dockerfile.name $BUDFILES/multi-stage-builds-small-as/Dockerfile.name test ! -s $root/etc/passwd run_buildah rm ${cid} run_buildah rmi -a _prefetch alpine target=multi-stage-mixed run_buildah build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/multi-stage-builds-small-as/Dockerfile.mixed $BUDFILES/multi-stage-builds-small-as run_buildah from --quiet ${target} cid=$output run_buildah mount ${cid} root=$output cmp $root/Dockerfile.name $BUDFILES/multi-stage-builds-small-as/Dockerfile.name cmp $root/Dockerfile.index $BUDFILES/multi-stage-builds-small-as/Dockerfile.index cmp $root/Dockerfile.mixed $BUDFILES/multi-stage-builds-small-as/Dockerfile.mixed } @test "bud-preserve-subvolumes" { # This Dockerfile needs us to be able to handle a working RUN instruction. skip_if_no_runtime _prefetch alpine for layers in "" --layers ; do for compat in "" --compat-volumes ; do target=volume-image$compat$layers run_buildah build $WITH_POLICY_JSON -t ${target} ${layers} ${compat} $BUDFILES/preserve-volumes run_buildah from --quiet ${target} cid=$output run_buildah mount ${cid} root=$output # these files were created before VOLUME instructions froze the directories that contained them test -s $root/vol/subvol/subsubvol/subsubvolfile test -s $root/vol/volfile if test "$compat" != "" ; then # true, these files should have been discarded after they were created by RUN instructions test ! -s $root/vol/subvol/subvolfile test ! -s $root/vol/anothervolfile else # false, these files should not have been discarded, despite being created by RUN instructions test -s $root/vol/subvol/subvolfile test -s $root/vol/anothervolfile fi # and these were ADDed test -s $root/vol/Dockerfile test -s $root/vol/Dockerfile2 run_buildah rm ${cid} run_buildah rmi ${target} done done } # Helper function for several of the tests which pull from http. # # Usage: _test_http SUBDIRECTORY URL_PATH [EXTRA ARGS] # # SUBDIRECTORY is a subdirectory path under the 'bud' subdirectory. # This will be the argument to starthttpd(), i.e. where # the httpd will serve files. # # URL_PATH is the path requested by buildah from the http server, # probably 'Dockerfile' or 'context.tar' # # [EXTRA ARGS] if present, will be passed to buildah on the 'build' # command line; it is intended for '-f subdir/Dockerfile'. # function _test_http() { local testdir=$1; shift; # in: subdirectory under bud/ local urlpath=$1; shift; # in: path to request from localhost starthttpd "$BUDFILES/$testdir" target=scratch-image run_buildah build $WITH_POLICY_JSON \ -t ${target} \ "$@" \ http://0.0.0.0:${HTTP_SERVER_PORT}/$urlpath stophttpd run_buildah from ${target} } # Helper function for several of the tests which verifies compression. # # Usage: validate_instance_compression INDEX MANIFEST ARCH COMPRESSION # # INDEX instance which needs to be verified in # provided manifest list. # # MANIFEST OCI manifest specification in json format # # ARCH instance architecture # # COMPRESSION compression algorithm name; e.g "zstd". # function validate_instance_compression { case $4 in gzip) run jq -r '.manifests['$1'].annotations' <<< $2 # annotation is `null` for gzip compression assert "$output" = "null" ".manifests[$1].annotations (null means gzip)" ;; zstd) # annotation `'"io.github.containers.compression.zstd": "true"'` must be there for zstd compression run jq -r '.manifests['$1'].annotations."io.github.containers.compression.zstd"' <<< $2 assert "$output" = "true" ".manifests[$1].annotations.'io.github.containers.compression.zstd' (io.github.containers.compression.zstd must be set)" ;; esac run jq -r '.manifests['$1'].platform.architecture' <<< $2 assert "$output" = $3 ".manifests[$1].platform.architecture" } @test "bud-http-Dockerfile" { _test_http from-scratch Containerfile } @test "bud-http-context-with-Dockerfile" { _test_http http-context context.tar } @test "bud-http-context-dir-with-Dockerfile" { _test_http http-context-subdir context.tar -f context/Dockerfile } @test "bud-git-context" { # We need git to be around to handle cloning a repository. if ! which git ; then skip "no git in PATH" fi target=giturl-image # Any repo would do, but this one is small, is FROM: scratch, and local. if ! start_git_daemon ; then skip "error running git daemon" fi gitrepo=git://localhost:${GITPORT}/repo run_buildah build $WITH_POLICY_JSON -t ${target} "${gitrepo}" run_buildah from ${target} } @test "bud-git-context-subdirectory" { # We need git to be around to handle cloning a repository. if ! which git ; then skip "no git in PATH" fi target=giturl-image # Any repo would do, but this one is small, is FROM: scratch, local, and has # its entire build context in a subdirectory of the repository. if ! start_git_daemon ${TEST_SOURCES}/git-daemon/subdirectory.tar.gz ; then skip "error running git daemon" fi gitrepo=git://localhost:${GITPORT}/repo#main:nested/subdirectory tmpdir="${TEST_SCRATCH_DIR}/build" mkdir -p "${tmpdir}" TMPDIR="${tmpdir}" run_buildah build $WITH_POLICY_JSON -t ${target} "${gitrepo}" run_buildah from "${target}" run find "${tmpdir}" -type d -print echo "$output" test "${#lines[*]}" -le 2 } @test "bud-git-context-failure" { # We need git to be around to try cloning a repository, even though it'll fail # and buildah will exit with return code 125. if ! which git ; then skip "no git in PATH" fi target=giturl-image gitrepo=git:///tmp/no-such-repository run_buildah 125 build $WITH_POLICY_JSON -t ${target} "${gitrepo}" # Expect part of what git would have told us... before things went horribly wrong expect_output --substring "failed while performing" expect_output --substring "git fetch" } @test "bud-github-context" { target=github-image # Any repo should do, but this one is small and is FROM: scratch. gitrepo=github.com/projectatomic/nulecule-library run_buildah build $WITH_POLICY_JSON -t ${target} "${gitrepo}" run_buildah from ${target} } # Containerfile in this repo should only exist on older commit and # not on HEAD or the default branch. @test "bud-github-context-from-commit" { if ! which git ; then skip "no git in PATH" fi target=giturl-image # Any repo would do, but this one is small, is FROM: scratch, local, and has # its entire build context in a subdirectory of the repository. if ! start_git_daemon ${TEST_SOURCES}/git-daemon/repo-with-containerfile-on-old-commit.tar.gz ; then skip "error running git daemon" fi # Containerfile in this repo should only exist on older commit and # not on HEAD or the default branch. gitrepo=git://localhost:${GITPORT}/repo#f94193d34548eb58650a10a5183936d32c2d3280 run_buildah build $WITH_POLICY_JSON -t ${target} "${gitrepo}" expect_output --substring "FROM scratch" expect_output --substring "COMMIT giturl-image" # Verify that build must fail on default `main` branch since we # don't have a `Containerfile` on main branch. gitrepo=git://localhost:${GITPORT}/repo#main run_buildah 125 build $WITH_POLICY_JSON -t ${target} "${gitrepo}" expect_output --substring "cannot find Containerfile or Dockerfile" } @test "bud-github-context-with-branch-subdir-commit" { subdir=tests/bud/from-scratch target=github-image gitrepo=https://github.com/containers/buildah.git#main:$subdir run_buildah build $WITH_POLICY_JSON -t ${target} "${gitrepo}" # check syntax only for subdirectory gitrepo=https://github.com/containers/buildah.git#:$subdir run_buildah build $WITH_POLICY_JSON -t ${target} "${gitrepo}" # Try pulling repo with specific commit # This commit is the initial commit, which used Dockerfile rather then Containerfile gitrepo=https://github.com/containers/buildah.git#761597056c8dc2bb1efd67e937a196ddff1fa7a6:$subdir run_buildah build $WITH_POLICY_JSON -t ${target} "${gitrepo}" } @test "bud-additional-tags" { target=scratch-image target2=another-scratch-image target3=so-many-scratch-images run_buildah build $WITH_POLICY_JSON -t ${target} -t docker.io/${target2} -t ${target3} $BUDFILES/from-scratch run_buildah images run_buildah from --quiet ${target} cid=$output run_buildah rm ${cid} run_buildah from --quiet $WITH_POLICY_JSON library/${target2} cid=$output run_buildah rm ${cid} run_buildah from --quiet $WITH_POLICY_JSON ${target3}:latest run_buildah rm $output run_buildah rmi $target3 $target2 $target expect_line_count 4 for i in 0 1 2;do expect_output --substring --from="${lines[$i]}" "untagged: " done expect_output --substring --from="${lines[3]}" '^[0-9a-f]{64}$' } @test "bud-additional-tags-cached" { _prefetch busybox target=tagged-image target2=another-tagged-image target3=yet-another-tagged-image target4=still-another-tagged-image run_buildah build --layers $WITH_POLICY_JSON -t ${target} $BUDFILES/addtl-tags run_buildah build --layers $WITH_POLICY_JSON -t ${target2} -t ${target3} -t ${target4} $BUDFILES/addtl-tags run_buildah inspect -f '{{.FromImageID}}' busybox busyboxid="$output" run_buildah inspect -f '{{.FromImageID}}' ${target} targetid="$output" assert "$targetid" != "$busyboxid" "FromImageID(target) != busybox" run_buildah inspect -f '{{.FromImageID}}' ${target2} expect_output "$targetid" "target2 -> .FromImageID" run_buildah inspect -f '{{.FromImageID}}' ${target3} expect_output "$targetid" "target3 -> .FromImageID" run_buildah inspect -f '{{.FromImageID}}' ${target4} expect_output "$targetid" "target4 -> .FromImageID" } @test "bud-volume-perms" { # This Dockerfile needs us to be able to handle a working RUN instruction. skip_if_no_runtime _prefetch alpine for layers in "" --layers ; do for compat in "" --compat-volumes ; do target=volume-image$compat$layers run_buildah build $WITH_POLICY_JSON -t ${target} ${layers} ${compat} $BUDFILES/volume-perms run_buildah from --quiet $WITH_POLICY_JSON ${target} cid=$output run_buildah mount ${cid} root=$output if test "$compat" != "" ; then # true, /vol/subvol should not have contents, and its permissions should be the default 0755 test -d $root/vol/subvol test ! -s $root/vol/subvol/subvolfile run stat -c %a $root/vol/subvol assert "$status" -eq 0 "status code from stat $root/vol/subvol" expect_output "755" "stat($root/vol/subvol)" else # true, /vol/subvol should have contents, and its permissions should be the changed 0711 test -d $root/vol/subvol test -s $root/vol/subvol/subvolfile run stat -c %a $root/vol/subvol assert "$status" -eq 0 "status code from stat $root/vol/subvol" expect_output "711" "stat($root/vol/subvol)" fi run_buildah rm ${cid} run_buildah rmi ${target} done done } @test "bud-volume-ownership" { # This Dockerfile needs us to be able to handle a working RUN instruction. skip_if_no_runtime _prefetch alpine target=volume-image run_buildah build $WITH_POLICY_JSON -t ${target} $BUDFILES/volume-ownership run_buildah from --quiet $WITH_POLICY_JSON ${target} cid=$output run_buildah run $cid stat -c "%U %G" /vol/subvol expect_output "testuser testgroup" } @test "bud-builtin-volume-symlink" { # This Dockerfile needs us to be able to handle a working RUN instruction. skip_if_no_runtime _prefetch alpine target=volume-symlink run_buildah build $WITH_POLICY_JSON -t ${target} $BUDFILES/volume-symlink run_buildah from --quiet $WITH_POLICY_JSON ${target} cid=$output run_buildah run $cid echo hello expect_output "hello" target=volume-no-symlink run_buildah build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/volume-symlink/Dockerfile.no-symlink $BUDFILES/volume-symlink run_buildah from --quiet $WITH_POLICY_JSON ${target} cid=$output run_buildah run $cid echo hello expect_output "hello" } @test "bud-from-glob" { _prefetch alpine target=alpine-image run_buildah build $WITH_POLICY_JSON -t ${target} -f Dockerfile2.glob $BUDFILES/from-multiple-files run_buildah from --quiet $WITH_POLICY_JSON ${target} cid=$output run_buildah mount ${cid} root=$output cmp $root/Dockerfile1.alpine $BUDFILES/from-multiple-files/Dockerfile1.alpine cmp $root/Dockerfile2.withfrom $BUDFILES/from-multiple-files/Dockerfile2.withfrom } @test "bud-maintainer" { _prefetch alpine target=alpine-image run_buildah build $WITH_POLICY_JSON -t ${target} $BUDFILES/maintainer run_buildah inspect --type=image --format '{{.Docker.Author}}' ${target} expect_output "kilroy" run_buildah inspect --type=image --format '{{.OCIv1.Author}}' ${target} expect_output "kilroy" } @test "bud-unrecognized-instruction" { _prefetch alpine target=alpine-image run_buildah 125 build $WITH_POLICY_JSON -t ${target} $BUDFILES/unrecognized expect_output --substring "BOGUS" } @test "bud-shell" { _prefetch alpine target=alpine-image run_buildah build --format docker $WITH_POLICY_JSON -t ${target} $BUDFILES/shell run_buildah inspect --type=image --format '{{printf "%q" .Docker.Config.Shell}}' ${target} expect_output '["/bin/sh" "-c"]' ".Docker.Config.Shell (original)" run_buildah from --quiet $WITH_POLICY_JSON ${target} ctr=$output run_buildah config --shell "/bin/bash -c" ${ctr} run_buildah inspect --type=container --format '{{printf "%q" .Docker.Config.Shell}}' ${ctr} expect_output '["/bin/bash" "-c"]' ".Docker.Config.Shell (changed)" } @test "bud-shell during build in Docker format" { _prefetch alpine target=alpine-image run_buildah build --format docker $WITH_POLICY_JSON -t ${target} -f $BUDFILES/shell/Dockerfile.build-shell-default $BUDFILES/shell expect_output --substring "SHELL=/bin/sh" } @test "bud-shell during build in OCI format" { _prefetch alpine target=alpine-image run_buildah build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/shell/Dockerfile.build-shell-default $BUDFILES/shell expect_output --substring "SHELL=/bin/sh" } @test "bud-shell changed during build in Docker format" { _prefetch ubuntu target=ubuntu-image run_buildah build --format docker $WITH_POLICY_JSON -t ${target} -f $BUDFILES/shell/Dockerfile.build-shell-custom $BUDFILES/shell expect_output --substring "SHELL=/bin/bash" } @test "bud-shell changed during build in OCI format" { _prefetch ubuntu target=ubuntu-image run_buildah build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/shell/Dockerfile.build-shell-custom $BUDFILES/shell expect_output --substring "SHELL=/bin/bash" # should work, but should also emit warning that it won't be persisted into saved image expect_output --substring "SHELL is not persisted in the OCI image format, \[/bin/bash -c\]" } @test "bud with symlinks" { _prefetch alpine target=alpine-image run_buildah build $WITH_POLICY_JSON -t ${target} $BUDFILES/symlink run_buildah from --quiet $WITH_POLICY_JSON ${target} cid=$output run_buildah mount ${cid} root=$output run ls $root/data/log assert "$status" -eq 0 "status from ls $root/data/log" expect_output --substring "test" "ls \$root/data/log" expect_output --substring "blah.txt" "ls \$root/data/log" run ls -al $root assert "$status" -eq 0 "status from ls -al $root" expect_output --substring "test-log -> /data/log" "ls -l \$root/data/log" expect_output --substring "blah -> /test-log" "ls -l \$root/data/log" } @test "bud with symlinks to relative path" { _prefetch alpine target=alpine-image run_buildah build $WITH_POLICY_JSON -t ${target} -f Dockerfile.relative-symlink $BUDFILES/symlink run_buildah from --quiet $WITH_POLICY_JSON ${target} cid=$output run_buildah mount ${cid} root=$output run ls $root/log assert "$status" -eq 0 "status from ls $root/log" expect_output --substring "test" "ls \$root/log" run ls -al $root assert "$status" -eq 0 "status from ls -al $root" expect_output --substring "test-log -> ../log" "ls -l \$root/log" test -r $root/var/data/empty } @test "bud with multiple symlinks in a path" { _prefetch alpine target=alpine-image run_buildah build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/symlink/Dockerfile.multiple-symlinks $BUDFILES/symlink run_buildah from --quiet $WITH_POLICY_JSON ${target} cid=$output run_buildah mount ${cid} root=$output run ls $root/data/log assert "$status" -eq 0 "status from ls $root/data/log" expect_output --substring "bin" "ls \$root/data/log" expect_output --substring "blah.txt" "ls \$root/data/log" run ls -al $root/myuser assert "$status" -eq 0 "status from ls -al $root/myuser" expect_output --substring "log -> /test" "ls -al \$root/myuser" run ls -al $root/test assert "$status" -eq 0 "status from ls -al $root/test" expect_output --substring "bar -> /test-log" "ls -al \$root/test" run ls -al $root/test-log assert "$status" -eq 0 "status from ls -al $root/test-log" expect_output --substring "foo -> /data/log" "ls -al \$root/test-log" } @test "bud with multiple symlink pointing to itself" { _prefetch alpine target=alpine-image run_buildah 125 build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/symlink/Dockerfile.symlink-points-to-itself $BUDFILES/symlink assert "$output" =~ "building .* open /test-log/test: too many levels of symbolic links" } @test "bud multi-stage with symlink to absolute path" { _prefetch ubuntu target=ubuntu-image run_buildah build $WITH_POLICY_JSON -t ${target} -f Dockerfile.absolute-symlink $BUDFILES/symlink run_buildah from --quiet $WITH_POLICY_JSON ${target} cid=$output run_buildah mount ${cid} root=$output run ls $root/bin assert "$status" -eq 0 "status from ls $root/bin" expect_output --substring "myexe" "ls \$root/bin" run cat $root/bin/myexe assert "$status" -eq 0 "status from cat $root/bin/myexe" expect_output "symlink-test" "cat \$root/bin/myexe" } @test "bud multi-stage with dir symlink to absolute path" { _prefetch ubuntu target=ubuntu-image run_buildah build $WITH_POLICY_JSON -t ${target} -f Dockerfile.absolute-dir-symlink $BUDFILES/symlink run_buildah from --quiet $WITH_POLICY_JSON ${target} cid=$output run_buildah mount ${cid} root=$output run ls $root/data assert "$status" -eq 0 "status from ls $root/data" expect_output --substring "myexe" "ls \$root/data" } @test "bud with ENTRYPOINT and RUN" { _prefetch alpine target=alpine-image run_buildah build $WITH_POLICY_JSON -t ${target} -f Dockerfile.entrypoint-run $BUDFILES/run-scenarios expect_output --substring "unique.test.string" run_buildah from --quiet $WITH_POLICY_JSON ${target} } @test "bud with ENTRYPOINT and empty RUN" { _prefetch alpine target=alpine-image run_buildah 2 bud $WITH_POLICY_JSON -t ${target} -f Dockerfile.entrypoint-empty-run $BUDFILES/run-scenarios expect_output --substring " -c requires an argument" expect_output --substring "building at STEP.*: exit status 2" } @test "bud with CMD and RUN" { _prefetch alpine target=alpine-image run_buildah build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/run-scenarios/Dockerfile.cmd-run $BUDFILES/run-scenarios expect_output --substring "unique.test.string" run_buildah from --quiet $WITH_POLICY_JSON ${target} } @test "bud with CMD and empty RUN" { _prefetch alpine target=alpine-image run_buildah 2 bud $WITH_POLICY_JSON -t ${target} -f Dockerfile.cmd-empty-run $BUDFILES/run-scenarios expect_output --substring " -c requires an argument" expect_output --substring "building at STEP.*: exit status 2" } @test "bud with ENTRYPOINT, CMD and RUN" { _prefetch alpine target=alpine-image run_buildah build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/run-scenarios/Dockerfile.entrypoint-cmd-run $BUDFILES/run-scenarios expect_output --substring "unique.test.string" run_buildah from $WITH_POLICY_JSON ${target} } @test "bud with ENTRYPOINT, CMD and empty RUN" { _prefetch alpine target=alpine-image run_buildah 2 bud $WITH_POLICY_JSON -t ${target} -f $BUDFILES/run-scenarios/Dockerfile.entrypoint-cmd-empty-run $BUDFILES/run-scenarios expect_output --substring " -c requires an argument" expect_output --substring "building at STEP.*: exit status 2" } # Determines if a variable set with ENV is available to following commands in the Dockerfile @test "bud access ENV variable defined in same source file" { _prefetch alpine target=env-image run_buildah build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/env/Dockerfile.env-same-file $BUDFILES/env expect_output --substring ":unique.test.string:" run_buildah from $WITH_POLICY_JSON ${target} } # Determines if a variable set with ENV in an image is available to commands in downstream Dockerfile @test "bud access ENV variable defined in FROM image" { _prefetch alpine from_target=env-from-image target=env-image run_buildah build $WITH_POLICY_JSON -t ${from_target} -f $BUDFILES/env/Dockerfile.env-same-file $BUDFILES/env run_buildah build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/env/Dockerfile.env-from-image $BUDFILES/env expect_output --substring "@unique.test.string@" run_buildah from --quiet ${from_target} from_cid=$output run_buildah from ${target} } @test "bud ENV preserves special characters after commit" { _prefetch ubuntu from_target=special-chars run_buildah build $WITH_POLICY_JSON -t ${from_target} -f $BUDFILES/env/Dockerfile.special-chars $BUDFILES/env run_buildah from --quiet ${from_target} cid=$output run_buildah run ${cid} env expect_output --substring "LIB=\\$\(PREFIX\)/lib" } @test "bud with Dockerfile from valid URL" { target=url-image url=https://raw.githubusercontent.com/containers/buildah/main/tests/bud/from-scratch/Dockerfile run_buildah build $WITH_POLICY_JSON -t ${target} ${url} run_buildah from ${target} } @test "bud with Dockerfile from invalid URL" { target=url-image url=https://raw.githubusercontent.com/containers/buildah/main/tests/bud/from-scratch/Dockerfile.bogus run_buildah 125 build $WITH_POLICY_JSON -t ${target} ${url} expect_output --substring "invalid response status 404" } # When provided with a -f flag and directory, buildah will look for the alternate Dockerfile name in the supplied directory @test "bud with -f flag, alternate Dockerfile name" { target=fileflag-image run_buildah build $WITH_POLICY_JSON -t ${target} -f Dockerfile.noop-flags $BUDFILES/run-scenarios run_buildah from ${target} } # Following flags are configured to result in noop but should not affect buildah bud behavior @test "bud with --cache-from noop flag" { target=noop-image run_buildah build --cache-from=invalidimage $WITH_POLICY_JSON -t ${target} -f Dockerfile.noop-flags $BUDFILES/run-scenarios run_buildah from ${target} } @test "bud with --compress noop flag" { target=noop-image run_buildah build --compress $WITH_POLICY_JSON -t ${target} -f Dockerfile.noop-flags $BUDFILES/run-scenarios run_buildah from ${target} } @test "bud with --cpu-shares flag, no argument" { target=bud-flag run_buildah 125 build --cpu-shares $WITH_POLICY_JSON -t ${target} -f $BUDFILES/from-scratch/Containerfile $BUDFILES/from-scratch expect_output --substring "invalid argument .* invalid syntax" } @test "bud with --cpu-shares flag, invalid argument" { target=bud-flag run_buildah 125 build --cpu-shares bogus $WITH_POLICY_JSON -t ${target} -f $BUDFILES/from-scratch/Containerfile $BUDFILES/from-scratch expect_output --substring "invalid argument \"bogus\" for " } @test "bud with --cpu-shares flag, valid argument" { target=bud-flag run_buildah build --cpu-shares 2 $WITH_POLICY_JSON -t ${target} -f $BUDFILES/from-scratch/Containerfile $BUDFILES/from-scratch run_buildah from ${target} } @test "bud with --cpu-shares short flag (-c), no argument" { target=bud-flag run_buildah 125 build -c $WITH_POLICY_JSON -t ${target} -f $BUDFILES/from-scratch/Containerfile $BUDFILES/from-scratch expect_output --substring "invalid argument .* invalid syntax" } @test "bud with --cpu-shares short flag (-c), invalid argument" { target=bud-flag run_buildah 125 build -c bogus $WITH_POLICY_JSON -t ${target} -f $BUDFILES/from-scratch/Containerfile $BUDFILES/from-scratch expect_output --substring "invalid argument \"bogus\" for " } @test "bud with --cpu-shares short flag (-c), valid argument" { target=bud-flag run_buildah build -c 2 $WITH_POLICY_JSON -t ${target} $BUDFILES/from-scratch run_buildah from ${target} } @test "bud-onbuild" { _prefetch alpine target=onbuild run_buildah build --format docker $WITH_POLICY_JSON -t ${target} $BUDFILES/onbuild run_buildah inspect --format '{{printf "%q" .Docker.Config.OnBuild}}' ${target} expect_output '["RUN touch /onbuild1" "RUN touch /onbuild2"]' run_buildah from --quiet ${target} cid=${lines[0]} run_buildah mount ${cid} root=$output test -e ${root}/onbuild1 test -e ${root}/onbuild2 run_buildah umount ${cid} run_buildah rm ${cid} target=onbuild-image2 run_buildah build --format docker $WITH_POLICY_JSON -t ${target} -f Dockerfile1 $BUDFILES/onbuild run_buildah inspect --format '{{printf "%q" .Docker.Config.OnBuild}}' ${target} expect_output '["RUN touch /onbuild3"]' run_buildah from --quiet ${target} cid=${lines[0]} run_buildah mount ${cid} root=$output test -e ${root}/onbuild1 test -e ${root}/onbuild2 test -e ${root}/onbuild3 run_buildah umount ${cid} run_buildah config --onbuild "RUN touch /onbuild4" ${cid} target=onbuild-image3 run_buildah commit $WITH_POLICY_JSON --format docker ${cid} ${target} run_buildah inspect --format '{{printf "%q" .Docker.Config.OnBuild}}' ${target} expect_output '["RUN touch /onbuild4"]' } @test "bud-onbuild-layers" { _prefetch alpine target=onbuild run_buildah build --format docker $WITH_POLICY_JSON --layers -t ${target} -f Dockerfile2 $BUDFILES/onbuild run_buildah inspect --format '{{printf "%q" .Docker.Config.OnBuild}}' ${target} expect_output '["RUN touch /onbuild1" "RUN touch /onbuild2"]' } @test "bud-logfile" { _prefetch alpine rm -f ${TEST_SCRATCH_DIR}/logfile run_buildah build --logfile ${TEST_SCRATCH_DIR}/logfile $WITH_POLICY_JSON $BUDFILES/preserve-volumes test -s ${TEST_SCRATCH_DIR}/logfile } @test "bud-logfile-with-split-logfile-by-platform" { mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p $mytmpdir cat > $mytmpdir/Containerfile << _EOF FROM alpine COPY . . _EOF rm -f ${TEST_SCRATCH_DIR}/logfile run_buildah build --logfile ${TEST_SCRATCH_DIR}/logfile --logsplit --platform linux/arm64,linux/amd64 $WITH_POLICY_JSON ${mytmpdir} run cat ${TEST_SCRATCH_DIR}/logfile_linux_arm64 expect_output --substring "FROM alpine" expect_output --substring "[linux/arm64]" run cat ${TEST_SCRATCH_DIR}/logfile_linux_amd64 expect_output --substring "FROM alpine" expect_output --substring "[linux/amd64]" } @test "bud with ARGS" { _prefetch alpine target=alpine-image run_buildah build $WITH_POLICY_JSON -t ${target} -f Dockerfile.args $BUDFILES/run-scenarios expect_output --substring "arg_value" } @test "bud with unused ARGS" { _prefetch alpine target=alpine-image run_buildah build $WITH_POLICY_JSON -t ${target} -f Dockerfile.multi-args --build-arg USED_ARG=USED_VALUE $BUDFILES/run-scenarios expect_output --substring "USED_VALUE" assert "$output" !~ "one or more build args were not consumed: [UNUSED_ARG]" run_buildah build $WITH_POLICY_JSON -t ${target} -f Dockerfile.multi-args --build-arg USED_ARG=USED_VALUE --build-arg UNUSED_ARG=whaaaat $BUDFILES/run-scenarios expect_output --substring "USED_VALUE" expect_output --substring "one or more build args were not consumed: \[UNUSED_ARG\]" } @test "bud with multi-value ARGS" { _prefetch alpine target=alpine-image run_buildah build $WITH_POLICY_JSON -t ${target} -f Dockerfile.multi-args --build-arg USED_ARG=plugin1,plugin2,plugin3 $BUDFILES/run-scenarios expect_output --substring "plugin1,plugin2,plugin3" if [[ "$output" =~ "one or more build args were not consumed" ]]; then expect_output "[not expecting to see 'one or more build args were not consumed']" fi } @test "bud-from-stdin" { target=scratch-image cat $BUDFILES/from-multiple-files/Dockerfile1.scratch | run_buildah build $WITH_POLICY_JSON -t ${target} -f - $BUDFILES/from-multiple-files run_buildah from --quiet ${target} cid=$output run_buildah mount ${cid} root=$output test -s $root/Dockerfile1 } @test "bud with preprocessor" { _prefetch busybox target=alpine-image starthttpd $BUDFILES/preprocess run_buildah build --build-arg HTTP_SERVER_PORT=${HTTP_SERVER_PORT} --network=host $WITH_POLICY_JSON -t ${target} -f Decomposed.in $BUDFILES/preprocess } @test "bud with preprocessor error" { _prefetch busybox target=alpine-image starthttpd $BUDFILES/preprocess run_buildah bud --build-arg HTTP_SERVER_PORT=${HTTP_SERVER_PORT} --network=host $WITH_POLICY_JSON -t ${target} -f Error.in $BUDFILES/preprocess expect_output --substring "Ignoring :5:2: error: #error" } @test "bud-with-rejected-name" { target=ThisNameShouldBeRejected run_buildah 125 build -q $WITH_POLICY_JSON -t ${target} $BUDFILES/from-scratch expect_output --substring "must be lower" } @test "bud with chown copy" { _prefetch alpine imgName=alpine-image ctrName=alpine-chown run_buildah build $WITH_POLICY_JSON -t ${imgName} $BUDFILES/copy-chown expect_output --substring "user:2367 group:3267" run_buildah from --name ${ctrName} ${imgName} run_buildah run alpine-chown -- stat -c '%u' /tmp/copychown.txt # Validate that output starts with "2367" expect_output --substring "2367" run_buildah run alpine-chown -- stat -c '%g' /tmp/copychown.txt # Validate that output starts with "3267" expect_output --substring "3267" } @test "bud with combined chown and chmod copy" { _prefetch alpine imgName=alpine-image ctrName=alpine-chmod run_buildah build $WITH_POLICY_JSON -t ${imgName} -f $BUDFILES/copy-chmod/Dockerfile.combined $BUDFILES/copy-chmod expect_output --substring "chmod:777 user:2367 group:3267" } @test "bud with combined chown and chmod add" { _prefetch alpine imgName=alpine-image ctrName=alpine-chmod run_buildah build $WITH_POLICY_JSON -t ${imgName} -f $BUDFILES/add-chmod/Dockerfile.combined $BUDFILES/add-chmod expect_output --substring "chmod:777 user:2367 group:3267" } @test "bud with chown copy with bad chown flag in Dockerfile with --layers" { _prefetch alpine imgName=alpine-image ctrName=alpine-chown run_buildah 125 build $WITH_POLICY_JSON --layers -t ${imgName} -f $BUDFILES/copy-chown/Dockerfile.bad $BUDFILES/copy-chown expect_output --substring "COPY only supports the --chmod= --chown= --from= and the --exclude= flags" } @test "bud with chown copy with unknown substitutions in Dockerfile" { _prefetch alpine imgName=alpine-image ctrName=alpine-chown run_buildah 125 build $WITH_POLICY_JSON -t ${imgName} -f $BUDFILES/copy-chown/Dockerfile.bad2 $BUDFILES/copy-chown expect_output --substring "looking up UID/GID for \":\": can't find uid for user" } @test "bud with chmod copy" { _prefetch alpine imgName=alpine-image ctrName=alpine-chmod run_buildah build $WITH_POLICY_JSON -t ${imgName} $BUDFILES/copy-chmod expect_output --substring "rwxrwxrwx" run_buildah from --name ${ctrName} ${imgName} run_buildah run alpine-chmod ls -l /tmp/copychmod.txt # Validate that output starts with 777 == "rwxrwxrwx" expect_output --substring "rwxrwxrwx" } @test "bud with chmod copy with bad chmod flag in Dockerfile with --layers" { _prefetch alpine imgName=alpine-image ctrName=alpine-chmod run_buildah 125 build $WITH_POLICY_JSON --layers -t ${imgName} -f $BUDFILES/copy-chmod/Dockerfile.bad $BUDFILES/copy-chmod expect_output --substring "COPY only supports the --chmod= --chown= --from= and the --exclude= flags" } @test "bud with chmod add" { _prefetch alpine imgName=alpine-image ctrName=alpine-chmod run_buildah build $WITH_POLICY_JSON -t ${imgName} $BUDFILES/add-chmod expect_output --substring "rwxrwxrwx" run_buildah from --name ${ctrName} ${imgName} run_buildah run alpine-chmod ls -l /tmp/addchmod.txt # Validate that rights equal 777 == "rwxrwxrwx" expect_output --substring "rwxrwxrwx" } @test "bud with chown add" { _prefetch alpine imgName=alpine-image ctrName=alpine-chown run_buildah build $WITH_POLICY_JSON -t ${imgName} $BUDFILES/add-chown expect_output --substring "user:2367 group:3267" run_buildah from --name ${ctrName} ${imgName} run_buildah run alpine-chown -- stat -c '%u' /tmp/addchown.txt # Validate that output starts with "2367" expect_output --substring "2367" run_buildah run alpine-chown -- stat -c '%g' /tmp/addchown.txt # Validate that output starts with "3267" expect_output --substring "3267" } @test "bud with chown add with bad chown flag in Dockerfile with --layers" { _prefetch alpine imgName=alpine-image ctrName=alpine-chown run_buildah 125 build $WITH_POLICY_JSON --layers -t ${imgName} -f $BUDFILES/add-chown/Dockerfile.bad $BUDFILES/add-chown expect_output --substring "ADD only supports the --chmod=, --chown=, and --checksum= --exclude= flags" } @test "bud with chmod add with bad chmod flag in Dockerfile with --layers" { _prefetch alpine imgName=alpine-image ctrName=alpine-chmod run_buildah 125 build $WITH_POLICY_JSON --layers -t ${imgName} -f $BUDFILES/add-chmod/Dockerfile.bad $BUDFILES/add-chmod expect_output --substring "ADD only supports the --chmod=, --chown=, and --checksum= --exclude= flags" } @test "bud with ADD with checksum flag" { _prefetch alpine target=alpine-image run_buildah build $WITH_POLICY_JSON -t alpine-image -f $BUDFILES/add-checksum/Containerfile $BUDFILES/add-checksum run_buildah from --quiet $WITH_POLICY_JSON --name alpine-ctr alpine-image run_buildah run alpine-ctr -- ls -l /README.md expect_output --substring "README.md" } @test "bud with ADD with bad checksum" { _prefetch alpine target=alpine-image run_buildah 125 build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/add-checksum/Containerfile.bad-checksum $BUDFILES/add-checksum expect_output --substring "unexpected response digest for \"https://raw.githubusercontent.com/containers/buildah/bf3b55ba74102cc2503eccbaeffe011728d46b20/README.md\": sha256:4fd3aed66b5488b45fe83dd11842c2324fadcc38e1217bb45fbd28d660afdd39, want sha256:0000000000000000000000000000000000000000000000000000000000000000" } @test "bud with ADD with bad checksum flag" { _prefetch alpine target=alpine-image run_buildah 125 build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/add-checksum/Containerfile.bad $BUDFILES/add-checksum expect_output --substring "ADD only supports the --chmod=, --chown=, and --checksum= --exclude= flags" } @test "bud with ADD file construct" { _prefetch busybox run_buildah build $WITH_POLICY_JSON -t test1 $BUDFILES/add-file run_buildah images -a expect_output --substring "test1" run_buildah from --quiet $WITH_POLICY_JSON test1 ctr=$output run_buildah containers -a expect_output --substring "test1" run_buildah run $ctr ls /var/file2 expect_output --substring "/var/file2" } @test "bud with COPY of single file creates absolute path with correct permissions" { _prefetch ubuntu imgName=ubuntu-image ctrName=ubuntu-copy run_buildah build $WITH_POLICY_JSON -t ${imgName} $BUDFILES/copy-create-absolute-path expect_output --substring "permissions=755" run_buildah from --name ${ctrName} ${imgName} run_buildah run ${ctrName} -- stat -c "%a" /usr/lib/python3.7/distutils expect_output "755" } @test "bud with COPY of single file creates relative path with correct permissions" { _prefetch ubuntu imgName=ubuntu-image ctrName=ubuntu-copy run_buildah build $WITH_POLICY_JSON -t ${imgName} $BUDFILES/copy-create-relative-path expect_output --substring "permissions=755" run_buildah from --name ${ctrName} ${imgName} run_buildah run ${ctrName} -- stat -c "%a" lib/custom expect_output "755" } @test "bud with ADD of single file creates absolute path with correct permissions" { _prefetch ubuntu imgName=ubuntu-image ctrName=ubuntu-copy run_buildah build $WITH_POLICY_JSON -t ${imgName} $BUDFILES/add-create-absolute-path expect_output --substring "permissions=755" run_buildah from --name ${ctrName} ${imgName} run_buildah run ${ctrName} -- stat -c "%a" /usr/lib/python3.7/distutils expect_output "755" } @test "bud with ADD of single file creates relative path with correct permissions" { _prefetch ubuntu imgName=ubuntu-image ctrName=ubuntu-copy run_buildah build $WITH_POLICY_JSON -t ${imgName} $BUDFILES/add-create-relative-path expect_output --substring "permissions=755" run_buildah from --name ${ctrName} ${imgName} run_buildah run ${ctrName} -- stat -c "%a" lib/custom expect_output "755" } @test "bud multi-stage COPY creates absolute path with correct permissions" { _prefetch ubuntu imgName=ubuntu-image ctrName=ubuntu-copy run_buildah build $WITH_POLICY_JSON -f $BUDFILES/copy-multistage-paths/Dockerfile.absolute -t ${imgName} $BUDFILES/copy-multistage-paths expect_output --substring "permissions=755" run_buildah from --name ${ctrName} ${imgName} run_buildah run ${ctrName} -- stat -c "%a" /my/bin expect_output "755" } @test "bud multi-stage COPY creates relative path with correct permissions" { _prefetch ubuntu imgName=ubuntu-image ctrName=ubuntu-copy run_buildah build $WITH_POLICY_JSON -f $BUDFILES/copy-multistage-paths/Dockerfile.relative -t ${imgName} $BUDFILES/copy-multistage-paths expect_output --substring "permissions=755" run_buildah from --name ${ctrName} ${imgName} run_buildah run ${ctrName} -- stat -c "%a" my/bin expect_output "755" } @test "bud multi-stage COPY with invalid from statement" { _prefetch ubuntu imgName=ubuntu-image ctrName=ubuntu-copy run_buildah 125 build $WITH_POLICY_JSON -f $BUDFILES/copy-multistage-paths/Dockerfile.invalid_from -t ${imgName} $BUDFILES/copy-multistage-paths expect_output --substring "COPY only supports the --chmod= --chown= --from= and the --exclude= flags" } @test "bud COPY to root succeeds" { _prefetch ubuntu run_buildah build $WITH_POLICY_JSON $BUDFILES/copy-root } @test "bud with FROM AS construct" { _prefetch alpine run_buildah build $WITH_POLICY_JSON -t test1 $BUDFILES/from-as run_buildah images -a expect_output --substring "test1" run_buildah from --quiet $WITH_POLICY_JSON test1 ctr=$output run_buildah containers -a expect_output --substring "test1" run_buildah inspect --format "{{.Docker.ContainerConfig.Env}}" --type image test1 expect_output --substring "LOCAL=/1" } @test "bud with FROM AS construct with layers" { _prefetch alpine run_buildah build --layers $WITH_POLICY_JSON -t test1 $BUDFILES/from-as run_buildah images -a expect_output --substring "test1" run_buildah from --quiet $WITH_POLICY_JSON test1 ctr=$output run_buildah containers -a expect_output --substring "test1" run_buildah inspect --format "{{.Docker.ContainerConfig.Env}}" --type image test1 expect_output --substring "LOCAL=/1" } @test "bud with FROM AS skip FROM construct" { _prefetch alpine run_buildah build $WITH_POLICY_JSON -t test1 -f $BUDFILES/from-as/Dockerfile.skip $BUDFILES/from-as expect_output --substring "LOCAL=/1" expect_output --substring "LOCAL2=/2" run_buildah images -a expect_output --substring "test1" run_buildah from --quiet $WITH_POLICY_JSON test1 ctr=$output run_buildah containers -a expect_output --substring "test1" run_buildah mount $ctr mnt=$output test -e $mnt/1 test ! -e $mnt/2 run_buildah inspect --format "{{.Docker.ContainerConfig.Env}}" --type image test1 expect_output "[PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin LOCAL=/1]" } @test "build with -f pointing to not a file should fail" { _prefetch alpine target=alpine-image run_buildah 125 build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/dockerfile/ expect_output --substring "cannot be path to a directory" } @test "bud with symlink Dockerfile not specified in file" { _prefetch alpine target=alpine-image run_buildah build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/symlink/Dockerfile $BUDFILES/symlink expect_output --substring "FROM alpine" } @test "bud with ARG before FROM default value" { _prefetch busybox target=leading-args-default run_buildah build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/leading-args/Dockerfile $BUDFILES/leading-args } @test "bud with ARG before FROM" { _prefetch busybox:musl target=leading-args run_buildah build $WITH_POLICY_JSON -t ${target} --build-arg=VERSION=musl -f $BUDFILES/leading-args/Dockerfile $BUDFILES/leading-args # Verify https://github.com/containers/buildah/issues/4312 # stage `FROM stage_${my_env}` must be resolved with default arg value and build should be successful. run_buildah build $WITH_POLICY_JSON -t source -f $BUDFILES/multi-stage-builds/Dockerfile.arg_in_stage # Verify https://github.com/containers/buildah/issues/4573 # stage `COPY --from=stage_${my_env}` must be resolved with default arg value and build should be successful. run_buildah build $WITH_POLICY_JSON -t source -f $BUDFILES/multi-stage-builds/Dockerfile.arg_in_copy } @test "bud COPY --from= with ARG in stage scope" { _prefetch busybox imgname=copy-from-stage-scoped-arg-$(safename) ctrname=copy-from-stage-scoped-arg-ctr-$(safename) run_buildah build $WITH_POLICY_JSON -t ${imgname} -f $BUDFILES/copy-from-stage-scoped-arg/Containerfile $BUDFILES/copy-from-stage-scoped-arg run_buildah from --name ${ctrname} ${imgname} run_buildah run ${ctrname} cat /variant assert "$output" =~ "build:staging" imgname_multi=copy-from-stage-scoped-arg-multi-$(safename) ctrname_multi=copy-from-stage-scoped-arg-multi-ctr-$(safename) run_buildah build $WITH_POLICY_JSON -t ${imgname_multi} -f $BUDFILES/copy-from-stage-scoped-arg/Containerfile.multi-copy-arg-override $BUDFILES/copy-from-stage-scoped-arg run_buildah from --name ${ctrname_multi} ${imgname_multi} run_buildah run ${ctrname_multi} cat /variant_first assert "$output" =~ "build:staging" run_buildah run ${ctrname_multi} cat /variant_second assert "$output" =~ "build:dev" } @test "bud ARG scope: table of scope combinations" { _prefetch alpine busybox local ctx=$BUDFILES/arg-scope # containerfile_suffix | build_arg (or empty) | path | expected_substring local cases=( "from-arg::/base.txt:alpine" "from-arg:BASE=busybox:/base.txt:busybox" "stage-overrides-header::/foo.txt:stage" "copy-from-arg::/from.txt:a" "copy-from-arg:SRC=b:/from.txt:b" "stage-overrides-header-copy-from::/from.txt:b" "multiple-args-stage-overrides-one::/out.txt:9 1" "multiple-args-stage-only::/out.txt:1 2" "multiple-args-one-step::/out.txt:0 1" "multiple-args-one-step:bar=88:/out.txt:0 88" "multiple-args-one-step:foo=99 bar=88:/out.txt:99 88" "arg-mixed-defaults::/out.txt:set" "arg-mixed-defaults:a=given:/out.txt:given set" ) for c in "${cases[@]}"; do IFS=: read -r cf_suffix build_arg path expected <<< "$c" build_arg_safe=$(echo "${build_arg//=/-}" | tr '[:upper:]' '[:lower:]' | tr ' ' '-') imgname=arg-scope-$(safename)-${cf_suffix}-${build_arg_safe:-none} build_args="" if [[ -n "$build_arg" ]]; then for arg in $build_arg; do build_args="$build_args --build-arg $arg"; done fi run_buildah build $WITH_POLICY_JSON $build_args -t ${imgname} -f ${ctx}/Containerfile.${cf_suffix} ${ctx} run_buildah from $WITH_POLICY_JSON ${imgname} ctr=$(echo "$output" | tail -1) run_buildah mount ${ctr} mnt=$output run cat ${mnt}${path} expect_output --substring "$expected" "case: \"$c\"" run_buildah unmount ${ctr} done } @test "bud-with-healthcheck" { _prefetch alpine target=alpine-image run_buildah build $WITH_POLICY_JSON -t ${target} --format docker $BUDFILES/healthcheck run_buildah inspect -f '{{printf "%q" .Docker.Config.Healthcheck.Test}} {{printf "%d" .Docker.Config.Healthcheck.StartInterval}} {{printf "%d" .Docker.Config.Healthcheck.StartPeriod}} {{printf "%d" .Docker.Config.Healthcheck.Interval}} {{printf "%d" .Docker.Config.Healthcheck.Timeout}} {{printf "%d" .Docker.Config.Healthcheck.Retries}}' ${target} second=1000000000 threeseconds=$(( 3 * $second )) thirtyseconds=$(( 30 * $second )) fiveminutes=$(( 5 * 60 * $second )) tenminutes=$(( 10 * 60 * $second )) expect_output '["CMD-SHELL" "curl -f http://localhost/ || exit 1"]'" $thirtyseconds $tenminutes $fiveminutes $threeseconds 4" "Healthcheck config" } @test "bud with unused build arg" { _prefetch alpine busybox target=busybox-image run_buildah build $WITH_POLICY_JSON -t ${target} --build-arg foo=bar --build-arg foo2=bar2 -f $BUDFILES/build-arg/Dockerfile $BUDFILES/build-arg expect_output --substring "one or more build args were not consumed: \[foo2\]" run_buildah build $WITH_POLICY_JSON -t ${target} --build-arg IMAGE=alpine -f $BUDFILES/build-arg/Dockerfile2 $BUDFILES/build-arg assert "$output" !~ "one or more build args were not consumed: \[IMAGE\]" expect_output --substring "FROM alpine" } @test "bud with copy-from and cache" { _prefetch busybox run_buildah build $WITH_POLICY_JSON --layers --iidfile ${TEST_SCRATCH_DIR}/iid1 -f $BUDFILES/copy-from/Dockerfile2 $BUDFILES/copy-from cat ${TEST_SCRATCH_DIR}/iid1 test -s ${TEST_SCRATCH_DIR}/iid1 run_buildah build $WITH_POLICY_JSON --layers --iidfile ${TEST_SCRATCH_DIR}/iid2 -f $BUDFILES/copy-from/Dockerfile2 $BUDFILES/copy-from cat ${TEST_SCRATCH_DIR}/iid2 test -s ${TEST_SCRATCH_DIR}/iid2 cmp ${TEST_SCRATCH_DIR}/iid1 ${TEST_SCRATCH_DIR}/iid2 } @test "bud with copy-from in Dockerfile no prior FROM" { want_tag=20221018 _prefetch busybox quay.io/libpod/testimage:$want_tag target=no-prior-from run_buildah build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/copy-from/Dockerfile $BUDFILES/copy-from run_buildah from --quiet $WITH_POLICY_JSON ${target} ctr=$output run_buildah mount ${ctr} mnt=$output newfile="/home/busyboxpodman/copied-testimage-id" test -e $mnt/$newfile expect_output --from="$(< $mnt/$newfile)" "$want_tag" "Contents of $newfile" } @test "bud with copy-from with bad from flag in Dockerfile with --layers" { _prefetch busybox target=bad-from-flag run_buildah 125 build $WITH_POLICY_JSON --layers -t ${target} -f $BUDFILES/copy-from/Dockerfile.bad $BUDFILES/copy-from expect_output --substring "COPY only supports the --chmod= --chown= --from= and the --exclude= flags" } @test "bud with copy-from referencing the base image" { _prefetch busybox target=busybox-derived target_mt=busybox-mt-derived run_buildah build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/copy-from/Dockerfile3 $BUDFILES/copy-from run_buildah build $WITH_POLICY_JSON --jobs 4 -t ${target} -f $BUDFILES/copy-from/Dockerfile3 $BUDFILES/copy-from run_buildah build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/copy-from/Dockerfile4 $BUDFILES/copy-from run_buildah build --no-cache $WITH_POLICY_JSON --jobs 4 -t ${target_mt} -f $BUDFILES/copy-from/Dockerfile4 $BUDFILES/copy-from run_buildah from --quiet ${target} cid=$output run_buildah mount ${cid} root_single_job=$output run_buildah from --quiet ${target_mt} cid=$output run_buildah mount ${cid} root_multi_job=$output # Check that both the version with --jobs 1 and --jobs=N have the same number of files test $(find $root_single_job -type f | wc -l) = $(find $root_multi_job -type f | wc -l) } @test "bud with copy-from referencing the current stage" { _prefetch busybox target=busybox-derived run_buildah 125 build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/copy-from/Dockerfile2.bad $BUDFILES/copy-from expect_output --substring "COPY --from=build: no stage or image found with that name" } @test "bud-target" { _prefetch alpine ubuntu target=target run_buildah build $WITH_POLICY_JSON -t ${target} --target mytarget $BUDFILES/target expect_output --substring "\[1/2] STEP 1/3: FROM ubuntu:latest" expect_output --substring "\[2/2] STEP 1/3: FROM alpine:latest AS mytarget" run_buildah from --quiet ${target} cid=$output run_buildah mount ${cid} root=$output test -e ${root}/2 test ! -e ${root}/3 } @test "bud-no-target-name" { _prefetch alpine run_buildah build $WITH_POLICY_JSON $BUDFILES/maintainer } @test "bud-multi-stage-nocache-nocommit" { _prefetch alpine # okay, build an image with two stages run_buildah --log-level=debug bud $WITH_POLICY_JSON -f $BUDFILES/multi-stage-builds/Dockerfile.name $BUDFILES/multi-stage-builds # debug messages should only record us creating one new image: the one for the second stage, since we don't base anything on the first run grep "created new image ID" <<< "$output" expect_line_count 1 } @test "bud-multi-stage-cache-nocontainer" { skip "FIXME: Broken in CI right now" _prefetch alpine # first time through, quite normal run_buildah build --layers -t base $WITH_POLICY_JSON -f $BUDFILES/multi-stage-builds/Dockerfile.rebase $BUDFILES/multi-stage-builds # second time through, everything should be cached, and we shouldn't create a container based on the final image run_buildah --log-level=debug bud --layers -t base $WITH_POLICY_JSON -f $BUDFILES/multi-stage-builds/Dockerfile.rebase $BUDFILES/multi-stage-builds # skip everything up through the final COMMIT step, and make sure we didn't log a "Container ID:" after it run sed '0,/COMMIT base/ d' <<< "$output" echo "$output" >&2 test "${#lines[@]}" -gt 1 run grep "Container ID:" <<< "$output" expect_output "" } @test "bud copy to symlink" { _prefetch alpine target=alpine-image ctr=alpine-ctr run_buildah build $WITH_POLICY_JSON -t ${target} $BUDFILES/dest-symlink expect_output --substring "STEP 5/6: RUN ln -s " run_buildah from $WITH_POLICY_JSON --name=${ctr} ${target} expect_output --substring ${ctr} run_buildah run ${ctr} ls -alF /etc/hbase expect_output --substring "/etc/hbase -> /usr/local/hbase/" run_buildah run ${ctr} ls -alF /usr/local/hbase expect_output --substring "Dockerfile" } @test "bud copy to dangling symlink" { _prefetch ubuntu target=ubuntu-image ctr=ubuntu-ctr run_buildah build $WITH_POLICY_JSON -t ${target} $BUDFILES/dest-symlink-dangling expect_output --substring "STEP 3/5: RUN ln -s " run_buildah from $WITH_POLICY_JSON --name=${ctr} ${target} expect_output --substring ${ctr} run_buildah run ${ctr} ls -alF /src expect_output --substring "/src -> /symlink" run_buildah run ${ctr} ls -alF /symlink expect_output --substring "Dockerfile" } @test "bud WORKDIR isa symlink" { _prefetch alpine target=alpine-image ctr=alpine-ctr run_buildah build $WITH_POLICY_JSON -t ${target} $BUDFILES/workdir-symlink expect_output --substring "STEP 3/6: RUN ln -sf " run_buildah from $WITH_POLICY_JSON --name=${ctr} ${target} expect_output --substring ${ctr} run_buildah run ${ctr} ls -alF /tempest expect_output --substring "/tempest -> /var/lib/tempest/" run_buildah run ${ctr} ls -alF /etc/notareal.conf expect_output --substring "\-rw\-rw\-r\-\-" } @test "bud WORKDIR isa symlink no target dir" { _prefetch alpine target=alpine-image ctr=alpine-ctr run_buildah build $WITH_POLICY_JSON -t ${target} -f Dockerfile-2 $BUDFILES/workdir-symlink expect_output --substring "STEP 2/6: RUN ln -sf " run_buildah from $WITH_POLICY_JSON --name=${ctr} ${target} expect_output --substring ${ctr} run_buildah run ${ctr} ls -alF /tempest expect_output --substring "/tempest -> /var/lib/tempest/" run_buildah run ${ctr} ls /tempest expect_output --substring "Dockerfile-2" run_buildah run ${ctr} ls -alF /etc/notareal.conf expect_output --substring "\-rw\-rw\-r\-\-" } @test "bud WORKDIR isa symlink no target dir and follow on dir" { _prefetch alpine target=alpine-image ctr=alpine-ctr run_buildah build $WITH_POLICY_JSON -t ${target} -f Dockerfile-3 $BUDFILES/workdir-symlink expect_output --substring "STEP 2/9: RUN ln -sf " run_buildah from $WITH_POLICY_JSON --name=${ctr} ${target} expect_output --substring ${ctr} run_buildah run ${ctr} ls -alF /tempest expect_output --substring "/tempest -> /var/lib/tempest/" run_buildah run ${ctr} ls /tempest expect_output --substring "Dockerfile-3" run_buildah run ${ctr} ls /tempest/lowerdir expect_output --substring "Dockerfile-3" run_buildah run ${ctr} ls -alF /etc/notareal.conf expect_output --substring "\-rw\-rw\-r\-\-" } @test "buildah bud --volume" { voldir=${TEST_SCRATCH_DIR}/bud-volume mkdir -p ${voldir} _prefetch alpine run_buildah build $WITH_POLICY_JSON -v ${voldir}:/testdir $BUDFILES/mount expect_output --substring "/testdir" run_buildah build $WITH_POLICY_JSON -v ${voldir}:/testdir:rw $BUDFILES/mount expect_output --substring "/testdir" run_buildah build $WITH_POLICY_JSON -v ${voldir}:/testdir:rw,z $BUDFILES/mount expect_output --substring "/testdir" } @test "bud-copy-dot with --layers picks up changed file" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/use-layers cp -a $BUDFILES/use-layers $contextdir mkdir -p $contextdir/subdir touch $contextdir/file.txt run_buildah build $WITH_POLICY_JSON --layers --iidfile ${TEST_SCRATCH_DIR}/iid1 -f Dockerfile.7 $contextdir touch $contextdir/file.txt run_buildah build $WITH_POLICY_JSON --layers --iidfile ${TEST_SCRATCH_DIR}/iid2 -f Dockerfile.7 $contextdir if [[ $(< ${TEST_SCRATCH_DIR}/iid1) != $(< ${TEST_SCRATCH_DIR}/iid2) ]]; then echo "Expected image id to not change after touching a file copied into the image" >&2 false fi } @test "buildah-bud-policy" { target=foo # A deny-all policy should prevent us from pulling the base image. run_buildah 125 build --signature-policy ${TEST_SOURCES}/deny.json -t ${target} -v ${TEST_SOURCES}:/testdir $BUDFILES/mount expect_output --substring 'Source image rejected: Running image .* rejected by policy.' # A docker-only policy should allow us to pull the base image and commit. run_buildah build --signature-policy ${TEST_SOURCES}/docker.json -t ${target} -v ${TEST_SOURCES}:/testdir $BUDFILES/mount # A deny-all policy shouldn't break pushing, since policy is only evaluated # on the source image, and we force it to allow local storage. run_buildah push --signature-policy ${TEST_SOURCES}/deny.json ${target} dir:${TEST_SCRATCH_DIR}/mount run_buildah rmi ${target} # A docker-only policy should allow us to pull the base image first... run_buildah pull --signature-policy ${TEST_SOURCES}/docker.json alpine # ... and since we don't need to pull the base image, a deny-all policy shouldn't break a build. run_buildah build --signature-policy ${TEST_SOURCES}/deny.json -t ${target} -v ${TEST_SOURCES}:/testdir $BUDFILES/mount # A deny-all policy shouldn't break pushing, since policy is only evaluated # on the source image, and we force it to allow local storage. run_buildah push --signature-policy ${TEST_SOURCES}/deny.json ${target} dir:${TEST_SCRATCH_DIR}/mount # Similarly, a deny-all policy shouldn't break committing directly to other locations. run_buildah build --signature-policy ${TEST_SOURCES}/deny.json -t dir:${TEST_SCRATCH_DIR}/mount -v ${TEST_SOURCES}:/testdir $BUDFILES/mount } @test "bud-copy-replace-symlink" { local contextdir=${TEST_SCRATCH_DIR}/top mkdir -p $contextdir cp $BUDFILES/symlink/Dockerfile.replace-symlink $contextdir/ ln -s Dockerfile.replace-symlink $contextdir/symlink echo foo > $contextdir/.dockerignore run_buildah build $WITH_POLICY_JSON -f $contextdir/Dockerfile.replace-symlink $contextdir } @test "bud-copy-recurse" { local contextdir=${TEST_SCRATCH_DIR}/recurse mkdir -p $contextdir cp $BUDFILES/recurse/Dockerfile $contextdir echo foo > $contextdir/.dockerignore run_buildah build $WITH_POLICY_JSON $contextdir } @test "bud copy with .dockerignore #1" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p $mytmpdir/stuff/huge/usr/bin/ touch $mytmpdir/stuff/huge/usr/bin/{file1,file2} touch $mytmpdir/stuff/huge/usr/file3 cat > $mytmpdir/.dockerignore << _EOF stuff/huge/* !stuff/huge/usr/bin/* _EOF cat > $mytmpdir/Containerfile << _EOF FROM alpine COPY stuff /tmp/stuff RUN find /tmp/stuff -type f _EOF run_buildah build -t testbud $WITH_POLICY_JSON ${mytmpdir} expect_output --substring "file1" expect_output --substring "file2" assert "$output" !~ "file3" } @test "bud copy with .dockerignore #2" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir1 mkdir -p $mytmpdir/stuff/huge/usr/bin/ touch $mytmpdir/stuff/huge/usr/bin/{file1,file2} cat > $mytmpdir/.dockerignore << _EOF stuff/huge/* _EOF cat > $mytmpdir/Containerfile << _EOF FROM alpine COPY stuff /tmp/stuff RUN find /tmp/stuff -type f _EOF run_buildah build -t testbud $WITH_POLICY_JSON ${mytmpdir} assert "$output" !~ file1 assert "$output" !~ file2 } @test "bud-copy-workdir" { target=testimage run_buildah build $WITH_POLICY_JSON -t ${target} $BUDFILES/copy-workdir run_buildah from ${target} cid="$output" run_buildah mount "${cid}" root="$output" test -s "${root}"/file1.txt test -d "${root}"/subdir test -s "${root}"/subdir/file2.txt } # regression test for https://github.com/containers/podman/issues/10671 @test "bud-copy-workdir --layers" { _prefetch alpine target=testimage run_buildah build $WITH_POLICY_JSON --layers -t ${target} -f Dockerfile.2 $BUDFILES/copy-workdir run_buildah from ${target} cid="$output" run_buildah mount "${cid}" root="$output" test -d "${root}"/subdir test -s "${root}"/subdir/file1.txt } @test "bud-build-arg-cache" { _prefetch busybox alpine target=derived-image run_buildah build $WITH_POLICY_JSON --layers -t ${target} -f Dockerfile3 $BUDFILES/build-arg run_buildah inspect -f '{{.FromImageID}}' ${target} targetid="$output" # With build args, we should not find the previous build as a cached result. This will be true because there is a RUN command after all the ARG # commands in the containerfile, so this does not truly test if the ARG commands were using cache or not. There is a test for that case below. run_buildah build $WITH_POLICY_JSON --layers -t ${target} -f Dockerfile3 --build-arg=UID=17122 --build-arg=CODE=/copr/coprs_frontend --build-arg=USERNAME=praiskup --build-arg=PGDATA=/pgdata $BUDFILES/build-arg run_buildah inspect -f '{{.FromImageID}}' ${target} argsid="$output" assert "$argsid" != "$initialid" \ ".FromImageID of test-img-2 ($argsid) == same as test-img, it should be different" # With build args, even in a different order, we should end up using the previous build as a cached result. run_buildah build $WITH_POLICY_JSON --layers -t ${target} -f Dockerfile3 --build-arg=UID=17122 --build-arg=CODE=/copr/coprs_frontend --build-arg=USERNAME=praiskup --build-arg=PGDATA=/pgdata $BUDFILES/build-arg run_buildah inspect -f '{{.FromImageID}}' ${target} expect_output "$argsid" "FromImageID of build 3" run_buildah build $WITH_POLICY_JSON --layers -t ${target} -f Dockerfile3 --build-arg=CODE=/copr/coprs_frontend --build-arg=USERNAME=praiskup --build-arg=PGDATA=/pgdata --build-arg=UID=17122 $BUDFILES/build-arg run_buildah inspect -f '{{.FromImageID}}' ${target} expect_output "$argsid" "FromImageID of build 4" run_buildah build $WITH_POLICY_JSON --layers -t ${target} -f Dockerfile3 --build-arg=USERNAME=praiskup --build-arg=PGDATA=/pgdata --build-arg=UID=17122 --build-arg=CODE=/copr/coprs_frontend $BUDFILES/build-arg run_buildah inspect -f '{{.FromImageID}}' ${target} expect_output "$argsid" "FromImageID of build 5" run_buildah build $WITH_POLICY_JSON --layers -t ${target} -f Dockerfile3 --build-arg=PGDATA=/pgdata --build-arg=UID=17122 --build-arg=CODE=/copr/coprs_frontend --build-arg=USERNAME=praiskup $BUDFILES/build-arg run_buildah inspect -f '{{.FromImageID}}' ${target} expect_output "$argsid" "FromImageID of build 6" # If build-arg is specified via the command line and is different from the previous cached build, it should not use the cached layers. # Note, this containerfile does not have any RUN commands and we verify that the ARG steps are being rebuilt when a change is detected. run_buildah build $WITH_POLICY_JSON --layers -t test-img -f Dockerfile4 $BUDFILES/build-arg run_buildah inspect -f '{{.FromImageID}}' test-img initialid="$output" # Build the same containerfile again and verify that the cached layers were used run_buildah build $WITH_POLICY_JSON --layers -t test-img-1 -f Dockerfile4 $BUDFILES/build-arg run_buildah inspect -f '{{.FromImageID}}' test-img-1 expect_output "$initialid" "FromImageID of test-img-1 should match test-img" # Set the build-arg flag and verify that the cached layers are not used run_buildah build $WITH_POLICY_JSON --layers -t test-img-2 --build-arg TEST=foo -f Dockerfile4 $BUDFILES/build-arg run_buildah inspect -f '{{.FromImageID}}' test-img-2 argsid="$output" assert "$argsid" != "$initialid" \ ".FromImageID of test-img-2 ($argsid) == same as test-img, it should be different" # Set the build-arg via an ENV in the local environment and verify that the cached layers are not used export TEST=bar run_buildah build $WITH_POLICY_JSON --layers -t test-img-3 --build-arg TEST -f Dockerfile4 $BUDFILES/build-arg run_buildah inspect -f '{{.FromImageID}}' test-img-3 argsid="$output" assert "$argsid" != "$initialid" \ ".FromImageID of test-img-3 ($argsid) == same as test-img, it should be different" } @test "bud test RUN with a privileged command" { _prefetch busybox target=busyboxpriv starthttpd $BUDFILES/run-privd run_buildah build --network=host --build-arg HTTP_SERVER_PORT=${HTTP_SERVER_PORT} $WITH_POLICY_JSON -t ${target} -f $BUDFILES/run-privd/Dockerfile $BUDFILES/run-privd expect_output --substring "[^:][^[:graph:]]COMMIT ${target}" run_buildah images -q expect_line_count 2 } @test "bud-copy-dockerignore-hardlinks" { target=image local contextdir=${TEST_SCRATCH_DIR}/hardlinks mkdir -p $contextdir/subdir cp $BUDFILES/recurse/Dockerfile $contextdir echo foo > $contextdir/.dockerignore echo test1 > $contextdir/subdir/test1.txt ln $contextdir/subdir/test1.txt $contextdir/subdir/test2.txt ln $contextdir/subdir/test2.txt $contextdir/test3.txt ln $contextdir/test3.txt $contextdir/test4.txt run_buildah build $WITH_POLICY_JSON -t ${target} $contextdir run_buildah from ${target} ctrid="$output" run_buildah mount "$ctrid" root="$output" run stat -c "%d:%i" ${root}/subdir/test1.txt id1=$output run stat -c "%h" ${root}/subdir/test1.txt expect_output 4 "test1: number of hardlinks" run stat -c "%d:%i" ${root}/subdir/test2.txt expect_output $id1 "stat(test2) == stat(test1)" run stat -c "%h" ${root}/subdir/test2.txt expect_output 4 "test2: number of hardlinks" run stat -c "%d:%i" ${root}/test3.txt expect_output $id1 "stat(test3) == stat(test1)" run stat -c "%h" ${root}/test3.txt expect_output 4 "test3: number of hardlinks" run stat -c "%d:%i" ${root}/test4.txt expect_output $id1 "stat(test4) == stat(test1)" run stat -c "%h" ${root}/test4.txt expect_output 4 "test4: number of hardlinks" } @test "bud without any arguments should succeed" { cd $BUDFILES/from-scratch run_buildah build --signature-policy ${TEST_SOURCES}/policy.json } @test "bud without any arguments should fail when no Dockerfile exists" { cd $TEST_SCRATCH_DIR run_buildah 125 build --signature-policy ${TEST_SOURCES}/policy.json expect_output --substring "no such file or directory" } @test "bud with specified context should fail if directory contains no Dockerfile" { mkdir -p $TEST_SCRATCH_DIR/empty-dir run_buildah 125 build $WITH_POLICY_JSON "$TEST_SCRATCH_DIR"/empty-dir expect_output --substring "no such file or directory" } @test "bud with specified context should fail if Dockerfile in context directory is actually a file" { mkdir -p "$TEST_SCRATCH_DIR"/Dockerfile run_buildah 125 build $WITH_POLICY_JSON "$TEST_SCRATCH_DIR" expect_output --substring "is not a file" } @test "bud with specified context should fail if context directory does not exist" { run_buildah 125 build $WITH_POLICY_JSON "$TEST_SCRATCH_DIR"/Dockerfile expect_output --substring "no such file or directory" } @test "bud with specified context should succeed if context contains existing Dockerfile" { _prefetch alpine echo "FROM alpine" > $TEST_SCRATCH_DIR/Dockerfile run_buildah bud $WITH_POLICY_JSON $TEST_SCRATCH_DIR/Dockerfile } @test "bud with specified context should fail if context contains empty Dockerfile" { touch $TEST_SCRATCH_DIR/Dockerfile run_buildah 125 build $WITH_POLICY_JSON $TEST_SCRATCH_DIR/Dockerfile expect_output --substring "no contents in \"$TEST_SCRATCH_DIR/Dockerfile\"" } @test "bud-no-change" { _prefetch alpine parent=alpine target=no-change-image run_buildah build $WITH_POLICY_JSON -t ${target} $BUDFILES/no-change run_buildah inspect --format '{{printf "%q" .FromImageDigest}}' ${parent} parentid="$output" run_buildah inspect --format '{{printf "%q" .FromImageDigest}}' ${target} expect_output "$parentid" } @test "bud-no-change-label" { run_buildah --version local -a output_fields=($output) buildah_version=${output_fields[2]} want_output='map["io.buildah.version":"'$buildah_version'" "test":"label"]' _prefetch alpine parent=alpine target=no-change-image run_buildah build --label "test=label" $WITH_POLICY_JSON -t ${target} $BUDFILES/no-change run_buildah inspect --format '{{printf "%q" .Docker.Config.Labels}}' ${target} expect_output "$want_output" } @test "bud-no-change-annotation" { _prefetch alpine target=no-change-image run_buildah build --annotation "test=annotation" $WITH_POLICY_JSON -t ${target} $BUDFILES/no-change run_buildah inspect --format '{{index .ImageAnnotations "test"}}' ${target} expect_output "annotation" } @test "bud-squash-layers" { _prefetch alpine run_buildah build $WITH_POLICY_JSON --squash $BUDFILES/layers-squash } @test "bud-squash-hardlinks" { _prefetch busybox run_buildah build $WITH_POLICY_JSON --squash $BUDFILES/layers-squash/Dockerfile.hardlinks } # Following test must pass for both rootless and rootfull @test "rootless: support --device and renaming device using bind-mount" { _prefetch alpine skip_if_in_container # unable to perform mount of /dev/null for test in CI container setup local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/Dockerfile << _EOF FROM alpine RUN ls /test/dev _EOF run_buildah build $WITH_POLICY_JSON --device /dev/null:/test/dev/null -t test -f $contextdir/Dockerfile expect_output --substring "null" } @test "bud with additional directory of devices" { skip_if_rootless_environment skip_if_chroot skip_if_rootless _prefetch alpine target=alpine-image local contextdir=${TEST_SCRATCH_DIR}/foo mkdir -p $contextdir mknod $contextdir/null c 1 3 run_buildah build $WITH_POLICY_JSON --device $contextdir:/dev/fuse -t ${target} -f $BUDFILES/device/Dockerfile $BUDFILES/device expect_output --substring "null" } @test "bud with additional device" { _prefetch alpine target=alpine-image run_buildah build $WITH_POLICY_JSON --device /dev/fuse -t ${target} -f $BUDFILES/device/Dockerfile $BUDFILES/device expect_output --substring "/dev/fuse" } @test "bud with Containerfile" { _prefetch alpine target=alpine-image run_buildah build $WITH_POLICY_JSON -t ${target} $BUDFILES/containerfile expect_output --substring "FROM alpine" } @test "bud with Containerfile.in, --cpp-flag" { _prefetch alpine target=alpine-image run_buildah build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/containerfile/Containerfile.in $BUDFILES/containerfile expect_output --substring "Ignoring In file included .* invalid preprocessing directive #This" expect_output --substring "FROM alpine" expect_output --substring "success" expect_output --substring "debug=no" "with no cpp-flag or BUILDAH_CPPFLAGS" run_buildah build $WITH_POLICY_JSON -t ${target} --cpp-flag "-DTESTCPPDEBUG" -f $BUDFILES/containerfile/Containerfile.in $BUDFILES/containerfile expect_output --substring "Ignoring In file included .* invalid preprocessing directive #This" expect_output --substring "FROM alpine" expect_output --substring "success" expect_output --substring "debug=yes" "with --cpp-flag -DTESTCPPDEBUG" } @test "bud with Containerfile.in, via envariable" { _prefetch alpine target=alpine-image BUILDAH_CPPFLAGS="-DTESTCPPDEBUG" run_buildah build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/containerfile/Containerfile.in $BUDFILES/containerfile expect_output --substring "Ignoring In file included .* invalid preprocessing directive #This" expect_output --substring "FROM alpine" expect_output --substring "success" expect_output --substring "debug=yes" "with BUILDAH_CPPFLAGS=-DTESTCPPDEBUG" } @test "bud with Dockerfile" { _prefetch alpine target=alpine-image run_buildah build $WITH_POLICY_JSON -t ${target} $BUDFILES/dockerfile expect_output --substring "FROM alpine" } @test "bud with Containerfile and Dockerfile" { _prefetch alpine target=alpine-image run_buildah build $WITH_POLICY_JSON -t ${target} $BUDFILES/containeranddockerfile expect_output --substring "FROM alpine" } @test "bud-http-context-with-Containerfile" { _test_http http-context-containerfile context.tar } @test "bud with Dockerfile from stdin" { _prefetch alpine target=df-stdin run_buildah build $WITH_POLICY_JSON -t ${target} - < $BUDFILES/context-from-stdin/Dockerfile run_buildah from --quiet ${target} cid=$output run_buildah mount ${cid} root=$output test -s $root/scratchfile run cat $root/scratchfile expect_output "stdin-context" "contents of \$root/scratchfile" # FROM scratch overrides FROM alpine test ! -s $root/etc/alpine-release } @test "bud with Dockerfile from stdin tar" { _prefetch alpine target=df-stdin # 'cmd1 < <(cmd2)' == 'cmd2 | cmd1' but runs cmd1 in this shell, not sub. run_buildah build $WITH_POLICY_JSON -t ${target} - < <(tar -c -C $BUDFILES/context-from-stdin .) run_buildah from --quiet ${target} cid=$output run_buildah mount ${cid} root=$output test -s $root/scratchfile run cat $root/scratchfile expect_output "stdin-context" "contents of \$root/scratchfile" # FROM scratch overrides FROM alpine test ! -s $root/etc/alpine-release } @test "bud containerfile with args" { _prefetch alpine target=use-args touch $BUDFILES/use-args/abc.txt run_buildah build $WITH_POLICY_JSON -t ${target} --build-arg=abc.txt $BUDFILES/use-args expect_output --substring "COMMIT use-args" run_buildah from --quiet ${target} ctrID=$output run_buildah run $ctrID ls abc.txt expect_output --substring "abc.txt" run_buildah build $WITH_POLICY_JSON -t ${target} -f Containerfile.destination --build-arg=testArg=abc.txt --build-arg=destination=/tmp $BUDFILES/use-args expect_output --substring "COMMIT use-args" run_buildah from --quiet ${target} ctrID=$output run_buildah run $ctrID ls /tmp/abc.txt expect_output --substring "abc.txt" run_buildah build $WITH_POLICY_JSON -t ${target} -f Containerfile.dest_nobrace --build-arg=testArg=abc.txt --build-arg=destination=/tmp $BUDFILES/use-args expect_output --substring "COMMIT use-args" run_buildah from --quiet ${target} ctrID=$output run_buildah run $ctrID ls /tmp/abc.txt expect_output --substring "abc.txt" rm $BUDFILES/use-args/abc.txt } @test "bud using gitrepo and branch" { if ! start_git_daemon ${TEST_SOURCES}/git-daemon/release-1.11-rhel.tar.gz ; then skip "error running git daemon" fi run_buildah build $WITH_POLICY_JSON --layers -t gittarget -f $BUDFILES/shell/Dockerfile git://localhost:${GITPORT}/repo#release-1.11-rhel } @test "bud using gitrepo with .git and branch" { run_buildah build $WITH_POLICY_JSON --layers -t gittarget -f $BUDFILES/shell/Dockerfile https://github.com/containers/buildah.git#release-1.11-rhel } # Fixes #1906: buildah was not detecting changed tarfile @test "bud containerfile with tar archive in copy" { _prefetch busybox # First check to verify cache is used if the tar file does not change target=copy-archive date > $BUDFILES/${target}/test tar -C $TEST_SOURCES -cJf $BUDFILES/${target}/test.tar.xz bud/${target}/test run_buildah build $WITH_POLICY_JSON --layers -t ${target} $BUDFILES/${target} expect_output --substring "COMMIT copy-archive" run_buildah build $WITH_POLICY_JSON --layers -t ${target} $BUDFILES/${target} expect_output --substring " Using cache" expect_output --substring "COMMIT copy-archive" # Now test that we do NOT use cache if the tar file changes echo This is a change >> $BUDFILES/${target}/test tar -C $TEST_SOURCES -cJf $BUDFILES/${target}/test.tar.xz bud/${target}/test run_buildah build $WITH_POLICY_JSON --layers -t ${target} $BUDFILES/${target} if [[ "$output" =~ " Using cache" ]]; then expect_output "[no instance of 'Using cache']" "no cache used" fi expect_output --substring "COMMIT copy-archive" rm -f $BUDFILES/${target}/test* } @test "bud pull never" { target=pull run_buildah 125 build $WITH_POLICY_JSON -t ${target} --pull-never $BUDFILES/pull expect_output --substring "busybox: image not known" run_buildah 125 build $WITH_POLICY_JSON -t ${target} --pull=false $BUDFILES/pull expect_output --substring "busybox: image not known" run_buildah build $WITH_POLICY_JSON -t ${target} --pull $BUDFILES/pull expect_output --substring "COMMIT pull" run_buildah build $WITH_POLICY_JSON -t ${target} --pull=never $BUDFILES/pull expect_output --substring "COMMIT pull" } @test "bud pull false no local image" { target=pull run_buildah 125 build $WITH_POLICY_JSON -t ${target} --pull=false $BUDFILES/pull expect_output --substring "Error: creating build container: busybox: image not known" } @test "bud with Containerfile should fail with nonexistent authfile" { target=alpine-image run_buildah 125 build --authfile /tmp/nonexistent $WITH_POLICY_JSON -t ${target} $BUDFILES/containerfile assert "$output" =~ "Error: credential file is not accessible: (faccessat|stat) /tmp/nonexistent: no such file or directory" } @test "bud for multi-stage Containerfile with invalid registry and --authfile as a fd, should fail with no such host" { target=alpine-multi-stage-image run_buildah 125 build --authfile=<(echo "{ \"auths\": { \"myrepository.example\": { \"auth\": \"$(echo 'username:password' | base64 --wrap=0)\" } } }") -t ${target} --file $BUDFILES/from-invalid-registry/Containerfile # Should fail with `no such host` instead of: error reading JSON file "/dev/fd/x" expect_output --substring "no such host" } @test "bud COPY with URL should fail" { local contextdir=${TEST_SCRATCH_DIR}/budcopy mkdir $contextdir FILE=$contextdir/Dockerfile.url /bin/cat <$FILE FROM alpine:latest COPY https://getfedora.org/index.html . EOM run_buildah 125 build $WITH_POLICY_JSON -t foo -f $contextdir/Dockerfile.url expect_output --substring "building .* source can.t be a URL for COPY" } @test "bud quiet" { _prefetch alpine run_buildah build --format docker -t quiet-test $WITH_POLICY_JSON -q $BUDFILES/shell expect_line_count 1 expect_output --substring '^[0-9a-f]{64}$' } @test "bud COPY with Env Var in Containerfile" { _prefetch alpine run_buildah build $WITH_POLICY_JSON -t testctr $BUDFILES/copy-envvar run_buildah from testctr run_buildah run testctr-working-container ls /file-0.0.1.txt run_buildah rm -a run_buildah build $WITH_POLICY_JSON --layers -t testctr $BUDFILES/copy-envvar run_buildah from testctr run_buildah run testctr-working-container ls /file-0.0.1.txt run_buildah rm -a } @test "bud with custom arch" { run_buildah build $WITH_POLICY_JSON \ -f $BUDFILES/from-scratch/Containerfile \ -t arch-test \ --arch=arm run_buildah inspect --format "{{ .Docker.Architecture }}" arch-test expect_output arm run_buildah inspect --format "{{ .OCIv1.Architecture }}" arch-test expect_output arm } @test "bud with custom os" { run_buildah build $WITH_POLICY_JSON \ -f $BUDFILES/from-scratch/Containerfile \ -t os-test \ --os=windows run_buildah inspect --format "{{ .Docker.OS }}" os-test expect_output windows run_buildah inspect --format "{{ .OCIv1.OS }}" os-test expect_output windows } @test "bud with custom os-version" { run_buildah build $WITH_POLICY_JSON \ -f $BUDFILES/from-scratch/Containerfile \ -t os-version-test \ --os-version=1.0 run_buildah inspect --format "{{ .Docker.OSVersion }}" os-version-test expect_output 1.0 run_buildah inspect --format "{{ .OCIv1.OSVersion }}" os-version-test expect_output 1.0 } @test "bud with custom os-features" { run_buildah build $WITH_POLICY_JSON \ -f $BUDFILES/from-scratch/Containerfile \ -t os-features-test \ --os-feature removed --os-feature removed- --os-feature win32k run_buildah inspect --format "{{ .Docker.OSFeatures }}" os-features-test expect_output '[win32k]' run_buildah inspect --format "{{ .OCIv1.OSFeatures }}" os-features-test expect_output '[win32k]' } @test "bud with custom platform" { run_buildah build $WITH_POLICY_JSON \ -f $BUDFILES/from-scratch/Containerfile \ -t platform-test \ --platform=windows/arm run_buildah inspect --format "{{ .Docker.OS }}" platform-test expect_output windows run_buildah inspect --format "{{ .OCIv1.OS }}" platform-test expect_output windows run_buildah inspect --format "{{ .Docker.Architecture }}" platform-test expect_output arm run_buildah inspect --format "{{ .OCIv1.Architecture }}" platform-test expect_output arm } @test "bud with custom platform and empty os or arch" { run_buildah build $WITH_POLICY_JSON \ -f $BUDFILES/from-scratch/Containerfile \ -t platform-test \ --platform=windows/ run_buildah inspect --format "{{ .Docker.OS }}" platform-test expect_output windows run_buildah inspect --format "{{ .OCIv1.OS }}" platform-test expect_output windows run_buildah build $WITH_POLICY_JSON \ -f $BUDFILES/from-scratch/Containerfile \ -t platform-test2 \ --platform=/arm run_buildah inspect --format "{{ .Docker.Architecture }}" platform-test2 expect_output arm run_buildah inspect --format "{{ .OCIv1.Architecture }}" platform-test2 expect_output arm } @test "bud Add with linked tarball" { _prefetch alpine run_buildah build $WITH_POLICY_JSON -f $BUDFILES/symlink/Containerfile.add-tar-with-link -t testctr $BUDFILES/symlink run_buildah from testctr run_buildah run testctr-working-container ls /tmp/testdir/testfile.txt run_buildah rm -a run_buildah rmi -a -f _prefetch alpine run_buildah build $WITH_POLICY_JSON -f $BUDFILES/symlink/Containerfile.add-tar-gz-with-link -t testctr $BUDFILES/symlink run_buildah from testctr run_buildah run testctr-working-container ls /tmp/testdir/testfile.txt run_buildah rm -a run_buildah rmi -a -f } @test "bud file above context directory" { run_buildah 125 build $WITH_POLICY_JSON -t testctr $BUDFILES/context-escape-dir/testdir expect_output --substring "escaping context directory error" } @test "bud-multi-stage-args-scope" { _prefetch alpine run_buildah build $WITH_POLICY_JSON --layers -t multi-stage-args --build-arg SECRET=secretthings -f Dockerfile.arg $BUDFILES/multi-stage-builds run_buildah from --name test-container multi-stage-args run_buildah run test-container -- cat test_file expect_output "" } @test "bud-multi-stage-args-history" { _prefetch alpine run_buildah build $WITH_POLICY_JSON --layers -t multi-stage-args --build-arg SECRET=secretthings -f Dockerfile.arg $BUDFILES/multi-stage-builds run_buildah inspect --format '{{range .History}}{{println .CreatedBy}}{{end}}' multi-stage-args run grep "secretthings" <<< "$output" expect_output "" run_buildah inspect --format '{{range .OCIv1.History}}{{println .CreatedBy}}{{end}}' multi-stage-args run grep "secretthings" <<< "$output" expect_output "" run_buildah inspect --format '{{range .Docker.History}}{{println .CreatedBy}}{{end}}' multi-stage-args run grep "secretthings" <<< "$output" expect_output "" } @test "bud-implicit-no-history" { _prefetch busybox local ocidir=${TEST_SCRATCH_DIR}/oci mkdir -p $ocidir/blobs/sha256 # Build an image config and image manifest in parallel local configos=$(${BUILDAH_BINARY} info --format '{{.host.os}}') local configarch=$(${BUILDAH_BINARY} info --format '{{.host.arch}}') local configvariant=$(${BUILDAH_BINARY} info --format '{{.host.variant}}') local configvariantkv=${configvariant:+'"variant": "'${configvariant}'", '} echo '{"architecture": "'"${configarch}"'", "os": "'"${configos}"'", '"${configvariantkv}"'"rootfs": {"type": "layers", "diff_ids": [' > ${TEST_SCRATCH_DIR}/config.json echo '{"schemaVersion": 2, "mediaType": "application/vnd.oci.image.manifest.v1+json", "layers": [' > ${TEST_SCRATCH_DIR}/manifest.json # Create some layers for layer in $(seq 8) ; do # Content for the layer createrandom ${TEST_SCRATCH_DIR}/file$layer $((RANDOM+1024)) # Layer blob tar -c -C ${TEST_SCRATCH_DIR} -f ${TEST_SCRATCH_DIR}/layer$layer.tar file$layer # Get the layer blob's digest and size local diffid=$(sha256sum ${TEST_SCRATCH_DIR}/layer$layer.tar) local diffsize=$(wc -c ${TEST_SCRATCH_DIR}/layer$layer.tar) # Link the blob into where an OCI layout would put it. ln ${TEST_SCRATCH_DIR}/layer$layer.tar $ocidir/blobs/sha256/${diffid%% *} # Try to keep the resulting files at least kind of readable. if test $layer -gt 1 ; then echo "," >> ${TEST_SCRATCH_DIR}/config.json echo "," >> ${TEST_SCRATCH_DIR}/manifest.json fi # Add the layer to the config blob's list of diffIDs for its rootfs. echo -n ' "sha256:'${diffid%% *}'"' >> ${TEST_SCRATCH_DIR}/config.json # Add the layer blob to the manifest's list of blobs. echo -n ' {"mediaType": "application/vnd.oci.image.layer.v1.tar", "digest": "sha256:'${diffid%% *}'", "size": '${diffsize%% *}'}' >> ${TEST_SCRATCH_DIR}/manifest.json done # Finish the diffID and layer blob lists. echo >> ${TEST_SCRATCH_DIR}/config.json echo >> ${TEST_SCRATCH_DIR}/manifest.json # Finish the config blob with some boilerplate stuff. echo ']}, "config": { "Cmd": ["/bin/sh"], "Env": [ "PATH=/usr/local/sbin:/usr/sbin:/sbin:/usr/local/bin:/usr/bin:/bin" ]}}' >> ${TEST_SCRATCH_DIR}/config.json # Compute the config blob's digest and size, so that we can list it in the manifest. local configsize=$(wc -c ${TEST_SCRATCH_DIR}/config.json) local configdigest=$(sha256sum ${TEST_SCRATCH_DIR}/config.json) # Finish the manifest with information about the config blob. echo '], "config": { "mediaType": "application/vnd.oci.image.config.v1+json", "digest": "sha256:'${configdigest%% *}'", "size": '${configsize%% *}'}}' >> ${TEST_SCRATCH_DIR}/manifest.json # Compute the manifest's digest and size, so that we can list it in the OCI layout index. local manifestsize=$(wc -c ${TEST_SCRATCH_DIR}/manifest.json) local manifestdigest=$(sha256sum ${TEST_SCRATCH_DIR}/manifest.json) # Link the config blob and manifest into where an OCI layout would put them. ln ${TEST_SCRATCH_DIR}/config.json $ocidir/blobs/sha256/${configdigest%% *} ln ${TEST_SCRATCH_DIR}/manifest.json $ocidir/blobs/sha256/${manifestdigest%% *} # Write the layout index with just the one image manifest in it. echo '{"schemaVersion": 2, "manifests": [ {"mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": "sha256:'${manifestdigest%% *}'", "size": '${manifestsize%% *}' } ]}' > $ocidir/index.json # Write the "this is an OCI layout directory" identifier. echo '{"imageLayoutVersion":"1.0.0"}' > $ocidir/oci-layout # Import the image from the OCI layout into buildah's normal storage. run_buildah pull --log-level=debug $WITH_POLICY_JSON oci:$ocidir # Tag the image (we know its ID is the config blob digest, since it's an OCI # image) with the name the Dockerfile will specify as its base image. run_buildah tag ${configdigest%% *} fakeregistry.podman.invalid/notreal # Double-check that the image has no history, which is what we wanted to get # out of all of this. run_buildah inspect --format '{{.History}}' fakeregistry.podman.invalid/notreal assert "${lines}" == '[]' "base image generated for test had history field that was not an empty slice" # Build images using our image-with-no-history as a base, to check that we # don't trip over ourselves when doing so. run_buildah build $WITH_POLICY_JSON --pull=never --layers=false $BUDFILES/no-history run_buildah build $WITH_POLICY_JSON --pull=never --layers=true $BUDFILES/no-history } @test "bud with encrypted FROM image" { _prefetch busybox local contextdir=${TEST_SCRATCH_DIR}/tmp mkdir $contextdir openssl genrsa -out $contextdir/mykey.pem 1024 openssl genrsa -out $contextdir/mykey2.pem 1024 openssl rsa -in $contextdir/mykey.pem -pubout > $contextdir/mykey.pub start_registry run_buildah push $WITH_POLICY_JSON --tls-verify=false --creds testuser:testpassword --encryption-key jwe:$contextdir/mykey.pub busybox docker://localhost:${REGISTRY_PORT}/buildah/busybox_encrypted:latest target=busybox-image echo FROM localhost:${REGISTRY_PORT}/buildah/busybox_encrypted:latest > $contextdir/Dockerfile # Try to build from encrypted image without key run_buildah 125 build $WITH_POLICY_JSON --tls-verify=false --creds testuser:testpassword -t ${target} -f $contextdir/Dockerfile assert "$output" =~ "does not match config's DiffID" # Try to build from encrypted image with wrong key run_buildah 125 build $WITH_POLICY_JSON --tls-verify=false --creds testuser:testpassword --decryption-key $contextdir/mykey2.pem -t ${target} -f $contextdir/Dockerfile assert "$output" =~ "no suitable key found for decrypting layer key" assert "$output" =~ "- JWE: No suitable private key found for decryption" # Try to build with the correct key run_buildah build $WITH_POLICY_JSON --tls-verify=false --creds testuser:testpassword --decryption-key $contextdir/mykey.pem -t ${target} -f $contextdir/Dockerfile assert "$output" =~ "Successfully tagged localhost:$REGISTRY_PORT/" rm -rf $contextdir } @test "bud with --build-arg" { _prefetch alpine busybox target=busybox-image # Envariable not present at all run_buildah --log-level "warn" bud $WITH_POLICY_JSON -t ${target} $BUDFILES/build-arg expect_output --substring 'missing \\"foo\\" build argument. Try adding' # Envariable explicitly set on command line run_buildah build $WITH_POLICY_JSON -t ${target} --build-arg foo=bar $BUDFILES/build-arg assert "${lines[3]}" = "bar" # Envariable from environment export foo=$(random_string 20) run_buildah build $WITH_POLICY_JSON -t ${target} --build-arg foo $BUDFILES/build-arg assert "${lines[3]}" = "$foo" } @test "bud arg and env var with same name" { _prefetch busybox # Regression test for https://github.com/containers/buildah/issues/2345 run_buildah build $WITH_POLICY_JSON -t testctr $BUDFILES/dupe-arg-env-name expect_output --substring "https://example.org/bar" } @test "bud copy chown with newuser" { _prefetch $SAFEIMAGE # Regression test for https://github.com/containers/buildah/issues/2192 run_buildah build $WITH_POLICY_JSON -t testctr \ --build-arg SAFEIMAGE=$SAFEIMAGE \ -f $BUDFILES/copy-chown/Containerfile.chown_user $BUDFILES/copy-chown expect_output --substring "myuser:myuser" } @test "bud-builder-identity" { _prefetch alpine parent=alpine target=no-change-image run_buildah build $WITH_POLICY_JSON -t ${target} $BUDFILES/from-scratch run_buildah --version local -a output_fields=($output) buildah_version=${output_fields[2]} run_buildah inspect --format '{{ index .Docker.Config.Labels "io.buildah.version"}}' $target expect_output "$buildah_version" } @test "run check --from with arg" { skip_if_no_runtime ${OCI} --version _prefetch alpine busybox run_buildah build --build-arg base=alpine --build-arg toolchainname=busybox --build-arg destinationpath=/tmp --pull=false $WITH_POLICY_JSON -f $BUDFILES/from-with-arg/Containerfile . expect_output --substring "FROM alpine" expect_output --substring 'STEP 4/4: COPY --from=\$\{toolchainname\} \/ \$\{destinationpath\}' run_buildah rm -a } @test "bud preserve rootfs for --mount=type=bind,from=" { _prefetch alpine run_buildah build --build-arg NONCE="$(date)" --layers --pull=false $WITH_POLICY_JSON -f Dockerfile.3 $BUDFILES/cache-stages expect_output --substring "Worked" } @test "bud timestamp" { _prefetch alpine timestamp=40 run_buildah build --timestamp=${timestamp} --quiet --pull=false $WITH_POLICY_JSON -t timestamp -f Dockerfile.1 $BUDFILES/cache-stages cid=$output run_buildah inspect --format '{{ .Docker.Created }}' timestamp expect_output --substring "1970-01-01" run_buildah inspect --format '{{ .OCIv1.Created }}' timestamp expect_output --substring "1970-01-01" run_buildah inspect --format '{{ .History }}' timestamp expect_output --substring "1970-01-01 00:00:${timestamp}" run_buildah from --quiet --pull=false $WITH_POLICY_JSON timestamp cid=$output run_buildah run $cid ls -l /tmpfile expect_output --substring "1970" run_buildah images --format "{{.Created}}" timestamp expect_output ${timestamp} rm -rf ${TEST_SCRATCH_DIR}/tmp } @test "bud timestamp compare" { _prefetch alpine TIMESTAMP=$(date '+%s') run_buildah build --timestamp=${TIMESTAMP} --quiet --pull=false $WITH_POLICY_JSON -t timestamp -f Dockerfile.1 $BUDFILES/cache-stages cid=$output run_buildah images --format "{{.Created}}" timestamp expect_output ${timestamp} run_buildah build --timestamp=${TIMESTAMP} --quiet --pull=false $WITH_POLICY_JSON -t timestamp -f Dockerfile.1 $BUDFILES/cache-stages expect_output "$cid" rm -rf ${TEST_SCRATCH_DIR}/tmp } @test "bud with-rusage" { _prefetch alpine run_buildah build --log-rusage --layers --pull=false --format docker $WITH_POLICY_JSON $BUDFILES/shell cid=$output # expect something that looks like it was formatted using pkg/rusage.FormatDiff() expect_output --substring ".*\(system\).*\(user\).*\(elapsed\).*input.*output" } @test "bud with-rusage-logfile" { _prefetch alpine run_buildah build --log-rusage --rusage-logfile ${TEST_SCRATCH_DIR}/foo.log --layers --pull=false --format docker $WITH_POLICY_JSON $BUDFILES/shell # the logfile should exist if [ ! -e ${TEST_SCRATCH_DIR}/foo.log ]; then die "rusage-logfile foo.log did not get created!"; fi # expect that foo.log only contains lines that were formatted using pkg/rusage.FormatDiff() formatted_lines=$(grep ".*\(system\).*\(user\).*\(elapsed\).*input.*output" ${TEST_SCRATCH_DIR}/foo.log | wc -l) line_count=$(wc -l <${TEST_SCRATCH_DIR}/foo.log) if [[ "$formatted_lines" -ne "$line_count" ]]; then die "Got ${formatted_lines} lines formatted with pkg/rusage.FormatDiff() but rusage-logfile has ${line_count} lines" fi } @test "bud-caching-from-scratch" { _prefetch alpine # run the build once run_buildah build --quiet --layers --pull=false --format docker $WITH_POLICY_JSON $BUDFILES/cache-scratch iid="$output" # now run it again - the cache should give us the same final image ID run_buildah build --quiet --layers --pull=false --format docker $WITH_POLICY_JSON $BUDFILES/cache-scratch assert "$output" = "$iid" # now run it *again*, except with more content added at an intermediate step, which should invalidate the cache run_buildah build --quiet --layers --pull=false --format docker $WITH_POLICY_JSON -f Dockerfile.different1 $BUDFILES/cache-scratch assert "$output" !~ "$iid" # now run it *again* again, except with more content added at an intermediate step, which should invalidate the cache run_buildah build --quiet --layers --pull=false --format docker $WITH_POLICY_JSON -f Dockerfile.different2 $BUDFILES/cache-scratch assert "$output" !~ "$iid" } @test "bud-caching-from-scratch-config" { _prefetch alpine # run the build once run_buildah build --quiet --layers --pull=false --format docker $WITH_POLICY_JSON -f Dockerfile.config $BUDFILES/cache-scratch iid="$output" # now run it again - the cache should give us the same final image ID run_buildah build --quiet --layers --pull=false --format docker $WITH_POLICY_JSON -f Dockerfile.config $BUDFILES/cache-scratch assert "$output" = "$iid" # now run it *again*, except with more content added at an intermediate step, which should invalidate the cache run_buildah build --quiet --layers --pull=false --format docker $WITH_POLICY_JSON -f Dockerfile.different1 $BUDFILES/cache-scratch assert "$output" !~ "$iid" # now run it *again* again, except with more content added at an intermediate step, which should invalidate the cache run_buildah build --quiet --layers --pull=false --format docker $WITH_POLICY_JSON -f Dockerfile.different2 $BUDFILES/cache-scratch assert "$output" !~ "$iid" } @test "bud capabilities test" { _prefetch busybox # something not enabled by default in containers.conf run_buildah build --cap-add cap_sys_ptrace -t testcap $WITH_POLICY_JSON -f $BUDFILES/capabilities/Dockerfile expect_output --substring "uid=3267" expect_output --substring "CapBnd: 00000000a80c25fb" expect_output --substring "CapEff: 0000000000000000" # some things enabled by default in containers.conf run_buildah build --cap-drop cap_chown,cap_dac_override,cap_fowner -t testcapd $WITH_POLICY_JSON -f $BUDFILES/capabilities/Dockerfile expect_output --substring "uid=3267" expect_output --substring "CapBnd: 00000000a80425f0" expect_output --substring "CapEff: 0000000000000000" } @test "bud does not gobble stdin" { _prefetch alpine ctxdir=${TEST_SCRATCH_DIR}/bud mkdir -p $ctxdir cat >$ctxdir/Dockerfile </dev/null; cat)) expect_output --from="$passthru" "$random_msg" "stdin was passed through" } @test "bud cache by format" { _prefetch alpine # Build first in Docker format. Whether we do OCI or Docker first shouldn't matter, so we picked one. run_buildah build --iidfile ${TEST_SCRATCH_DIR}/first-docker --metadata-file ${TEST_SCRATCH_DIR}/first-docker.meta --format docker --layers --quiet $WITH_POLICY_JSON $BUDFILES/cache-format # Build in OCI format. Cache should not reuse the same images, so we should get a different image ID. run_buildah build --iidfile ${TEST_SCRATCH_DIR}/first-oci --metadata-file ${TEST_SCRATCH_DIR}/first-oci.meta --format oci --layers --quiet $WITH_POLICY_JSON $BUDFILES/cache-format # Build in Docker format again. Cache traversal should 100% hit the Docker image, so we should get its image ID. run_buildah build --iidfile ${TEST_SCRATCH_DIR}/second-docker --metadata-file ${TEST_SCRATCH_DIR}/second-docker.meta --format docker --layers --quiet $WITH_POLICY_JSON $BUDFILES/cache-format # Build in OCI format again. Cache traversal should 100% hit the OCI image, so we should get its image ID. run_buildah build --iidfile ${TEST_SCRATCH_DIR}/second-oci --metadata-file ${TEST_SCRATCH_DIR}/second-oci.meta --format oci --layers --quiet $WITH_POLICY_JSON $BUDFILES/cache-format # Compare them. The two images we built in Docker format should be the same, the two we built in OCI format # should be the same, but the OCI and Docker format images should be different. assert "$(< ${TEST_SCRATCH_DIR}/first-docker)" = "$(< ${TEST_SCRATCH_DIR}/second-docker)" \ "iidfile(first docker) == iidfile(second docker)" assert "$(< ${TEST_SCRATCH_DIR}/first-oci)" = "$(< ${TEST_SCRATCH_DIR}/second-oci)" \ "iidfile(first oci) == iidfile(second oci)" assert "$(< ${TEST_SCRATCH_DIR}/first-docker)" != "$(< ${TEST_SCRATCH_DIR}/first-oci)" \ "iidfile(first docker) != iidfile(first oci)" assert "$(< ${TEST_SCRATCH_DIR}/first-docker.meta)" = "$(< ${TEST_SCRATCH_DIR}/second-docker.meta)" \ "metadata(first docker) == metadata(second docker)" assert "$(< ${TEST_SCRATCH_DIR}/first-oci.meta)" = "$(< ${TEST_SCRATCH_DIR}/second-oci.meta)" \ "metadata(first oci) == metadata(second oci)" assert "$(< ${TEST_SCRATCH_DIR}/first-docker.meta)" != "$(< ${TEST_SCRATCH_DIR}/first-oci.meta)" \ "metadata(first docker) != metadata(first oci)" jq . ${TEST_SCRATCH_DIR}/first-docker.meta jq . ${TEST_SCRATCH_DIR}/first-oci.meta assert "$(wc -c < ${TEST_SCRATCH_DIR}/first-docker.meta)" -gt 2 assert "$(wc -c < ${TEST_SCRATCH_DIR}/first-oci.meta)" -gt 2 } @test "bud cache add-copy-chown" { contentdir=${TEST_SCRATCH_DIR}/content mkdir -p ${contentdir} echo sure, this counts as a readme file > ${TEST_SCRATCH_DIR}/content/README.md starthttpd $contentdir # Build each variation of COPY (from context, from previous stage) and ADD (from context, not overriding an archive, URL) twice. # Each second build should produce an image with the same ID as the first build, because the cache matches, but they should # otherwise all be different. local actions="copy prev add tar url"; for i in 1 2 3; do for action in $actions; do # iidfiles are 1 2 3, but dockerfiles are only 1 2 then back to 1 iidfile=${TEST_SCRATCH_DIR}/${action}${i} containerfile=Dockerfile.${action}$(((i-1) % 2 + 1)) run_buildah build --build-arg=HTTP_SERVER_PORT=${HTTP_SERVER_PORT} --iidfile $iidfile --layers --quiet $WITH_POLICY_JSON -f $containerfile $BUDFILES/cache-chown done done for action in $actions; do # The third round of builds should match all of the first rounds by way # of caching. assert "$(< ${TEST_SCRATCH_DIR}/${action}1)" = "$(< ${TEST_SCRATCH_DIR}/${action}3)" \ "iidfile(${action}1) = iidfile(${action}3)" # The second round of builds should not match the first rounds, since # the different ownership makes the changes look different to the cache, # except for cases where we extract an archive, where --chown is ignored. local op="!=" if [[ $action = "tar" ]]; then op="="; fi assert "$(< ${TEST_SCRATCH_DIR}/${action}1)" $op "$(< ${TEST_SCRATCH_DIR}/${action}2)" \ "iidfile(${action}1) $op iidfile(${action}2)" # The first rounds of builds should all be different from each other, # as a sanity thing. for other in $actions; do if [[ $other != $action ]]; then assert "$(< ${TEST_SCRATCH_DIR}/${action}1)" != "$(< ${TEST_SCRATCH_DIR}/${other}1)" \ "iidfile(${action}1) != iidfile(${other}1)" fi done done } @test "bud-terminal" { _prefetch busybox run_buildah build $BUDFILES/terminal } @test "bud --ignorefile containerignore" { _prefetch alpine busybox CONTEXTDIR=${TEST_SCRATCH_DIR}/dockerignore cp -r $BUDFILES/dockerignore ${CONTEXTDIR} mv ${CONTEXTDIR}/.dockerignore ${TEST_SCRATCH_DIR}/containerignore run_buildah build -t testbud $WITH_POLICY_JSON -f ${CONTEXTDIR}/Dockerfile.succeed --ignorefile ${TEST_SCRATCH_DIR}/containerignore ${CONTEXTDIR} run_buildah from --name myctr testbud run_buildah 1 run myctr ls -l test1.txt expect_output --substring "ls: test1.txt: No such file or directory" run_buildah run myctr ls -l test2.txt run_buildah 1 run myctr ls -l sub1.txt expect_output --substring "ls: sub1.txt: No such file or directory" run_buildah 1 run myctr ls -l sub2.txt expect_output --substring "ls: sub2.txt: No such file or directory" run_buildah 1 run myctr ls -l subdir/ expect_output --substring "ls: subdir/: No such file or directory" } @test "bud with network options" { skip_if_chroot _prefetch alpine target=alpine-image run_buildah build --network=none $WITH_POLICY_JSON -t ${target} $BUDFILES/containerfile expect_output --substring "FROM alpine" run_buildah build --network=private $WITH_POLICY_JSON -t ${target} $BUDFILES/containerfile expect_output --substring "FROM alpine" run_buildah build --network=container $WITH_POLICY_JSON -t ${target} $BUDFILES/containerfile expect_output --substring "FROM alpine" } @test "bud-replace-from-in-containerfile" { _prefetch alpine busybox # override the first FROM (fedora) image in the Containerfile # with alpine, leave the second (busybox) alone. run_buildah build $WITH_POLICY_JSON --from=alpine $BUDFILES/build-with-from expect_output --substring "\[1/2] STEP 1/1: FROM alpine AS builder" expect_output --substring "\[2/2] STEP 1/2: FROM busybox" } @test "bud test no --stdin" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p ${mytmpdir} cat > $mytmpdir/Containerfile << _EOF FROM alpine RUN read -t 1 x && echo test got \<\$x\> RUN touch /tmp/done _EOF # fail without --stdin run_buildah 1 bud -t testbud $WITH_POLICY_JSON ${mytmpdir} <<< input expect_output --substring "building .*: exit status 1" run_buildah build --stdin -t testbud $WITH_POLICY_JSON ${mytmpdir} <<< input expect_output --substring "test got " } @test "bud with --arch flag" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p ${mytmpdir} cat > $mytmpdir/Containerfile << _EOF FROM alpine #RUN arch _EOF run_buildah build --arch=arm64 -t arch-test $WITH_POLICY_JSON ${mytmpdir} <<< input # expect_output --substring "aarch64" # run_buildah from --quiet --pull=false $WITH_POLICY_JSON arch-test # cid=$output # run_buildah run $cid arch # expect_output --substring "aarch64" } @test "bud with --manifest flag new manifest" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p ${mytmpdir} cat > $mytmpdir/Containerfile << _EOF from alpine run echo hello _EOF run_buildah build -q --manifest=testlist -t arch-test $WITH_POLICY_JSON ${mytmpdir} <<< input cid=$output run_buildah images expect_output --substring testlist run_buildah inspect --format '{{ .FromImageDigest }}' $cid digest=$output run_buildah manifest inspect testlist expect_output --substring $digest } @test "bud with --manifest flag existing manifest" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p ${mytmpdir} cat > $mytmpdir/Containerfile << _EOF from alpine run echo hello _EOF run_buildah manifest create testlist run_buildah build -q --manifest=testlist -t arch-test $WITH_POLICY_JSON ${mytmpdir} <<< input cid=$output run_buildah images expect_output --substring testlist run_buildah inspect --format '{{ .FromImageDigest }}' $cid digest=$output run_buildah manifest inspect testlist expect_output --substring $digest } @test "bud test empty newdir" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p ${mytmpdir} cat > $mytmpdir/Containerfile << _EOF FROM alpine as galaxy RUN mkdir -p /usr/share/ansible/roles /usr/share/ansible/collections RUN echo "bar" RUN echo "foo" > /usr/share/ansible/collections/file.txt FROM galaxy RUN mkdir -p /usr/share/ansible/roles /usr/share/ansible/collections COPY --from=galaxy /usr/share/ansible/roles /usr/share/ansible/roles COPY --from=galaxy /usr/share/ansible/collections /usr/share/ansible/collections _EOF run_buildah build --layers $WITH_POLICY_JSON -t testbud $mytmpdir expect_output --substring "COPY --from=galaxy /usr/share/ansible/collections /usr/share/ansible/collections" } @test "bud retain intermediary image" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p ${mytmpdir} cat > $mytmpdir/Containerfile.a << _EOF FROM alpine LABEL image=a RUN echo foo _EOF cat > $mytmpdir/Containerfile.b << _EOF FROM image-a FROM scratch _EOF run_buildah build -f Containerfile.a -q --manifest=testlist -t image-a $WITH_POLICY_JSON ${mytmpdir} <<< input cid=$output run_buildah images -f "label=image=a" expect_output --substring image-a run_buildah build -f Containerfile.b -q --manifest=testlist -t image-b $WITH_POLICY_JSON ${mytmpdir} <<< input cid=$output run_buildah images expect_output --substring image-a } @test "bud --pull=ifmissing --arch test" { mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p ${mytmpdir} cat > $mytmpdir/Containerfile << _EOF FROM $SAFEIMAGE _EOF run_buildah build --pull=ifmissing -q --arch=amd64 -t image-amd $WITH_POLICY_JSON ${mytmpdir} run_buildah inspect --format '{{ .OCIv1.Architecture }}' image-amd expect_output amd64 # Tag the image to localhost/safeimage to make sure that the image gets # pulled since the local one does not match the requested architecture. run_buildah tag image-amd localhost/${SAFEIMAGE_NAME}:${SAFEIMAGE_TAG} run_buildah build --pull=ifmissing -q --arch=arm64 -t image-arm $WITH_POLICY_JSON ${mytmpdir} run_buildah inspect --format '{{ .OCIv1.Architecture }}' image-arm expect_output arm64 run_buildah inspect --format '{{ .FromImageID }}' image-arm fromiid=$output run_buildah inspect --format '{{ .OCIv1.Architecture }}' $fromiid expect_output arm64 } @test "bud --file with directory" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir1 mkdir -p ${mytmpdir} cat > $mytmpdir/Containerfile << _EOF FROM alpine _EOF run_buildah 125 build -t testbud $WITH_POLICY_JSON --file ${mytmpdir} . } @test "bud --authfile" { _prefetch alpine start_registry run_buildah login --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth --username testuser --password testpassword localhost:${REGISTRY_PORT} run_buildah push $WITH_POLICY_JSON --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth alpine docker://localhost:${REGISTRY_PORT}/buildah/alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p ${mytmpdir} cat > $mytmpdir/Containerfile << _EOF FROM localhost:${REGISTRY_PORT}/buildah/alpine RUN touch /test _EOF run_buildah build -t myalpine --authfile ${TEST_SCRATCH_DIR}/test.auth --tls-verify=false $WITH_POLICY_JSON --file ${mytmpdir}/Containerfile . run_buildah rmi localhost:${REGISTRY_PORT}/buildah/alpine run_buildah rmi myalpine } @test "build verify cache behaviour with --cache-ttl" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/Dockerfile1 << _EOF FROM alpine RUN touch hello RUN echo world _EOF # Build with --timestamp somewhere in the past run_buildah build $WITH_POLICY_JSON --timestamp 1628099045 --layers -t source -f $contextdir/Dockerfile1 # Specify --cache-ttl 0.5s and cache should # not be used since cached image is created # with timestamp somewhere in past ( in ~2021 ) run_buildah build $WITH_POLICY_JSON --cache-ttl=0.5s --layers -t source -f $contextdir/Dockerfile1 # Should not contain `Using cache` since all # cached layers are 1s old. assert "$output" !~ "Using cache" # clean all images and cache run_buildah rmi --all -f _prefetch alpine run_buildah build $WITH_POLICY_JSON --layers -t source -f $contextdir/Dockerfile1 # Cache should be used since our ttl is 1h but # cache layers are just built so they should be # few seconds old. run_buildah build $WITH_POLICY_JSON --cache-ttl=1h --layers -t source -f $contextdir/Dockerfile1 # must use already cached images. expect_output --substring "Using cache" } @test "build verify cache must reuse most recent layer in conflict" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/Dockerfile1 << _EOF FROM alpine RUN touch hello RUN echo world _EOF # Populate cache run_buildah build $WITH_POLICY_JSON --layers -t source -f $contextdir/Dockerfile1 # Populate cache again with new layers run_buildah build $WITH_POLICY_JSON --layers --cache-ttl=.01s --iidfile ${TEST_SCRATCH_DIR}/image1.txt -t source -f $contextdir/Dockerfile1 # Should not contain `Using cache` since all # cached layers are older than .01s. assert "$output" !~ "Using cache" # Build again use cache and make sure most recently built image is reused run_buildah build $WITH_POLICY_JSON --layers --iidfile ${TEST_SCRATCH_DIR}/image2.txt -t source -f $contextdir/Dockerfile1 # Must use cache expect_output --substring "Using cache" # image id should be same as the most recently built image which already exists in cache. cmp ${TEST_SCRATCH_DIR}/image1.txt ${TEST_SCRATCH_DIR}/image2.txt } @test "build verify cache behaviour with --cache-ttl=0s" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/Dockerfile1 << _EOF FROM alpine RUN touch hello RUN echo world _EOF # Build with --timestamp somewhere in the past run_buildah build $WITH_POLICY_JSON --timestamp 1628099045 --layers -t source -f $contextdir/Dockerfile1 # Specify --cache-ttl 0.5s and cache should # not be used since cached image is created # with timestamp somewhere in past ( in ~2021 ) run_buildah --log-level debug build $WITH_POLICY_JSON --cache-ttl=0 --layers -t source -f $contextdir/Dockerfile1 # Should not contain `Using cache` since all # cached layers are 1s old. assert "$output" !~ "Using cache" expect_output --substring "Setting --no-cache=true" } @test "build test pushing and pulling from multiple remote cache sources" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p ${mytmpdir} echo something > ${mytmpdir}/somefile cat > $mytmpdir/Containerfile << _EOF FROM alpine RUN echo hello RUN echo world RUN touch hello ADD somefile somefile FROM alpine RUN echo hello COPY --from=0 hello hello _EOF start_registry run_buildah login --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth --username testuser --password testpassword localhost:${REGISTRY_PORT} # ------ Test case ------ # # prepare expected output beforehand # must push cache twice i.e for first step and second step run printf "STEP 2/5: RUN echo hello\nhello\n--> Pushing cache" step1=$output run printf "STEP 3/5: RUN echo world\nworld\n--> Pushing cache" step2=$output run printf "STEP 4/5: RUN touch hello\n--> Pushing cache" step3=$output run printf "STEP 5/5: ADD somefile somefile\n--> Pushing cache" step4=$output # First run step in second stage should not be pushed since its already pushed run printf "STEP 2/3: RUN echo hello\n--> Using cache" step5=$output # Last step is `COPY --from=0 hello hello' so it must be committed and pushed # actual output is `[2/2] STEP 3/3: COPY --from=0 hello hello\n[2/2] COMMIT test\n-->Pushing cache` # but lets match smaller suffix run printf "COMMIT test\n--> Pushing cache" step6=$output # actually run build run_buildah build $WITH_POLICY_JSON --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth --layers --cache-to localhost:${REGISTRY_PORT}/temp2 --cache-to localhost:${REGISTRY_PORT}/temp -t test -f ${mytmpdir}/Containerfile ${mytmpdir} expect_output --substring "$step1" expect_output --substring "$step2" expect_output --substring "$step3" expect_output --substring "$step4" expect_output --substring "$step5" expect_output --substring "$step6" # clean all cache and intermediate images # to make sure that we are only using cache # from remote repo and not the local storage. run_buildah rmi --all -f # ------ Test case ------ # # expect cache to be pushed on remote stream # now a build on clean slate must pull cache # from remote instead of actually computing the # run steps run printf "STEP 2/5: RUN echo hello\n--> Cache pulled from remote" step1=$output run printf "STEP 3/5: RUN echo world\n--> Cache pulled from remote" step2=$output run printf "STEP 4/5: RUN touch hello\n--> Cache pulled from remote" step3=$output run printf "STEP 5/5: ADD somefile somefile\n--> Cache pulled from remote" step4=$output # First run step in second stage should not be pulled since its already pulled run printf "STEP 2/3: RUN echo hello\n--> Using cache" step5=$output run printf "COPY --from=0 hello hello\n--> Cache pulled from remote" step6=$output run_buildah build $WITH_POLICY_JSON --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth --layers --cache-from localhost:${REGISTRY_PORT}/temp -t test -f ${mytmpdir}/Containerfile ${mytmpdir} expect_output --substring "$step1" expect_output --substring "$step2" expect_output --substring "$step3" expect_output --substring "$step4" expect_output --substring "$step5" expect_output --substring "$step6" ##### Test when cache source is: localhost:${REGISTRY_PORT}/temp2 # clean all cache and intermediate images # to make sure that we are only using cache # from remote repo and not the local storage. run_buildah rmi --all -f # ------ Test case ------ # # expect cache to be pushed on remote stream # now a build on clean slate must pull cache # from remote instead of actually computing the # run steps run printf "STEP 2/5: RUN echo hello\n--> Cache pulled from remote" step1=$output run printf "STEP 3/5: RUN echo world\n--> Cache pulled from remote" step2=$output run printf "STEP 4/5: RUN touch hello\n--> Cache pulled from remote" step3=$output run printf "STEP 5/5: ADD somefile somefile\n--> Cache pulled from remote" step4=$output # First run step in second stage should not be pulled since its already pulled run printf "STEP 2/3: RUN echo hello\n--> Using cache" step5=$output run printf "COPY --from=0 hello hello\n--> Cache pulled from remote" step6=$output run_buildah build $WITH_POLICY_JSON --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth --layers --cache-from localhost:${REGISTRY_PORT}/temp2 -t test -f ${mytmpdir}/Containerfile ${mytmpdir} expect_output --substring "$step1" expect_output --substring "$step2" expect_output --substring "$step3" expect_output --substring "$step4" expect_output --substring "$step5" expect_output --substring "$step6" } @test "build test pushing and pulling from remote cache sources" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p ${mytmpdir} echo something > ${mytmpdir}/somefile cat > $mytmpdir/Containerfile << _EOF FROM alpine RUN echo hello RUN echo world RUN touch hello ADD somefile somefile FROM alpine RUN echo hello COPY --from=0 hello hello RUN --mount=type=cache,id=YfHI60aApFM-target,target=/target echo world > /target/hello _EOF start_registry run_buildah login --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth --username testuser --password testpassword localhost:${REGISTRY_PORT} # ------ Test case ------ # # prepare expected output beforehand # must push cache twice i.e for first step and second step run printf "STEP 2/5: RUN echo hello\nhello\n--> Pushing cache" step1=$output run printf "STEP 3/5: RUN echo world\nworld\n--> Pushing cache" step2=$output run printf "STEP 4/5: RUN touch hello\n--> Pushing cache" step3=$output run printf "STEP 5/5: ADD somefile somefile\n--> Pushing cache" step4=$output # First run step in second stage should not be pushed since its already pushed run printf "STEP 2/4: RUN echo hello\n--> Using cache" step5=$output # Last step is `COPY --from=0 hello hello' so it must be committed and pushed # actual output is `[2/2] STEP 3/3: COPY --from=0 hello hello\n[2/2] COMMIT test\n-->Pushing cache` # but lets match smaller suffix run printf "COMMIT test\n--> Pushing cache" step6=$output # actually run build run_buildah build $WITH_POLICY_JSON --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth --layers --cache-to localhost:${REGISTRY_PORT}/temp -t test -f ${mytmpdir}/Containerfile ${mytmpdir} expect_output --substring "$step1" expect_output --substring "$step2" expect_output --substring "$step3" expect_output --substring "$step4" expect_output --substring "$step5" expect_output --substring "$step6" # clean all cache and intermediate images # to make sure that we are only using cache # from remote repo and not the local storage. run_buildah rmi --all -f # ------ Test case ------ # # expect cache to be pushed on remote stream # now a build on clean slate must pull cache # from remote instead of actually computing the # run steps run printf "STEP 2/5: RUN echo hello\n--> Cache pulled from remote" step1=$output run printf "STEP 3/5: RUN echo world\n--> Cache pulled from remote" step2=$output run printf "STEP 4/5: RUN touch hello\n--> Cache pulled from remote" step3=$output run printf "STEP 5/5: ADD somefile somefile\n--> Cache pulled from remote" step4=$output # First run step in second stage should not be pulled since its already pulled run printf "STEP 2/4: RUN echo hello\n--> Using cache" step5=$output run printf "COPY --from=0 hello hello\n--> Cache pulled from remote" step6=$output run_buildah build $WITH_POLICY_JSON --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth --layers --cache-from localhost:${REGISTRY_PORT}/temp --cache-to localhost:${REGISTRY_PORT}/temp -t test -f ${mytmpdir}/Containerfile ${mytmpdir} expect_output --substring "$step1" expect_output --substring "$step2" expect_output --substring "$step3" expect_output --substring "$step4" expect_output --substring "$step5" expect_output --substring "$step6" # ------ Test case ------ # # Try building again with --cache-from to make sure # we don't pull image if we already have it in our # local storage run_buildah build $WITH_POLICY_JSON --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth --layers --cache-from localhost:${REGISTRY_PORT}/temp -t test -f ${mytmpdir}/Containerfile ${mytmpdir} # must use cache since we have cache in local storage expect_output --substring "Using cache" # should not pull cache if its already in local storage assert "$output" !~ "Cache pulled" # ------ Test case ------ # # Build again with --cache-to and --cache-from # Since intermediate images are already present # on local storage so nothing must be pulled but # intermediate must be pushed since buildah is not # aware if they on remote repo or not. run_buildah build $WITH_POLICY_JSON --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth --layers --cache-from localhost:${REGISTRY_PORT}/temp --cache-to localhost:${REGISTRY_PORT}/temp -t test -f ${mytmpdir}/Containerfile ${mytmpdir} # must use cache since we have cache in local storage expect_output --substring "Using cache" # must also push cache since nothing was pulled from remote repo expect_output --substring "Pushing cache" # should not pull cache if its already in local storage assert "$output" !~ "Cache pulled" } @test "build test pushing and pulling from remote cache sources - after adding content summary" { _prefetch alpine start_registry run_buildah login --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth --username testuser --password testpassword localhost:${REGISTRY_PORT} # ------ Test case ------ # # prepare expected output beforehand # must push cache twice i.e for first step and second step run printf "STEP 2/3: ARG VAR=hello\n--> Pushing cache" step1=$output run printf "STEP 3/3: RUN echo \"Hello \$VAR\"" step2=$output run printf "Hello hello" step3=$output run printf "COMMIT test\n--> Pushing cache" step6=$output # actually run build run_buildah build $WITH_POLICY_JSON --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth --layers --cache-to localhost:${REGISTRY_PORT}/temp -t test -f $BUDFILES/cache-from/Containerfile expect_output --substring "$step1" #expect_output --substring "$step2" expect_output --substring "$step3" expect_output --substring "$step6" # clean all cache and intermediate images # to make sure that we are only using cache # from remote repo and not the local storage. # Important side-note: don't use `run_buildah rmi --all -f` # since on podman-remote test this will remove prefetched alpine # and it will try to pull alpine from docker.io with # completely different digest (ruining our cache logic). run_buildah rmi test # ------ Test case ------ # # expect cache to be pushed on remote stream # now a build on clean slate must pull cache # from remote instead of actually computing the # run steps run printf "STEP 2/3: ARG VAR=hello\n--> Cache pulled from remote" step1=$output run printf "VAR\"\n--> Cache pulled from remote" step2=$output run_buildah build $WITH_POLICY_JSON --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth --layers --cache-from localhost:${REGISTRY_PORT}/temp --cache-to localhost:${REGISTRY_PORT}/temp -t test -f $BUDFILES/cache-from/Containerfile expect_output --substring "$step1" expect_output --substring "$step2" # ------ Test case ------ # # Try building again with --cache-from to make sure # we don't pull image if we already have it in our # local storage run_buildah build $WITH_POLICY_JSON --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth --layers --cache-from localhost:${REGISTRY_PORT}/temp -t test -f $BUDFILES/cache-from/Containerfile # must use cache since we have cache in local storage expect_output --substring "Using cache" # should not pull cache if its already in local storage assert "$output" !~ "Cache pulled" # ------ Test case ------ # # Build again with --cache-to and --cache-from # Since intermediate images are already present # on local storage so nothing must be pulled but # intermediate must be pushed since buildah is not # aware if they on remote repo or not. run_buildah build $WITH_POLICY_JSON --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth --layers --cache-from localhost:${REGISTRY_PORT}/temp --cache-to localhost:${REGISTRY_PORT}/temp -t test -f $BUDFILES/cache-from/Containerfile # must use cache since we have cache in local storage expect_output --substring "Using cache" # must also push cache since nothing was pulled from remote repo expect_output --substring "Pushing cache" # should not pull cache if its already in local storage assert "$output" !~ "Cache pulled" } @test "build test run mounting stage cached from remote cache source" { _prefetch alpine start_registry run_buildah login --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth --username testuser --password testpassword localhost:${REGISTRY_PORT} # ------ Test case ------ # # prepare expected output beforehand run printf "STEP 2/2: COPY / /\n--> Pushing cache" step1_2=$output run printf "COMMIT test\n--> Pushing cache" step2_2=$output # actually run build run_buildah build $WITH_POLICY_JSON --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth --layers --cache-to localhost:${REGISTRY_PORT}/temp -t test -f $BUDFILES/cache-from-stage/Containerfile $BUDFILES/cache-from-stage expect_output --substring "$step1_2" #expect_output --substring "$step2_2" # clean all cache and intermediate images # to make sure that we are only using cache # from remote repo and not the local storage. # Important side-note: don't use `run_buildah rmi --all -f` # since on podman-remote test this will remove prefetched alpine # and it will try to pull alpine from docker.io with # completely different digest (ruining our cache logic). run_buildah rmi test # ------ Test case ------ # # expect cache to be pushed on remote stream # now a build on clean slate must pull cache # from remote instead of actually computing the # run steps run printf "STEP 2/2: COPY / /\n--> Using cache" step1_2=$output run printf "STEP 2/2: RUN --mount=type=bind,from=stage1,target=/mnt echo hi > test\n--> Cache pulled from remote" step2_2=$output run_buildah build $WITH_POLICY_JSON --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth --layers --cache-from localhost:${REGISTRY_PORT}/temp --cache-to localhost:${REGISTRY_PORT}/temp -t test -f $BUDFILES/cache-from-stage/Containerfile $BUDFILES/cache-from-stage expect_output --substring "$step1_2" expect_output --substring "$step2_2" } @test "bud with undefined build arg directory" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir1 mkdir -p ${mytmpdir} cat > $mytmpdir/Containerfile << _EOF FROM alpine ARG SECRET="Itismysecret" ARG NEWSECRET RUN echo $SECRET RUN touch hello FROM alpine COPY --from=0 hello . RUN echo "$SECRET" _EOF run_buildah build -t testbud $WITH_POLICY_JSON --file ${mytmpdir}/Containerfile . assert "$output" !~ '--build-arg SECRET=' expect_output --substring '\-\-build-arg NEWSECRET=' run_buildah build -t testbud $WITH_POLICY_JSON --build-arg NEWSECRET="VerySecret" --file ${mytmpdir}/Containerfile . assert "$output" !~ '--build-arg SECRET=' assert "$output" !~ '--build-arg NEWSECRET=' # case should similarly honor globally declared args cat > $mytmpdir/Containerfile << _EOF ARG SECRET="Itismysecret" FROM alpine ARG SECRET ARG NEWSECRET RUN echo $SECRET RUN touch hello FROM alpine COPY --from=0 hello . RUN echo "$SECRET" _EOF run_buildah build -t testbud $WITH_POLICY_JSON --file ${mytmpdir}/Containerfile . assert "$output" !~ '--build-arg SECRET=' expect_output --substring '\-\-build-arg NEWSECRET=' run_buildah build -t testbud $WITH_POLICY_JSON --build-arg NEWSECRET="VerySecret" --file ${mytmpdir}/Containerfile . assert "$output" !~ '--build-arg SECRET=' assert "$output" !~ '--build-arg NEWSECRET=' } @test "bud with arg in from statement" { _prefetch alpine run_buildah build -t testbud $WITH_POLICY_JSON --build-arg app_type=x --build-arg another_app_type=m --file $BUDFILES/with-arg/Dockerfilefromarg . expect_output --substring 'world' } @test "bud with --runtime and --runtime-flag" { # This Containerfile needs us to be able to handle a working RUN instruction. skip_if_no_runtime skip_if_chroot _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p ${mytmpdir} cat > $mytmpdir/Containerfile << _EOF from alpine run echo hello _EOF local found_runtime= # runc-1.0.0-70.rc92 and 1.0.1-3 have completely different # debug messages. This is the only string common to both. local flag_accepted_rx="level=debug.*msg=.child process in init" if [ -n "$(command -v runc)" ]; then found_runtime=y run_buildah build --runtime=runc --runtime-flag=debug \ -q -t alpine-bud-runc $WITH_POLICY_JSON --file ${mytmpdir}/Containerfile . expect_output --substring "$flag_accepted_rx" fi if [ -n "$(command -v crun)" ]; then found_runtime=y # Use seccomp to make crun output a warning message because crun writes few logs. cat > ${TEST_SCRATCH_DIR}/seccomp.json << _EOF { "defaultAction": "SCMP_ACT_ALLOW", "syscalls": [ { "name": "unknown", "action": "SCMP_ACT_KILL" } ] } _EOF # crun caches seccomp profiles, so this test fails if run more than once. # See https://github.com/containers/crun/issues/1475 cruntmp=${TEST_SCRATCH_DIR}/crun mkdir $cruntmp run_buildah build --runtime=crun --runtime-flag=debug --runtime-flag=root=$cruntmp \ --security-opt seccomp=${TEST_SCRATCH_DIR}/seccomp.json \ -q -t alpine-bud-crun $WITH_POLICY_JSON --file ${mytmpdir}/Containerfile . expect_output --substring "unknown seccomp syscall" fi if [ -z "${found_runtime}" ]; then die "Did not find 'runc' nor 'crun' in \$PATH - could not run this test!" fi } @test "bud - invalid runtime flags test" { skip_if_no_runtime skip_if_chroot _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p ${mytmpdir} cat > $mytmpdir/Containerfile << _EOF from alpine run echo hello _EOF run_buildah 1 build $WITH_POLICY_JSON --runtime-flag invalidflag -t build_test $mytmpdir assert "$output" =~ ".*invalidflag" "failed when passing undefined flags to the runtime" } @test "bud - accept at most one arg" { run_buildah 125 build $WITH_POLICY_JSON $BUDFILES/dns extraarg assert "$output" =~ ".*accepts at most 1 arg\(s\), received 2" "Should fail when passed extra arg after context directory" } @test "bud with --no-hostname" { skip_if_no_runtime skip_if_chroot _prefetch alpine run_buildah build --no-cache -t testbud \ $WITH_POLICY_JSON $BUDFILES/no-hostname assert "${lines[2]}" != "localhost" "Should be set to something other then localhost" run_buildah build --no-hostname --no-cache -t testbud \ $WITH_POLICY_JSON \ $BUDFILES/no-hostname assert "${lines[2]}" == "localhost" "Should be set to localhost" run_buildah 1 build --network=none --no-hostname --no-cache -t testbud \ $WITH_POLICY_JSON \ -f $BUDFILES/no-hostname/Containerfile.noetc \ $BUDFILES/no-hostname assert "$output" =~ ".*ls: /etc: No such file or directory" "/etc/ directory should be gone" } @test "bud with --add-host" { skip_if_no_runtime _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p ${mytmpdir} cat > $mytmpdir/Containerfile << _EOF from alpine run grep "myhostname" /etc/hosts _EOF ip=123.45.67.$(( $RANDOM % 256 )) run_buildah build --add-host=myhostname:$ip -t testbud \ $WITH_POLICY_JSON --file ${mytmpdir}/Containerfile . expect_output --from="${lines[2]}" --substring "^$ip\s+myhostname" run_buildah 125 build --no-cache --add-host=myhostname:$ip \ --no-hosts \ $WITH_POLICY_JSON --file ${mytmpdir}/Containerfile . expect_output --substring "\-\-no-hosts and \-\-add-host conflict, can not be used together" run_buildah 1 build --no-cache --no-hosts \ $WITH_POLICY_JSON --file ${mytmpdir}/Containerfile . expect_output --substring 'building at STEP "RUN grep "myhostname" /etc/hosts' } @test "bud with --add-host with host-gateway" { skip_if_no_runtime _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p ${mytmpdir} cat > $mytmpdir/Containerfile << _EOF from alpine run cat /etc/hosts _EOF run_buildah build --add-host=myhostname:host-gateway -t testbud \ $WITH_POLICY_JSON --file ${mytmpdir}/Containerfile ${mytimedir} expect_output --substring "myhostname" } @test "bud with --cgroup-parent" { skip_if_rootless_environment skip_if_chroot _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p ${mytmpdir} cat > $mytmpdir/Containerfile << _EOF FROM alpine RUN .linux.cgroupsPath _EOF # with cgroup-parent run_buildah --cgroup-manager cgroupfs build --cgroupns=host --cgroup-parent test-cgroup -t with-flag \ --runtime ${DUMPSPEC_BINARY} $WITH_POLICY_JSON --file ${mytmpdir}/Containerfile . expect_output --substring "test-cgroup" # without cgroup-parent run_buildah --cgroup-manager cgroupfs build -t without-flag \ --runtime ${DUMPSPEC_BINARY} $WITH_POLICY_JSON --file ${mytmpdir}/Containerfile . assert "$output" !~ test-cgroup } @test "bud with --cpu-period and --cpu-quota" { skip_if_chroot skip_if_rootless_environment skip_if_no_runtime _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p ${mytmpdir} cat > $mytmpdir/Containerfile << _EOF from alpine run cat /sys/fs/cgroup/\$(awk -F: '{print \$NF}' /proc/self/cgroup)/cpu.max _EOF run_buildah build --cpu-period=1234 --cpu-quota=5678 -t testcpu \ $WITH_POLICY_JSON --file ${mytmpdir}/Containerfile . expect_output --from="${lines[2]}" "5678 1234" } @test "bud check mount /sys/fs/cgroup" { mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p ${mytmpdir} cat > $mytmpdir/Containerfile << _EOF from alpine run ls /sys/fs/cgroup _EOF run_buildah build $WITH_POLICY_JSON --file ${mytmpdir}/Containerfile . expect_output --substring "cpu" expect_output --substring "memory" } @test "bud with --cpu-shares, checked" { skip_if_chroot skip_if_rootless_environment skip_if_no_runtime _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p ${mytmpdir} cat > $mytmpdir/Containerfile << _EOF FROM alpine RUN printf "weight " && cat /sys/fs/cgroup/\$(awk -F : '{print \$NF}' /proc/self/cgroup)/cpu.weight _EOF for shares in 2 200 2000 12345 20000 200000 ; do local converted="$(convert_v1_shares_to_v2_weight ${shares})" local expect="(weight ${converted##* }|weight ${converted%% *})" echo requesting "${shares}" shares run_buildah build --cpu-shares=${shares} -t testcpu \ $WITH_POLICY_JSON --file ${mytmpdir}/Containerfile . echo expected "${expect}" expect_output --from="${lines[2]}" --substring "${expect}" done } @test "bud with --cpuset-cpus" { skip_if_chroot skip_if_rootless_environment skip_if_no_runtime _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p ${mytmpdir} cat > $mytmpdir/Containerfile << _EOF from alpine run printf "cpuset-cpus " && cat /sys/fs/cgroup/\$(awk -F : '{print \$NF}' /proc/self/cgroup)/cpuset.cpus _EOF run_buildah build --cpuset-cpus=0 -t testcpuset \ $WITH_POLICY_JSON --file ${mytmpdir}/Containerfile . expect_output --from="${lines[2]}" "cpuset-cpus 0" } @test "bud with --cpuset-mems" { skip_if_chroot skip_if_rootless_environment skip_if_no_runtime _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p ${mytmpdir} cat > $mytmpdir/Containerfile << _EOF from alpine run printf "cpuset-mems " && cat /sys/fs/cgroup/\$(awk -F : '{print \$NF}' /proc/self/cgroup)/cpuset.mems _EOF run_buildah build --cpuset-mems=0 -t testcpuset \ $WITH_POLICY_JSON --file ${mytmpdir}/Containerfile . expect_output --from="${lines[2]}" "cpuset-mems 0" } @test "bud with --isolation" { skip_if_rootless_environment skip_if_no_runtime test -z "${BUILDAH_ISOLATION}" || skip "BUILDAH_ISOLATION=${BUILDAH_ISOLATION} overrides --isolation" _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p ${mytmpdir} cat > $mytmpdir/Containerfile << _EOF from alpine run readlink /proc/self/ns/pid _EOF run readlink /proc/self/ns/pid host_pidns=$output run_buildah build --isolation chroot -t testisolation --pid private \ $WITH_POLICY_JSON --file ${mytmpdir}/Containerfile . # chroot isolation doesn't make a new PID namespace. expect_output --from="${lines[2]}" "${host_pidns}" } @test "bud with --pull-always" { _prefetch docker.io/library/alpine run_buildah build --pull-always $WITH_POLICY_JSON -t testpull $BUDFILES/containerfile expect_output --substring "Trying to pull docker.io/library/alpine:latest..." run_buildah build --pull=always $WITH_POLICY_JSON -t testpull $BUDFILES/containerfile expect_output --substring "Trying to pull docker.io/library/alpine:latest..." } @test "bud with --memory and --memory-swap" { skip_if_chroot skip_if_no_runtime skip_if_rootless_environment _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p ${mytmpdir} cat > $mytmpdir/Containerfile << _EOF from alpine run printf "memory-max=" && cat /sys/fs/cgroup/\$(awk -F : '{print \$NF}' /proc/self/cgroup)/memory.max run printf "memory-swap-result=" && cat /sys/fs/cgroup/\$(awk -F : '{print \$NF}' /proc/self/cgroup)/memory.swap.max _EOF run_buildah build --memory=40m --memory-swap=70m -t testmemory \ $WITH_POLICY_JSON --file ${mytmpdir}/Containerfile . expect_output --from="${lines[2]}" "memory-max=41943040" expect_output --from="${lines[4]}" "memory-swap-result=31457280" } @test "bud with --shm-size" { skip_if_chroot skip_if_no_runtime _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p ${mytmpdir} cat > $mytmpdir/Containerfile << _EOF from alpine run df -h /dev/shm _EOF run_buildah build --shm-size=80m -t testshm \ $WITH_POLICY_JSON --file ${mytmpdir}/Containerfile . expect_output --from="${lines[3]}" --substring "shm\s+80.0M" } @test "bud with --ulimit" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p ${mytmpdir} cat > $mytmpdir/Containerfile << _EOF from alpine run printf "ulimit=" && ulimit -t _EOF run_buildah build --ulimit cpu=300 -t testulimit \ $WITH_POLICY_JSON --file ${mytmpdir}/Containerfile . expect_output --from="${lines[2]}" "ulimit=300" } @test "bud with .dockerignore #3" { run_buildah build -t test $WITH_POLICY_JSON $BUDFILES/copy-globs run_buildah build -t test2 -f Containerfile.missing $WITH_POLICY_JSON $BUDFILES/copy-globs run_buildah 125 build -t test3 -f Containerfile.bad $WITH_POLICY_JSON $BUDFILES/copy-globs expect_output --substring 'building.*"COPY \*foo /testdir".*no such file or directory' } @test "bud with copy --exclude" { run_buildah build -t test $WITH_POLICY_JSON $BUDFILES/copy-exclude assert "$output" !~ "test1.txt" run_buildah build -t test2 -f Containerfile.missing $WITH_POLICY_JSON $BUDFILES/copy-exclude assert "$output" !~ "test2.txt" } @test "bud with copy --parents" { run_buildah build -t test $WITH_POLICY_JSON $BUDFILES/copy-parents assert "$output" =~ "\/no_parents\/a\.txt" assert "$output" =~ "\/parents\/x\/a\.txt" assert "$output" =~ "\/parents\/y\/a\.txt" assert "$output" =~ "\/parents_file_point\/y\/b\.txt" assert "$output" =~ "\/parents_file_point\/y\/a\.txt" assert "$output" =~ "\/parents_dir_point\/y\/b\.txt" assert "$output" =~ "\/parents_dir_point\/y\/a\.txt" } @test "bud-copy--parents-links" { target=image run_buildah build $WITH_POLICY_JSON -t ${target} -f $BUDFILES/copy-parents/Containerfile-hardlinks $BUDFILES/copy-parents run_buildah from ${target} ctrid="$output" run_buildah mount "$ctrid" root="$output" for dir in parents/x parents_dir_point ;do # Target of symlink-a.txt is not changed when using --parents with pivot point # Docker does not change the symlink target when using --parents with pivot point run stat -c "%N" ${root}/${dir}/z/symlink-b.txt expect_output "'${root}/${dir}/z/symlink-b.txt' -> '/x/z/b.txt'" "symlink-b.txt: symlink to b.txt" run stat -c "%d:%i" ${root}/${dir}/y/b.txt file_b1=$output run stat -c "%h" ${root}/${dir}/y/b.txt expect_output 2 "b.txt: number of hardlinks" run stat -c "%d:%i" ${root}/${dir}/z/hardlink-y-b.txt expect_output $file_b1 "stat(hardlink-y-b.txt) == stat(b.txt)" run stat -c "%d:%i" ${root}/${dir}/z/a.txt file_a1=$output run stat -c "%h" ${root}/${dir}/z/a.txt expect_output 2 "a.txt: number of hardlinks" run stat -c "%d:%i" ${root}/${dir}/z/hardlink-a.txt expect_output $file_a1 "stat(hardlink-a.txt) == stat(a.txt)" done } @test "bud with containerfile secret" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir1 mkdir -p ${mytmpdir} cat > $mytmpdir/mysecret << _EOF SOMESECRETDATA _EOF run_buildah build --secret=id=mysecret,src=${mytmpdir}/mysecret $WITH_POLICY_JSON -t secretimg -f $BUDFILES/run-mounts/Dockerfile.secret $BUDFILES/run-mounts expect_output --substring "SOMESECRETDATA" run_buildah from secretimg run_buildah 1 run secretimg-working-container cat /run/secrets/mysecret expect_output --substring "cat: can't open '/run/secrets/mysecret': No such file or directory" run_buildah rm -a } @test "bud with containerfile secret and secret is accessed twice and build should be successful" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir1 mkdir -p ${mytmpdir} cat > $mytmpdir/mysecret << _EOF SOMESECRETDATA _EOF cat > $mytmpdir/Dockerfile << _EOF FROM alpine RUN --mount=type=secret,id=mysecret,dst=/home/root/mysecret cat /home/root/mysecret RUN --mount=type=secret,id=mysecret,dst=/home/root/mysecret2 echo hello && cat /home/root/mysecret2 _EOF run_buildah build --secret=id=mysecret,src=${mytmpdir}/mysecret $WITH_POLICY_JSON -t secretimg -f ${mytmpdir}/Dockerfile expect_output --substring "hello" expect_output --substring "SOMESECRETDATA" } @test "bud with containerfile secret accessed on second RUN" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir1 mkdir -p ${mytmpdir} cat > $mytmpdir/mysecret << _EOF SOMESECRETDATA _EOF run_buildah 1 bud --secret=id=mysecret,src=${mytmpdir}/mysecret $WITH_POLICY_JSON -t secretimg -f $BUDFILES/run-mounts/Dockerfile.secret-access $BUDFILES/run-mounts expect_output --substring "SOMESECRETDATA" expect_output --substring "cat: can't open '/mysecret': No such file or directory" run_buildah 1 bud --no-cache --layers --secret=id=mysecret,src=${mytmpdir}/mysecret $WITH_POLICY_JSON -t secretimg -f $BUDFILES/run-mounts/Dockerfile.secret-access $BUDFILES/run-mounts expect_output --substring "SOMESECRETDATA" expect_output --substring "cat: can't open '/mysecret': No such file or directory" } @test "bud with default mode perms" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir1 mkdir -p ${mytmpdir} cat > $mytmpdir/mysecret << _EOF SOMESECRETDATA _EOF run_buildah bud --secret=id=mysecret,src=${mytmpdir}/mysecret,type=file $WITH_POLICY_JSON -t secretmode -f $BUDFILES/run-mounts/Dockerfile.secret-mode $BUDFILES/run-mounts expect_output --substring "400" } @test "bud with containerfile secret options" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir1 mkdir -p ${mytmpdir} cat > $mytmpdir/mysecret << _EOF SOMESECRETDATA _EOF run_buildah build --secret=id=mysecret,src=${mytmpdir}/mysecret $WITH_POLICY_JSON -t secretopts -f $BUDFILES/run-mounts/Dockerfile.secret-options $BUDFILES/run-mounts expect_output --substring "444" expect_output --substring "1000" expect_output --substring "1001" } @test "bud with containerfile secret not required" { _prefetch alpine run_buildah build $WITH_POLICY_JSON -t secretnotreq -f $BUDFILES/run-mounts/Dockerfile.secret-not-required $BUDFILES/run-mounts run_buildah 1 build $WITH_POLICY_JSON -t secretnotreq -f $BUDFILES/run-mounts/Dockerfile.secret-required-false $BUDFILES/run-mounts expect_output --substring "No such file or directory" assert "$output" !~ "secret required but no secret with id mysecret found" } @test "bud with containerfile secret required" { _prefetch alpine run_buildah 125 build $WITH_POLICY_JSON -t secretreq -f $BUDFILES/run-mounts/Dockerfile.secret-required $BUDFILES/run-mounts expect_output --substring 'secret required but no secret with id "mysecret" found' # Also test secret required without value run_buildah 125 build $WITH_POLICY_JSON -t secretreq -f $BUDFILES/run-mounts/Dockerfile.secret-required-wo-value $BUDFILES/run-mounts expect_output --substring 'secret required but no secret with id "mysecret" found' } @test "bud with containerfile env secret" { _prefetch alpine export MYSECRET=SOMESECRETDATA run_buildah build --secret=id=mysecret,src=MYSECRET,type=env $WITH_POLICY_JSON -t secretimg -f $BUDFILES/run-mounts/Dockerfile.secret $BUDFILES/run-mounts expect_output --substring "SOMESECRETDATA" run_buildah from secretimg run_buildah 1 run secretimg-working-container cat /run/secrets/mysecret expect_output --substring "cat: can't open '/run/secrets/mysecret': No such file or directory" run_buildah rm -a run_buildah build --secret=id=mysecret,env=MYSECRET $WITH_POLICY_JSON -t secretimg -f $BUDFILES/run-mounts/Dockerfile.secret $BUDFILES/run-mounts expect_output --substring "SOMESECRETDATA" run_buildah from secretimg run_buildah 1 run secretimg-working-container cat /run/secrets/mysecret expect_output --substring "cat: can't open '/run/secrets/mysecret': No such file or directory" run_buildah 125 build --secret=id=mysecret2,env=MYSECRET,true=false $WITH_POLICY_JSON -f $BUDFILES/run-mounts/Dockerfile.secret $BUDFILES/run-mounts expect_output --substring "incorrect secret flag format" run_buildah rm -a } @test "bud with containerfile env secret priority" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir1 mkdir -p ${mytmpdir} cat > $mytmpdir/mysecret << _EOF SOMESECRETDATA _EOF export mysecret=ENVDATA run_buildah build --secret=id=mysecret $WITH_POLICY_JSON -t secretimg -f $BUDFILES/run-mounts/Dockerfile.secret $BUDFILES/run-mounts expect_output --substring "ENVDATA" } @test "bud-multiple-platform-values" { skip "FIXME: #4396 - this test is broken, and is failing gating tests" outputlist=testlist # check if we can run a couple of 32-bit versions of an image, and if we can, # assume that emulation for other architectures is in place. os=`go env GOOS` run_buildah from $WITH_POLICY_JSON --name try-386 --platform=$os/386 alpine run_buildah '?' run try-386 true if test $status -ne 0 ; then skip "unable to run 386 container, assuming emulation is not available" fi run_buildah from $WITH_POLICY_JSON --name try-arm --platform=$os/arm alpine run_buildah '?' run try-arm true if test $status -ne 0 ; then skip "unable to run arm container, assuming emulation is not available" fi # build for those architectures - RUN gets exercised run_buildah build $WITH_POLICY_JSON --jobs=0 --platform=$os/arm,$os/386 --manifest $outputlist $BUDFILES/multiarch run_buildah manifest inspect $outputlist list="$output" run jq -r '.manifests[0].digest' <<< "$list" d1="$output" run jq -r '.manifests[1].digest' <<< "$list" d2="$output" assert "$d1" =~ "^sha256:[0-9a-f]{64}\$" assert "$d2" =~ "^sha256:[0-9a-f]{64}\$" assert "$d1" != "$d2" "digest(arm) should != digest(386)" } @test "bud-multiple-platform-no-partial-manifest-list" { outputlist=localhost/testlist run_buildah 1 bud $WITH_POLICY_JSON --platform=linux/arm,linux/amd64 --manifest $outputlist -f $BUDFILES/multiarch/Dockerfile.fail $BUDFILES/multiarch expect_output --substring "building at STEP \"RUN test .arch. = x86_64" run_buildah 125 manifest inspect $outputlist expect_output --substring "reading image .* pinging container registry" } @test "bud-multiple-platform-failure" { # check if we can run a couple of 32-bit versions of an image, and if we can, # assume that emulation for other architectures is in place. os=$(go env GOOS) if [[ "$os" != linux ]]; then skip "GOOS is '$os'; this test can only run on linux" fi run_buildah from $WITH_POLICY_JSON --name try-386 --platform=$os/386 alpine run_buildah '?' run try-386 true if test $status -ne 0 ; then skip "unable to run 386 container, assuming emulation is not available" fi run_buildah from $WITH_POLICY_JSON --name try-arm --platform=$os/arm alpine run_buildah '?' run try-arm true if test $status -ne 0 ; then skip "unable to run arm container, assuming emulation is not available" fi outputlist=localhost/testlist run_buildah 1 build $WITH_POLICY_JSON \ --jobs=0 \ --platform=linux/arm64,linux/amd64 \ --manifest $outputlist \ --build-arg SAFEIMAGE=$SAFEIMAGE \ -f $BUDFILES/multiarch/Dockerfile.fail-multistage \ $BUDFILES/multiarch expect_output --substring 'building at STEP "RUN false"' } @test "bud-multiple-platform-no-run" { outputlist=localhost/testlist run_buildah build $WITH_POLICY_JSON \ --jobs=0 \ --all-platforms \ --manifest $outputlist \ --build-arg SAFEIMAGE=$SAFEIMAGE \ -f $BUDFILES/multiarch/Dockerfile.no-run \ $BUDFILES/multiarch run_buildah manifest inspect $outputlist manifests=$(jq -r '.manifests[].platform.architecture' <<<"$output" |sort|fmt) assert "$manifests" = "amd64 arm64 ppc64le s390x" "arch list in manifest" } # attempts to resolve heading arg as base-image with --all-platforms @test "bud-multiple-platform-with-base-as-default-arg" { outputlist=localhost/testlist run_buildah build $WITH_POLICY_JSON \ --jobs=1 \ --all-platforms \ --manifest $outputlist \ -f $BUDFILES/all-platform/Containerfile.default-arg \ $BUDFILES/all-platform run_buildah manifest inspect $outputlist manifests=$(jq -r '.manifests[].platform.architecture' <<<"$output" |sort|fmt) assert "$manifests" = "386 amd64 arm arm arm64 ppc64le s390x" "arch list in manifest" } @test "bud-multiple-platform-with-base-as-arg-with-tag-and-digest" { outputlist=localhost/testlist run_buildah build $WITH_POLICY_JSON \ --build-arg=foo=${SAFEIMAGE}@${SAFEIMAGE_DIGEST} \ --all-platforms \ --manifest $outputlist \ -f $BUDFILES/all-platform/Containerfile.default-arg \ $BUDFILES/all-platform run_buildah manifest inspect $outputlist manifests=$(jq -r '.manifests[].platform.architecture' <<< "$output" | sort | fmt) assert "$manifests" = "amd64 arm64 ppc64le s390x" "arch list in manifest did not match ${SAFEIMAGE}@${SAFEIMAGE_DIGEST}" } @test "bud-multiple-platform for --all-platform with additional-build-context" { outputlist=localhost/testlist local contextdir=${TEST_SCRATCH_DIR}/bud/platform mkdir -p $contextdir cat > $contextdir/Dockerfile1 << _EOF FROM busybox _EOF # Pulled images must be $SAFEIMAGE since we configured --build-context run_buildah build $WITH_POLICY_JSON --all-platforms --build-context busybox=docker://$SAFEIMAGE --manifest $outputlist -f $contextdir/Dockerfile1 # must contain pulling logs for $SAFEIMAGE instead of busybox expect_output --substring "STEP 1/1: FROM $SAFEIMAGE" assert "$output" =~ "\[linux/s390x\] COMMIT" assert "$output" =~ "\[linux/ppc64le\] COMMIT" assert "$output" !~ "busybox" # Confirm the manifests and their architectures. It is not possible for # this to change, unless we bump $SAFEIMAGE to a new versioned tag. run_buildah manifest inspect $outputlist manifests=$(jq -r '.manifests[].platform.architecture' <<<"$output" |sort|fmt) assert "$manifests" = "amd64 arm64 ppc64le s390x" "arch list in manifest" } @test "bud-targetplatform-as-build-arg" { outputlist=localhost/testlist for targetplatform in linux/arm64 linux/amd64 ; do run_buildah build $WITH_POLICY_JSON \ --build-arg SAFEIMAGE=$SAFEIMAGE \ --build-arg TARGETPLATFORM=$targetplatform \ -f $BUDFILES/multiarch/Dockerfile.built-in-args \ $BUDFILES/multiarch expect_output --substring "I'm compiling for $targetplatform" done } @test "build must reset platform for stages if needed" { run_buildah info --format '{{.host.arch}}' myarch="$output" otherarch="arm64" # just make sure that other arch is not equivalent to host arch if [[ "$otherarch" == "$myarch" ]]; then otherarch="amd64" fi run_buildah build $WITH_POLICY_JSON --build-arg FOREIGNARCH=$otherarch -f $BUDFILES/multiarch/Containerfile.reset-platform $BUDFILES/multiarch run_buildah build $WITH_POLICY_JSON --build-arg TARGETPLATFORM=linux/$myarch --build-arg FOREIGNARCH=$otherarch -f $BUDFILES/multiarch/Containerfile.reset-platform $BUDFILES/multiarch } # * Performs multi-stage build with label1=value1 and verifies # * Relabels build with label1=value2 and verifies # * Rebuild with label1=value1 and makes sure everything is used from cache @test "bud-multistage-relabel" { _prefetch alpine busybox run_buildah inspect --format "{{.FromImageDigest}}" busybox fromDigest="$output" target=relabel run_buildah build --layers --label "label1=value1" $WITH_POLICY_JSON -t ${target} -f $BUDFILES/multi-stage-builds/Dockerfile.reused $BUDFILES/multi-stage-builds # Store base digest of first image run_buildah inspect --format '{{index .ImageAnnotations "org.opencontainers.image.base.digest" }}' ${target} firstDigest="$output" # Store image id of first build run_buildah inspect --format '{{ .FromImageID }}' ${target} firstImageID="$output" # Label of first build must contain label1:value1 run_buildah inspect --format '{{ .Docker.ContainerConfig.Labels }}' ${target} expect_output --substring "label1:value1" # Rebuild with new label run_buildah build --layers --label "label1=value2" $WITH_POLICY_JSON -t ${target} -f $BUDFILES/multi-stage-builds/Dockerfile.reused $BUDFILES/multi-stage-builds # Base digest should match with first build run_buildah inspect --format '{{index .ImageAnnotations "org.opencontainers.image.base.digest" }}' ${target} expect_output "$firstDigest" "base digest from busybox" # Label of second build must contain label1:value2 run_buildah inspect --format '{{ .Docker.ContainerConfig.Labels }}' ${target} expect_output --substring "label1:value2" # Rebuild everything with label1=value1 and everything should be cached from first image run_buildah build --layers --label "label1=value1" $WITH_POLICY_JSON -t ${target} -f $BUDFILES/multi-stage-builds/Dockerfile.reused $BUDFILES/multi-stage-builds # Entire image must be picked from cache run_buildah inspect --format '{{ .FromImageID }}' ${target} expect_output "$firstImageID" "Image ID cached from first build" } @test "bud-from-relabel" { _prefetch alpine busybox run_buildah inspect --format "{{.FromImageDigest}}" alpine alpineDigest="$output" run_buildah inspect --format "{{.FromImageDigest}}" busybox busyboxDigest="$output" target=relabel2 run_buildah build --layers --label "label1=value1" --from=alpine -t ${target} $BUDFILES/from-scratch run_buildah inspect --format '{{index .ImageAnnotations "org.opencontainers.image.base.digest" }}' ${target} expect_output "$alpineDigest" "base digest from alpine" # Label of second build must contain label1:value1 run_buildah inspect --format '{{ .Docker.ContainerConfig.Labels }}' ${target} expect_output --substring "label1:value1" run_buildah build --layers --label "label1=value2" --from=busybox -t ${target} $BUDFILES/from-scratch run_buildah inspect --format '{{index .ImageAnnotations "org.opencontainers.image.base.digest" }}' ${target} expect_output "$busyboxDigest" "base digest from busybox" # Label of second build must contain label1:value2 run_buildah inspect --format '{{ .Docker.ContainerConfig.Labels }}' ${target} expect_output --substring "label1:value2" } @test "bud with run should not leave mounts behind cleanup test" { skip_if_in_container skip_if_no_podman _prefetch alpine # Create target dir where we will export tar target=cleanable local contextdir=${TEST_SCRATCH_DIR}/${target} mkdir $contextdir # Build and export container to tar run_buildah build --no-cache $WITH_POLICY_JSON -t ${target} -f $BUDFILES/containerfile/Containerfile.in $BUDFILES/containerfile podman export $(podman create --name ${target} --net=host ${target}) --output=$contextdir.tar # We are done exporting so remove images and containers which are not needed podman rm -f ${target} run_buildah rmi ${target} # Explode tar tar -xf $contextdir.tar -C $contextdir count=$(ls -A $contextdir/run | wc -l) ## exported /run should be empty assert "$count" == "0" } @test "bud with custom files in /run/ should persist cleanup test" { skip_if_in_container skip_if_no_podman _prefetch alpine # Create target dir where we will export tar target=cleanable local contextdir=${TEST_SCRATCH_DIR}/${target} mkdir $contextdir # Build and export container to tar run_buildah build --no-cache $WITH_POLICY_JSON -t ${target} -f $BUDFILES/add-run-dir/Dockerfile podman export $(podman create --name ${target} --net=host ${target}) --output=$contextdir.tar # We are done exporting so remove images and containers which are not needed podman rm -f ${target} run_buildah rmi ${target} # Explode tar tar -xf $contextdir.tar -C $contextdir count=$(ls -A $contextdir/run | wc -l) ## exported /run should not be empty assert "$count" == "1" } @test "bud-with-mount-like-buildkit" { skip_if_no_runtime skip_if_in_container _prefetch alpine local contextdir=$BUDFILES/buildkit-mount run_buildah build -t testbud $WITH_POLICY_JSON -f $contextdir/Dockerfile $contextdir/ expect_output --substring "hello" } @test "bud-with-mount-no-source-like-buildkit" { skip_if_no_runtime skip_if_in_container _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/buildkit-mount cp -R $BUDFILES/buildkit-mount $contextdir run_buildah build -t testbud $WITH_POLICY_JSON -f $contextdir/Dockerfile2 $contextdir/ expect_output --substring "hello" } @test "bud-with-mount-with-only-target-like-buildkit" { skip_if_no_runtime skip_if_in_container _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/buildkit-mount cp -R $BUDFILES/buildkit-mount $contextdir run_buildah build -t testbud $WITH_POLICY_JSON -f $contextdir/Dockerfile6 $contextdir/ expect_output --substring "hello" } @test "bud-with-mount-no-subdir-like-buildkit" { skip_if_no_runtime skip_if_in_container _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/buildkit-mount cp -R $BUDFILES/buildkit-mount $contextdir run_buildah build -t testbud $WITH_POLICY_JSON -f $contextdir/Dockerfile $contextdir/subdir/ expect_output --substring "hello" } @test "bud-with-mount-relative-path-like-buildkit" { skip_if_no_runtime skip_if_in_container _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/buildkit-mount cp -R $BUDFILES/buildkit-mount $contextdir run_buildah build -t testbud $WITH_POLICY_JSON -f $contextdir/Dockerfile4 $contextdir/ expect_output --substring "hello" } @test "bud-with-mount-relative-path-like-buildkit-arg-in-source" { skip_if_no_runtime skip_if_in_container _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/buildkit-mount cp -R $BUDFILES/buildkit-mount $contextdir run_buildah build --build-arg INPUTPATH_1=subdir -t testbud $WITH_POLICY_JSON -f $contextdir/Containerfile5 $contextdir/ expect_output --substring "hello" } @test "bud-with-mount-with-rw-like-buildkit" { skip_if_no_runtime skip_if_in_container _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/buildkit-mount cp -R $BUDFILES/buildkit-mount $contextdir run_buildah build --isolation chroot -t testbud $WITH_POLICY_JSON -f $contextdir/Dockerfile3 $contextdir/subdir/ expect_output --substring "world" } @test "bud-verify-if-we-dont-clean-preexisting-path" { skip_if_no_runtime skip_if_in_container _prefetch alpine ubuntu run_buildah 1 build -t testbud $WITH_POLICY_JSON --secret id=secret-foo,src=$BUDFILES/verify-cleanup/secret1.txt $BUDFILES/verify-cleanup/ expect_output --substring "hello" expect_output --substring "secrettext" expect_output --substring "Directory /tmp exists." expect_output --substring "Directory /var/tmp exists." expect_output --substring "Directory /testdir DOES NOT exist." expect_output --substring "Cache Directory /cachedir DOES NOT exist." expect_output --substring "Secret File secret1.txt DOES NOT exist." expect_output --substring "/tmp/hey: No such file or directory" } @test "bud-with-mount-with-tmpfs-like-buildkit" { skip_if_no_runtime skip_if_in_container _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/buildkit-mount cp -R $BUDFILES/buildkit-mount $contextdir # tmpfs mount: target should be available on container without creating any special directory on container run_buildah build -t testbud $WITH_POLICY_JSON -f $contextdir/Dockerfiletmpfs } @test "bud-with-mount-with-tmpfs-with-copyup-like-buildkit" { skip_if_no_runtime skip_if_in_container _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/buildkit-mount cp -R $BUDFILES/buildkit-mount $contextdir run_buildah build -t testbud $WITH_POLICY_JSON -f $contextdir/Dockerfiletmpfscopyup expect_output --substring "certs" } @test "bud-with-mount-cache-like-buildkit" { skip_if_no_runtime skip_if_in_container _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/buildkit-mount cp -R $BUDFILES/buildkit-mount $contextdir # Use a private TMPDIR so type=cache tests can run in parallel # try writing something to persistent cache TMPDIR=${TEST_SCRATCH_DIR} run_buildah build -t testbud $WITH_POLICY_JSON -f $contextdir/Dockerfilecachewrite # try reading something from persistent cache in a different build TMPDIR=${TEST_SCRATCH_DIR} run_buildah build -t testbud2 $WITH_POLICY_JSON -f $contextdir/Dockerfilecacheread expect_output --substring "hello" } @test "bud-with-mount-cache-like-buildkit with buildah prune should clear the cache" { skip_if_no_runtime skip_if_in_container _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/buildkit-mount cp -R $BUDFILES/buildkit-mount $contextdir # try writing something to persistent cache TMPDIR=${TEST_SCRATCH_DIR} run_buildah build -t testbud $WITH_POLICY_JSON -f $contextdir/Dockerfilecachewrite # prune the mount cache TMPDIR=${TEST_SCRATCH_DIR} run_buildah prune # try reading something from persistent cache in a different build TMPDIR=${TEST_SCRATCH_DIR} run_buildah 1 build -t testbud2 $WITH_POLICY_JSON -f $contextdir/Dockerfilecacheread expect_output --substring "No such file or directory" } @test "bud-with-mount-cache-like-buildkit-verify-default-selinux-option" { skip_if_no_runtime skip_if_in_container _prefetch alpine # try writing something to persistent cache TMPDIR=${TEST_SCRATCH_DIR} run_buildah build -t testbud $WITH_POLICY_JSON -f $BUDFILES/buildkit-mount/Dockerfilecachewritewithoutz # try reading something from persistent cache in a different build TMPDIR=${TEST_SCRATCH_DIR} run_buildah build -t testbud2 $WITH_POLICY_JSON -f $BUDFILES/buildkit-mount/Dockerfilecachereadwithoutz buildah_cache_dir="${TEST_SCRATCH_DIR}/buildah-cache-$UID" # buildah cache parent must have been created for our uid specific to this test test -d "$buildah_cache_dir" expect_output --substring "hello" } @test "bud-with-mount-cache-like-buildkit-locked-across-steps" { # Note: this test is just testing syntax for sharing, actual behaviour test needs parallel build in order to test locking. skip_if_no_runtime skip_if_in_container _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/buildkit-mount cp -R $BUDFILES/buildkit-mount $contextdir # try writing something to persistent cache TMPDIR=${TEST_SCRATCH_DIR} run_buildah build -t testbud $WITH_POLICY_JSON -f $contextdir/Dockerfilecachewritesharing expect_output --substring "world" } @test "bud-with-multiple-mount-keeps-default-bind-mount" { skip_if_no_runtime skip_if_in_container _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/buildkit-mount cp -R $BUDFILES/buildkit-mount $contextdir run_buildah build -t testbud $WITH_POLICY_JSON -f $contextdir/Dockerfilemultiplemounts $contextdir/ expect_output --substring "hello" } @test "bud with user in groups" { target=bud-group run_buildah build $WITH_POLICY_JSON -t ${target} $BUDFILES/group } @test "build proxy" { _prefetch alpine mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p $mytmpdir cat > $mytmpdir/Containerfile << _EOF FROM alpine run printenv _EOF target=env-image check="FTP_PROXY="FTP" ftp_proxy=ftp http_proxy=http HTTPS_PROXY=HTTPS" bogus="BOGUS_PROXY=BOGUS" eval $check $bogus run_buildah build --unsetenv PATH $WITH_POLICY_JSON -t oci-${target} -f $mytmpdir/Containerfile . for i in $check; do expect_output --substring "$i" "Environment variables available within build" done if [ -n "$(grep "$bogus" <<< "$output")" ]; then die "Unexpected bogus environment." fi } @test "build proxy - ADD URL" { _prefetch alpine target=alpine-image http_proxy=http://127.0.0.1:47 HTTPS_PROXY=http://127.0.0.1:47 \ run_buildah 125 build $WITH_POLICY_JSON -t alpine-image -f $BUDFILES/add-checksum/Containerfile $BUDFILES/add-checksum expect_output --substring "127.0.0.1:47" "connection to fake proxy must fail" expect_output --substring "connection refused" "connection to fake proxy must fail" } @test "bud-with-mount-bind-from-like-buildkit" { skip_if_no_runtime skip_if_in_container _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/buildkit-mount-from cp -R $BUDFILES/buildkit-mount-from $contextdir # build base image which we will use as our `from` run_buildah build -t buildkitbase $WITH_POLICY_JSON -f $contextdir/Dockerfilebuildkitbase $contextdir/ # try reading something from another image in a different build run_buildah build -t testbud $WITH_POLICY_JSON -f $contextdir/Dockerfilebindfrom expect_output --substring "hello" } @test "bud-with-writeable-mount-bind-from-like-buildkit" { skip_if_no_runtime skip_if_in_container _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/buildkit-mount-from cp -R $BUDFILES/buildkit-mount-from $contextdir # build base image which we will use as our `from` run_buildah build -t buildkitbase $WITH_POLICY_JSON -f $contextdir/Dockerfilebuildkitbase $contextdir/ # try reading something from another image in a different build run_buildah build -t testbud $WITH_POLICY_JSON -f $contextdir/Dockerfilebindfromwriteable expect_output --substring "world" } @test "bud-with-mount-bind-from-without-source-like-buildkit" { skip_if_no_runtime skip_if_in_container _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/buildkit-mount-from cp -R $BUDFILES/buildkit-mount-from $contextdir # build base image which we will use as our `from` run_buildah build -t buildkitbase $WITH_POLICY_JSON -f $contextdir/Dockerfilebuildkitbase $contextdir/ # try reading something from another image in a different build run_buildah build -t testbud $WITH_POLICY_JSON -f $contextdir/Dockerfilebindfromwithoutsource expect_output --substring "hello" } @test "bud-with-mount-bind-from-with-empty-from-like-buildkit" { skip_if_no_runtime skip_if_in_container _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/buildkit-mount-from cp -R $BUDFILES/buildkit-mount-from $contextdir # build base image which we will use as our `from` run_buildah build -t buildkitbase $WITH_POLICY_JSON -f $contextdir/Dockerfilebuildkitbase $contextdir/ # try reading something from image in a different build run_buildah 125 build -t testbud $WITH_POLICY_JSON -f $contextdir/Dockerfilebindfromwithemptyfrom expect_output --substring "points to an empty value" } @test "bud-with-mount-cache-from-like-buildkit" { skip_if_no_runtime skip_if_in_container _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/buildkit-mount-from cp -R $BUDFILES/buildkit-mount-from $contextdir # try reading something from persistent cache in a different build TMPDIR=${TEST_SCRATCH_DIR} run_buildah build -t testbud $WITH_POLICY_JSON -f $contextdir/Dockerfilecachefrom $contextdir/ expect_output --substring "hello" } @test "bud-with-mount-cache-image-from-like-buildkit" { skip_if_no_runtime skip_if_in_container _prefetch alpine local contextdir=${BUDFILES}/buildkit-mount-from # build base image which we will use as our `from` TMPDIR=${TEST_SCRATCH_DIR} run_buildah build -t buildkitbase $WITH_POLICY_JSON -f $contextdir/Dockerfilebuildkitbase $contextdir/ # try reading something from persistent cache in a different build TMPDIR=${TEST_SCRATCH_DIR} run_buildah build -t testbud $WITH_POLICY_JSON -f $contextdir/Dockerfilecachefromimage } @test "bud-with-mount-cache-multiple-from-like-buildkit" { skip_if_no_runtime skip_if_in_container _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/buildkit-mount-from cp -R $BUDFILES/buildkit-mount-from $contextdir # try reading something from persistent cache in a different build TMPDIR=${TEST_SCRATCH_DIR} run_buildah build -t testbud $WITH_POLICY_JSON -f $contextdir/Dockerfilecachemultiplefrom $contextdir/ expect_output --substring "hello" expect_output --substring "hello2" } @test "bud-with-mount-bind-from-relative-like-buildkit" { skip_if_no_runtime skip_if_in_container _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/buildkit-mount-from cp -R $BUDFILES/buildkit-mount-from $contextdir # build base image which we will use as our `from` run_buildah build -t buildkitbaserelative $WITH_POLICY_JSON -f $contextdir/Dockerfilebuildkitbaserelative $contextdir/ # try reading something from image in a different build run_buildah build -t testbud $WITH_POLICY_JSON -f $contextdir/Dockerfilebindfromrelative expect_output --substring "hello" } @test "bud-with-mount-bind-from-multistage-relative-like-buildkit" { local contextdir=${TEST_SCRATCH_DIR}/buildkit-mount-from cp -R $BUDFILES/buildkit-mount-from $contextdir skip_if_no_runtime skip_if_in_container _prefetch alpine # build base image which we will use as our `from` run_buildah build -t testbud $WITH_POLICY_JSON -f $contextdir/Dockerfilemultistagefrom $contextdir/ expect_output --substring "hello" } @test "bud-with-mount-bind-from-cache-multistage-relative-like-buildkit" { skip_if_no_runtime skip_if_in_container _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/buildkit-mount-from cp -R $BUDFILES/buildkit-mount-from $contextdir # build base image which we will use as our `from` run_buildah build -t testbud $WITH_POLICY_JSON -f $contextdir/Dockerfilemultistagefromcache $contextdir/ expect_output --substring "hello" expect_output --substring "hello2" } @test "bud with network names" { skip_if_no_runtime skip_if_in_container skip_if_rootless_environment _prefetch alpine run_buildah 125 bud $WITH_POLICY_JSON --network notexists $BUDFILES/network expect_output --substring "network not found" if test "$BUILDAH_ISOLATION" = "oci"; then run_buildah bud $WITH_POLICY_JSON --network podman $BUDFILES/network # default subnet is 10.88.0.0/16 expect_output --substring "10.88." fi } @test "bud with --network slirp4netns" { skip_if_no_runtime skip_if_in_container skip_if_chroot _prefetch alpine run_buildah bud $WITH_POLICY_JSON --network slirp4netns $BUDFILES/network # default subnet is 10.0.2.100/24 assert "$output" =~ "10.0.2.100/24" "ip addr shows default subnet" run_buildah bud $WITH_POLICY_JSON --network slirp4netns:cidr=192.168.255.0/24,mtu=2000 $BUDFILES/network assert "$output" =~ "192.168.255.100/24" "ip addr shows custom subnet" assert "$output" =~ "mtu 2000" "ip addr shows mtu 2000" } @test "bud with --network pasta" { skip_if_no_runtime skip_if_chroot skip_if_root_environment "pasta only works rootless" _prefetch alpine # pasta by default copies the host ip ip=$(hostname -I | cut -f 1 -d " ") run_buildah bud $WITH_POLICY_JSON --network pasta $BUDFILES/network assert "$output" =~ "$ip" "ip addr shows default subnet" # check some entwork options, it accepts raw pasta(1) areguments mac="9a:dd:31:ea:92:98" run_buildah bud $WITH_POLICY_JSON --network pasta:--mtu,2000,--ns-mac-addr,"$mac" $BUDFILES/network assert "$output" =~ "$mac" "ip addr shows custom mac address" assert "$output" =~ "mtu 2000" "ip addr shows mtu 2000" } @test "bud WORKDIR owned by USER" { _prefetch alpine target=alpine-image ctr=alpine-ctr run_buildah build $WITH_POLICY_JSON -t ${target} $BUDFILES/workdir-user expect_output --substring "1000:1000 /home/http/public" } function build_signalled { skip_if_no_runtime _prefetch alpine mkfifo ${TEST_SCRATCH_DIR}/pipe # start the build running in the background - don't use the function wrapper because that sets '$!' to a value that's not what we want ${BUILDAH_BINARY} ${BUILDAH_REGISTRY_OPTS} ${ROOTDIR_OPTS} $WITH_POLICY_JSON build $BUDFILES/long-sleep > ${TEST_SCRATCH_DIR}/pipe 2>&1 & buildah_pid="${!}" echo buildah is pid ${buildah_pid} # save what's written to the fifo to a plain file coproc cat ${TEST_SCRATCH_DIR}/pipe > ${TEST_SCRATCH_DIR}/log cat_pid="${COPROC_PID}" echo cat is pid ${cat_pid} # kill the buildah process early sleep 30 kill -s ${1} "${buildah_pid}" # wait for output to stop getting written from anywhere wait "${buildah_pid}" "${cat_pid}" echo log: cat ${TEST_SCRATCH_DIR}/log echo checking: ! grep 'not fully killed' ${TEST_SCRATCH_DIR}/log } @test "build interrupted" { build_signalled SIGINT } @test "build terminated" { build_signalled SIGTERM } @test "build killed" { build_signalled SIGKILL } @test "build-multiple-parse" { _prefetch alpine echo 'FROM alpine' | tee ${TEST_SCRATCH_DIR}/Dockerfile1 echo '# escape=|\nFROM alpine' | tee ${TEST_SCRATCH_DIR}/Dockerfile2 run_buildah 125 build -f ${TEST_SCRATCH_DIR}/Dockerfile1 -f ${TEST_SCRATCH_DIR}/Dockerfile2 ${TEST_SCRATCH_DIR} assert "$output" =~ "parsing additional Dockerfile .*Dockerfile2: invalid ESCAPE" } @test "build-with-network-test" { skip_if_in_container # Test only works in OCI isolation, which doesn't work in CI/CD systems. Buildah defaults to chroot isolation image="quay.io/libpod/alpine_nginx:latest" _prefetch $image cat > ${TEST_SCRATCH_DIR}/Containerfile << _EOF FROM $image RUN curl -k -o /dev/null http://www.redhat.com:80 _EOF # curl results show success run_buildah build ${WITH_POLICY_JSON} ${TEST_SCRATCH_DIR} # A proper test would use ping or nc, and check for ENETUNREACH. # But in a tightly firewalled environment, even the expected-success # test will fail. A not-quite-equivalent workaround is to use curl # and hope that $http_proxy is set; we then rely on curl to fail # in a slightly different way expect_rc=6 expect_err="Could not resolve host: www.redhat.com" if [[ $http_proxy != "" ]]; then expect_rc=5 expect_err="Could not resolve proxy:" fi run_buildah $expect_rc build --network=none ${WITH_POLICY_JSON} ${TEST_SCRATCH_DIR} expect_output --substring "$expect_err" } @test "build-with-no-new-privileges-test" { _prefetch alpine cat > ${TEST_SCRATCH_DIR}/Containerfile << _EOF FROM alpine RUN grep NoNewPrivs /proc/self/status _EOF run_buildah build --security-opt no-new-privileges $WITH_POLICY_JSON ${TEST_SCRATCH_DIR} expect_output --substring "NoNewPrivs:.*1" } @test "build --group-add" { skip_if_no_runtime id=$RANDOM _prefetch alpine cat > ${TEST_SCRATCH_DIR}/Containerfile << _EOF FROM alpine RUN id -G _EOF run_buildah build --group-add $id $WITH_POLICY_JSON ${TEST_SCRATCH_DIR} expect_output --substring "$id" if is_rootless && has_supplemental_groups && ! [[ $OCI =~ runc ]]; then run_buildah build --group-add keep-groups $WITH_POLICY_JSON ${TEST_SCRATCH_DIR} expect_output --substring "65534" else skip "test is being run by root or without any supplemental groups ($(id))" fi } @test "build-env-precedence" { skip_if_no_runtime _prefetch alpine run_buildah build --no-cache --env E=F --env G=H --env I=J --env K=L -f ${BUDFILES}/env/Dockerfile.env-precedence ${BUDFILES}/env expect_output --substring "a=b c=d E=F G=H" expect_output --substring "a=b c=d E=E G=G" expect_output --substring "w=x y=z I=J K=L" expect_output --substring "w=x y=z I=I K=K" run_buildah build --no-cache --layers --env E=F --env G=H --env I=J --env K=L -f ${BUDFILES}/env/Dockerfile.env-precedence ${BUDFILES}/env expect_output --substring "a=b c=d E=F G=H" expect_output --substring "a=b c=d E=E G=G" expect_output --substring "w=x y=z I=J K=L" expect_output --substring "w=x y=z I=I K=K" } @test "build prints 12-digit hash" { run_buildah build -t test -f $BUDFILES/containerfile/Containerfile . regex='--> [0-9a-zA-Z]{12}' if ! [[ $output =~ $regex ]]; then false fi } @test "build with name path changes" { _prefetch busybox run_buildah build --no-cache --quiet --pull=false $WITH_POLICY_JSON -t foo/bar $BUDFILES/commit/name-path-changes/ run_buildah build --no-cache --quiet --pull=false $WITH_POLICY_JSON -t bar $BUDFILES/commit/name-path-changes/ run_buildah images expect_output --substring "localhost/foo/bar" expect_output --substring "localhost/bar" } @test "build test default ulimits" { skip_if_no_runtime skip "FIXME: we cannot rely on podman-run ulimits matching buildah-bud (see #5820)" _prefetch alpine run podman --events-backend=none run --rm alpine sh -c "echo -n Files=; awk '/open files/{print \$4 \"/\" \$5}' /proc/self/limits" podman_files=$output run podman --events-backend=none run --rm alpine sh -c "echo -n Processes=; awk '/processes/{print \$3 \"/\" \$4}' /proc/self/limits" podman_processes=$output CONTAINERS_CONF=/dev/null run_buildah build --no-cache --pull=false $WITH_POLICY_JSON -t foo/bar $BUDFILES/bud.limits expect_output --substring "$podman_files" expect_output --substring "$podman_processes" } @test "build no write file on host - CVE-2024-1753" { _prefetch alpine cat > ${TEST_SCRATCH_DIR}/Containerfile << _EOF FROM alpine as base RUN ln -s / /rootdir FROM alpine # With exploit show host root, not the container's root, and create /BIND_BREAKOUT in / on the host RUN --mount=type=bind,from=base,source=/rootdir,destination=/exploit,rw ls -l /exploit; touch /exploit/BIND_BREAKOUT; ls -l /exploit _EOF run_buildah build $WITH_POLICY_JSON ${TEST_SCRATCH_DIR} expect_output --substring "/BIND_BREAKOUT" run ls /BIND_BREAKOUT rm -f /BIND_BREAKOUT assert "$status" -eq 2 "exit code from ls" expect_output --substring "No such file or directory" } @test "pull policy" { echo FROM busybox > ${TEST_SCRATCH_DIR}/Containerfile arch=amd64 if test $(arch) = x86_64 ; then arch=arm64 fi # specifying the arch should trigger "just pull it anyway" in containers/common run_buildah build --pull=missing --arch $arch --iidfile ${TEST_SCRATCH_DIR}/image1.txt ${TEST_SCRATCH_DIR} # not specifying the arch should trigger "yeah, fine, whatever we already have is fine" in containers/common run_buildah build --pull=missing --iidfile ${TEST_SCRATCH_DIR}/image2.txt ${TEST_SCRATCH_DIR} # both of these should have just been the base image's ID, which shouldn't have changed the second time around cmp ${TEST_SCRATCH_DIR}/image1.txt ${TEST_SCRATCH_DIR}/image2.txt } # Verify: https://github.com/containers/buildah/issues/5185 @test "build-test --mount=type=secret test from env with chroot isolation" { skip_if_root_environment "Need to not be root for this test to work" local contextdir=$BUDFILES/secret-env export MYSECRET=SOMESECRETDATA run_buildah build $WITH_POLICY_JSON --no-cache --isolation chroot --secret id=MYSECRET -t test -f $contextdir/Dockerfile expect_output --substring "SOMESECRETDATA" } @test "build-logs-from-platform" { run_buildah info --format '{{.host.os}}/{{.host.arch}}{{if .host.variant}}/{{.host.variant}}{{ end }}' local platform="$output" echo FROM --platform=$platform busybox > ${TEST_SCRATCH_DIR}/Containerfile run_buildah build ${TEST_SCRATCH_DIR} expect_output --substring "\-\-platform=$platform" } @test "build add https retry ca" { createrandom ${TEST_SCRATCH_DIR}/randomfile mkdir -p ${TEST_SCRATCH_DIR}/private starthttpd ${TEST_SCRATCH_DIR} "" ${TEST_SCRATCH_DIR}/localhost.crt ${TEST_SCRATCH_DIR}/private/localhost.key echo FROM scratch | tee ${TEST_SCRATCH_DIR}/Dockerfile echo ADD "https://localhost:${HTTP_SERVER_PORT}/randomfile" / | tee -a ${TEST_SCRATCH_DIR}/Dockerfile run_buildah build --retry-delay=0.142857s --retry=14 --cert-dir ${TEST_SCRATCH_DIR} ${TEST_SCRATCH_DIR} run_buildah build --retry-delay=0.142857s --retry=14 --tls-verify=false $cid ${TEST_SCRATCH_DIR} run_buildah 125 build --retry-delay=0.142857s --retry=14 $cid ${TEST_SCRATCH_DIR} assert "$output" =~ "x509: certificate signed by unknown authority" stophttpd run_buildah 125 build --retry-delay=0.142857s --retry=14 --cert-dir ${TEST_SCRATCH_DIR} $cid ${TEST_SCRATCH_DIR} assert "$output" =~ "retrying in 142.*ms .*14/14.*" } @test "bud with ADD with git repository source" { # quay.io/hummingbird/git is only available on these arches: skip_unless_arch amd64 arm64 _prefetch quay.io/hummingbird/git # any image with git preinstalled would do local repodir=${TEST_SCRATCH_DIR}/repository mkdir -p ${repodir}/podman.git tar -C ${repodir}/podman.git -xz < ${TEST_SOURCES}/git-daemon/bare-podman-repo.tar.gz starthttpd /git/=${repodir}:"git http-backend":GIT_HTTP_EXPORT_ALL=1:GIT_PROJECT_ROOT=${repodir} ${repodir} local contextdir=${TEST_SCRATCH_DIR}/add-git mkdir -p $contextdir cat > $contextdir/Dockerfile << _EOF FROM quay.io/hummingbird/git USER 0:0 ADD http://0.0.0.0:${HTTP_SERVER_PORT}/git/podman.git#v5.0 /podman-branch ADD http://0.0.0.0:${HTTP_SERVER_PORT}/git/podman.git#v5.0.0 /podman-tag _EOF run_buildah build -f $contextdir/Dockerfile -t git-image $contextdir run_buildah from --quiet $WITH_POLICY_JSON --name testctr git-image run_buildah run --network=host testctr -- sh -c 'git -C /podman-branch rev-parse HEAD' local_head_hash=$output run_buildah run --network=host testctr -- sh -c 'git -C /podman-branch ls-remote origin v5.0 | cut -f1' assert "$output" = "$local_head_hash" run_buildah run --network=host testctr -- sh -c 'git -C /podman-tag rev-parse HEAD' local_head_hash=$output run_buildah run --network=host testctr -- sh -c 'git -C /podman-tag ls-remote --tags origin v5.0.0 | cut -f1' assert "$output" = "$local_head_hash" cat > $contextdir/Dockerfile << _EOF FROM scratch ADD http://0.0.0.0:${HTTP_SERVER_PORT}/git/podman.git#nosuchbranch /src _EOF run_buildah 125 build -f $contextdir/Dockerfile -t git-image $contextdir expect_output --substring "couldn't find remote ref nosuchbranch" } @test "build-validates-bind-bind-propagation" { _prefetch alpine cat > ${TEST_SCRATCH_DIR}/Containerfile << _EOF FROM alpine as base FROM alpine RUN --mount=type=bind,from=base,source=/,destination=/var/empty,rw,bind-propagation=suid pwd _EOF run_buildah 125 build $WITH_POLICY_JSON ${TEST_SCRATCH_DIR} expect_output --substring "invalid mount option" } @test "build-validates-cache-bind-propagation" { _prefetch alpine cat > ${TEST_SCRATCH_DIR}/Containerfile << _EOF FROM alpine RUN --mount=type=cache,destination=/var/empty,rw,bind-propagation=suid pwd _EOF run_buildah 125 build $WITH_POLICY_JSON ${TEST_SCRATCH_DIR} expect_output --substring "invalid mount option" } @test "build-check-cve-2024-9675" { _prefetch alpine touch ${TEST_SCRATCH_DIR}/file.txt cat > ${TEST_SCRATCH_DIR}/Containerfile < ${TEST_SCRATCH_DIR}/Containerfile < ${TEST_SCRATCH_DIR}/cve20249675/Containerfile < ${TEST_SCRATCH_DIR}/Containerfile echo RUN --mount=type=bind,src=Containerfile,target=Containerfile test -s Containerfile >> ${TEST_SCRATCH_DIR}/Containerfile echo RUN --mount=type=cache,id=cash,target=cachesubdir truncate -s 1024 cachesubdir/cachefile >> ${TEST_SCRATCH_DIR}/Containerfile echo RUN --mount=type=cache,id=cash,target=cachesubdir2 test -s cachesubdir2/cachefile >> ${TEST_SCRATCH_DIR}/Containerfile echo RUN --mount=type=tmpfs,target=tmpfssubdir test '`stat -f -c %i .`' '!=' '`stat -f -c %i tmpfssubdir`' >> ${TEST_SCRATCH_DIR}/Containerfile run_buildah build --security-opt label=disable ${TEST_SCRATCH_DIR} } @test "build-security-opt-mask" { base=busybox _prefetch $base run_buildah build bud/masks expect_output --substring "masked" "Everything should be masked" run_buildah build --security-opt unmask=all bud/masks expect_output --substring "unmasked" "Everything should be masked" run_buildah build --security-opt unmask=/proc/* bud/masks expect_output --substring "unmasked" "Everything should be masked" run_buildah build --security-opt unmask=/proc/acpi bud/masks expect_output --substring "unmasked" "Everything should be masked" } @test "build-mounts-build-context-rw" { zflag= if which selinuxenabled > /dev/null 2> /dev/null ; then if selinuxenabled ; then zflag=,z fi fi base=busybox _prefetch $base mkdir -p ${TEST_SCRATCH_DIR}/buildcontext cat > ${TEST_SCRATCH_DIR}/buildcontext/Dockerfile << EOF FROM $base RUN --mount=type=bind,dst=/dst,source=/,rw${zflag} \ mkdir /dst/subdir ; \ chown 1000:1000 /dst/subdir ; \ chmod 777 /dst/subdir ; \ touch /dst/subdir/file-suid ; \ chmod 4777 /dst/subdir/file-suid EOF run_buildah build ${TEST_SCRATCH_DIR}/buildcontext run find ${TEST_SCRATCH_DIR}/buildcontext -name file-suid -ls find ${TEST_SCRATCH_DIR}/buildcontext -ls expect_output "" "build should not be able to write to build context" } @test "build-with-two-outputs" { _prefetch busybox mkdir -p "${TEST_SCRATCH_DIR}"/context cat > "${TEST_SCRATCH_DIR}"/context/Containerfile << _EOF FROM busybox RUN truncate -s1 /built.txt _EOF run_buildah build --output type=local,dest=${TEST_SCRATCH_DIR}/output1 --output ${TEST_SCRATCH_DIR}/output2 $WITH_POLICY_JSON "${TEST_SCRATCH_DIR}"/context test -s "${TEST_SCRATCH_DIR}"/output1/built.txt test -s "${TEST_SCRATCH_DIR}"/output2/built.txt } @test "build-with-timestamp-applies-to-oci-archive" { local outpath="${TEST_SCRATCH_DIR}/timestamp-oci.tar" run_buildah build -f <(echo 'FROM scratch') --tag=oci-archive:${outpath}.a --timestamp 0 run_buildah build -f <(echo 'FROM scratch') --tag=oci-archive:${outpath}.b --timestamp 1 sleep 1.1 # sleep at least 1 second, so that timestamps are incremented run_buildah build -f <(echo 'FROM scratch') --tag=oci-archive:${outpath}.c --timestamp 1 # should be different ( || false is due to bats weirdness ) ! diff "${outpath}.a" "${outpath}.b" || false # should be the same diff "${outpath}.b" "${outpath}.c" } @test "build-with-timestamp-applies-to-oci-archive-with-base" { local outpath="${TEST_SCRATCH_DIR}/timestamp-oci.tar" run_buildah build -f <(echo 'FROM busybox') --tag=oci-archive:${outpath}.a --timestamp 0 sleep 1.1 # sleep at least 1 second, so that timestamps are incremented run_buildah build -f <(echo 'FROM busybox') --tag=oci-archive:${outpath}.b --timestamp 0 # should be the same diff "${outpath}.a" "${outpath}.b" } @test "bud-source-date-epoch-arg" { _prefetch busybox local timestamp=60 mkdir -p $TEST_SCRATCH_DIR/context cat > $TEST_SCRATCH_DIR/context/Dockerfile < $TEST_SCRATCH_DIR/context/Dockerfile < $TEST_SCRATCH_DIR/context/Dockerfile << EOF FROM busybox RUN hostname | tee /hostname.txt EOF local config local diff local hostname for cliflag in timestamp source-date-epoch ; do run_buildah build --"$cliflag"=$timestamp --layers --no-cache -t dir:$TEST_SCRATCH_DIR/docker-layers-$cliflag --format=docker $TEST_SCRATCH_DIR/context config=$(dir_image_config "$TEST_SCRATCH_DIR"/docker-layers-$cliflag) run jq -r .config.Hostname "$TEST_SCRATCH_DIR"/docker-layers-$cliflag/"$config" echo "$output" test $status -eq 0 assert "$output" == sandbox run jq -r .config.Domainname "$TEST_SCRATCH_DIR"/docker-layers-$cliflag/"$config" echo "$output" test $status -eq 0 assert "$output" == "" run jq -r .container "$TEST_SCRATCH_DIR"/docker-layers-$cliflag/"$config" echo "$output" test $status -eq 0 assert "$output" == null mkdir "$TEST_SCRATCH_DIR"/diff-docker-layers-$cliflag diff=$(dir_image_last_diff "$TEST_SCRATCH_DIR"/docker-layers-$cliflag) tar -C "$TEST_SCRATCH_DIR"/docker-layers-$cliflag -xf "$TEST_SCRATCH_DIR"/docker-layers-$cliflag/"$diff" hostname=$(< "$TEST_SCRATCH_DIR"/docker-layers-$cliflag/hostname.txt) assert $hostname = sandbox "expected the hostname to be the static value 'sandbox'" run_buildah build --"$cliflag"=$timestamp -t dir:"$TEST_SCRATCH_DIR"/docker-squashed-$cliflag --format=docker $TEST_SCRATCH_DIR/context config=$(dir_image_config "$TEST_SCRATCH_DIR"/docker-squashed-$cliflag) run jq -r .config.Hostname "$TEST_SCRATCH_DIR"/docker-squashed-$cliflag/"$config" echo "$output" test $status -eq 0 assert "$output" == sandbox run jq -r .config.Domainname "$TEST_SCRATCH_DIR"/docker-squashed-$cliflag/"$config" echo "$output" test $status -eq 0 assert "$output" == "" run jq -r .container "$TEST_SCRATCH_DIR"/docker-squashed-$cliflag/"$config" echo "$output" test $status -eq 0 assert "$output" == null mkdir "$TEST_SCRATCH_DIR"/diff-docker-squashed-$cliflag diff=$(dir_image_last_diff "$TEST_SCRATCH_DIR"/docker-squashed-$cliflag) tar -C "$TEST_SCRATCH_DIR"/docker-squashed-$cliflag -xf "$TEST_SCRATCH_DIR"/docker-squashed-$cliflag/"$diff" hostname=$(< "$TEST_SCRATCH_DIR"/docker-squashed-$cliflag/hostname.txt) assert $hostname = sandbox "expected the hostname to be the static value 'sandbox'" done } @test "bud-sets-created-annotation" { _prefetch busybox mkdir ${TEST_SCRATCH_DIR}/buildcontext cat > ${TEST_SCRATCH_DIR}/buildcontext/Dockerfile << _EOF FROM busybox RUN pwd _EOF for flagdir in default: timestamp:--timestamp=0 sde:--source-date-epoch=0 suppressed:--unsetannotation=org.opencontainers.image.created specific:--created-annotation=false explicit:--created-annotation=true ; do local flag=${flagdir##*:} local subdir=${flagdir%%:*} run_buildah build --layers --no-cache $flag -t oci:${TEST_SCRATCH_DIR}/$subdir ${TEST_SCRATCH_DIR}/buildcontext local manifest=${TEST_SCRATCH_DIR}/$subdir/$(oci_image_manifest ${TEST_SCRATCH_DIR}/$subdir) run jq -r '.annotations["org.opencontainers.image.created"]' "$manifest" assert $status -eq 0 echo "$output" local manifestcreated="$output" local config=${TEST_SCRATCH_DIR}/$subdir/$(oci_image_config ${TEST_SCRATCH_DIR}/$subdir) run jq -r '.created' "$config" assert $status -eq 0 echo "$output" local configcreated="$output" if [[ "$flag" =~ "=0" ]]; then assert $manifestcreated = $configcreated "manifest and config disagree on the image's created-time" assert $manifestcreated = "1970-01-01T00:00:00Z" elif [[ "$flag" =~ "unsetannotation" ]]; then assert $configcreated != "" assert $manifestcreated = "null" elif [[ "$flag" =~ "created-annotation=false" ]]; then assert $configcreated != "" assert $manifestcreated = "null" else assert $manifestcreated = $configcreated "manifest and config disagree on the image's created-time" assert $manifestcreated != "" assert $manifestcreated != "1970-01-01T00:00:00Z" fi done } @test "bud-with-source-date-epoch-arg" { _prefetch busybox local timestamp=60 local datestamp="1970-01-01T00:01:00Z" mkdir -p $TEST_SCRATCH_DIR/buildcontext cat > $TEST_SCRATCH_DIR/buildcontext/Dockerfile < $contextdir/Dockerfile <<-EOF FROM busybox ARG SOURCE_DATE_EPOCH COPY Dockerfile /root/Dockerfile RUN pwd > /pwd.txt EOF run_buildah build --layers=true --iidfile ${TEST_SCRATCH_DIR}/baseiidfile ${contextdir} local baseiid=$(< ${TEST_SCRATCH_DIR}/baseiidfile) iid # Try an assortment of layer-mutating flags. run_buildah build --layers=true --iidfile ${TEST_SCRATCH_DIR}/iidfile-sde --source-date-epoch=86400 ${contextdir} local iidsde=$(< ${TEST_SCRATCH_DIR}/iidfile-sde) assert "$iidsde" != "$baseiid" run_buildah build --layers=true --iidfile ${TEST_SCRATCH_DIR}/iidfile-ts --timestamp=86400 ${contextdir} local iidts=$(< ${TEST_SCRATCH_DIR}/iidfile-ts) assert "$iidts" != "$baseiid" assert "$iidts" != "$basesde" run_buildah build --layers=true --iidfile ${TEST_SCRATCH_DIR}/iidfile-rt --source-date-epoch=86400 --rewrite-timestamp ${contextdir} local iidrt=$(< ${TEST_SCRATCH_DIR}/iidfile-rt) assert "$iidrt" != "$baseiid" assert "$iidrt" != "$basets" assert "$iidrt" != "$basesde" run_buildah build --layers=true --iidfile ${TEST_SCRATCH_DIR}/iidfile --source-date-epoch=86400 ${contextdir} iid=$(< ${TEST_SCRATCH_DIR}/iidfile) assert "$iid" == "$iidsde" run_buildah build --layers=true --iidfile ${TEST_SCRATCH_DIR}/iidfile --timestamp=86400 ${contextdir} iid=$(< ${TEST_SCRATCH_DIR}/iidfile) assert "$iid" == "$iidts" run_buildah build --layers=true --iidfile ${TEST_SCRATCH_DIR}/iidfile --source-date-epoch=86400 --rewrite-timestamp ${contextdir} iid=$(< ${TEST_SCRATCH_DIR}/iidfile) assert "$iid" == "$iidrt" } @test "bud unset/inherit=false for labels and annotations inform cache checks" { _prefetch busybox local contextdir=${TEST_SCRATCH_DIR}/context mkdir $contextdir # We'll build a multiple-layer image based on busybox. cat > $contextdir/Dockerfile <<-EOF FROM busybox COPY Dockerfile /root/Dockerfile RUN pwd > /pwd.txt EOF # Build an image with a label and an annotation that we don't want in the end. run_buildah build --layers=true --annotation=annotation1=annotationvalue1 --annotation=annotation2=annotationvalue2 --label=label1=labelvalue1 --label=label2=labelvalue2 --iidfile ${TEST_SCRATCH_DIR}/baseiidfile ${contextdir} local baseiid=$(< ${TEST_SCRATCH_DIR}/baseiidfile) iid baseiid="${baseiid##*:}" assert "${baseiid}" != "" # Double-check that the label and annotation made it in. run_buildah inspect -t image "${baseiid}" run jq -r .ImageAnnotations <<< "${output}" assert "${status}" == 0 assert "${output}" =~ annotation1 assert "${output}" =~ annotation2 run_buildah inspect -t image "${baseiid}" run jq -r .OCIv1.config.Labels <<< "${output}" assert "${status}" == 0 assert "${output}" =~ label1 assert "${output}" =~ label2 # Derive future images from the one we just built. cat > $contextdir/Dockerfile <<-EOF FROM ${baseiid} COPY Dockerfile /root/Dockerfile RUN pwd > /pwd.txt EOF # Build a derived image without that annotation. run_buildah build --layers=true --unsetannotation=annotation1 --iidfile ${TEST_SCRATCH_DIR}/unsetaiidfile ${contextdir} local unsetaiid=$(< ${TEST_SCRATCH_DIR}/unsetaiidfile) unsetaiid="${unsetaiid##*:}" assert "${unsetaiid}" != "${baseiid}" # Try it again. We should get cache hits all the way to the end. run_buildah build --layers=true --unsetannotation=annotation1 --iidfile ${TEST_SCRATCH_DIR}/iidfile ${contextdir} iid=$(< ${TEST_SCRATCH_DIR}/iidfile) iid="${iid##*:}" assert "${iid}" == "${unsetaiid}" # Double-check that the annotation that we wanted to discard was discarded. run_buildah inspect -t image "${iid}" run jq -r .ImageAnnotations <<< "${output}" assert "${status}" == 0 assert "${output}" !~ annotation1 assert "${output}" =~ annotation2 # Build a derived image without that label. run_buildah build --layers=true --unsetlabel=label1 --iidfile ${TEST_SCRATCH_DIR}/unsetliidfile ${contextdir} local unsetliid=$(< ${TEST_SCRATCH_DIR}/unsetliidfile) unsetliid="${unsetliid##*:}" assert "${unsetliid}" != "${baseiid}" # Try it again. We should get cache hits all the way to the end. run_buildah build --layers=true --unsetlabel=label1 --iidfile ${TEST_SCRATCH_DIR}/iidfile ${contextdir} iid=$(< ${TEST_SCRATCH_DIR}/iidfile) iid="${iid##*:}" assert "${iid}" == "${unsetliid}" # Double-check that the label that we wanted to discard was discarded. run_buildah inspect -t image "${iid}" run jq -r .OCIv1.config.Labels <<< "${output}" assert "${status}" == 0 assert "${output}" !~ label1 assert "${output}" =~ label2 # Build a derived image without any inherited annotations. run_buildah build --layers=true --inherit-annotations=false --iidfile ${TEST_SCRATCH_DIR}/disavowaiidfile ${contextdir} local disavowaiid=$(< ${TEST_SCRATCH_DIR}/disavowaiidfile) disavowaiid="${disavowaiid##*:}" assert "${disavowaiid}" != "${baseiid}" # Try it again. We should get cache hits all the way to the end. run_buildah build --layers=true --inherit-annotations=false --iidfile ${TEST_SCRATCH_DIR}/iidfile ${contextdir} iid=$(< ${TEST_SCRATCH_DIR}/iidfile) iid="${iid##*:}" assert "${iid}" == "${disavowaiid}" assert "${iid}" != "${unsetaiid}" # Double-check that the annotations were discarded. run_buildah inspect -t image "${iid}" run jq -r .ImageAnnotations <<< "${output}" assert "${status}" == 0 assert "${output}" !~ annotation1 assert "${output}" !~ annotation2 # Build a derived image without any inherited labels. run_buildah build --layers=true --inherit-labels=false --iidfile ${TEST_SCRATCH_DIR}/disavowliidfile ${contextdir} local disavowliid=$(< ${TEST_SCRATCH_DIR}/disavowliidfile) disavowliid="${disavowliid##*:}" assert "${disavowliid}" != "${baseiid}" # Try it again. We should get cache hits all the way to the end. run_buildah build --layers=true --inherit-labels=false --iidfile ${TEST_SCRATCH_DIR}/iidfile ${contextdir} iid=$(< ${TEST_SCRATCH_DIR}/iidfile) iid="${iid##*:}" assert "${iid}" == "${disavowliid}" assert "${iid}" != "${unsetliid}" # Double-check that the labels were discarded. run_buildah inspect -t image "${iid}" run jq -r .OCIv1.config.Labels <<< "${output}" assert "${status}" == 0 assert "${output}" !~ label1 assert "${output}" !~ label2 } @test "bud --link consistent diffID with --no-cache" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/link-diffid-nocache mkdir -p $contextdir cat > $contextdir/Dockerfile << EOF FROM alpine RUN echo "before link layer" > /before.txt COPY --link test.txt /test.txt RUN echo "after link layer" > /after.txt RUN ls -l /test.txt RUN cat /test.txt EOF echo "test content" > $contextdir/test.txt run_buildah build --no-cache --layers $WITH_POLICY_JSON -t oci:${TEST_SCRATCH_DIR}/oci-layout1 $contextdir run_buildah build --no-cache --layers $WITH_POLICY_JSON -t oci:${TEST_SCRATCH_DIR}/oci-layout2 $contextdir diffid1=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-layout1 2) diffid2=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-layout2 2) echo $diffid1 echo $diffid2 assert "$diffid1" = "$diffid2" "COPY --link should produce identical diffIDs with --no-cache" } @test "bud --link consistent diffID with different base images" { _prefetch alpine busybox local contextdir=${TEST_SCRATCH_DIR}/bud/link-diffid-bases mkdir -p $contextdir echo "shared content" > $contextdir/shared.txt cat > $contextdir/Containerfile.alpine << EOF FROM alpine RUN echo "alpine setup" > /setup.txt COPY --link shared.txt /shared.txt RUN echo "alpine complete" > /complete.txt RUN cat /shared.txt EOF cat > $contextdir/Containerfile.busybox << EOF FROM busybox RUN echo "busybox setup" > /setup.txt COPY --link shared.txt /shared.txt RUN echo "busybox complete" > /complete.txt RUN cat /shared.txt EOF run_buildah build --no-cache --layers $WITH_POLICY_JSON -f $contextdir/Containerfile.alpine -t oci:${TEST_SCRATCH_DIR}/oci-layout-alpine $contextdir run_buildah build --no-cache --layers $WITH_POLICY_JSON -f $contextdir/Containerfile.busybox -t oci:${TEST_SCRATCH_DIR}/oci-layout-busybox $contextdir diffid_alpine=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-layout-alpine 2) diffid_busybox=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-layout-busybox 2) assert "$diffid_alpine" = "$diffid_busybox" "COPY --link should produce identical diffIDs regardless of base image" } @test "bud --link consistent diffID with multiple files" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/link-diffid-multi mkdir -p $contextdir/subdir echo "file1" > $contextdir/file1.txt echo "file2" > $contextdir/file2.txt echo "subfile" > $contextdir/subdir/sub.txt cat > $contextdir/Dockerfile << EOF FROM alpine RUN echo "setup" > /setup.txt COPY --link file1.txt file2.txt /files/ ADD --link subdir /subdir RUN echo "complete" > /complete.txt RUN ls -l /files/ EOF run_buildah build --no-cache --layers $WITH_POLICY_JSON -t oci:${TEST_SCRATCH_DIR}/oci-layout-multi1 $contextdir run_buildah build --no-cache --layers $WITH_POLICY_JSON -t oci:${TEST_SCRATCH_DIR}/oci-layout-multi2 $contextdir copy_diffid1=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-layout-multi1 2) copy_diffid2=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-layout-multi2 2) add_diffid1=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-layout-multi1 3) add_diffid2=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-layout-multi2 3) assert "$copy_diffid1" = "$copy_diffid2" "COPY --link with multiple files should have consistent diffID" assert "$add_diffid1" = "$add_diffid2" "ADD --link should have consistent diffID" } @test "bud --link with glob patterns consistent diffID" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/link-diffid-glob mkdir -p $contextdir echo "test1" > $contextdir/test1.txt echo "test2" > $contextdir/test2.txt echo "json1" > $contextdir/data1.json echo "json2" > $contextdir/data2.json cat > $contextdir/Dockerfile << EOF FROM alpine COPY --link test*.txt /tests/ COPY --link *.json /data/ RUN echo "globbing complete" > /complete.txt RUN ls -l /tests/ RUN ls -l /data/ EOF run_buildah build --no-cache --layers $WITH_POLICY_JSON -t oci:${TEST_SCRATCH_DIR}/oci-glob-1 $contextdir run_buildah build --no-cache --layers $WITH_POLICY_JSON -t oci:${TEST_SCRATCH_DIR}/oci-glob-2 $contextdir glob_txt_diffid1=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-glob-1 1) glob_txt_diffid2=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-glob-2 1) glob_json_diffid1=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-glob-1 2) glob_json_diffid2=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-glob-2 2) assert "$glob_txt_diffid1" = "$glob_txt_diffid2" "COPY --link with glob *.txt should have consistent diffID" assert "$glob_json_diffid1" = "$glob_json_diffid2" "COPY --link with glob *.json should have consistent diffID" } @test "bud --link cache behavior basic" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/link-cache mkdir -p $contextdir echo "test content" > $contextdir/testfile.txt cat > $contextdir/Dockerfile << EOF FROM alpine COPY --link testfile.txt /testfile.txt RUN echo "build complete" > /complete.txt RUN cat /testfile.txt EOF # First build run_buildah build --layers $WITH_POLICY_JSON -t link-cache1 $contextdir run_buildah build --layers $WITH_POLICY_JSON -t link-cache2 $contextdir assert "$output" =~ "Using cache" # Modify content echo "modified content" > $contextdir/testfile.txt run_buildah build --layers $WITH_POLICY_JSON -t link-cache3 $contextdir assert "$output" !~ "STEP 2/3: COPY --link testfile.txt /testfile.txt"$'\n'".*Using cache" } @test "bud --link with chmod and chown" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/link-perms mkdir -p $contextdir echo "test content" > $contextdir/testfile.txt cat > $contextdir/Dockerfile << EOF FROM alpine COPY --link --chmod=755 --chown=1000:1000 testfile.txt /testfile.txt RUN stat -c '%u:%g %a' /testfile.txt EOF run_buildah build --layers $WITH_POLICY_JSON -t link-perms $contextdir expect_output --substring "1000:1000 755" } @test "bud --link multi-stage build cache" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/link-multistage mkdir -p $contextdir echo "stage1 content" > $contextdir/stage1.txt echo "stage2 content" > $contextdir/stage2.txt cat > $contextdir/Dockerfile << EOF FROM alpine AS stage1 ADD --link stage1.txt /stage1.txt RUN echo "stage1 complete" > /complete.txt RUN cat /stage1.txt FROM alpine AS stage2 COPY --from=stage1 /stage1.txt /from-stage1.txt ADD --link stage2.txt /stage2.txt RUN echo "stage2 complete" > /complete.txt RUN cat /stage2.txt EOF run_buildah build --layers $WITH_POLICY_JSON -t link-multistage1 $contextdir run_buildah build --layers $WITH_POLICY_JSON -t link-multistage2 $contextdir assert "$output" =~ "Using cache" run_buildah from --name test-ctr link-multistage2 run_buildah run test-ctr cat /from-stage1.txt expect_output "stage1 content" run_buildah run test-ctr cat /stage2.txt expect_output "stage2 content" run_buildah rm test-ctr } @test "bud --link ADD with remote URL consistent diffID" { _prefetch alpine local contentdir=${TEST_SCRATCH_DIR}/content mkdir -p $contentdir echo this is a readmin > ${contentdir}/README.md starthttpd ${contentdir} local contextdir=${TEST_SCRATCH_DIR}/bud/link-url mkdir -p $contextdir cat > $contextdir/Dockerfile << EOF FROM alpine ADD --link http://0.0.0.0:${HTTP_SERVER_PORT}/README.md /README.md RUN echo "remote add complete" > /complete.txt RUN cat /README.md EOF run_buildah build --no-cache --layers $WITH_POLICY_JSON -t oci:${TEST_SCRATCH_DIR}/oci-url1 $contextdir run_buildah build --no-cache --layers $WITH_POLICY_JSON -t oci:${TEST_SCRATCH_DIR}/oci-url2 $contextdir diffid1=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-url1 1) diffid2=$(oci_image_diff_id ${TEST_SCRATCH_DIR}/oci-url2 1) assert "$diffid1" = "$diffid2" "ADD --link with URL should have consistent diffID" } @test "bud --link respects dockerignore" { _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/bud/link-ignore mkdir -p $contextdir echo "included" > $contextdir/included.txt echo "excluded" > $contextdir/excluded.txt echo "excluded.txt" > $contextdir/.dockerignore cat > $contextdir/Dockerfile << EOF FROM alpine RUN echo "Starting" > /start.txt COPY --link *.txt /files/ RUN echo "Ending" > /end.txt RUN ls -l /files/ EOF run_buildah build --layers $WITH_POLICY_JSON -t link-ignore $contextdir expect_output --substring "included.txt" assert "$output" !~ "excluded.txt" } @test "bud multi-platform --layers should create separate images for each platform" { _prefetch busybox local contextdir=${TEST_SCRATCH_DIR}/bud/multiplatform-layers mkdir -p $contextdir # Create a simple Dockerfile that starts from scratch to test the specific caching bug cat > $contextdir/hello.txt << _EOF Hello from multi-platform build! _EOF cat > $contextdir/Dockerfile << _EOF FROM scratch COPY hello.txt /hello.txt CMD ["/hello.txt"] _EOF # Build for multiple platforms with --layers and --manifest # This should create separate images for each platform, not reuse cached images incorrectly local manifestname="multiplatform-test-$(safename)" run_buildah build $WITH_POLICY_JSON --layers --platform linux/amd64,linux/arm64 --manifest "$manifestname" -f $contextdir/Dockerfile $contextdir # Verify that the build completed successfully expect_output --substring "linux/amd64" expect_output --substring "linux/arm64" # Check that we have a manifest list run_buildah manifest exists "$manifestname" # Inspect the manifest list to verify it contains entries for both platforms run_buildah manifest inspect "$manifestname" local manifest_content="$output" # Verify the manifest contains both amd64 and arm64 entries assert "$manifest_content" =~ "amd64" "manifest should contain amd64 platform" assert "$manifest_content" =~ "arm64" "manifest should contain arm64 platform" # Count the number of manifests - should have at least 2 (one for each platform) run jq -r '.manifests | length' <<< "$manifest_content" local manifest_count="$output" assert "$manifest_count" -ge 2 "manifest list should contain at least 2 platform-specific manifests" # Verify that the platforms are correctly specified in the manifest run jq -r '.manifests[].platform.architecture' <<< "$manifest_content" local architectures="$output" assert "$architectures" =~ "amd64" "manifest should list amd64 architecture" assert "$architectures" =~ "arm64" "manifest should list arm64 architecture" # Verify we have multiple different image IDs (not the same image reused) # This is the key test - before the fix, the same image would be reused for both platforms run jq -r '.manifests[].digest' <<< "$manifest_content" local digests=($output) # Each platform should have a unique digest - they should not be the same if [ "${#digests[@]}" -ge 2 ]; then local first_digest="${digests[0]}" local found_different=false for digest in "${digests[@]:1}"; do if [ "$digest" != "$first_digest" ]; then found_different=true break fi done assert "$found_different" = true "each platform should have a unique image digest, not reuse the same cached image" fi } @test "bud multi-platform --layers should not reuse different platform if default platform is being used" { run_buildah info --format '{{.host.arch}}' myarch="$output" otherarch="ppc64le" # just make sure that other arch is not equivalent to host arch if [[ "$otherarch" == "$myarch" ]]; then otherarch="amd64" fi run_buildah build --layers --platform linux/$otherarch -f $BUDFILES/from-scratch/Containerfile2 $BUDFILES/from-scratch/ # Second build should not use cache at all run_buildah build --layers -f $BUDFILES/from-scratch/Containerfile2 $BUDFILES/from-scratch/ assert "$output" !~ "Using cache" } @test "bud --layers should not include pulled up parent directories of mount points" { _prefetch quay.io/libpod/busybox local contextdir=${TEST_SCRATCH_DIR}/context mkdir $contextdir mkdir $contextdir/dev mkdir $contextdir/proc mkdir $contextdir/sys cat > $contextdir/Dockerfile << _EOF FROM scratch COPY / / COPY --from=quay.io/libpod/busybox / / RUN rm -f /Dockerfile RUN --mount=type=bind,ro,src=/Dockerfile,target=/var/spool/mail/tmpfile touch /newfile _EOF # the most recent layer should only have the one newly-added file in it run_buildah build --layers -t oci:${TEST_SCRATCH_DIR}/oci-image ${contextdir} lastlayer=$(oci_image_last_diff ${TEST_SCRATCH_DIR}/oci-image) run tar tf ${TEST_SCRATCH_DIR}/oci-image/"${lastlayer}" echo "$output" assert "$status" = "0" assert "${#lines[*]}" = "1" } @test "bud should include excluded pulled up parent directories in squashed images" { _prefetch quay.io/libpod/busybox local contextdir=${TEST_SCRATCH_DIR}/context mkdir $contextdir mkdir $contextdir/dev mkdir $contextdir/etc mkdir $contextdir/proc mkdir $contextdir/sys cat > $contextdir/Dockerfile << _EOF FROM scratch COPY / / COPY --from=quay.io/libpod/busybox / / RUN rm -f /Dockerfile RUN --mount=type=bind,ro,src=/Dockerfile,target=/etc/removed-file --mount=type=bind,ro,src=/Dockerfile,target=/var/spool/mail/tmpfile touch /newfile _EOF local source_date_epoch=$(date +%s) # build a copy of the image that's all squashed from the start run_buildah build --no-cache --squash=true --source-date-epoch=$source_date_epoch --rewrite-timestamp -t dir:${TEST_SCRATCH_DIR}/squashed-image ${contextdir} # build a copy of the image where we only commit at the end run_buildah build --no-cache --layers=false --source-date-epoch=$source_date_epoch --rewrite-timestamp -t not-layered-image ${contextdir} # build a copy of the image where we commit at every step run_buildah build --no-cache --layers=true --source-date-epoch=$source_date_epoch --rewrite-timestamp -t layered-image ${contextdir} # now squash them, with no internal record of needing to exclude anything carrying over run_buildah from --name not-layered-container not-layered-image run_buildah commit --squash not-layered-container dir:${TEST_SCRATCH_DIR}/squashed-not-layered-image run_buildah from --name layered-container layered-image run_buildah commit --squash layered-container dir:${TEST_SCRATCH_DIR}/squashed-layered-image # find the diffs for the three versions, which should look the same thanks to the --source-date-epoch --rewrite-timestamp combo local squashed=${TEST_SCRATCH_DIR}/squashed-image/$(dir_image_last_diff ${TEST_SCRATCH_DIR}/squashed-image) local notlayered=${TEST_SCRATCH_DIR}/squashed-not-layered-image/$(dir_image_last_diff ${TEST_SCRATCH_DIR}/squashed-not-layered-image) local layered=${TEST_SCRATCH_DIR}/squashed-layered-image/$(dir_image_last_diff ${TEST_SCRATCH_DIR}/squashed-layered-image) tar tvf ${squashed} > ${TEST_SCRATCH_DIR}/squashed-image-rootfs.txt tar tvf ${notlayered} > ${TEST_SCRATCH_DIR}/squashed-not-layered-image-rootfs.txt tar tvf ${layered} > ${TEST_SCRATCH_DIR}/squashed-layered-image-rootfs.txt diff -u ${TEST_SCRATCH_DIR}/squashed-layered-image-rootfs.txt ${TEST_SCRATCH_DIR}/squashed-image-rootfs.txt diff -u ${TEST_SCRATCH_DIR}/squashed-layered-image-rootfs.txt ${TEST_SCRATCH_DIR}/squashed-not-layered-image-rootfs.txt } @test "bud COPY one file to ..../. creates the destination directory" { _prefetch busybox local contextdir=${TEST_SCRATCH_DIR}/context mkdir -p ${contextdir} echo hello, this is a file > ${contextdir}/file.txt cat > ${contextdir}/Dockerfile << _EOF FROM busybox COPY /file.txt /app/. WORKDIR /app RUN pwd _EOF run_buildah build --layers ${contextdir} run_buildah build ${contextdir} } @test "bud with a previously-used graphroot with base image in it used as imagestore" { # there are subtle differences between "this was always the imagestore" and # "this was a graphroot, but i'm using it as an imagestore now" case "${STORAGE_DRIVER}" in overlay) ;; *) skip "imagestore flag is compatible with overlay, but not ${STORAGE_DRIVER}" ;; esac _prefetch quay.io/libpod/alpine:latest local contextdir=${TEST_SCRATCH_DIR}/context mkdir -p ${contextdir} cat > ${contextdir}/Dockerfile << _EOF FROM quay.io/libpod/alpine:latest RUN mkdir hello _EOF run_buildah --root=${TEST_SCRATCH_DIR}/newroot --storage-opt=imagestore=${TEST_SCRATCH_DIR}/root build --pull=never --no-cache --layers=true ${contextdir} run_buildah --root=${TEST_SCRATCH_DIR}/newroot --storage-opt=imagestore=${TEST_SCRATCH_DIR}/root build --pull=never ${contextdir} run_buildah --root=${TEST_SCRATCH_DIR}/newroot --storage-opt=imagestore=${TEST_SCRATCH_DIR}/root build --pull=never --squash ${contextdir} } @test "bud with exec-form RUN instruction" { # This test requires a statically linked busybox which is not # the case for quay.io/libpod/busybox on ppc64le & s390x skip_unless_arch amd64 arm64 baseimage=busybox _prefetch $baseimage local contextdir=${TEST_SCRATCH_DIR}/context mkdir -p "${contextdir}" cat > "${contextdir}"/Dockerfile <<-EOF FROM scratch AS mkdir RUN --mount=type=bind,from="${baseimage}",destination=/usr ["busybox", "sh", "-x", "-c", "mkdir /brand-new-subdir"] FROM "${baseimage}" RUN --mount=type=bind,from=mkdir,destination=/mounted find /mounted -print EOF run_buildah build --layers=true "${contextdir}" expect_output --substring /mounted/brand-new-subdir run_buildah build --layers=false "${contextdir}" expect_output --substring /mounted/brand-new-subdir } @test "bud with --iidfile" { target=scratch-image local contextdir=${TEST_SCRATCH_DIR}/context mkdir -p "${contextdir}" cat > "${contextdir}"/Dockerfile <<-EOF FROM scratch COPY . . EOF for layers in "--layers=true" "--layers=false" ; do for destination in "dir:${TEST_SCRATCH_DIR}/dir" "oci-archive:${TEST_SCRATCH_DIR}/oci-archive" "docker-archive:${TEST_SCRATCH_DIR}/docker-archive" "oci:${TEST_SCRATCH_DIR}/oci-layout" "local" ; do rm -f "${TEST_SCRATCH_DIR}"/iidfile fsname="${destination#*:}" # assume : is used in a non-containers-storage name rather than a repository name + tag combination if test "${fsname}" != "${destination}" ; then rm -fr "${fsname}" fi run_buildah build --iidfile "${TEST_SCRATCH_DIR}"/iidfile --no-cache "${layers}" -t "${destination}" "${contextdir}" local iid=$(< "${TEST_SCRATCH_DIR}"/iidfile) assert "${iid}" != "" assert "${iid#sha256:}" != "" if test "${fsname}" != "${destination}" ; then test -e "${fsname}" fi done done } @test "bud with --iidfile-raw" { target=scratch-image local contextdir=${TEST_SCRATCH_DIR}/context mkdir -p "${contextdir}" cat > "${contextdir}"/Dockerfile <<-EOF FROM scratch COPY . . EOF for layers in "--layers=true" "--layers=false" ; do for destination in "dir:${TEST_SCRATCH_DIR}/dir" "oci-archive:${TEST_SCRATCH_DIR}/oci-archive" "docker-archive:${TEST_SCRATCH_DIR}/docker-archive" "oci:${TEST_SCRATCH_DIR}/oci-layout" "local" ; do rm -f "${TEST_SCRATCH_DIR}"/iidfile-raw fsname="${destination#*:}" # assume : is used in a non-containers-storage name rather than a repository name + tag combination if test "${fsname}" != "${destination}" ; then rm -fr "${fsname}" fi run_buildah build --iidfile-raw "${TEST_SCRATCH_DIR}"/iidfile-raw --no-cache "${layers}" -t "${destination}" "${contextdir}" local iid_raw=$(< "${TEST_SCRATCH_DIR}"/iidfile-raw) # iidfile-raw should contain just the hash without sha256: prefix assert "${iid_raw}" != "" assert "${iid_raw}" =~ "^[0-9a-f]{64}$" # Verify it doesn't contain sha256: prefix assert "${iid_raw}" !~ "sha256:" if test "${fsname}" != "${destination}" ; then test -e "${fsname}" fi done done } @test "bud with --iidfile and --iidfile-raw comparison" { target=scratch-image local contextdir=${TEST_SCRATCH_DIR}/context mkdir -p "${contextdir}" cat > "${contextdir}"/Dockerfile <<-EOF FROM scratch COPY . . EOF # Build with both --iidfile and --iidfile-raw to verify they write the same image ID run_buildah build --iidfile "${TEST_SCRATCH_DIR}"/iidfile --iidfile-raw "${TEST_SCRATCH_DIR}"/iidfile-raw -t "${target}" "${contextdir}" local iid=$(< "${TEST_SCRATCH_DIR}"/iidfile) local iid_raw=$(< "${TEST_SCRATCH_DIR}"/iidfile-raw) # iid should have sha256: prefix, iid_raw should not assert "${iid}" =~ "^sha256:[0-9a-f]{64}$" assert "${iid_raw}" =~ "^[0-9a-f]{64}$" # The hash portion should be identical assert "${iid#sha256:}" == "${iid_raw}" } @test "bud with --raw-iidfile alias" { target=scratch-image local contextdir=${TEST_SCRATCH_DIR}/context mkdir -p "${contextdir}" cat > "${contextdir}"/Dockerfile <<-EOF FROM scratch COPY . . EOF # Test that --raw-iidfile works as an alias for --iidfile-raw run_buildah build --raw-iidfile "${TEST_SCRATCH_DIR}"/iidfile-alias -t "${target}" "${contextdir}" local iid_alias=$(< "${TEST_SCRATCH_DIR}"/iidfile-alias) # Should contain just the hash without sha256: prefix assert "${iid_alias}" != "" assert "${iid_alias}" =~ "^[0-9a-f]{64}$" assert "${iid_alias}" !~ "sha256:" } @test "build-oci-archive-switch" { local base=busybox _prefetch $base run_buildah from -q $base run_buildah inspect --format '{{.FromImageID}}' "$output" local imageID="$output" mkdir -p ${TEST_SCRATCH_DIR}/buildcontext copy containers-storage:$imageID oci-archive:${TEST_SCRATCH_DIR}/buildcontext/source-oci-archive.tar copy containers-storage:$imageID oci:${TEST_SCRATCH_DIR}/buildcontext/source-oci copy containers-storage:$imageID docker-archive:${TEST_SCRATCH_DIR}/buildcontext/source-docker-archive.tar copy containers-storage:$imageID dir:${TEST_SCRATCH_DIR}/buildcontext/source-dir pushd ${TEST_SCRATCH_DIR} cat > ${TEST_SCRATCH_DIR}/buildcontext/Dockerfile << EOF FROM $base RUN --mount=type=bind,rw,target=/bc mkdir /bc/oci-archive /bc/oci /bc/docker-archive /bc/dir RUN --mount=type=bind,rw,target=/bc cp /bc/source-oci-archive.tar /bc/oci-archive/archive.tar RUN --mount=type=bind,rw,target=/bc cp -a /bc/source-oci/* /bc/oci/ RUN --mount=type=bind,rw,target=/bc cp /bc/source-docker-archive.tar /bc/docker-archive/archive.tar RUN --mount=type=bind,rw,target=/bc cp -a /bc/source-dir/* /bc/dir/ EOF local stage=0 local mounts= for base in oci-archive:oci-archive/archive.tar oci:oci docker-archive:docker-archive/archive.tar dir:dir ; do local dirsuffix= case $base in dir:*|oci:*) dirsuffix=/ ;; esac for prefix in "" / ./ ; do for suffix in "" ${dirsuffix} ; do local adjustedbase=${base/:/:${prefix}}${suffix} echo FROM $adjustedbase >> ${TEST_SCRATCH_DIR}/buildcontext/Dockerfile mounts="$mounts --mount=type=bind,from=$stage,target=/var/tmp/$stage" echo "RUN$mounts :" >> ${TEST_SCRATCH_DIR}/buildcontext/Dockerfile stage=$((stage+1)) done done done run_buildah build ${TEST_SCRATCH_DIR}/buildcontext } @test "build-with-run-mount" { local out_path=${TEST_SCRATCH_DIR}/run-mount.tar # add an ephemeral mount FOO=world run_buildah build \ --secret id=mysecret,env=FOO \ --mount type=secret,id=mysecret,dst=/bar,required \ --timestamp 0 \ --tag oci-archive:"${out_path}.a" \ -f <(printf 'FROM busybox\nRUN echo "hello $(cat /bar) welcome"') expect_output --substring "hello world welcome" # if we run the same, but no secret and no mount, we should get identical tarball run_buildah build \ --timestamp 0 \ --tag oci-archive:"${out_path}.b" \ -f <(printf 'FROM busybox\nRUN echo "hello $(cat /bar) welcome"') expect_output --substring "hello welcome" # should be the same, as the ephemeral run mount should not affect the result diff "${out_path}.a" "${out_path}.b" } @test "build-with-run-image-mount" { _prefetch busybox local contextdir=${TEST_SCRATCH_DIR}/context mkdir -p "${contextdir}" cat > "${contextdir}"/Containerfile << _EOF FROM busybox RUN cat /alpine/etc/os-release | tee /alpine.os-release _EOF run_buildah build --mount=type=bind,from=alpine,target=/alpine "${contextdir}" } @test "bud with FROM --after" { # This tests the 'FROM --after' workflow, i.e. that: # - --after is enough to pull in the builder stage as a dep # - even with --jobs=4, --after forces us to wait before even trying to import # the image # - build arguments for --after are handled correctly # - the final "built" image matches the after stage output exactly _prefetch busybox local contextdir=${TEST_SCRATCH_DIR}/context mkdir -p "${contextdir}" copy containers-storage:busybox oci-archive:"${contextdir}"/busybox.ociarchive # Re-import it so we have it in OCI format and not v2s2 so we can compare # image IDs later on. copy oci-archive:"${contextdir}"/busybox.ociarchive containers-storage:busybox-oci cat > "${contextdir}"/Containerfile << 'EOF' ARG TESTARG=builder FROM busybox AS builder # copy it to a different name as proof that this RUN stage must've run RUN --mount=type=bind,target=/src,rw cp /src/busybox.ociarchive /src/out.ociarchive FROM --after=${TESTARG} oci-archive:out.ociarchive EOF run_buildah build $WITH_POLICY_JSON --jobs=4 -t test-after "${contextdir}" # Verify the final image is identical to the OCI-converted busybox run_buildah inspect --format '{{.FromImageID}}' busybox-oci local busybox_id="$output" run_buildah inspect --format '{{.FromImageID}}' test-after local test_after_id="$output" assert "$busybox_id" == "$test_after_id" } @test "bud with FROM --after and COPY --from" { _prefetch busybox local contextdir=${TEST_SCRATCH_DIR}/context mkdir -p "${contextdir}" cat > "${contextdir}"/Containerfile << 'EOF' FROM busybox AS builder RUN echo "Artifact Created" > /data.txt FROM busybox AS verifier COPY --from=builder /data.txt /data.txt RUN echo "Artifact Appended" >> /data.txt # this must wait until the builder stage is finished FROM --after=builder busybox AS final # this must wait until the verifier stage is finished COPY --from=verifier /data.txt /root/data.txt RUN grep "Artifact Created" /root/data.txt RUN grep "Artifact Appended" /root/data.txt EOF run_buildah build $WITH_POLICY_JSON --jobs=4 -t test-after-copy-from "${contextdir}" } @test "bud with FROM --after error cases" { local contextdir=${TEST_SCRATCH_DIR}/context mkdir -p "${contextdir}" # self-reference should fail cat > "${contextdir}"/Containerfile << 'EOF' FROM --after=self scratch AS self EOF run_buildah 125 build $WITH_POLICY_JSON "${contextdir}" expect_output --substring "FROM --after=self: cannot depend on later stage" # attempt a circular dependency cat > "${contextdir}"/Containerfile << 'EOF' FROM --after=second busybox AS first RUN echo first FROM first AS second RUN echo second EOF run_buildah 125 build $WITH_POLICY_JSON "${contextdir}" expect_output --substring "FROM --after=second: stage \"second\" not found" } @test "bud-windows-host-process-container" { skip_if_rootless_environment # Test building a Windows container image on Linux # This validates that the Windows layer mutations are applied correctly _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/windows-context mkdir -p ${contextdir} # Create a test file to copy into the Windows image echo "test" > ${contextdir}/testfile # Create inline Dockerfile for Windows container cat > ${contextdir}/Dockerfile << _EOF FROM alpine as build RUN echo "test" > testfile FROM --platform=windows/amd64 mcr.microsoft.com/oss/kubernetes/windows-host-process-containers-base-image:v1.0.0 COPY --from=build testfile /testfile ENV PATH="C:\Windows\system32;C:\Windows;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;" USER ContainerAdministrator _EOF # Build the Windows container image directly to OCI format local ocidir=${TEST_SCRATCH_DIR}/oci-windows run_buildah build $WITH_POLICY_JSON -t oci:${ocidir} ${contextdir} # Create a container from the OCI image and mount it run_buildah from --quiet oci:${ocidir} # Extract just the container ID (last line), ignoring warning messages cid=$(echo "$output" | tail -1) run_buildah mount ${cid} root=$(echo "$output" | tail -1) # Verify the Windows-specific directory structure was created # The Windows mutator should create /Hives and /Files directories test -d ${root}/Hives/ test -d ${root}/Files/ # Verify that the copied testfile exists in the correct location # The COPY instruction copies to /testfile, but the Windows mutator # should relocate it to /Files/testfile test -f ${root}/Files/testfile # Verify Windows-specific PAX attributes are present in the layer # Get the last layer blob using helper function local layer_file=${ocidir}/$(oci_image_last_diff ${ocidir}) # Decompress the layer and check for Windows-specific PAX headers # The layer contains PAX extended headers with MSWINDOWS.fileattr local layer_tar=${TEST_SCRATCH_DIR}/layer.tar gzip -dc ${layer_file} > ${layer_tar} 2>/dev/null || cat ${layer_file} > ${layer_tar} # Verify the layer contains the expected Windows structure run tar -tf ${layer_tar} expect_output --substring "Hives/" expect_output --substring "Files/testfile" # Check for Windows-specific PAX headers on all expected paths # Each file/directory should have: MSWINDOWS.fileattr, MSWINDOWS.rawsd, and LIBARCHIVE.creationtime # Use tar's verbose output which shows when it encounters these headers local required_paths=("Hives/" "Files/" "Files/testfile") # Run tar and capture its stderr which contains PAX header warnings local tar_output=${TEST_SCRATCH_DIR}/tar_output.txt tar -tvf ${layer_tar} 2>&1 | tee ${tar_output} # For each path, verify that the PAX headers appear immediately before it for path in "${required_paths[@]}"; do # Extract lines before this path entry (PAX warnings appear just before the file) # We need to find the block of PAX warnings that precede this specific path local line_num=$(grep -n "^.*${path}\$" ${tar_output} | head -1 | cut -d: -f1) if [ -z "${line_num}" ]; then echo "ERROR: Path ${path} not found in tar listing" false fi # Get the preceding 10 lines which should contain PAX header warnings local context_start=$((line_num - 10)) if [ ${context_start} -lt 1 ]; then context_start=1 fi local preceding_lines=$(sed -n "${context_start},$((line_num - 1))p" ${tar_output}) # Check for required headers in the preceding PAX warnings if ! echo "${preceding_lines}" | grep -q "MSWINDOWS.fileattr" ; then echo "ERROR: MSWINDOWS.fileattr not found for ${path}" echo "Context around ${path}:" sed -n "${context_start},${line_num}p" ${tar_output} false fi if ! echo "${preceding_lines}" | grep -q "LIBARCHIVE.creationtime" ; then echo "ERROR: LIBARCHIVE.creationtime not found for ${path}" echo "Context around ${path}:" sed -n "${context_start},${line_num}p" ${tar_output} false fi # MSWINDOWS.rawsd should be present for all paths (files and directories) if ! echo "${preceding_lines}" | grep -q "MSWINDOWS.rawsd" ; then echo "ERROR: MSWINDOWS.rawsd not found for ${path}" echo "Context around ${path}:" sed -n "${context_start},${line_num}p" ${tar_output} false fi done # Cleanup run_buildah umount ${cid} run_buildah rm ${cid} } @test "bud-windows-with-timestamp-reproducible" { skip_if_rootless_environment _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/windows-context mkdir -p ${contextdir} cat > ${contextdir}/Dockerfile << _EOF FROM alpine as build RUN echo "test" > testfile FROM --platform=windows/amd64 mcr.microsoft.com/oss/kubernetes/windows-host-process-containers-base-image:v1.0.0 COPY --from=build testfile /testfile _EOF # Build twice with the same timestamp - should produce identical results local timestamp=86400 run_buildah build $WITH_POLICY_JSON --timestamp=${timestamp} -t oci:${TEST_SCRATCH_DIR}/windows-a ${contextdir} sleep 1.1 # sleep at least 1 second, so that timestamps are incremented run_buildah build $WITH_POLICY_JSON --timestamp=${timestamp} -t oci:${TEST_SCRATCH_DIR}/windows-b ${contextdir} # should be the same (all timestamps including Hives and Files directories should be identical) diff -r "${TEST_SCRATCH_DIR}/windows-a" "${TEST_SCRATCH_DIR}/windows-b" # Verify that all files in the layer have the correct timestamp local layer=${TEST_SCRATCH_DIR}/windows-a/$(oci_image_last_diff ${TEST_SCRATCH_DIR}/windows-a) mkdir -p ${TEST_SCRATCH_DIR}/layer gzip -dc ${layer} > ${TEST_SCRATCH_DIR}/layer.tar 2>/dev/null || cat ${layer} > ${TEST_SCRATCH_DIR}/layer.tar tar -C ${TEST_SCRATCH_DIR}/layer -xvf ${TEST_SCRATCH_DIR}/layer.tar # Check timestamps on all files including Hives and Files directories for file in $(find ${TEST_SCRATCH_DIR}/layer/* -print) ; do run stat -c %Y $file assert $status = 0 "checking datestamp on $file in layer" assert "$output" = "$timestamp" "unexpected datestamp on $file in layer (expected ${timestamp}, got ${output})" done } @test "bud-windows-with-source-date-epoch-reproducible" { skip_if_rootless_environment _prefetch alpine local contextdir=${TEST_SCRATCH_DIR}/windows-context mkdir -p ${contextdir} # Create files with different timestamps to test clamping behavior local old_timestamp=10000 # Older than source-date-epoch (should be preserved) local source_date_epoch=86400 # The clamping threshold # Create an old file (timestamp older than source-date-epoch) echo "old" > ${contextdir}/oldfile touch -d @${old_timestamp} ${contextdir}/oldfile # Create a new file (timestamp will be current time, newer than source-date-epoch) echo "new" > ${contextdir}/newfile cat > ${contextdir}/Dockerfile << _EOF FROM --platform=windows/amd64 mcr.microsoft.com/oss/kubernetes/windows-host-process-containers-base-image:v1.0.0 COPY oldfile /oldfile COPY newfile /newfile _EOF # Build twice with the same source-date-epoch and rewrite-timestamp run_buildah build $WITH_POLICY_JSON --source-date-epoch=${source_date_epoch} --rewrite-timestamp -t oci:${TEST_SCRATCH_DIR}/windows-a ${contextdir} sleep 1.1 # sleep at least 1 second, so that timestamps are incremented run_buildah build $WITH_POLICY_JSON --source-date-epoch=${source_date_epoch} --rewrite-timestamp -t oci:${TEST_SCRATCH_DIR}/windows-b ${contextdir} # should be the same (reproducible builds) diff -r "${TEST_SCRATCH_DIR}/windows-a" "${TEST_SCRATCH_DIR}/windows-b" # Verify timestamp clamping behavior local layer=${TEST_SCRATCH_DIR}/windows-a/$(oci_image_last_diff ${TEST_SCRATCH_DIR}/windows-a) mkdir -p ${TEST_SCRATCH_DIR}/layer gzip -dc ${layer} > ${TEST_SCRATCH_DIR}/layer.tar 2>/dev/null || cat ${layer} > ${TEST_SCRATCH_DIR}/layer.tar tar -C ${TEST_SCRATCH_DIR}/layer -xvf ${TEST_SCRATCH_DIR}/layer.tar # Check that Hives and Files directories are clamped to source-date-epoch run stat -c %Y ${TEST_SCRATCH_DIR}/layer/Hives assert $status = 0 "checking datestamp on Hives directory" assert "$output" = "$source_date_epoch" "Hives directory should be clamped to source-date-epoch (expected ${source_date_epoch}, got ${output})" run stat -c %Y ${TEST_SCRATCH_DIR}/layer/Files assert $status = 0 "checking datestamp on Files directory" assert "$output" = "$source_date_epoch" "Files directory should be clamped to source-date-epoch (expected ${source_date_epoch}, got ${output})" # Check that oldfile preserves its old timestamp (older than source-date-epoch) run stat -c %Y ${TEST_SCRATCH_DIR}/layer/Files/oldfile assert $status = 0 "checking datestamp on oldfile" assert "$output" = "$old_timestamp" "oldfile should preserve old timestamp (expected ${old_timestamp}, got ${output})" # Check that newfile is clamped to source-date-epoch (was newer, got clamped) run stat -c %Y ${TEST_SCRATCH_DIR}/layer/Files/newfile assert $status = 0 "checking datestamp on newfile" assert "$output" = "$source_date_epoch" "newfile should be clamped to source-date-epoch (expected ${source_date_epoch}, got ${output})" } @test "use-secret-to-env-variable" { local outpath="${TEST_SCRATCH_DIR}/timestamp-after-secret.tar" BAR=baz run_buildah build --secret id=mysecret,env=BAR -f <(printf "FROM alpine\nRUN --mount=type=secret,id=mysecret,env=FOO,required sh -c 'echo "'"Hello $FOO"'"'") --tag=oci-archive:${outpath}.a --timestamp 0 expect_output --substring "Hello baz" BAR=boz run_buildah build --secret id=mysecret,env=BAR -f <(printf "FROM alpine\nRUN --mount=type=secret,id=mysecret,env=FOO,required sh -c 'echo "'"Hello $FOO"'"'") --tag=oci-archive:${outpath}.b --timestamp 0 expect_output --substring "Hello boz" # even though different secret was passed to each(baz vs boz), we expect the same result, ie should not affect build history diff "${outpath}.a" "${outpath}.b" } @test "use-secret-to-env-variable-and-file-path" { local outpath="${TEST_SCRATCH_DIR}/timestamp-after-secret-and-path.tar" # here we mount the secret as both an env variable AND at a file path # neither of which should affect the build history BAR=baz run_buildah build --secret id=mysecret,env=BAR -f <(printf "FROM alpine\nRUN --mount=type=secret,id=mysecret,env=FOO,target=/foo,required sh -c 'echo -n "'"Hello $FOO "'" && cat /foo'") --tag=oci-archive:${outpath}.a --timestamp 0 expect_output --substring "Hello baz baz" BAR=boz run_buildah build --secret id=mysecret,env=BAR -f <(printf "FROM alpine\nRUN --mount=type=secret,id=mysecret,env=FOO,target=/foo,required sh -c 'echo -n "'"Hello $FOO "'" && cat /foo'") --tag=oci-archive:${outpath}.b --timestamp 0 expect_output --substring "Hello boz boz" # even though different secret was passed to each(baz vs boz), we expect the same result, ie should not affect build history diff "${outpath}.a" "${outpath}.b" } @test "[test flags] --stage-labels without --save-stages should error" { _prefetch alpine target="stage-labels-error-test-$(safename)" run_buildah 125 build $WITH_POLICY_JSON --stage-labels -t ${target} -f $BUDFILES/save-stages/Dockerfile.single-build-stage $BUDFILES/save-stages expect_output --substring '"--stage-labels" requires "--save-stages"' } @test "[test flags] bud with --save-stages=false does not save intermediate images" { _prefetch alpine target="save-stages-$(safename)" run_buildah build $WITH_POLICY_JSON --save-stages=false -t ${target}-without-save -f $BUDFILES/save-stages/Dockerfile.single-build-stage $BUDFILES/save-stages run_buildah images -a baseline_images=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') baseline_count=$(echo "$baseline_images" | wc -w) assert "$baseline_count" == "0" "without save-stages should have 0 intermediate images" } @test "[test flags] --save-stages and --stage-labels=false does not label intermediate images" { _prefetch alpine target="save-stages-$(safename)" run_buildah build $WITH_POLICY_JSON --save-stages --stage-labels=false -t ${target} -f $BUDFILES/save-stages/Dockerfile.single-build-stage $BUDFILES/save-stages run_buildah images -a intermediate_images=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') intermediate_count=$(echo "$intermediate_images" | wc -w) assert "$intermediate_count" == "1" "should have 1 intermediate image" for image_id in $intermediate_images; do run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.name"}}' $image_id assert "$output" == "" "stage.name label should not be present when --stage-labels=false" run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.base"}}' $image_id assert "$output" == "" "stage.base label should not be present when --stage-labels=false" done } @test "[test saving intermediate images] --save-stages on single-stage Dockerfile should not create intermediate images" { _prefetch alpine target="single-stage-test-$(safename)" run_buildah build $WITH_POLICY_JSON --save-stages -t ${target} -f $BUDFILES/save-stages/Dockerfile.simple $BUDFILES/save-stages run_buildah images -a intermediate_images=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') intermediate_count=$(echo "$intermediate_images" | wc -w) assert "$intermediate_count" == "0" "single-stage build should not create intermediate images" run_buildah rmi ${target} } @test "[test saving intermediate images] --save-stages preserves intermediate image and skips unused" { _prefetch alpine target="save-stages-$(safename)" # Build without --save-stages (baseline - should produce 0 intermediate images) run_buildah build $WITH_POLICY_JSON -t ${target}-no-save -f $BUDFILES/save-stages/Dockerfile.two-build-stages-one-unused $BUDFILES/save-stages run_buildah images -a baseline_images=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') baseline_count=$(echo "$baseline_images" | wc -w) assert "$baseline_count" == "0" "without save-stages should have 0 intermediate images" run_buildah rmi ${target}-no-save # Build with --save-stages (should produce 1 intermediate image: 'intermediate' stage) # Note: 'builder' stage is skipped due to --skip-unused-stages=true (default) run_buildah build $WITH_POLICY_JSON --save-stages -t ${target} -f $BUDFILES/save-stages/Dockerfile.two-build-stages-one-unused $BUDFILES/save-stages run_buildah images -a save_images=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') save_count=$(echo "$save_images" | wc -w) assert "$save_count" == "1" "with save-stages should have 1 intermediate image (intermediate stage)" } @test "[test saving intermediate images] --save-stages preserves intermediate images for all stages" { _prefetch alpine target="save-stages-all-test-$(safename)" # Build without --save-stages (baseline - should produce 0 intermediate images) run_buildah build $WITH_POLICY_JSON -t ${target}-no-save -f $BUDFILES/save-stages/Dockerfile.three-build-stages-parent-child-independent $BUDFILES/save-stages run_buildah images -a baseline_images=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') baseline_count=$(echo "$baseline_images" | wc -w) assert "$baseline_count" == "0" "without save-stages should have 0 intermediate images" run_buildah rmi ${target}-no-save # Build with --save-stages (should produce 3 intermediate images: parent, child, independent) # This demonstrates that intermediate images are saved for ALL stages: for each in parent-child chain and independent stage run_buildah build $WITH_POLICY_JSON --save-stages -t ${target} -f $BUDFILES/save-stages/Dockerfile.three-build-stages-parent-child-independent $BUDFILES/save-stages run_buildah images -a save_images=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') save_count=$(echo "$save_images" | wc -w) assert "$save_count" == "3" "with save-stages should have 3 intermediate images (parent, child, independent)" } @test "[test labeling intermediate images] --stage-labels adds metadata to config and history of the intermediate image and final image" { _prefetch alpine target="stage-labels-test-$(safename)" run_buildah build $WITH_POLICY_JSON --save-stages --stage-labels -t ${target} -f $BUDFILES/save-stages/Dockerfile.single-build-stage $BUDFILES/save-stages run_buildah images -a intermediate_image=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') intermediate_count=$(echo "$intermediate_image" | wc -w) assert "$intermediate_count" == "1" "should have exactly 1 intermediate image" run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.name"}}' $intermediate_image assert "$output" "==" "intermediate" "stage.name should be 'intermediate'" run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.base"}}' $intermediate_image assert "$output" "=~" "alpine" "stage.base should reference alpine image" # Verify LABEL instruction appears in history (not just in config.Labels) run_buildah inspect --format '{{range .OCIv1.History}}{{.CreatedBy}}{{"\n"}}{{end}}' $intermediate_image expect_output --substring "LABEL" # Verify exact label names and values in history expect_output --substring '"io.buildah.stage.name"="intermediate"' "history should contain exact stage.name label" expect_output --substring '"io.buildah.stage.base"="alpine"' "history should contain exact stage.base label" # Verify final image also has stage labels (stage position and base pullspec) run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.name"}}' ${target} assert "$output" "==" "1" "final stage.name should be stage position '1' (no alias)" run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.base"}}' ${target} assert "$output" "==" "alpine" "final stage.base should be 'alpine' pullspec" } @test "[test labeling intermediate images] chain of two stages - labels are indicating that one stage is using another stage as base" { _prefetch alpine target="chained-stages-test-$(safename)" run_buildah build $WITH_POLICY_JSON --save-stages --stage-labels -t ${target} -f $BUDFILES/save-stages/Dockerfile.chained-two-build-stages $BUDFILES/save-stages run_buildah images -a all_intermediate_images=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') parent_image_id="" parent_name="" parent_base="" child_image_id="" child_name="" child_base="" for image_id in $all_intermediate_images; do run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.name"}}' $image_id name="$output" run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.base"}}' $image_id base="$output" # Check if base is in the list of intermediate images (stage is used by another stage as base) is_child=false for iid in $all_intermediate_images; do # base in label is full image ID, but all_intermediate_images contains short IDs, so check prefix if [[ "$base" == "$iid"* ]]; then is_child=true break fi done if [ "$is_child" = true ]; then # This is a child stage: its base is another intermediate image child_image_id="$image_id" child_name="$name" child_base="$base" else # This is a parent stage that serves as base for another stage parent_image_id="$image_id" parent_name="$name" parent_base="$base" fi done assert "$parent_image_id" "!=" "" "parent stage should be present" assert "$child_image_id" "!=" "" "child stage should be present" assert "$parent_name" "==" "parent-stage" "parent stage name should be 'parent-stage'" assert "$child_name" "==" "child-stage" "child stage name should be 'child-stage'" assert "$parent_base" "=~" "alpine" "parent stage.base should reference alpine image" # Verify child stage uses parent as base (child_base contains full ID, parent_image_id contains short ID) assert "$child_base" "=~" "^$parent_image_id" "child stage.base should start with parent image ID" } @test "[test labeling intermediate images] --jobs 1 and 2 correctly resolve stage references to imageID in labels" { _prefetch alpine for jobs in 1 2; do target="imageid-resolution-jobs${jobs}-$(safename)" # Build with specified --jobs value run_buildah build $WITH_POLICY_JSON --jobs $jobs --save-stages --stage-labels --layers -t ${target} -f $BUDFILES/save-stages/Dockerfile.chained-two-build-stages $BUDFILES/save-stages run_buildah images -a all_intermediate_images=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') # Find parent-stage and child-stage intermediate images parent_image="" child_image="" for image_id in $all_intermediate_images; do run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.name"}}' $image_id if [ "$output" = "parent-stage" ]; then parent_image="$image_id" elif [ "$output" = "child-stage" ]; then child_image="$image_id" fi done assert "$parent_image" "!=" "" "parent-stage intermediate image should exist (jobs=$jobs)" assert "$child_image" "!=" "" "child-stage intermediate image should exist (jobs=$jobs)" # Get child-stage base label value run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.base"}}' $child_image child_base="$output" # Verify base label is an imageID from intermediate images (not alias "parent-stage") found_in_images=false for iid in $all_intermediate_images; do if [[ "$child_base" == "$iid"* ]]; then found_in_images=true break fi done assert "$found_in_images" == "true" "child stage.base should be imageID from intermediate images (jobs=$jobs)" run_buildah rmi -a -f _prefetch alpine done } @test "[test labeling intermediate images] two independent intermediate images are correctly labeled" { _prefetch alpine target="two-stages-test-$(safename)" run_buildah build $WITH_POLICY_JSON --save-stages --stage-labels -t ${target} -f $BUDFILES/save-stages/Dockerfile.two-build-stages $BUDFILES/save-stages run_buildah images -a all_intermediate_images=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') intermediate_count=$(echo "$all_intermediate_images" | wc -w) assert "$intermediate_count" == "2" "should have exactly 2 intermediate images" stage_names="" for image_id in $all_intermediate_images; do run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.name"}}' $image_id stage_name="$output" stage_names="$stage_names $stage_name" run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.base"}}' $image_id assert "$output" "=~" "alpine" "stage base should be alpine" done assert "$stage_names" "=~" "alias1" "should have alias1 stage" assert "$stage_names" "=~" "alias2" "should have alias2 stage" } @test "[test labeling intermediate images] intermediate images from chain of three stages are correctly labeled" { _prefetch alpine target="chained-multiple-stages-test-$(safename)" run_buildah build $WITH_POLICY_JSON --save-stages --stage-labels -t ${target} -f $BUDFILES/save-stages/Dockerfile.chained-three-build-stages $BUDFILES/save-stages run_buildah images -a all_intermediate_images=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') intermediate_count=$(echo "$all_intermediate_images" | wc -w) assert "$intermediate_count" == "3" "should have exactly 3 intermediate images in chain" declare -A stage_ids declare -A stage_bases for image_id in $all_intermediate_images; do run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.name"}}' $image_id stage_name="$output" stage_ids[$stage_name]="$image_id" run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.base"}}' $image_id stage_bases[$stage_name]="$output" done # Verify all 3 stages exist and have correct names assert "${stage_ids[base]}" "!=" "" "should have base stage" assert "${stage_ids[stage1]}" "!=" "" "should have stage1 stage" assert "${stage_ids[stage2]}" "!=" "" "should have stage2 stage" # Verify stage base references assert "${stage_bases[base]}" "=~" "alpine" "base should use alpine as base" # stage_bases contains full IDs, stage_ids contains short IDs - use prefix match assert "${stage_bases[stage1]}" "=~" "^${stage_ids[base]}" "stage1 base should start with base image ID" assert "${stage_bases[stage2]}" "=~" "^${stage_ids[stage1]}" "stage2 base should start with stage1 image ID" } @test "[test labeling intermediate images] intermediate images with ARG in stage names and bases in Containerfile are correctly labeled" { _prefetch alpine target="arg-stages-chained-stages-test-$(safename)" run_buildah build $WITH_POLICY_JSON --save-stages --stage-labels -t ${target} -f $BUDFILES/save-stages/Dockerfile.arg-build-stages-and-chained-build-stages $BUDFILES/save-stages run_buildah images -a all_intermediate_images=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') intermediate_count=$(echo "$all_intermediate_images" | wc -w) assert "$intermediate_count" == "2" "should have exactly 2 intermediate images" declare -A stage_ids declare -A stage_bases declare -a found_stage_names for image_id in $all_intermediate_images; do run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.name"}}' $image_id stage_name="$output" stage_ids[$stage_name]="$image_id" found_stage_names+=("$stage_name") assert "$stage_name" "!~" '\$\{' "stage name should not contain unresolved ARG placeholders like \${...}" run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.base"}}' $image_id stage_base="$output" stage_bases[$stage_name]="$stage_base" assert "$stage_base" "!~" '\$\{' "stage base should not contain unresolved ARG placeholders like \${...}" done # Verify exact stage names are resolved from ARG variables (images returned newest first) assert "${found_stage_names[0]}" == "second-stage" "first image should be 'second-stage' (resolved from \${SECOND_STAGE})" assert "${found_stage_names[1]}" == "first-stage" "second image should be 'first-stage' (resolved from \${FIRST_STAGE})" # Verify stage bases are resolved correctly assert "${stage_bases[first-stage]}" == "alpine" "first-stage base should reference alpine image" assert "${stage_bases[second-stage]}" "=~" "^${stage_ids[first-stage]}" "second-stage base should reference first-stage image ID (resolved from \${FIRST_STAGE})" } @test "[test labeling intermediate images] intermediate images without aliases are correctly labeled" { _prefetch alpine target=save-stages-no-aliases-$(safename) run_buildah build $WITH_POLICY_JSON --save-stages --stage-labels -t ${target} -f $BUDFILES/save-stages/Dockerfile.chained-two-build-stages-no-aliases $BUDFILES/save-stages run_buildah images -a intermediate_images=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') num_intermediate=$(echo "$intermediate_images" | wc -w) assert "$num_intermediate" "==" "2" "should have exactly 2 intermediate images (stage 0 and stage 1)" declare -A stage_ids for img_id in $intermediate_images; do run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.name"}}' $img_id stage_name="$output" stage_ids[$stage_name]="$img_id" done stage0_id="${stage_ids[0]}" assert "$stage0_id" "!=" "" "stage 0 should exist" run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.base"}}' $stage0_id stage0_base="$output" assert "$stage0_base" "=~" "alpine" "stage 0 stage.base should reference alpine image" stage1_id="${stage_ids[1]}" assert "$stage1_id" "!=" "" "stage 1 should exist" run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.base"}}' $stage1_id stage1_base="$output" assert "$stage1_base" "=~" "^$stage0_id" "stage 1 stage.base should reference stage 0 image ID" } @test "[test labeling intermediate images] empty intermediate stage (only FROM, no instructions) does not create labeled image" { _prefetch alpine target="empty-stage-$(safename)" # Build with --save-stages --stage-labels # The 'builder' stage has no instructions (only FROM alpine:latest AS builder) run_buildah build $WITH_POLICY_JSON --save-stages --stage-labels -t ${target} -f $BUDFILES/save-stages/Dockerfile.empty-intermediate-build-stage $BUDFILES/save-stages # Check intermediate images run_buildah images -a intermediate_images=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') num_intermediate=$(echo "$intermediate_images" | wc -w) # Explanation: Empty stage (only FROM AS alias, no RUN/COPY/etc) is just an alias # for the base image. It doesn't need metadata labels or a separate image. # The final stage can reference it via COPY --from=builder, which resolves to alpine:latest. assert "$num_intermediate" "==" "0" "empty intermediate stage (only FROM) should not create intermediate image with labels" } @test "[test final image] --save-stages build has same final image RootFS.DiffIDs count as build without args (all parent layers and single layer squashing all final stage instructions)" { _prefetch alpine target_cache="save-stages-layer-count-$(safename)" target_normal="normal-layer-count-$(safename)" target_layers="with-layers-count-$(safename)" run_buildah build $WITH_POLICY_JSON --save-stages --stage-labels -t ${target_cache} -f $BUDFILES/save-stages/Dockerfile.two-build-stages $BUDFILES/save-stages run_buildah inspect --format '{{len .OCIv1.RootFS.DiffIDs}}' ${target_cache} save_stages_count=$output run_buildah build $WITH_POLICY_JSON -t ${target_normal} -f $BUDFILES/save-stages/Dockerfile.two-build-stages $BUDFILES/save-stages run_buildah inspect --format '{{len .OCIv1.RootFS.DiffIDs}}' ${target_normal} normal_count=$output run_buildah build $WITH_POLICY_JSON --layers -t ${target_layers} -f $BUDFILES/save-stages/Dockerfile.two-build-stages $BUDFILES/save-stages run_buildah inspect --format '{{len .OCIv1.RootFS.DiffIDs}}' ${target_layers} layers_count=$output assert "$save_stages_count" == "2" "--save-stages build should have exactly 2 layers for given Containerfile" assert "$normal_count" == "2" "regular build should have exactly 2 layers for given Containerfile" assert "$layers_count" == "4" "--layers should create exactly 4 layers (one per instruction) for given Containerfile" } @test "[test final image] final stage uses previous stage as base, but does not inherit its labels - it has its own labels" { _prefetch alpine target="final-no-inherit-labels-$(safename)" run_buildah build $WITH_POLICY_JSON --save-stages --stage-labels -t ${target} -f $BUDFILES/save-stages/Dockerfile.final-uses-build-stage $BUDFILES/save-stages run_buildah images -a intermediate_image=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') # Verify intermediate stage has stage labels run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.name"}}' $intermediate_image assert "$output" "==" "intermediate" "intermediate stage should have stage.name label" run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.base"}}' $intermediate_image assert "$output" "=~" "alpine" "intermediate stage should have stage.base label referencing alpine" # Verify final image has its own labels run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.name"}}' ${target} assert "$output" "==" "1" "final image should have 1 as stage.name label" run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.base"}}' ${target} assert "$output" "=~" "^$intermediate_image" "final image should have image ID of the previous stage as stage.base label" } @test "[test caching] Build 1: --layers --save-stages --stage-labels | Build 2: --layers --save-stages --stage-labels | Cache: layers can be reused" { _prefetch alpine target="cache-reuse-test-$(safename)" run_buildah build $WITH_POLICY_JSON --layers --save-stages --stage-labels -t ${target}-first -f $BUDFILES/save-stages/Dockerfile.single-build-stage $BUDFILES/save-stages first_build_image_id=$(echo "$output" | tail -1) # Get all intermediate layers from first build run_buildah images -a all_intermediate_layers_first=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') first_intermediate_layers_count=$(echo "$all_intermediate_layers_first" | wc -w) assert "$first_intermediate_layers_count" -eq 4 "first build should create 4 intermediate layers (LABEL from --stage-labels for intermediate and final stage, RUN from intermediate stage, COPY from final stage)" # Second build should reuse the layers created by first build (cache hit) run_buildah build $WITH_POLICY_JSON --layers --save-stages --stage-labels -t ${target}-second -f $BUDFILES/save-stages/Dockerfile.single-build-stage $BUDFILES/save-stages second_build_image_id=$(echo "$output" | tail -1) expect_output --substring "Using cache" # Verify cache was used 4 times (LABEL from --stage-labels (intermediate and final stage), RUN from intermediate stage, COPY from final stage, RUN from final stage) cache_count=$(echo "$output" | grep -c "Using cache") assert "$cache_count" -eq 5 "should use cache 5 times (LABEL (intermediate and final stage), RUN intermediate, COPY, RUN final)" # Get all intermediate layers from second build - second build should not add any more run_buildah images -a all_intermediate_layers_second=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') second_intermediate_layers_count=$(echo "$all_intermediate_layers_second" | wc -w) assert "$second_intermediate_layers_count" -eq 4 "second build should also have 4 intermediate layers" # Verify final image IDs match (complete cache hit) assert "$first_build_image_id" "==" "$second_build_image_id" "final image ID should match when cache is fully reused" } @test "[test caching] Build 1: --save-stages | Build 2: --save-stages | Cache: intermediate images cannot be reused" { _prefetch alpine target="cache-accumulate-$(safename)" # First build run_buildah build $WITH_POLICY_JSON --save-stages -t ${target}-first -f $BUDFILES/save-stages/Dockerfile.single-build-stage $BUDFILES/save-stages run_buildah images -a intermediate_images_first=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') count_first=$(echo "$intermediate_images_first" | wc -w) assert "$count_first" -eq 1 "first build should create 1 intermediate image" # Second build - intermediate images accumulate (no cache reuse without --layers) run_buildah build $WITH_POLICY_JSON --save-stages -t ${target}-second -f $BUDFILES/save-stages/Dockerfile.single-build-stage $BUDFILES/save-stages assert "$output" "!~" "Using cache" "cache should not be used without --layers" run_buildah images -a intermediate_images_second=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') count_second=$(echo "$intermediate_images_second" | wc -w) assert "$count_second" -eq 2 "second build should create another intermediate image (total 2)" # Third build - accumulate more run_buildah build $WITH_POLICY_JSON --save-stages -t ${target}-third -f $BUDFILES/save-stages/Dockerfile.single-build-stage $BUDFILES/save-stages assert "$output" "!~" "Using cache" "cache should not be used without --layers" run_buildah images -a intermediate_images_third=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') count_third=$(echo "$intermediate_images_third" | wc -w) assert "$count_third" -eq 3 "third build should create another intermediate image (total 3)" } @test "[test caching] Build 1: --save-stages --stage-labels | Build 2: --save-stages --stage-labels | Cache: intermediate images cannot be reused" { _prefetch alpine target="cache-accumulate-labels-$(safename)" # First build run_buildah build $WITH_POLICY_JSON --save-stages --stage-labels -t ${target}-first -f $BUDFILES/save-stages/Dockerfile.single-build-stage $BUDFILES/save-stages run_buildah images -a intermediate_images_first=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') count_first=$(echo "$intermediate_images_first" | wc -w) assert "$count_first" -eq 1 "first build should create 1 intermediate image with labels" # Second build - intermediate images accumulate (no cache reuse without --layers) run_buildah build $WITH_POLICY_JSON --save-stages --stage-labels -t ${target}-second -f $BUDFILES/save-stages/Dockerfile.single-build-stage $BUDFILES/save-stages assert "$output" "!~" "Using cache" "cache should not be used without --layers (even with stage labels)" run_buildah images -a intermediate_images_second=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') count_second=$(echo "$intermediate_images_second" | wc -w) assert "$count_second" -eq 2 "second build should create another intermediate image (total 2)" # Third build - accumulate more run_buildah build $WITH_POLICY_JSON --save-stages --stage-labels -t ${target}-third -f $BUDFILES/save-stages/Dockerfile.single-build-stage $BUDFILES/save-stages assert "$output" "!~" "Using cache" "cache should not be used without --layers (even with stage labels)" run_buildah images -a intermediate_images_third=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') count_third=$(echo "$intermediate_images_third" | wc -w) assert "$count_third" -eq 3 "third build should create another intermediate image (total 3)" } @test "[test caching] Build 1: --save-stages --layers | Build 2: --save-stages --layers | Cache: intermediate images can be reused" { _prefetch alpine target="cache-reuse-layers-$(safename)" # First build run_buildah build $WITH_POLICY_JSON --save-stages --layers -t ${target}-first -f $BUDFILES/save-stages/Dockerfile.single-build-stage $BUDFILES/save-stages first_build_final_image_id=$(echo "$output" | tail -1) run_buildah images -a intermediate_images_first=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') count_first=$(echo "$intermediate_images_first" | wc -w) assert "$count_first" -eq 2 "first build should create 2 intermediate layers (RUN from intermediate stage, COPY from final stage)" # Build 2 should reuse all layers run_buildah build $WITH_POLICY_JSON --save-stages --layers -t ${target}-second -f $BUDFILES/save-stages/Dockerfile.single-build-stage $BUDFILES/save-stages second_build_final_image_id=$(echo "$output" | tail -1) expect_output --substring "Using cache" # Verify cache was used 3 times (RUN from intermediate stage, COPY from final stage, RUN from final stage) cache_count=$(echo "$output" | grep -c "Using cache") assert "$cache_count" -eq 3 "should use cache 3 times (RUN intermediate, COPY, RUN final)" run_buildah images -a intermediate_images_second=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') count_second=$(echo "$intermediate_images_second" | wc -w) assert "$count_second" -eq 2 "second build should reuse layers, not create new ones (still 2 total)" # Full cache reuse - final image is same assert $first_build_final_image_id "==" $second_build_final_image_id } @test "[test caching] Build 1: --save-stages --stage-labels --layers | Build 2: --save-stages --layers | Cache: No cache reuse - missing labels are causing cache miss for each stage" { _prefetch alpine target="cache-no-reuse-$(safename)" # First build run_buildah build $WITH_POLICY_JSON --save-stages --stage-labels --layers -t ${target}-first -f $BUDFILES/save-stages/Dockerfile.single-build-stage $BUDFILES/save-stages run_buildah images -a intermediate_images_first=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') count_first=$(echo "$intermediate_images_first" | wc -w) assert "$count_first" -eq 4 "first build should create 4 intermediate layers (LABEL (intermediate and final), RUN, COPY)" # Build 2 - no cache reuse run_buildah build $WITH_POLICY_JSON --save-stages --layers -t ${target}-second -f $BUDFILES/save-stages/Dockerfile.single-build-stage $BUDFILES/save-stages # Verify cache was not used assert "$output" !~ "Using cache" "should NOT use cache because LABEL was added" run_buildah images -a intermediate_images_second=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') count_second=$(echo "$intermediate_images_second" | wc -w) assert "$count_second" -eq 6 "second build should have 6 intermediate layers (4 from first + 2 instruction layers from second)" } @test "[test caching] Build 1: --save-stages --layers | Build 2: --save-stages --stage-labels --layers | Cache: No cache reuse - added labels are causing cache miss for each stage" { _prefetch alpine target="cache-no-reuse-$(safename)" # First build run_buildah build $WITH_POLICY_JSON --save-stages --layers -t ${target}-first -f $BUDFILES/save-stages/Dockerfile.single-build-stage $BUDFILES/save-stages run_buildah images -a intermediate_images_first=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') count_first=$(echo "$intermediate_images_first" | wc -w) assert "$count_first" -eq 2 "first build should create 2 intermediate layers (RUN from intermediate, COPY from final)" # Second build (adds LABEL layers) run_buildah build $WITH_POLICY_JSON --save-stages --stage-labels --layers -t ${target}-second -f $BUDFILES/save-stages/Dockerfile.single-build-stage $BUDFILES/save-stages # Verify cache was not used assert "$output" !~ "Using cache" "should NOT use cache because LABEL was added" run_buildah images -a intermediate_images_second=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') count_second=$(echo "$intermediate_images_second" | wc -w) assert "$count_second" -eq 6 "second build should have 6 intermediate layers (2 from first + 4 from second)" } @test "[test caching] Build 1: --layers | Build 2: --save-stages --stage-labels --layers | Cache: No cache reuse - added labels are causing cache miss for each stage" { _prefetch alpine target="cache-no-reuse-$(safename)" # First build run_buildah build $WITH_POLICY_JSON --layers -t ${target}-first -f $BUDFILES/save-stages/Dockerfile.single-build-stage $BUDFILES/save-stages run_buildah images -a intermediate_images_first=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') count_first=$(echo "$intermediate_images_first" | wc -w) assert "$count_first" -eq 2 "first build should create 2 intermediate layers (RUN, COPY)" # Second build (adds LABEL layer for interemediate and final stage) run_buildah build $WITH_POLICY_JSON --save-stages --stage-labels --layers -t ${target}-second -f $BUDFILES/save-stages/Dockerfile.single-build-stage $BUDFILES/save-stages # Verify cache was not used assert "$output" !~ "Using cache" "should NOT use cache because LABEL was added" run_buildah images -a intermediate_images_second=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') count_second=$(echo "$intermediate_images_second" | wc -w) assert "$count_second" -eq 6 "second build should have 6 intermediate layers (2 from first (instructions) + 4 from second (labels and instructions))" } @test "[test caching] Build 1: --save-stages --stage-labels --layers | Build 2: same flags, modified stage alias | Cache: intermediate stage rebuilt (name label changed)" { _prefetch alpine target="cache-miss-alias-change-$(safename)" # Build 1: with alias "intermediate" run_buildah build $WITH_POLICY_JSON --save-stages --stage-labels --layers -t ${target}-first -f $BUDFILES/save-stages/Dockerfile.single-build-stage-modifiable $BUDFILES/save-stages first_build_final_image_id=$(echo "$output" | tail -1) run_buildah images -a intermediate_images_first=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') count_first=$(echo "$intermediate_images_first" | wc -w) assert "$count_first" -eq 4 "first build should create 4 intermediate layers (LABEL (intermediate and final), RUN, COPY)" # Get last layer from first stage (intermediate stage image) first_intermediate_run_layer=$(echo "$intermediate_images_first" | awk 'NR==3') run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.name"}}' $first_intermediate_run_layer assert "$output" "==" "intermediate" "first build intermediate stage image should have 'intermediate' as stage name" run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.name"}}' $first_build_final_image_id assert "$output" "==" "1" "first build final image should have '1' index as stage name" # Build 2: with modified alias "intermediate-renamed" - same Containerfile but different stage name run_buildah build $WITH_POLICY_JSON --save-stages --stage-labels --layers -t ${target}-second -f $BUDFILES/save-stages/Dockerfile.single-build-stage-modifiable-renamed $BUDFILES/save-stages second_build_final_image_id=$(echo "$output" | tail -1) expect_output --substring "Using cache" # Verify cache was used 3 times cache_count=$(echo "$output" | grep -c "Using cache") assert "$cache_count" -eq 3 "should use cache 3 times (LABEL final, COPY final, RUN final)" run_buildah images -a intermediate_images_second=$(echo "$output" | awk '$1 == "" && $2 == "" {print $3}') count_second=$(echo "$intermediate_images_second" | wc -w) assert "$count_second" -eq 6 "second build should have 5 intermediate layers (4 from first + 2 new; LABEL intermediate and RUN intermediate)" # Get last layer from first stage (intermediate stage image) second_intermediate_run_layer=$(echo "$intermediate_images_second" | awk 'NR==1') run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.name"}}' $second_intermediate_run_layer assert "$output" "==" "intermediate-renamed" "second build should have 'intermediate-renamed' as stage name" # Unchanged run_buildah inspect --format '{{index .OCIv1.Config.Labels "io.buildah.stage.name"}}' $second_build_final_image_id assert "$output" "==" "1" "second build final image should have '1' index as stage name" # Verify final image IDs match (complete cache hit) assert "$first_build_final_image_id" "==" "$second_build_final_image_id" "final image ID should match when cache is fully reused" } ================================================ FILE: tests/bud_overlay_leaks.bats ================================================ #!/usr/bin/env bats load helpers @test "bud overlay storage leaked mount" { if test \! -e /usr/bin/fuse-overlayfs -a "$BUILDAH_ISOLATION" = "rootless"; then skip "BUILDAH_ISOLATION = $BUILDAH_ISOLATION" and no /usr/bin/fuse-overlayfs present fi target=pull run_buildah 125 --storage-driver=overlay bud $WITH_POLICY_JSON -t ${target} --pull-never $BUDFILES/pull expect_output --substring "image not known" leftover=$(mount | grep $TEST_SCRATCH_DIR | cat) if [ -n "$leftover" ]; then die "buildah leaked a mount on error: $leftover" fi } ================================================ FILE: tests/byid.bats ================================================ #!/usr/bin/env bats load helpers @test "from-by-id" { image=busybox _prefetch $image # Pull down the image, if we have to. run_buildah from --quiet --pull=false $WITH_POLICY_JSON $image expect_output "$image-working-container" cid=$output run_buildah rm $cid # Get the image's ID. run_buildah images -q $image expect_line_count 1 iid="$output" # Use the image's ID to create a container. run_buildah from --pull=false $WITH_POLICY_JSON ${iid} expect_line_count 1 cid="$output" run_buildah rm $cid # Use a truncated form of the image's ID to create a container. run_buildah from --pull=false $WITH_POLICY_JSON ${iid:0:6} expect_line_count 1 cid="$output" run_buildah rm $cid run_buildah rmi $iid } @test "inspect-by-id" { image=busybox _prefetch $image # Pull down the image, if we have to. run_buildah from --quiet --pull=false $WITH_POLICY_JSON $image expect_output "$image-working-container" cid=$output run_buildah rm $cid # Get the image's ID. run_buildah images -q $image expect_line_count 1 iid="$output" # Use the image's ID to inspect it. run_buildah inspect --type=image ${iid} # Use a truncated copy of the image's ID to inspect it. run_buildah inspect --type=image ${iid:0:6} run_buildah rmi $iid } @test "push-by-id" { for image in busybox registry.k8s.io/pause ; do echo pulling/pushing image $image _prefetch $image TARGET=${TEST_SCRATCH_DIR}/subdir-$(basename $image) mkdir -p $TARGET $TARGET-truncated # Pull down the image, if we have to. run_buildah from --quiet --cidfile ${TEST_SCRATCH_DIR}/cid --pull=false $WITH_POLICY_JSON $image cid="$(< ${TEST_SCRATCH_DIR}/cid)" run_buildah rm $cid # Get the image's ID. run_buildah images -q $image expect_output --substring '^[0-9a-f]{12,64}$' iid="$output" # Use the image's ID to push it. run_buildah push $WITH_POLICY_JSON $iid dir:$TARGET # Use a truncated form of the image's ID to push it. run_buildah push $WITH_POLICY_JSON ${iid:0:6} dir:$TARGET-truncated # Use the image's complete ID to remove it. run_buildah rmi $iid done } @test "rmi-by-id" { image=busybox _prefetch $image # Pull down the image, if we have to. run_buildah from --quiet --pull=false $WITH_POLICY_JSON $image expect_output "$image-working-container" run_buildah rm $output # Get the image's ID. run_buildah images -q $image expect_output --substring '^[0-9a-f]{12,64}$' iid="$output" # Use a truncated copy of the image's ID to remove it. run_buildah rmi ${iid:0:6} } ================================================ FILE: tests/cdi.bats ================================================ #!/usr/bin/env bats load helpers @test "bud with CDI" { skip_if_chroot _prefetch busybox cdidir=${TEST_SCRATCH_DIR}/cdi-config-dir mkdir -p $cdidir sed -e s:@@hostcdipath@@:$cdidir:g $BUDFILES/cdi/containers-cdi.yaml > $cdidir/containers-cdi.yaml chmod 644 $cdidir/containers-cdi.yaml echo === Begin CDI configuration in $cdidir/containers-cdi.yaml === cat $cdidir/containers-cdi.yaml echo === End CDI configuration === run_buildah build $WITH_POLICY_JSON --cdi-config-dir=$cdidir --security-opt label=disable --device=containers.github.io/sample=all --device=/dev/null:/dev/outsidenull:rwm $BUDFILES/cdi } @test "from with CDI" { skip_if_chroot _prefetch busybox cdidir=${TEST_SCRATCH_DIR}/cdi-config-dir mkdir -p $cdidir sed -e s:@@hostcdipath@@:$cdidir:g $BUDFILES/cdi/containers-cdi.yaml > $cdidir/containers-cdi.yaml chmod 644 $cdidir/containers-cdi.yaml echo === Begin CDI configuration in $cdidir/containers-cdi.yaml === cat $cdidir/containers-cdi.yaml echo === End CDI configuration === run_buildah from $WITH_POLICY_JSON --security-opt label=disable --cdi-config-dir=$cdidir --device=containers.github.io/sample=all --device=/dev/null:/dev/outsidenull:rwm busybox cid="$output" run_buildah run "$cid" cat /dev/containers-cdi.yaml /dev/outsidenull } @test "run with CDI" { skip_if_chroot _prefetch busybox cdidir=${TEST_SCRATCH_DIR}/cdi-config-dir mkdir -p $cdidir sed -e s:@@hostcdipath@@:$cdidir:g $BUDFILES/cdi/containers-cdi.yaml > $cdidir/containers-cdi.yaml chmod 644 $cdidir/containers-cdi.yaml echo === Begin CDI configuration in $cdidir/containers-cdi.yaml === cat $cdidir/containers-cdi.yaml echo === End CDI configuration === run_buildah from $WITH_POLICY_JSON --security-opt label=disable busybox cid="$output" run_buildah run --cdi-config-dir=$cdidir --device=containers.github.io/sample=all "$cid" cat /dev/containers-cdi.yaml } ================================================ FILE: tests/chroot.bats ================================================ #!/usr/bin/env bats load helpers @test "chroot mount flags" { skip_if_no_unshare if ! test -e /etc/subuid ; then skip "we can't bind mount over /etc/subuid during the test if there is no /etc/subuid file" fi if ! test -e /etc/subgid ; then skip "we can't bind mount over /etc/subgid during the test if there is no /etc/subgid file" fi # whom should we map to root in a nested namespace? if is_rootless ; then subid=128 rangesize=1024 else subid=1048576 rangesize=16384 fi # we're going to have to prefetch into storage used by someone else image # chosen because its rootfs doesn't have any uid/gid ownership above # $rangesize, because the nested namespace needs to be able to represent all # of them baseimage=registry.access.redhat.com/ubi9-micro:latest _prefetch $baseimage baseimagef=$(tr -c a-zA-Z0-9.- - <<< "$baseimage") # create the directories that we need tmpfs=${TEST_SCRATCH_DIR}/tmpfs mkdir $tmpfs context=${TEST_SCRATCH_DIR}/context mkdir $context storagedir=${TEST_SCRATCH_DIR}/storage mkdir $storagedir rootdir=${storagedir}/rootdir mkdir $rootdir runrootdir=${storagedir}/runrootdir mkdir $runrootdir xdgruntimedir=${storagedir}/xdgruntime mkdir $xdgruntimedir xdgconfighome=${storagedir}/xdgconfighome mkdir $xdgconfighome xdgdatahome=${storagedir}/xdgdatahome mkdir $xdgdatahome storageopts="--storage-driver vfs --root $rootdir --runroot $runrootdir" # our temporary parent directory might not be world-searchable, which will # cause someone in the nested user namespace to hit permissions issues even # looking for $storagedir, so tweak perms to let them do at least that much fixupdir=$storagedir while test $(stat -c %d:%i $fixupdir) != $(stat -c %d:%i /) ; do # walk up to root, or the first parent that we don't own if test $(stat -c %u $fixupdir) -ne $(id -u) ; then break fi chmod +x $fixupdir fixupdir=$fixupdir/.. done # start writing the script to run in the nested user namespace cp -v ${TEST_SOURCES}/containers.conf ${TEST_SCRATCH_DIR}/containers.conf chmod ugo+r ${TEST_SCRATCH_DIR}/containers.conf echo set -e > ${TEST_SCRATCH_DIR}/script.sh echo export XDG_RUNTIME_DIR=$xdgruntimedir >> ${TEST_SCRATCH_DIR}/script.sh echo export XDG_CONFIG_HOME=$xdgconfighome >> ${TEST_SCRATCH_DIR}/script.sh echo export XDG_DATA_HOME=$xdgdatahome >> ${TEST_SCRATCH_DIR}/script.sh echo export CONTAINERS_CONF=${TEST_SCRATCH_DIR}/containers.conf >> ${TEST_SCRATCH_DIR}/script.sh echo export CONTAINERS_STORAGE_CONF=/dev/null >> ${TEST_SCRATCH_DIR}/script.sh # give our would-be user ownership of that directory echo chown --recursive ${subid}:${subid} ${storagedir} >> ${TEST_SCRATCH_DIR}/script.sh # make newuidmap/newgidmap, invoked by unshare even for uid=0, happy echo root:0:4294967295 > ${TEST_SCRATCH_DIR}/subid echo mount --bind -r ${TEST_SCRATCH_DIR}/subid /etc/subuid >> ${TEST_SCRATCH_DIR}/script.sh echo mount --bind -r ${TEST_SCRATCH_DIR}/subid /etc/subgid >> ${TEST_SCRATCH_DIR}/script.sh # don't get tripped up by ${TEST_SCRATCH_DIR} potentially being on a filesystem with non-default mount flags echo mount -t tmpfs -o size=256K tmpfs $tmpfs >> ${TEST_SCRATCH_DIR}/script.sh # mount a small tmpfs with every mount flag combination that concerns us, and # be ready to tell buildah to mount everything conservatively, to mirror the # TransientMounts API being used to nodev/noexec/nosuid/ro bind in a source # that doesn't necessarily have those flags already set on it for d in dev nodev ; do for e in exec noexec ; do for s in suid nosuid ; do for r in ro rw ; do subdir=$tmpfs/d-$d-$e-$s-$r echo mkdir ${subdir} >> ${TEST_SCRATCH_DIR}/script.sh echo mount -t tmpfs -o size=256K,$d,$e,$s,$r tmpfs ${subdir} >> ${TEST_SCRATCH_DIR}/script.sh mounts="${mounts:+${mounts} }--volume ${subdir}:/mounts/d-$d-$e-$s-$r:nodev,noexec,nosuid,ro" done done done done # copy binaries to a location where parent directory permissions are less # likely to interfere with running them from a different UID cp ${COPY_BINARY} ${TEST_SCRATCH_DIR}/copy cp ${BUILDAH_BINARY} ${TEST_SCRATCH_DIR}/buildah # make sure that RUN doesn't just break when we try to use volume mounts with # flags set that we're not allowed to modify echo FROM $baseimage > $context/Dockerfile echo RUN cat /proc/mounts >> $context/Dockerfile # copy in the prefetched image # unshare from util-linux 2.39 also accepts INNER:OUTER:SIZE for --map-users # and --map-groups, but fedora 37's is too old, so the older OUTER,INNER,SIZE # (using commas instead of colons as field separators) will have to do echo "env | sort" >> ${TEST_SCRATCH_DIR}/script.sh echo "env _CONTAINERS_USERNS_CONFIGURED=done unshare -Umpf --mount-proc --setuid 0 --setgid 0 --map-users=${subid},0,${rangesize} --map-groups=${subid},0,${rangesize} ${TEST_SCRATCH_DIR}/copy ${storageopts} dir:$_BUILDAH_IMAGE_CACHEDIR/$baseimagef containers-storage:$baseimage" >> ${TEST_SCRATCH_DIR}/script.sh # try to do a build with all of the volume mounts echo "env _CONTAINERS_USERNS_CONFIGURED=done unshare -Umpf --mount-proc --setuid 0 --setgid 0 --map-users=${subid},0,${rangesize} --map-groups=${subid},0,${rangesize} ${TEST_SCRATCH_DIR}/buildah ${BUILDAH_REGISTRY_OPTS} ${storageopts} build --isolation chroot --pull=never $mounts $context" >> ${TEST_SCRATCH_DIR}/script.sh # run that whole script in a nested mount namespace with no $XDG_... # variables leaked into it if is_rootless ; then run_buildah unshare env -i bash -x ${TEST_SCRATCH_DIR}/script.sh else unshare -mpf --mount-proc env -i bash -x ${TEST_SCRATCH_DIR}/script.sh fi } @test "chroot with overlay root" { if test `uname` != Linux ; then skip "not meaningful except on Linux" fi skip_if_no_unshare if [ "$(id -u)" -ne 0 ]; then skip "expects to already be root" fi _prefetch docker.io/library/busybox cp -v ${TEST_SOURCES}/containers.conf ${TEST_SCRATCH_DIR}/containers.conf chmod ugo+r ${TEST_SCRATCH_DIR}/containers.conf mkdir -p ${TEST_SCRATCH_DIR}/chroot ${COPY_BINARY} containers-storage:[${STORAGE_DRIVER}@${TEST_SCRATCH_DIR}/root+${TEST_SCRATCH_DIR}/runroot]docker.io/library/busybox:latest dir:${TEST_SCRATCH_DIR}/base-image chown -R 1:1 ${TEST_SCRATCH_DIR}/root ${TEST_SCRATCH_DIR}/runroot ${TEST_SCRATCH_DIR}/chroot if test ${STORAGE_DRIVER} = overlay ; then if test -x /usr/bin/fuse-overlayfs ; then local storage_opts="overlay.mount_program=/usr/bin/fuse-overlayfs" else skip "trying to use overlay on top of overlay, but fuse-overlayfs is not present" fi fi # a script that runs inside of a new mount namespace and mounts the current # rootfs as the "lower" for an overlay, then pivots into it cat > ${TEST_SCRATCH_DIR}/script1 <<- EOF PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin${PATH:+:$PATH} set -e set -x if test \$(stat -f -c %T "${TEST_SCRATCH_DIR}/chroot") = overlayfs ; then mount -t tmpfs -o size=16M none ${TEST_SCRATCH_DIR}/chroot fi mkdir -p ${TEST_SCRATCH_DIR}/chroot/workdir mkdir -p ${TEST_SCRATCH_DIR}/chroot/upperdir mkdir -p ${TEST_SCRATCH_DIR}/chroot/merged mount -t overlay overlay -o upperdir=${TEST_SCRATCH_DIR}/chroot/upperdir,workdir=${TEST_SCRATCH_DIR}/chroot/workdir,lowerdir=/ ${TEST_SCRATCH_DIR}/chroot/merged mount -t proc proc ${TEST_SCRATCH_DIR}/chroot/merged/proc mount -t sysfs sysfs ${TEST_SCRATCH_DIR}/chroot/merged/sys mount --bind /dev ${TEST_SCRATCH_DIR}/chroot/merged/dev mount --bind /etc ${TEST_SCRATCH_DIR}/chroot/merged/etc echo build > ${TEST_SCRATCH_DIR}/chroot/hostname chmod 644 ${TEST_SCRATCH_DIR}/chroot/hostname mount --bind ${TEST_SCRATCH_DIR}/chroot/hostname ${TEST_SCRATCH_DIR}/chroot/merged/etc/hostname touch ${TEST_SCRATCH_DIR}/chroot/hosts chmod 644 ${TEST_SCRATCH_DIR}/chroot/hosts mount --bind ${TEST_SCRATCH_DIR}/chroot/hosts ${TEST_SCRATCH_DIR}/chroot/merged/etc/hosts touch ${TEST_SCRATCH_DIR}/chroot/resolv.conf chmod 644 ${TEST_SCRATCH_DIR}/chroot/resolv.conf mount --bind ${TEST_SCRATCH_DIR}/chroot/resolv.conf ${TEST_SCRATCH_DIR}/chroot/merged/etc/resolv.conf mount --bind /tmp ${TEST_SCRATCH_DIR}/chroot/merged/tmp mkdir -p ${TEST_SCRATCH_DIR}/chroot/merged/var/tmp chmod 1777 ${TEST_SCRATCH_DIR}/chroot/merged/var/tmp if test -d /var/tmp; then mount --bind /var/tmp ${TEST_SCRATCH_DIR}/chroot/merged/var/tmp fi mkdir -p ${TEST_SCRATCH_DIR}/chroot/merged/run mount -t tmpfs -o size=1024k none ${TEST_SCRATCH_DIR}/chroot/merged/run chmod 755 ${TEST_SCRATCH_DIR}/chroot/merged/run mkdir -p ${TEST_SCRATCH_DIR}/chroot/merged/run/containers/storage chmod 755 ${TEST_SCRATCH_DIR}/chroot/merged/run/containers/storage mkdir -p ${TEST_SCRATCH_DIR}/chroot/merged/var/lib/containers/storage chmod 755 ${TEST_SCRATCH_DIR}/chroot/merged/var/lib/containers/storage chown -R 1:1 ${TEST_SCRATCH_DIR}/chroot/merged/run ${TEST_SCRATCH_DIR}/chroot/merged/var/lib/containers mount --bind ${TEST_SCRATCH_DIR} ${TEST_SCRATCH_DIR}/chroot/merged/${TEST_SCRATCH_DIR} mkdir -p ${TEST_SCRATCH_DIR}/chroot/merged/usr/local/bin chmod 755 ${TEST_SCRATCH_DIR}/chroot/merged/usr/local/bin touch ${TEST_SCRATCH_DIR}/chroot/merged/usr/local/bin/buildah mount --bind ${BUILDAH_BINARY:-$TEST_SOURCES/../bin/buildah} ${TEST_SCRATCH_DIR}/chroot/merged/usr/local/bin/buildah cd ${TEST_SCRATCH_DIR}/chroot/merged ${COPY_BINARY} --root ${TEST_SCRATCH_DIR}/root --runroot ${TEST_SCRATCH_DIR}/runroot --storage-driver ${STORAGE_DRIVER} ${storage_opts:+--storage-opt ${storage_opts}} dir:${TEST_SCRATCH_DIR}/base-image dir:${TEST_SCRATCH_DIR}/chroot/merged/base-image pivot_root . tmp mount --make-rslave tmp umount -f -l tmp mount -o remount --make-rshared / grep ' / / ' /proc/self/mountinfo # unshare from util-linux 2.39 also accepts INNER:OUTER:SIZE for --map-users # and --map-groups, but fedora 37's is too old, so the older OUTER,INNER,SIZE # (using commas instead of colons as field separators) will have to do unshare --setuid 0 --setgid 0 --map-users=1,0,1024 --map-users=1025,65534,2 --map-groups=1,0,1024 --map-groups=1025,65534,2 -UinCfpm bash ${TEST_SCRATCH_DIR}/script2 EOF # a script that runs inside of a new user namespace with an unprivileged ID # mapped to root, which is expected to be able to run, with the proper # configuration options, on top of that overlay filesystem cat > ${TEST_SCRATCH_DIR}/script2 <<- EOF set -e set -x export _CONTAINERS_USERNS_CONFIGURED=done export CONTAINERS_CONF=${TEST_SCRATCH_DIR}/containers.conf export CONTAINERS_STORAGE_CONF=/dev/null # needed to avoid file lookup permission errors under /root/... cat /proc/self/uid_map cat /proc/self/gid_map mount --make-shared / /usr/local/bin/buildah ${BUILDAH_REGISTRY_OPTS} --root /var/lib/containers/storage --runroot /run/containers/storage --storage-driver ${STORAGE_DRIVER} ${storage_opts:+--storage-opt ${storage_opts}} pull dir:/base-image baseID=\$(jq -r .config.digest /base-image/manifest.json) /usr/local/bin/buildah ${BUILDAH_REGISTRY_OPTS} --root /var/lib/containers/storage --runroot /run/containers/storage --storage-driver ${STORAGE_DRIVER} ${storage_opts:+--storage-opt ${storage_opts}} tag \${baseID} docker.io/library/busybox /usr/local/bin/buildah ${BUILDAH_REGISTRY_OPTS} --root /var/lib/containers/storage --runroot /run/containers/storage --storage-driver ${STORAGE_DRIVER} ${storage_opts:+--storage-opt ${storage_opts}} from --name ctrid --pull=never --quiet docker.io/library/busybox /usr/local/bin/buildah ${BUILDAH_REGISTRY_OPTS} --root /var/lib/containers/storage --runroot /run/containers/storage --storage-driver ${STORAGE_DRIVER} ${storage_opts:+--storage-opt ${storage_opts}} run --isolation=chroot ctrid pwd EOF chmod +x ${TEST_SCRATCH_DIR} chmod +rx ${TEST_SCRATCH_DIR}/script1 ${TEST_SCRATCH_DIR}/script2 env -i unshare -inCfpm bash ${TEST_SCRATCH_DIR}/script1 } ================================================ FILE: tests/commit.bats ================================================ #!/usr/bin/env bats load helpers @test "commit-flags-order-verification" { run_buildah 125 commit cnt1 --tls-verify check_options_flag_err "--tls-verify" run_buildah 125 commit cnt1 -q check_options_flag_err "-q" run_buildah 125 commit cnt1 -f=docker --quiet --creds=bla:bla check_options_flag_err "-f=docker" run_buildah 125 commit cnt1 --creds=bla:bla check_options_flag_err "--creds=bla:bla" } @test "commit" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah commit $WITH_POLICY_JSON $cid alpine-image run_buildah images alpine-image } # Mainly this test is added for rootless setups where XDG_RUNTIME_DIR # is not set and we end up setting incorrect runroot at various steps # Use case is typically seen on environments where current session # is invalid login session. @test "commit image on rootless setup with mount" { unset XDG_RUNTIME_DIR run dd if=/dev/zero of=${TEST_SCRATCH_DIR}/file count=1 bs=10M run_buildah from scratch CONT=$output unset XDG_RUNTIME_DIR run_buildah mount $CONT MNT=$output run cp ${TEST_SCRATCH_DIR}/file $MNT/file run_buildah umount $CONT run_buildah commit $CONT foo run_buildah images foo expect_output --substring "10.5 MB" } @test "commit-with-identity-label" { run_buildah from scratch cid=$output touch $TEST_SCRATCH_DIR/content.txt run_buildah add $cid $TEST_SCRATCH_DIR/content.txt / run_buildah commit $cid scratch-image-1 run_buildah inspect --format '{{printf "%q" .Docker.Config.Labels}}' scratch-image-1 assert "$output" != "map[]" run_buildah commit --identity-label=true $cid scratch-image-2 run_buildah inspect --format '{{printf "%q" .Docker.Config.Labels}}' scratch-image-2 assert "$output" != "map[]" } @test "commit-without-identity-label" { run_buildah from scratch cid=$output touch $TEST_SCRATCH_DIR/content.txt run_buildah add $cid $TEST_SCRATCH_DIR/content.txt / run_buildah commit --identity-label=false $WITH_POLICY_JSON $cid scratch-image run_buildah images scratch-image run_buildah inspect --format '{{printf "%q" .Docker.Config.Labels}}' scratch-image assert "$output" = "map[]" } @test "commit-suppressed-identity-label" { run_buildah from scratch cid=$output touch $TEST_SCRATCH_DIR/content.txt run_buildah add $cid $TEST_SCRATCH_DIR/content.txt / run_buildah commit --source-date-epoch=60 $WITH_POLICY_JSON $cid scratch-image-1 run_buildah images scratch-image-1 run_buildah inspect --format '{{printf "%q" .Docker.Config.Labels}}' scratch-image-1 assert "$output" = "map[]" export SOURCE_DATE_EPOCH=90 run_buildah commit $WITH_POLICY_JSON $cid scratch-image-2 unset SOURCE_DATE_EPOCH run_buildah images scratch-image-2 run_buildah inspect --format '{{printf "%q" .Docker.Config.Labels}}' scratch-image-2 assert "$output" = "map[]" run_buildah commit --timestamp=60 $WITH_POLICY_JSON $cid scratch-image-3 run_buildah images scratch-image-3 run_buildah inspect --format '{{printf "%q" .Docker.Config.Labels}}' scratch-image-3 assert "$output" = "map[]" } @test "commit format test" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah commit $WITH_POLICY_JSON $cid alpine-image-oci run_buildah commit --format docker --disable-compression=false $WITH_POLICY_JSON $cid alpine-image-docker run_buildah inspect --type=image --format '{{.Manifest}}' alpine-image-oci mediatype=$(jq -r '.layers[0].mediaType' <<<"$output") expect_output --from="$mediatype" "application/vnd.oci.image.layer.v1.tar" run_buildah inspect --type=image --format '{{.Manifest}}' alpine-image-docker mediatype=$(jq -r '.layers[1].mediaType' <<<"$output") expect_output --from="$mediatype" "application/vnd.docker.image.rootfs.diff.tar.gzip" } @test "commit --unsetenv PATH" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah commit --unsetenv PATH $WITH_POLICY_JSON $cid alpine-image-oci run_buildah commit --unsetenv PATH --format docker --disable-compression=false $WITH_POLICY_JSON $cid alpine-image-docker run_buildah inspect --type=image --format '{{.OCIv1.Config.Env}}' alpine-image-oci expect_output "[]" "No Path should be defined" run_buildah inspect --type=image --format '{{.Docker.Config.Env}}' alpine-image-docker expect_output "[]" "No Path should be defined" } @test "commit quiet test" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah commit --iidfile /dev/null $WITH_POLICY_JSON -q $cid alpine-image expect_output "" } @test "commit iidfile" { _prefetch busybox run_buildah from --quiet --pull=false $WITH_POLICY_JSON busybox cid=$output run_buildah commit --iidfile ${TEST_SCRATCH_DIR}/iidfile.txt $cid test -s ${TEST_SCRATCH_DIR}/iidfile.txt } @test "commit metadata-file" { _prefetch busybox run_buildah from --quiet --pull=false $WITH_POLICY_JSON busybox cid=$output run_buildah commit --metadata-file ${TEST_SCRATCH_DIR}/metadata.txt $cid jq . ${TEST_SCRATCH_DIR}/metadata.txt test -s ${TEST_SCRATCH_DIR}/metadata.txt assert $(wc -c < ${TEST_SCRATCH_DIR}/metadata.txt) -gt 2 } @test "commit rm test" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah commit $WITH_POLICY_JSON --rm $cid alpine-image run_buildah 125 rm $cid expect_output --substring "removing container \"alpine-working-container\": container not known" } @test "commit-alternate-storage" { _prefetch alpine echo FROM run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output echo COMMIT run_buildah commit $WITH_POLICY_JSON $cid "containers-storage:[vfs@${TEST_SCRATCH_DIR}/root2+${TEST_SCRATCH_DIR}/runroot2]newimage" echo FROM run_buildah --storage-driver vfs --root ${TEST_SCRATCH_DIR}/root2 --runroot ${TEST_SCRATCH_DIR}/runroot2 from $WITH_POLICY_JSON newimage } @test "commit-rejected-name" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah 125 commit $WITH_POLICY_JSON $cid ThisNameShouldBeRejected expect_output --substring "must be lower" } @test "commit-no-empty-created-by" { if ! python3 -c 'import json, sys' 2> /dev/null ; then skip "python interpreter with json module not found" fi target=new-image _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah config --created-by "untracked actions" $cid run_buildah commit $WITH_POLICY_JSON $cid ${target} run_buildah inspect --format '{{.Config}}' ${target} config="$output" run python3 -c 'import json, sys; config = json.load(sys.stdin); print(config["history"][len(config["history"])-1]["created_by"])' <<< "$config" echo "$output" assert "$status" -eq 0 "status from python command 1" expect_output "untracked actions" run_buildah config --created-by "" $cid run_buildah commit $WITH_POLICY_JSON $cid ${target} run_buildah inspect --format '{{.Config}}' ${target} config="$output" run python3 -c 'import json, sys; config = json.load(sys.stdin); print(config["history"][len(config["history"])-1]["created_by"])' <<< "$config" echo "$output" assert "$status" -eq 0 "status from python command 2" expect_output "/bin/sh" } @test "commit-no-name" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah commit $WITH_POLICY_JSON $cid } @test "commit should fail with nonexistent authfile" { _prefetch alpine run_buildah from --quiet --pull $WITH_POLICY_JSON alpine cid=$output run_buildah 125 commit --authfile /tmp/nonexistent $WITH_POLICY_JSON $cid alpine-image } @test "commit-builder-identity" { _prefetch alpine run_buildah from --quiet --pull $WITH_POLICY_JSON alpine cid=$output run_buildah commit $WITH_POLICY_JSON $cid alpine-image run_buildah --version local -a output_fields=($output) buildah_version=${output_fields[2]} run_buildah inspect --format '{{ index .Docker.Config.Labels "io.buildah.version"}}' alpine-image expect_output "$buildah_version" } @test "commit-container-id" { _prefetch alpine run_buildah from --quiet --pull $WITH_POLICY_JSON alpine # There is exactly one container. Get its ID. run_buildah containers --format '{{.ContainerID}}' cid=$output run_buildah commit $WITH_POLICY_JSON --format docker $cid alpine-image run_buildah inspect --format '{{.Docker.Container}}' alpine-image expect_output "$cid" "alpine-image -> .Docker.Container" } @test "commit with name" { _prefetch busybox run_buildah from --quiet $WITH_POLICY_JSON --name busyboxc busybox expect_output "busyboxc" # Commit with a new name newname="commitbyname/busyboxname" run_buildah commit $WITH_POLICY_JSON busyboxc $newname run_buildah from $WITH_POLICY_JSON localhost/$newname expect_output "busyboxname-working-container" cname=$output run_buildah inspect --format '{{.FromImage}}' $cname expect_output "localhost/$newname:latest" } @test "commit to docker-distribution" { _prefetch busybox run_buildah from $WITH_POLICY_JSON --name busyboxc busybox start_registry run_buildah commit $WITH_POLICY_JSON --tls-verify=false --creds testuser:testpassword busyboxc docker://localhost:${REGISTRY_PORT}/commit/busybox run_buildah from $WITH_POLICY_JSON --name fromdocker --tls-verify=false --creds testuser:testpassword docker://localhost:${REGISTRY_PORT}/commit/busybox } @test "commit encrypted local oci image" { skip_if_rootless_environment _prefetch busybox mkdir ${TEST_SCRATCH_DIR}/tmp openssl genrsa -out ${TEST_SCRATCH_DIR}/tmp/mykey.pem 1024 openssl rsa -in ${TEST_SCRATCH_DIR}/tmp/mykey.pem -pubout > ${TEST_SCRATCH_DIR}/tmp/mykey.pub run_buildah from --quiet --pull=false $WITH_POLICY_JSON busybox cid=$output run_buildah commit --iidfile /dev/null $WITH_POLICY_JSON --encryption-key jwe:${TEST_SCRATCH_DIR}/tmp/mykey.pub -q $cid oci:${TEST_SCRATCH_DIR}/tmp/busybox_enc imgtype -show-manifest oci:${TEST_SCRATCH_DIR}/tmp/busybox_enc | grep "+encrypted" rm -rf ${TEST_SCRATCH_DIR}/tmp } @test "commit oci encrypt to registry" { _prefetch busybox mkdir ${TEST_SCRATCH_DIR}/tmp openssl genrsa -out ${TEST_SCRATCH_DIR}/tmp/mykey.pem 1024 openssl rsa -in ${TEST_SCRATCH_DIR}/tmp/mykey.pem -pubout > ${TEST_SCRATCH_DIR}/tmp/mykey.pub start_registry run_buildah from --quiet --pull=false $WITH_POLICY_JSON busybox cid=$output run_buildah commit --iidfile /dev/null --tls-verify=false --creds testuser:testpassword $WITH_POLICY_JSON --encryption-key jwe:${TEST_SCRATCH_DIR}/tmp/mykey.pub -q $cid docker://localhost:${REGISTRY_PORT}/buildah/busybox_encrypted:latest # this test, just checks the ability to commit an image to a registry # there is no good way to test the details of the image unless with ./buildah pull, test will be in pull.bats rm -rf ${TEST_SCRATCH_DIR}/tmp # verify that encrypted layers are not cached or reused for an non-encrypted image (See containers/image#1533) run_buildah commit --iidfile /dev/null --tls-verify=false --creds testuser:testpassword $WITH_POLICY_JSON -q $cid docker://localhost:${REGISTRY_PORT}/buildah/busybox_not_encrypted:latest run_buildah from $WITH_POLICY_JSON --tls-verify=false --creds testuser:testpassword docker://localhost:${REGISTRY_PORT}/buildah/busybox_not_encrypted:latest } @test "commit omit-timestamp" { _prefetch busybox run_buildah from --quiet --pull=false $WITH_POLICY_JSON busybox cid=$output run_buildah run $cid touch /test run_buildah commit $WITH_POLICY_JSON --omit-timestamp -q $cid omit run_buildah inspect --format '{{ .Docker.Created }}' omit expect_output --substring "1970-01-01" run_buildah inspect --format '{{ .OCIv1.Created }}' omit expect_output --substring "1970-01-01" run_buildah from --quiet --pull=false $WITH_POLICY_JSON omit cid=$output run_buildah run $cid ls -l /test expect_output --substring "1970" rm -rf ${TEST_SCRATCH_DIR}/tmp } @test "commit timestamp" { _prefetch busybox run_buildah from --quiet --pull=false $WITH_POLICY_JSON busybox cid=$output run_buildah run $cid touch /test run_buildah commit $WITH_POLICY_JSON --timestamp 0 -q $cid omit run_buildah inspect --format '{{ .Docker.Created }}' omit expect_output --substring "1970-01-01" run_buildah inspect --format '{{ .OCIv1.Created }}' omit expect_output --substring "1970-01-01" run_buildah from --quiet --pull=false $WITH_POLICY_JSON omit cid=$output run_buildah run $cid ls -l /test expect_output --substring "1970" rm -rf ${TEST_SCRATCH_DIR}/tmp } @test "commit with authfile" { _prefetch busybox run_buildah from --quiet --pull=false $WITH_POLICY_JSON busybox cid=$output run_buildah run $cid touch /test start_registry run_buildah login --authfile ${TEST_SCRATCH_DIR}/test.auth --username testuser --password testpassword --tls-verify=false localhost:${REGISTRY_PORT} run_buildah commit --authfile ${TEST_SCRATCH_DIR}/test.auth $WITH_POLICY_JSON --tls-verify=false $cid docker://localhost:${REGISTRY_PORT}/buildah/my-busybox expect_output --substring "Writing manifest to image destination" } @test "commit-without-names" { _prefetch busybox run_buildah from --quiet --pull=false $WITH_POLICY_JSON busybox cid=$output run_buildah run $cid touch /testfile run_buildah run $cid chown $(id -u):$(id -g) /testfile run_buildah commit $cid dir:${TEST_SCRATCH_DIR}/new-image config=$(jq -r .config.digest ${TEST_SCRATCH_DIR}/new-image/manifest.json) echo "config blob is $config" diffid=$(jq -r '.rootfs.diff_ids[-1]' ${TEST_SCRATCH_DIR}/new-image/${config##*:}) echo "new layer is $diffid" run_buildah copy $cid ${TEST_SCRATCH_DIR}/new-image/${diffid##*:} /testdiff.tar # use in-container version of tar to avoid worrying about differences in # output formats between tar implementations run_buildah run $cid tar tvf /testdiff.tar testfile echo "new file looks like [$output]" # ownership information should be forced to be in number/number format # instead of name/name because the names are gone assert "$output" =~ $(id -u)/$(id -g) } @test "commit-with-extra-files" { _prefetch busybox run_buildah from --quiet --pull=false $WITH_POLICY_JSON busybox cid=$output createrandom ${TEST_SCRATCH_DIR}/randomfile1 createrandom ${TEST_SCRATCH_DIR}/randomfile2 for method in --squash=false --squash=true ; do run_buildah commit $method --add-file ${TEST_SCRATCH_DIR}/randomfile1:/randomfile1 $cid with-random-1 run_buildah commit $method --add-file ${TEST_SCRATCH_DIR}/randomfile2:/in-a-subdir/randomfile2 $cid with-random-2 run_buildah commit $method --add-file ${TEST_SCRATCH_DIR}/randomfile1:/randomfile1 --add-file ${TEST_SCRATCH_DIR}/randomfile2:/in-a-subdir/randomfile2 $cid with-random-both # first one should have the first file and not the second, and the shell should be there run_buildah from --quiet --pull=false $WITH_POLICY_JSON with-random-1 cid=$output run_buildah mount $cid mountpoint=$output test -s $mountpoint/bin/sh || test -L $mountpoint/bin/sh cmp ${TEST_SCRATCH_DIR}/randomfile1 $mountpoint/randomfile1 run stat -c %u:%g $mountpoint [ $status -eq 0 ] rootowner=$output run stat -c %u:%g:%A $mountpoint/randomfile1 [ $status -eq 0 ] assert ${rootowner}:-rw-r--r-- ! test -f $mountpoint/randomfile2 # second one should have the second file and not the first, and the shell should be there run_buildah from --quiet --pull=false $WITH_POLICY_JSON with-random-2 cid=$output run_buildah mount $cid mountpoint=$output test -s $mountpoint/bin/sh || test -L $mountpoint/bin/sh cmp ${TEST_SCRATCH_DIR}/randomfile2 $mountpoint/in-a-subdir/randomfile2 run stat -c %u:%g $mountpoint [ $status -eq 0 ] rootowner=$output run stat -c %u:%g:%A $mountpoint/in-a-subdir/randomfile2 [ $status -eq 0 ] assert ${rootowner}:-rw-r--r-- ! test -f $mountpoint/randomfile1 # third one should have both files, and the shell should be there run_buildah from --quiet --pull=false $WITH_POLICY_JSON with-random-both cid=$output run_buildah mount $cid mountpoint=$output test -s $mountpoint/bin/sh || test -L $mountpoint/bin/sh cmp ${TEST_SCRATCH_DIR}/randomfile1 $mountpoint/randomfile1 run stat -c %u:%g $mountpoint [ $status -eq 0 ] rootowner=$output run stat -c %u:%g:%A $mountpoint/randomfile1 [ $status -eq 0 ] assert ${rootowner}:-rw-r--r-- cmp ${TEST_SCRATCH_DIR}/randomfile2 $mountpoint/in-a-subdir/randomfile2 run stat -c %u:%g:%A $mountpoint/in-a-subdir/randomfile2 [ $status -eq 0 ] assert ${rootowner}:-rw-r--r-- done } @test "commit with insufficient disk space" { skip_if_rootless_environment _prefetch busybox local tmp=$TEST_SCRATCH_DIR/buildah-test mkdir -p $tmp mount -t tmpfs -o size=4M tmpfs $tmp # Create a temporary file which should not be easy to compress, # which we'll add to our container for committing, but which is # larger than the filesystem where the layer blob that would # contain it, compressed or not, would be written during commit. run dd if=/dev/urandom of=$TEST_SCRATCH_DIR/8M bs=1M count=8 # Create a working container. run_buildah from --pull=never $WITH_POLICY_JSON busybox ctrID="$output" # Copy the file into the working container. run_buildah copy $ctrID $TEST_SCRATCH_DIR/8M /8M # Try to commit the image. The temporary copy of the layer diff should # require more space than is available where we're telling it to store # temporary things. TMPDIR=$tmp run_buildah '?' commit $ctrID umount $tmp expect_output --substring "no space left on device" } @test "commit-with-source-date-epoch" { _prefetch busybox local url=https://raw.githubusercontent.com/containers/buildah/main/tests/bud/from-scratch/Dockerfile local timestamp=60 local datestamp="1970-01-01T00:01:00Z" mkdir -p $TEST_SCRATCH_DIR/context createrandom $TEST_SCRATCH_DIR/context/randomfile1 createrandom $TEST_SCRATCH_DIR/context/randomfile2 run_buildah from -q busybox local cid="$output" run_buildah add --add-history "$cid" $TEST_SCRATCH_DIR/context/* /context # commit using defaults run_buildah commit "$cid" oci:$TEST_SCRATCH_DIR/default # commit with an implicitly-provided timestamp export local SOURCE_DATE_EPOCH=$timestamp run_buildah commit "$cid" oci:$TEST_SCRATCH_DIR/implicit run_buildah commit --rewrite-timestamp "$cid" oci:$TEST_SCRATCH_DIR/implicit-rewritten unset SOURCE_DATE_EPOCH # commit with an explicity-provided timestamp run_buildah commit --source-date-epoch=$timestamp "$cid" oci:$TEST_SCRATCH_DIR/explicit run_buildah commit --source-date-epoch=$timestamp --rewrite-timestamp "$cid" oci:$TEST_SCRATCH_DIR/explicit-rewritten # check timestamps in the ones we forced: find the manifest's and config's digests manifestdigest=$(oci_image_manifest_digest "$TEST_SCRATCH_DIR"/explicit) manifestalg=${manifestdigest%%:*} manifestval=${manifestdigest##*:} configdigest=$(oci_image_config_digest "$TEST_SCRATCH_DIR"/explicit) configalg=${configdigest%%:*} configval=${configdigest##*:} # check timestamps in the ones we forced: read the image creation date config="$TEST_SCRATCH_DIR"/explicit/$(oci_image_config "$TEST_SCRATCH_DIR"/explicit) run jq -r '.created' "$config" echo "$output" assert $status = 0 "looking for the image creation date" assert "$output" = "$datestamp" "unexpected creation date for image" # check timestamps in the ones we forced: read the image history entry dates run jq -r '.history[-2].created' "$config" echo "$output" assert $status = 0 "looking for the image history entries" jq '.history' "$config" for line in "$lines[@]"; do assert "$output" = "$datestamp" "unexpected datestamp for history entry" done # check timestamps in the ones we forced: extract the layer blob layer="$TEST_SCRATCH_DIR"/explicit/$(oci_image_last_diff "$TEST_SCRATCH_DIR"/explicit) mkdir -p "$TEST_SCRATCH_DIR"/layer tar -C "$TEST_SCRATCH_DIR"/layer -xvf "$layer" # check timestamps in the ones we forced: walk the layer blob, checking # timestamps for file in $(find $TEST_SCRATCH_DIR/layer/* -print) ; do run stat -c %Y $file assert $status = 0 "checking datestamp on $file in layer" assert "$output" -gt "$timestamp" "unexpected datestamp on $file in layer" done # check timestamps in the ones we forced: check that we have an image config # and manifest with the same digests when we set the source date epoch # implicitly as we did when we forced them explicitly test -s $TEST_SCRATCH_DIR/implicit/blobs/"$manifestalg"/"$manifestval" test -s $TEST_SCRATCH_DIR/implicit/blobs/"$configalg"/"$configval" # check timestamps in the ones we forced and rewrote timestamps in: the # version where we rewrote timestamps in the layer should have produced # different diffIDs, and thus a different config blob, and a different # manifest, so the ones we just looked at shouldn't _also_ be in there ! test -s $TEST_SCRATCH_DIR/explicit-rewritten/blobs/"$manifestalg"/"$manifestval" ! test -s $TEST_SCRATCH_DIR/explicit-rewritten/blobs/"$configalg"/"$configval" # check timestamps in the ones we forced and rewrote timestamps in: find the # manifest's and config's digests manifestdigest=$(oci_image_manifest_digest "$TEST_SCRATCH_DIR"/explicit-rewritten) manifestalg=${manifestdigest%%:*} manifestval=${manifestdigest##*:} configdigest=$(oci_image_config_digest "$TEST_SCRATCH_DIR"/explicit-rewritten) configalg=${configdigest%%:*} configval=${configdigest##*:} # check timestamps in the ones we forced and rewrote timestamps in: read the # image creation date config="$TEST_SCRATCH_DIR"/explicit-rewritten/$(oci_image_config "$TEST_SCRATCH_DIR"/explicit-rewritten) run jq -r '.created' "$config" echo "$output" assert $status = 0 "looking for the image creation date" assert "$output" = "$datestamp" "unexpected creation date for image" # check timestamps in the ones we forced and rewrote timestamps in: read the # image history entry dates run jq -r '.history[-2].created' "$config" echo "$output" assert $status = 0 "looking for the image history entries" jq '.history' "$config" for line in "$lines[@]"; do assert "$output" = "$datestamp" "unexpected datestamp for history entry" done # check timestamps in the ones we forced and rewrote timestamps in: extract # the layer blob layer="$TEST_SCRATCH_DIR"/explicit-rewritten/$(oci_image_last_diff "$TEST_SCRATCH_DIR"/explicit-rewritten) rm -fr $TEST_SCRATCH_DIR/layer; mkdir -p $TEST_SCRATCH_DIR/layer tar -C $TEST_SCRATCH_DIR/layer -xvf "$layer" # check timestamps in the ones we forced and rewrote timestamps in: walk the # layer blob, checking timestamps for file in $(find $TEST_SCRATCH_DIR/layer/* -print) ; do run stat -c %Y $file assert $status = 0 "checking datestamp on $file in layer" assert "$output" -le "$timestamp" "unexpected datestamp on $file in layer" done # check timestamps in the ones we forced and rewrote timestamps in: check # that we have an image config and manifest with the same digests when we set # the source date epoch implicitly as we did when we forced them explicitly test -s $TEST_SCRATCH_DIR/implicit-rewritten/blobs/"$manifestalg"/"$manifestval" test -s $TEST_SCRATCH_DIR/implicit-rewritten/blobs/"$configalg"/"$configval" # check timestamps in the one we didn't force: find the manifest's and config's digests manifestdigest=$(oci_image_manifest_digest "$TEST_SCRATCH_DIR"/default) manifestalg=${manifestdigest%%:*} manifestval=${manifestdigest##*:} configdigest=$(oci_image_config_digest "$TEST_SCRATCH_DIR"/default) configalg=${configdigest%%:*} configval=${configdigest##*:} # check timestamps in the one we didn't force: read the image creation date config="$TEST_SCRATCH_DIR"/default/$(oci_image_config "$TEST_SCRATCH_DIR"/default) run jq -r '.created' "$config" echo "$output" assert $status = 0 "looking for the image creation date" assert "$output" != "$datestamp" "unexpected creation date for image" # check timestamps in the one we didn't force: read the image history entry dates run jq -r '.history[-2].created' "$config" echo "$output" assert $status = 0 "looking for the image history entries" jq '.history' "$config" for line in "$lines[@]"; do assert "$output" != "$datestamp" "unexpected datestamp for history entry" done # check timestamps in the ones we didn't force: extract the layer blob layer="$TEST_SCRATCH_DIR"/default/$(oci_image_last_diff "$TEST_SCRATCH_DIR"/default) rm -fr $TEST_SCRATCH_DIR/layer; mkdir -p $TEST_SCRATCH_DIR/layer tar -C $TEST_SCRATCH_DIR/layer -xvf "$layer" # check timestamps in the ones we didn't force: walk the layer blob, checking timestamps for file in $(find $TEST_SCRATCH_DIR/layer/* -print) ; do run stat -c %Y $file assert $status = 0 "checking datestamp on $file in layer" assert "$output" != "$timestamp" "unexpected datestamp on $file in layer" done } @test "commit-sets-created-annotation" { _prefetch busybox run_buildah from -q busybox local cid="$output" for annotation in a=b c=d ; do local subdir=${annotation%%=*} run_buildah commit --annotation $annotation "$cid" oci:${TEST_SCRATCH_DIR}/$subdir local manifest=${TEST_SCRATCH_DIR}/$subdir/$(oci_image_manifest ${TEST_SCRATCH_DIR}/$subdir) run jq -r '.annotations["'$subdir'"]' "$manifest" assert $status -eq 0 echo "$output" assert "$output" = ${annotation##*=} done for flagdir in default: timestamp:--timestamp=0 sde:--source-date-epoch=0 suppressed:--unsetannotation=org.opencontainers.image.created specific:--created-annotation=false explicit:--created-annotation=true ; do local flag=${flagdir##*:} local subdir=${flagdir%%:*} run_buildah commit $flag "$cid" oci:${TEST_SCRATCH_DIR}/$subdir local manifest=${TEST_SCRATCH_DIR}/$subdir/$(oci_image_manifest ${TEST_SCRATCH_DIR}/$subdir) run jq -r '.annotations["org.opencontainers.image.created"]' "$manifest" assert $status -eq 0 echo "$output" local manifestcreated="$output" local config=${TEST_SCRATCH_DIR}/$subdir/$(oci_image_config ${TEST_SCRATCH_DIR}/$subdir) run jq -r '.created' "$config" assert $status -eq 0 echo "$output" local configcreated="$output" if [[ "$flag" =~ "=0" ]]; then assert $manifestcreated = $configcreated "manifest and config disagree on the image's created-time" assert $manifestcreated = "1970-01-01T00:00:00Z" elif [[ "$flag" =~ "unsetannotation" ]]; then assert $configcreated != "" assert $manifestcreated = "null" elif [[ "$flag" =~ "created-annotation=false" ]]; then assert $configcreated != "" assert $manifestcreated = "null" else assert $manifestcreated = $configcreated "manifest and config disagree on the image's created-time" assert $manifestcreated != "" assert $manifestcreated != "1970-01-01T00:00:00Z" fi done } ================================================ FILE: tests/config.bats ================================================ #!/usr/bin/env bats load helpers @test "config-flags-order-verification" { run_buildah 125 config cnt1 --author=user1 check_options_flag_err "--author=user1" run_buildah 125 config cnt1 --arch x86_54 check_options_flag_err "--arch" run_buildah 125 config cnt1 --created-by buildahcli --cmd "/usr/bin/run.sh" --hostname "localhost1" check_options_flag_err "--created-by" run_buildah 125 config cnt1 --annotation=service=cache check_options_flag_err "--annotation=service=cache" } @test "config-flags-verification" { run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah config --label LABEL $cid run_buildah config --annotation ANNOTATION $cid run_buildah 125 config --healthcheck 'AB "CD' $cid expect_output --substring 'parsing --healthcheck "AB \\"CD": invalid command line string' run_buildah 125 config --healthcheck-interval ABCD $cid expect_output --substring 'parsing --healthcheck-interval "ABCD": time: invalid duration "?ABCD"?' run_buildah 125 config --cmd 'AB "CD' $cid expect_output --substring 'parsing --cmd "AB \\"CD": invalid command line string' run_buildah 125 config --env ENV $cid expect_output --substring 'setting env "ENV": no value given' run_buildah 125 config --shell 'AB "CD' $cid expect_output --substring 'parsing --shell "AB \\"CD": invalid command line string' } function check_matrix() { local setting=$1 local expect=$2 # matrix test: all permutations of .Docker.* and .OCIv1.* in all image types for image in docker oci; do for which in Docker OCIv1; do run_buildah inspect --type=image --format "{{.$which.$setting}}" scratch-image-$image expect_output "$expect" done done } @test "config entrypoint using single element in JSON array (exec form)" { run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah config --entrypoint '[ "/ENTRYPOINT" ]' $cid run_buildah commit --format docker $WITH_POLICY_JSON $cid scratch-image-docker run_buildah commit --format oci $WITH_POLICY_JSON $cid scratch-image-oci check_matrix "Config.Entrypoint" '[/ENTRYPOINT]' } @test "config entrypoint using multiple elements in JSON array (exec form)" { run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah config --entrypoint '[ "/ENTRYPOINT", "ELEMENT2" ]' $cid run_buildah commit --format docker $WITH_POLICY_JSON $cid scratch-image-docker run_buildah commit --format oci $WITH_POLICY_JSON $cid scratch-image-oci check_matrix 'Config.Entrypoint' '[/ENTRYPOINT ELEMENT2]' } @test "config entrypoint using string (shell form)" { run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah config --entrypoint /ENTRYPOINT $cid run_buildah commit --format docker $WITH_POLICY_JSON $cid scratch-image-docker run_buildah commit --format oci $WITH_POLICY_JSON $cid scratch-image-oci check_matrix 'Config.Entrypoint' '[/bin/sh -c /ENTRYPOINT]' } @test "config --unsetlabel" { base=quay.io/libpod/testimage:20241011 _prefetch $base run_buildah from --quiet --pull=false $WITH_POLICY_JSON $base cid=$output run_buildah commit $WITH_POLICY_JSON $cid with-created-by-label run_buildah config --unsetlabel created_by $cid run_buildah commit $WITH_POLICY_JSON $cid without-created-by-label run_buildah inspect --format '{{ index .Docker.Config.Labels "created_by"}}' with-created-by-label assert "$output" != "" "label should be set in base image" run_buildah inspect --format '{{ index .Docker.Config.Labels "created_by"}}' without-created-by-label assert "$output" == "" "created_by label should be removed" } @test "config --unsetannotation" { base=quay.io/libpod/testimage:20241011 _prefetch $base run_buildah from --quiet --pull=false $WITH_POLICY_JSON $base cid=$output run_buildah commit $WITH_POLICY_JSON $cid with-name-annotation run_buildah config --unsetannotation org.opencontainers.image.base.name $cid run_buildah commit $WITH_POLICY_JSON $cid without-name-annotation run_buildah inspect --format '{{ index .ImageAnnotations "org.opencontainers.image.base.name"}}' with-name-annotation assert "$output" != "" "annotation should be set in base image" run_buildah inspect --format '{{ index .ImageAnnotations "org.opencontainers.image.base.name"}}' without-name-annotation assert "$output" == "" "name annotation should be removed" } @test "config set empty entrypoint doesn't wipe cmd" { run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah config --cmd "command" $cid run_buildah config --entrypoint "" $cid run_buildah commit --format docker $WITH_POLICY_JSON $cid scratch-image-docker run_buildah commit --format oci $WITH_POLICY_JSON $cid scratch-image-oci check_matrix 'Config.Cmd' '[command]' } @test "config cmd without entrypoint" { run_buildah from --pull-never $WITH_POLICY_JSON scratch cid=$output run_buildah config \ --cmd COMMAND-OR-ARGS \ $cid run_buildah commit --format docker $WITH_POLICY_JSON $cid scratch-image-docker run_buildah commit --format oci $WITH_POLICY_JSON $cid scratch-image-oci check_matrix 'Config.Cmd' '[COMMAND-OR-ARGS]' check_matrix 'Config.Entrypoint' '[]' } @test "config entrypoint with cmd" { run_buildah from --pull-never $WITH_POLICY_JSON scratch cid=$output run_buildah config \ --entrypoint /ENTRYPOINT \ --cmd COMMAND-OR-ARGS \ $cid run_buildah commit --format docker $WITH_POLICY_JSON $cid scratch-image-docker run_buildah commit --format oci $WITH_POLICY_JSON $cid scratch-image-oci check_matrix 'Config.Cmd' '[COMMAND-OR-ARGS]' run_buildah from --pull-never $WITH_POLICY_JSON scratch cid=$output run_buildah config \ --entrypoint /ENTRYPOINT \ $cid run_buildah commit --format docker $WITH_POLICY_JSON $cid scratch-image-docker run_buildah commit --format oci $WITH_POLICY_JSON $cid scratch-image-oci check_matrix 'Config.Cmd' '[]' run_buildah config \ --entrypoint /ENTRYPOINT \ --cmd COMMAND-OR-ARGS \ $cid run_buildah commit --format docker $WITH_POLICY_JSON $cid scratch-image-docker run_buildah commit --format oci $WITH_POLICY_JSON $cid scratch-image-oci check_matrix 'Config.Cmd' '[COMMAND-OR-ARGS]' run_buildah config \ --entrypoint /ENTRYPOINT \ --cmd '[ "/COMMAND", "ARG1", "ARG2"]' \ $cid run_buildah commit --format docker $WITH_POLICY_JSON $cid scratch-image-docker run_buildah commit --format oci $WITH_POLICY_JSON $cid scratch-image-oci check_matrix 'Config.Cmd' '[/COMMAND ARG1 ARG2]' } @test "config remove all" { run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah config \ --port 12345 \ --annotation ANNOTATION=VALUE1,VALUE2 \ --env VARIABLE=VALUE1,VALUE2 \ --volume /VOLUME \ --label LABEL=VALUE \ $cid run_buildah commit --format docker $WITH_POLICY_JSON $cid scratch-image-docker run_buildah commit --format oci $WITH_POLICY_JSON $cid scratch-image-oci run_buildah inspect --type=image --format '{{index .ImageAnnotations "ANNOTATION"}}' scratch-image-oci expect_output "VALUE1,VALUE2" run_buildah inspect --format '{{index .ImageAnnotations "ANNOTATION"}}' $cid expect_output "VALUE1,VALUE2" check_matrix 'Config.ExposedPorts' 'map[12345:{}]' check_matrix 'Config.Env' '[VARIABLE=VALUE1,VALUE2]' check_matrix 'Config.Labels.LABEL' 'VALUE' run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah config \ --port - \ --annotation - \ --env - \ --volume - \ --label - \ $cid run_buildah commit --format docker $WITH_POLICY_JSON $cid scratch-image-docker run_buildah commit --created-annotation=false --format oci $WITH_POLICY_JSON $cid scratch-image-oci run_buildah inspect --type=image --format '{{.ImageAnnotations}}' scratch-image-oci expect_output "map[]" run_buildah inspect --format '{{.ImageAnnotations}}' $cid expect_output "map[]" check_matrix 'Config.ExposedPorts' 'map[]' check_matrix 'Config.Env' '[]' check_matrix 'Config.Labels.LABEL' '' } @test "config" { run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah config \ --author TESTAUTHOR \ --created-by COINCIDENCE \ --arch amd64 \ --os linux \ --variant abc \ --user likes:things \ --port 12345 \ --env VARIABLE=VALUE1,VALUE2 \ --entrypoint /ENTRYPOINT \ --cmd COMMAND-OR-ARGS \ --comment INFORMATIVE \ --history-comment PROBABLY-EMPTY \ --volume /VOLUME \ --workingdir /tmp \ --label LABEL=VALUE \ --label exec='podman run -it --mount=type=bind,bind-propagation=Z,source=foo,destination=bar /script buz'\ --stop-signal SIGINT \ --annotation ANNOTATION=VALUE1,VALUE2 \ --shell /bin/arbitrarysh \ --domainname mydomain.local \ --hostname cleverhostname \ --healthcheck "CMD /bin/true" \ --healthcheck-start-period 5s \ --healthcheck-start-interval 30s \ --healthcheck-interval 6s \ --healthcheck-timeout 7s \ --healthcheck-retries 8 \ --onbuild "RUN touch /foo" \ --os-version "1.0" \ --os-feature dynamic --os-feature - --os-feature removed --os-feature removed- --os-feature win32k \ $cid run_buildah commit --format docker $WITH_POLICY_JSON $cid scratch-image-docker run_buildah commit --format oci $WITH_POLICY_JSON $cid scratch-image-oci check_matrix 'Author' 'TESTAUTHOR' check_matrix 'Architecture' 'amd64' check_matrix 'OS' 'linux' check_matrix 'Variant' 'abc' check_matrix 'OSVersion' '1.0' check_matrix 'OSFeatures' '[win32k]' run_buildah inspect --format '{{.ImageCreatedBy}}' $cid expect_output "COINCIDENCE" check_matrix 'Config.Cmd' '[COMMAND-OR-ARGS]' check_matrix 'Config.Entrypoint' '[/bin/sh -c /ENTRYPOINT]' check_matrix 'Config.Env' '[VARIABLE=VALUE1,VALUE2]' check_matrix 'Config.ExposedPorts' 'map[12345:{}]' check_matrix 'Config.Labels.exec' 'podman run -it --mount=type=bind,bind-propagation=Z,source=foo,destination=bar /script buz' check_matrix 'Config.Labels.LABEL' 'VALUE' check_matrix 'Config.StopSignal' 'SIGINT' check_matrix 'Config.User' 'likes:things' check_matrix 'Config.Volumes' "map[/VOLUME:{}]" check_matrix 'Config.WorkingDir' '/tmp' run_buildah inspect --type=image --format '{{(index .Docker.History 0).Comment}}' scratch-image-docker expect_output "PROBABLY-EMPTY" run_buildah inspect --type=image --format '{{(index .OCIv1.History 0).Comment}}' scratch-image-docker expect_output "PROBABLY-EMPTY" run_buildah inspect --type=image --format '{{(index .Docker.History 0).Comment}}' scratch-image-oci expect_output "PROBABLY-EMPTY" run_buildah inspect --type=image --format '{{(index .OCIv1.History 0).Comment}}' scratch-image-oci expect_output "PROBABLY-EMPTY" # The following aren't part of the Docker v2 spec, so they're discarded when we save to Docker format. run_buildah inspect --type=image --format '{{index .ImageAnnotations "ANNOTATION"}}' scratch-image-oci expect_output "VALUE1,VALUE2" run_buildah inspect --format '{{index .ImageAnnotations "ANNOTATION"}}' $cid expect_output "VALUE1,VALUE2" run_buildah inspect --type=image --format '{{.Docker.Comment}}' scratch-image-docker expect_output "INFORMATIVE" run_buildah inspect --type=image --format '{{.Docker.Config.Domainname}}' scratch-image-docker expect_output "mydomain.local" run_buildah inspect --type=image --format '{{.Docker.Config.Hostname}}' scratch-image-docker expect_output "cleverhostname" run_buildah inspect --type=image --format '{{.Docker.Config.Shell}}' scratch-image-docker expect_output "[/bin/arbitrarysh]" run_buildah inspect -f '{{.Docker.Config.Healthcheck.Test}}' scratch-image-docker expect_output "[CMD /bin/true]" run_buildah inspect -f '{{.Docker.Config.Healthcheck.StartPeriod}}' scratch-image-docker expect_output "5s" run_buildah inspect -f '{{.Docker.Config.Healthcheck.StartInterval}}' scratch-image-docker expect_output "30s" run_buildah inspect -f '{{.Docker.Config.Healthcheck.Interval}}' scratch-image-docker expect_output "6s" run_buildah inspect -f '{{.Docker.Config.Healthcheck.Timeout}}' scratch-image-docker expect_output "7s" run_buildah inspect -f '{{.Docker.Config.Healthcheck.Retries}}' scratch-image-docker expect_output "8" run_buildah inspect -f '{{.Docker.Config.OnBuild}}' scratch-image-docker expect_output "[RUN touch /foo]" rm -rf /VOLUME } @test "config env using local environment" { export foo=bar run_buildah from --pull-never $WITH_POLICY_JSON scratch cid=$output run_buildah config --env 'foo' $cid run_buildah commit --format docker $WITH_POLICY_JSON $cid env-image-docker run_buildah commit --format oci $WITH_POLICY_JSON $cid env-image-oci run_buildah inspect --type=image --format '{{.Docker.Config.Env}}' env-image-docker expect_output --substring "foo=bar" run_buildah inspect --type=image --format '{{.OCIv1.Config.Env}}' env-image-docker expect_output --substring "foo=bar" } @test "docker formatted builds must inherit healthcheck from base image" { _prefetch busybox ctxdir=${TEST_SCRATCH_DIR}/bud mkdir -p $ctxdir cat >$ctxdir/Dockerfile <$ctxdir/Dockerfile <' check_matrix 'Config.ExposedPorts' 'map[]' run_buildah inspect --type=image --format '{{index .ImageAnnotations "ANNOTATION"}}' scratch-image-oci expect_output "" run_buildah inspect --format '{{index .ImageAnnotations "ANNOTATION"}}' $cid expect_output "" run_buildah config \ --created-by COINCIDENCE \ --volume /VOLUME- \ --env VARIABLE=VALUE1,VALUE2 \ --label LABEL=VALUE \ --annotation ANNOTATION=VALUE1,VALUE2 \ $cid run_buildah commit --format docker $WITH_POLICY_JSON $cid scratch-image-docker run_buildah commit --format oci $WITH_POLICY_JSON $cid scratch-image-oci run_buildah inspect --format '{{.ImageCreatedBy}}' $cid expect_output "COINCIDENCE" check_matrix 'Config.Volumes' "map[]" } ================================================ FILE: tests/conformance/README.md ================================================ ![buildah logo](https://cdn.rawgit.com/containers/buildah/main/logos/buildah-logo_large.png) # Buildah/Docker Conformance Test Suite The conformance test for buildah is used to verify the images built with Buildah are equivalent to those built by Docker. It does this by building an image using the version of buildah library that's being tested, building what should be the same image using the docker engine's build API, and comparing them. ## Installing dependencies The additional dependencies for conformance testing are: * docker ### Install Docker CE Conformance tests use Docker CE to build images to be compared with images built with Buildah. Install Docker CE with dnf, yum or apt-get, based on your distribution and verify that the `docker` service is started. In Fedora, RHEL and CentOS `docker` or `moby-engine` rather than Docker CE may be installed by default. In Debian or Ubuntu you may instead have the `docker.io` package. Please verify that you install at least version 19.03. ## Run conformance tests These are the base images used by various conformance tests: ``` bash docker pull mirror.gcr.io/golang docker pull mirror.gcr.io/alpine docker pull mirror.gcr.io/busybox docker pull quay.io/libpod/centos:7 docker pull quay.io/libpod/ubuntu:latest ``` This test program is used as input in a few of the conformance tests: ``` make tests/conformance/testdata/mount-targets/true ``` You can run all of the tests with go test (and under `buildah unshare` or `podman unshare` if you're not root): ``` go test -v -timeout=30m -tags "$(./btrfs_installed_tag.sh)" ./tests/conformance ``` If you want to run one of the test cases you can use the "-run" flag: ``` go test -v -timeout=30m -tags "$(./btrfs_installed_tag.sh)" -run TestConformance/shell ./tests/conformance ``` If you also want to build and compare on a line-by-line basis, run: ``` go test -v -timeout=60m -tags "$(./btrfs_installed_tag.sh)" ./tests/conformance -compare-layers ``` ================================================ FILE: tests/conformance/conformance_test.go ================================================ package conformance import ( "archive/tar" "bytes" "context" "encoding/json" "errors" "flag" "fmt" "io" "io/fs" "maps" "os" "path" "path/filepath" "reflect" "regexp" "slices" "strconv" "strings" "sync" "syscall" "testing" "text/tabwriter" "time" "github.com/containers/buildah" "github.com/containers/buildah/copier" "github.com/containers/buildah/define" "github.com/containers/buildah/imagebuildah" "github.com/containers/buildah/internal/config" docker "github.com/fsouza/go-dockerclient" mobyclient "github.com/moby/moby/client" "github.com/moby/moby/client/pkg/versions" digest "github.com/opencontainers/go-digest" rspec "github.com/opencontainers/runtime-spec/specs-go" "github.com/openshift/imagebuilder" "github.com/openshift/imagebuilder/dockerclient" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.podman.io/image/v5/docker/daemon" "go.podman.io/image/v5/image" "go.podman.io/image/v5/manifest" "go.podman.io/image/v5/pkg/compression" is "go.podman.io/image/v5/storage" "go.podman.io/image/v5/transports" "go.podman.io/image/v5/transports/alltransports" "go.podman.io/image/v5/types" "go.podman.io/storage" "go.podman.io/storage/pkg/archive" "go.podman.io/storage/pkg/idtools" "go.podman.io/storage/pkg/ioutils" "go.podman.io/storage/pkg/reexec" ) const ( // See http://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html#tag_20_92_13_06, from archive/tar cISUID = 0o4000 // Set uid, from archive/tar cISGID = 0o2000 // Set gid, from archive/tar cISVTX = 0o1000 // Save text (sticky bit), from archive/tar // xattrs in the PAXRecords map are namespaced with this prefix xattrPAXRecordNamespace = "SCHILY.xattr." ) var ( originalSkip = []string{ "created", "container", "docker_version", "container_config:hostname", "config:hostname", "config:image", "container_config:cmd", "container_config:image", "history", "rootfs:diff_ids", "moby.buildkit.buildinfo.v1", } ociSkip = []string{ "created", "history", "rootfs:diff_ids", } fsSkip = []string{ // things that we volume mount or synthesize for RUN statements that currently bleed through "(dir):dev:mtime", // we let the timestamp be changed, if it exists at all, and so does docker build "(dir):proc:mtime", // we let the timestamp be changed, and so does docker build, which creates it on RUN "(dir):sys:mtime", // we let the timestamp be changed, and so does docker build, which creates it on RUN "(dir):etc:mtime", // we try to preserve the timestamp on RUN, but docker build doesn't } testDate = time.Unix(1485449953, 0) compareLayers = false compareImagebuilder = false testDataDir = "" dockerDir = "" imagebuilderDir = "" buildahDir = "" contextCanDoXattrs *bool storageCanDoXattrs *bool ) func TestMain(m *testing.M) { var logLevel string if reexec.Init() { return } cwd, err := os.Getwd() if err != nil { logrus.Fatalf("error finding current directory: %v", err) } testDataDir = filepath.Join(cwd, "testdata") flag.StringVar(&logLevel, "log-level", "error", "buildah logging log level") flag.BoolVar(&compareLayers, "compare-layers", compareLayers, "compare instruction-by-instruction") flag.BoolVar(&compareImagebuilder, "compare-imagebuilder", compareImagebuilder, "also compare using imagebuilder") flag.StringVar(&testDataDir, "testdata", testDataDir, "location of conformance testdata") flag.StringVar(&dockerDir, "docker-dir", dockerDir, "location to save docker build results") flag.StringVar(&imagebuilderDir, "imagebuilder-dir", imagebuilderDir, "location to save imagebuilder build results") flag.StringVar(&buildahDir, "buildah-dir", buildahDir, "location to save buildah build results") flag.Parse() var tempdir string if buildahDir == "" || dockerDir == "" || imagebuilderDir == "" { if tempdir == "" { if tempdir, err = os.MkdirTemp("", "conformance"); err != nil { logrus.Fatalf("creating temporary directory: %v", err) os.Exit(1) } } } if buildahDir == "" { buildahDir = filepath.Join(tempdir, "buildah") } if dockerDir == "" { dockerDir = filepath.Join(tempdir, "docker") } if imagebuilderDir == "" { imagebuilderDir = filepath.Join(tempdir, "imagebuilder") } level, err := logrus.ParseLevel(logLevel) if err != nil { logrus.Fatalf("error parsing log level %q: %v", logLevel, err) } logrus.SetLevel(level) result := m.Run() if err = os.RemoveAll(tempdir); err != nil { logrus.Errorf("removing temporary directory %q: %v", tempdir, err) } os.Exit(result) } func TestConformance(t *testing.T) { t.Parallel() dateStamp := fmt.Sprintf("%d", time.Now().UnixNano()) for i := range internalTestCases { t.Run(internalTestCases[i].name, func(t *testing.T) { if internalTestCases[i].testUsingSetParent { t.Run("new-set-parent", func(t *testing.T) { testConformanceInternal(t, dateStamp, i, func(test *testCase) { test.dockerBuilderVersion = docker.BuilderBuildKit test.compatSetParent = types.OptionalBoolFalse test.compatScratchConfig = types.OptionalBoolFalse }) }) t.Run("old-set-parent", func(t *testing.T) { testConformanceInternal(t, dateStamp, i, func(test *testCase) { test.dockerBuilderVersion = docker.BuilderV1 test.compatSetParent = types.OptionalBoolTrue test.compatScratchConfig = types.OptionalBoolTrue }) }) } else if internalTestCases[i].testUsingVolumes { t.Run("new-volumes", func(t *testing.T) { testConformanceInternal(t, dateStamp, i, func(test *testCase) { test.dockerBuilderVersion = docker.BuilderBuildKit test.compatVolumes = types.OptionalBoolFalse test.compatScratchConfig = types.OptionalBoolFalse }) }) t.Run("old-volumes", func(t *testing.T) { testConformanceInternal(t, dateStamp, i, func(test *testCase) { test.dockerBuilderVersion = docker.BuilderV1 test.compatVolumes = types.OptionalBoolTrue test.compatScratchConfig = types.OptionalBoolTrue test.fsSkip = slices.Concat(test.fsSkip, test.fsSkipCompatVolumesTrue) }) }) } else { testConformanceInternal(t, dateStamp, i, nil) } }) } } func testConformanceInternal(t *testing.T, dateStamp string, testIndex int, mutate func(*testCase)) { test := internalTestCases[testIndex] if mutate != nil { mutate(&test) } ctx := context.TODO() cwd, err := os.Getwd() require.NoError(t, err, "error finding current directory") // create a temporary directory to hold our build context tempdir := t.TempDir() // create subdirectories to use as the build context and for buildah storage contextDir := filepath.Join(tempdir, "context") rootDir := filepath.Join(tempdir, "root") runrootDir := filepath.Join(tempdir, "runroot") // check if we can test xattrs where we're storing build contexts if contextCanDoXattrs == nil { testDir := filepath.Join(tempdir, "test") if err := os.Mkdir(testDir, 0o700); err != nil { require.NoErrorf(t, err, "error creating test directory to check if xattrs are testable: %v", err) } testFile := filepath.Join(testDir, "testfile") if err := os.WriteFile(testFile, []byte("whatever"), 0o600); err != nil { require.NoErrorf(t, err, "error creating test file to check if xattrs are testable: %v", err) } can := false if err := copier.Lsetxattrs(testFile, map[string]string{"user.test": "test"}); err == nil { can = true } contextCanDoXattrs = &can } // copy either a directory or just a Dockerfile into the temporary directory pipeReader, pipeWriter := io.Pipe() var getErr, putErr error var wg sync.WaitGroup wg.Add(1) go func() { if test.contextDir != "" { getErr = copier.Get("", testDataDir, copier.GetOptions{}, []string{test.contextDir}, pipeWriter) } else if test.dockerfile != "" { getErr = copier.Get("", testDataDir, copier.GetOptions{}, []string{test.dockerfile}, pipeWriter) } pipeWriter.Close() wg.Done() }() wg.Add(1) go func() { if test.contextDir != "" || test.dockerfile != "" { putErr = copier.Put("", contextDir, copier.PutOptions{}, pipeReader) } else { putErr = os.Mkdir(contextDir, 0o755) } pipeReader.Close() wg.Done() }() wg.Wait() assert.NoErrorf(t, getErr, "error reading build info from %q", filepath.Join("testdata", test.dockerfile)) assert.NoErrorf(t, putErr, "error writing build info to %q", contextDir) if t.Failed() { t.FailNow() } // construct the names that we want to assign to the images. these should be reasonably unique buildahImage := fmt.Sprintf("conformance-buildah:%s-%d", dateStamp, testIndex) dockerImage := fmt.Sprintf("conformance-docker:%s-%d", dateStamp, testIndex) imagebuilderImage := fmt.Sprintf("conformance-imagebuilder:%s-%d", dateStamp, testIndex) if mutate != nil { buildahImage += path.Base(t.Name()) dockerImage += path.Base(t.Name()) imagebuilderImage += path.Base(t.Name()) } // compute the name of the Dockerfile in the build context directory var dockerfileName string if test.dockerfile != "" { dockerfileName = filepath.Join(contextDir, test.dockerfile) } else { dockerfileName = filepath.Join(contextDir, "Dockerfile") } // read the Dockerfile, for inclusion in failure messages dockerfileContents := []byte(test.dockerfileContents) if len(dockerfileContents) == 0 { // no inlined contents -> read them from the specified location contents, err := os.ReadFile(dockerfileName) require.NoErrorf(t, err, "error reading Dockerfile %q", filepath.Join(tempdir, dockerfileName)) dockerfileContents = contents } // initialize storage for buildah options := storage.StoreOptions{ GraphDriverName: os.Getenv("STORAGE_DRIVER"), GraphRoot: rootDir, RunRoot: runrootDir, } store, err := storage.GetStore(options) require.NoErrorf(t, err, "error creating buildah storage at %q", rootDir) defer func() { if store != nil { _, err := store.Shutdown(true) require.NoError(t, err, "error shutting down storage for buildah") } }() storageDriver := store.GraphDriverName() storageRoot := store.GraphRoot() // now that we have a Store, check if we can test xattrs in storage layers if storageCanDoXattrs == nil { layer, err := store.CreateLayer("", "", nil, "", true, nil) if err != nil { require.NoErrorf(t, err, "error creating test layer to check if xattrs are testable: %v", err) } mountPoint, err := store.Mount(layer.ID, "") if err != nil { require.NoErrorf(t, err, "error mounting test layer to check if xattrs are testable: %v", err) } testFile := filepath.Join(mountPoint, "testfile") if err := os.WriteFile(testFile, []byte("whatever"), 0o600); err != nil { require.NoErrorf(t, err, "error creating file in test layer to check if xattrs are testable: %v", err) } can := false if err := copier.Lsetxattrs(testFile, map[string]string{"user.test": "test"}); err == nil { can = true } storageCanDoXattrs = &can err = store.DeleteLayer(layer.ID) if err != nil { require.NoErrorf(t, err, "error removing test layer after checking if xattrs are testable: %v", err) } } // connect to dockerd using the docker client library mobyClient, err := mobyclient.New(mobyclient.FromEnv) require.NoError(t, err, "unable to initialize docker.client") _, err = mobyClient.Ping(ctx, mobyclient.PingOptions{ NegotiateAPIVersion: true, }) require.NoError(t, err) if test.dockerUseBuildKit || test.dockerBuilderVersion != "" { negotiatedVersion := mobyClient.ClientVersion() if versions.LessThan(negotiatedVersion, "1.38") { t.Skipf("negotiated version %q is too low", err) } } // connect to dockerd using go-dockerclient // Later, the client.BuildImage implementation chooses an API version based on // fields set in docker.BuildImageOptions. Because we don’t use _so_ new features, // that can use a fairly old version. // // If we don’t want this old version to be used, we must specify a version ourselves // (the API requries it to be specified when initializing the client). We could create // a client only to call .Version() and then create another client witht the discovered version, // but that’s not really any more correct. // // As of 2026-02, the client is choosing 1.39, while our server supports 1.44–1.52. // Previously we used 1.51, so let’s hard-code that. client, err := docker.NewVersionedClientFromEnv("1.51") require.NoError(t, err, "unable to initialize docker client") var dockerVersion []string if version, err := client.Version(); err == nil { if version != nil { for _, s := range *version { dockerVersion = append(dockerVersion, s) } } } else { require.NoError(t, err, "unable to connect to docker daemon") } // make any last-minute tweaks to the build context directory that this test requires if test.tweakContextDir != nil { err = test.tweakContextDir(t, contextDir, storageDriver, storageRoot) require.NoErrorf(t, err, "error tweaking context directory using test-specific callback: %v", err) } // decide whether we're building just one image for this Dockerfile, or // one for each line in it after the first, which we'll assume is a FROM if compareLayers { // build and compare one line at a time line := 1 for i := range dockerfileContents { // scan the byte slice for newlines or the end of the slice, and build using the contents up to that point if i == len(dockerfileContents)-1 || (dockerfileContents[i] == '\n' && (i == 0 || dockerfileContents[i-1] != '\\')) { if line > 1 || !bytes.HasPrefix(dockerfileContents, []byte("FROM ")) { // hack: skip trying to build just the first FROM line t.Run(fmt.Sprintf("%d", line), func(t *testing.T) { testConformanceInternalBuild(ctx, t, cwd, store, client, mobyClient, fmt.Sprintf("%s.%d", buildahImage, line), fmt.Sprintf("%s.%d", dockerImage, line), fmt.Sprintf("%s.%d", imagebuilderImage, line), contextDir, dockerfileName, dockerfileContents[:i+1], test, line, i == len(dockerfileContents)-1, dockerVersion) }) } line++ } } } else { // build to completion testConformanceInternalBuild(ctx, t, cwd, store, client, mobyClient, buildahImage, dockerImage, imagebuilderImage, contextDir, dockerfileName, dockerfileContents, test, 0, true, dockerVersion) } } func testConformanceInternalBuild(ctx context.Context, t *testing.T, cwd string, store storage.Store, client *docker.Client, mobyClient *mobyclient.Client, buildahImage, dockerImage, imagebuilderImage, contextDir, dockerfileName string, dockerfileContents []byte, test testCase, line int, finalOfSeveral bool, dockerVersion []string) { var buildahLog, dockerLog, imagebuilderLog []byte var buildahRef, dockerRef, imagebuilderRef types.ImageReference // overwrite the Dockerfile in the build context for this run using the // contents we were passed, which may only be an initial subset of the // original file, or inlined information, in which case the file didn't // necessarily exist err := os.WriteFile(dockerfileName, dockerfileContents, 0o644) require.NoErrorf(t, err, "error writing Dockerfile at %q", dockerfileName) err = os.Chtimes(dockerfileName, testDate, testDate) require.NoErrorf(t, err, "error resetting timestamp on Dockerfile at %q", dockerfileName) err = os.Chtimes(contextDir, testDate, testDate) require.NoErrorf(t, err, "error resetting timestamp on context directory at %q", contextDir) defer func() { if t.Failed() { if test.contextDir != "" { t.Logf("Context %q", filepath.Join(cwd, "testdata", test.contextDir)) } if test.dockerfile != "" { if test.contextDir != "" { t.Logf("Dockerfile: %q", filepath.Join(cwd, "testdata", test.contextDir, test.dockerfile)) } else { t.Logf("Dockerfile: %q", filepath.Join(cwd, "testdata", test.dockerfile)) } } if !bytes.HasSuffix(dockerfileContents, []byte{'\n'}) && !bytes.HasSuffix(dockerfileContents, []byte{'\r'}) { dockerfileContents = append(dockerfileContents, []byte("\n(no final end-of-line)")...) } t.Logf("Dockerfile contents:\n%s", dockerfileContents) if dockerignoreContents, err := os.ReadFile(filepath.Join(contextDir, ".dockerignore")); err == nil { t.Logf(".dockerignore contents:\n%s", string(dockerignoreContents)) } } }() // build using docker if !test.withoutDocker { dockerRef, dockerLog = buildUsingDocker(ctx, t, client, mobyClient, test, dockerImage, contextDir, dockerfileName, line, finalOfSeveral) if dockerRef != nil { defer func() { err := client.RemoveImageExtended(dockerImage, docker.RemoveImageOptions{ Context: ctx, Force: true, }) assert.Nil(t, err, "error deleting newly-built-by-docker image %q", dockerImage) }() } saveReport(ctx, t, dockerRef, filepath.Join(dockerDir, t.Name()), dockerfileContents, dockerLog, dockerVersion) if finalOfSeveral && compareLayers { saveReport(ctx, t, dockerRef, filepath.Join(dockerDir, t.Name(), ".."), dockerfileContents, dockerLog, dockerVersion) } } if t.Failed() { t.FailNow() } // build using imagebuilder if we're testing with it, too if compareImagebuilder && !test.withoutImagebuilder { imagebuilderRef, imagebuilderLog = buildUsingImagebuilder(t, client, test, imagebuilderImage, contextDir, dockerfileName, line, finalOfSeveral) if imagebuilderRef != nil { defer func() { err := client.RemoveImageExtended(imagebuilderImage, docker.RemoveImageOptions{ Context: ctx, Force: true, }) assert.Nil(t, err, "error deleting newly-built-by-imagebuilder image %q", imagebuilderImage) }() } saveReport(ctx, t, imagebuilderRef, filepath.Join(imagebuilderDir, t.Name()), dockerfileContents, imagebuilderLog, dockerVersion) if finalOfSeveral && compareLayers { saveReport(ctx, t, imagebuilderRef, filepath.Join(imagebuilderDir, t.Name(), ".."), dockerfileContents, imagebuilderLog, dockerVersion) } } if t.Failed() { t.FailNow() } // always build using buildah buildahRef, buildahLog = buildUsingBuildah(ctx, t, store, test, buildahImage, contextDir, dockerfileName, line, finalOfSeveral) if buildahRef != nil { defer func() { err := buildahRef.DeleteImage(ctx, nil) assert.Nil(t, err, "error deleting newly-built-by-buildah image %q", buildahImage) }() } saveReport(ctx, t, buildahRef, filepath.Join(buildahDir, t.Name()), dockerfileContents, buildahLog, nil) if finalOfSeveral && compareLayers { saveReport(ctx, t, buildahRef, filepath.Join(buildahDir, t.Name(), ".."), dockerfileContents, buildahLog, nil) } if t.Failed() { t.FailNow() } if test.shouldFailAt != 0 { // the build is expected to fail, so there's no point in comparing information about any images return } // the report on the buildah image should always be there _, originalBuildahConfig, ociBuildahConfig, fsBuildah := readReport(t, filepath.Join(buildahDir, t.Name())) if t.Failed() { t.FailNow() } deleteIdentityLabel := func(config map[string]any) { for _, configName := range []string{"config", "container_config"} { if configStruct, ok := config[configName]; ok { if configMap, ok := configStruct.(map[string]any); ok { if labels, ok := configMap["Labels"]; ok { if labelMap, ok := labels.(map[string]any); ok { delete(labelMap, buildah.BuilderIdentityAnnotation) } } } } } } deleteIdentityLabel(originalBuildahConfig) deleteIdentityLabel(ociBuildahConfig) var originalDockerConfig, ociDockerConfig, fsDocker map[string]any // the report on the docker image should be there if we expected the build to succeed if !test.withoutDocker { var mediaType string mediaType, originalDockerConfig, ociDockerConfig, fsDocker = readReport(t, filepath.Join(dockerDir, t.Name())) assert.Equal(t, manifest.DockerV2Schema2MediaType, mediaType, "Image built by docker build didn't use Docker MIME type - tests require update") if t.Failed() { t.FailNow() } // Some of the base images for our tests were built with buildah, too deleteIdentityLabel(originalDockerConfig) deleteIdentityLabel(ociDockerConfig) miss, left, diff, same := compareJSON(originalDockerConfig, originalBuildahConfig, originalSkip) if !same { assert.Failf(t, "Image configurations differ as committed in Docker format", configCompareResult(miss, left, diff, "buildah")) } miss, left, diff, same = compareJSON(ociDockerConfig, ociBuildahConfig, ociSkip) if !same { assert.Failf(t, "Image configurations differ when converted to OCI format", configCompareResult(miss, left, diff, "buildah")) } miss, left, diff, same = compareJSON(fsDocker, fsBuildah, append(fsSkip, test.fsSkip...)) if !same { assert.Failf(t, "Filesystem contents differ", fsCompareResult(miss, left, diff, "buildah")) } } // the report on the imagebuilder image should be there if we expected the build to succeed if compareImagebuilder && !test.withoutImagebuilder { _, originalDockerConfig, ociDockerConfig, fsDocker = readReport(t, filepath.Join(dockerDir, t.Name())) if t.Failed() { t.FailNow() } _, originalImagebuilderConfig, ociImagebuilderConfig, fsImagebuilder := readReport(t, filepath.Join(imagebuilderDir, t.Name())) if t.Failed() { t.FailNow() } // compare the reports between docker and imagebuilder miss, left, diff, same := compareJSON(originalDockerConfig, originalImagebuilderConfig, originalSkip) if !same { assert.Failf(t, "Image configurations differ as committed in Docker format", configCompareResult(miss, left, diff, "imagebuilder")) } miss, left, diff, same = compareJSON(ociDockerConfig, ociImagebuilderConfig, ociSkip) if !same { assert.Failf(t, "Image configurations differ when converted to OCI format", configCompareResult(miss, left, diff, "imagebuilder")) } miss, left, diff, same = compareJSON(fsDocker, fsImagebuilder, append(fsSkip, test.fsSkip...)) if !same { assert.Failf(t, "Filesystem contents differ", fsCompareResult(miss, left, diff, "imagebuilder")) } } } func buildUsingBuildah(ctx context.Context, t *testing.T, store storage.Store, test testCase, buildahImage, contextDir, dockerfileName string, line int, finalOfSeveral bool) (buildahRef types.ImageReference, buildahLog []byte) { // buildah tests might be using transient mounts. replace "@@TEMPDIR@@" // in such specifications with the path of the context directory var transientMounts []string for _, mount := range test.transientMounts { transientMounts = append(transientMounts, strings.Replace(mount, "@@TEMPDIR@@", contextDir, 1)) } // set up build options output := &bytes.Buffer{} if test.compatSetParent != types.OptionalBoolUndefined { compat := "default" switch test.compatSetParent { case types.OptionalBoolFalse: compat = "false" case types.OptionalBoolTrue: compat = "true" } t.Logf("using buildah flag CompatSetParent = %s", compat) } if test.compatVolumes != types.OptionalBoolUndefined { compat := "default" switch test.compatVolumes { case types.OptionalBoolFalse: compat = "false" case types.OptionalBoolTrue: compat = "true" } t.Logf("using buildah flag CompatVolumes = %s", compat) } if test.compatScratchConfig != types.OptionalBoolUndefined { compat := "default" switch test.compatScratchConfig { case types.OptionalBoolFalse: compat = "false" case types.OptionalBoolTrue: compat = "true" } t.Logf("using buildah flag CompatScratchConfig = %s", compat) } options := define.BuildOptions{ ContextDirectory: contextDir, CommonBuildOpts: &define.CommonBuildOptions{}, NamespaceOptions: []define.NamespaceOption{{ Name: string(rspec.NetworkNamespace), Host: true, }}, TransientMounts: transientMounts, Output: buildahImage, OutputFormat: buildah.Dockerv2ImageManifest, Out: output, Err: output, Layers: true, NoCache: true, RemoveIntermediateCtrs: true, ForceRmIntermediateCtrs: true, CompatSetParent: test.compatSetParent, CompatVolumes: test.compatVolumes, CompatScratchConfig: test.compatScratchConfig, CompatLayerOmissions: test.compatLayerOmissions, Args: maps.Clone(test.buildArgs), } // build the image and gather output. log the output if the build part of the test failed imageID, _, err := imagebuildah.BuildDockerfiles(ctx, store, options, dockerfileName) if err != nil { output.WriteString("\n" + err.Error()) } outputString := output.String() defer func() { if t.Failed() { t.Logf("buildah output:\n%s", outputString) } }() buildPost(t, test, err, "buildah", outputString, test.buildahRegex, test.buildahErrRegex, line, finalOfSeveral) // return a reference to the new image, if we succeeded if err == nil { buildahRef, err = is.Transport.ParseStoreReference(store, imageID) assert.Nil(t, err, "error parsing reference to newly-built image with ID %q", imageID) } return buildahRef, []byte(outputString) } func pullImageIfMissing(t *testing.T, client *docker.Client, image string) { if _, err := client.InspectImage(image); err != nil { repository, tag := docker.ParseRepositoryTag(image) if tag == "" { tag = "latest" } pullOptions := docker.PullImageOptions{ Repository: repository, Tag: tag, } pullAuths := docker.AuthConfiguration{} if err := client.PullImage(pullOptions, pullAuths); err != nil { t.Fatalf("while pulling %q: %v", image, err) } } } func buildUsingDocker(ctx context.Context, t *testing.T, client *docker.Client, mobyClient *mobyclient.Client, test testCase, dockerImage, contextDir, dockerfileName string, line int, finalOfSeveral bool) (dockerRef types.ImageReference, dockerLog []byte) { // compute the path of the dockerfile relative to the build context dockerfileRelativePath, err := filepath.Rel(contextDir, dockerfileName) require.NoErrorf(t, err, "unable to compute path of dockerfile %q relative to context directory %q", dockerfileName, contextDir) // read the Dockerfile so that we can pull base images dockerfileContent, err := os.ReadFile(dockerfileName) require.NoErrorf(t, err, "reading dockerfile %q", dockerfileName) for line := range strings.SplitSeq(string(dockerfileContent), "\n") { line = strings.TrimSpace(line) if after, ok := strings.CutPrefix(line, "# syntax="); ok { pullImageIfMissing(t, client, after) } } parsed, err := imagebuilder.ParseDockerfile(bytes.NewReader(dockerfileContent)) require.NoErrorf(t, err, "parsing dockerfile %q", dockerfileName) dummyBuilder := imagebuilder.NewBuilder(nil) stages, err := imagebuilder.NewStages(parsed, dummyBuilder) require.NoErrorf(t, err, "breaking dockerfile %q up into stages", dockerfileName) for i := range stages { stageBase, err := dummyBuilder.From(stages[i].Node) require.NoErrorf(t, err, "parsing base image for stage %d in %q", i, dockerfileName) if stageBase == "" || stageBase == imagebuilder.NoBaseImageSpecifier { continue } needToEnsureBase := true for j := range i { if stageBase == stages[j].Name { needToEnsureBase = false } } if !needToEnsureBase { continue } pullImageIfMissing(t, client, stageBase) } excludes, err := imagebuilder.ParseDockerignore(contextDir) require.NoErrorf(t, err, "parsing ignores file in %q", contextDir) excludes = append(excludes, "!"+dockerfileRelativePath, "!.dockerignore") tarOptions := &archive.TarOptions{ ExcludePatterns: excludes, ChownOpts: &idtools.IDPair{UID: 0, GID: 0}, } input, err := archive.TarWithOptions(contextDir, tarOptions) require.NoErrorf(t, err, "archiving context directory %q", contextDir) defer input.Close() var buildArgs []docker.BuildArg for k, v := range test.buildArgs { buildArgs = append(buildArgs, docker.BuildArg{Name: k, Value: v}) } // set up build options output := &bytes.Buffer{} options := docker.BuildImageOptions{ Context: ctx, Dockerfile: dockerfileRelativePath, InputStream: input, OutputStream: output, Name: dockerImage, NoCache: true, RmTmpContainer: true, ForceRmTmpContainer: true, BuildArgs: buildArgs, } if test.dockerUseBuildKit || test.dockerBuilderVersion != "" { if test.dockerBuilderVersion != "" { var version string switch test.dockerBuilderVersion { case docker.BuilderBuildKit: version = "BuildKit" case docker.BuilderV1: version = "V1 (classic)" default: version = "(unknown)" } t.Logf("requesting docker builder %s", version) options.Version = test.dockerBuilderVersion } else { t.Log("requesting docker builder BuildKit") options.Version = docker.BuilderBuildKit } } // build the image and gather output. log the output if the build part of the test failed err = client.BuildImage(options) if err != nil { output.WriteString("\n" + err.Error()) } if _, err := mobyClient.BuildCachePrune(ctx, mobyclient.BuildCachePruneOptions{All: true}); err != nil { t.Logf("docker build cache prune: %v", err) } outputString := output.String() defer func() { if t.Failed() { t.Logf("docker build output:\n%s", outputString) } }() buildPost(t, test, err, "docker build", outputString, test.dockerRegex, test.dockerErrRegex, line, finalOfSeveral) // return a reference to the new image, if we succeeded if err == nil { dockerRef, err = daemon.ParseReference(dockerImage) assert.Nil(t, err, "error parsing reference to newly-built image with name %q", dockerImage) } return dockerRef, []byte(outputString) } func buildUsingImagebuilder(t *testing.T, client *docker.Client, test testCase, imagebuilderImage, contextDir, dockerfileName string, line int, finalOfSeveral bool) (imagebuilderRef types.ImageReference, imagebuilderLog []byte) { // compute the path of the dockerfile relative to the build context dockerfileRelativePath, err := filepath.Rel(contextDir, dockerfileName) require.NoErrorf(t, err, "unable to compute path of dockerfile %q relative to context directory %q", dockerfileName, contextDir) // set up build options output := &bytes.Buffer{} executor := dockerclient.NewClientExecutor(client) executor.Directory = contextDir executor.Tag = imagebuilderImage executor.AllowPull = true executor.Out = output executor.ErrOut = output executor.LogFn = func(format string, args ...any) { fmt.Fprintf(output, "--> %s\n", fmt.Sprintf(format, args...)) } // buildah tests might be using transient mounts. replace "@@TEMPDIR@@" // in such specifications with the path of the context directory for _, mount := range test.transientMounts { var src, dest string mountSpec := strings.SplitN(strings.Replace(mount, "@@TEMPDIR@@", contextDir, 1), ":", 2) if len(mountSpec) > 1 { src = mountSpec[0] } dest = mountSpec[len(mountSpec)-1] executor.TransientMounts = append(executor.TransientMounts, dockerclient.Mount{ SourcePath: src, DestinationPath: dest, }) } // build the image and gather output. log the output if the build part of the test failed builder := imagebuilder.NewBuilder(maps.Clone(test.buildArgs)) node, err := imagebuilder.ParseFile(filepath.Join(contextDir, dockerfileRelativePath)) if err != nil { assert.Nil(t, err, "error parsing Dockerfile: %v", err) } if _, err = os.Stat(filepath.Join(contextDir, ".dockerignore")); err == nil { if builder.Excludes, err = imagebuilder.ParseDockerignore(contextDir); err != nil { assert.Nil(t, err, "error parsing .dockerignore file: %v", err) } } stages, err := imagebuilder.NewStages(node, builder) if err != nil { assert.Nil(t, err, "error breaking Dockerfile into stages") } else { if finalExecutor, err := executor.Stages(builder, stages, ""); err != nil { output.WriteString("\n" + err.Error()) } else { if err = finalExecutor.Commit(stages[len(stages)-1].Builder); err != nil { assert.Nil(t, err, "error committing final stage: %v", err) } } } outputString := output.String() defer func() { if t.Failed() { t.Logf("imagebuilder build output:\n%s", outputString) } for err := range executor.Release() { t.Logf("imagebuilder build post-error: %v", err) } }() buildPost(t, test, err, "imagebuilder", outputString, test.imagebuilderRegex, test.imagebuilderErrRegex, line, finalOfSeveral) // return a reference to the new image, if we succeeded if err == nil { imagebuilderRef, err = daemon.ParseReference(imagebuilderImage) assert.Nil(t, err, "error parsing reference to newly-built image with name %q", imagebuilderImage) } return imagebuilderRef, []byte(outputString) } func buildPost(t *testing.T, test testCase, err error, buildTool, outputString, stdoutRegex, stderrRegex string, line int, finalOfSeveral bool) { // check if the build succeeded or failed, whichever was expected if test.shouldFailAt != 0 && (line == 0 || line >= test.shouldFailAt) { // this is expected to fail, and we're either at/past // the line where it should fail, or we're not going // line-by-line assert.NotNil(t, err, fmt.Sprintf("%s build was expected to fail, but succeeded", buildTool)) } else { assert.Nil(t, err, fmt.Sprintf("%s build was expected to succeed, but failed", buildTool)) } // if the build failed, and we have an error message we expected, check for it if err != nil && test.failureRegex != "" { outputTokens := strings.Join(strings.Fields(err.Error()), " ") assert.Regexpf(t, regexp.MustCompile(test.failureRegex), outputTokens, "build failure did not match %q", test.failureRegex) } // if this is the last image we're building for this case, we can scan // the build log for expected messages if finalOfSeveral { outputTokens := strings.Join(strings.Fields(outputString), " ") // check for expected output if stdoutRegex != "" { assert.Regexpf(t, regexp.MustCompile(stdoutRegex), outputTokens, "build output did not match %q", stdoutRegex) } if stderrRegex != "" { assert.Regexpf(t, regexp.MustCompile(stderrRegex), outputTokens, "build error did not match %q", stderrRegex) } } } // FSTree holds the information we have about an image's filesystem type FSTree struct { Layers []Layer `json:"layers,omitempty"` Tree FSEntry `json:"tree"` } // Layer keeps track of the digests and contents of a layer blob type Layer struct { UncompressedDigest digest.Digest `json:"uncompressed-digest,omitempty"` CompressedDigest digest.Digest `json:"compressed-digest,omitempty"` Headers []FSHeader `json:"-"` } // FSHeader is the parts of the tar.Header for an entry in a layer blob that // are relevant type FSHeader struct { Typeflag byte `json:"typeflag,omitempty"` Name string `json:"name,omitempty"` Linkname string `json:"linkname,omitempty"` Size int64 `json:"size"` Mode int64 `json:"mode,omitempty"` UID int `json:"uid"` GID int `json:"gid"` ModTime time.Time `json:"mtime"` Devmajor int64 `json:"devmajor,omitempty"` Devminor int64 `json:"devminor,omitempty"` Xattrs map[string]string `json:"xattrs,omitempty"` Digest digest.Digest `json:"digest,omitempty"` } // FSEntry stores one item in a filesystem tree. If it represents a directory, // its contents are stored as its children type FSEntry struct { FSHeader Children map[string]*FSEntry `json:"(dir),omitempty"` } // fsHeaderForEntry converts a tar header to an FSHeader, in the process // discarding some fields which we don't care to compare func fsHeaderForEntry(hdr *tar.Header) FSHeader { return FSHeader{ Typeflag: hdr.Typeflag, Name: hdr.Name, Linkname: hdr.Linkname, Size: hdr.Size, Mode: (hdr.Mode & int64(fs.ModePerm)), UID: hdr.Uid, GID: hdr.Gid, ModTime: hdr.ModTime, Devmajor: hdr.Devmajor, Devminor: hdr.Devminor, Xattrs: hdr.Xattrs, //nolint:staticcheck } } // save information about the specified image to the specified directory func saveReport(ctx context.Context, t *testing.T, ref types.ImageReference, directory string, dockerfileContents []byte, buildLog []byte, version []string) { imageName := "" // make sure the directory exists err := os.MkdirAll(directory, 0o755) require.NoErrorf(t, err, "error ensuring directory %q exists for storing a report", directory) // save the Dockerfile that was used to generate the image err = os.WriteFile(filepath.Join(directory, "Dockerfile"), dockerfileContents, 0o644) require.NoErrorf(t, err, "error saving Dockerfile for image %q", imageName) // save the log generated while building the image err = os.WriteFile(filepath.Join(directory, "build.log"), buildLog, 0o644) require.NoErrorf(t, err, "error saving build log for image %q", imageName) // save the version information if len(version) > 0 { err = os.WriteFile(filepath.Join(directory, "version"), []byte(strings.Join(version, "\n")+"\n"), 0o644) require.NoErrorf(t, err, "error saving builder version information for image %q", imageName) } // open the image for reading if ref == nil { return } imageName = transports.ImageName(ref) src, err := ref.NewImageSource(ctx, nil) require.NoErrorf(t, err, "error opening image %q as source to read its configuration", imageName) closer := io.Closer(src) defer func() { closer.Close() }() img, err := image.FromSource(ctx, nil, src) require.NoErrorf(t, err, "error opening image %q to read its configuration", imageName) closer = img // read the manifest in its original form rawManifest, _, err := img.Manifest(ctx) require.NoErrorf(t, err, "error reading raw manifest from image %q", imageName) // read the config blob in its original form rawConfig, err := img.ConfigBlob(ctx) require.NoErrorf(t, err, "error reading configuration from image %q", imageName) // read the config blob, converted to OCI format by the image library, and re-encode it ociConfig, err := img.OCIConfig(ctx) require.NoErrorf(t, err, "error reading OCI-format configuration from image %q", imageName) encodedConfig, err := json.Marshal(ociConfig) require.NoErrorf(t, err, "error encoding OCI-format configuration from image %q for saving", imageName) // save the manifest in its original form err = os.WriteFile(filepath.Join(directory, "manifest.json"), rawManifest, 0o644) require.NoErrorf(t, err, "error saving original manifest from image %q", imageName) // save the config blob in the OCI format err = os.WriteFile(filepath.Join(directory, "oci-config.json"), encodedConfig, 0o644) require.NoErrorf(t, err, "error saving OCI-format configuration from image %q", imageName) // save the config blob in its original format err = os.WriteFile(filepath.Join(directory, "config.json"), rawConfig, 0o644) require.NoErrorf(t, err, "error saving original configuration from image %q", imageName) // start pulling layer information layerBlobInfos, err := img.LayerInfosForCopy(ctx) require.NoErrorf(t, err, "error reading blob infos for image %q", imageName) if len(layerBlobInfos) == 0 { layerBlobInfos = img.LayerInfos() } fstree := FSTree{Tree: FSEntry{Children: make(map[string]*FSEntry)}} // grab digest and header information from the layer blob for _, layerBlobInfo := range layerBlobInfos { rc, _, err := src.GetBlob(ctx, layerBlobInfo, nil) require.NoErrorf(t, err, "error reading blob %+v for image %q", layerBlobInfo, imageName) defer rc.Close() layer := summarizeLayer(t, imageName, layerBlobInfo, rc) fstree.Layers = append(fstree.Layers, layer) } // apply the header information from blobs, in the order they're listed // in the config blob, to produce what we think the filesystem tree // would look like for _, diffID := range ociConfig.RootFS.DiffIDs { var layer *Layer for i := range fstree.Layers { if fstree.Layers[i].CompressedDigest == diffID { layer = &fstree.Layers[i] break } if fstree.Layers[i].UncompressedDigest == diffID { layer = &fstree.Layers[i] break } } if layer == nil { require.Failf(t, "missing layer diff", "config for image %q specifies a layer with diffID %q, but we found no layer blob matching that digest", imageName, diffID) } applyLayerToFSTree(t, layer, &fstree.Tree) } // encode the filesystem tree information and save it to a file, // discarding the layer summaries because different tools may choose // between marking a directory as opaque and removing each of its // contents individually, which would produce the same result, so // there's no point in saving them for comparison later encodedFSTree, err := json.Marshal(fstree.Tree) require.NoErrorf(t, err, "error encoding filesystem tree from image %q for saving", imageName) err = os.WriteFile(filepath.Join(directory, "fs.json"), encodedFSTree, 0o644) require.NoErrorf(t, err, "error saving filesystem tree from image %q", imageName) } // summarizeLayer reads a blob and returns a summary of the parts of its contents that we care about func summarizeLayer(t *testing.T, imageName string, blobInfo types.BlobInfo, reader io.Reader) (layer Layer) { compressedDigest := digest.Canonical.Digester() counter := ioutils.NewWriteCounter(compressedDigest.Hash()) compressionAlgorithm, _, reader, err := compression.DetectCompressionFormat(reader) require.NoErrorf(t, err, "error checking if blob %+v for image %q is compressed", blobInfo, imageName) uncompressedBlob, wasCompressed, err := compression.AutoDecompress(io.TeeReader(reader, counter)) require.NoErrorf(t, err, "error decompressing blob %+v for image %q", blobInfo, imageName) defer uncompressedBlob.Close() uncompressedDigest := digest.Canonical.Digester() tarToRead := io.TeeReader(uncompressedBlob, uncompressedDigest.Hash()) tr := tar.NewReader(tarToRead) hdr, err := tr.Next() for err == nil { header := fsHeaderForEntry(hdr) if hdr.Size != 0 { contentDigest := digest.Canonical.Digester() n, err := io.Copy(contentDigest.Hash(), tr) require.NoErrorf(t, err, "error digesting contents of %q from layer %+v for image %q", hdr.Name, blobInfo, imageName) require.Equal(t, hdr.Size, n, "error reading contents of %q from layer %+v for image %q: wrong size", hdr.Name, blobInfo, imageName) header.Digest = contentDigest.Digest() } layer.Headers = append(layer.Headers, header) hdr, err = tr.Next() } require.Equal(t, io.EOF, err, "unexpected error reading layer contents %+v for image %q", blobInfo, imageName) _, err = io.Copy(io.Discard, tarToRead) require.NoError(t, err, "reading out any not-usually-present zero padding at the end") layer.CompressedDigest = compressedDigest.Digest() blobFormatDescription := "uncompressed" if wasCompressed { if compressionAlgorithm.Name() != "" { blobFormatDescription = "compressed with " + compressionAlgorithm.Name() } else { blobFormatDescription = "compressed (?)" } } require.Equalf(t, blobInfo.Digest, layer.CompressedDigest, "calculated digest of %s blob didn't match expected digest (expected length %d, actual length %d)", blobFormatDescription, blobInfo.Size, counter.Count) layer.UncompressedDigest = uncompressedDigest.Digest() return layer } // applyLayerToFSTree updates the in-memory summary of a tree to incorporate // changes described in the layer. This is a little naive, in that we don't // expect the pathname to include symlinks, which we don't resolve, as // components, but tools that currently generate layer diffs don't create // those. func applyLayerToFSTree(t *testing.T, layer *Layer, root *FSEntry) { for i, entry := range layer.Headers { if entry.Typeflag == tar.TypeLink { // if the entry is a hard link, replace it with the // contents of the hard-linked file replaced := false name := entry.Name for j, otherEntry := range layer.Headers { if j >= i { break } if otherEntry.Name == entry.Linkname { entry = otherEntry entry.Name = name replaced = true break } } if !replaced { require.Fail(t, "layer diff error", "hardlink entry referenced a file that isn't in the layer") } } // parse the name from the entry, and don't get tripped up by a final '/' dirEntry := root components := strings.Split(strings.Trim(entry.Name, string(os.PathSeparator)), string(os.PathSeparator)) require.NotEmpty(t, entry.Name, "layer diff error", "entry has no name") require.NotZerof(t, len(components), "entry name %q has no components", entry.Name) require.NotZerof(t, components[0], "entry name %q has no components", entry.Name) // "split" the final part of the path from the rest base := components[len(components)-1] components = components[:len(components)-1] // find the directory that contains this entry for i, component := range components { // this should be a parent directory, so check if it looks like a parent directory if dirEntry.Children == nil { require.Failf(t, "layer diff error", "layer diff %q includes entry for %q, but %q is not a directory", layer.UncompressedDigest, entry.Name, strings.Join(components[:i], string(os.PathSeparator))) } // if the directory is already there, move into it if child, ok := dirEntry.Children[component]; ok { dirEntry = child continue } // if the directory should be there, but we haven't // created it yet, blame the tool that generated this // layer diff require.Failf(t, "layer diff error", "layer diff %q includes entry for %q, but %q doesn't exist", layer.UncompressedDigest, entry.Name, strings.Join(components[:i], string(os.PathSeparator))) } // if the current directory is marked as "opaque", remove all // of its contents if base == ".wh..opq" { dirEntry.Children = make(map[string]*FSEntry) continue } // if the item is a whiteout, strip the "this is a whiteout // entry" prefix and remove the item it names if after, ok := strings.CutPrefix(base, ".wh."); ok { delete(dirEntry.Children, after) continue } // if the item already exists, make sure we don't get confused // by replacing a directory with a non-directory or vice-versa if child, ok := dirEntry.Children[base]; ok { if child.Children != nil { // it's a directory if entry.Typeflag == tar.TypeDir { // new entry is a directory, too. no // sweat, just update the metadata child.FSHeader = entry continue } // nope, a directory no longer } else { // it's not a directory if entry.Typeflag != tar.TypeDir { // new entry is not a directory, too. // no sweat, just update the metadata dirEntry.Children[base].FSHeader = entry continue } // well, it's a directory now } } // the item doesn't already exist, or it needs to be replaced, so we need to add it var children map[string]*FSEntry if entry.Typeflag == tar.TypeDir { // only directory entries can hold items children = make(map[string]*FSEntry) } dirEntry.Children[base] = &FSEntry{FSHeader: entry, Children: children} } } // read information about the specified image from the specified directory func readReport(t *testing.T, directory string) (manifestType string, original, oci, fs map[string]any) { // read the manifest in the as-committed format, whatever that is originalManifest, err := os.ReadFile(filepath.Join(directory, "manifest.json")) require.NoErrorf(t, err, "error reading manifest %q", filepath.Join(directory, "manifest.json")) // dump it into a map manifest := make(map[string]any) err = json.Unmarshal(originalManifest, &manifest) require.NoErrorf(t, err, "error decoding manifest %q", filepath.Join(directory, "manifest.json")) if str, ok := manifest["mediaType"].(string); ok { manifestType = str } // read the config in the as-committed (docker) format originalConfig, err := os.ReadFile(filepath.Join(directory, "config.json")) require.NoErrorf(t, err, "error reading configuration file %q", filepath.Join(directory, "config.json")) // dump it into a map original = make(map[string]any) err = json.Unmarshal(originalConfig, &original) require.NoErrorf(t, err, "error decoding configuration from file %q", filepath.Join(directory, "config.json")) // read the config in converted-to-OCI format ociConfig, err := os.ReadFile(filepath.Join(directory, "oci-config.json")) require.NoErrorf(t, err, "error reading OCI configuration file %q", filepath.Join(directory, "oci-config.json")) // dump it into a map oci = make(map[string]any) err = json.Unmarshal(ociConfig, &oci) require.NoErrorf(t, err, "error decoding OCI configuration from file %q", filepath.Join(directory, "oci.json")) // read the filesystem fsInfo, err := os.ReadFile(filepath.Join(directory, "fs.json")) require.NoErrorf(t, err, "error reading filesystem summary file %q", filepath.Join(directory, "fs.json")) // dump it into a map for comparison fs = make(map[string]any) err = json.Unmarshal(fsInfo, &fs) require.NoErrorf(t, err, "error decoding filesystem summary from file %q", filepath.Join(directory, "fs.json")) // return both return manifestType, original, oci, fs } // contains is used to check if item exists in []string or not, ignoring case func contains(slice []string, item string) bool { for _, s := range slice { if strings.EqualFold(s, item) { return true } } return false } // addPrefix prepends the given prefix to each string in []string. // The prefix and the string are joined with ":" func addPrefix(a []string, prefix string) []string { b := make([]string, 0, len(a)) for _, s := range a { b = append(b, prefix+":"+s) } return b } // diffDebug returns a row for a tabwriter that summarizes a field name and the // values for that field in two documents func diffDebug(k string, a, b any) string { if k == "mode" { // force modes to be displayed in octal instead of decimal a, aok := a.(float64) b, bok := b.(float64) if aok && bok { return fmt.Sprintf("%v\t0%o\t0%o\n", k, int64(a), int64(b)) } } return fmt.Sprintf("%v\t%v\t%v\n", k, a, b) } // compareJSON compares two parsed JSON structures. missKeys and leftKeys are // lists of field names present only in the first map or the second, // respectively, while diffKeys is a list of items which are present in both // maps, but which have different values, formatted with diffDebug. func compareJSON(a, b map[string]any, skip []string) (missKeys, leftKeys, diffKeys []string, isSame bool) { isSame = true for k, v := range a { vb, ok := b[k] if ok { // remove this item from b. when we're done, all that's // left in b will be the items that weren't also in a. delete(b, k) } if contains(skip, k) { continue } if !ok { // key is in a, but not in b missKeys = append(missKeys, k) isSame = false continue } if reflect.TypeOf(v) != reflect.TypeOf(vb) { if reflect.TypeOf(v) == nil && reflect.ValueOf(vb).Len() == 0 { continue } if reflect.TypeOf(vb) == nil && reflect.ValueOf(v).Len() == 0 { continue } diffKeys = append(diffKeys, diffDebug(k, v, vb)) isSame = false continue } switch v.(type) { case map[string]any: // this field in the object is itself an object (e.g. // "config" or "container_config"), so recursively // compare them var nextSkip []string prefix := k + ":" for _, s := range skip { if after, ok0 := strings.CutPrefix(s, prefix); ok0 { nextSkip = append(nextSkip, after) } } submiss, subleft, subdiff, ok := compareJSON(v.(map[string]any), vb.(map[string]any), nextSkip) missKeys = append(missKeys, addPrefix(submiss, k)...) leftKeys = append(leftKeys, addPrefix(subleft, k)...) diffKeys = append(diffKeys, addPrefix(subdiff, k)...) if !ok { isSame = false } case []any: // this field in the object is an array; make sure both // arrays have the same set of elements, which is more // or less correct for labels and environment // variables. // this will break if it tries to compare an array of // objects like "history", since maps, slices, and // functions can't be used as keys in maps tmpa := v.([]any) tmpb := vb.([]any) if len(tmpa) != len(tmpb) { diffKeys = append(diffKeys, diffDebug(k, v, vb)) isSame = false break } m := make(map[any]struct{}) for i := range len(tmpb) { m[tmpb[i]] = struct{}{} } for i := range len(tmpa) { if _, ok := m[tmpa[i]]; !ok { diffKeys = append(diffKeys, diffDebug(k, v, vb)) isSame = false break } } default: // this field in the object is neither an object nor an // array, so assume it's a scalar item if !reflect.DeepEqual(v, vb) { diffKeys = append(diffKeys, diffDebug(k, v, vb)) isSame = false } } } if len(b) > 0 { for k := range b { if !contains(skip, k) { leftKeys = append(leftKeys, k) } } } return slices.Clone(missKeys), slices.Clone(leftKeys), slices.Clone(diffKeys), isSame } // configCompareResult summarizes the output of compareJSON for display func configCompareResult(miss, left, diff []string, notDocker string) string { var buffer bytes.Buffer if len(miss) > 0 { fmt.Fprintf(&buffer, "Fields missing from %s version: %s\n", notDocker, strings.Join(miss, " ")) } if len(left) > 0 { fmt.Fprintf(&buffer, "Fields which only exist in %s version: %s\n", notDocker, strings.Join(left, " ")) } if len(diff) > 0 { buffer.WriteString("Fields present in both versions have different values:\n") tw := tabwriter.NewWriter(&buffer, 1, 1, 8, ' ', 0) if _, err := fmt.Fprintf(tw, "Field\tDocker\t%s\n", notDocker); err != nil { panic(err) } for _, d := range diff { if _, err := tw.Write([]byte(d)); err != nil { panic(err) } } tw.Flush() } return buffer.String() } // fsCompareResult summarizes the output of compareJSON for display func fsCompareResult(miss, left, diff []string, notDocker string) string { var buffer bytes.Buffer fixup := func(names []string) []string { n := make([]string, 0, len(names)) for _, name := range names { n = append(n, strings.ReplaceAll(strings.ReplaceAll(name, ":(dir):", "/"), "(dir):", "/")) } return n } if len(miss) > 0 { fmt.Fprintf(&buffer, "Content missing from %s version: %s\n", notDocker, strings.Join(fixup(miss), " ")) } if len(left) > 0 { fmt.Fprintf(&buffer, "Content which only exists in %s version: %s\n", notDocker, strings.Join(fixup(left), " ")) } if len(diff) > 0 { buffer.WriteString("File attributes in both versions have different values:\n") tw := tabwriter.NewWriter(&buffer, 1, 1, 8, ' ', 0) if _, err := fmt.Fprintf(tw, "File:attr\tDocker\t%s\n", notDocker); err != nil { panic(err) } for _, d := range fixup(diff) { if _, err := tw.Write([]byte(d)); err != nil { panic(err) } } tw.Flush() } return buffer.String() } type ( testCaseTweakContextDirFn func(*testing.T, string, string, string) error testCase struct { name string // name of the test dockerfileContents string // inlined Dockerfile content to use instead of possible file in the build context dockerfile string // name of the Dockerfile, relative to contextDir, if not Dockerfile contextDir string // name of context subdirectory, if there is one to be copied tweakContextDir testCaseTweakContextDirFn // callback to make updates to the temporary build context before we build it shouldFailAt int // line where a build is expected to fail (starts with 1, 0 if it should succeed buildahRegex string // if set, expect this to be present in output dockerRegex string // if set, expect this to be present in output imagebuilderRegex string // if set, expect this to be present in output buildahErrRegex string // if set, expect this to be present in output dockerErrRegex string // if set, expect this to be present in output imagebuilderErrRegex string // if set, expect this to be present in output failureRegex string // if set, expect this to be present in output when the build fails withoutImagebuilder bool // don't build this with imagebuilder, because it depends on a buildah-specific feature withoutDocker bool // don't build this with docker, because it depends on a buildah-specific feature dockerUseBuildKit bool // if building with docker, request that dockerd use buildkit dockerBuilderVersion docker.BuilderVersion // if building with docker, request the specific builder testUsingSetParent bool // test both with old (gets set) and new (left blank) config.Parent behavior compatSetParent types.OptionalBool // placeholder for a value to set for the buildah CompatSetParent flag testUsingVolumes bool // test both with old (preserved) and new (just a config note) volume behavior compatVolumes types.OptionalBool // placeholder for a value to set for the buildah CompatVolumes flag compatScratchConfig types.OptionalBool // placeholder for a value to set for the buildah CompatScratchConfig flag compatLayerOmissions types.OptionalBool // value to set for the buildah CompatLayerOmissions flag transientMounts []string // one possible buildah-specific feature fsSkip []string // expected filesystem differences, typically timestamps on files or directories we create or modify during the build and don't reset fsSkipCompatVolumesTrue []string // more expected filesystem differences when compatVolumes=true buildArgs map[string]string // build args to supply, as if --build-arg was used } ) var internalTestCases = []testCase{ { name: "shell test", dockerfile: "Dockerfile.shell", buildahRegex: "(?s)[0-9a-z]+(.*)--", dockerRegex: "(?s)RUN env.*?Running in [0-9a-z]+(.*?)---", }, { name: "copy-escape-glob", contextDir: "copy-escape-glob", fsSkip: []string{"(dir):app:mtime", "(dir):app2:mtime", "(dir):app3:mtime", "(dir):app4:mtime", "(dir):app5:mtime"}, tweakContextDir: func(t *testing.T, contextDir, _, _ string) error { appDir := filepath.Join(contextDir, "app") jklDir := filepath.Join(appDir, "jkl?") require.NoError(t, os.Mkdir(jklDir, 0o700)) jklFile := filepath.Join(jklDir, "file.txt") require.NoError(t, os.WriteFile(jklFile, []byte("another"), 0o600)) nopeDir := filepath.Join(appDir, "n?pe") require.NoError(t, os.Mkdir(nopeDir, 0o700)) nopeFile := filepath.Join(nopeDir, "file.txt") require.NoError(t, os.WriteFile(nopeFile, []byte("and also"), 0o600)) stuvDir := filepath.Join(appDir, "st*uv") require.NoError(t, os.Mkdir(stuvDir, 0o700)) stuvFile := filepath.Join(stuvDir, "file.txt") require.NoError(t, os.WriteFile(stuvFile, []byte("and yet"), 0o600)) return nil }, dockerUseBuildKit: true, }, { name: "copy file to root", dockerfile: "Dockerfile.copyfrom_1", buildahRegex: "[-rw]+.*?/a", fsSkip: []string{"(dir):a:mtime"}, }, { name: "copy file to same file", dockerfile: "Dockerfile.copyfrom_2", buildahRegex: "[-rw]+.*?/a", fsSkip: []string{"(dir):a:mtime"}, }, { name: "copy file to workdir", dockerfile: "Dockerfile.copyfrom_3", buildahRegex: "[-rw]+.*?/b/a", fsSkip: []string{"(dir):b:mtime", "(dir):b:(dir):a:mtime"}, }, { name: "copy file to workdir rename", dockerfile: "Dockerfile.copyfrom_3_1", buildahRegex: "[-rw]+.*?/b/b", fsSkip: []string{"(dir):b:mtime", "(dir):b:(dir):a:mtime"}, }, { name: "copy folder contents to higher level", dockerfile: "Dockerfile.copyfrom_4", buildahRegex: "(?s)[-rw]+.*?/b/1.*?[-rw]+.*?/b/2.*?/b.*?[-rw]+.*?1.*?[-rw]+.*?2", buildahErrRegex: "/a: No such file or directory", fsSkip: []string{"(dir):b:mtime"}, }, { name: "copy wildcard folder contents to higher level", dockerfile: "Dockerfile.copyfrom_5", buildahRegex: "(?s)[-rw]+.*?/b/1.*?[-rw]+.*?/b/2.*?/b.*?[-rw]+.*?1.*?[-rw]+.*?2", buildahErrRegex: "(?s)/a: No such file or directory.*?/b/a: No such file or directory.*?/b/b: No such file or director", fsSkip: []string{"(dir):b:mtime", "(dir):b:(dir):1:mtime", "(dir):b:(dir):2:mtime"}, }, { name: "copy folder with dot contents to higher level", dockerfile: "Dockerfile.copyfrom_6", buildahRegex: "(?s)[-rw]+.*?/b/1.*?[-rw]+.*?/b/2.*?/b.*?[-rw]+.*?1.*?[-rw]+.*?2", buildahErrRegex: "(?s)/a: No such file or directory.*?/b/a: No such file or directory.*?/b/b: No such file or director", fsSkip: []string{"(dir):b:mtime", "(dir):b:(dir):1:mtime", "(dir):b:(dir):2:mtime"}, }, { name: "copy root file to different root name", dockerfile: "Dockerfile.copyfrom_7", buildahRegex: "[-rw]+.*?/a", buildahErrRegex: "/b: No such file or directory", fsSkip: []string{"(dir):a:mtime"}, }, { name: "copy nested file to different root name", dockerfile: "Dockerfile.copyfrom_8", buildahRegex: "[-rw]+.*?/a", buildahErrRegex: "/b: No such file or directory", fsSkip: []string{"(dir):a:mtime"}, }, { name: "copy file to deeper directory with explicit slash", dockerfile: "Dockerfile.copyfrom_9", buildahRegex: "[-rw]+.*?/a/b/c/1", buildahErrRegex: "/a/b/1: No such file or directory", fsSkip: []string{"(dir):a:mtime", "(dir):a:(dir):b:mtime", "(dir):a:(dir):b:(dir):c:mtime", "(dir):a:(dir):b:(dir):c:(dir):1:mtime"}, }, { name: "copy file to deeper directory without explicit slash", dockerfile: "Dockerfile.copyfrom_10", buildahRegex: "[-rw]+.*?/a/b/c", buildahErrRegex: "/a/b/1: No such file or directory", fsSkip: []string{"(dir):a:mtime", "(dir):a:(dir):b:mtime", "(dir):a:(dir):b:(dir):c:mtime"}, }, { name: "copy directory to deeper directory without explicit slash", dockerfile: "Dockerfile.copyfrom_11", buildahRegex: "[-rw]+.*?/a/b/c/1", buildahErrRegex: "/a/b/1: No such file or directory", fsSkip: []string{ "(dir):a:mtime", "(dir):a:(dir):b:mtime", "(dir):a:(dir):b:(dir):c:mtime", "(dir):a:(dir):b:(dir):c:(dir):1:mtime", }, }, { name: "copy directory to root without explicit slash", dockerfile: "Dockerfile.copyfrom_12", buildahRegex: "[-rw]+.*?/a/1", buildahErrRegex: "/a/a: No such file or directory", fsSkip: []string{"(dir):a:mtime", "(dir):a:(dir):1:mtime"}, }, { name: "copy directory trailing to root without explicit slash", dockerfile: "Dockerfile.copyfrom_13", buildahRegex: "[-rw]+.*?/a/1", buildahErrRegex: "/a/a: No such file or directory", fsSkip: []string{"(dir):a:mtime", "(dir):a:(dir):1:mtime"}, }, { name: "multi stage base", dockerfile: "Dockerfile.reusebase", buildahRegex: "[0-9a-z]+ /1", fsSkip: []string{"(dir):1:mtime"}, }, { name: "directory", contextDir: "dir", fsSkip: []string{"(dir):dir:mtime", "(dir):test:mtime"}, }, { name: "copy to dir", contextDir: "copy", fsSkip: []string{"(dir):usr:(dir):bin:mtime"}, }, { name: "copy dir", contextDir: "copydir", fsSkip: []string{"(dir):dir"}, }, { name: "copy from symlink source", contextDir: "copysymlink", }, { name: "copy-symlink-2", contextDir: "copysymlink", dockerfile: "Dockerfile2", }, { name: "copy from subdir to new directory", contextDir: "copydir", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY dir/file /subdir/", }, "\n"), fsSkip: []string{"(dir):subdir"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "copy to renamed file", contextDir: "copyrename", fsSkip: []string{"(dir):usr:(dir):bin:mtime"}, }, { name: "copy with --chown", contextDir: "copychown", fsSkip: []string{"(dir):usr:(dir):bin:mtime", "(dir):usr:(dir):local:(dir):bin:mtime"}, }, { name: "directory with slash", contextDir: "overlapdirwithslash", }, { name: "directory without slash", contextDir: "overlapdirwithoutslash", }, { name: "environment", dockerfile: "Dockerfile.env", shouldFailAt: 12, }, { name: "edgecases", dockerfile: "Dockerfile.edgecases", fsSkip: []string{ "(dir):test:mtime", "(dir):test:(dir):copy:mtime", "(dir):test2:mtime", "(dir):test3:mtime", "(dir):test3:(dir):copy:mtime", "(dir):test3:(dir):test:mtime", "(dir):tmp:mtime", "(dir):tmp:(dir):passwd:mtime", }, }, { name: "exposed default", dockerfile: "Dockerfile.exposedefault", testUsingSetParent: true, }, { name: "add", dockerfile: "Dockerfile.add", fsSkip: []string{"(dir):b:mtime", "(dir):tmp:mtime"}, }, { name: "run with JSON", dockerfile: "Dockerfile.run.args", buildahRegex: "(first|third|fifth|inner) (second|fourth|sixth|outer)", dockerRegex: "Running in [0-9a-z]+.*?(first|third|fifth|inner) (second|fourth|sixth|outer)", }, { name: "wildcard", contextDir: "wildcard", fsSkip: []string{"(dir):usr:mtime", "(dir):usr:(dir):test:mtime"}, }, { name: "volume", contextDir: "volume", fsSkip: []string{"(dir):var:mtime", "(dir):var:(dir):www:mtime"}, testUsingVolumes: true, }, { name: "volumerun", contextDir: "volumerun", fsSkip: []string{"(dir):var:mtime", "(dir):var:(dir):www:mtime"}, testUsingVolumes: true, }, { name: "mount", contextDir: "mount", buildahRegex: "/tmp/test/file.*?regular file.*?/tmp/test/file2.*?regular file", withoutDocker: true, transientMounts: []string{"@@TEMPDIR@@:/tmp/test" + selinuxMountFlag()}, }, { name: "transient-mount", contextDir: "transientmount", buildahRegex: "file2.*?FROM mirror.gcr.io/busybox ENV name value", withoutDocker: true, transientMounts: []string{ "@@TEMPDIR@@:/mountdir" + selinuxMountFlag(), "@@TEMPDIR@@/Dockerfile.env:/mountfile" + selinuxMountFlag(), }, }, { // from internal team chat name: "ci-pipeline-modified", dockerfileContents: strings.Join([]string{ "FROM mirror.gcr.io/busybox", "WORKDIR /go/src/github.com/openshift/ocp-release-operator-sdk/", "ENV GOPATH=/go", "RUN env | grep -E -v '^(HOSTNAME|OLDPWD)=' | LANG=C sort | tee /env-contents.txt\n", }, "\n"), fsSkip: []string{ "(dir):go:mtime", "(dir):go:(dir):src:mtime", "(dir):go:(dir):src:(dir):github.com:mtime", "(dir):go:(dir):src:(dir):github.com:(dir):openshift:mtime", "(dir):go:(dir):src:(dir):github.com:(dir):openshift:(dir):ocp-release-operator-sdk:mtime", "(dir):env-contents.txt:mtime", }, }, { name: "add-permissions", withoutDocker: true, dockerfileContents: strings.Join([]string{ "FROM scratch", "# Does ADD preserve permissions differently for archives and files?", "ADD archive.tar subdir1/", "ADD archive/ subdir2/", }, "\n"), tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { content := []byte("test content") if err := os.Mkdir(filepath.Join(contextDir, "archive"), 0o755); err != nil { return fmt.Errorf("creating subdirectory of temporary context directory: %w", err) } filename := filepath.Join(contextDir, "archive", "should-be-owned-by-root") if err = os.WriteFile(filename, content, 0o640); err != nil { return fmt.Errorf("creating file owned by root in temporary context directory: %w", err) } if err = os.Chown(filename, 0, 0); err != nil { return fmt.Errorf("setting ownership on file owned by root in temporary context directory: %w", err) } if err = os.Chtimes(filename, testDate, testDate); err != nil { return fmt.Errorf("setting date on file owned by root file in temporary context directory: %w", err) } filename = filepath.Join(contextDir, "archive", "should-be-owned-by-99") if err = os.WriteFile(filename, content, 0o640); err != nil { return fmt.Errorf("creating file owned by 99 in temporary context directory: %w", err) } if err = os.Chown(filename, 99, 99); err != nil { return fmt.Errorf("setting ownership on file owned by 99 in temporary context directory: %w", err) } if err = os.Chtimes(filename, testDate, testDate); err != nil { return fmt.Errorf("setting date on file owned by 99 in temporary context directory: %w", err) } filename = filepath.Join(contextDir, "archive.tar") f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { return fmt.Errorf("creating archive file: %w", err) } defer f.Close() tw := tar.NewWriter(f) defer tw.Close() err = tw.WriteHeader(&tar.Header{ Name: "archive/should-be-owned-by-root", Typeflag: tar.TypeReg, Size: int64(len(content)), ModTime: testDate, Mode: 0o640, Uid: 0, Gid: 0, }) if err != nil { return fmt.Errorf("writing archive file header: %w", err) } n, err := tw.Write(content) if err != nil { return fmt.Errorf("writing archive file contents: %w", err) } if n != len(content) { return errors.New("short write writing archive file contents") } err = tw.WriteHeader(&tar.Header{ Name: "archive/should-be-owned-by-99", Typeflag: tar.TypeReg, Size: int64(len(content)), ModTime: testDate, Mode: 0o640, Uid: 99, Gid: 99, }) if err != nil { return fmt.Errorf("writing archive file header: %w", err) } n, err = tw.Write(content) if err != nil { return fmt.Errorf("writing archive file contents: %w", err) } if n != len(content) { return errors.New("short write writing archive file contents") } return nil }, fsSkip: []string{"(dir):subdir1:mtime", "(dir):subdir2:mtime"}, }, { name: "copy-permissions", dockerfileContents: strings.Join([]string{ "FROM scratch", "# Does COPY --chown change permissions on already-present directories?", "COPY subdir/ subdir/", "COPY --chown=99:99 subdir/ subdir/", }, "\n"), tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { content := []byte("test content") if err := os.Mkdir(filepath.Join(contextDir, "subdir"), 0o755); err != nil { return fmt.Errorf("creating subdirectory of temporary context directory: %w", err) } filename := filepath.Join(contextDir, "subdir", "would-be-owned-by-root") if err = os.WriteFile(filename, content, 0o640); err != nil { return fmt.Errorf("creating file owned by root in temporary context directory: %w", err) } if err = os.Chown(filename, 0, 0); err != nil { return fmt.Errorf("setting ownership on file owned by root in temporary context directory: %w", err) } if err = os.Chtimes(filename, testDate, testDate); err != nil { return fmt.Errorf("setting date on file owned by root file in temporary context directory: %w", err) } filename = filepath.Join(contextDir, "subdir", "would-be-owned-by-99") if err = os.WriteFile(filename, content, 0o640); err != nil { return fmt.Errorf("creating file owned by 99 in temporary context directory: %w", err) } if err = os.Chown(filename, 99, 99); err != nil { return fmt.Errorf("setting ownership on file owned by 99 in temporary context directory: %w", err) } if err = os.Chtimes(filename, testDate, testDate); err != nil { return fmt.Errorf("setting date on file owned by 99 in temporary context directory: %w", err) } return nil }, fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "copy-permissions-implicit", dockerfileContents: strings.Join([]string{ "FROM scratch", "# Does COPY --chown change permissions on already-present directories?", "COPY --chown=99:99 subdir/ subdir/", "COPY subdir/ subdir/", }, "\n"), tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { content := []byte("test content") if err := os.Mkdir(filepath.Join(contextDir, "subdir"), 0o755); err != nil { return fmt.Errorf("creating subdirectory of temporary context directory: %w", err) } filename := filepath.Join(contextDir, "subdir", "would-be-owned-by-root") if err = os.WriteFile(filename, content, 0o640); err != nil { return fmt.Errorf("creating file owned by root in temporary context directory: %w", err) } if err = os.Chown(filename, 0, 0); err != nil { return fmt.Errorf("setting ownership on file owned by root in temporary context directory: %w", err) } if err = os.Chtimes(filename, testDate, testDate); err != nil { return fmt.Errorf("setting date on file owned by root file in temporary context directory: %w", err) } filename = filepath.Join(contextDir, "subdir", "would-be-owned-by-99") if err = os.WriteFile(filename, content, 0o640); err != nil { return fmt.Errorf("creating file owned by 99 in temporary context directory: %w", err) } if err = os.Chown(filename, 99, 99); err != nil { return fmt.Errorf("setting ownership on file owned by 99 in temporary context directory: %w", err) } if err = os.Chtimes(filename, testDate, testDate); err != nil { return fmt.Errorf("setting date on file owned by 99 in temporary context directory: %w", err) } return nil }, fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { // the digest just ensures that we can handle a digest // reference to a manifest list; the digest of any manifest // list in the image repository would do name: "stage-container-as-source-plus-hardlinks", dockerfileContents: strings.Join([]string{ "FROM mirror.gcr.io/busybox@sha256:9ae97d36d26566ff84e8893c64a6dc4fe8ca6d1144bf5b87b2b85a32def253c7 AS build", "RUN mkdir -p /target/subdir", "RUN cp -p /etc/passwd /target/", "RUN ln /target/passwd /target/subdir/passwd", "RUN ln /target/subdir/passwd /target/subdir/passwd2", "FROM scratch", "COPY --from=build /target/subdir /subdir", }, "\n"), fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerfile-in-subdirectory", dockerfile: "subdir/Dockerfile", contextDir: "subdir", compatScratchConfig: types.OptionalBoolTrue, }, { name: "setuid-file-in-context", dockerfileContents: strings.Join([]string{ "FROM scratch", fmt.Sprintf("# Does the setuid file (0%o) in the context dir end up setuid in the image?", syscall.S_ISUID), "COPY . subdir1", "ADD . subdir2", }, "\n"), tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { filename := filepath.Join(contextDir, "should-be-setuid-file") if err = os.WriteFile(filename, []byte("test content"), 0o644); err != nil { return fmt.Errorf("creating setuid test file in temporary context directory: %w", err) } if err = syscall.Chmod(filename, syscall.S_ISUID|0o755); err != nil { return fmt.Errorf("setting setuid bit on test file in temporary context directory: %w", err) } if err = os.Chtimes(filename, testDate, testDate); err != nil { return fmt.Errorf("setting date on setuid test file in temporary context directory: %w", err) } filename = filepath.Join(contextDir, "should-be-setgid-file") if err = os.WriteFile(filename, []byte("test content"), 0o644); err != nil { return fmt.Errorf("creating setgid test file in temporary context directory: %w", err) } if err = syscall.Chmod(filename, syscall.S_ISGID|0o755); err != nil { return fmt.Errorf("setting setgid bit on test file in temporary context directory: %w", err) } if err = os.Chtimes(filename, testDate, testDate); err != nil { return fmt.Errorf("setting date on setgid test file in temporary context directory: %w", err) } filename = filepath.Join(contextDir, "should-be-sticky-file") if err = os.WriteFile(filename, []byte("test content"), 0o644); err != nil { return fmt.Errorf("creating sticky test file in temporary context directory: %w", err) } if err = syscall.Chmod(filename, syscall.S_ISVTX|0o755); err != nil { return fmt.Errorf("setting permissions on sticky test file in temporary context directory: %w", err) } if err = os.Chtimes(filename, testDate, testDate); err != nil { return fmt.Errorf("setting date on sticky test file in temporary context directory: %w", err) } filename = filepath.Join(contextDir, "should-not-be-setuid-setgid-sticky-file") if err = os.WriteFile(filename, []byte("test content"), 0o644); err != nil { return fmt.Errorf("creating non-suid non-sgid non-sticky test file in temporary context directory: %w", err) } if err = syscall.Chmod(filename, 0o640); err != nil { return fmt.Errorf("setting permissions on plain test file in temporary context directory: %w", err) } if err = os.Chtimes(filename, testDate, testDate); err != nil { return fmt.Errorf("setting date on plain test file in temporary context directory: %w", err) } return nil }, fsSkip: []string{"(dir):subdir1:mtime", "(dir):subdir2:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "xattrs-file-in-context", dockerfileContents: strings.Join([]string{ "FROM scratch", "# Do the xattrs on a file in the context dir end up in the image?", "COPY . subdir1", "ADD . subdir2", }, "\n"), tweakContextDir: func(t *testing.T, contextDir, storageDriver, storageRoot string) (err error) { if !*contextCanDoXattrs { t.Skipf("test context directory %q doesn't support xattrs", contextDir) } if !*storageCanDoXattrs { t.Skipf("test storage driver %q and directory %q don't support xattrs together", storageDriver, storageRoot) } filename := filepath.Join(contextDir, "xattrs-file") if err = os.WriteFile(filename, []byte("test content"), 0o644); err != nil { return fmt.Errorf("creating test file with xattrs in temporary context directory: %w", err) } if err = copier.Lsetxattrs(filename, map[string]string{"user.a": "test"}); err != nil { return fmt.Errorf("setting xattrs on test file in temporary context directory: %w", err) } if err = syscall.Chmod(filename, 0o640); err != nil { return fmt.Errorf("setting permissions on test file in temporary context directory: %w", err) } if err = os.Chtimes(filename, testDate, testDate); err != nil { return fmt.Errorf("setting date on test file in temporary context directory: %w", err) } return nil }, fsSkip: []string{"(dir):subdir1:mtime", "(dir):subdir2:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "from-scratch-simple-file", // Fix On ARM64: fields missing from buildah version: variant dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY file-a.txt /", }, "\n"), contextDir: "dockerignore/populated", compatScratchConfig: types.OptionalBoolTrue, }, { name: "setuid-file-in-archive", dockerfileContents: strings.Join([]string{ "FROM scratch", fmt.Sprintf("# Do the setuid/setgid/sticky files in this archive end up setuid(0%o)/setgid(0%o)/sticky(0%o)?", syscall.S_ISUID, syscall.S_ISGID, syscall.S_ISVTX), "ADD archive.tar .", }, "\n"), tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { filename := filepath.Join(contextDir, "archive.tar") f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { return fmt.Errorf("creating new archive file in temporary context directory: %w", err) } defer f.Close() tw := tar.NewWriter(f) defer tw.Close() hdr := tar.Header{ Name: "setuid-file", Uid: 0, Gid: 0, Typeflag: tar.TypeReg, Size: 8, Mode: cISUID | 0o755, ModTime: testDate, } if err = tw.WriteHeader(&hdr); err != nil { return fmt.Errorf("writing tar archive header: %w", err) } if _, err = io.Copy(tw, bytes.NewReader([]byte("whatever"))); err != nil { return fmt.Errorf("writing tar archive content: %w", err) } hdr = tar.Header{ Name: "setgid-file", Uid: 0, Gid: 0, Typeflag: tar.TypeReg, Size: 8, Mode: cISGID | 0o755, ModTime: testDate, } if err = tw.WriteHeader(&hdr); err != nil { return fmt.Errorf("writing tar archive header: %w", err) } if _, err = io.Copy(tw, bytes.NewReader([]byte("whatever"))); err != nil { return fmt.Errorf("writing tar archive content: %w", err) } hdr = tar.Header{ Name: "sticky-file", Uid: 0, Gid: 0, Typeflag: tar.TypeReg, Size: 8, Mode: cISVTX | 0o755, ModTime: testDate, } if err = tw.WriteHeader(&hdr); err != nil { return fmt.Errorf("writing tar archive header: %w", err) } if _, err = io.Copy(tw, bytes.NewReader([]byte("whatever"))); err != nil { return fmt.Errorf("writing tar archive content: %w", err) } hdr = tar.Header{ Name: "setuid-dir", Uid: 0, Gid: 0, Typeflag: tar.TypeDir, Size: 0, Mode: cISUID | 0o755, ModTime: testDate, } if err = tw.WriteHeader(&hdr); err != nil { return fmt.Errorf("error writing tar archive header: %w", err) } hdr = tar.Header{ Name: "setgid-dir", Uid: 0, Gid: 0, Typeflag: tar.TypeDir, Size: 0, Mode: cISGID | 0o755, ModTime: testDate, } if err = tw.WriteHeader(&hdr); err != nil { return fmt.Errorf("error writing tar archive header: %w", err) } hdr = tar.Header{ Name: "sticky-dir", Uid: 0, Gid: 0, Typeflag: tar.TypeDir, Size: 0, Mode: cISVTX | 0o755, ModTime: testDate, } if err = tw.WriteHeader(&hdr); err != nil { return fmt.Errorf("error writing tar archive header: %w", err) } return nil }, compatScratchConfig: types.OptionalBoolTrue, }, { name: "xattrs-file-in-archive", dockerfileContents: strings.Join([]string{ "FROM scratch", "# Do the xattrs on a file in an archive end up in the image?", "ADD archive.tar .", }, "\n"), tweakContextDir: func(t *testing.T, contextDir, storageDriver, storageRoot string) (err error) { if !*contextCanDoXattrs { t.Skipf("test context directory %q doesn't support xattrs", contextDir) } if !*storageCanDoXattrs { t.Skipf("test storage driver %q and directory %q don't support xattrs together", storageDriver, storageRoot) } filename := filepath.Join(contextDir, "archive.tar") f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { return fmt.Errorf("creating new archive file in temporary context directory: %w", err) } defer f.Close() tw := tar.NewWriter(f) defer tw.Close() hdr := tar.Header{ Name: "xattr-file", Uid: 0, Gid: 0, Typeflag: tar.TypeReg, Size: 8, Mode: 0o640, ModTime: testDate, PAXRecords: map[string]string{ xattrPAXRecordNamespace + "user.a": "test", }, } if err = tw.WriteHeader(&hdr); err != nil { return fmt.Errorf("writing tar archive header: %w", err) } if _, err = io.Copy(tw, bytes.NewReader([]byte("whatever"))); err != nil { return fmt.Errorf("writing tar archive content: %w", err) } return nil }, fsSkip: []string{"(dir):subdir1:mtime", "(dir):subdir2:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { // docker build apparently stopped preserving this bit somewhere between 18.09.7 and 19.03, // possibly around https://github.com/moby/moby/pull/38599 name: "setuid-file-in-other-stage", dockerfileContents: strings.Join([]string{ "FROM mirror.gcr.io/busybox", "RUN mkdir /a && echo whatever > /a/setuid && chmod u+xs /a/setuid && touch -t @1485449953 /a/setuid", "RUN mkdir /b && echo whatever > /b/setgid && chmod g+xs /b/setgid && touch -t @1485449953 /b/setgid", "RUN mkdir /c && echo whatever > /c/sticky && chmod o+x /c/sticky && chmod +t /c/sticky && touch -t @1485449953 /c/sticky", "FROM scratch", fmt.Sprintf("# Does this setuid/setgid/sticky file copied from another stage end up setuid/setgid/sticky (0%o/0%o/0%o)?", syscall.S_ISUID, syscall.S_ISGID, syscall.S_ISVTX), "COPY --from=0 /a/setuid /b/setgid /c/sticky /", }, "\n"), compatScratchConfig: types.OptionalBoolTrue, }, { name: "xattrs-file-in-other-stage", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY . .", "FROM scratch", "# Do the xattrs on a file in another stage end up in the image?", "COPY --from=0 /xattrs-file /", }, "\n"), tweakContextDir: func(t *testing.T, contextDir, storageDriver, storageRoot string) (err error) { if !*contextCanDoXattrs { t.Skipf("test context directory %q doesn't support xattrs", contextDir) } if !*storageCanDoXattrs { t.Skipf("test storage driver %q and directory %q don't support xattrs together", storageDriver, storageRoot) } filename := filepath.Join(contextDir, "xattrs-file") if err = os.WriteFile(filename, []byte("test content"), 0o644); err != nil { return fmt.Errorf("creating test file with xattrs in temporary context directory: %w", err) } if err = copier.Lsetxattrs(filename, map[string]string{"user.a": "test"}); err != nil { return fmt.Errorf("setting xattrs on test file in temporary context directory: %w", err) } if err = syscall.Chmod(filename, 0o640); err != nil { return fmt.Errorf("setting permissions on test file in temporary context directory: %w", err) } if err = os.Chtimes(filename, testDate, testDate); err != nil { return fmt.Errorf("setting date on test file in temporary context directory: %w", err) } return nil }, compatScratchConfig: types.OptionalBoolTrue, }, { name: "copy-multiple-some-missing", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY file-a.txt subdir-a file-z.txt subdir-z subdir/", }, "\n"), contextDir: "dockerignore/populated", shouldFailAt: 2, }, { name: "copy-multiple-missing-file-with-glob", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY file-z.txt subdir-* subdir/", }, "\n"), contextDir: "dockerignore/populated", shouldFailAt: 2, }, { name: "copy-multiple-missing-file-with-nomatch-on-glob", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY missing* subdir/", }, "\n"), contextDir: "dockerignore/populated", shouldFailAt: 2, }, { name: "copy-multiple-some-missing-glob", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY file-a.txt subdir-* file-?.txt missing* subdir/", }, "\n"), contextDir: "dockerignore/populated", fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "file-in-workdir-in-other-stage", dockerfileContents: strings.Join([]string{ "FROM scratch AS base", "COPY . /subdir/", "WORKDIR /subdir", "FROM base", "COPY --from=base . .", // --from=otherstage ignores that stage's WORKDIR }, "\n"), contextDir: "dockerignore/populated", fsSkip: []string{"(dir):subdir:mtime", "(dir):subdir:(dir):subdir:mtime"}, }, { name: "copy-integration1", contextDir: "dockerignore/integration1", shouldFailAt: 3, failureRegex: "(no such file or directory)|(file not found)|(file does not exist)", }, { name: "copy-integration2", contextDir: "dockerignore/integration2", compatScratchConfig: types.OptionalBoolTrue, }, { name: "copy-integration3", contextDir: "dockerignore/integration3", shouldFailAt: 4, failureRegex: "(no such file or directory)|(file not found)|(file does not exist)", }, { name: "copy-empty-1", contextDir: "copyempty", dockerfile: "Dockerfile", fsSkip: []string{"(dir):usr:(dir):local:mtime", "(dir):usr:(dir):local:(dir):tmp:mtime"}, }, { name: "copy-empty-2", contextDir: "copyempty", dockerfile: "Dockerfile2", fsSkip: []string{"(dir):usr:(dir):local:mtime", "(dir):usr:(dir):local:(dir):tmp:mtime"}, }, { name: "copy-absolute-directory-1", contextDir: "copyblahblub", dockerfile: "Dockerfile", fsSkip: []string{"(dir):var:mtime"}, }, { name: "copy-absolute-directory-2", contextDir: "copyblahblub", dockerfile: "Dockerfile2", fsSkip: []string{"(dir):var:mtime"}, }, { name: "copy-absolute-directory-3", contextDir: "copyblahblub", dockerfile: "Dockerfile3", fsSkip: []string{"(dir):var:mtime"}, }, { name: "multi-stage-through-base", dockerfileContents: strings.Join([]string{ "FROM mirror.gcr.io/alpine AS base", "RUN touch -t @1485449953 /1", "ENV LOCAL=/1", "RUN find $LOCAL", "FROM base", "RUN find $LOCAL", }, "\n"), fsSkip: []string{"(dir):root:mtime", "(dir):1:mtime"}, }, { name: "multi-stage-derived", // from #2415 dockerfileContents: strings.Join([]string{ "FROM mirror.gcr.io/busybox as layer", "RUN touch /root/layer", "FROM layer as derived", "RUN touch -t @1485449953 /root/derived ; rm /root/layer", "FROM mirror.gcr.io/busybox AS output", "COPY --from=layer /root /root", }, "\n"), fsSkip: []string{"(dir):root:mtime", "(dir):root:(dir):layer:mtime"}, }, { name: "dockerignore-minimal-test", // from #2237 contextDir: "dockerignore/minimal_test", withoutDocker: true, fsSkip: []string{"(dir):tmp:mtime", "(dir):tmp:(dir):stuff:mtime"}, }, { name: "dockerignore-is-even-there", contextDir: "dockerignore/empty", fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerignore-irrelevant", contextDir: "dockerignore/empty", tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { dockerignore := []byte(strings.Join([]string{"**/*-a", "!**/*-c"}, "\n")) if err := os.WriteFile(filepath.Join(contextDir, ".dockerignore"), dockerignore, 0o600); err != nil { return fmt.Errorf("writing .dockerignore file: %w", err) } if err = os.Chtimes(filepath.Join(contextDir, ".dockerignore"), testDate, testDate); err != nil { return fmt.Errorf("setting date on .dockerignore file in temporary context directory: %w", err) } return nil }, fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerignore-exceptions-dot", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY . subdir/", }, "\n"), contextDir: "dockerignore/populated", tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { dockerignore := []byte(strings.Join([]string{"**/*-a", "!**/*-c"}, "\n")) if err := os.WriteFile(filepath.Join(contextDir, ".dockerignore"), dockerignore, 0o644); err != nil { return fmt.Errorf("writing .dockerignore file: %w", err) } if err = os.Chtimes(filepath.Join(contextDir, ".dockerignore"), testDate, testDate); err != nil { return fmt.Errorf("setting date on .dockerignore file in temporary context directory: %w", err) } return nil }, fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerignore-extensions-star", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY * subdir/", }, "\n"), contextDir: "dockerignore/populated", tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { dockerignore := []byte(strings.Join([]string{"**/*-a", "!**/*-c"}, "\n")) if err := os.WriteFile(filepath.Join(contextDir, ".dockerignore"), dockerignore, 0o600); err != nil { return fmt.Errorf("writing .dockerignore file: %w", err) } if err = os.Chtimes(filepath.Join(contextDir, ".dockerignore"), testDate, testDate); err != nil { return fmt.Errorf("setting date on .dockerignore file in temporary context directory: %w", err) } return nil }, fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerignore-includes-dot", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY . subdir/", }, "\n"), contextDir: "dockerignore/populated", tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { dockerignore := []byte(strings.Join([]string{"!**/*-c"}, "\n")) if err := os.WriteFile(filepath.Join(contextDir, ".dockerignore"), dockerignore, 0o640); err != nil { return fmt.Errorf("writing .dockerignore file: %w", err) } if err = os.Chtimes(filepath.Join(contextDir, ".dockerignore"), testDate, testDate); err != nil { return fmt.Errorf("setting date on .dockerignore file in temporary context directory: %w", err) } return nil }, fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "add--exclude-includes-star", dockerfileContents: strings.Join([]string{ "# syntax=mirror.gcr.io/docker/dockerfile:1.9-labs", "FROM scratch", "ADD --exclude=**/*-c ./ subdir/", }, "\n"), contextDir: "dockerignore/populated", fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolFalse, dockerUseBuildKit: true, }, { name: "add--exclude-includes-slash", dockerfileContents: strings.Join([]string{ "# syntax=mirror.gcr.io/docker/dockerfile:1.9-labs", "FROM scratch", "ADD --exclude=*.txt / subdir/", }, "\n"), contextDir: "dockerignore/populated", fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolFalse, dockerUseBuildKit: true, }, { name: "add--exclude-includes-dot", dockerfileContents: strings.Join([]string{ "# syntax=mirror.gcr.io/docker/dockerfile:1.9-labs", "FROM scratch", "ADD --exclude=*-c . subdir/", }, "\n"), contextDir: "dockerignore/populated", fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolFalse, dockerUseBuildKit: true, }, { name: "copy--exclude-includes-subdir-slash", dockerfileContents: strings.Join([]string{ "# syntax=mirror.gcr.io/docker/dockerfile:1.9-labs", "FROM scratch", "COPY --exclude=**/*-c / subdir/", }, "\n"), contextDir: "dockerignore/populated", fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolFalse, dockerUseBuildKit: true, }, { name: "copy--exclude-includes-dot-slash", dockerfileContents: strings.Join([]string{ "# syntax=mirror.gcr.io/docker/dockerfile:1.9-labs", "FROM scratch", "COPY --exclude='!**/*-c' ./ subdir/", }, "\n"), contextDir: "dockerignore/populated", fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolFalse, dockerUseBuildKit: true, }, { name: "copy--exclude-includes-slash", dockerfileContents: strings.Join([]string{ "# syntax=mirror.gcr.io/docker/dockerfile:1.9-labs", "FROM scratch", "COPY --exclude='!**/*-c' . subdir/", }, "\n"), contextDir: "dockerignore/populated", fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolFalse, dockerUseBuildKit: true, }, { name: "copy--parents", dockerfileContents: strings.Join([]string{ "# syntax=mirror.gcr.io/docker/dockerfile:1.7-labs", "FROM scratch", "COPY ./x/a.txt ./y/a.txt /no_parents/", "COPY --parents ./x/a.txt ./y/a.txt /parents/", }, "\n"), contextDir: "copy-parents", fsSkip: []string{"(dir):parents:mtime", "(dir):no_parents:mtime", "(dir):parents:(dir):x:mtime", "(dir):parents:(dir):y:mtime"}, compatScratchConfig: types.OptionalBoolFalse, dockerUseBuildKit: true, }, { name: "copy--parents-directories", dockerfileContents: strings.Join([]string{ "# syntax=mirror.gcr.io/docker/dockerfile:1.7-labs", "FROM scratch", "COPY --parents ./x /parents/", }, "\n"), contextDir: "copy-parents", fsSkip: []string{"(dir):parents:mtime", "(dir):parents:(dir):x:mtime"}, compatScratchConfig: types.OptionalBoolFalse, dockerUseBuildKit: true, }, { name: "copy--parents-dir-with-links", dockerfileContents: strings.Join([]string{ "# syntax=mirror.gcr.io/docker/dockerfile:1.7-labs", "FROM mirror.gcr.io/alpine as base", "COPY . .", "# Symlink and hardlink part", "RUN ln -s /x/z/b.txt /x/z/symlink-b.txt", "RUN ln /x/z/a.txt /x/z/hardlink-a.txt", "RUN ln /x/y/b.txt /x/z/hardlink-y-b.txt", "FROM scratch", "COPY --from=base --parents ./x/ /parents", "COPY --from=base --parents ./x/./z/ /parents_dir_point/", }, "\n"), contextDir: "copy-parents", fsSkip: []string{ "(dir):parents:mtime", "(dir):parents:(dir):x:mtime", "(dir):parents:(dir):x:(dir):z:mtime", "(dir):parents:(dir):x:(dir):z:(dir):symlink-b.txt:mtime", "(dir):parents_dir_point:mtime", "(dir):parents_dir_point:(dir):z:mtime", "(dir):parents_dir_point:(dir):z:(dir):symlink-b.txt:mtime", }, compatScratchConfig: types.OptionalBoolFalse, dockerUseBuildKit: true, }, { name: "copy--parents-all-directories", dockerfileContents: strings.Join([]string{ "# syntax=mirror.gcr.io/docker/dockerfile:1.7-labs", "FROM scratch", "COPY --parents ./ /parents/", }, "\n"), contextDir: "copy-parents", fsSkip: []string{"(dir):parents:mtime", "(dir):parents:(dir):x:mtime"}, compatScratchConfig: types.OptionalBoolFalse, dockerUseBuildKit: true, }, { name: "copy--parents-directory-with-parent-directory", dockerfileContents: strings.Join([]string{ "# syntax=mirror.gcr.io/docker/dockerfile:1.7-labs", "FROM scratch", "COPY --parents ./x/y/ /parents/", }, "\n"), contextDir: "copy-parents", fsSkip: []string{"(dir):parents:mtime", "(dir):parents:(dir):x:mtime", "(dir):parents:(dir):x:(dir):y:mtime"}, compatScratchConfig: types.OptionalBoolFalse, dockerUseBuildKit: true, }, { name: "copy--parents-path-with-star", dockerfileContents: strings.Join([]string{ "# syntax=mirror.gcr.io/docker/dockerfile:1.7-labs", "FROM scratch", "COPY --parents ./x/*/a.txt ./*/b.txt /parents/", }, "\n"), contextDir: "copy-parents", fsSkip: []string{"(dir):parents:mtime", "(dir):parents:(dir):x:mtime", "(dir):parents:(dir):y:mtime", "(dir):parents:(dir):x:(dir):y:mtime", "(dir):parents:(dir):x:(dir):z:mtime"}, compatScratchConfig: types.OptionalBoolFalse, dockerUseBuildKit: true, }, { name: "copy--parents-two-parent-directories", dockerfileContents: strings.Join([]string{ "# syntax=mirror.gcr.io/docker/dockerfile:1.7-labs", "FROM scratch", "COPY --parents ./x/y/*.txt /parents/", }, "\n"), contextDir: "copy-parents", fsSkip: []string{"(dir):parents:mtime", "(dir):parents:(dir):x:mtime", "(dir):parents:(dir):x:(dir):y:mtime"}, compatScratchConfig: types.OptionalBoolFalse, dockerUseBuildKit: true, }, { name: "copy--parents-unknown-count-parent-directories", dockerfileContents: strings.Join([]string{ "# syntax=mirror.gcr.io/docker/dockerfile:1.7-labs", "FROM scratch", "COPY --parents ./x/* /parents/", }, "\n"), contextDir: "copy-parents", fsSkip: []string{"(dir):parents:mtime", "(dir):parents:(dir):x:mtime", "(dir):parents:(dir):x:(dir):y:mtime", "(dir):parents:(dir):x:(dir):z:mtime"}, compatScratchConfig: types.OptionalBoolFalse, dockerUseBuildKit: true, }, { name: "copy--parents-directory-limit-parents-unknown-count-parent-directories", dockerfileContents: strings.Join([]string{ "# syntax=mirror.gcr.io/docker/dockerfile:1.7-labs", "FROM scratch", "COPY --parents ./x/./ /parents/", }, "\n"), contextDir: "copy-parents", fsSkip: []string{"(dir):parents:mtime", "(dir):parents:(dir):y:mtime", "(dir):parents:(dir):z:mtime"}, compatScratchConfig: types.OptionalBoolFalse, dockerUseBuildKit: true, }, { name: "copy--parents-limit-parents-unknown-count-parent-directories", dockerfileContents: strings.Join([]string{ "# syntax=mirror.gcr.io/docker/dockerfile:1.7-labs", "FROM scratch", "COPY --parents ./x/./* /parents/", }, "\n"), contextDir: "copy-parents", fsSkip: []string{"(dir):parents:mtime", "(dir):parents:(dir):y:mtime", "(dir):parents:(dir):z:mtime"}, compatScratchConfig: types.OptionalBoolFalse, dockerUseBuildKit: true, }, { name: "copy--parents-limit-parent-directories", dockerfileContents: strings.Join([]string{ "# syntax=mirror.gcr.io/docker/dockerfile:1.7-labs", "FROM scratch", "COPY --parents ./x/./y/*.txt /parents/", /* Build context: ./x/y/a.txt ./x/y/b.txt Output: /parents/y/a.txt /parents/y/b.txt */ }, "\n"), contextDir: "copy-parents", fsSkip: []string{"(dir):parents:mtime", "(dir):parents:(dir):y:mtime"}, compatScratchConfig: types.OptionalBoolFalse, dockerUseBuildKit: true, }, { name: "copy--parents-limit-parent-directories-1", dockerfileContents: strings.Join([]string{ "# syntax=mirror.gcr.io/docker/dockerfile:1.7-labs", "FROM scratch", "COPY --parents ./x/y/./*.txt /parents/", /* Build context: ./x/y/a.txt ./x/y/b.txt Output: /parents/a.txt /parents/b.txt */ }, "\n"), contextDir: "copy-parents", fsSkip: []string{"(dir):parents:mtime"}, compatScratchConfig: types.OptionalBoolFalse, dockerUseBuildKit: true, }, { name: "dockerignore-includes-star", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY * subdir/", }, "\n"), contextDir: "dockerignore/populated", tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { dockerignore := []byte("!**/*-c\n") if err := os.WriteFile(filepath.Join(contextDir, ".dockerignore"), dockerignore, 0o100); err != nil { return fmt.Errorf("writing .dockerignore file: %w", err) } if err = os.Chtimes(filepath.Join(contextDir, ".dockerignore"), testDate, testDate); err != nil { return fmt.Errorf("setting date on .dockerignore file in temporary context directory: %w", err) } return nil }, fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerignore-exclude-plain-dot", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY . subdir/", }, "\n"), contextDir: "dockerignore/populated", tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { dockerignore := []byte("subdir-c") if err := os.WriteFile(filepath.Join(contextDir, ".dockerignore"), dockerignore, 0o200); err != nil { return fmt.Errorf("writing .dockerignore file: %w", err) } if err = os.Chtimes(filepath.Join(contextDir, ".dockerignore"), testDate, testDate); err != nil { return fmt.Errorf("setting date on .dockerignore file in temporary context directory: %w", err) } return nil }, fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerignore-exclude-plain-star", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY * subdir/", }, "\n"), contextDir: "dockerignore/populated", tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { dockerignore := []byte("subdir-c") if err := os.WriteFile(filepath.Join(contextDir, ".dockerignore"), dockerignore, 0o400); err != nil { return fmt.Errorf("writing .dockerignore file: %w", err) } if err = os.Chtimes(filepath.Join(contextDir, ".dockerignore"), testDate, testDate); err != nil { return fmt.Errorf("setting date on .dockerignore file in temporary context directory: %w", err) } return nil }, fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerignore-exclude-plain-dot", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY . subdir/", }, "\n"), contextDir: "dockerignore/populated", tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { dockerignore := []byte("**/subdir-c") if err := os.WriteFile(filepath.Join(contextDir, ".dockerignore"), dockerignore, 0o200); err != nil { return fmt.Errorf("writing .dockerignore file: %w", err) } if err = os.Chtimes(filepath.Join(contextDir, ".dockerignore"), testDate, testDate); err != nil { return fmt.Errorf("setting date on .dockerignore file in temporary context directory: %w", err) } return nil }, fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerignore-exclude-plain-star", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY * subdir/", }, "\n"), contextDir: "dockerignore/populated", tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { dockerignore := []byte("**/subdir-c") if err := os.WriteFile(filepath.Join(contextDir, ".dockerignore"), dockerignore, 0o400); err != nil { return fmt.Errorf("writing .dockerignore file: %w", err) } if err = os.Chtimes(filepath.Join(contextDir, ".dockerignore"), testDate, testDate); err != nil { return fmt.Errorf("setting date on .dockerignore file in temporary context directory: %w", err) } return nil }, fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerignore-exclude-wildcard-dot", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY . subdir/", }, "\n"), contextDir: "dockerignore/populated", tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { dockerignore := []byte("subdir-*") if err := os.WriteFile(filepath.Join(contextDir, ".dockerignore"), dockerignore, 0o000); err != nil { return fmt.Errorf("writing .dockerignore file: %w", err) } if err = os.Chtimes(filepath.Join(contextDir, ".dockerignore"), testDate, testDate); err != nil { return fmt.Errorf("setting date on .dockerignore file in temporary context directory: %w", err) } return nil }, fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerignore-exclude-wildcard-star", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY * subdir/", }, "\n"), contextDir: "dockerignore/populated", tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { dockerignore := []byte("subdir-*") if err := os.WriteFile(filepath.Join(contextDir, ".dockerignore"), dockerignore, 0o660); err != nil { return fmt.Errorf("writing .dockerignore file: %w", err) } if err = os.Chtimes(filepath.Join(contextDir, ".dockerignore"), testDate, testDate); err != nil { return fmt.Errorf("setting date on .dockerignore file in temporary context directory: %w", err) } return nil }, fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerignore-exclude-deep-wildcard-dot", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY . subdir/", }, "\n"), contextDir: "dockerignore/populated", tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { dockerignore := []byte("**/subdir-*") if err := os.WriteFile(filepath.Join(contextDir, ".dockerignore"), dockerignore, 0o000); err != nil { return fmt.Errorf("writing .dockerignore file: %w", err) } if err = os.Chtimes(filepath.Join(contextDir, ".dockerignore"), testDate, testDate); err != nil { return fmt.Errorf("setting date on .dockerignore file in temporary context directory: %w", err) } return nil }, fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerignore-exclude-deep-wildcard-star", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY * subdir/", }, "\n"), contextDir: "dockerignore/populated", tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { dockerignore := []byte("**/subdir-*") if err := os.WriteFile(filepath.Join(contextDir, ".dockerignore"), dockerignore, 0o660); err != nil { return fmt.Errorf("writing .dockerignore file: %w", err) } if err = os.Chtimes(filepath.Join(contextDir, ".dockerignore"), testDate, testDate); err != nil { return fmt.Errorf("setting date on .dockerignore file in temporary context directory: %w", err) } return nil }, fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerignore-exclude-deep-subdir-dot", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY . subdir/", }, "\n"), contextDir: "dockerignore/populated", tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { dockerignore := []byte("**/subdir-f") if err := os.WriteFile(filepath.Join(contextDir, ".dockerignore"), dockerignore, 0o666); err != nil { return fmt.Errorf("writing .dockerignore file: %w", err) } if err = os.Chtimes(filepath.Join(contextDir, ".dockerignore"), testDate, testDate); err != nil { return fmt.Errorf("setting date on .dockerignore file in temporary context directory: %w", err) } return nil }, fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerignore-exclude-deep-subdir-star", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY * subdir/", }, "\n"), contextDir: "dockerignore/populated", tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { dockerignore := []byte("**/subdir-f") if err := os.WriteFile(filepath.Join(contextDir, ".dockerignore"), dockerignore, 0o640); err != nil { return fmt.Errorf("writing .dockerignore file: %w", err) } if err = os.Chtimes(filepath.Join(contextDir, ".dockerignore"), testDate, testDate); err != nil { return fmt.Errorf("setting date on .dockerignore file in temporary context directory: %w", err) } return nil }, fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerignore-exclude-not-so-deep-subdir-dot", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY . subdir/", }, "\n"), contextDir: "dockerignore/populated", tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { dockerignore := []byte("**/subdir-b") if err := os.WriteFile(filepath.Join(contextDir, ".dockerignore"), dockerignore, 0o705); err != nil { return fmt.Errorf("writing .dockerignore file: %w", err) } if err = os.Chtimes(filepath.Join(contextDir, ".dockerignore"), testDate, testDate); err != nil { return fmt.Errorf("setting date on .dockerignore file in temporary context directory: %w", err) } return nil }, fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerignore-exclude-not-so-deep-subdir-star", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY * subdir/", }, "\n"), contextDir: "dockerignore/populated", tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { dockerignore := []byte("**/subdir-b") if err := os.WriteFile(filepath.Join(contextDir, ".dockerignore"), dockerignore, 0o750); err != nil { return fmt.Errorf("writing .dockerignore file: %w", err) } if err = os.Chtimes(filepath.Join(contextDir, ".dockerignore"), testDate, testDate); err != nil { return fmt.Errorf("setting date on .dockerignore file in temporary context directory: %w", err) } return nil }, fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerignore-exclude-kind-of-deep-subdir-dot", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY . subdir/", }, "\n"), contextDir: "dockerignore/populated", tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { dockerignore := []byte(strings.Join([]string{"**/subdir-e", "!**/subdir-f"}, "\n")) if err := os.WriteFile(filepath.Join(contextDir, ".dockerignore"), dockerignore, 0o750); err != nil { return fmt.Errorf("writing .dockerignore file: %w", err) } if err = os.Chtimes(filepath.Join(contextDir, ".dockerignore"), testDate, testDate); err != nil { return fmt.Errorf("setting date on .dockerignore file in temporary context directory: %w", err) } return nil }, fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerignore-exclude-kind-of-deep-subdir-star", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY * subdir/", }, "\n"), contextDir: "dockerignore/populated", tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { dockerignore := []byte(strings.Join([]string{"**/subdir-e", "!**/subdir-f"}, "\n")) if err := os.WriteFile(filepath.Join(contextDir, ".dockerignore"), dockerignore, 0o750); err != nil { return fmt.Errorf("writing .dockerignore file: %w", err) } if err = os.Chtimes(filepath.Join(contextDir, ".dockerignore"), testDate, testDate); err != nil { return fmt.Errorf("setting date on .dockerignore file in temporary context directory: %w", err) } return nil }, fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerignore-exclude-deep-subdir-dot", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY . subdir/", }, "\n"), contextDir: "dockerignore/populated", tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { dockerignore := []byte(strings.Join([]string{"**/subdir-f", "!**/subdir-g"}, "\n")) if err := os.WriteFile(filepath.Join(contextDir, ".dockerignore"), dockerignore, 0o750); err != nil { return fmt.Errorf("writing .dockerignore file: %w", err) } if err = os.Chtimes(filepath.Join(contextDir, ".dockerignore"), testDate, testDate); err != nil { return fmt.Errorf("setting date on .dockerignore file in temporary context directory: %w", err) } return nil }, fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerignore-exclude-deep-subdir-star", dockerfileContents: strings.Join([]string{ "FROM scratch", "COPY * subdir/", }, "\n"), contextDir: "dockerignore/populated", tweakContextDir: func(_ *testing.T, contextDir, _, _ string) (err error) { dockerignore := []byte(strings.Join([]string{"**/subdir-f", "!**/subdir-g"}, "\n")) if err := os.WriteFile(filepath.Join(contextDir, ".dockerignore"), dockerignore, 0o750); err != nil { return fmt.Errorf("writing .dockerignore file: %w", err) } if err = os.Chtimes(filepath.Join(contextDir, ".dockerignore"), testDate, testDate); err != nil { return fmt.Errorf("setting date on .dockerignore file in temporary context directory: %w", err) } return nil }, fsSkip: []string{"(dir):subdir:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "env-whitespace", contextDir: "copy", dockerfileContents: strings.Join([]string{ `FROM scratch`, `COPY script .`, `ENV name value`, }, "\n"), compatScratchConfig: types.OptionalBoolTrue, }, { name: "env-simple", contextDir: "copy", dockerfileContents: strings.Join([]string{ `FROM scratch`, `COPY script .`, `ENV name=value`, }, "\n"), compatScratchConfig: types.OptionalBoolTrue, }, { name: "env-unquoted-list", contextDir: "copy", dockerfileContents: strings.Join([]string{ `FROM scratch`, `COPY script .`, `ENV name=value name2=value2`, }, "\n"), compatScratchConfig: types.OptionalBoolTrue, }, { name: "env-dquoted-list", contextDir: "copy", dockerfileContents: strings.Join([]string{ `FROM scratch`, `COPY script .`, `ENV name="value value1"`, }, "\n"), compatScratchConfig: types.OptionalBoolTrue, }, { name: "env-escaped-value", contextDir: "copy", dockerfileContents: strings.Join([]string{ `FROM scratch`, `COPY script .`, `ENV name=value\ value2`, }, "\n"), compatScratchConfig: types.OptionalBoolTrue, }, { name: "env-squote-in-dquote", contextDir: "copy", dockerfileContents: strings.Join([]string{ `FROM scratch`, `COPY script .`, `ENV name="value'quote space'value2"`, }, "\n"), compatScratchConfig: types.OptionalBoolTrue, }, { name: "env-dquote-in-squote", contextDir: "copy", dockerfileContents: strings.Join([]string{ `FROM scratch`, `COPY script .`, `ENV name='value"double quote"value2'`, }, "\n"), compatScratchConfig: types.OptionalBoolTrue, }, { name: "env-escaped-list", contextDir: "copy", dockerfileContents: strings.Join([]string{ `FROM scratch`, `COPY script .`, `ENV name=value\ value2 name2=value2\ value3`, }, "\n"), compatScratchConfig: types.OptionalBoolTrue, }, { name: "env-eddquote", contextDir: "copy", dockerfileContents: strings.Join([]string{ `FROM scratch`, `COPY script .`, `ENV name="a\"b"`, }, "\n"), compatScratchConfig: types.OptionalBoolTrue, }, { name: "env-invalid-ssquote", contextDir: "copy", dockerfileContents: strings.Join([]string{ `FROM scratch`, `COPY script .`, `ENV name='a\'b'`, }, "\n"), shouldFailAt: 3, }, { name: "env-esdquote", contextDir: "copy", dockerfileContents: strings.Join([]string{ `FROM scratch`, `COPY script .`, `ENV name="a\'b"`, }, "\n"), compatScratchConfig: types.OptionalBoolTrue, }, { name: "env-essquote", contextDir: "copy", dockerfileContents: strings.Join([]string{ `FROM scratch`, `COPY script .`, `ENV name='a\'b''`, }, "\n"), compatScratchConfig: types.OptionalBoolTrue, }, { name: "env-edsquote", contextDir: "copy", dockerfileContents: strings.Join([]string{ `FROM scratch`, `COPY script .`, `ENV name='a\"b'`, }, "\n"), compatScratchConfig: types.OptionalBoolTrue, }, { name: "env-empty-squote-in-empty-dquote", contextDir: "copy", dockerfileContents: strings.Join([]string{ `FROM scratch`, `COPY script .`, `ENV name="''"`, }, "\n"), compatScratchConfig: types.OptionalBoolTrue, }, { name: "env-multiline", contextDir: "copy", dockerfileContents: strings.Join([]string{ `FROM scratch`, `COPY script .`, `# don't put anything after the next line - it must be the last line of the`, `# Dockerfile and it must end with \`, `ENV name=value \`, ` name1=value1 \`, ` name2="value2a \`, ` value2b" \`, ` name3="value3a\n\"value3b\"" \`, ` name4="value4a\\nvalue4b" \`, }, "\n"), compatScratchConfig: types.OptionalBoolTrue, }, { name: "copy-from-owner", // from issue #2518 dockerfileContents: strings.Join([]string{ `FROM mirror.gcr.io/alpine`, `RUN set -ex; touch -t @1485449953 /test; chown 65:65 /test`, `FROM scratch`, `USER 66:66`, `COPY --from=0 /test /test`, }, "\n"), fsSkip: []string{"test:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "copy-from-owner-with-chown", // issue #2518, but with chown to override dockerfileContents: strings.Join([]string{ `FROM mirror.gcr.io/alpine`, `RUN set -ex; touch -t @1485449953 /test; chown 65:65 /test`, `FROM scratch`, `USER 66:66`, `COPY --from=0 --chown=1:1 /test /test`, }, "\n"), fsSkip: []string{"test:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "copy-for-user", // flip side of issue #2518 contextDir: "copy", dockerfileContents: strings.Join([]string{ `FROM mirror.gcr.io/alpine`, `USER 66:66`, `COPY /script /script`, }, "\n"), }, { name: "copy-for-user-with-chown", // flip side of issue #2518, but with chown to override contextDir: "copy", dockerfileContents: strings.Join([]string{ `FROM mirror.gcr.io/alpine`, `USER 66:66`, `COPY --chown=1:1 /script /script`, }, "\n"), }, { name: "add-parent-symlink", contextDir: "add/parent-symlink", fsSkip: []string{"(dir):testsubdir:mtime", "(dir):testsubdir:(dir):etc:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "add-parent-dangling", contextDir: "add/parent-dangling", fsSkip: []string{"(dir):symlink:mtime", "(dir):symlink-target:mtime", "(dir):symlink-target:(dir):subdirectory:mtime"}, }, { name: "add-parent-clean", contextDir: "add/parent-clean", fsSkip: []string{"(dir):symlink:mtime", "(dir):symlink-target:mtime", "(dir):symlink-target:(dir):subdirectory:mtime"}, }, { name: "add-archive-1", contextDir: "add/archive", dockerfile: "Dockerfile.1", compatScratchConfig: types.OptionalBoolTrue, }, { name: "add-archive-2", contextDir: "add/archive", dockerfile: "Dockerfile.2", compatScratchConfig: types.OptionalBoolTrue, }, { name: "add-archive-3", contextDir: "add/archive", dockerfile: "Dockerfile.3", compatScratchConfig: types.OptionalBoolTrue, }, { name: "add-archive-4", contextDir: "add/archive", dockerfile: "Dockerfile.4", fsSkip: []string{"(dir):sub:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "add-archive-5", contextDir: "add/archive", dockerfile: "Dockerfile.5", fsSkip: []string{"(dir):sub:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "add-archive-6", contextDir: "add/archive", dockerfile: "Dockerfile.6", fsSkip: []string{"(dir):sub:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "add-archive-7", contextDir: "add/archive", dockerfile: "Dockerfile.7", fsSkip: []string{"(dir):sub:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "add-dir-not-dir", contextDir: "add/dir-not-dir", }, { name: "add-not-dir-dir", contextDir: "add/not-dir-dir", }, { name: "add-populated-dir-not-dir", contextDir: "add/populated-dir-not-dir", }, { name: "dockerignore-allowlist-subdir-nofile-dir", contextDir: "dockerignore/allowlist/subdir-nofile", shouldFailAt: 2, failureRegex: "(no such file or directory)|(file not found)|(file does not exist)", }, { name: "dockerignore-allowlist-subdir-nofile-file", contextDir: "dockerignore/allowlist/subdir-nofile", shouldFailAt: 2, failureRegex: "(no such file or directory)|(file not found)|(file does not exist)", }, { name: "dockerignore-allowlist-subdir-file-dir", contextDir: "dockerignore/allowlist/subdir-file", fsSkip: []string{"(dir):f1:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerignore-allowlist-subdir-file-file", contextDir: "dockerignore/allowlist/subdir-file", fsSkip: []string{"(dir):f1:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerignore-allowlist-nothing-dot", contextDir: "dockerignore/allowlist/nothing-dot", fsSkip: []string{"file:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerignore-allowlist-nothing-slash", contextDir: "dockerignore/allowlist/nothing-slash", fsSkip: []string{"file:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { // the directories are excluded, so entries for them don't get // included in the build context archive, so they only get // created implicitly when extracted, so there's no point in us // trying to preserve any of that, either name: "dockerignore-allowlist-subsubdir-file", contextDir: "dockerignore/allowlist/subsubdir-file", withoutDocker: true, fsSkip: []string{"(dir):folder:mtime", "(dir):folder:(dir):subfolder:mtime", "file:mtime"}, }, { name: "dockerignore-allowlist-subsubdir-nofile", contextDir: "dockerignore/allowlist/subsubdir-nofile", fsSkip: []string{"file:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerignore-allowlist-subsubdir-nosubdir", contextDir: "dockerignore/allowlist/subsubdir-nosubdir", fsSkip: []string{"file:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "dockerignore-allowlist-alternating", contextDir: "dockerignore/allowlist/alternating", withoutDocker: true, fsSkip: []string{ "(dir):subdir1:mtime", "(dir):subdir1:(dir):subdir2:(dir):subdir3:mtime", "(dir):subdir1:(dir):subdir2:(dir):subdir3:(dir):subdir4:(dir):subdir5:mtime", "(dir):subdir2:(dir):subdir3:mtime", "(dir):subdir2:(dir):subdir3:(dir):subdir4:(dir):subdir5:mtime", "(dir):subdir3:mtime", "(dir):subdir3:(dir):subdir4:(dir):subdir5:mtime", "(dir):subdir4:(dir):subdir5:mtime", "(dir):subdir5:mtime", }, }, { name: "dockerignore-allowlist-alternating-nothing", contextDir: "dockerignore/allowlist/alternating-nothing", shouldFailAt: 7, failureRegex: "(no such file or directory)|(file not found)|(file does not exist)", }, { name: "dockerignore-allowlist-alternating-other", contextDir: "dockerignore/allowlist/alternating-other", shouldFailAt: 7, failureRegex: "(no such file or directory)|(file not found)|(file does not exist)", }, { name: "tar-g", contextDir: "tar-g", withoutDocker: true, fsSkip: []string{"(dir):tmp:mtime"}, }, { name: "dockerignore-exceptions-skip", contextDir: "dockerignore/exceptions-skip", fsSkip: []string{"(dir):volume:mtime"}, }, { name: "dockerignore-exceptions-weirdness-1", contextDir: "dockerignore/exceptions-weirdness-1", fsSkip: []string{"(dir):newdir:mtime", "(dir):newdir:(dir):subdir:mtime"}, }, { name: "dockerignore-exceptions-weirdness-2", contextDir: "dockerignore/exceptions-weirdness-2", fsSkip: []string{"(dir):newdir:mtime", "(dir):newdir:(dir):subdir:mtime"}, }, { name: "multistage-builtin-args", // By default, BUILDVARIANT/TARGETVARIANT should be empty. dockerfile: "Dockerfile.margs", dockerUseBuildKit: true, }, { name: "heredoc-copy", dockerfile: "Dockerfile.heredoc_copy", dockerUseBuildKit: true, contextDir: "heredoc", fsSkip: []string{ "(dir):test:mtime", "(dir):test2:mtime", "(dir):test:(dir):humans.txt:mtime", "(dir):test:(dir):robots.txt:mtime", "(dir):test2:(dir):humans.txt:mtime", "(dir):test2:(dir):robots.txt:mtime", "(dir):test2:(dir):image_file:mtime", "(dir):etc:(dir):hostname", /* buildkit does not contains /etc/hostname like buildah */ }, }, { name: "replace-symlink-with-directory", contextDir: "replace/symlink-with-directory", compatScratchConfig: types.OptionalBoolTrue, }, { name: "replace-directory-with-symlink", contextDir: "replace/symlink-with-directory", dockerfile: "Dockerfile.2", compatScratchConfig: types.OptionalBoolTrue, }, { name: "replace-symlink-with-directory-subdir", contextDir: "replace/symlink-with-directory", dockerfile: "Dockerfile.3", fsSkip: []string{"(dir):tree:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "replace-directory-with-symlink-subdir", contextDir: "replace/symlink-with-directory", dockerfile: "Dockerfile.4", fsSkip: []string{"(dir):tree:mtime"}, compatScratchConfig: types.OptionalBoolTrue, }, { name: "workdir-owner", // from issue #3620 dockerfileContents: strings.Join([]string{ `# syntax=mirror.gcr.io/docker/dockerfile:1.4`, `FROM mirror.gcr.io/alpine`, `USER daemon`, `WORKDIR /created/directory`, `RUN ls /created`, }, "\n"), fsSkip: []string{"(dir):created:mtime", "(dir):created:(dir):directory:mtime"}, dockerUseBuildKit: true, }, { name: "buildin-args", dockerfileContents: strings.Join([]string{ "FROM mirror.gcr.io/alpine", "ARG BUILDPLATFORM", "ARG BUILDOS", "ARG BUILDARCH", "ARG BUILDVARIANT", "ARG TARGETPLATFORM", "ARG TARGETOS", "ARG TARGETARCH", "ARG TARGETVARIANT", "RUN echo ${BUILDPLATFORM} > buildplatform", "RUN echo ${BUILDOS} > buildos", "RUN echo ${BUILDARCH} > buildarch", "RUN echo ${BUILDVARIANT} > buildvariant", "RUN echo ${TARGETPLATFORM} > targetplatform", "RUN echo ${TARGETOS} > targetos", "RUN echo ${TARGETARCH} > targetarch", "RUN echo ${TARGETVARIANT} > targetvariant", }, "\n"), dockerUseBuildKit: true, fsSkip: []string{ "(dir):targetarch:mtime", "(dir):targetos:mtime", "(dir):buildos:mtime", "(dir):buildvariant:mtime", "(dir):targetvariant:mtime", "(dir):targetplatform:mtime", "(dir):buildplatform:mtime", "(dir):buildarch:mtime", }, }, { name: "env-precedence", contextDir: "env/precedence", dockerUseBuildKit: true, }, { name: "multistage-copyback", contextDir: "multistage/copyback", dockerUseBuildKit: true, }, { name: "heredoc-quoting", dockerfile: "Dockerfile.heredoc-quoting", dockerUseBuildKit: true, fsSkip: []string{"(dir):etc:(dir):hostname"}, // buildkit does not create a phantom /etc/hostname }, { name: "workdir with trailing separator", dockerfileContents: strings.Join([]string{ "FROM mirror.gcr.io/busybox", "USER daemon", "WORKDIR /tmp/", }, "\n"), }, { name: "workdir without trailing separator", dockerfileContents: strings.Join([]string{ "FROM mirror.gcr.io/busybox", "USER daemon", "WORKDIR /tmp", }, "\n"), }, { name: "chown-volume", // from podman #22530 contextDir: "chown-volume", testUsingVolumes: true, fsSkipCompatVolumesTrue: []string{"(dir):volumea:mtime", "(dir):volumeb:mtime", "(dir):volumec:mtime"}, }, { name: "builtins", contextDir: "builtins", dockerUseBuildKit: true, buildArgs: map[string]string{"SOURCE": "source", "BUSYBOX": "mirror.gcr.io/busybox", "ALPINE": "mirror.gcr.io/alpine", "OWNERID": "0", "SECONDBASE": "localhost/no-such-image"}, }, { name: "header-builtin", contextDir: "header-builtin", dockerUseBuildKit: true, }, { name: "copyglob-1", contextDir: "copyglob", dockerUseBuildKit: true, buildArgs: map[string]string{"SOURCE": "**/*.txt"}, }, { name: "copyglob-2", contextDir: "copyglob", dockerUseBuildKit: true, buildArgs: map[string]string{"SOURCE": "**/sub/*.txt"}, }, { name: "copyglob-3", contextDir: "copyglob", dockerUseBuildKit: true, buildArgs: map[string]string{"SOURCE": "e/**/*sub/*.txt"}, }, { name: "copyglob-4", contextDir: "copyglob", dockerUseBuildKit: true, buildArgs: map[string]string{"SOURCE": "e/**/**/*sub/*.txt"}, }, { name: "mount-cache-by-ownership", dockerUseBuildKit: true, dockerfileContents: strings.Join([]string{ "FROM mirror.gcr.io/busybox", "USER 10", "RUN --mount=type=cache,uid=10,target=/cache touch /cache/10.txt", "USER 0", "RUN --mount=type=cache,target=/cache touch /cache/0.txt", "RUN mkdir -m 770 /results /results/0 /results/10 /results/0+10", "RUN chown -R 10 /results", "RUN --mount=type=cache,target=/cache cp -a /cache/* /results/0", "USER 10", "RUN --mount=type=cache,uid=10,target=/cache cp -a /cache/* /results/10", "USER 0", "RUN --mount=type=cache,uid=10,target=/cache cp -a /cache/* /results/0+10", "RUN touch -r /bin `find /results -print`", }, "\n"), }, { name: "mount-targets-new", contextDir: "mount-targets", dockerUseBuildKit: true, }, { name: "mount-targets-old", contextDir: "mount-targets", dockerUseBuildKit: false, compatScratchConfig: types.OptionalBoolTrue, compatLayerOmissions: types.OptionalBoolTrue, }, { name: "mount-targets-mount", contextDir: "mount-targets", dockerfile: "Dockerfile.mount", dockerUseBuildKit: true, }, { name: "quoted and inherited arg", dockerfile: "Dockerfile.quoted-arg", fsSkip: []string{"(dir):arg-expansion.txt:mtime"}, dockerUseBuildKit: true, }, { name: "build-arg overrides header arg for FROM", dockerfileContents: strings.Join([]string{ "ARG BASE=mirror.gcr.io/alpine", "FROM $BASE", "RUN echo $BASE > /base.txt", }, "\n"), buildArgs: map[string]string{"BASE": "mirror.gcr.io/busybox"}, fsSkip: []string{"(dir):base.txt:mtime"}, }, { name: "header arg default for FROM when no build-arg", dockerfileContents: strings.Join([]string{ "ARG BASE=mirror.gcr.io/alpine", "FROM $BASE", "RUN echo $BASE > /base.txt", }, "\n"), fsSkip: []string{"(dir):base.txt:mtime"}, }, { name: "stage arg overrides header arg in same stage", dockerfileContents: strings.Join([]string{ "ARG FOO=header", "FROM mirror.gcr.io/busybox", "ARG FOO=stage", "RUN echo $FOO > /foo.txt", }, "\n"), fsSkip: []string{"(dir):foo.txt:mtime"}, }, { name: "multiple ARGs in single instruction", dockerfileContents: strings.Join([]string{ "FROM mirror.gcr.io/alpine", "ARG foo=0 bar=1", "RUN echo $foo $bar > /out.txt", }, "\n"), fsSkip: []string{"(dir):out.txt:mtime"}, }, { name: "multiple ARGs in single instruction with build-arg override", dockerfileContents: strings.Join([]string{ "FROM mirror.gcr.io/alpine", "ARG foo=0 bar=1", "RUN echo $foo $bar > /out.txt", }, "\n"), buildArgs: map[string]string{"foo": "9", "bar": "8"}, fsSkip: []string{"(dir):out.txt:mtime"}, }, } func TestCommit(t *testing.T) { t.Parallel() testCases := []struct { description string baseImage string changes, derivedChanges []string config, derivedConfig *docker.Config }{ { description: "defaults", baseImage: "mirror.gcr.io/busybox", }, { description: "empty change", baseImage: "mirror.gcr.io/busybox", changes: []string{""}, }, { description: "empty config", baseImage: "mirror.gcr.io/busybox", config: &docker.Config{}, }, { description: "cmd just changes", baseImage: "mirror.gcr.io/busybox", changes: []string{"CMD /bin/imaginarySh"}, }, { description: "cmd just config", baseImage: "mirror.gcr.io/busybox", config: &docker.Config{ Cmd: []string{"/usr/bin/imaginarySh"}, }, }, { description: "cmd conflict", baseImage: "mirror.gcr.io/busybox", changes: []string{"CMD /bin/imaginarySh"}, config: &docker.Config{ Cmd: []string{"/usr/bin/imaginarySh"}, }, }, { description: "entrypoint just changes", baseImage: "mirror.gcr.io/busybox", changes: []string{"ENTRYPOINT /bin/imaginarySh"}, }, { description: "entrypoint just config", baseImage: "mirror.gcr.io/busybox", config: &docker.Config{ Entrypoint: []string{"/usr/bin/imaginarySh"}, }, }, { description: "entrypoint conflict", baseImage: "mirror.gcr.io/busybox", changes: []string{"ENTRYPOINT /bin/imaginarySh"}, config: &docker.Config{ Entrypoint: []string{"/usr/bin/imaginarySh"}, }, }, { description: "environment just changes", baseImage: "mirror.gcr.io/busybox", changes: []string{"ENV A=1", "ENV C=2"}, }, { description: "environment just config", baseImage: "mirror.gcr.io/busybox", config: &docker.Config{ Env: []string{"A=B"}, }, }, { description: "environment with conflict union", baseImage: "mirror.gcr.io/busybox", changes: []string{"ENV A=1", "ENV C=2"}, config: &docker.Config{ Env: []string{"A=B"}, }, }, { description: "expose just changes", baseImage: "mirror.gcr.io/busybox", changes: []string{"EXPOSE 12345"}, }, { description: "expose just config", baseImage: "mirror.gcr.io/busybox", config: &docker.Config{ ExposedPorts: map[docker.Port]struct{}{"23456/tcp": {}}, }, }, { description: "expose union implicit", baseImage: "mirror.gcr.io/busybox", changes: []string{"EXPOSE 12345"}, config: &docker.Config{ ExposedPorts: map[docker.Port]struct{}{"23456/tcp": {}}, }, }, { description: "expose union explicit", baseImage: "mirror.gcr.io/busybox", changes: []string{"EXPOSE 12345/tcp"}, config: &docker.Config{ ExposedPorts: map[docker.Port]struct{}{"23456/tcp": {}}, }, }, { description: "healthcheck just changes", baseImage: "mirror.gcr.io/busybox", changes: []string{`HEALTHCHECK --interval=1s --timeout=1s --start-period=1s --retries=1 CMD ["/bin/false"]`}, }, { description: "healthcheck just config", baseImage: "mirror.gcr.io/busybox", config: &docker.Config{ Healthcheck: &docker.HealthConfig{ Test: []string{"/bin/true"}, Interval: 2 * time.Second, Timeout: 2 * time.Second, StartPeriod: 2 * time.Second, Retries: 2, }, }, }, { description: "healthcheck conflict", baseImage: "mirror.gcr.io/busybox", changes: []string{`HEALTHCHECK --interval=1s --timeout=1s --start-period=1s --retries=1 CMD ["/bin/false"]`}, config: &docker.Config{ Healthcheck: &docker.HealthConfig{ Test: []string{"/bin/true"}, Interval: 2 * time.Second, Timeout: 2 * time.Second, StartPeriod: 2 * time.Second, Retries: 2, }, }, }, { description: "label just changes", baseImage: "mirror.gcr.io/busybox", changes: []string{"LABEL A=1 C=2"}, }, { description: "label just config", baseImage: "mirror.gcr.io/busybox", config: &docker.Config{ Labels: map[string]string{"A": "B"}, }, }, { description: "label with conflict union", baseImage: "mirror.gcr.io/busybox", changes: []string{"LABEL A=1 C=2"}, config: &docker.Config{ Labels: map[string]string{"A": "B"}, }, }, // n.b. dockerd didn't like a MAINTAINER change, so no test for it, and it's not in a config blob { description: "onbuild just changes", baseImage: "mirror.gcr.io/busybox", changes: []string{"ONBUILD USER alice", "ONBUILD LABEL A=1"}, derivedChanges: []string{"LABEL C=3"}, }, { description: "onbuild just config", baseImage: "mirror.gcr.io/busybox", config: &docker.Config{ OnBuild: []string{"USER bob", `CMD ["/bin/smash"]`, "LABEL B=2"}, }, derivedChanges: []string{"LABEL C=3"}, }, { description: "onbuild conflict", baseImage: "mirror.gcr.io/busybox", changes: []string{"ONBUILD USER alice", "ONBUILD LABEL A=1"}, config: &docker.Config{ OnBuild: []string{"USER bob", `CMD ["/bin/smash"]`, "LABEL B=2"}, }, derivedChanges: []string{"LABEL C=3"}, }, // n.b. dockerd didn't like a SHELL change, so no test for it or a conflict with a config blob { description: "shell just config", baseImage: "mirror.gcr.io/busybox", config: &docker.Config{ Shell: []string{"/usr/bin/imaginarySh"}, }, }, { description: "stop signal conflict", baseImage: "mirror.gcr.io/busybox", changes: []string{"STOPSIGNAL SIGTERM"}, config: &docker.Config{ StopSignal: "SIGKILL", }, }, { description: "stop timeout=0", baseImage: "mirror.gcr.io/busybox", config: &docker.Config{ StopTimeout: 0, }, }, { description: "stop timeout=15", baseImage: "mirror.gcr.io/busybox", config: &docker.Config{ StopTimeout: 15, }, }, { description: "stop timeout=15, then 0", baseImage: "mirror.gcr.io/busybox", config: &docker.Config{ StopTimeout: 15, }, derivedConfig: &docker.Config{ StopTimeout: 0, }, }, { description: "stop timeout=0, then 15", baseImage: "mirror.gcr.io/busybox", config: &docker.Config{ StopTimeout: 0, }, derivedConfig: &docker.Config{ StopTimeout: 15, }, }, { description: "user just changes", baseImage: "mirror.gcr.io/busybox", changes: []string{"USER 1001:1001"}, }, { description: "user just config", baseImage: "mirror.gcr.io/busybox", config: &docker.Config{ User: "1000:1000", }, }, { description: "user with conflict", baseImage: "mirror.gcr.io/busybox", changes: []string{"USER 1001:1001"}, config: &docker.Config{ User: "1000:1000", }, }, { description: "volume just changes", baseImage: "mirror.gcr.io/busybox", changes: []string{"VOLUME /a-volume"}, }, { description: "volume just config", baseImage: "mirror.gcr.io/busybox", config: &docker.Config{ Volumes: map[string]struct{}{"/b-volume": {}}, }, }, { description: "volume union", baseImage: "mirror.gcr.io/busybox", changes: []string{"VOLUME /a-volume"}, config: &docker.Config{ Volumes: map[string]struct{}{"/b-volume": {}}, }, }, { description: "workdir just changes", baseImage: "mirror.gcr.io/busybox", changes: []string{"WORKDIR /yeah"}, }, { description: "workdir just config", baseImage: "mirror.gcr.io/busybox", config: &docker.Config{ WorkingDir: "/naw", }, }, { description: "workdir with conflict", baseImage: "mirror.gcr.io/busybox", changes: []string{"WORKDIR /yeah"}, config: &docker.Config{ WorkingDir: "/naw", }, }, } var tempdir string buildahDir := buildahDir if buildahDir == "" { if tempdir == "" { tempdir = t.TempDir() } buildahDir = filepath.Join(tempdir, "buildah") } dockerDir := dockerDir if dockerDir == "" { if tempdir == "" { tempdir = t.TempDir() } dockerDir = filepath.Join(tempdir, "docker") } ctx := context.TODO() // connect to dockerd using go-dockerclient client, err := docker.NewClientFromEnv() require.NoErrorf(t, err, "unable to initialize docker client") var dockerVersion []string if version, err := client.Version(); err == nil { if version != nil { for _, s := range *version { dockerVersion = append(dockerVersion, s) } } } else { require.NoErrorf(t, err, "unable to connect to docker daemon") } // find a new place to store buildah builds tempdir = t.TempDir() // create subdirectories to use for buildah storage rootDir := filepath.Join(tempdir, "root") runrootDir := filepath.Join(tempdir, "runroot") // initialize storage for buildah options := storage.StoreOptions{ GraphDriverName: os.Getenv("STORAGE_DRIVER"), GraphRoot: rootDir, RunRoot: runrootDir, } store, err := storage.GetStore(options) require.NoErrorf(t, err, "error creating buildah storage at %q", rootDir) defer func() { if store != nil { _, err := store.Shutdown(true) require.NoErrorf(t, err, "error shutting down storage for buildah") } }() // walk through test cases for testIndex, testCase := range testCases { t.Run(testCase.description, func(t *testing.T) { test := testCases[testIndex] // create the test container, then commit it, using the docker client baseImage := test.baseImage repository, tag := docker.ParseRepositoryTag(baseImage) if tag == "" { tag = "latest" } baseImage = repository + ":" + tag if _, err := client.InspectImage(test.baseImage); err != nil && errors.Is(err, docker.ErrNoSuchImage) { // oh, we need to pull the base image err = client.PullImage(docker.PullImageOptions{ Repository: repository, Tag: tag, }, docker.AuthConfiguration{}) require.NoErrorf(t, err, "pulling base image") } container, err := client.CreateContainer(docker.CreateContainerOptions{ Context: ctx, Config: &docker.Config{ Image: baseImage, }, }) require.NoErrorf(t, err, "creating the working container with docker") if err == nil { defer func(containerName string) { err := client.RemoveContainer(docker.RemoveContainerOptions{ ID: containerName, Force: true, }) assert.Nil(t, err, "error deleting working docker container %q", containerName) }(container.ID) } dockerImageName := "committed:" + strconv.Itoa(testIndex) dockerImage, err := client.CommitContainer(docker.CommitContainerOptions{ Container: container.ID, Changes: test.changes, Run: test.config, Repository: dockerImageName, }) assert.NoErrorf(t, err, "committing the working container with docker") if err == nil { defer func(dockerImageName string) { err := client.RemoveImageExtended(dockerImageName, docker.RemoveImageOptions{ Context: ctx, Force: true, }) assert.Nil(t, err, "error deleting newly-built docker image %q", dockerImage.ID) }(dockerImageName) } dockerRef, err := alltransports.ParseImageName("docker-daemon:" + dockerImageName) assert.NoErrorf(t, err, "parsing name of newly-committed docker image") if len(test.derivedChanges) > 0 || test.derivedConfig != nil { container, err := client.CreateContainer(docker.CreateContainerOptions{ Context: ctx, Config: &docker.Config{ Image: dockerImage.ID, }, }) require.NoErrorf(t, err, "creating the derived container with docker") if err == nil { defer func(containerName string) { err := client.RemoveContainer(docker.RemoveContainerOptions{ ID: containerName, Force: true, }) assert.Nil(t, err, "error deleting derived docker container %q", containerName) }(container.ID) } derivedImageName := "derived:" + strconv.Itoa(testIndex) derivedImage, err := client.CommitContainer(docker.CommitContainerOptions{ Container: container.ID, Changes: test.derivedChanges, Run: test.derivedConfig, Repository: derivedImageName, }) assert.NoErrorf(t, err, "committing the derived container with docker") defer func(derivedImageName string) { err := client.RemoveImageExtended(derivedImageName, docker.RemoveImageOptions{ Context: ctx, Force: true, }) assert.Nil(t, err, "error deleting newly-derived docker image %q", derivedImage.ID) }(derivedImageName) dockerRef, err = alltransports.ParseImageName("docker-daemon:" + derivedImageName) assert.NoErrorf(t, err, "parsing name of newly-derived docker image") } // create the test container, then commit it, using the buildah API builder, err := buildah.NewBuilder(ctx, store, buildah.BuilderOptions{ FromImage: baseImage, }) require.NoErrorf(t, err, "creating the working container with buildah") defer func(builder *buildah.Builder) { err := builder.Delete() assert.NoErrorf(t, err, "removing the working container") }(builder) var overrideConfig *manifest.Schema2Config if test.config != nil { overrideConfig = config.Schema2ConfigFromGoDockerclientConfig(test.config) } buildahID, _, _, err := builder.Commit(ctx, nil, buildah.CommitOptions{ PreferredManifestType: manifest.DockerV2Schema2MediaType, OverrideChanges: test.changes, OverrideConfig: overrideConfig, }) assert.NoErrorf(t, err, "committing buildah image") buildahRef, err := is.Transport.NewStoreReference(store, nil, buildahID) assert.NoErrorf(t, err, "parsing name of newly-built buildah image") if len(test.derivedChanges) > 0 || test.derivedConfig != nil { derivedBuilder, err := buildah.NewBuilder(ctx, store, buildah.BuilderOptions{ FromImage: buildahID, }) require.NoErrorf(t, err, "creating the derived container with buildah") defer func(builder *buildah.Builder) { err := builder.Delete() assert.NoErrorf(t, err, "removing the derived container") }(derivedBuilder) var overrideConfig *manifest.Schema2Config if test.derivedConfig != nil { overrideConfig = config.Schema2ConfigFromGoDockerclientConfig(test.derivedConfig) } derivedID, _, _, err := builder.Commit(ctx, nil, buildah.CommitOptions{ PreferredManifestType: manifest.DockerV2Schema2MediaType, OverrideChanges: test.derivedChanges, OverrideConfig: overrideConfig, }) assert.NoErrorf(t, err, "committing derived buildah image") buildahRef, err = is.Transport.NewStoreReference(store, nil, derivedID) assert.NoErrorf(t, err, "parsing name of newly-derived buildah image") } // scan the images saveReport(ctx, t, dockerRef, filepath.Join(dockerDir, t.Name()), []byte{}, []byte{}, dockerVersion) saveReport(ctx, t, buildahRef, filepath.Join(buildahDir, t.Name()), []byte{}, []byte{}, dockerVersion) // compare the scans _, originalDockerConfig, ociDockerConfig, fsDocker := readReport(t, filepath.Join(dockerDir, t.Name())) _, originalBuildahConfig, ociBuildahConfig, fsBuildah := readReport(t, filepath.Join(buildahDir, t.Name())) miss, left, diff, same := compareJSON(originalDockerConfig, originalBuildahConfig, originalSkip) if !same { assert.Failf(t, "Image configurations differ as committed in Docker format", configCompareResult(miss, left, diff, "buildah")) } miss, left, diff, same = compareJSON(ociDockerConfig, ociBuildahConfig, ociSkip) if !same { assert.Failf(t, "Image configurations differ when converted to OCI format", configCompareResult(miss, left, diff, "buildah")) } miss, left, diff, same = compareJSON(fsDocker, fsBuildah, fsSkip) if !same { assert.Failf(t, "Filesystem contents differ", fsCompareResult(miss, left, diff, "buildah")) } }) } } ================================================ FILE: tests/conformance/selinux_linux_test.go ================================================ package conformance import ( selinux "github.com/opencontainers/selinux/go-selinux" ) func selinuxMountFlag() string { if selinux.GetEnabled() { return ":Z" } return "" } ================================================ FILE: tests/conformance/selinux_unsupported_test.go ================================================ //go:build !linux package conformance func selinuxMountFlag() string { return "" } ================================================ FILE: tests/conformance/testdata/Dockerfile.add ================================================ FROM mirror.gcr.io/busybox ADD https://github.com/openshift/origin/raw/main/README.md README.md USER 1001 ADD https://github.com/openshift/origin/raw/main/LICENSE . ADD https://github.com/openshift/origin/raw/main/LICENSE A ADD https://github.com/openshift/origin/raw/main/LICENSE ./a USER root RUN mkdir ./b ADD https://github.com/openshift/origin/raw/main/LICENSE ./b/a ADD https://github.com/openshift/origin/raw/main/LICENSE ./b/. ADD https://github.com/openshift/ruby-hello-world/archive/master.zip /tmp/ ================================================ FILE: tests/conformance/testdata/Dockerfile.copyfrom_1 ================================================ FROM mirror.gcr.io/busybox as base RUN touch -t @1485449953 /a /b FROM mirror.gcr.io/busybox COPY --from=base /a / RUN ls -al /a ================================================ FILE: tests/conformance/testdata/Dockerfile.copyfrom_10 ================================================ FROM mirror.gcr.io/busybox as base RUN mkdir -p /a && touch -t @1485449953 /a/1 FROM mirror.gcr.io/busybox COPY --from=base /a/1 /a/b/c RUN ls -al /a/b/c && ! ls -al /a/b/1 ================================================ FILE: tests/conformance/testdata/Dockerfile.copyfrom_11 ================================================ FROM mirror.gcr.io/busybox as base RUN mkdir -p /a && touch -t @1485449953 /a/1 FROM mirror.gcr.io/busybox COPY --from=base /a /a/b/c RUN ls -al /a/b/c/1 && ! ls -al /a/b/1 ================================================ FILE: tests/conformance/testdata/Dockerfile.copyfrom_12 ================================================ FROM mirror.gcr.io/busybox as base RUN mkdir -p /a && touch -t @1485449953 /a/1 FROM mirror.gcr.io/busybox COPY --from=base a /a RUN ls -al /a/1 && ! ls -al /a/a ================================================ FILE: tests/conformance/testdata/Dockerfile.copyfrom_13 ================================================ FROM mirror.gcr.io/busybox as base RUN mkdir -p /a && touch -t @1485449953 /a/1 FROM mirror.gcr.io/busybox COPY --from=base a/. /a RUN ls -al /a/1 && ! ls -al /a/a ================================================ FILE: tests/conformance/testdata/Dockerfile.copyfrom_2 ================================================ FROM mirror.gcr.io/busybox as base RUN touch -t @1485449953 /a FROM mirror.gcr.io/busybox COPY --from=base /a /a RUN ls -al /a ================================================ FILE: tests/conformance/testdata/Dockerfile.copyfrom_3 ================================================ FROM mirror.gcr.io/busybox as base RUN touch -t @1485449953 /a FROM mirror.gcr.io/busybox WORKDIR /b COPY --from=base /a . RUN ls -al /b/a ================================================ FILE: tests/conformance/testdata/Dockerfile.copyfrom_3_1 ================================================ FROM mirror.gcr.io/busybox as base RUN touch -t @1485449953 /a FROM mirror.gcr.io/busybox WORKDIR /b COPY --from=base /a ./b RUN ls -al /b/b ================================================ FILE: tests/conformance/testdata/Dockerfile.copyfrom_4 ================================================ FROM mirror.gcr.io/busybox as base RUN mkdir -p /a/b && touch -t @1485449953 /a/b/1 /a/b/2 FROM mirror.gcr.io/busybox COPY --from=base /a/b/ /b/ RUN ls -al /b/1 /b/2 /b && ! ls -al /a ================================================ FILE: tests/conformance/testdata/Dockerfile.copyfrom_5 ================================================ FROM mirror.gcr.io/busybox as base RUN mkdir -p /a/b && touch -t @1485449953 /a/b/1 /a/b/2 FROM mirror.gcr.io/busybox COPY --from=base /a/b/* /b/ RUN ls -al /b/1 /b/2 /b && ! ls -al /a /b/a /b/b ================================================ FILE: tests/conformance/testdata/Dockerfile.copyfrom_6 ================================================ FROM mirror.gcr.io/busybox as base RUN mkdir -p /a/b && touch -t @1485449953 /a/b/1 /a/b/2 FROM mirror.gcr.io/busybox COPY --from=base /a/b/. /b/ RUN ls -al /b/1 /b/2 /b && ! ls -al /a /b/a /b/b ================================================ FILE: tests/conformance/testdata/Dockerfile.copyfrom_7 ================================================ FROM mirror.gcr.io/busybox as base RUN touch -t @1485449953 /b FROM mirror.gcr.io/busybox COPY --from=base /b /a RUN ls -al /a && ! ls -al /b ================================================ FILE: tests/conformance/testdata/Dockerfile.copyfrom_8 ================================================ FROM mirror.gcr.io/busybox as base RUN mkdir -p /a && touch -t @1485449953 /a/b FROM mirror.gcr.io/busybox COPY --from=base /a/b /a RUN ls -al /a && ! ls -al /b ================================================ FILE: tests/conformance/testdata/Dockerfile.copyfrom_9 ================================================ FROM mirror.gcr.io/busybox as base RUN mkdir -p /a && touch -t @1485449953 /a/1 FROM mirror.gcr.io/busybox COPY --from=base /a/1 /a/b/c/ RUN ls -al /a/b/c/1 && ! ls -al /a/b/1 ================================================ FILE: tests/conformance/testdata/Dockerfile.edgecases ================================================ # Note: Hopefully a registries.conf alias redirects this to quay.io/libpod/mirror.gcr.io/busybox FROM mirror.gcr.io/busybox MAINTAINER docker ONBUILD RUN ["echo", "test"] ONBUILD RUN echo test ONBUILD COPY . / # RUN Commands \ # linebreak in comment \ RUN ["ls", "-la"] RUN ["echo", "'1234'"] RUN echo "1234" RUN echo 1234 RUN echo '1234' && \ echo "456" && \ echo 789 RUN sh -c 'echo root:testpass \ > /tmp/passwd' RUN mkdir -p /test /test2 /test3/test # ENV \ ENV SCUBA 1 DUBA 3 ENV SCUBA "1 DUBA 3" # CMD \ CMD ["echo", "test"] CMD echo test CMD echo "test" CMD echo 'test' CMD echo 'test' | wc - #EXPOSE\ EXPOSE 3000 EXPOSE 9000 5000 6000 USER docker USER docker:root VOLUME ["/test"] VOLUME ["/test", "/test2"] VOLUME /test3 WORKDIR /test ADD . / COPY . copy ================================================ FILE: tests/conformance/testdata/Dockerfile.env ================================================ FROM mirror.gcr.io/busybox ENV name value ENV name=value ENV name=value name2=value2 ENV name="value value1" ENV name=value\ value2 ENV name="value'quote space'value2" ENV name='value"double quote"value2' ENV name=value\ value2 name2=value2\ value3 ENV name="a\"b" ENV name="a\'b" ENV name='a\'b' ENV name='a\'b'' ENV name='a\"b' ENV name="''" # don't put anything after the next line - it must be the last line of the # Dockerfile and it must end with \ ENV name=value \ name1=value1 \ name2="value2a \ value2b" \ name3="value3a\n\"value3b\"" \ name4="value4a\\nvalue4b" \ ================================================ FILE: tests/conformance/testdata/Dockerfile.exposedefault ================================================ FROM mirror.gcr.io/busybox EXPOSE 3469 ================================================ FILE: tests/conformance/testdata/Dockerfile.heredoc-quoting ================================================ FROM mirror.gcr.io/busybox ARG argA=argvA ENV varA=valueA # An argument, an environment variable, and one set in the heredoc RUN < first/buildplatform=`echo ${BUILDPLATFORM} | sed s,/,_,g` RUN echo ${BUILDOS} > first/buildos=`echo ${BUILDOS} | sed s,/,_,g` RUN echo ${BUILDARCH} > first/buildarch=`echo ${BUILDARCH} | sed s,/,_,g` RUN echo ${BUILDVARIANT} > first/buildvariant=`echo ${BUILDVARIANT} | sed s,/,_,g` RUN echo ${TARGETPLATFORM} > first/targetplatform=`echo ${TARGETPLATFORM} | sed s,/,_,g` RUN echo ${TARGETOS} > first/targetos=`echo ${TARGETOS} | sed s,/,_,g` RUN echo ${TARGETARCH} > first/targetarch=`echo ${TARGETARCH} | sed s,/,_,g` RUN echo ${TARGETVARIANT} > first/targetvariant=`echo ${TARGETVARIANT} | sed s,/,_,g` FROM mirror.gcr.io/alpine ARG BUILDPLATFORM ARG BUILDOS ARG BUILDARCH ARG BUILDVARIANT ARG TARGETPLATFORM ARG TARGETOS ARG TARGETARCH ARG TARGETVARIANT RUN mkdir second RUN echo ${BUILDPLATFORM} > second/buildplatform=`echo ${BUILDPLATFORM} | sed s,/,_,g` RUN echo ${BUILDOS} > second/buildos=`echo ${BUILDOS} | sed s,/,_,g` RUN echo ${BUILDARCH} > second/buildarch=`echo ${BUILDARCH} | sed s,/,_,g` RUN echo ${BUILDVARIANT} > second/buildvariant=`echo ${BUILDVARIANT} | sed s,/,_,g` RUN echo ${TARGETPLATFORM} > second/targetplatform=`echo ${TARGETPLATFORM} | sed s,/,_,g` RUN echo ${TARGETOS} > second/targetos=`echo ${TARGETOS} | sed s,/,_,g` RUN echo ${TARGETARCH} > second/targetarch=`echo ${TARGETARCH} | sed s,/,_,g` RUN echo ${TARGETVARIANT} > second/targetvariant=`echo ${TARGETVARIANT} | sed s,/,_,g` COPY --from=0 first/* ./first/ RUN touch -r /etc/os-release first first/* second second/* ================================================ FILE: tests/conformance/testdata/Dockerfile.quoted-arg ================================================ FROM quay.io/libpod/centos:7 AS lower ARG quoted="words with quotes" ARG expanded=$quoted RUN printf "%q\n" lower: $quoted >> /arg-expansion.txt RUN printf "%q\n" lower: $expanded >> /arg-expansion.txt FROM lower AS upper RUN printf "%q\n" upper: $quoted >> /arg-expansion.txt RUN printf "%q\n" upper: $expanded >> /arg-expansion.txt ================================================ FILE: tests/conformance/testdata/Dockerfile.reusebase ================================================ FROM quay.io/libpod/centos:7 AS base RUN touch -t 201701261659.13 /1 ENV LOCAL=/1 FROM base RUN find $LOCAL ================================================ FILE: tests/conformance/testdata/Dockerfile.run.args ================================================ FROM mirror.gcr.io/busybox RUN echo first second RUN /bin/echo third fourth RUN ["/bin/echo", "fifth", "sixth"] RUN ["/bin/sh", "-c", "echo inner $1", "", "outer"] ================================================ FILE: tests/conformance/testdata/Dockerfile.shell ================================================ FROM quay.io/libpod/centos:7 SHELL ["/bin/bash", "-xc"] RUN env ================================================ FILE: tests/conformance/testdata/add/archive/Dockerfile.1 ================================================ FROM scratch ADD . . ================================================ FILE: tests/conformance/testdata/add/archive/Dockerfile.2 ================================================ FROM scratch ADD sub/ / ================================================ FILE: tests/conformance/testdata/add/archive/Dockerfile.3 ================================================ FROM scratch ADD sub/subdirectory.tar.gz / ================================================ FILE: tests/conformance/testdata/add/archive/Dockerfile.4 ================================================ FROM scratch ADD / / ================================================ FILE: tests/conformance/testdata/add/archive/Dockerfile.5 ================================================ FROM scratch ADD sub/ sub/ ================================================ FILE: tests/conformance/testdata/add/archive/Dockerfile.6 ================================================ FROM scratch ADD sub/subdirectory.tar.gz /sub/ ================================================ FILE: tests/conformance/testdata/add/archive/Dockerfile.7 ================================================ FROM scratch ADD sub/* /sub/ ================================================ FILE: tests/conformance/testdata/add/dir-not-dir/Dockerfile ================================================ FROM mirror.gcr.io/busybox RUN mkdir fileone ADD test.tar / ================================================ FILE: tests/conformance/testdata/add/not-dir-dir/Dockerfile ================================================ FROM mirror.gcr.io/busybox RUN touch /new-directory ADD test.tar / RUN ls -ld /new-directory ================================================ FILE: tests/conformance/testdata/add/parent-clean/Dockerfile ================================================ FROM mirror.gcr.io/busybox RUN ln -s /symlink-target/subdirectory /symlink ADD . /symlink/.. RUN find /symlink* -print ================================================ FILE: tests/conformance/testdata/add/parent-dangling/Dockerfile ================================================ FROM mirror.gcr.io/busybox RUN ln -s symlink-target /symlink ADD . /symlink/subdirectory/ RUN find /symlink* -print ================================================ FILE: tests/conformance/testdata/add/parent-symlink/Dockerfile ================================================ FROM scratch ADD foobar.tar /testsubdir ================================================ FILE: tests/conformance/testdata/add/populated-dir-not-dir/Dockerfile ================================================ FROM mirror.gcr.io/busybox RUN mkdir dirone RUN echo one > dirone/onefile.txt RUN echo two > dirone/twofile.txt ADD test.tar / ================================================ FILE: tests/conformance/testdata/builtins/Dockerfile ================================================ ARG ALPINE FROM mirror.gcr.io/busybox RUN echo TARGETPLATFORM=$TARGETPLATFORM | tee 0.txt ARG TARGETPLATFORM RUN echo TARGETPLATFORM=$TARGETPLATFORM | tee -a 0.txt RUN touch -d @0 0.txt FROM ${SECONDBASE:-mirror.gcr.io/busybox} COPY --from=0 /*.txt / COPY --chown=${OWNERID:-1}:${OWNERID:-1} ${SOURCE:-other}file.txt /1a.txt ARG OWNERID=1 ARG SOURCE= COPY --chown=${OWNERID:-1}:${OWNERID:-1} ${SOURCE:-other}file.txt /1b.txt FROM ${ALPINE:-mirror.gcr.io/busybox} ARG SECONDBASE=localhost/no-such-image COPY --from=1 /*.txt / RUN cp -p /etc/nsswitch.conf /2.txt FROM ${BUSYBOX:-mirror.gcr.io/alpine} COPY --from=2 /*.txt / RUN cp -p /etc/nsswitch.conf /3.txt ================================================ FILE: tests/conformance/testdata/builtins/otherfile.txt ================================================ other ================================================ FILE: tests/conformance/testdata/builtins/sourcefile.txt ================================================ source ================================================ FILE: tests/conformance/testdata/chown-volume/Dockerfile ================================================ FROM mirror.gcr.io/busybox AS first RUN mkdir /volumea /volumeb /volumec # This next change will be omitted from naive diff output without # both https://github.com/containers/storage/pull/1962 and # https://github.com/containers/storage/pull/1968 RUN touch -r /bin/ls /volumea /volumeb /volumec VOLUME /volumea VOLUME /volumeb VOLUME /volumec FROM first RUN chown 1000:1000 /volumea /volumeb /volumec ================================================ FILE: tests/conformance/testdata/copy/Dockerfile ================================================ FROM quay.io/libpod/centos:7 COPY script /usr/bin RUN ls -al /usr/bin/script ================================================ FILE: tests/conformance/testdata/copy/script ================================================ #!/usr/bin/env bash exit 0 ================================================ FILE: tests/conformance/testdata/copy-escape-glob/Dockerfile ================================================ FROM scratch WORKDIR /app # The "[]" sequence immediately after the "[" signals to filepath.Glob() that # it is _not_ the beginning of a "class" chunk in the globbing pattern. N.B. # this is a golang-specific way to do it, different from what glob(7) suggests. COPY ./app/[[]xyz]/file.txt /app/ COPY ./app/[[]xyz]/[[]abc]/file.txt /app2/ COPY ./app/* /app3/ # This should only match one file. COPY ./app/n\?pe/file.txt /app4/file.txt # This should only match one file. COPY ./app/st\*uv/file.txt /app5/file.txt ================================================ FILE: tests/conformance/testdata/copy-escape-glob/app/[xyz]/[abc]/file.txt ================================================ also a file ================================================ FILE: tests/conformance/testdata/copy-escape-glob/app/[xyz]/file.txt ================================================ a file ================================================ FILE: tests/conformance/testdata/copy-escape-glob/app/nope/file.txt ================================================ but not ================================================ FILE: tests/conformance/testdata/copy-escape-glob/app/strauv/file.txt ================================================ not this one ================================================ FILE: tests/conformance/testdata/copy-parents/x/a.txt ================================================ A-FILE ================================================ FILE: tests/conformance/testdata/copy-parents/x/y/a.txt ================================================ A-FILE ================================================ FILE: tests/conformance/testdata/copy-parents/x/y/b.txt ================================================ Hello ================================================ FILE: tests/conformance/testdata/copy-parents/x/z/a.txt ================================================ A-FILE ================================================ FILE: tests/conformance/testdata/copy-parents/x/z/b.txt ================================================ Hello ================================================ FILE: tests/conformance/testdata/copy-parents/y/a.txt ================================================ A-FILE ================================================ FILE: tests/conformance/testdata/copy-parents/y/b.txt ================================================ Hello ================================================ FILE: tests/conformance/testdata/copyblahblub/Dockerfile ================================================ FROM mirror.gcr.io/busybox COPY firstdir/seconddir /var RUN ls -la /var RUN ls -la /var/dir-a ================================================ FILE: tests/conformance/testdata/copyblahblub/Dockerfile2 ================================================ FROM mirror.gcr.io/busybox COPY /firstdir/seconddir /var RUN ls -la /var RUN ls -la /var/dir-a ================================================ FILE: tests/conformance/testdata/copyblahblub/Dockerfile3 ================================================ FROM mirror.gcr.io/busybox COPY /firstdir/seconddir /var RUN ls -la /var RUN ls -la /var/dir-a FROM mirror.gcr.io/busybox COPY --from=0 /var/dir-a /var RUN ls -la /var RUN ls -la /var/file-a ================================================ FILE: tests/conformance/testdata/copyblahblub/firstdir/seconddir/dir-a/file-a ================================================ file-a ================================================ FILE: tests/conformance/testdata/copyblahblub/firstdir/seconddir/dir-b/file-b ================================================ file-b ================================================ FILE: tests/conformance/testdata/copychown/Dockerfile ================================================ FROM quay.io/libpod/centos:7 COPY --chown=1:2 script /usr/bin/script.12 COPY --chown=1:adm script /usr/bin/script.1-adm COPY --chown=1 script /usr/bin/script.1 COPY --chown=lp:adm script /usr/bin/script.lp-adm COPY --chown=2:mail script /usr/bin/script.2-mail COPY --chown=2 script /usr/bin/script.2 COPY --chown=bin script /usr/bin/script.bin COPY --chown=lp script /usr/bin/script.lp COPY --chown=3 script script2 /usr/local/bin/ RUN ls -al /usr/bin/script ================================================ FILE: tests/conformance/testdata/copychown/script ================================================ #!/bin/bash exit 0 ================================================ FILE: tests/conformance/testdata/copychown/script2 ================================================ #!/bin/bash exit 1 ================================================ FILE: tests/conformance/testdata/copydir/Dockerfile ================================================ FROM quay.io/libpod/centos:7 COPY dir /dir RUN ls -al /dir/file ================================================ FILE: tests/conformance/testdata/copydir/dir/file ================================================ ================================================ FILE: tests/conformance/testdata/copyempty/.script ================================================ #!/bin/bash : ================================================ FILE: tests/conformance/testdata/copyempty/Dockerfile ================================================ FROM quay.io/libpod/centos:7 COPY "" /usr/local/tmp/ ================================================ FILE: tests/conformance/testdata/copyempty/Dockerfile2 ================================================ FROM quay.io/libpod/centos:7 COPY script1 "" script2 /usr/local/tmp/ ================================================ FILE: tests/conformance/testdata/copyempty/script1 ================================================ #!/bin/bash exit 0 ================================================ FILE: tests/conformance/testdata/copyempty/script2 ================================================ #!/bin/bash exit 1 ================================================ FILE: tests/conformance/testdata/copyglob/Beach.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/Dockerfile ================================================ FROM scratch ARG SOURCE COPY $SOURCE / ================================================ FILE: tests/conformance/testdata/copyglob/a/sub/subsub/protopatriarchal.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/a/sub/subsub/undestructible.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/b/.sub/gade.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/b/.sub/parcae.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/b/heliacally.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/b/overgoing.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/b/sub/ileosigmoidostomy.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/b/sub/overdilation.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/c/sub/disadvise.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/c/sub/subsub/subsubsub/fiddlecome.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/c/sub/subsub/subsubsub/unweariableness.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/c/sub/travel-sick.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/d/.sub/restocks.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/d/.sub/unblazoned.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/d/sub/alkalinity.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/d/sub/glandules.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/e/.sub/Tytonidae.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/e/.sub/vice-guilty.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/e/Towroy.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/e/sub/subsub/near-blindness.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/e/sub/subsub/paymaster-generalship.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/e/subjectivities.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/f/.sub/bilobate.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/f/.sub/fine-headed.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/f/Etnean.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/f/Sheya.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/f/sub/Vernaccia.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/f/sub/inaccordance.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/f/sub/subsub/subsubsub/ankylosing.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/f/sub/subsub/subsubsub/ocean-born.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/g/sub/huerta.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/g/sub/smopple.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/g/triple-throw.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/g/unexciting.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/h/.sub/Ellston.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/h/.sub/mine-run.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/h/sub/chromaticness.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/h/sub/noninhabitancy.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/i/.sub/Auer.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/i/.sub/hexachlorocyclohexane.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/i/sub/subsub/Minnnie.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/i/sub/subsub/papsquidder.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/i/sub/subsub/subsubsub/Croghan.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/i/sub/subsub/subsubsub/stacc..txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/j/sub/Hamon.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/j/sub/subsub/subsubsub/anapaestic.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/j/sub/subsub/subsubsub/castlelike.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/j/sub/topsy-turvydom.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/k/coembody.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/k/unvoweled.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/l/.sub/galliots.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/l/.sub/minning.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/l/sub/subsub/misidentification.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/l/sub/subsub/palling.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/l/torpifying.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/l/unmarring.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/m/sub/cache.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/m/sub/ribbon-marked.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyglob/pasteups.txt ================================================ text ================================================ FILE: tests/conformance/testdata/copyrename/Dockerfile ================================================ FROM quay.io/libpod/centos:7 COPY file1 /usr/bin/file2 RUN ls -al /usr/bin/file2 && ! ls -al /usr/bin/file1 ================================================ FILE: tests/conformance/testdata/copyrename/file1 ================================================ #!/usr/bin/env bash exit 0 ================================================ FILE: tests/conformance/testdata/copysymlink/Dockerfile ================================================ FROM quay.io/libpod/centos:7 COPY file-link.tar.gz / ================================================ FILE: tests/conformance/testdata/copysymlink/Dockerfile2 ================================================ FROM quay.io/libpod/centos:7 COPY file.tar.gz / RUN ln -s file.tar.gz file-link.tar.gz RUN ls -l /file-link.tar.gz FROM quay.io/libpod/centos:7 COPY --from=0 /file-link.tar.gz / RUN ls -l /file-link.tar.gz ================================================ FILE: tests/conformance/testdata/dir/Dockerfile ================================================ FROM mirror.gcr.io/busybox COPY . / COPY . dir COPY subdir/ test/ ================================================ FILE: tests/conformance/testdata/dir/file ================================================ ================================================ FILE: tests/conformance/testdata/dir/subdir/file2 ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/alternating/.dockerignore ================================================ ** !subdir subdir/subdir1 !subdir/subdir1/subdir2 subdir/subdir1/subdir2/subdir3 !subdir/subdir1/subdir2/subdir3/subdir4 subdir/subdir1/subdir2/subdir3/subdir4/subdir5 !subdir/subdir1/subdir2/subdir3/subdir4/subdir5/file ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/alternating/Dockerfile ================================================ FROM scratch ADD subdir / ADD subdir/subdir1 / ADD subdir/subdir1/subdir2 / ADD subdir/subdir1/subdir2/subdir3 / ADD subdir/subdir1/subdir2/subdir3/subdir4 / ADD subdir/subdir1/subdir2/subdir3/subdir4/subdir5 / ADD subdir/subdir1/subdir2/subdir3/subdir4/subdir5/file / ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/alternating/subdir/subdir1/subdir2/subdir3/subdir4/subdir5/file ================================================ Hi, I'm a file. ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/alternating-nothing/.dockerignore ================================================ ** !subdir subdir/subdir1 !subdir/subdir1/subdir2 subdir/subdir1/subdir2/subdir3 !subdir/subdir1/subdir2/subdir3/subdir4 subdir/subdir1/subdir2/subdir3/subdir4/subdir5 ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/alternating-nothing/Dockerfile ================================================ FROM scratch ADD subdir / ADD subdir/subdir1 / ADD subdir/subdir1/subdir2 / ADD subdir/subdir1/subdir2/subdir3 / ADD subdir/subdir1/subdir2/subdir3/subdir4 / ADD subdir/subdir1/subdir2/subdir3/subdir4/subdir5 / ADD subdir/subdir1/subdir2/subdir3/subdir4/subdir5/file / ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/alternating-nothing/subdir/subdir1/subdir2/subdir3/subdir4/subdir5/file ================================================ Hi, I'm a file. ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/alternating-other/.dockerignore ================================================ ** !subdir subdir/subdir1 !subdir/subdir1/subdir2 subdir/subdir1/subdir2/subdir3 !subdir/subdir1/subdir2/subdir3/subdir4 subdir/subdir1/subdir2/subdir3/subdir4/subdir5 !subdir/subdir1/subdir2/subdir3/subdir4/subdir5/file ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/alternating-other/Dockerfile ================================================ FROM scratch ADD subdir / ADD subdir/subdir1 / ADD subdir/subdir1/subdir2 / ADD subdir/subdir1/subdir2/subdir3 / ADD subdir/subdir1/subdir2/subdir3/subdir4 / ADD subdir/subdir1/subdir2/subdir3/subdir4/subdir5 / ADD subdir/subdir1/subdir2/subdir3/subdir4/subdir5/file / ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/alternating-other/subdir/subdir1/subdir2/subdir3/subdir4/subdir5/file2 ================================================ Hi, I'm a file. ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/nothing-dot/.dockerignore ================================================ ** ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/nothing-dot/Dockerfile ================================================ FROM mirror.gcr.io/busybox RUN touch -t @1485449953 /file FROM scratch COPY --from=0 /file / ADD . . ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/nothing-slash/.dockerignore ================================================ ** ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/nothing-slash/Dockerfile ================================================ FROM mirror.gcr.io/busybox RUN touch -t @1485449953 /file FROM scratch COPY --from=0 /file / ADD / / ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/subdir-file/.dockerignore ================================================ ** !folder1/file1 ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/subdir-file/Dockerfile ================================================ FROM scratch ADD folder1 /f1 ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/subdir-file/folder1/file1 ================================================ Hi, I'm file 1. ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/subdir-file/folder1/file2 ================================================ Hi, I'm file 2. ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/subdir-nofile/.dockerignore ================================================ ** !folder1/file ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/subdir-nofile/Dockerfile ================================================ FROM scratch ADD folder1 /f1 ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/subdir-nofile/folder1/file1 ================================================ Hi, I'm file 1. ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/subdir-nofile/folder1/file2 ================================================ Hi, I'm file 2. ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/subsubdir-file/.dockerignore ================================================ ** !folder/subfolder/file ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/subsubdir-file/Dockerfile ================================================ FROM mirror.gcr.io/busybox RUN touch -t @1485449953 /file FROM scratch COPY --from=0 /file / ADD . . ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/subsubdir-file/folder/subfolder/file ================================================ I'm a file. ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/subsubdir-nofile/.dockerignore ================================================ ** !folder/subfolder/file ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/subsubdir-nofile/Dockerfile ================================================ FROM mirror.gcr.io/busybox RUN touch -t @1485449953 /file FROM scratch COPY --from=0 /file / ADD . . ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/subsubdir-nofile/folder/file ================================================ I'm a file. ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/subsubdir-nosubdir/.dockerignore ================================================ ** !folder/subfolder/file ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/subsubdir-nosubdir/Dockerfile ================================================ FROM mirror.gcr.io/busybox RUN touch -t @1485449953 /file FROM scratch COPY --from=0 /file / ADD . . ================================================ FILE: tests/conformance/testdata/dockerignore/allowlist/subsubdir-nosubdir/folder/file ================================================ Hi, I'm a file ================================================ FILE: tests/conformance/testdata/dockerignore/empty/.dockerignore ================================================ # Functionally empty. ================================================ FILE: tests/conformance/testdata/dockerignore/empty/Dockerfile ================================================ FROM scratch COPY . subdir/ ================================================ FILE: tests/conformance/testdata/dockerignore/empty/test1.txt ================================================ test1 ================================================ FILE: tests/conformance/testdata/dockerignore/exceptions-skip/.dockerignore ================================================ volume/ !**/oneline.txt ================================================ FILE: tests/conformance/testdata/dockerignore/exceptions-skip/Dockerfile ================================================ FROM mirror.gcr.io/alpine COPY ./ ./ ================================================ FILE: tests/conformance/testdata/dockerignore/exceptions-skip/volume/data/oneline.txt ================================================ one line of text ================================================ FILE: tests/conformance/testdata/dockerignore/exceptions-weirdness-1/.dockerignore ================================================ subdir !*/sub1* ================================================ FILE: tests/conformance/testdata/dockerignore/exceptions-weirdness-1/Dockerfile ================================================ FROM mirror.gcr.io/alpine COPY ./ /newdir ================================================ FILE: tests/conformance/testdata/dockerignore/exceptions-weirdness-1/subdir/sub1.txt ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/exceptions-weirdness-1/subdir/sub2.txt ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/exceptions-weirdness-1/subdir/sub3.txt ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/exceptions-weirdness-2/.dockerignore ================================================ subdir !*/sub1* !subdir/sub3* ================================================ FILE: tests/conformance/testdata/dockerignore/exceptions-weirdness-2/Dockerfile ================================================ FROM mirror.gcr.io/alpine COPY ./ /newdir ================================================ FILE: tests/conformance/testdata/dockerignore/exceptions-weirdness-2/subdir/sub1.txt ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/exceptions-weirdness-2/subdir/sub2.txt ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/exceptions-weirdness-2/subdir/sub3.txt ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/integration1/.dockerignore ================================================ # comment * test* !test2* subdir !*/sub1* ================================================ FILE: tests/conformance/testdata/dockerignore/integration1/Dockerfile ================================================ FROM mirror.gcr.io/alpine COPY ./ ./ COPY subdir ./ ================================================ FILE: tests/conformance/testdata/dockerignore/integration1/subdir/sub1.txt ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/integration1/subdir/sub2.txt ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/integration1/test1.txt ================================================ test1 failed ================================================ FILE: tests/conformance/testdata/dockerignore/integration1/test2.txt ================================================ test2 failed ================================================ FILE: tests/conformance/testdata/dockerignore/integration2/.dockerignore ================================================ unmatched ================================================ FILE: tests/conformance/testdata/dockerignore/integration2/Dockerfile ================================================ FROM scratch COPY . . ================================================ FILE: tests/conformance/testdata/dockerignore/integration2/subdir/sub1.txt ================================================ sub1 ================================================ FILE: tests/conformance/testdata/dockerignore/integration2/subdir/subsubdir/subsub1.txt ================================================ subsub1 ================================================ FILE: tests/conformance/testdata/dockerignore/integration3/.dockerignore ================================================ # comment * !test* !src **/*.in src/etc *.md !README*.md README-secret.md test1.txt ================================================ FILE: tests/conformance/testdata/dockerignore/integration3/BUILD.md ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/integration3/COPYRIGHT ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/integration3/Dockerfile ================================================ FROM mirror.gcr.io/busybox COPY . /upload/ COPY src /upload/src2/ COPY test1.txt /upload/test1.txt RUN echo "CUT HERE"; /bin/find /upload | LANG=en_US.UTF-8 sort; echo "CUT HERE" ================================================ FILE: tests/conformance/testdata/dockerignore/integration3/LICENSE ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/integration3/README-secret.md ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/integration3/README.md ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/integration3/manifest ================================================ /upload /upload/README.md /upload/src /upload/src/Makefile /upload/src/cmd /upload/src/cmd/Makefile /upload/src/lib /upload/src/lib/Makefile /upload/src2 /upload/src2/Makefile /upload/src2/cmd /upload/src2/cmd/Makefile /upload/src2/lib /upload/src2/lib/Makefile /upload/test2.txt /upload/test3.txt ================================================ FILE: tests/conformance/testdata/dockerignore/integration3/src/Makefile ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/integration3/src/cmd/Makefile ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/integration3/src/cmd/main.in ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/integration3/src/etc/foo.conf ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/integration3/src/etc/foo.conf.d/dropin.conf ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/integration3/src/lib/Makefile ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/integration3/src/lib/framework.in ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/integration3/test1.txt ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/integration3/test2.txt ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/integration3/test3.txt ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/minimal_test/.dockerignore ================================================ stuff/huge/* !stuff/huge/usr/bin/* ================================================ FILE: tests/conformance/testdata/dockerignore/minimal_test/Dockerfile ================================================ FROM scratch COPY stuff /tmp/stuff ================================================ FILE: tests/conformance/testdata/dockerignore/minimal_test/stuff/huge/usr/bin/file1 ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/minimal_test/stuff/huge/usr/bin/file2 ================================================ ================================================ FILE: tests/conformance/testdata/dockerignore/populated/.dotfile-a.txt ================================================ dotfile-a ================================================ FILE: tests/conformance/testdata/dockerignore/populated/file-a.txt ================================================ file-a ================================================ FILE: tests/conformance/testdata/dockerignore/populated/file-b.txt ================================================ file-b ================================================ FILE: tests/conformance/testdata/dockerignore/populated/file-c.txt ================================================ file-c ================================================ FILE: tests/conformance/testdata/dockerignore/populated/subdir-b/.dotfile-b.txt ================================================ dotfile-b ================================================ FILE: tests/conformance/testdata/dockerignore/populated/subdir-e/file-n.txt ================================================ file-n ================================================ FILE: tests/conformance/testdata/dockerignore/populated/subdir-e/subdir-f/file-o.txt ================================================ file-o ================================================ FILE: tests/conformance/testdata/env/precedence/Dockerfile ================================================ # syntax=mirror.gcr.io/docker/dockerfile:1.4 FROM mirror.gcr.io/busybox ENV a=b ENV c=d RUN echo E=$E G=$G ENV E=E G=G RUN echo E=$E G=$G ================================================ FILE: tests/conformance/testdata/header-builtin/Dockerfile ================================================ # Pull architecture-specific ubuntu images from cached registry FROM --platform=linux/amd64 quay.io/libpod/ubuntu:latest AS amd64 FROM --platform=linux/arm64 quay.io/libpod/ubuntu:latest AS arm64 # run "file" against both shared libraries FROM quay.io/libpod/ubuntu:latest AS native COPY --from=amd64 /lib/x86_64-linux-gnu/libc.so.6 /libc-amd64 COPY --from=arm64 /lib/aarch64-linux-gnu/libc.so.6 /libc-arm64 RUN apt-get update && apt-get install -y file && apt-get clean RUN file /libc-* | tee /libc-types.txt && touch -d @0 /libc-types.txt # expect them to have different target architectures listed in their ELF headers FROM quay.io/libpod/ubuntu:latest COPY --from=native /libc-types.txt / ================================================ FILE: tests/conformance/testdata/heredoc/Dockerfile.heredoc_copy ================================================ # syntax=mirror.gcr.io/docker/dockerfile:1.3-labs FROM mirror.gcr.io/busybox as one RUN echo helloworld > image_file FROM mirror.gcr.io/busybox RUN echo hello # copy two heredoc and one from context COPY <=:", for example: # "nofile=1024:2048" # See setrlimit(2) for a list of resource names. # Any limit not specified here will be inherited from the process launching the # container engine. # Ulimits has limits for non privileged container engines. # default_ulimits = [ "nofile=500:500", ] # Environment variable list for the conmon process; used for passing necessary # environment variables to conmon or the runtime. # env = [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "foo=bar", ] # container engines use container separation using MAC(SELinux) labeling. # Flag is ignored on label disabled systems. # label = true # Size of /dev/shm. Specified as . # Unit is optional, values: # b (bytes), k (kilobytes), m (megabytes), or g (gigabytes). # If the unit is omitted, the system uses bytes. # shm_size = "200k" # List of additional devices. Specified as # "::", for example: # "/dev/sdc:/dev/xvdc:rwm". # If it is empty or commented out, only the default devices will be used # devices = [ ] # List of default capabilities for containers. If it is empty or commented out, # the default capabilities defined in the container engine will be added. # default_capabilities = [ "AUDIT_WRITE", "CHOWN", "DAC_OVERRIDE", "FOWNER", "FSETID", "KILL", "MKNOD", "NET_BIND_SERVICE", "NET_RAW", "SETFCAP", "SETGID", "SETPCAP", "SETUID", "SYS_CHROOT", ] default_sysctls = [ "net.ipv4.ping_group_range=0 0", ] ================================================ FILE: tests/containers_conf.bats ================================================ #!/usr/bin/env bats load helpers @test "containers.conf selinux test" { if ! which selinuxenabled > /dev/null 2> /dev/null ; then skip "No selinuxenabled executable" elif ! selinuxenabled ; then skip "selinux is disabled" fi _prefetch alpine cid=$(buildah from $WITH_POLICY_JSON alpine) run_buildah --log-level=error run $cid sh -c "cat /proc/self/attr/current | grep container_t" run_buildah rm $cid sed "s/^label = true/label = false/g" ${TEST_SOURCES}/containers.conf > ${TEST_SCRATCH_DIR}/containers.conf cid=$(buildah from $WITH_POLICY_JSON alpine) CONTAINERS_CONF=${TEST_SCRATCH_DIR}/containers.conf run_buildah 1 --log-level=error run $cid sh -c "cat /proc/self/attr/current | grep container_t" } @test "containers.conf ulimit test" { if test "$BUILDAH_ISOLATION" = "chroot" -o "$BUILDAH_ISOLATION" = "rootless" ; then skip "BUILDAH_ISOLATION = $BUILDAH_ISOLATION" fi _prefetch alpine cid=$(buildah from $WITH_POLICY_JSON alpine) run_buildah --log-level=error run $cid awk '/open files/{print $4}' /proc/self/limits expect_output "500" "limits: open files (w/file limit)" cid=$(buildah from --ulimit nofile=300:400 $WITH_POLICY_JSON alpine) run_buildah --log-level=error run $cid awk '/open files/{print $4}' /proc/self/limits expect_output "300" "limits: open files (w/file limit)" } @test "containers.conf additional devices test" { skip_if_rootless_environment if test "$BUILDAH_ISOLATION" = "chroot" -o "$BUILDAH_ISOLATION" = "rootless" ; then skip "BUILDAH_ISOLATION = $BUILDAH_ISOLATION" fi _prefetch alpine cid=$(buildah from $WITH_POLICY_JSON alpine) CONTAINERS_CONF=$CONTAINERS_CONF run_buildah 1 --log-level=error run $cid ls /dev/foo1 run_buildah rm $cid sed '/^devices.*/a "\/dev\/foo:\/dev\/foo1:rmw",' ${TEST_SOURCES}/containers.conf > ${TEST_SCRATCH_DIR}/containers.conf rm -f /dev/foo; mknod /dev/foo c 1 1 CONTAINERS_CONF=${TEST_SCRATCH_DIR}/containers.conf run_buildah from --quiet $WITH_POLICY_JSON alpine cid="$output" CONTAINERS_CONF=${TEST_SCRATCH_DIR}/containers.conf run_buildah --log-level=error run $cid ls /dev/foo1 rm -f /dev/foo } @test "containers.conf capabilities test" { _prefetch alpine run_buildah from --quiet $WITH_POLICY_JSON alpine cid="$output" run_buildah --log-level=error run $cid sh -c 'grep CapEff /proc/self/status | cut -f2' CapEff="$output" expect_output "00000000a80425fb" run_buildah rm $cid sed "/AUDIT_WRITE/d" ${TEST_SOURCES}/containers.conf > ${TEST_SCRATCH_DIR}/containers.conf CONTAINERS_CONF=${TEST_SCRATCH_DIR}/containers.conf run_buildah from --quiet $WITH_POLICY_JSON alpine cid="$output" CONTAINERS_CONF=${TEST_SCRATCH_DIR}/containers.conf run_buildah --log-level=error run $cid sh -c 'grep CapEff /proc/self/status | cut -f2' run_buildah rm $cid test "$output" != "$CapEff" } @test "containers.conf /dev/shm test" { if test "$BUILDAH_ISOLATION" = "chroot" -o "$BUILDAH_ISOLATION" = "rootless" ; then skip "BUILDAH_ISOLATION = $BUILDAH_ISOLATION" fi _prefetch alpine run_buildah from --quiet $WITH_POLICY_JSON alpine cid="$output" run_buildah --log-level=error run $cid sh -c 'df /dev/shm | awk '\''/shm/{print $4}'\''' # On architectures with 64k page size (e.g. ppc64le), the # kernel rounds shm sizes up to the nearest page boundary. pagesize="$(getconf PAGESIZE)" expected=200 if [ "$pagesize" -eq 65536 ]; then expected=256 fi expect_output "$expected" } @test "containers.conf custom runtime" { if test "$BUILDAH_ISOLATION" = "chroot" -o "$BUILDAH_ISOLATION" = "rootless" ; then skip "BUILDAH_ISOLATION = $BUILDAH_ISOLATION" fi test -x /usr/bin/crun || skip "/usr/bin/crun doesn't exist" ln -s /usr/bin/crun ${TEST_SCRATCH_DIR}/runtime cat >${TEST_SCRATCH_DIR}/containers.conf << EOF [engine] runtime = "nonstandard_runtime_name" [engine.runtimes] nonstandard_runtime_name = ["${TEST_SCRATCH_DIR}/runtime"] EOF _prefetch alpine cid=$(buildah from $WITH_POLICY_JSON alpine) CONTAINERS_CONF=${TEST_SCRATCH_DIR}/containers.conf run_buildah --log-level=error run $cid true } @test "containers.conf network sysctls" { if test "$BUILDAH_ISOLATION" = "chroot" ; then skip "BUILDAH_ISOLATION = $BUILDAH_ISOLATION" fi cat >${TEST_SCRATCH_DIR}/containers.conf << EOF [containers] default_sysctls = [ "net.ipv4.tcp_timestamps=123" ] EOF _prefetch alpine cat >${TEST_SCRATCH_DIR}/Containerfile << _EOF FROM alpine RUN echo -n "timestamp="; cat /proc/sys/net/ipv4/tcp_timestamps RUN echo -n "ping_group_range="; cat /proc/sys/net/ipv4/ping_group_range _EOF run_buildah build ${TEST_SCRATCH_DIR} expect_output --substring "timestamp=1" expect_output --substring "ping_group_range=0.*0" CONTAINERS_CONF=${TEST_SCRATCH_DIR}/containers.conf run_buildah build ${TEST_SCRATCH_DIR} expect_output --substring "timestamp=123" if is_rootless ; then expect_output --substring "ping_group_range=65534.*65534" else expect_output --substring "ping_group_range=1.*0" fi } @test "containers.conf retry" { cat >${TEST_SCRATCH_DIR}/containers.conf << EOF [engine] retry=10 retry_delay="5s" EOF CONTAINERS_CONF=${TEST_SCRATCH_DIR}/containers.conf run_buildah build --help expect_output --substring "retry.*\(default 10\)" expect_output --substring "retry-delay.*\(default \"5s\"\)" CONTAINERS_CONF=${TEST_SCRATCH_DIR}/containers.conf run_buildah push --help expect_output --substring "retry.*\(default 10\)" expect_output --substring "retry-delay.*\(default \"5s\"\)" CONTAINERS_CONF=${TEST_SCRATCH_DIR}/containers.conf run_buildah pull --help expect_output --substring "retry.*\(default 10\)" expect_output --substring "retry-delay.*\(default \"5s\"\)" CONTAINERS_CONF=${TEST_SCRATCH_DIR}/containers.conf run_buildah from --help expect_output --substring "retry.*\(default 10\)" expect_output --substring "retry-delay.*\(default \"5s\"\)" CONTAINERS_CONF=${TEST_SCRATCH_DIR}/containers.conf run_buildah manifest push --help expect_output --substring "retry.*\(default 10\)" expect_output --substring "retry-delay.*\(default \"5s\"\)" } ================================================ FILE: tests/copy/copy.go ================================================ package main import ( "context" "errors" "fmt" "os" "strings" "github.com/containers/buildah" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "go.podman.io/common/libnetwork/network" "go.podman.io/common/pkg/config" cp "go.podman.io/image/v5/copy" "go.podman.io/image/v5/manifest" "go.podman.io/image/v5/pkg/compression" "go.podman.io/image/v5/signature" imageStorage "go.podman.io/image/v5/storage" "go.podman.io/image/v5/transports/alltransports" "go.podman.io/image/v5/types" "go.podman.io/storage" "go.podman.io/storage/pkg/unshare" ) func main() { var storeOptions storage.StoreOptions var systemContext types.SystemContext var logLevel string var maxParallelDownloads uint var compressionFormat string var manifestFormat string compressionLevel := -1 if buildah.InitReexec() { return } unshare.MaybeReexecUsingUserNamespace(false) storeOptions, err := storage.DefaultStoreOptions() if err != nil { storeOptions = storage.StoreOptions{} } rootCmd := &cobra.Command{ Use: "copy [flags] source destination", Long: "copies an image", RunE: func(cmd *cobra.Command, args []string) error { if err := cobra.ExactArgs(2)(cmd, args); err != nil { return err } if compressionLevel != -1 { systemContext.CompressionLevel = &compressionLevel } if compressionFormat != "" { alg, err := compression.AlgorithmByName(compressionFormat) if err != nil { return err } systemContext.CompressionFormat = &alg } switch strings.ToLower(manifestFormat) { case "oci": manifestFormat = v1.MediaTypeImageManifest case "docker", "dockerv2s2": manifestFormat = manifest.DockerV2Schema2MediaType } level, err := logrus.ParseLevel(logLevel) if err != nil { return err } logrus.SetLevel(level) store, err := storage.GetStore(storeOptions) if err != nil { return err } imageStorage.Transport.SetStore(store) conf, err := config.Default() if err != nil { return err } _, _, err = network.NetworkBackend(store, conf, false) if err != nil { return err } if len(args) < 1 { return errors.New("no source name provided") } src, err := alltransports.ParseImageName(args[0]) if err != nil { return fmt.Errorf("parsing source name: %w", err) } if len(args) < 1 { return errors.New("no destination name provided") } dest, err := alltransports.ParseImageName(args[1]) if err != nil { return fmt.Errorf("parsing destination name: %w", err) } policy, err := signature.DefaultPolicy(&systemContext) if err != nil { return fmt.Errorf("reading signature policy: %w", err) } policyContext, err := signature.NewPolicyContext(policy) if err != nil { return fmt.Errorf("creating new signature policy context: %w", err) } defer func() { if err := policyContext.Destroy(); err != nil { logrus.Error(fmt.Errorf("destroying signature policy context: %w", err)) } }() options := cp.Options{ ReportWriter: os.Stdout, SourceCtx: &systemContext, DestinationCtx: &systemContext, MaxParallelDownloads: maxParallelDownloads, ForceManifestMIMEType: manifestFormat, } if _, err = cp.Image(context.TODO(), policyContext, dest, src, &options); err != nil { return err } defer func() { _, err := store.Shutdown(false) if err != nil { logrus.Error(err) } }() return nil }, } rootCmd.PersistentFlags().StringVar(&storeOptions.GraphRoot, "root", "", "storage root") rootCmd.PersistentFlags().StringVar(&storeOptions.RunRoot, "runroot", "", "runtime root") rootCmd.PersistentFlags().StringVar(&storeOptions.ImageStore, "imagestore", "", "storage imagestore") rootCmd.PersistentFlags().BoolVar(&storeOptions.TransientStore, "transient-store", false, "store some information in transient storage") rootCmd.PersistentFlags().StringVar(&storeOptions.GraphDriverName, "storage-driver", "", "storage driver") rootCmd.PersistentFlags().StringSliceVar(&storeOptions.GraphDriverOptions, "storage-opt", nil, "storage option") rootCmd.PersistentFlags().StringVar(&systemContext.SystemRegistriesConfPath, "registries-conf", "", "location of registries.conf") rootCmd.PersistentFlags().StringVar(&systemContext.SystemRegistriesConfDirPath, "registries-conf-dir", "", "location of registries.d") rootCmd.PersistentFlags().StringVar(&systemContext.SignaturePolicyPath, "signature-policy", "", "`pathname` of signature policy file") rootCmd.PersistentFlags().StringVar(&systemContext.UserShortNameAliasConfPath, "short-name-alias-conf", "", "`pathname` of short name alias cache file (not usually used)") rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "warn", "logging level") rootCmd.PersistentFlags().UintVar(&maxParallelDownloads, "max-parallel-downloads", 0, "maximum `number` of blobs to copy at once") rootCmd.PersistentFlags().StringVar(&manifestFormat, "format", "", "image manifest type") rootCmd.PersistentFlags().BoolVar(&systemContext.DirForceCompress, "dest-compress", false, "force compression of layers for dir: destinations") rootCmd.PersistentFlags().BoolVar(&systemContext.DirForceDecompress, "dest-decompress", false, "force decompression of layers for dir: destinations") rootCmd.PersistentFlags().StringVar(&compressionFormat, "dest-compress-format", "", "compression type") rootCmd.PersistentFlags().IntVar(&compressionLevel, "dest-compress-level", 0, "compression level") if err := rootCmd.Execute(); err != nil { os.Exit(1) } } ================================================ FILE: tests/copy.bats ================================================ #!/usr/bin/env bats load helpers @test "copy-flags-order-verification" { run_buildah 125 copy container1 -q /tmp/container1 check_options_flag_err "-q" run_buildah 125 copy container1 --chown /tmp/container1 --quiet check_options_flag_err "--chown" run_buildah 125 copy container1 /tmp/container1 --quiet check_options_flag_err "--quiet" } @test "copy-local-multiple" { createrandom ${TEST_SCRATCH_DIR}/randomfile createrandom ${TEST_SCRATCH_DIR}/other-randomfile createrandom ${TEST_SCRATCH_DIR}/third-randomfile run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah mount $cid root=$output run_buildah config --workingdir / $cid # copy ${TEST_SCRATCH_DIR}/randomfile to a file of the same name in the container's working directory run_buildah copy --retry 4 --retry-delay 4s $cid ${TEST_SCRATCH_DIR}/randomfile # copy ${TEST_SCRATCH_DIR}/other-randomfile and ${TEST_SCRATCH_DIR}/third-randomfile to a new directory named ${TEST_SCRATCH_DIR}/randomfile in the container run_buildah copy $cid ${TEST_SCRATCH_DIR}/other-randomfile ${TEST_SCRATCH_DIR}/third-randomfile ${TEST_SCRATCH_DIR}/randomfile # try to copy ${TEST_SCRATCH_DIR}/other-randomfile and ${TEST_SCRATCH_DIR}/third-randomfile to a /randomfile, which already exists and is a file run_buildah 125 copy $cid ${TEST_SCRATCH_DIR}/other-randomfile ${TEST_SCRATCH_DIR}/third-randomfile /randomfile # copy ${TEST_SCRATCH_DIR}/other-randomfile and ${TEST_SCRATCH_DIR}/third-randomfile to previously-created directory named ${TEST_SCRATCH_DIR}/randomfile in the container run_buildah copy $cid ${TEST_SCRATCH_DIR}/other-randomfile ${TEST_SCRATCH_DIR}/third-randomfile ${TEST_SCRATCH_DIR}/randomfile run_buildah rm $cid _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah mount $cid root=$output run_buildah config --workingdir / $cid run_buildah copy $cid ${TEST_SCRATCH_DIR}/randomfile run_buildah copy $cid ${TEST_SCRATCH_DIR}/other-randomfile ${TEST_SCRATCH_DIR}/third-randomfile ${TEST_SCRATCH_DIR}/randomfile /etc run_buildah rm $cid run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah mount $cid root=$output run_buildah config --workingdir / $cid run_buildah copy $cid "${TEST_SCRATCH_DIR}/*randomfile" /etc (cd ${TEST_SCRATCH_DIR}; for i in *randomfile; do cmp $i ${root}/etc/$i; done) } @test "copy-local-plain" { createrandom ${TEST_SCRATCH_DIR}/randomfile createrandom ${TEST_SCRATCH_DIR}/other-randomfile createrandom ${TEST_SCRATCH_DIR}/third-randomfile run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah mount $cid root=$output run_buildah config --workingdir / $cid run_buildah copy $cid ${TEST_SCRATCH_DIR}/randomfile run_buildah copy $cid ${TEST_SCRATCH_DIR}/other-randomfile run_buildah unmount $cid run_buildah commit $WITH_POLICY_JSON $cid containers-storage:new-image run_buildah rm $cid run_buildah from --quiet $WITH_POLICY_JSON new-image newcid=$output run_buildah mount $newcid newroot=$output test -s $newroot/randomfile cmp ${TEST_SCRATCH_DIR}/randomfile $newroot/randomfile test -s $newroot/other-randomfile cmp ${TEST_SCRATCH_DIR}/other-randomfile $newroot/other-randomfile } @test "copy-local-subdirectory" { mkdir -p ${TEST_SCRATCH_DIR}/subdir createrandom ${TEST_SCRATCH_DIR}/subdir/randomfile createrandom ${TEST_SCRATCH_DIR}/subdir/other-randomfile run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah config --workingdir /container-subdir $cid run_buildah copy $cid ${TEST_SCRATCH_DIR}/subdir run_buildah mount $cid root=$output test -s $root/container-subdir/randomfile cmp ${TEST_SCRATCH_DIR}/subdir/randomfile $root/container-subdir/randomfile test -s $root/container-subdir/other-randomfile cmp ${TEST_SCRATCH_DIR}/subdir/other-randomfile $root/container-subdir/other-randomfile run_buildah copy $cid ${TEST_SCRATCH_DIR}/subdir /other-subdir test -s $root/other-subdir/randomfile cmp ${TEST_SCRATCH_DIR}/subdir/randomfile $root/other-subdir/randomfile test -s $root/other-subdir/other-randomfile cmp ${TEST_SCRATCH_DIR}/subdir/other-randomfile $root/other-subdir/other-randomfile } @test "copy-local-force-directory" { createrandom ${TEST_SCRATCH_DIR}/randomfile run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah config --workingdir / $cid run_buildah copy $cid ${TEST_SCRATCH_DIR}/randomfile /randomfile run_buildah mount $cid root=$output test -s $root/randomfile cmp ${TEST_SCRATCH_DIR}/randomfile $root/randomfile run_buildah rm $cid run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah config --workingdir / $cid run_buildah copy $cid ${TEST_SCRATCH_DIR}/randomfile /randomsubdir/ run_buildah mount $cid root=$output test -s $root/randomsubdir/randomfile cmp ${TEST_SCRATCH_DIR}/randomfile $root/randomsubdir/randomfile } @test "copy-url-mtime" { # Create a file with random content and a non-now timestamp (so we can # can trust that buildah correctly set mtime on copy) createrandom ${TEST_SCRATCH_DIR}/randomfile touch -t 201910310123.45 ${TEST_SCRATCH_DIR}/randomfile run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah config --workingdir / $cid starthttpd ${TEST_SCRATCH_DIR} run_buildah copy $cid http://0.0.0.0:${HTTP_SERVER_PORT}/randomfile /urlfile stophttpd run_buildah mount $cid root=$output test -s $root/urlfile cmp ${TEST_SCRATCH_DIR}/randomfile $root/urlfile # Compare timestamps. Display them in human-readable form, so if there's # a mismatch it will be shown in the test log. mtime_randomfile=$(stat --format %y ${TEST_SCRATCH_DIR}/randomfile) mtime_urlfile=$(stat --format %y $root/urlfile) expect_output --from="$mtime_randomfile" "$mtime_urlfile" "mtime[randomfile] == mtime[urlfile]" } @test "copy --chown" { mkdir -p ${TEST_SCRATCH_DIR}/subdir mkdir -p ${TEST_SCRATCH_DIR}/other-subdir createrandom ${TEST_SCRATCH_DIR}/subdir/randomfile createrandom ${TEST_SCRATCH_DIR}/subdir/other-randomfile createrandom ${TEST_SCRATCH_DIR}/randomfile createrandom ${TEST_SCRATCH_DIR}/other-subdir/randomfile createrandom ${TEST_SCRATCH_DIR}/other-subdir/other-randomfile _prefetch alpine run_buildah from --quiet $WITH_POLICY_JSON alpine cid=$output run_buildah config --workingdir / $cid run_buildah copy --chown 1:1 $cid ${TEST_SCRATCH_DIR}/randomfile run_buildah copy --chown root:1 $cid ${TEST_SCRATCH_DIR}/randomfile /randomfile2 run_buildah copy --chown nobody $cid ${TEST_SCRATCH_DIR}/randomfile /randomfile3 run_buildah copy --chown nobody:root $cid ${TEST_SCRATCH_DIR}/subdir /subdir run_buildah run $cid stat -c "%u:%g" /randomfile expect_output "1:1" "stat ug /randomfile" run_buildah run $cid stat -c "%U:%g" /randomfile2 expect_output "root:1" "stat Ug /randomfile2" run_buildah run $cid stat -c "%U" /randomfile3 expect_output "nobody" "stat U /randomfile3" for i in randomfile other-randomfile ; do run_buildah run $cid stat -c "%U:%G" /subdir/$i expect_output "nobody:root" "stat UG /subdir/$i" done # subdir will have been implicitly created, and the --chown should have had an effect run_buildah run $cid stat -c "%U:%G" /subdir expect_output "nobody:root" "stat UG /subdir" run_buildah copy --chown root:root $cid ${TEST_SCRATCH_DIR}/other-subdir /subdir for i in randomfile other-randomfile ; do run_buildah run $cid stat -c "%U:%G" /subdir/$i expect_output "root:root" "stat UG /subdir/$i (after chown)" done # subdir itself will have not been copied (the destination directory was created implicitly), so its permissions should not have changed run_buildah run $cid stat -c "%U:%G" /subdir expect_output "nobody:root" "stat UG /subdir" } @test "copy --chmod" { mkdir -p ${TEST_SCRATCH_DIR}/subdir mkdir -p ${TEST_SCRATCH_DIR}/other-subdir createrandom ${TEST_SCRATCH_DIR}/subdir/randomfile createrandom ${TEST_SCRATCH_DIR}/subdir/other-randomfile createrandom ${TEST_SCRATCH_DIR}/randomfile createrandom ${TEST_SCRATCH_DIR}/other-subdir/randomfile createrandom ${TEST_SCRATCH_DIR}/other-subdir/other-randomfile _prefetch alpine run_buildah from --quiet $WITH_POLICY_JSON alpine cid=$output run_buildah config --workingdir / $cid run_buildah copy --chmod 777 $cid ${TEST_SCRATCH_DIR}/randomfile run_buildah copy --chmod 700 $cid ${TEST_SCRATCH_DIR}/randomfile /randomfile2 run_buildah copy --chmod 755 $cid ${TEST_SCRATCH_DIR}/randomfile /randomfile3 run_buildah copy --chmod 660 $cid ${TEST_SCRATCH_DIR}/subdir /subdir run_buildah run $cid ls -l /randomfile expect_output --substring rwxrwxrwx run_buildah run $cid ls -l /randomfile2 expect_output --substring rwx------ run_buildah run $cid ls -l /randomfile3 expect_output --substring rwxr-xr-x for i in randomfile other-randomfile ; do run_buildah run $cid ls -l /subdir/$i expect_output --substring rw-rw---- done run_buildah run $cid ls -l /subdir expect_output --substring rw-rw---- run_buildah copy --chmod 600 $cid ${TEST_SCRATCH_DIR}/other-subdir /subdir for i in randomfile other-randomfile ; do run_buildah run $cid ls -l /subdir/$i expect_output --substring rw------- done } @test "copy-symlink" { createrandom ${TEST_SCRATCH_DIR}/randomfile ln -s ${TEST_SCRATCH_DIR}/randomfile ${TEST_SCRATCH_DIR}/link-randomfile run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah mount $cid root=$output run_buildah config --workingdir / $cid run_buildah copy $cid ${TEST_SCRATCH_DIR}/link-randomfile run_buildah unmount $cid run_buildah commit $WITH_POLICY_JSON $cid containers-storage:new-image run_buildah rm $cid run_buildah from --quiet $WITH_POLICY_JSON new-image newcid=$output run_buildah mount $newcid newroot=$output test -s $newroot/link-randomfile test -f $newroot/link-randomfile cmp ${TEST_SCRATCH_DIR}/randomfile $newroot/link-randomfile } @test "ignore-socket" { createrandom ${TEST_SCRATCH_DIR}/randomfile # This seems to be the least-worst way to create a socket: run and kill nc nc -lkU ${TEST_SCRATCH_DIR}/test.socket & nc_pid=$! # This should succeed fairly quickly. We test with a timeout in case of # failure (likely reason: 'nc' not installed.) retries=50 while ! test -e ${TEST_SCRATCH_DIR}/test.socket; do sleep 0.1 retries=$((retries - 1)) if [[ $retries -eq 0 ]]; then die "Timed out waiting for ${TEST_SCRATCH_DIR}/test.socket (is nc installed?)" fi done kill $nc_pid run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah mount $cid root=$output run_buildah config --workingdir / $cid run_buildah unmount $cid run_buildah commit $WITH_POLICY_JSON $cid containers-storage:new-image run_buildah rm $cid run_buildah from --quiet $WITH_POLICY_JSON new-image newcid=$output run_buildah mount $newcid newroot=$output test \! -e $newroot/test.socket } @test "copy-symlink-archive-suffix" { createrandom ${TEST_SCRATCH_DIR}/randomfile.tar.gz ln -s ${TEST_SCRATCH_DIR}/randomfile.tar.gz ${TEST_SCRATCH_DIR}/link-randomfile.tar.gz run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah mount $cid root=$output run_buildah config --workingdir / $cid run_buildah copy $cid ${TEST_SCRATCH_DIR}/link-randomfile.tar.gz run_buildah unmount $cid run_buildah commit $WITH_POLICY_JSON $cid containers-storage:new-image run_buildah rm $cid run_buildah from --quiet $WITH_POLICY_JSON new-image newcid=$output run_buildah mount $newcid newroot=$output test -s $newroot/link-randomfile.tar.gz test -f $newroot/link-randomfile.tar.gz cmp ${TEST_SCRATCH_DIR}/randomfile.tar.gz $newroot/link-randomfile.tar.gz } @test "copy-detect-missing-data" { _prefetch busybox : > ${TEST_SCRATCH_DIR}/Dockerfile echo FROM busybox AS builder >> ${TEST_SCRATCH_DIR}/Dockerfile echo FROM scratch >> ${TEST_SCRATCH_DIR}/Dockerfile echo COPY --from=builder /bin/-no-such-file-error- /usr/bin >> ${TEST_SCRATCH_DIR}/Dockerfile run_buildah 125 build-using-dockerfile $WITH_POLICY_JSON ${TEST_SCRATCH_DIR} expect_output --substring "no such file or directory" } @test "copy --ignorefile" { mytest=${TEST_SCRATCH_DIR}/mytest mkdir -p ${mytest} touch ${mytest}/mystuff touch ${mytest}/source.go mkdir -p ${mytest}/notmystuff touch ${mytest}/notmystuff/notmystuff cat > ${mytest}/.ignore << _EOF *.go .ignore notmystuff _EOF expect=" stuff stuff/mystuff" run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah 125 copy --ignorefile ${mytest}/.ignore $cid ${mytest} /stuff expect_output -- "Error: --ignorefile option requires that you specify a context dir using --contextdir" "container file list" run_buildah copy --contextdir=${mytest} --ignorefile ${mytest}/.ignore $cid ${mytest} /stuff run_buildah mount $cid mnt=$output run find $mnt -printf "%P\n" filelist=$(LC_ALL=C sort <<<"$output") run_buildah umount $cid expect_output --from="$filelist" "$expect" "container file list" } @test "copy --exclude" { mytest=${TEST_SCRATCH_DIR}/mytest mkdir -p ${mytest} touch ${mytest}/mystuff touch ${mytest}/source.go mkdir -p ${mytest}/notmystuff touch ${mytest}/notmystuff/notmystuff expect=" stuff stuff/mystuff" run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah copy --exclude=**/*.go --exclude=.ignore --exclude=**/notmystuff $cid ${mytest} /stuff run_buildah mount $cid mnt=$output run find $mnt -printf "%P\n" filelist=$(LC_ALL=C sort <<<"$output") run_buildah umount $cid expect_output --from="$filelist" "$expect" "container file list" } @test "copy --parents" { mytest=${TEST_SCRATCH_DIR}/mytest mkdir -p ${mytest} mkdir -p ${mytest}/x mkdir -p ${mytest}/y touch ${mytest}/x/a.txt touch ${mytest}/y/a.txt touch ${mytest}/y/b.txt expect_all=" parents parents/x parents/x/a.txt parents/y parents/y/a.txt parents/y/b.txt" expect_a_txt=" parents parents/x parents/x/a.txt parents/y parents/y/a.txt" expect_b_txt=" parents parents/y parents/y/b.txt" expect_only_y_dir=" parents parents/y parents/y/a.txt parents/y/b.txt" # paths | expected tests=" ${mytest}/./*/a.txt | ${expect_a_txt} ${mytest}/./y/* | ${expect_only_y_dir} ${mytest}/./*/b.txt | ${expect_b_txt} ${mytest}/./* | ${expect_all} " local -A paths local -A expected while read paths expected; do run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah copy --parents $cid ${paths} /parents/ run_buildah mount $cid mnt=$output run find $mnt -printf "%P\n" filelist=$(LC_ALL=C sort <<<"$output") run_buildah umount $cid expect_output --from="$filelist" "$expect" "container file list" done < <(parse_table "$tests") } @test "copy-quiet" { createrandom ${TEST_SCRATCH_DIR}/randomfile _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah mount $cid root=$output run_buildah copy --quiet $cid ${TEST_SCRATCH_DIR}/randomfile / expect_output "" cmp ${TEST_SCRATCH_DIR}/randomfile $root/randomfile run_buildah umount $cid run_buildah rm $cid } @test "copy-from-container" { _prefetch busybox createrandom ${TEST_SCRATCH_DIR}/randomfile run_buildah from --quiet $WITH_POLICY_JSON busybox from=$output run_buildah from --quiet $WITH_POLICY_JSON busybox cid=$output run_buildah copy --quiet $from ${TEST_SCRATCH_DIR}/randomfile /tmp/random expect_output "" run_buildah copy --quiet $WITH_POLICY_JSON --from $from $cid /tmp/random /tmp/random # absolute path expect_output "" run_buildah copy --quiet $WITH_POLICY_JSON --from $from $cid tmp/random /tmp/random2 # relative path expect_output "" run_buildah mount $cid croot=$output cmp ${TEST_SCRATCH_DIR}/randomfile ${croot}/tmp/random cmp ${TEST_SCRATCH_DIR}/randomfile ${croot}/tmp/random2 } @test "copy-container-root" { _prefetch busybox createrandom ${TEST_SCRATCH_DIR}/randomfile run_buildah from --quiet $WITH_POLICY_JSON busybox from=$output run_buildah from --quiet $WITH_POLICY_JSON busybox cid=$output run_buildah copy --quiet $from ${TEST_SCRATCH_DIR}/randomfile /tmp/random expect_output "" run_buildah copy --quiet $WITH_POLICY_JSON --from $from $cid / /tmp/ expect_output "" || \ expect_output --substring "copier: file disappeared while reading" run_buildah mount $cid croot=$output cmp ${TEST_SCRATCH_DIR}/randomfile ${croot}/tmp/tmp/random } @test "add-from-image" { _prefetch busybox run_buildah from --quiet $WITH_POLICY_JSON busybox cid=$output run_buildah add $WITH_POLICY_JSON --quiet --from ubuntu $cid /etc/passwd /tmp/passwd # should pull the image, absolute path expect_output "" run_buildah add --quiet $WITH_POLICY_JSON --from ubuntu $cid etc/passwd /tmp/passwd2 # relative path expect_output "" run_buildah from --quiet $WITH_POLICY_JSON ubuntu ubuntu=$output run_buildah mount $cid croot=$output run_buildah mount $ubuntu ubuntu=$output cmp $ubuntu/etc/passwd ${croot}/tmp/passwd cmp $ubuntu/etc/passwd ${croot}/tmp/passwd2 } @test "copy with .dockerignore" { _prefetch alpine busybox run_buildah from --quiet $WITH_POLICY_JSON alpine from=$output run_buildah copy --contextdir=$BUDFILES/dockerignore $from $BUDFILES/dockerignore ./ run_buildah 1 run $from ls -l test1.txt run_buildah run $from ls -l test2.txt run_buildah 1 run $from ls -l sub1.txt run_buildah 1 run $from ls -l sub2.txt run_buildah 1 run $from ls -l subdir/ } @test "copy-preserving-extended-attributes" { createrandom ${TEST_SCRATCH_DIR}/randomfile # if we need to change which image we use, any image that can provide a working setattr/setcap/getfattr will do image="quay.io/libpod/ubuntu" if ! which setfattr > /dev/null 2> /dev/null; then skip "setfattr not available, unable to check if it'll work in filesystem at ${TEST_SCRATCH_DIR}" fi run setfattr -n user.yeah -v butno ${TEST_SCRATCH_DIR}/root if [ "$status" -ne 0 ] ; then if [[ "$output" =~ "not supported" ]] ; then skip "setfattr not supported in filesystem at ${TEST_SCRATCH_DIR}" fi skip "$output" fi _prefetch $image run_buildah from --quiet $WITH_POLICY_JSON $image first="$output" run_buildah run $first apt-get -y update run_buildah run $first apt-get -y install attr libcap2-bin run_buildah copy $first ${TEST_SCRATCH_DIR}/randomfile / # set security.capability run_buildah run $first setcap cap_setuid=ep /randomfile # set user.something run_buildah run $first setfattr -n user.yeah -v butno /randomfile # copy the file to a second container run_buildah from --quiet $WITH_POLICY_JSON $image second="$output" run_buildah run $second apt-get -y update run_buildah run $second apt-get -y install attr run_buildah copy --from $first $second /randomfile / # compare what the extended attributes look like. if we're on a system with SELinux, there's a label in here, too run_buildah run $first sh -c "getfattr -d -m . --absolute-names /randomfile | grep -v ^security.selinux | sort" expected="$output" run_buildah run $second sh -c "getfattr -d -m . --absolute-names /randomfile | grep -v ^security.selinux | sort" expect_output "$expected" } @test "copy-relative-context-dir" { image=busybox _prefetch $image mkdir -p ${TEST_SCRATCH_DIR}/context createrandom ${TEST_SCRATCH_DIR}/context/excluded_test_file createrandom ${TEST_SCRATCH_DIR}/context/test_file echo excluded_test_file | tee ${TEST_SCRATCH_DIR}/context/.containerignore | tee ${TEST_SCRATCH_DIR}/context/.dockerignore run_buildah from --quiet $WITH_POLICY_JSON $image ctr="$output" cd ${TEST_SCRATCH_DIR}/context run_buildah copy --contextdir . $ctr / /opt/ run_buildah run $ctr ls -1 /opt/ expect_line_count 1 assert "$output" = "test_file" "only contents of copied directory" } @test "copy-file-relative-context-dir" { image=busybox _prefetch $image mkdir -p ${TEST_SCRATCH_DIR}/context createrandom ${TEST_SCRATCH_DIR}/context/test_file run_buildah from --quiet $WITH_POLICY_JSON $image ctr="$output" run_buildah copy --contextdir ${TEST_SCRATCH_DIR}/context $ctr test_file /opt/ run_buildah run $ctr ls -1 /opt/ expect_line_count 1 assert "$output" = "test_file" "only the one file" } @test "copy-file-absolute-context-dir" { image=busybox _prefetch $image mkdir -p ${TEST_SCRATCH_DIR}/context/subdir createrandom ${TEST_SCRATCH_DIR}/context/subdir/test_file run_buildah from --quiet $WITH_POLICY_JSON $image ctr="$output" run_buildah copy --contextdir ${TEST_SCRATCH_DIR}/context $ctr /subdir/test_file /opt/ run_buildah run $ctr ls -1 /opt/ expect_line_count 1 assert "$output" = "test_file" "only the one file" } @test "copy-file-relative-no-context-dir" { image=busybox _prefetch $image mkdir -p ${TEST_SCRATCH_DIR}/context createrandom ${TEST_SCRATCH_DIR}/context/test_file run_buildah from --quiet $WITH_POLICY_JSON $image ctr="$output" # we're not in that directory currently run_buildah 125 copy $ctr test_file /opt/ # now we are cd ${TEST_SCRATCH_DIR}/context run_buildah copy $ctr test_file /opt/ run_buildah run $ctr ls -1 /opt/ expect_line_count 1 assert "$output" = "test_file" "only the one file" } @test "copy-from-ownership" { # Build both a container and an image that have contents owned by a # non-default user. truncate -s 256 ${TEST_SCRATCH_DIR}/random-file-1 truncate -s 256 ${TEST_SCRATCH_DIR}/random-file-2 truncate -s 256 ${TEST_SCRATCH_DIR}/random-file-3 truncate -s 256 ${TEST_SCRATCH_DIR}/random-file-4 truncate -s 256 ${TEST_SCRATCH_DIR}/random-file-5 truncate -s 256 ${TEST_SCRATCH_DIR}/random-file-6 run_buildah from scratch sourcectr="$output" run_buildah copy --chown 123:123 $sourcectr ${TEST_SCRATCH_DIR}/random-file-1 run_buildah copy --chown 123:123 $sourcectr ${TEST_SCRATCH_DIR}/random-file-2 run_buildah copy --chown 456:456 $sourcectr ${TEST_SCRATCH_DIR}/random-file-4 run_buildah copy --chown 456:456 $sourcectr ${TEST_SCRATCH_DIR}/random-file-5 sourceimg=testimage run_buildah commit $sourcectr $sourceimg _prefetch busybox run_buildah from --pull=never $WITH_POLICY_JSON busybox ctr="$output" run_buildah copy $ctr ${TEST_SCRATCH_DIR}/random-file-3 run_buildah copy --from=$sourceimg $ctr /random-file-1 # should be preserved as 123:123 run_buildah copy --from=$sourceimg --chown=456:456 $ctr /random-file-2 run_buildah copy --from=$sourcectr $ctr /random-file-4 # should be preserved as 456:456 run_buildah copy --from=$sourcectr --chown=123:123 $ctr /random-file-5 run_buildah copy $ctr ${TEST_SCRATCH_DIR}/random-file-3 run_buildah copy --chown=789:789 $ctr ${TEST_SCRATCH_DIR}/random-file-6 run_buildah run $ctr stat -c %u:%g /random-file-1 assert 123:123 run_buildah run $ctr stat -c %u:%g /random-file-2 assert 456:456 run_buildah run $ctr stat -c %u:%g /random-file-3 assert 0:0 run_buildah run $ctr stat -c %u:%g /random-file-4 assert 456:456 run_buildah run $ctr stat -c %u:%g /random-file-5 assert 123:123 run_buildah run $ctr stat -c %u:%g /random-file-6 assert 789:789 } @test "copy-link-flag" { createrandom ${TEST_SCRATCH_DIR}/randomfile createrandom ${TEST_SCRATCH_DIR}/other-randomfile run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah mount $cid root=$output run_buildah config --workingdir=/ $cid run_buildah copy --link $cid ${TEST_SCRATCH_DIR}/randomfile run_buildah copy --link $cid ${TEST_SCRATCH_DIR}/randomfile ${TEST_SCRATCH_DIR}/other-randomfile /subdir/ run_buildah unmount $cid run_buildah commit $WITH_POLICY_JSON $cid copy-link-image run_buildah inspect --type=image copy-link-image layers=$(echo "$output" | jq -r '.OCIv1.rootfs.diff_ids | length') assert "$layers" -eq 3 "Expected 3 layers from 2 --link operations and base, but found $layers" run_buildah from $WITH_POLICY_JSON copy-link-image newcid=$output run_buildah mount $newcid newroot=$output test -s $newroot/randomfile cmp ${TEST_SCRATCH_DIR}/randomfile $newroot/randomfile test -s $newroot/subdir/randomfile cmp ${TEST_SCRATCH_DIR}/randomfile $newroot/subdir/randomfile test -s $newroot/subdir/other-randomfile cmp ${TEST_SCRATCH_DIR}/other-randomfile $newroot/subdir/other-randomfile } @test "copy-link-directory" { mkdir -p ${TEST_SCRATCH_DIR}/sourcedir createrandom ${TEST_SCRATCH_DIR}/sourcedir/file1 createrandom ${TEST_SCRATCH_DIR}/sourcedir/file2 run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah config --workingdir=/ $cid run_buildah copy --link $cid ${TEST_SCRATCH_DIR}/sourcedir /destdir run_buildah commit $WITH_POLICY_JSON $cid copy-link-dir-image run_buildah from $WITH_POLICY_JSON copy-link-dir-image newcid=$output run_buildah mount $newcid newroot=$output test -d $newroot/destdir test -s $newroot/destdir/file1 cmp ${TEST_SCRATCH_DIR}/sourcedir/file1 $newroot/destdir/file1 test -s $newroot/destdir/file2 cmp ${TEST_SCRATCH_DIR}/sourcedir/file2 $newroot/destdir/file2 } @test "copy-link-with-chown" { createrandom ${TEST_SCRATCH_DIR}/randomfile _prefetch busybox run_buildah from --quiet $WITH_POLICY_JSON busybox cid=$output run_buildah copy --link --chown bin:bin $cid ${TEST_SCRATCH_DIR}/randomfile /tmp/random run_buildah commit $WITH_POLICY_JSON $cid copy-link-chown-image run_buildah from $WITH_POLICY_JSON copy-link-chown-image newcid=$output run_buildah run $newcid ls -l /tmp/random expect_output --substring "bin.*bin" } @test "copy-link-with-chmod" { createrandom ${TEST_SCRATCH_DIR}/randomfile _prefetch busybox run_buildah from --quiet $WITH_POLICY_JSON busybox cid=$output run_buildah copy --link --chmod 777 $cid ${TEST_SCRATCH_DIR}/randomfile /tmp/random run_buildah commit $WITH_POLICY_JSON $cid copy-link-chmod-image run_buildah from $WITH_POLICY_JSON copy-link-chmod-image newcid=$output run_buildah run $newcid ls -l /tmp/random expect_output --substring "rwxrwxrwx" } ================================================ FILE: tests/crash/crash_notunix.go ================================================ //go:build !unix package main // This is really only here to prevent complaints about the source directory // for a helper that's used in a Unix-specific test not having something that // will compile on non-Unix platforms. func main() { } ================================================ FILE: tests/crash/crash_unix.go ================================================ package main import ( "os" "os/exec" "syscall" "github.com/sirupsen/logrus" ) // Launch a child process and behave, rather unconvincingly, as an OCI runtime. func main() { if err := exec.Command("sh", "-c", "sleep 0 &").Start(); err != nil { logrus.Fatalf("%v", err) } for _, arg := range os.Args { switch arg { case "create": logrus.Info("created\n") os.Exit(0) case "delete": logrus.Info("deleted\n") os.Exit(0) case "kill": logrus.Info("killed\n") os.Exit(0) case "start": logrus.Info("starting\n") // crash here, so that our caller, being run under // "wait", will have to reap us and our errant child // process, lest "wait" complain if err := syscall.Kill(os.Getpid(), syscall.SIGSEGV); err != nil { logrus.Fatalf("awkward: error sending SIGSEGV to myself: %v", err) } } } if err := syscall.Kill(os.Getpid(), syscall.SIGSEGV); err != nil { logrus.Fatalf("awkward: error sending SIGSEGV to myself: %v", err) } } ================================================ FILE: tests/deny.json ================================================ { "default": [ { "type": "reject" } ] } ================================================ FILE: tests/digest/README.md ================================================ This subdirectory contains a script used to create images for testing. To rephrase: this script is used **before testing**, not used **in** testing. _Much_ before testing (days/weeks/months/years), and manually. The script is `make-v2sN` but it is never invoked as such. Instead, various different symlinks point to the script, and the script figures out its use by picking apart the name under which it is called. As of the initial commit on 2020-02-10 there are three symlinks: * make-v2s1 - Create a schema 1 image * make-v2s2 - Create a schema 2 image * make-v2s1-with-dups - Create a schema 1 image with two identical layers If the script is successful, it will emit instructions on how to push the images to quay and what else you might need to do. Updating ======== Should you need new image types, e.g. schema version 3 or an image with purple elephant GIFs in it: 1. Decide on a name. Create a new symlink pointing to `make-v2sN` 1. Add the relevant code to `make-v2sN`: a conditional check at the top, the actual image-creating code, and if possible a new test to make sure the generated image is good 1. Run the script. Verify that the generated image is what you expect. 1. Add new test(s) to `digest.bats` ================================================ FILE: tests/digest/make-v2sN ================================================ #!/bin/bash # # make-v2sN - create a v2sN image, possibly with dups # # This is a helper script used for creating custom images for buildah testing. # The images are used in the digest.bats test. # ME=$(basename $0) die() { echo "$ME: $*" >&2 exit 1 } ############################################################################### # # From the script name, determine the desired schema version (1 or 2) and # whether or not we want duplicate layers. schemaversion=$(expr "$ME" : ".*-v2s\([12]\)") test -n "$schemaversion" || die "Could not find 'v2s[12]' in basename" test "$schemaversion" = "N" && die "Script must be invoked via symlink" dup= if expr "$ME" : ".*-dup" &>/dev/null; then dup="_with_dups" fi IMGNAME=testdigest_v2s${schemaversion}${dup} ############################################################################### # Create the image. set -e # First layer cid=$(buildah from scratch) buildah commit -q $cid interim1 # Create a second layer containing this script and a README cid2=$(buildah from interim1) mp=$(buildah mount $cid2) cp $0 $mp/ cat <$mp/README This is a test image used for buildah testing. EOF # In the README include creation timestamp, user, script name, git tree state function add_to_readme() { printf " %-12s : %s\n" "$1" "$2" >>$mp/README } add_to_readme "Created" "$(date --iso-8601=seconds)" # FIXME: do we really need to know? Will it ever, in practice, be non-root? user=$(id -un) if [ -n "$user" -a "$user" != "root" ]; then add_to_readme "By (user)" "$user" fi create_script=$(cd $(dirname $0) && git ls-files --full-name $ME) if [ -z "$create_script" ]; then create_script=$0 fi add_to_readme "By (script)" "$create_script" git_state=$(cd $(dirname $0) && git describe --dirty) if [ -n "$git_state" ]; then add_to_readme "git state" "$git_state" fi echo "-----------------------------------------------------------------" cat $mp/README echo "-----------------------------------------------------------------" buildah umount $cid2 buildah commit -q $cid2 interim2 layers="interim2 interim1" buildah tag interim2 my_image ############################################################################### # # Push/pull the image to/from a tempdir. This is a kludge allowing us to # clean up interim layers. It's also necessary for dealing with v2s1 layers. TMPDIR=$(mktemp --tmpdir -d $(basename $0).XXXXXXX) push_flags= if [[ $schemaversion -eq 1 ]]; then # buildah can't actually create a v2s1 image; only v2s2. To create v2s1, # dir-push it to a tmpdir using '--format v2s1'; that will be inherited # when we reload it push_flags="--format v2s1" fi buildah push $push_flags my_image dir:${TMPDIR}/${IMGNAME} # Clean up containers and images buildah rm -a buildah rmi -f my_image $layers if [ -n "$dup" ]; then manifest=${TMPDIR}/${IMGNAME}/manifest.json cat $manifest | jq -c '.fsLayers |= [.[0]] + .' | jq -c '.history |= [.[0]] + .' | tr -d '\012' >$manifest.tmp mv $manifest $manifest.BAK mv $manifest.tmp $manifest fi # Delete possibly-existing image, because 'buildah pull' will not overwrite it buildah rmi -f localhost/${IMGNAME}:latest &>/dev/null || true # Reload the image (cd $TMPDIR && buildah pull dir:${IMGNAME}) # Leave the tmpdir behind for the -dup image! if [ -z "$dup" ]; then rm -rf ${TMPDIR} fi ############################################################################### # # We should now have a 'localhost/IMGNAME' image with desired SchemaVersion # and other features as requested. # # Now verify what we have what we intended. echo if type -p jq >&/dev/null; then # Manifest is embedded in the image but as a string, not actual JSON; # the eval-echo converts it to usable JSON manifest=$(eval echo $(buildah inspect ${IMGNAME} | jq .Manifest)) # Check desired schema version: actual_schemaversion=$(jq .schemaVersion <<<"$manifest") if [[ $actual_schemaversion -ne $schemaversion ]]; then die "Expected .schemaVersion $schemaversion, got '$actual_schemaversion'" fi echo "Image localhost/${IMGNAME} looks OK; feel free to:" echo if [ -n "$dup" ]; then echo " \$SKOPEO copy dir:${TMPDIR}/${IMGNAME} docker://quay.io/libpod/${IMGNAME}:\$(date +%Y%m%d)" echo " ^^^^^^^--- must be specially-crafted skopeo(*), see below" else echo " buildah push localhost/${IMGNAME} quay.io/libpod/${IMGNAME}:$(date +%Y%m%d)" echo " buildah push localhost/${IMGNAME} quay.io/libpod/${IMGNAME}:latest" fi echo echo "You may then need to log in to the https://quay.io/ web UI" echo "make those images public, then update tags and/or SHAs" echo "in test/digest.bats." echo echo "Note that the Digest SHA on quay.io != the SHA on the locally" echo "created image. You can get the real SHA on quay.io by clicking" echo "on the image name, then the luggage-tag icon on the left," echo "then the gray box with the text 'SHA256' (not the actual" echo "hash shown in blue to its right), and copy-pasting the SHA" echo "from the popup window." echo echo "NOTE: the first push to quay.io sometimes fails with some sort of" echo "500 error, trying to reuse blob, blah blah. Just ignore it and" echo "retry. IME it works the second time." if [ -n "$dup" ]; then echo echo "(*) skopeo WILL NOT push an image with dup layers. To get it to" echo " do that, build a custom skopeo using the patch here:" echo " https://gist.github.com/nalind/b491204ff05c3c3f3b6ef014b333a60c" echo " ...then use that skopeo in the above 'copy' command." # And, for posterity should the gist ever disappear: # vendor/github.com/containers/image/v5/manifest/docker_schema1.go # - remove lines 66-68 ('if ... s1.fixManifestLayers()...') fi else echo "WARNING: 'jq' not found; unable to verify built image" >&2 fi ================================================ FILE: tests/digest.bats ================================================ #!/usr/bin/env bats load helpers fromreftest() { local img=$1 run_buildah from --quiet --cidfile ${TEST_SCRATCH_DIR}/cid --pull $WITH_POLICY_JSON $img cid="$(< ${TEST_SCRATCH_DIR}/cid)" # If image includes '_v2sN', verify that image is schema version N local expected_schemaversion=$(expr "$img" : '.*_v2s\([0-9]\)') if [ -n "$expected_schemaversion" ]; then actual_schemaversion=$(imgtype -expected-manifest-type '*' -show-manifest $img | jq .schemaVersion) expect_output --from="$actual_schemaversion" "$expected_schemaversion" \ ".schemaversion of $img" fi # This is all we test: basically, that buildah doesn't crash when pushing pushdir=${TEST_SCRATCH_DIR}/fromreftest mkdir -p ${pushdir}/{1,2,3} run_buildah push $WITH_POLICY_JSON $img dir:${pushdir}/1 run_buildah commit $WITH_POLICY_JSON $cid new-image run_buildah push $WITH_POLICY_JSON new-image dir:${pushdir}/2 run_buildah rmi new-image run_buildah commit $WITH_POLICY_JSON $cid dir:${pushdir}/3 run_buildah rm $cid rm -fr ${pushdir} } @test "from-by-digest-s1" { test -n "$CI_USE_REGISTRY_CACHE" && skip "Cannot test against local cache registry" skip_if_rootless_environment fromreftest quay.io/libpod/testdigest_v2s1@sha256:816563225d7baae4782653efc9410579341754fe32cbe20f7600b39fc37d8ec7 } @test "from-by-digest-s1-a-discarded-layer" { test -n "$CI_USE_REGISTRY_CACHE" && skip "Cannot test against local cache registry" skip_if_rootless_environment IMG=quay.io/libpod/testdigest_v2s1_with_dups@sha256:2c619fffbed29d8677e246798333e7d1b288333cb61c020575f6372c76fdbb52 fromreftest ${IMG} # Verify that image meets our expectations (duplicate layers) # Surprisingly, we do this after fromreftest, not before, because fromreftest # has to pull the image for us. # # Check that the first and second .fsLayers and .history elements are dups local manifest=$(imgtype -expected-manifest-type '*' -show-manifest ${IMG}) for element in fsLayers history; do local first=$(jq ".${element}[0]" <<<"$manifest") local second=$(jq ".${element}[1]" <<<"$manifest") expect_output --from="$second" "$first" "${IMG}: .${element}[1] == [0]" done } @test "from-by-tag-s1" { test -n "$CI_USE_REGISTRY_CACHE" && skip "Cannot test against local cache registry" skip_if_rootless_environment fromreftest quay.io/libpod/testdigest_v2s1:20200210 } @test "from-by-digest-s2" { skip_if_rootless_environment fromreftest quay.io/libpod/testdigest_v2s2@sha256:755f4d90b3716e2bf57060d249e2cd61c9ac089b1233465c5c2cb2d7ee550fdb } @test "from-by-tag-s2" { skip_if_rootless_environment fromreftest quay.io/libpod/testdigest_v2s2:20200210 } @test "from-by-repo-only-s2" { skip_if_rootless_environment fromreftest quay.io/libpod/testdigest_v2s2 } ================================================ FILE: tests/docker.json ================================================ { "default": [ { "type": "reject" } ], "transports": { "docker": { "": [ { "type": "insecureAcceptAnything" } ] } } } ================================================ FILE: tests/dumpspec/dumpspec.go ================================================ package main import ( "bytes" "encoding/json" "errors" "fmt" "io" "os" "path/filepath" "strconv" "strings" "syscall" "time" "unicode" rspec "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "go.podman.io/storage/pkg/ioutils" "go.podman.io/storage/pkg/reexec" ) // use defined names for our various commands. we absolutely don't support // everything that an actual functional runtime would, and have no intention of // expanding to do so type modeType string const ( modeCreate = modeType("create") modeStart = modeType("start") modeState = modeType("state") modeKill = modeType("kill") modeDelete = modeType("delete") subprocName = "dumpspec-subproc" ) // signalsByName is a guess at which signals we'd be asked to send to a child // process, currently restricted to the subset defined across all of our // targets var signalsByName = map[string]syscall.Signal{ "SIGABRT": syscall.SIGABRT, "SIGALRM": syscall.SIGALRM, "SIGBUS": syscall.SIGBUS, "SIGFPE": syscall.SIGFPE, "SIGHUP": syscall.SIGHUP, "SIGILL": syscall.SIGILL, "SIGINT": syscall.SIGINT, "SIGKILL": syscall.SIGKILL, "SIGPIPE": syscall.SIGPIPE, "SIGQUIT": syscall.SIGQUIT, "SIGSEGV": syscall.SIGSEGV, "SIGTERM": syscall.SIGTERM, "SIGTRAP": syscall.SIGTRAP, } var ( globalArgs struct { debug bool cgroupManager string log string logFormat string logLevel string root string systemdCgroup bool rootless bool } createArgs struct { bundleDir string configFile string consoleSocket string pidFile string noPivot bool noNewKeyring bool preserveFds int } stateArgs struct { all bool pid int regex string } killArgs struct { all bool pid int regex string signal int } deleteArgs struct { force bool regex string } ) func main() { if reexec.Init() { return } if len(os.Args) < 2 { return } var container, containerID, containerDir string mainCommand := cobra.Command{ Use: "dumpspec", Short: "fake OCI runtime", PersistentPreRunE: func(_ *cobra.Command, args []string) error { tmpdir, ok := os.LookupEnv("XDG_RUNTIME_DIR") if !ok { tmpdir = filepath.Join(os.TempDir(), strconv.Itoa(os.Getuid())) } if globalArgs.root != "" { tmpdir = globalArgs.root } tmpdir = filepath.Join(tmpdir, "dumpspec") if err := os.MkdirAll(tmpdir, 0o700); err != nil && !errors.Is(err, os.ErrExist) { return fmt.Errorf("ensuring that %q exists: %w", tmpdir, err) } if len(args) > 0 { // this is the first arg for all of the commands that we care about container = args[0] } containerID = mapToContainerID(container) containerDir = filepath.Join(tmpdir, containerID) return nil }, } mainFlags := mainCommand.PersistentFlags() mainFlags.BoolVar(&globalArgs.debug, "debug", false, "log for debugging") mainFlags.BoolVar(&globalArgs.systemdCgroup, "systemd-cgroup", false, "use systemd for handling cgroups") mainFlags.BoolVar(&globalArgs.rootless, "rootless", false, "ignore some settings to that conflict with rootless operation") mainFlags.StringVar(&globalArgs.cgroupManager, "cgroup-manager", "cgroupfs", "method for managing cgroups") mainFlags.StringVar(&globalArgs.log, "log", "", "logging destination") mainFlags.StringVar(&globalArgs.logFormat, "log-format", "", "logging format specifier") mainFlags.StringVar(&globalArgs.logLevel, "log-level", "", "logging level") rootUsage := "root `directory` of runtime data" rootDefault := "" if xdgRuntimeDir, ok := os.LookupEnv("XDG_RUNTIME_DIR"); ok { rootUsage += " (default $XDG_RUNTIME_DIR)" rootDefault = xdgRuntimeDir } mainFlags.StringVar(&globalArgs.root, "root", rootDefault, rootUsage) createCommand := &cobra.Command{ Use: string(modeCreate), Args: cobra.ExactArgs(1), Short: "create a ready-to-start container process", RunE: func(_ *cobra.Command, _ []string) error { if err := os.MkdirAll(containerDir, 0o700); err != nil { return fmt.Errorf("creating container directory: %w", err) } configFile := createArgs.configFile if configFile == "" { configFile = filepath.Join(createArgs.bundleDir, "config.json") } config, err := os.ReadFile(configFile) if err != nil { return fmt.Errorf("reading runtime configuration: %w", err) } var spec rspec.Spec if err := json.Unmarshal(config, &spec); err != nil { return fmt.Errorf("parsing runtime configuration: %w", err) } if err := os.WriteFile(filepath.Join(containerDir, "config.json"), config, 0o600); err != nil { return fmt.Errorf("saving copy of runtime configuration: %w", err) } state := rspec.State{ Version: rspec.Version, ID: container, Bundle: createArgs.bundleDir, } stateBytes, err := json.Marshal(state) if err != nil { return fmt.Errorf("encoding initial runtime state: %w", err) } if err := os.WriteFile(filepath.Join(containerDir, "state"), stateBytes, 0o600); err != nil { return fmt.Errorf("writing initial runtime state: %w", err) } pr, pw, err := os.Pipe() if err != nil { return fmt.Errorf("internal error: %w", err) } defer pr.Close() cmd := getStarter(containerDir, createArgs.consoleSocket, createArgs.pidFile, spec, pw) if err := cmd.Start(); err != nil { return fmt.Errorf("internal error: %w", err) } pw.Close() ready, err := io.ReadAll(pr) if err != nil { return fmt.Errorf("waiting for child to start: %w", err) } if strings.TrimSpace(string(ready)) != "OK" { return fmt.Errorf("unexpected child status %q", string(ready)) } return nil }, } createFlags := createCommand.Flags() createFlags.StringVarP(&createArgs.bundleDir, "bundle", "b", "", "`directory` containing config.json") createFlags.StringVarP(&createArgs.configFile, "config", "f", "", "`path` to config.json") createFlags.StringVar(&createArgs.consoleSocket, "console-socket", "", "socket `path` for passing PTY") createFlags.StringVar(&createArgs.pidFile, "pid-file", "", "`path` in which to store child PID") createFlags.BoolVar(&createArgs.noPivot, "no-pivot", false, "use chroot() instead of pivot_root()") createFlags.BoolVar(&createArgs.noNewKeyring, "no-new-keyring", false, "don't create a new keyring") mainCommand.AddCommand(createCommand) startCommand := &cobra.Command{ Use: string(modeStart), Args: cobra.ExactArgs(1), Short: "start a previously-created container process", RunE: func(_ *cobra.Command, _ []string) error { if err := ioutils.AtomicWriteFile(filepath.Join(containerDir, "start"), []byte("start"), 0o600); err != nil { return fmt.Errorf("writing start file: %w", err) } return nil }, } mainCommand.AddCommand(startCommand) stateCommand := &cobra.Command{ Use: string(modeState), Args: cobra.ExactArgs(1), Short: "poll the state of a container process", RunE: func(_ *cobra.Command, _ []string) error { stateFile, err := os.Open(filepath.Join(containerDir, "state")) if err != nil { return err } defer stateFile.Close() if _, err := io.Copy(os.Stdout, stateFile); err != nil { return fmt.Errorf("copying state file: %w", err) } return nil }, } stateFlags := stateCommand.Flags() stateFlags.BoolVarP(&stateArgs.all, "all", "a", false, "start all containers") stateFlags.IntVar(&stateArgs.pid, "pid", 0, "start container by `pid`") stateFlags.StringVarP(&stateArgs.regex, "regex", "r", "", "start containers with IDs matching a `regex`") mainCommand.AddCommand(stateCommand) killCommand := &cobra.Command{ Use: string(modeKill), Args: cobra.RangeArgs(1, 2), Short: "signal/kill a container process", RunE: func(_ *cobra.Command, args []string) error { if len(args) > 1 { signalString := args[1] signalNumber, err := strconv.Atoi(signalString) if err != nil { n, ok := signalsByName[signalString] if !ok { n, ok = signalsByName["SIG"+signalString] if !ok { return fmt.Errorf("%v: unrecognized signal %q", os.Args, signalString) } } signalNumber = int(n) } killArgs.signal = signalNumber } if err := ioutils.AtomicWriteFile(filepath.Join(containerDir, "kill"), []byte(strconv.Itoa(killArgs.signal)), 0o600); err != nil { return fmt.Errorf("writing exit status file: %w", err) } return nil }, } killFlags := killCommand.Flags() killFlags.BoolVarP(&killArgs.all, "all", "a", false, "signal/kill all containers") killFlags.IntVar(&killArgs.pid, "pid", 0, "signal/kill container by `pid`") killFlags.StringVarP(&killArgs.regex, "regex", "r", "", "signal/kill containers with IDs matching a `regex`") mainCommand.AddCommand(killCommand) deleteCommand := &cobra.Command{ Use: string(modeDelete), Args: cobra.ExactArgs(1), Short: "delete a container process", RunE: func(_ *cobra.Command, _ []string) error { if err := os.RemoveAll(containerDir); err != nil { return fmt.Errorf("removing container directory: %w", err) } return nil }, } deleteFlags := deleteCommand.Flags() deleteFlags.StringVarP(&deleteArgs.regex, "regex", "r", "", "delete containers with IDs matching a `regex`") deleteFlags.BoolVarP(&deleteArgs.force, "force", "f", false, "forcibly stop containers which are not stopped") mainCommand.AddCommand(deleteCommand) err := mainCommand.Execute() if err != nil { logrus.Fatal(err) os.Exit(1) } os.Exit(0) } func mapToContainerID(container string) string { var encoder strings.Builder for _, c := range container { if unicode.IsLetter(c) || unicode.IsNumber(c) { if _, err := encoder.WriteRune(c); err != nil { logrus.Fatalf("%v: encoding container ID: %q: %v", os.Args, c, err) } } else { if _, err := encoder.WriteString(strconv.Itoa(int(c))); err != nil { logrus.Fatalf("%v: encoding container ID: %q: %v", os.Args, c, err) } } } return encoder.String() } func waitForFile(dirname, basename string) string { waitedFile := filepath.Join(dirname, basename) for { if _, err := os.Stat(dirname); err != nil { logrus.Fatalf("%v: %v", os.Args, err) } st, err := os.Stat(waitedFile) if err != nil && !errors.Is(err, os.ErrNotExist) { logrus.Fatalf("%v: %v", os.Args, err) } if err != nil || st.Size() == 0 { time.Sleep(100 * time.Millisecond) continue } contents, err := os.ReadFile(waitedFile) if err != nil { logrus.Fatalf("%v: %v", os.Args, err) } text := strings.TrimSpace(string(contents)) return text } } func init() { reexec.Register(subprocName, subproc) } func subproc() { mainCommand := cobra.Command{ Use: "dumpspec", Short: "fake OCI runtime", Long: "dumpspec containerDir consoleSocket pidFile [spec ...]", Args: cobra.ExactArgs(3), RunE: func(_ *cobra.Command, args []string) error { dir := args[0] consoleSocket := args[1] pidFile := args[2] config, err := os.ReadFile(filepath.Join(dir, "config.json")) if err != nil { return fmt.Errorf("reading runtime configuration: %w", err) } var spec rspec.Spec if err := json.Unmarshal(config, &spec); err != nil { return fmt.Errorf("parsing runtime configuration: %w", err) } stateBytes, err := os.ReadFile(filepath.Join(dir, "state")) if err != nil { return fmt.Errorf("reading initial state : %w", err) } var state rspec.State if err := json.Unmarshal(stateBytes, &state); err != nil { return fmt.Errorf("parsing initial state: %w", err) } saveState := func() error { stateBytes, err := json.Marshal(state) if err != nil { return fmt.Errorf("encoding updated state: %w", err) } err = ioutils.AtomicWriteFile(filepath.Join(dir, "state"), stateBytes, 0o600) if err != nil { return fmt.Errorf("writing updated state: %w", err) } return nil } output := io.Writer(os.Stdout) if pidFile != "" { if err := ioutils.AtomicWriteFile(pidFile, []byte(strconv.Itoa(os.Getpid())), 0o600); err != nil { return fmt.Errorf("writing pid file %q: %w", pidFile, err) } } state.Pid = os.Getpid() state.Status = rspec.StateCreated if err := saveState(); err != nil { return err } if consoleSocket != "" { if output, err = sendConsoleDescriptor(consoleSocket); err != nil { return fmt.Errorf("sending terminal control fd to parent process: %w", err) } } ok := os.NewFile(3, "startup status pipe") fmt.Fprintf(ok, "OK") ok.Close() start := waitForFile(dir, "start") if start != "start" { return fmt.Errorf("unexpected start indicator %q", start) } state.Status = rspec.StateRunning if err := saveState(); err != nil { return err } if spec.Process == nil || len(spec.Process.Args) == 0 { if _, err := io.Copy(output, bytes.NewReader(config)); err != nil { return fmt.Errorf("writing configuration: %w", err) } } else { for _, query := range spec.Process.Args { var data any if err := json.Unmarshal(config, &data); err != nil { return fmt.Errorf("parsing runtime configuration: %w", err) } path := strings.Split(query, ".") for i, component := range path { if component == "" { continue } pathSoFar := strings.Join(path[:i], ".") if data == nil { return fmt.Errorf("unable to descend into %q after %q", component, pathSoFar) } if m, ok := data.(map[string]any); ok { data = m[component] } else if s, ok := data.([]any); ok { i, err := strconv.Atoi(component) if err != nil { return fmt.Errorf("%q is not numeric while indexing slice at %q", component, pathSoFar) } data = s[i] } else { return fmt.Errorf("unable to descend into %q after %q", component, pathSoFar) } } final, err := json.Marshal(data) if err != nil { return fmt.Errorf("encoding query result: %w", err) } if len(final) == 0 || final[len(final)-1] != '\n' { final = append(final, byte('\n')) } if _, err := io.Copy(output, bytes.NewReader(final)); err != nil { return fmt.Errorf("writing configuration subset %q: %w", query, err) } } } state.Status = rspec.StateStopped if err := saveState(); err != nil { return err } return nil }, } err := mainCommand.Execute() if err != nil { logrus.Fatal(err) os.Exit(1) } os.Exit(0) } ================================================ FILE: tests/dumpspec/dumpspec_linux.go ================================================ package main import ( "os" "slices" "syscall" rspec "github.com/opencontainers/runtime-spec/specs-go" "go.podman.io/storage/pkg/unshare" ) func getStarter(containerDir, consoleSocket, pidFile string, spec rspec.Spec, extraFile *os.File) interface{ Start() error } { cmd := unshare.Command(subprocName, containerDir, consoleSocket, pidFile) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if spec.Linux != nil { for _, ns := range spec.Linux.Namespaces { switch ns.Type { case rspec.UserNamespace: cmd.UnshareFlags |= syscall.CLONE_NEWUSER case rspec.NetworkNamespace: // caller is expecting to configure networking for this process's network namespace cmd.UnshareFlags |= syscall.CLONE_NEWNET case rspec.MountNamespace: cmd.UnshareFlags |= syscall.CLONE_NEWNS case rspec.IPCNamespace: cmd.UnshareFlags |= syscall.CLONE_NEWIPC case rspec.UTSNamespace: cmd.UnshareFlags |= syscall.CLONE_NEWUTS case rspec.CgroupNamespace: cmd.UnshareFlags |= syscall.CLONE_NEWCGROUP } } cmd.UidMappings = slices.Clone(spec.Linux.UIDMappings) cmd.GidMappings = slices.Clone(spec.Linux.GIDMappings) } if extraFile != nil { cmd.ExtraFiles = append([]*os.File{extraFile}, cmd.ExtraFiles...) } return cmd } ================================================ FILE: tests/dumpspec/dumpspec_notlinux.go ================================================ //go:build !linux package main import ( "os" "os/exec" rspec "github.com/opencontainers/runtime-spec/specs-go" ) func getStarter(containerDir, consoleSocket, pidFile string, _ rspec.Spec, extraFile *os.File) interface{ Start() error } { cmd := exec.Command(subprocName, containerDir, consoleSocket, pidFile) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if extraFile != nil { cmd.ExtraFiles = append([]*os.File{extraFile}, cmd.ExtraFiles...) } return cmd } ================================================ FILE: tests/dumpspec/dumpspec_notunix.go ================================================ //go:build windows package main import ( "errors" "os" ) func sendConsoleDescriptor(consoleSocket string) (*os.File, error) { return nil, errors.New("unable to transport pseudoterminal descriptors") } ================================================ FILE: tests/dumpspec/dumpspec_unix.go ================================================ //go:build !windows package main import ( "fmt" "net" "os" "github.com/containers/buildah/internal/pty" "github.com/sirupsen/logrus" "golang.org/x/sys/unix" ) func sendConsoleDescriptor(consoleSocket string) (*os.File, error) { closePty := true control, pty, err := pty.GetPtyDescriptors() if err != nil { return nil, fmt.Errorf("allocating pseudo-terminal: %w", err) } defer unix.Close(control) defer func() { if closePty { if err := unix.Close(pty); err != nil { logrus.Errorf("closing pty descriptor %d: %v", pty, err) } } }() socketReceiver, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: consoleSocket, Net: "unix"}) if err != nil { return nil, fmt.Errorf("allocating pseudo-terminal: %w", err) } defer socketReceiver.Close() rights := unix.UnixRights(control) _, _, err = socketReceiver.WriteMsgUnix(nil, rights, nil) if err != nil { return nil, fmt.Errorf("sending terminal control fd to parent process: %w", err) } closePty = false return os.NewFile(uintptr(pty), "controlling terminal"), nil } ================================================ FILE: tests/dumpspec/notes.md ================================================ Global flags: crun: --cgroup-manager=MANAGER --debug --log=FILE --log-format={text|json} --log-level --root=DIR --rootless={true|false|auto} --systemd-cgroup runc: --debug --log=FILE --log-format={text|json} --root=DIR --systemd-cgroup --rootless={true|false|auto} create [-b|--bundle dir] [--console-socket[=]path] [--pid-file[=]path] [--no-pivot] [--preserve-fds[=]N] containerID runc: [--pidfd-socket=path] [--no-new-keyring] crun: [-f|--config file] [--no-subreaper (ignored)] [--no-new-keyring] runsc: [--pidfd-socket=path] * Start keeping track of containerID under --root or $XDG_RUNTIME_DIR/$runtimeName * If console socket given, allocate a pseudoterminal, connect to it, and pass a TTY descriptor. * If not, pass stdio down directly. * Prepare, but have babysitter wait before starting process. start containerID * Start process connected to stdio or terminal. state containerID crun: [-a|--all] [-r|--regex regex] runsc: [-all|--all] [-pid int (in parent pid namespace)] * Output a JSON-encoded github.com/opencontainers/runtime-spec/specs-go.State value on stdout. kill containerID [signal] crun: [-a|--all] [-r|--regex regex] runsc: [-all|--all] [-pid int (in parent pid namespace)] * Send signal to process tree. delete containerID runc: [-f|--force (SIGKILL first if need be)] crun: [-f|--force (SIGKILL first if need be)] [-r|--regex regex] runsc: [-force|--force] runc: checkpoint events exec features list pause ps resume restore run spec state update crun: checkpoint exec features list pause ps resume restore run spec state update runsc: checkpoint do events exec flags list pause port-forward ps restore resume run spec state wait ================================================ FILE: tests/formats.bats ================================================ #!/usr/bin/env bats load helpers ################### # check_imgtype # shortcut for running 'imgtype' and verifying image ################### function check_imgtype() { # First argument: image name image="$1" # Second argument: expected image type, 'oci' or 'docker' imgtype_oci="application/vnd.oci.image.manifest.v1+json" imgtype_dkr="application/vnd.docker.distribution.manifest.v2+json" expect="" case "$2" in oci) want=$imgtype_oci; reject=$imgtype_dkr;; docker) want=$imgtype_dkr; reject=$imgtype_oci;; *) die "Internal error: unknown image type '$2'";; esac # First test: run imgtype with expected type, confirm exit 0 + no output echo "\$ imgtype -expected-manifest-type $want $image" run imgtype -expected-manifest-type $want $image echo "$output" if [[ $status -ne 0 ]]; then die "exit status is $status (expected 0)" fi expect_output "" "Checking imagetype($image) == $2" # Second test: the converse. Run imgtype with the WRONG expected type, # confirm error message and exit status 1 echo "\$ imgtype -expected-manifest-type $reject $image [opposite test]" run imgtype -expected-manifest-type $reject $image echo "$output" if [[ $status -ne 1 ]]; then die "exit status is $status (expected 1)" fi # Can't embed entire string because the '+' sign is interpreted as regex expect_output --substring \ "level=error msg=\"expected .* type \\\\\".*, got " \ "Checking imagetype($image) == $2" } @test "write-formats" { skip_if_rootless_environment run_buildah from --pull=false $WITH_POLICY_JSON scratch cid=$output run_buildah commit $WITH_POLICY_JSON $cid scratch-image-default run_buildah commit --format docker $WITH_POLICY_JSON $cid scratch-image-docker run_buildah commit --format oci $WITH_POLICY_JSON $cid scratch-image-oci check_imgtype scratch-image-default oci check_imgtype scratch-image-oci oci check_imgtype scratch-image-docker docker } @test "bud-formats" { skip_if_rootless_environment run_buildah build-using-dockerfile $WITH_POLICY_JSON -t scratch-image-default -f Containerfile $BUDFILES/from-scratch run_buildah build-using-dockerfile --format docker $WITH_POLICY_JSON -t scratch-image-docker -f Containerfile $BUDFILES/from-scratch run_buildah build-using-dockerfile --format oci $WITH_POLICY_JSON -t scratch-image-oci -f Containerfile $BUDFILES/from-scratch check_imgtype scratch-image-default oci check_imgtype scratch-image-oci oci check_imgtype scratch-image-docker docker } ================================================ FILE: tests/from.bats ================================================ #!/usr/bin/env bats load helpers @test "from-flags-order-verification" { run_buildah 125 from scratch -q check_options_flag_err "-q" run_buildah 125 from scratch --pull check_options_flag_err "--pull" run_buildah 125 from scratch --ulimit=1024 check_options_flag_err "--ulimit=1024" run_buildah 125 from scratch --name container-name-irrelevant check_options_flag_err "--name" run_buildah 125 from scratch --cred="fake fake" --name small check_options_flag_err "--cred=fake fake" } @test "from-with-digest" { _prefetch alpine run_buildah inspect --format "{{.FromImageID}}" alpine digest=$output run_buildah from "sha256:$digest" run_buildah rm $output run_buildah 125 from sha256:1111111111111111111111111111111111111111111111111111111111111111 expect_output --substring "1111111111111111111111111111111111111111111111111111111111111111: image not known" } @test "commit-to-from-elsewhere" { elsewhere=${TEST_SCRATCH_DIR}/elsewhere-img mkdir -p ${elsewhere} run_buildah from --retry 4 --retry-delay 4s --pull $WITH_POLICY_JSON scratch cid=$output run_buildah commit $WITH_POLICY_JSON $cid dir:${elsewhere} run_buildah rm $cid run_buildah from --quiet --pull=false $WITH_POLICY_JSON dir:${elsewhere} expect_output "dir-working-container" run_buildah rm $output run_buildah from --quiet --pull-always $WITH_POLICY_JSON dir:${elsewhere} expect_output "dir-working-container" run_buildah from --pull $WITH_POLICY_JSON scratch cid=$output run_buildah commit $WITH_POLICY_JSON $cid oci-archive:${elsewhere}.oci run_buildah rm $cid run_buildah from --quiet --pull=false $WITH_POLICY_JSON oci-archive:${elsewhere}.oci expect_output "oci-archive-working-container" run_buildah rm $output run_buildah from --pull $WITH_POLICY_JSON scratch cid=$output run_buildah commit $WITH_POLICY_JSON $cid docker-archive:${elsewhere}.docker run_buildah rm $cid run_buildah from --quiet --pull=false $WITH_POLICY_JSON docker-archive:${elsewhere}.docker expect_output "docker-archive-working-container" run_buildah rm $output } @test "from-tagged-image" { # GitHub #396: Make sure the container name starts with the correct image even when it's tagged. run_buildah from --pull=false $WITH_POLICY_JSON scratch cid=$output run_buildah commit $WITH_POLICY_JSON "$cid" scratch2 # Also check for base-image annotations. run_buildah inspect --format '{{index .ImageAnnotations "org.opencontainers.image.base.digest" }}' scratch2 expect_output "" "no base digest for scratch" run_buildah inspect --format '{{index .ImageAnnotations "org.opencontainers.image.base.name" }}' scratch2 expect_output "" "no base name for scratch" run_buildah rm $cid run_buildah tag scratch2 scratch3 # Set --pull=false to prevent looking for a newer scratch3 image. run_buildah from --pull=false $WITH_POLICY_JSON scratch3 expect_output --substring "scratch3-working-container" run_buildah rm $output # Set --pull=never to prevent looking for a newer scratch3 image. run_buildah from --pull=never $WITH_POLICY_JSON scratch3 expect_output --substring "scratch3-working-container" run_buildah rm $output run_buildah rmi scratch2 scratch3 # GitHub https://github.com/containers/buildah/issues/396#issuecomment-360949396 run_buildah from --quiet --pull=true $WITH_POLICY_JSON alpine cid=$output run_buildah rm $cid run_buildah tag alpine alpine2 run_buildah from --quiet $WITH_POLICY_JSON localhost/alpine2 expect_output "alpine2-working-container" run_buildah rm $output tmp=$RANDOM run_buildah from --suffix $tmp --quiet $WITH_POLICY_JSON localhost/alpine2 expect_output "alpine2-$tmp" run_buildah rm $output run_buildah rmi alpine alpine2 run_buildah from --quiet --pull=true $WITH_POLICY_JSON docker.io/alpine run_buildah rm $output run_buildah rmi docker.io/alpine run_buildah from --quiet --pull=true $WITH_POLICY_JSON docker.io/alpine:latest run_buildah rm $output run_buildah rmi docker.io/alpine:latest # FIXME FIXME FIXME: I don't see the point of these. Any reason not to delete? # run_buildah from --quiet --pull=true $WITH_POLICY_JSON docker.io/centos:7 # run_buildah rm $output # run_buildah rmi docker.io/centos:7 # run_buildah from --quiet --pull=true $WITH_POLICY_JSON docker.io/centos:latest # run_buildah rm $output # run_buildah rmi docker.io/centos:latest } @test "from the following transports: docker-archive, oci-archive, and dir" { _prefetch alpine run_buildah from --quiet --pull=true $WITH_POLICY_JSON alpine run_buildah rm $output # #2205: The important thing here is differentiating 'docker:latest' # (the image) from 'docker:/path' ('docker' as a protocol identifier). # This is a parsing fix so we don't actually need to pull the image. run_buildah 125 from --quiet --pull=false $WITH_POLICY_JSON docker:latest assert "$output" = "Error: docker:latest: image not known" run_buildah push $WITH_POLICY_JSON alpine docker-archive:${TEST_SCRATCH_DIR}/docker-alp.tar:alpine run_buildah push $WITH_POLICY_JSON alpine oci-archive:${TEST_SCRATCH_DIR}/oci-alp.tar:alpine run_buildah push $WITH_POLICY_JSON alpine dir:${TEST_SCRATCH_DIR}/alp-dir run_buildah rmi alpine run_buildah from --quiet $WITH_POLICY_JSON docker-archive:${TEST_SCRATCH_DIR}/docker-alp.tar expect_output "alpine-working-container" run_buildah rm ${output} run_buildah rmi alpine run_buildah from --quiet $WITH_POLICY_JSON oci-archive:${TEST_SCRATCH_DIR}/oci-alp.tar expect_output "alpine-working-container" run_buildah rm ${output} run_buildah rmi alpine run_buildah from --quiet $WITH_POLICY_JSON dir:${TEST_SCRATCH_DIR}/alp-dir expect_output "dir-working-container" } @test "from the following transports: docker-archive and oci-archive with no image reference" { _prefetch alpine run_buildah from --quiet --pull=true $WITH_POLICY_JSON alpine run_buildah rm $output run_buildah push $WITH_POLICY_JSON alpine docker-archive:${TEST_SCRATCH_DIR}/docker-alp.tar run_buildah push $WITH_POLICY_JSON alpine oci-archive:${TEST_SCRATCH_DIR}/oci-alp.tar run_buildah rmi alpine run_buildah from --quiet $WITH_POLICY_JSON docker-archive:${TEST_SCRATCH_DIR}/docker-alp.tar expect_output "alpine-working-container" run_buildah rm $output run_buildah rmi -a run_buildah from --quiet $WITH_POLICY_JSON oci-archive:${TEST_SCRATCH_DIR}/oci-alp.tar expect_output "oci-archive-working-container" run_buildah rm $output run_buildah rmi -a } @test "from cpu-period test" { skip_if_rootless_environment skip_if_chroot skip_if_no_runtime _prefetch alpine run_buildah from --quiet --cpu-period=5000 --pull $WITH_POLICY_JSON alpine cid=$output run_buildah run $cid /bin/sh -c "cut -d ' ' -f 2 /sys/fs/cgroup/\$(awk -F: '{print \$NF}' /proc/self/cgroup)/cpu.max" expect_output "5000" } @test "from cpu-quota test" { skip_if_rootless_environment skip_if_chroot skip_if_no_runtime _prefetch alpine run_buildah from --quiet --cpu-quota=5000 --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah run $cid /bin/sh -c "cut -d ' ' -f 1 /sys/fs/cgroup/\$(awk -F: '{print \$NF}' /proc/self/cgroup)/cpu.max" expect_output "5000" } @test "from cpu-shares test" { skip_if_rootless_environment skip_if_chroot skip_if_no_runtime _prefetch alpine for shares in 2 200 2000 12345 20000 200000 ; do run_buildah from --quiet --cpu-shares=${shares} --pull $WITH_POLICY_JSON alpine cid=$output local converted="$(convert_v1_shares_to_v2_weight ${shares})" local expect="(weight ${converted##* }|weight ${converted%% *})" run_buildah run $cid /bin/sh -c "echo -n 'weight '; cat /sys/fs/cgroup/\$(awk -F : '{print \$NF}' /proc/self/cgroup)/cpu.weight" echo expected "${expect}" expect_output --substring "${expect}" done } @test "from cpuset-cpus test" { skip_if_rootless_environment skip_if_chroot skip_if_no_runtime _prefetch alpine run_buildah from --quiet --cpuset-cpus=0 --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah run $cid /bin/sh -c "cat /sys/fs/cgroup/\$(awk -F : '{print \$NF}' /proc/self/cgroup)/cpuset.cpus" expect_output "0" } @test "from cpuset-mems test" { skip_if_rootless_environment skip_if_chroot skip_if_no_runtime _prefetch alpine run_buildah from --quiet --cpuset-mems=0 --pull $WITH_POLICY_JSON alpine cid=$output run_buildah run $cid /bin/sh -c "cat /sys/fs/cgroup/\$(awk -F : '{print \$NF}' /proc/self/cgroup)/cpuset.mems" expect_output "0" } @test "from memory test" { skip_if_rootless_environment skip_if_chroot _prefetch alpine run_buildah from --quiet --memory=40m --memory-swap=70m --pull=false $WITH_POLICY_JSON alpine cid=$output mpath="/sys/fs/cgroup\$(awk -F: '{print \$3}' /proc/self/cgroup)/memory.max" spath="/sys/fs/cgroup\$(awk -F: '{print \$3}' /proc/self/cgroup)/memory.swap.max" run_buildah run $cid sh -c "cat $mpath" expect_output "41943040" "$mpath" run_buildah run $cid sh -c "cat $spath" expect_output "31457280" "$spath" } @test "from volume test" { skip_if_no_runtime _prefetch alpine run_buildah from --quiet --volume=${TEST_SCRATCH_DIR}:/myvol --pull $WITH_POLICY_JSON alpine cid=$output run_buildah run $cid -- cat /proc/mounts expect_output --substring " /myvol " } @test "from volume ro test" { skip_if_chroot skip_if_no_runtime _prefetch alpine run_buildah from --quiet --volume=${TEST_SCRATCH_DIR}:/myvol:ro --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah run $cid -- cat /proc/mounts expect_output --substring " /myvol " } @test "from --volume with U flag" { skip_if_rootless_environment skip_if_no_runtime # Check if we're running in an environment that can even test this. run readlink /proc/self/ns/user echo "readlink /proc/self/ns/user -> $output" [ $status -eq 0 ] || skip "user namespaces not supported" # Generate mappings for using a user namespace. uidbase=$((${RANDOM}+1024)) gidbase=$((${RANDOM}+1024)) uidsize=$((${RANDOM}+1024)) gidsize=$((${RANDOM}+1024)) # Create source volume. mkdir ${TEST_SCRATCH_DIR}/testdata touch ${TEST_SCRATCH_DIR}/testdata/testfile1.txt # Create a container that uses that mapping and U volume flag. _prefetch alpine run_buildah from --pull=false $WITH_POLICY_JSON --userns-uid-map 0:$uidbase:$uidsize --userns-gid-map 0:$gidbase:$gidsize --volume ${TEST_SCRATCH_DIR}/testdata:/mnt:z,U alpine ctr="$output" # Test mounted volume has correct UID and GID ownership. run_buildah run "$ctr" stat -c "%u:%g" /mnt/testfile1.txt expect_output "0:0" # Test user can create file in the mounted volume. run_buildah run "$ctr" touch /mnt/testfile2.txt # Test created file has correct UID and GID ownership. run_buildah run "$ctr" stat -c "%u:%g" /mnt/testfile2.txt expect_output "0:0" } @test "from shm-size test" { skip_if_chroot skip_if_no_runtime _prefetch alpine run_buildah from --quiet --shm-size=80m --pull $WITH_POLICY_JSON alpine cid=$output run_buildah run $cid -- df -h /dev/shm expect_output --substring " 80.0M " } @test "from add-host test" { skip_if_no_runtime _prefetch alpine run_buildah from --quiet --add-host=localhost:127.0.0.1 --pull $WITH_POLICY_JSON alpine cid=$output run_buildah run --net=container $cid -- cat /etc/hosts expect_output --substring "127.0.0.1[[:blank:]]*localhost" } @test "from name test" { _prefetch alpine container_name=mycontainer run_buildah from --quiet --name=${container_name} --pull $WITH_POLICY_JSON alpine cid=$output run_buildah inspect --format '{{.Container}}' ${container_name} } @test "from cidfile test" { _prefetch alpine run_buildah from --cidfile ${TEST_SCRATCH_DIR}/output.cid --pull=false $WITH_POLICY_JSON alpine cid=$(< ${TEST_SCRATCH_DIR}/output.cid) run_buildah containers -f id=${cid} } @test "from pull never" { run_buildah 125 from $WITH_POLICY_JSON --pull-never busybox echo "$output" expect_output --substring "busybox: image not known" run_buildah 125 from $WITH_POLICY_JSON --pull=false busybox echo "$output" expect_output --substring "busybox: image not known" run_buildah from $WITH_POLICY_JSON --pull=ifmissing busybox echo "$output" expect_output --substring "busybox-working-container" run_buildah from $WITH_POLICY_JSON --pull=never busybox echo "$output" expect_output --substring "busybox-working-container" } @test "from pull false no local image" { _prefetch busybox target=my-busybox run_buildah from $WITH_POLICY_JSON --pull=false busybox echo "$output" expect_output --substring "busybox-working-container" } @test "from with nonexistent authfile: fails" { run_buildah 125 from --authfile /no/such/file --pull $WITH_POLICY_JSON alpine assert "$output" =~ "Error: credential file is not accessible: (faccessat|stat) /no/such/file: no such file or directory" } @test "from --pull-always: emits 'Getting' even if image is cached" { _prefetch docker.io/busybox run_buildah inspect --format "{{.FromImageDigest}}" docker.io/busybox fromDigest="$output" run_buildah pull $WITH_POLICY_JSON docker.io/busybox run_buildah from $WITH_POLICY_JSON --name busyboxc --pull docker.io/busybox expect_output --substring "Getting" run_buildah rm busyboxc run_buildah from $WITH_POLICY_JSON --name busyboxc --pull=true docker.io/busybox expect_output --substring "Getting" run_buildah rm busyboxc run_buildah from $WITH_POLICY_JSON --name busyboxc --pull-always docker.io/busybox expect_output --substring "Getting" run_buildah commit $WITH_POLICY_JSON busyboxc fakename-img run_buildah 125 from $WITH_POLICY_JSON --pull=always fakename-img # Also check for base-image annotations. run_buildah inspect --format '{{index .ImageAnnotations "org.opencontainers.image.base.digest" }}' fakename-img expect_output "$fromDigest" "base digest from busybox" run_buildah inspect --format '{{index .ImageAnnotations "org.opencontainers.image.base.name" }}' fakename-img expect_output "docker.io/library/busybox:latest" "base name from busybox" } @test "from --quiet: should not emit progress messages" { # Force a pull. Normally this would say 'Getting image ...' and other # progress messages. With --quiet, we should see only the container name. run_buildah '?' rmi busybox run_buildah from $WITH_POLICY_JSON --quiet docker.io/busybox expect_output "busybox-working-container" } @test "from encrypted local image" { _prefetch busybox mkdir ${TEST_SCRATCH_DIR}/tmp openssl genrsa -out ${TEST_SCRATCH_DIR}/tmp/mykey.pem 1024 openssl genrsa -out ${TEST_SCRATCH_DIR}/tmp/mykey2.pem 1024 openssl rsa -in ${TEST_SCRATCH_DIR}/tmp/mykey.pem -pubout > ${TEST_SCRATCH_DIR}/tmp/mykey.pub run_buildah push $WITH_POLICY_JSON --tls-verify=false --creds testuser:testpassword --encryption-key jwe:${TEST_SCRATCH_DIR}/tmp/mykey.pub busybox oci:${TEST_SCRATCH_DIR}/tmp/busybox_enc # Try encrypted image without key should fail run_buildah 125 from oci:${TEST_SCRATCH_DIR}/tmp/busybox_enc expect_output --substring "does not match config's DiffID" # Try encrypted image with wrong key should fail run_buildah 125 from --decryption-key ${TEST_SCRATCH_DIR}/tmp/mykey2.pem oci:${TEST_SCRATCH_DIR}/tmp/busybox_enc expect_output --substring "decrypting layer .* no suitable key unwrapper found or none of the private keys could be used for decryption" # Providing the right key should succeed run_buildah from --decryption-key ${TEST_SCRATCH_DIR}/tmp/mykey.pem oci:${TEST_SCRATCH_DIR}/tmp/busybox_enc rm -rf ${TEST_SCRATCH_DIR}/tmp } @test "from encrypted registry image" { _prefetch busybox mkdir ${TEST_SCRATCH_DIR}/tmp openssl genrsa -out ${TEST_SCRATCH_DIR}/tmp/mykey.pem 2048 openssl genrsa -out ${TEST_SCRATCH_DIR}/tmp/mykey2.pem 2048 openssl rsa -in ${TEST_SCRATCH_DIR}/tmp/mykey.pem -pubout > ${TEST_SCRATCH_DIR}/tmp/mykey.pub start_registry run_buildah push $WITH_POLICY_JSON --tls-verify=false --creds testuser:testpassword --encryption-key jwe:${TEST_SCRATCH_DIR}/tmp/mykey.pub busybox docker://localhost:${REGISTRY_PORT}/buildah/busybox_encrypted:latest # Try encrypted image without key should fail run_buildah 125 from --tls-verify=false --creds testuser:testpassword docker://localhost:${REGISTRY_PORT}/buildah/busybox_encrypted:latest expect_output --substring "does not match config's DiffID" # Try encrypted image with wrong key should fail run_buildah 125 from --tls-verify=false --creds testuser:testpassword --decryption-key ${TEST_SCRATCH_DIR}/tmp/mykey2.pem docker://localhost:${REGISTRY_PORT}/buildah/busybox_encrypted:latest expect_output --substring "decrypting layer .* no suitable key unwrapper found or none of the private keys could be used for decryption" # Providing the right key should succeed run_buildah from --tls-verify=false --creds testuser:testpassword --decryption-key ${TEST_SCRATCH_DIR}/tmp/mykey.pem docker://localhost:${REGISTRY_PORT}/buildah/busybox_encrypted:latest run_buildah rm -a run_buildah rmi localhost:${REGISTRY_PORT}/buildah/busybox_encrypted:latest rm -rf ${TEST_SCRATCH_DIR}/tmp } @test "from with non buildah container" { skip_if_in_container skip_if_no_podman _prefetch busybox podman create --net=host --name busyboxc-podman busybox top run_buildah from $WITH_POLICY_JSON --name busyboxc busybox expect_output --substring "busyboxc" podman rm -f busyboxc-podman run_buildah rm busyboxc } @test "from --arch test" { skip_if_no_runtime _prefetch alpine run_buildah from --quiet --pull $WITH_POLICY_JSON --arch=arm64 alpine other=$output run_buildah from --quiet --pull $WITH_POLICY_JSON --arch=$(go env GOARCH) alpine cid=$output run_buildah copy --from $other $cid /etc/apk/arch /root/other-arch run_buildah run $cid cat /root/other-arch expect_output "aarch64" run_buildah from --quiet --pull $WITH_POLICY_JSON --arch=s390x alpine other=$output run_buildah copy --from $other $cid /etc/apk/arch /root/other-arch run_buildah run $cid cat /root/other-arch expect_output "s390x" } @test "from --platform test" { skip_if_no_runtime run_buildah version platform=$(grep ^BuildPlatform: <<< "$output") echo "$platform" platform=${platform##* } echo "$platform" _prefetch alpine run_buildah from --quiet --pull $WITH_POLICY_JSON --platform=linux/arm64 alpine other=$output run_buildah from --quiet --pull $WITH_POLICY_JSON --platform=${platform} alpine cid=$output run_buildah copy --from $other $cid /etc/apk/arch /root/other-arch run_buildah run $cid cat /root/other-arch expect_output "aarch64" run_buildah from --quiet --pull $WITH_POLICY_JSON --platform=linux/s390x alpine other=$output run_buildah copy --from $other $cid /etc/apk/arch /root/other-arch run_buildah run $cid cat /root/other-arch expect_output "s390x" } @test "from --authfile test" { _prefetch busybox start_registry run_buildah login --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth --username testuser --password testpassword localhost:${REGISTRY_PORT} run_buildah push $WITH_POLICY_JSON --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth busybox docker://localhost:${REGISTRY_PORT}/buildah/busybox:latest target=busybox-image run_buildah from -q $WITH_POLICY_JSON --tls-verify=false --authfile ${TEST_SCRATCH_DIR}/test.auth docker://localhost:${REGISTRY_PORT}/buildah/busybox:latest run_buildah rm $output run_buildah rmi localhost:${REGISTRY_PORT}/buildah/busybox:latest } @test "from --cap-add/--cap-drop test" { _prefetch alpine CAP_DAC_OVERRIDE=2 # unlikely to change # Try with default caps. run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah run $cid awk '/^CapEff/{print $2;}' /proc/self/status defaultcaps="$output" run_buildah rm $cid if ((0x$defaultcaps & 0x$CAP_DAC_OVERRIDE)); then run_buildah from --quiet --cap-drop CAP_DAC_OVERRIDE --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah run $cid awk '/^CapEff/{print $2;}' /proc/self/status droppedcaps="$output" run_buildah rm $cid if ((0x$droppedcaps & 0x$CAP_DAC_OVERRIDE)); then die "--cap-drop did not drop DAC_OVERRIDE: $droppedcaps" fi else run_buildah from --quiet --cap-add CAP_DAC_OVERRIDE --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah run $cid awk '/^CapEff/{print $2;}' /proc/self/status addedcaps="$output" run_buildah rm $cid if (( !(0x$addedcaps & 0x$CAP_DAC_OVERRIDE) )); then die "--cap-add did not add DAC_OVERRIDE: $addedcaps" fi fi } @test "from ulimit test" { _prefetch alpine run_buildah from -q --ulimit cpu=300 $WITH_POLICY_JSON alpine cid=$output run_buildah run $cid /bin/sh -c "ulimit -t" expect_output "300" "ulimit -t" } @test "from isolation test" { skip_if_rootless_environment _prefetch alpine run_buildah from -q --isolation chroot $WITH_POLICY_JSON alpine cid=$output run_buildah inspect $cid expect_output --substring '"Isolation": "chroot"' if [ -z "${BUILDAH_ISOLATION}" ]; then run readlink /proc/self/ns/pid host_pidns=$output run_buildah run --pid private $cid readlink /proc/self/ns/pid # chroot isolation doesn't make a new PID namespace. expect_output "${host_pidns}" fi } @test "from cgroup-parent test" { skip_if_rootless_environment skip_if_chroot _prefetch alpine # with cgroup-parent run_buildah from -q --cgroupns=host --cgroup-parent test-cgroup $WITH_POLICY_JSON alpine cid=$output run_buildah --cgroup-manager cgroupfs run $cid /bin/sh -c 'cat /proc/$$/cgroup' expect_output --substring "test-cgroup" # without cgroup-parent run_buildah from -q $WITH_POLICY_JSON alpine cid=$output run_buildah --cgroup-manager cgroupfs run $cid /bin/sh -c 'cat /proc/$$/cgroup' if [ -n "$(grep "test-cgroup" <<< "$output")" ]; then die "Unexpected cgroup." fi } @test "from-image-with-zstd-compression" { copy --format oci --dest-compress --dest-compress-format zstd docker://quay.io/libpod/alpine_nginx:latest dir:${TEST_SCRATCH_DIR}/base-image run_buildah from dir:${TEST_SCRATCH_DIR}/base-image } @test "from proxy test" { skip_if_no_runtime _prefetch alpine tmp=$RANDOM run_buildah from --quiet --pull $WITH_POLICY_JSON alpine cid=$output FTP_PROXY=$tmp run_buildah run $cid printenv FTP_PROXY expect_output "$tmp" ftp_proxy=$tmp run_buildah run $cid printenv ftp_proxy expect_output "$tmp" HTTP_PROXY=$tmp run_buildah run $cid printenv HTTP_PROXY expect_output "$tmp" https_proxy=$tmp run_buildah run $cid printenv https_proxy expect_output "$tmp" BOGUS_PROXY=$tmp run_buildah 1 run $cid printenv BOGUS_PROXY } @test "from-image-by-id" { skip_if_chroot skip_if_no_runtime _prefetch busybox run_buildah from --cidfile ${TEST_SCRATCH_DIR}/cid busybox cid=$(< ${TEST_SCRATCH_DIR}/cid) createrandom ${TEST_SCRATCH_DIR}/randomfile run_buildah copy ${cid} ${TEST_SCRATCH_DIR}/randomfile / run_buildah commit --iidfile ${TEST_SCRATCH_DIR}/iid ${cid} iid=$(< ${TEST_SCRATCH_DIR}/iid) run_buildah from --cidfile ${TEST_SCRATCH_DIR}/cid2 ${iid} cid2=$(< ${TEST_SCRATCH_DIR}/cid2) run_buildah run ${cid2} cat /etc/hosts truncated=${iid##*:} truncated="${truncated:0:12}" expect_output --substring ${truncated}-working-container run_buildah run ${cid2} hostname -f expect_output "${cid2:0:12}" } ================================================ FILE: tests/help.bats ================================================ #!/usr/bin/env bats load helpers # run 'buildah help', parse the output looking for 'Available Commands'; # return that list. function buildah_commands() { run_buildah help "$@" |\ awk '/^Available Commands:/{ok=1;next}/^Flags:/{ok=0}ok { print $1 }' |\ grep . } function check_help() { local count=0 local -A found for cmd in $(buildah_commands "$@"); do # Human-readable buildah command string, with multiple spaces collapsed command_string="buildah $* $cmd" command_string=${command_string// / } # 'buildah x' -> 'buildah x' # help command and --help flag have the same output run_buildah help "$@" $cmd local full_help=$output # The line immediately after 'Usage:' gives us a 1-line synopsis usage=$(echo "$output" | grep -A1 '^Usage:' | tail -1) [ -n "$usage" ] || die "$command_string: no Usage message found" expr "$usage" : "^ $command_string" > /dev/null || die "$command_string: Usage string doesn't match command" # If usage ends in '[command]', recurse into subcommands if expr "$usage" : '.*\[command\]$' >/dev/null; then found[subcommands]=1 check_help "$@" $cmd continue fi # Cross-check: if usage includes '[flags]', there must be a # longer 'Flags:' section in the full --help output; vice-versa, # if 'Flags:' is in full output, usage line must have '[flags]'. if expr "$usage" : '.*\[flags' >/dev/null; then if ! expr "$full_help" : ".*Flags:" >/dev/null; then die "$command_string: Usage includes '[flags]' but has no 'Flags:' subsection" fi elif expr "$full_help" : ".*Flags:" >/dev/null; then die "$command_string: --help has 'Flags:' section but no '[flags]' in synopsis" fi count=$(expr $count + 1) done run_buildah "$@" --help full_usage=$output # Any command that takes subcommands, must show usage if called without one. run_buildah "$@" expect_output "$full_usage" # 'NoSuchCommand' subcommand shows usage unless the command is root 'buildah' command. if [ -n "$*" ]; then run_buildah "$@" NoSuchCommand expect_output "$full_usage" else run_buildah 125 "$@" NoSuchCommand expect_output --substring "unknown command" fi # This can happen if the output of --help changes, such as between # the old command parser and cobra. assert "$count" -gt 0 \ "Internal error: no commands found in 'buildah help $@' list" # Sanity check: make sure the special loops above triggered at least once. # (We've had situations where a typo makes the conditional never run in podman) if [ -z "$*" ]; then # This loop is copied from podman test and redundant for buildah now. # But this is kept for future extension. for i in subcommands; do if [[ -z ${found[$i]} ]]; then die "Internal error: '$i' subtest did not trigger" fi done fi # This can happen if the output of --help changes, such as between # the old command parser and cobra. assert "$count" -gt 0 \ "Internal error: no commands found in 'buildah help list" } @test "buildah help - basic tests" { check_help } ================================================ FILE: tests/helpers.bash ================================================ #!/usr/bin/env bash # Directory in which tests live TEST_SOURCES=${TEST_SOURCES:-$(dirname ${BASH_SOURCE})} BUILDAH_BINARY=${BUILDAH_BINARY:-$TEST_SOURCES/../bin/buildah} IMGTYPE_BINARY=${IMGTYPE_BINARY:-$TEST_SOURCES/../bin/imgtype} COPY_BINARY=${COPY_BINARY:-$TEST_SOURCES/../bin/copy} TUTORIAL_BINARY=${TUTORIAL_BINARY:-$TEST_SOURCES/../bin/tutorial} INET_BINARY=${INET_BINARY:-$TEST_SOURCES/../bin/inet} DUMPSPEC_BINARY=${DUMPSPEC_BINARY:-$TEST_SOURCES/../bin/dumpspec} CRASH_BINARY=${CRASH_BINARY:-$TEST_SOURCES/../bin/crash} WAIT_BINARY=${WAIT_BINARY:-$TEST_SOURCES/../bin/wait} PASSWD_BINARY=${PASSWD_BINARY:-$TEST_SOURCES/../bin/passwd} GRPCNOOP_BINARY=${GRPCNOOP_BINARY:-$TEST_SOURCES/../bin/grpcnoop} STORAGE_DRIVER=${STORAGE_DRIVER:-vfs} PATH=$(dirname ${BASH_SOURCE})/../bin:${PATH} OCI=${BUILDAH_RUNTIME:-$(${BUILDAH_BINARY} info --format '{{.host.OCIRuntime}}' || command -v runc || command -v crun)} # Default timeout for a buildah command. BUILDAH_TIMEOUT=${BUILDAH_TIMEOUT:-300} # Safe reliable unchanging test image SAFEIMAGE_REGISTRY=${SAFEIMAGE_REGISTRY:-quay.io} SAFEIMAGE_USER=${SAFEIMAGE_USER:-libpod} SAFEIMAGE_NAME=${SAFEIMAGE_NAME:-testimage} SAFEIMAGE_TAG=${SAFEIMAGE_TAG:-20221018} SAFEIMAGE_DIGEST=sha256:c782d03c968791b10300fb15478f7555be560329c5182ea27cba5fc299c98911 SAFEIMAGE="${SAFEIMAGE:-$SAFEIMAGE_REGISTRY/$SAFEIMAGE_USER/$SAFEIMAGE_NAME:$SAFEIMAGE_TAG}" # Prompt to display when logging buildah commands; distinguish root/rootless _LOG_PROMPT='$' if [ $(id -u) -eq 0 ]; then _LOG_PROMPT='#' fi # Shortcut for directory containing Containerfiles for bud.bats BUDFILES=${TEST_SOURCES}/bud # Used hundreds of times throughout all the tests WITH_POLICY_JSON="--signature-policy ${TEST_SOURCES}/policy.json" # We don't invoke gnupg directly in many places, but this avoids ENOTTY errors # when we invoke it directly in batch mode, and CI runs us without a terminal # attached. export GPG_TTY=/dev/null function setup(){ setup_tests } function setup_tests() { pushd "$(dirname "$(readlink -f "$BASH_SOURCE")")" # $TEST_SCRATCH_DIR is a custom scratch directory for each @test, # but it is NOT EMPTY! It is the caller's responsibility to make # empty subdirectories as needed. All of it will be deleted upon # test completion. # # buildah/podman: "repository name must be lowercase". # me: "but it's a local file path, not a repository name!" # buildah/podman: "i dont care. no caps anywhere!" TEST_SCRATCH_DIR=$(mktemp -d --dry-run --tmpdir=${BATS_TMPDIR:-${TMPDIR:-/tmp}} buildah_tests.XXXXXX | tr A-Z a-z) mkdir --mode=0700 $TEST_SCRATCH_DIR mkdir -p ${TEST_SCRATCH_DIR}/{root,runroot,sigstore,registries.d} cat >${TEST_SCRATCH_DIR}/registries.d/default.yaml < /dev/null go build -o serve ${TEST_SOURCES}/serve/serve.go portfile=$(mktemp) if test -z "${portfile}"; then echo error creating temporary file exit 1 fi pidfile=$(mktemp) if test -z "${pidfile}"; then echo error creating temporary file exit 1 fi sh -c "./serve \"${1:-${BATS_TMPDIR}}\" 0 \"${portfile}\" \"${3}\" \"${4}\" ${pidfile} &" waited=0 while ! test -s ${pidfile} ; do sleep 0.1 if test $((++waited)) -ge 300 ; then echo test http server did not write pid file within timeout exit 1 fi done HTTP_SERVER_PID=$(< ${pidfile}) rm -f ${pidfile} waited=0 while ! test -s ${portfile} ; do sleep 0.1 if test $((++waited)) -ge 300 ; then echo test http server did not start listening within timeout exit 1 fi done HTTP_SERVER_PORT=$(< ${portfile}) rm -f ${portfile} popd > /dev/null } function stophttpd() { if test -n "$HTTP_SERVER_PID" ; then kill -HUP ${HTTP_SERVER_PID} unset HTTP_SERVER_PID unset HTTP_SERVER_PORT fi true } function teardown(){ teardown_tests } function teardown_tests() { stophttpd stop_git_daemon stop_registry # Workaround for #1991 - buildah + overlayfs leaks mount points. # Many tests leave behind /var/tmp/.../root/overlay and sub-mounts; # let's find those and clean them up, otherwise 'rm -rf' fails. # 'sort -r' guarantees that we umount deepest subpaths first. mount |\ awk '$3 ~ testdir { print $3 }' testdir="^${TEST_SCRATCH_DIR}/" |\ sort -r |\ xargs --no-run-if-empty --max-lines=1 umount rm -fr ${TEST_SCRATCH_DIR} popd } function normalize_image_name() { for img in "$@"; do if [[ "${img##*/}" == "$img" ]] ; then echo -n docker.io/library/"$img" elif [[ docker.io/"${img##*/}" == "$img" ]] ; then echo -n docker.io/library/"${img##*/}" else echo -n "$img" fi done } function _prefetch() { if [ -z "${_BUILDAH_IMAGE_CACHEDIR}" ]; then export _BUILDAH_IMAGE_CACHEDIR=${BATS_SUITE_TMPDIR}/buildah-image-cache mkdir -p ${_BUILDAH_IMAGE_CACHEDIR} # It's 700 by default; that prevents 'unshare' from reading cached images chmod 711 ${BATS_SUITE_TMPDIR:?is unset} ${BATS_SUITE_TMPDIR}/.. fi local storage= for img in "$@"; do if [[ "$img" =~ '[vfs@' ]] ; then storage="$img" continue fi img=$(normalize_image_name "$img") echo "# [checking for: $img]" >&2 fname=$(tr -c a-zA-Z0-9.- - <<< "$img") ( flock --timeout 300 9 || die "Could not flock"; _prefetch_locksafe $img $fname ) 9> $_BUILDAH_IMAGE_CACHEDIR/$fname.lock done } # DO NOT CALL THIS. EVER. This must only be called from _prefetch(). function _prefetch_locksafe() { local img="$1" local fname="$2" if [ -d $_BUILDAH_IMAGE_CACHEDIR/$fname ]; then echo "# [restoring from cache: $_BUILDAH_IMAGE_CACHEDIR / $img]" >&2 copy dir:$_BUILDAH_IMAGE_CACHEDIR/$fname containers-storage:"$storage""$img" else rm -fr ${_BUILDAH_IMAGE_CACHEDIR:?THIS CAN NEVER HAPPEN}/$fname echo "# [copy docker://$img dir:$_BUILDAH_IMAGE_CACHEDIR/$fname]" >&2 for attempt in $(seq 3) ; do if copy $COPY_REGISTRY_OPTS docker://"$img" dir:$_BUILDAH_IMAGE_CACHEDIR/$fname ; then break else # Failed. Clean up, so we don't leave incomplete remnants rm -fr ${_BUILDAH_IMAGE_CACHEDIR:?THIS CAN NEVER HAPPEN EITHER}/$fname fi sleep 5 done echo "# [copy dir:$_BUILDAH_IMAGE_CACHEDIR/$fname containers-storage:$storage$img]" >&2 copy dir:$_BUILDAH_IMAGE_CACHEDIR/$fname containers-storage:"$storage""$img" fi } function createrandom() { dd if=/dev/urandom bs=1 count=${2:-256} of=${1:-${BATS_TMPDIR}/randomfile} status=none } ################### # random_string # Returns a pseudorandom human-readable string ################### # # Numeric argument, if present, is desired length of string # function random_string() { local length=${1:-10} head /dev/urandom | tr -dc a-zA-Z0-9 | head -c$length } ############## # safename # Returns a pseudorandom string suitable for container/image/etc names ############## # # Name will include the bats test number and a pseudorandom element, # eg "t123-xyz123". safename() will return the same string across # multiple invocations within a given test; this makes it easier for # a maintainer to see common name patterns. # # String is lower-case so it can be used as an image name # function safename() { safenamepath=$BATS_SUITE_TMPDIR/.safename.$BATS_SUITE_TEST_NUMBER if [[ ! -e $safenamepath ]]; then echo -n "t${BATS_SUITE_TEST_NUMBER}-$(random_string 8 | tr A-Z a-z)" >$safenamepath fi cat $safenamepath } function buildah() { ${BUILDAH_BINARY} ${BUILDAH_REGISTRY_OPTS} ${ROOTDIR_OPTS} "$@" } function imgtype() { ${IMGTYPE_BINARY} ${ROOTDIR_OPTS} "$@" } function copy() { ${COPY_BINARY} --max-parallel-downloads=1 ${ROOTDIR_OPTS} ${BUILDAH_REGISTRY_OPTS} "$@" } function podman() { command ${PODMAN_BINARY:-podman} ${PODMAN_REGISTRY_OPTS} ${ROOTDIR_OPTS} "$@" } # There are various scenarios where we would like to execute `tests` as rootless user, however certain commands like `buildah mount` # do not work in rootless session since a normal user cannot mount a filesystem unless they're in a user namespace along with its # own mount namespace. In order to run such specific commands from a rootless session we must perform `buildah unshare`. # Following function makes sure that invoked command is triggered inside a `buildah unshare` session if env is rootless. function run_unshared() { if is_rootless; then $BUILDAH_BINARY unshare "$@" else command "$@" fi } function mkdir() { run_unshared mkdir "$@" } function touch() { run_unshared touch "$@" } function cp() { run_unshared cp "$@" } function rm() { run_unshared rm "$@" } ################# # run_with_log # Logs command before running it ################# # function run_with_log() { local expected_rc=0 local retry=1 local cmd="$*" case "$1" in [0-9]) expected_rc=$1; shift;; [1-9][0-9]) expected_rc=$1; shift;; [12][0-9][0-9]) expected_rc=$1; shift;; '?') expected_rc= ; shift;; # ignore exit code --retry) retry=3; shift;; # retry with sleep of 1 sec esac while [ $retry -gt 0 ]; do retry=$(( retry - 1 )) echo "$_LOG_PROMPT $cmd" run "$@" echo "$output" if [ "$status" -ne 0 ]; then echo -n "[ rc=$status "; if [ -n "$expected_rc" ]; then if [ "$status" -eq "$expected_rc" ]; then echo -n "(expected) "; else echo -n "(** EXPECTED $expected_rc **) "; fi fi echo "]" fi if [ -n "$expected_rc" ]; then if [ "$status" -eq "$expected_rc" ]; then return elif [ $retry -gt 0 ]; then echo "[ RETRYING ]" >&2 sleep 1 else die "exit code is $status; expected $expected_rc" fi fi done } ################# # run_buildah # Invoke buildah, with timeout, using BATS 'run' ################# # # This is the preferred mechanism for invoking buildah: # # * we use 'timeout' to abort (with a diagnostic) if something # takes too long; this is preferable to a CI hang. # * we log the command run and its output. This doesn't normally # appear in BATS output, but it will if there's an error. # * we check exit status. Since the normal desired code is 0, # that's the default; but the first argument can override: # # run_buildah 125 nonexistent-subcommand # run_buildah '?' some-other-command # let our caller check status # # Since we use the BATS 'run' mechanism, $output and $status will be # defined for our caller. # function run_buildah() { # Number as first argument = expected exit code; default 0 # --retry as first argument = retry 3 times on error (eg registry flakes) local expected_rc=0 local retry=1 case "$1" in [0-9]) expected_rc=$1; shift;; [1-9][0-9]) expected_rc=$1; shift;; [12][0-9][0-9]) expected_rc=$1; shift;; '?') expected_rc= ; shift;; # ignore exit code --retry) retry=3; shift;; # retry network flakes esac # Remember command args, for possible use in later diagnostic messages MOST_RECENT_BUILDAH_COMMAND="buildah $*" # If session is rootless and `buildah mount` is invoked, perform unshare, # since normal user cannot mount a filesystem unless they're in a user namespace along with its own mount namespace. if is_rootless; then if [[ "$1" =~ mount ]]; then set "unshare" "$BUILDAH_BINARY" ${BUILDAH_REGISTRY_OPTS} ${ROOTDIR_OPTS} "$@" fi fi while [ $retry -gt 0 ]; do retry=$(( retry - 1 )) # stdout is only emitted upon error; this echo is to help a debugger echo "${_LOG_PROMPT} ${BUILDAH_BINARY} $*" run env CONTAINERS_CONF=${CONTAINERS_CONF:-$(dirname ${BASH_SOURCE})/containers.conf} timeout --foreground --kill=10 $BUILDAH_TIMEOUT ${BUILDAH_BINARY} ${BUILDAH_REGISTRY_OPTS} ${ROOTDIR_OPTS} "$@" # without "quotes", multiple lines are glommed together into one if [ -n "$output" ]; then echo "$output" fi if [ "$status" -ne 0 ]; then echo -n "[ rc=$status "; if [ -n "$expected_rc" ]; then if [ "$status" -eq "$expected_rc" ]; then echo -n "(expected) "; else echo -n "(** EXPECTED $expected_rc **) "; fi fi echo "]" fi if [ "$status" -eq 124 -o "$status" -eq 137 ]; then # FIXME: 'timeout -v' requires coreutils-8.29; travis seems to have # an older version. If/when travis updates, please add -v # to the 'timeout' command above, and un-comment this out: # if expr "$output" : ".*timeout: sending" >/dev/null; then echo "*** TIMED OUT ***" # This does not get the benefit of a retry false fi if [ -n "$expected_rc" ]; then if [ "$status" -eq "$expected_rc" ]; then return elif [ $retry -gt 0 ]; then echo "[ RETRYING ]" >&2 sleep 30 else die "exit code is $status; expected $expected_rc" fi fi done } ######### # die # Abort with helpful message ######### function die() { echo "#/vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv" >&2 echo "#| FAIL: $*" >&2 echo "#\\^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" >&2 false } ############ # assert # Compare actual vs expected string; fail if mismatch ############ # # Compares string (default: $output) against the given string argument. # By default we do an exact-match comparison against $output, but there # are two different ways to invoke us, each with an optional description: # # assert "EXPECT" [DESCRIPTION] # assert "RESULT" "OP" "EXPECT" [DESCRIPTION] # # The first form (one or two arguments) does an exact-match comparison # of "$output" against "EXPECT". The second (three or four args) compares # the first parameter against EXPECT, using the given OPerator. If present, # DESCRIPTION will be displayed on test failure. # # Examples: # # assert "this is exactly what we expect" # assert "${lines[0]}" =~ "^abc" "first line begins with abc" # function assert() { local actual_string="$output" local operator='==' local expect_string="$1" local testname="$2" case "${#*}" in 0) die "Internal error: 'assert' requires one or more arguments" ;; 1|2) ;; 3|4) actual_string="$1" operator="$2" expect_string="$3" testname="$4" ;; *) die "Internal error: too many arguments to 'assert" ;; esac # Comparisons. # Special case: there is no !~ operator, so fake it via '! x =~ y' local not= local actual_op="$operator" if [[ $operator == '!~' ]]; then not='!' actual_op='=~' fi if [[ $operator == '=' || $operator == '==' ]]; then # Special case: we can't use '=' or '==' inside [[ ... ]] because # the right-hand side is treated as a pattern... and '[xy]' will # not compare literally. There seems to be no way to turn that off. if [ "$actual_string" = "$expect_string" ]; then return fi elif [[ $operator == '!=' ]]; then # Same special case as above if [ "$actual_string" != "$expect_string" ]; then return fi else if eval "[[ $not \$actual_string $actual_op \$expect_string ]]"; then return elif [ $? -gt 1 ]; then die "Internal error: could not process 'actual' $operator 'expect'" fi fi # Test has failed. Get a descriptive test name. if [ -z "$testname" ]; then testname="${MOST_RECENT_BUILDAH_COMMAND:-[no test name given]}" fi # Display optimization: the typical case for 'expect' is an # exact match ('='), but there are also '=~' or '!~' or '-ge' # and the like. Omit the '=' but show the others; and always # align subsequent output lines for ease of comparison. local op='' local ws='' if [ "$operator" != '==' ]; then op="$operator " ws=$(printf "%*s" ${#op} "") fi # This is a multi-line message, which may in turn contain multi-line # output, so let's format it ourself, readably local actual_split IFS=$'\n' read -rd '' -a actual_split <<<"$actual_string" || true printf "#/vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n" >&2 printf "#| FAIL: %s\n" "$testname" >&2 printf "#| expected: %s'%s'\n" "$op" "$expect_string" >&2 printf "#| actual: %s'%s'\n" "$ws" "${actual_split[0]}" >&2 local line for line in "${actual_split[@]:1}"; do printf "#| > %s'%s'\n" "$ws" "$line" >&2 done printf "#\\^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n" >&2 false } ################### # expect_output # [obsolete; kept for compatibility] ################### # # An earlier version of assert(). # function expect_output() { # By default we examine $output, the result of run_buildah local actual="$output" local operator='==' # option processing: recognize --from="...", --substring local opt for opt; do local value=$(expr "$opt" : '[^=]*=\(.*\)') case "$opt" in --from=*) actual="$value"; shift;; --substring) operator='=~'; shift;; --) shift; break;; -*) die "Invalid option '$opt'" ;; *) break;; esac done assert "$actual" "$operator" "$@" } ####################### # expect_line_count # Check the expected number of output lines ####################### # # ...from the most recent run_buildah command # function expect_line_count() { local expect="$1" local testname="${2:-${MOST_RECENT_BUILDAH_COMMAND:-[no test name given]}}" local actual="${#lines[@]}" if [ "$actual" -eq "$expect" ]; then return fi printf "#/vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n" >&2 printf "#| FAIL: $testname\n" >&2 printf "#| Expected %d lines of output, got %d\n" $expect $actual >&2 printf "#| Output was:\n" >&2 local line for line in "${lines[@]}"; do printf "#| >%s\n" "$line" >&2 done printf "#\\^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n" >&2 false } function check_options_flag_err() { flag="$1" [ "$status" -eq 125 ] [[ $output = *"no options ($flag) can be specified after"* ]] } ################# # is_rootless # Check if we run as normal user ################# function is_rootless() { [ "$(id -u)" -ne 0 ] } ############################# # has_supplemental_groups # Check that account has additional groups ############################# function has_supplemental_groups() { [ "$(id -g)" != "$(id -G)" ] } ################################# # skip_if_rootless_environment # `mount` or its variant needs unshare ################################# function skip_if_rootless_environment() { if is_rootless; then skip "${1:-test is being invoked from rootless environment and might need unshare}" fi } ################################# # skip_if_root_environment # ################################# function skip_if_root_environment() { if ! is_rootless; then skip "${1:-test is being invoked from root environment}" fi } #################### # skip_if_chroot # #################### function skip_if_chroot() { if test "$BUILDAH_ISOLATION" = "chroot"; then skip "${1:-test does not work when \$BUILDAH_ISOLATION = chroot}" fi } ###################### # skip_if_rootless # ###################### function skip_if_rootless() { if test "$BUILDAH_ISOLATION" = "rootless"; then skip "${1:-test does not work when \$BUILDAH_ISOLATION = rootless}" fi } ######################## # skip_if_no_runtime # 'buildah run' can't work without a runtime ######################## function skip_if_no_runtime() { if type -p "${OCI}" &> /dev/null; then return fi skip "runtime \"$OCI\" not found" } ####################### # skip_if_no_podman # we need 'podman' to test how we interact with podman ####################### function skip_if_no_podman() { run which ${PODMAN_BINARY:-podman} if [[ $status -ne 0 ]]; then skip "podman is not installed" fi } ########################## # skip_if_in_container # ########################## function skip_if_in_container() { if test "$CONTAINER" = "podman"; then skip "This test is not working inside a container" fi } ####################### # skip_if_no_docker # ####################### function skip_if_no_docker() { which docker || skip "docker is not installed" systemctl -q is-active docker || skip "docker.service is not active" # Confirm that this is really truly docker, not podman. docker_version=$(docker --version) if [[ $docker_version =~ podman ]]; then skip "this test needs actual docker, not podman-docker" fi } ######################## # skip_if_no_unshare # ######################## function skip_if_no_unshare() { run which ${UNSHARE_BINARY:-unshare} if [[ $status -ne 0 ]]; then skip "unshare is not installed" fi if ! unshare -Ur true ; then skip "unshare was not able to create a user namespace" fi if ! unshare -Urm true ; then skip "unshare was not able to create a mount namespace" fi if ! unshare -Urmpf true ; then skip "unshare was not able to create a pid namespace" fi if ! unshare -U --map-users $(id -u),0,1 true ; then skip "unshare does not support --map-users" fi if ! unshare -Ur --setuid 0 true ; then skip "unshare does not support --setuid" fi } ###################### # skip_unless_arch # ###################### function skip_unless_arch() { local goarch goarch="$(go env GOARCH 2>/dev/null)" [ -z "$goarch" ] && return 0 for arch in "$@"; do if [ "$arch" = "$goarch" ]; then return 0 fi done skip "test requires one of these architectures: $* (current: $goarch)" } ###################### # start_git_daemon # ###################### function start_git_daemon() { daemondir=${TEST_SCRATCH_DIR}/git-daemon mkdir -p ${daemondir}/repo gzip -dc < ${1:-${TEST_SOURCES}/git-daemon/repo.tar.gz} | tar x -C ${daemondir}/repo # git >=2.45 aborts with "dubious ownership" error if serving other user's files as root if ! is_rootless; then chown -R root:root ${daemondir}/repo fi ${INET_BINARY} -port-file ${TEST_SCRATCH_DIR}/git-daemon/port -pid-file=${TEST_SCRATCH_DIR}/git-daemon/pid -- git daemon --inetd --base-path=${daemondir} ${daemondir} & local waited=0 while ! test -s ${TEST_SCRATCH_DIR}/git-daemon/pid ; do sleep 0.1 if test $((++waited)) -ge 300 ; then echo test git server did not write pid file within timeout exit 1 fi done GITPORT=$(< ${TEST_SCRATCH_DIR}/git-daemon/port) } ##################### # stop_git_daemon # ##################### function stop_git_daemon() { if test -s ${TEST_SCRATCH_DIR}/git-daemon/pid ; then kill $(< ${TEST_SCRATCH_DIR}/git-daemon/pid) rm -f ${TEST_SCRATCH_DIR}/git-daemon/pid fi } #################### # start_registry # #################### # Bring up a registry server using buildah with vfs and chroot as a cheap # substitute for podman, accessible only to user $1 using password $2 on the # local system at a dynamically-allocated port. # Requires openssl. # A user name and password can be supplied as the two parameters, or default # values of "testuser" and "testpassword" will be used. # Sets REGISTRY_PID, REGISTRY_PORT (to append to "localhost:"), and # REGISTRY_DIR (where the CA cert can be found) on success. function start_registry() { local testuser="${1:-testuser}" local testpassword="${2:-testpassword}" local REGISTRY_IMAGE=quay.io/libpod/registry:2.8.2 local config=' version: 0.1 log: fields: service: registry storage: cache: blobdescriptor: inmemory filesystem: rootdirectory: /var/lib/registry http: addr: :0 headers: X-Content-Type-Options: [nosniff] tls: certificate: /etc/docker/registry/localhost.crt key: /etc/docker/registry/localhost.key health: storagedriver: enabled: true interval: 10s threshold: 3 auth: htpasswd: realm: buildah-realm path: /etc/docker/registry/htpasswd ' # roughly equivalent to "htpasswd -nbB testuser testpassword", the registry uses # the same package this does for verifying passwords against hashes in htpasswd files htpasswd=${testuser}:$(${PASSWD_BINARY} ${testpassword}) # generate the htpasswd and config.yml files for the registry mkdir -p "${TEST_SCRATCH_DIR}"/registry/root "${TEST_SCRATCH_DIR}"/registry/run "${TEST_SCRATCH_DIR}"/registry/certs "${TEST_SCRATCH_DIR}"/registry/config cat > "${TEST_SCRATCH_DIR}"/registry/config/htpasswd <<< "$htpasswd" cat > "${TEST_SCRATCH_DIR}"/registry/config/config.yml <<< "$config" chmod 644 "${TEST_SCRATCH_DIR}"/registry/config/htpasswd "${TEST_SCRATCH_DIR}"/registry/config/config.yml # generate a new key and certificate if ! openssl req -newkey rsa:4096 -nodes -sha256 -keyout "${TEST_SCRATCH_DIR}"/registry/certs/localhost.key -x509 -days 2 -addext "subjectAltName = DNS:localhost" -out "${TEST_SCRATCH_DIR}"/registry/certs/localhost.crt -subj "/CN=localhost" ; then die error creating new key and certificate fi chmod 644 "${TEST_SCRATCH_DIR}"/registry/certs/localhost.crt chmod 600 "${TEST_SCRATCH_DIR}"/registry/certs/localhost.key # use a copy of the server's certificate for validation from a client cp "${TEST_SCRATCH_DIR}"/registry/certs/localhost.crt "${TEST_SCRATCH_DIR}"/registry/ # create a container in its own storage _prefetch "[vfs@${TEST_SCRATCH_DIR}/registry/root+${TEST_SCRATCH_DIR}/registry/run]" ${REGISTRY_IMAGE} ctr=$(${BUILDAH_BINARY} --storage-driver vfs --root "${TEST_SCRATCH_DIR}"/registry/root --runroot "${TEST_SCRATCH_DIR}"/registry/run from --quiet --pull-never ${REGISTRY_IMAGE}) ${BUILDAH_BINARY} --storage-driver vfs --root "${TEST_SCRATCH_DIR}"/registry/root --runroot "${TEST_SCRATCH_DIR}"/registry/run copy $ctr "${TEST_SCRATCH_DIR}"/registry/config/htpasswd "${TEST_SCRATCH_DIR}"/registry/config/config.yml "${TEST_SCRATCH_DIR}"/registry/certs/localhost.key "${TEST_SCRATCH_DIR}"/registry/certs/localhost.crt /etc/docker/registry/ # fire it up coproc ${BUILDAH_BINARY} --storage-driver vfs --root "${TEST_SCRATCH_DIR}"/registry/root --runroot "${TEST_SCRATCH_DIR}"/registry/run run --net host "$ctr" /entrypoint.sh /etc/docker/registry/config.yml 2> "${TEST_SCRATCH_DIR}"/registry/registry.log # record the coprocess's ID and try to parse the listening port from the log # we're separating all of this from the storage for any test that might call # this function and using vfs to minimize the cleanup required REGISTRY_PID="${COPROC_PID}" REGISTRY_DIR="${TEST_SCRATCH_DIR}"/registry REGISTRY_PORT= local waited=0 while [ -z "${REGISTRY_PORT}" ] ; do if [ $waited -ge $BUILDAH_TIMEOUT ] ; then echo Could not determine listening port from log: sed -e 's/^/ >/' ${TEST_SCRATCH_DIR}/registry/registry.log stop_registry false fi waited=$((waited+1)) sleep 1 REGISTRY_PORT=$(sed -ne 's^.*listening on.*:\([0-9]\+\),.*^\1^p' ${TEST_SCRATCH_DIR}/registry/registry.log) done # push the registry image we just started... to itself, as a confidence check if ! ${BUILDAH_BINARY} --storage-driver vfs --root "${REGISTRY_DIR}"/root --runroot "${REGISTRY_DIR}"/run push --cert-dir "${REGISTRY_DIR}" --creds "${testuser}":"${testpassword}" "${REGISTRY_IMAGE}" localhost:"${REGISTRY_PORT}"/registry; then echo error pushing to /registry repository at localhost:$REGISTRY_PORT stop_registry false fi } ################### # stop_registry # ################### function stop_registry() { if test -n "${REGISTRY_PID}" ; then kill "${REGISTRY_PID}" wait "${REGISTRY_PID}" || true fi unset REGISTRY_PID unset REGISTRY_PORT if test -n "${REGISTRY_DIR}" ; then ${BUILDAH_BINARY} --storage-driver vfs --root "${REGISTRY_DIR}"/root --runroot "${REGISTRY_DIR}"/run rmi -a -f rm -fr "${REGISTRY_DIR}" fi unset REGISTRY_DIR } ############################### # oci_image_manifest_digest # ############################### # prints the digest of the form "sha256:xxx" of the manifest for the main image # in an OCI layout in "$1" function oci_image_manifest_digest() { run jq -r '.manifests[0].digest' "$1"/index.json assert $status = 0 "looking for the digest of the image manifest" assert "$output" != "" echo "$output" } ######################## # oci_image_manifest # ######################## # prints the relative path of the manifest for the main image in an OCI # layout in "$1" function oci_image_manifest() { local diff_id=$(oci_image_manifest_digest "$@") local alg=${diff_id%%:*} local val=${diff_id##*:} echo blobs/"$alg"/"$val" } ############################# # oci_image_config_digest # ############################# # prints the digest of the form "sha256:xxx" of the config blob for the main # image in an OCI layout in "$1" function oci_image_config_digest() { local digest=$(oci_image_manifest_digest "$1") local alg=${digest%%:*} local val=${digest##*:} run jq -r '.config.digest' "$1"/blobs/"$alg"/"$val" assert $status = 0 "looking for the digest of the image config" assert "$output" != "" echo "$output" } ###################### # oci_image_config # ###################### # prints the relative path of the config blob for the main image in an OCI # layout in "$1" function oci_image_config() { local diff_id=$(oci_image_config_digest "$@") local alg=${diff_id%%:*} local val=${diff_id##*:} echo blobs/"$alg"/"$val" } ######################## # oci_image_diff_ids # ######################## # prints the list of digests of the diff IDs for the main image in an OCI # layout in "$1" function oci_image_diff_ids() { local digest=$(oci_image_config_digest "$1") local alg=${digest%%:*} local val=${digest##*:} run jq -r '.rootfs.diff_ids[]' "$1"/blobs/"$alg"/"$val" assert $status = 0 "looking for the diff IDs in the image config" assert "$output" != "" echo "$output" } ####################### # oci_image_diff_id # ####################### # prints a single diff ID for the main image in an OCI layout in "$1", choosing # which one to print based on an index and arithmetic operands passed in # subsequent arguments function oci_image_diff_id() { local diff_ids=($(oci_image_diff_ids "$1")) shift case "$*" in -*) echo ${diff_ids[$((${#diff_ids[@]} "$@"))]} ;; *) echo ${diff_ids[$(("$@"))]} ;; esac } ############################ # oci_image_last_diff_id # ############################ # prints the diff ID of the most recent layer for the main image in an OCI # layout in "$1" function oci_image_last_diff_id() { local diff_id=($(oci_image_diff_id "$1" - 1)) echo "$diff_id" } #################### # oci_image_diff # #################### # prints the relative path of a single layer diff for the main image in an OCI # layout in "$1", choosing which one to print based on an index and arithmetic # operands passed in subsequent arguments function oci_image_diff() { local diff_id=$(oci_image_diff_id "$@") local alg=${diff_id%%:*} local val=${diff_id##*:} echo blobs/"$alg"/"$val" } ######################### # oci_image_last_diff # ######################### # prints the relative path of the most recent layer for the main image in an # OCI layout in "$1" function oci_image_last_diff() { local output=$(oci_image_diff "$1" - 1) echo "$output" } ############################# # dir_image_config_digest # ############################# # prints the digest of the form "sha256:xxx" of the config blob for the "dir" # image in "$1" function dir_image_config_digest() { run jq -r '.config.digest' "$1"/manifest.json assert $status = 0 "looking for the digest of the image config" assert "$output" != "" echo "$output" } ######################## # dir_image_diff_ids # ######################## # prints the list of digests of the diff IDs for the "dir" image in "$1" function dir_image_diff_ids() { local digest=$(dir_image_config_digest "$1") local alg=${digest%%:*} local val=${digest##*:} run jq -r '.rootfs.diff_ids[]' "$1"/"$val" assert $status = 0 "looking for the diff IDs in the image config" assert "$output" != "" echo "$output" } ####################### # dir_image_diff_id # ####################### # prints a single diff ID for the "dir" image in "$1", choosing which one to # print based on an index and arithmetic operands passed in subsequent # arguments function dir_image_diff_id() { local diff_ids=($(dir_image_diff_ids "$1")) shift case "$*" in -*) echo ${diff_ids[$((${#diff_ids[@]} "$@"))]} ;; *) echo ${diff_ids[$(("$@"))]} ;; esac } ############################ # dir_image_last_diff_id # ############################ # prints the diff ID of the most recent layer for "dir" image in "$1" function dir_image_last_diff_id() { local diff_id=($(dir_image_diff_id "$1" - 1)) echo "$diff_id" } ###################### # dir_image_config # ###################### # prints the relative path of the config blob for the "dir" image in "$1" function dir_image_config() { local diff_id=$(dir_image_config_digest "$@") local alg=${diff_id%%:*} local val=${diff_id##*:} echo "$val" } #################### # dir_image_diff # #################### # prints the relative path of a single layer diff for the "dir" image in "$1", # choosing which one to print based on an index and arithmetic operands passed # in subsequent arguments function dir_image_diff() { local diff_id=$(dir_image_diff_id "$@") local alg=${diff_id%%:*} local val=${diff_id##*:} echo "$val" } ######################### # dir_image_last_diff # ######################### # prints the relative path of the most recent layer for "dir" image in "$1" function dir_image_last_diff() { local output=$(dir_image_diff "$1" - 1) echo "$output" } #################################### # convert_v1_shares_to_v2_weight # #################################### function convert_v1_shares_to_v2_weight() { # https://kubernetes.io/blog/2026/01/30/new-cgroup-v1-to-v2-cpu-conversion-formula/ # there's an old way to convert the value, and a new way to convert the value, and we # don't know which one our runtime is using, so return the values we would get from # using both methods local shares="$1" local oldconverted="$((1 + ((${shares} - 2) * 9999) / 262142))" test -n "$oldconverted" local newconverted=$(awk '{if ($1 <= 2) { print "1"} else if ($1 >= 262144) {print "10000"} else {l=log($1)/log(2); e=((((l+125)*l)/612.0) - 7.0/34.0); p = exp(e*log(10)); if ( p == int(p) ) {print p} else { print int(p+1) }}}' <<< "${shares}") test -n "$newconverted" echo "$oldconverted" "$newconverted" } ================================================ FILE: tests/helpers.bash.t ================================================ #!/bin/bash # # tests for helpers.bash # . $(dirname ${BASH_SOURCE})/helpers.bash INDEX=1 RC=0 # t (true) : tests that should pass function t() { result=$(assert "$@" 2>&1) status=$? if [[ $status -eq 0 ]]; then echo "ok $INDEX $*" else echo "not ok $INDEX $*" echo "$result" RC=1 fi INDEX=$((INDEX + 1)) } # f (false) : tests that should fail function f() { result=$(assert "$@" 2>&1) status=$? if [[ $status -ne 0 ]]; then echo "ok $INDEX ! $*" else echo "not ok $INDEX ! $* [passed, should have failed]" RC=1 fi INDEX=$((INDEX + 1)) } t "" = "" t "a" != "" t "" != "a" t "a" = "a" t "aa" == "aa" t "a[b]{c}" = "a[b]{c}" t "abcde" =~ "a" t "abcde" =~ "b" t "abcde" =~ "c" t "abcde" =~ "d" t "abcde" =~ "e" t "abcde" =~ "ab" t "abcde" =~ "abc" t "abcde" =~ "abcd" t "abcde" =~ "bcde" t "abcde" =~ "cde" t "abcde" =~ "de" t "foo" =~ "foo" t "foobar" =~ "foo" t "barfoo" =~ "foo" t 'a "AB \"CD": ef' = 'a "AB \"CD": ef' t 'a "AB \"CD": ef' =~ 'a "AB \\"CD": ef' t 'abcdef' !~ 'efg' t 'abcdef' !~ 'x' ########### f "a" = "b" f "a" == "b" f "abcde" =~ "x" f "abcde" !~ "a" f "abcde" !~ "ab" f "abcde" !~ "abc" f "" != "" exit $RC ================================================ FILE: tests/history.bats ================================================ #!/usr/bin/env bats load helpers function testconfighistory() { config="$1" expected="$2" container=$(echo "c$config" | sed -E -e 's|[[:blank:]]|_|g' -e "s,[-=/:'],_,g" | tr '[A-Z]' '[a-z]') image=$(echo "i$config" | sed -E -e 's|[[:blank:]]|_|g' -e "s,[-=/:'],_,g" | tr '[A-Z]' '[a-z]') run_buildah from --name "$container" --format docker scratch run_buildah config $config --add-history "$container" run_buildah commit $WITH_POLICY_JSON "$container" "$image" run_buildah inspect --format '{{range .Docker.History}}{{println .CreatedBy}}{{end}}' "$image" run_buildah inspect --format '{{range .Docker.History}}{{println .CreatedBy}}{{end}}' "$image" expect_output --substring "$expected" if test "$3" != "not-oci" ; then run_buildah inspect --format '{{range .OCIv1.History}}{{println .CreatedBy}}{{end}}' "$image" expect_output --substring "$expected" fi } @test "history-cmd" { testconfighistory "--cmd /foo" "CMD /foo" } @test "history-entrypoint" { testconfighistory "--entrypoint /foo" "ENTRYPOINT /foo" } @test "history-env" { testconfighistory "--env FOO=BAR" "ENV FOO=BAR" } @test "history-healthcheck" { run_buildah from --name healthcheckctr --format docker scratch run_buildah config --healthcheck "CMD /foo" --healthcheck-timeout=10s --healthcheck-interval=20s --healthcheck-retries=7 --healthcheck-start-period=30s --add-history healthcheckctr run_buildah commit $WITH_POLICY_JSON healthcheckctr healthcheckimg run_buildah inspect --format '{{range .Docker.History}}{{println .CreatedBy}}{{end}}' healthcheckimg run_buildah inspect --format '{{range .Docker.History}}{{println .CreatedBy}}{{end}}' healthcheckimg expect_output --substring "HEALTHCHECK --interval=20s --retries=7 --start-period=30s --timeout=10s CMD /foo" } @test "history-label" { testconfighistory "--label FOO=BAR" "LABEL FOO=BAR" } @test "history-onbuild" { run_buildah from --name onbuildctr --format docker scratch run_buildah config --onbuild "CMD /foo" --add-history onbuildctr run_buildah commit $WITH_POLICY_JSON onbuildctr onbuildimg run_buildah inspect --format '{{range .Docker.History}}{{println .CreatedBy}}{{end}}' onbuildimg run_buildah inspect --format '{{range .Docker.History}}{{println .CreatedBy}}{{end}}' onbuildimg expect_output --substring "ONBUILD CMD /foo" } @test "commit-with-omit-history-set-to-true" { run_buildah from --name onbuildctr --format docker scratch run_buildah config --onbuild "CMD /foo" --add-history onbuildctr run_buildah commit --omit-history $WITH_POLICY_JSON onbuildctr onbuildimg run_buildah inspect --format "{{index .Docker.History}}" onbuildimg expect_output "[]" } @test "history-port" { testconfighistory "--port 80/tcp" "EXPOSE 80/tcp" } @test "history-shell" { testconfighistory "--shell /bin/wish" "SHELL /bin/wish" } @test "history-stop-signal" { testconfighistory "--stop-signal SIGHUP" "STOPSIGNAL SIGHUP" not-oci } @test "history-user" { testconfighistory "--user 10:10" "USER 10:10" } @test "history-volume" { testconfighistory "--volume /foo" "VOLUME /foo" } @test "history-workingdir" { testconfighistory "--workingdir /foo" "WORKDIR /foo" } @test "history-add" { createrandom ${TEST_SCRATCH_DIR}/randomfile run_buildah from --name addctr --format docker scratch run_buildah add --add-history addctr ${TEST_SCRATCH_DIR}/randomfile digest="$output" run_buildah commit $WITH_POLICY_JSON addctr addimg run_buildah inspect --format '{{range .Docker.History}}{{println .CreatedBy}}{{end}}' addimg run_buildah inspect --format '{{range .Docker.History}}{{println .CreatedBy}}{{end}}' addimg expect_output --substring "ADD file:$digest" } @test "history-copy" { createrandom ${TEST_SCRATCH_DIR}/randomfile run_buildah from --name copyctr --format docker scratch run_buildah copy --add-history copyctr ${TEST_SCRATCH_DIR}/randomfile digest="$output" run_buildah commit $WITH_POLICY_JSON copyctr copyimg run_buildah inspect --format '{{range .Docker.History}}{{println .CreatedBy}}{{end}}' copyimg run_buildah inspect --format '{{range .Docker.History}}{{println .CreatedBy}}{{end}}' copyimg expect_output --substring "COPY file:$digest" } @test "history-run" { _prefetch busybox run_buildah from --name runctr --format docker $WITH_POLICY_JSON busybox run_buildah run --add-history runctr -- uname -a run_buildah commit $WITH_POLICY_JSON runctr runimg run_buildah inspect --format '{{range .Docker.History}}{{println .CreatedBy}}{{end}}' runimg run_buildah inspect --format '{{range .Docker.History}}{{println .CreatedBy}}{{end}}' runimg expect_output --substring "/bin/sh -c uname -a" } @test "history should not contain vars in allowlist unless set in ARG" { _prefetch busybox ctxdir=${TEST_SCRATCH_DIR}/bud mkdir -p $ctxdir cat >$ctxdir/Dockerfile <$ctxdir/Dockerfile < ${contentdir}/README.md starthttpd ${contentdir} run_buildah bud $WITH_POLICY_JSON --build-arg=HTTP_SERVER_PORT=${HTTP_SERVER_PORT} --layers -t test $BUDFILES/use-layers run_buildah images expect_line_count 3 run_buildah images -a expect_line_count 9 # create a no name image which should show up when doing buildah images without the --all flag run_buildah bud $WITH_POLICY_JSON --build-arg=HTTP_SERVER_PORT=${HTTP_SERVER_PORT} $BUDFILES/use-layers run_buildah images expect_line_count 4 } @test "images filter test" { _prefetch registry.k8s.io/pause busybox run_buildah from --quiet --pull=false $WITH_POLICY_JSON registry.k8s.io/pause cid1=$output run_buildah from --quiet --pull=false $WITH_POLICY_JSON busybox cid2=$output run_buildah 125 images --noheading --filter since registry.k8s.io/pause expect_output 'Error: invalid image filter "since": must be in the format "filter=value or filter!=value"' run_buildah images --noheading --filter since=registry.k8s.io/pause expect_line_count 1 # pause* and u* should only give us pause image not busybox since its a AND between # two filters run_buildah images --noheading --filter "reference=pause*" --filter "reference=u*" expect_line_count 1 } @test "images format test" { _prefetch alpine busybox run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid1=$output run_buildah from --quiet --pull=false $WITH_POLICY_JSON busybox cid2=$output run_buildah images --format "{{.Name}}" expect_line_count 2 } @test "images noheading test" { _prefetch alpine busybox run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid1=$output run_buildah from --quiet --pull=false $WITH_POLICY_JSON busybox cid2=$output run_buildah images --noheading expect_line_count 2 } @test "images quiet test" { _prefetch alpine busybox run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid1=$output run_buildah from --quiet --pull=false $WITH_POLICY_JSON busybox cid2=$output run_buildah images --quiet expect_line_count 2 } @test "images no-trunc test" { _prefetch alpine busybox run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid1=$output run_buildah from --quiet --pull=false $WITH_POLICY_JSON busybox cid2=$output run_buildah images -q --no-trunc expect_line_count 2 expect_output --substring --from="${lines[0]}" "sha256" } @test "images json test" { _prefetch alpine busybox run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON busybox for img in '' alpine busybox; do run_buildah images --json $img run jq -re 'length > 0 and all(.[]; has("id") and has("names") and has("digest") and has("createdat") and has("size") and has("readonly") and has("history"))' <<<"$output" expect_output "true" assert "$status" -eq 0 "status from jq for $img" done } @test "images json dup test" { run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah commit $WITH_POLICY_JSON $cid test run_buildah tag test new-name run_buildah images --json expect_output --substring '"id": ' } @test "images json valid" { run_buildah from $WITH_POLICY_JSON scratch cid1=$output run_buildah from $WITH_POLICY_JSON scratch cid2=$output run_buildah commit $WITH_POLICY_JSON $cid1 test run_buildah commit $WITH_POLICY_JSON $cid2 test2 run_buildah images --json run python3 -m json.tool <<< "$output" assert "$status" -eq 0 "status from python json.tool" } @test "specify an existing image" { _prefetch alpine busybox run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid1=$output run_buildah from --quiet --pull=false $WITH_POLICY_JSON busybox cid2=$output run_buildah images alpine expect_line_count 2 } @test "specify a nonexistent image" { run_buildah 125 images alpine expect_output --from="${lines[0]}" "Error: alpine: image not known" expect_line_count 1 } @test "Test dangling images" { run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah commit $WITH_POLICY_JSON $cid test run_buildah commit $WITH_POLICY_JSON $cid test run_buildah images expect_line_count 3 run_buildah images --filter dangling=true expect_output --substring " " expect_line_count 2 run_buildah images --filter dangling=false expect_output --substring " latest " expect_line_count 2 } @test "image digest test" { _prefetch busybox run_buildah pull $WITH_POLICY_JSON busybox run_buildah images --digests expect_output --substring "sha256:" } @test "images in OCI format with no creation dates" { mkdir -p $TEST_SCRATCH_DIR/blobs/sha256 # Create a layer. dd if=/dev/zero bs=512 count=2 of=$TEST_SCRATCH_DIR/blob layerdigest=$(sha256sum $TEST_SCRATCH_DIR/blob | awk '{print $1}') layersize=$(stat -c %s $TEST_SCRATCH_DIR/blob) mv $TEST_SCRATCH_DIR/blob $TEST_SCRATCH_DIR/blobs/sha256/${layerdigest} # Create a configuration blob that doesn't include a "created" date. now=$(TZ=UTC date +%Y-%m-%dT%H:%M:%S.%NZ) arch=$(go env GOARCH) cat > $TEST_SCRATCH_DIR/blob << EOF { "architecture": "$arch", "os": "linux", "config": { "Env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ], "Cmd": [ "sh" ] }, "rootfs": { "type": "layers", "diff_ids": [ "sha256:${layerdigest}" ] }, "history": [ { "created": "${now}", "created_by": "/bin/sh -c #(nop) ADD file:${layerdigest} in / " } ] } EOF configdigest=$(sha256sum $TEST_SCRATCH_DIR/blob | awk '{print $1}') configsize=$(stat -c %s $TEST_SCRATCH_DIR/blob) mv $TEST_SCRATCH_DIR/blob $TEST_SCRATCH_DIR/blobs/sha256/${configdigest} # Create a manifest for that configuration blob and layer. cat > $TEST_SCRATCH_DIR/blob << EOF { "schemaVersion": 2, "config": { "mediaType": "application/vnd.oci.image.config.v1+json", "digest": "sha256:${configdigest}", "size": ${configsize} }, "layers": [ { "mediaType": "application/vnd.oci.image.layer.v1.tar", "digest": "sha256:${layerdigest}", "size": ${layersize} } ] } EOF manifestdigest=$(sha256sum $TEST_SCRATCH_DIR/blob | awk '{print $1}') manifestsize=$(stat -c %s $TEST_SCRATCH_DIR/blob) mv $TEST_SCRATCH_DIR/blob $TEST_SCRATCH_DIR/blobs/sha256/${manifestdigest} # Add the manifest to the image index. cat > $TEST_SCRATCH_DIR/index.json << EOF { "schemaVersion": 2, "manifests": [ { "mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": "sha256:${manifestdigest}", "size": ${manifestsize} } ] } EOF # Mark the directory as a layout directory. echo -n '{"imageLayoutVersion": "1.0.0"}' > $TEST_SCRATCH_DIR/oci-layout # Import the image. run_buildah pull oci:$TEST_SCRATCH_DIR # Inspect the image. We shouldn't crash. run_buildah inspect ${configdigest} # List images. We shouldn't crash. run_buildah images } @test "Test two image names" { _prefetch alpine busybox run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON busybox run_buildah 125 images --filter dangling=true alpine busybox expect_output "Error: 'buildah images' requires at most 1 argument" run_buildah 125 images alpine busybox expect_output "Error: 'buildah images' requires at most 1 argument" run_buildah 125 images --noheading alpine busybox expect_output "Error: 'buildah images' requires at most 1 argument" } ================================================ FILE: tests/imgtype/imgtype.go ================================================ package main import ( "context" "encoding/json" "flag" "fmt" "os" "strings" "github.com/containers/buildah" "github.com/containers/buildah/define" "github.com/containers/buildah/docker" "github.com/containers/buildah/util" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" "go.podman.io/image/v5/manifest" is "go.podman.io/image/v5/storage" "go.podman.io/image/v5/transports/alltransports" "go.podman.io/image/v5/types" "go.podman.io/storage" "go.podman.io/storage/pkg/unshare" ) func main() { if buildah.InitReexec() { return } unshare.MaybeReexecUsingUserNamespace(false) storeOptions, err := storage.DefaultStoreOptions() if err != nil { storeOptions = storage.StoreOptions{} } expectedManifestType := "" expectedConfigType := "" debug := flag.Bool("debug", false, "turn on debug logging") root := flag.String("root", storeOptions.GraphRoot, "storage root directory") runroot := flag.String("runroot", storeOptions.RunRoot, "storage runtime directory") driver := flag.String("storage-driver", storeOptions.GraphDriverName, "storage driver") imagestore := flag.String("imagestore", storeOptions.ImageStore, "storage imagestore") transientStore := flag.Bool("transient-store", storeOptions.TransientStore, "store some information in transient storage") opts := flag.String("storage-opts", "", "storage option list (comma separated)") policy := flag.String("signature-policy", "", "signature policy file") mtype := flag.String("expected-manifest-type", define.OCIv1ImageManifest, "expected manifest type") showm := flag.Bool("show-manifest", false, "output the manifest JSON") rebuildm := flag.Bool("rebuild-manifest", false, "rebuild the manifest JSON") showc := flag.Bool("show-config", false, "output the configuration JSON") rebuildc := flag.Bool("rebuild-config", false, "rebuild the configuration JSON") flag.Parse() logrus.SetLevel(logrus.ErrorLevel) if debug != nil && *debug { logrus.SetLevel(logrus.DebugLevel) } switch *mtype { case define.OCIv1ImageManifest: expectedManifestType = *mtype expectedConfigType = v1.MediaTypeImageConfig case define.Dockerv2ImageManifest: expectedManifestType = *mtype expectedConfigType = manifest.DockerV2Schema2ConfigMediaType case "*": expectedManifestType = "" expectedConfigType = "" default: logrus.Errorf("unknown -expected-manifest-type value, expected either %q or %q or %q", define.OCIv1ImageManifest, define.Dockerv2ImageManifest, "*") return } if root != nil { storeOptions.GraphRoot = *root } if runroot != nil { storeOptions.RunRoot = *runroot } if driver != nil { storeOptions.GraphDriverName = *driver storeOptions.GraphDriverOptions = nil } if imagestore != nil { storeOptions.ImageStore = *imagestore } if transientStore != nil { storeOptions.TransientStore = *transientStore } if opts != nil && *opts != "" { storeOptions.GraphDriverOptions = strings.Split(*opts, ",") } systemContext := &types.SystemContext{ SignaturePolicyPath: *policy, } args := flag.Args() if len(args) == 0 { flag.Usage() return } store, err := storage.GetStore(storeOptions) if err != nil { logrus.Errorf("error opening storage: %v", err) os.Exit(1) } is.Transport.SetStore(store) errors := false defer func() { store.Shutdown(false) //nolint:errcheck if errors { os.Exit(1) } }() for _, image := range args { var ref types.ImageReference oImage := v1.Image{} dImage := docker.V2Image{} oManifest := v1.Manifest{} dManifest := docker.V2S2Manifest{} manifestType := "" configType := "" ref, _, err := util.FindImage(store, "", systemContext, image) if err != nil { ref2, err2 := alltransports.ParseImageName(image) if err2 != nil { logrus.Errorf("error parsing reference %q to an image: %v", image, err) errors = true continue } ref = ref2 } ctx := context.Background() img, err := ref.NewImage(ctx, systemContext) if err != nil { logrus.Errorf("error opening image %q: %v", image, err) errors = true continue } defer img.Close() config, err := img.ConfigBlob(ctx) if err != nil { logrus.Errorf("error reading configuration from %q: %v", image, err) errors = true continue } manifest, manifestType, err := img.Manifest(ctx) if err != nil { logrus.Errorf("error reading manifest from %q: %v", image, err) errors = true continue } if expectedManifestType != "" && manifestType != expectedManifestType { logrus.Errorf("expected manifest type %q in %q, got %q", expectedManifestType, image, manifestType) errors = true continue } switch expectedManifestType { case define.OCIv1ImageManifest: err = json.Unmarshal(manifest, &oManifest) if err != nil { logrus.Errorf("error parsing manifest from %q: %v", image, err) errors = true continue } err = json.Unmarshal(config, &oImage) if err != nil { logrus.Errorf("error parsing config from %q: %v", image, err) errors = true continue } manifestType = oManifest.MediaType configType = oManifest.Config.MediaType case define.Dockerv2ImageManifest: err = json.Unmarshal(manifest, &dManifest) if err != nil { logrus.Errorf("error parsing manifest from %q: %v", image, err) errors = true continue } err = json.Unmarshal(config, &dImage) if err != nil { logrus.Errorf("error parsing config from %q: %v", image, err) errors = true continue } manifestType = dManifest.MediaType configType = dManifest.Config.MediaType } switch manifestType { case define.OCIv1ImageManifest: if rebuildm != nil && *rebuildm { err = json.Unmarshal(manifest, &oManifest) if err != nil { logrus.Errorf("error parsing manifest from %q: %v", image, err) errors = true continue } manifest, err = json.Marshal(oManifest) if err != nil { logrus.Errorf("error rebuilding manifest from %q: %v", image, err) errors = true continue } } if rebuildc != nil && *rebuildc { err = json.Unmarshal(config, &oImage) if err != nil { logrus.Errorf("error parsing config from %q: %v", image, err) errors = true continue } config, err = json.Marshal(oImage) if err != nil { logrus.Errorf("error rebuilding config from %q: %v", image, err) errors = true continue } } case define.Dockerv2ImageManifest: if rebuildm != nil && *rebuildm { err = json.Unmarshal(manifest, &dManifest) if err != nil { logrus.Errorf("error parsing manifest from %q: %v", image, err) errors = true continue } manifest, err = json.Marshal(dManifest) if err != nil { logrus.Errorf("error rebuilding manifest from %q: %v", image, err) errors = true continue } } if rebuildc != nil && *rebuildc { err = json.Unmarshal(config, &dImage) if err != nil { logrus.Errorf("error parsing config from %q: %v", image, err) errors = true continue } config, err = json.Marshal(dImage) if err != nil { logrus.Errorf("error rebuilding config from %q: %v", image, err) errors = true continue } } } if expectedConfigType != "" && configType != expectedConfigType { logrus.Errorf("expected config type %q in %q, got %q", expectedConfigType, image, configType) errors = true continue } if showm != nil && *showm { fmt.Println(string(manifest)) } if showc != nil && *showc { fmt.Println(string(config)) } } } ================================================ FILE: tests/inet/inet.go ================================================ package main import ( "errors" "flag" "fmt" "io" "net" "os" "os/exec" "path/filepath" "strconv" "syscall" multierror "github.com/hashicorp/go-multierror" "github.com/sirupsen/logrus" ) // This is similar to netcat's listen mode, except it logs which port it's // assigned if it's told to attempt to bind to port 0. Or it's similar to // inetd, if it wasn't a daemon, and it wasn't as well-written. func main() { pidFile := "" portFile := "" detach := false flag.BoolVar(&detach, "detach", false, "detach from terminal") flag.StringVar(&portFile, "port-file", "", "file to write listening port number") flag.StringVar(&pidFile, "pid-file", "", "file to write process ID to") flag.Parse() args := flag.Args() if len(args) < 1 { fmt.Printf("Usage: %s [-port-file filename] [-pid-file filename] command ...\n", filepath.Base(os.Args[0])) os.Exit(1) } // Start listening without specifying a port number. ln, err := net.ListenTCP("tcp", &net.TCPAddr{}) if err != nil { logrus.Fatalf("listening: %v", err) } // Retrieve the address we ended up bound to and write the port number // part to the specified file, if one was specified. addrString := ln.Addr().String() _, portString, err := net.SplitHostPort(addrString) if err != nil { logrus.Fatalf("finding the port number in %q: %v", addrString, err) } if portFile != "" { if err := os.WriteFile(portFile, []byte(portString), 0o644); err != nil { logrus.Fatalf("writing listening port to %q: %v", portFile, err) } defer os.Remove(portFile) } // Write our process ID to the specified file, if one was specified. if pidFile != "" { pid := strconv.Itoa(os.Getpid()) if err := os.WriteFile(pidFile, []byte(pid), 0o644); err != nil { logrus.Fatalf("writing pid %d to %q: %v", os.Getpid(), pidFile, err) } defer os.Remove(pidFile) } // Now we can log which port we're listening on. fmt.Printf("process %d listening on port %s\n", os.Getpid(), portString) closeCloser := func(closer io.Closer) { if err := closer.Close(); err != nil { logrus.Errorf("closing: %v", err) } } // Helper function to shuttle data between a reader and a writer. relay := func(reader io.Reader, writer io.Writer) error { buffer := make([]byte, 1024) for { nr, err := reader.Read(buffer) if err != nil { if errors.Is(err, io.EOF) { return nil } return err } if nr == 0 { return nil } if nr < 0 { // no error? break } nw, err := writer.Write(buffer[:nr]) if err != nil { return nil } if nw != nr { return fmt.Errorf("short write: %d != %d", nw, nr) } } return nil } for { // Accept the next incoming connection. conn, err := ln.AcceptTCP() if err != nil { logrus.Errorf("accepting new connection: %v", err) continue } if conn == nil { logrus.Error("no new connection?") continue } go func() { defer closeCloser(conn) rawConn, err := conn.SyscallConn() if err != nil { logrus.Errorf("getting underlying connection: %v", err) return } var setNonblockError error if err := rawConn.Control(func(fd uintptr) { setNonblockError = syscall.SetNonblock(int(fd), true) }); err != nil { logrus.Errorf("marking connection nonblocking (outer): %v", err) return } if setNonblockError != nil { logrus.Errorf("marking connection nonblocking (inner): %v", setNonblockError) return } // Create pipes for the subprocess's stdio. stdinReader, stdinWriter, err := os.Pipe() if err != nil { logrus.Errorf("opening pipe for stdin: %v", err) return } defer closeCloser(stdinWriter) stdoutReader, stdoutWriter, err := os.Pipe() if err != nil { logrus.Errorf("opening pipe for stdout: %v", err) closeCloser(stdinReader) return } defer closeCloser(stdoutReader) if err := syscall.SetNonblock(int(stdoutReader.Fd()), true); err != nil { logrus.Errorf("marking stdout reader nonblocking: %v", err) closeCloser(stdinReader) closeCloser(stdoutWriter) return } // Start the subprocess. cmd := exec.Command(args[0], args[1:]...) cmd.Stdin = stdinReader cmd.Stdout = stdoutWriter cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { logrus.Errorf("starting %v: %v", args, err) closeCloser(stdinReader) closeCloser(stdoutWriter) return } // Process the subprocess's stdio and wait for it to exit, // presumably when it runs out of data. var relayGroup multierror.Group relayGroup.Go(func() error { err := relay(conn, stdinWriter) closeCloser(stdinWriter) return err }) relayGroup.Go(func() error { err := relay(stdoutReader, conn) closeCloser(stdoutReader) return err }) relayGroup.Go(func() error { err := cmd.Wait() closeCloser(conn) return err }) merr := relayGroup.Wait() if merr != nil && merr.ErrorOrNil() != nil { logrus.Errorf("%v\n", merr) } }() } } ================================================ FILE: tests/info.bats ================================================ #!/usr/bin/env bats load helpers @test "info" { run_buildah info expect_output --substring "host" run_buildah info --format='{{.store}}' # All of the following keys must be present in results. Order # isn't guaranteed, nor is their value, but they must all exist. for key in ContainerStore GraphDriverName GraphRoot RunRoot;do expect_output --substring "map.*$key:" done } @test "logging levels" { # check that these logging levels are recognized run_buildah --log-level=trace info run_buildah --log-level=debug info run_buildah --log-level=warn info run_buildah --log-level=info info run_buildah --log-level=error info run_buildah --log-level=fatal info run_buildah --log-level=panic info # check that we reject bogus logging levels run_buildah 125 --log-level=telepathic info expect_output --substring "unable to parse log level: not a valid logrus Level" } ================================================ FILE: tests/inspect.bats ================================================ #!/usr/bin/env bats load helpers @test "inspect-flags-order-verification" { run_buildah 125 inspect img1 -f "{{.ContainerID}}" -t="container" check_options_flag_err "-f" run_buildah 125 inspect img1 --format="{{.ContainerID}}" check_options_flag_err "--format={{.ContainerID}}" run_buildah 125 inspect img1 -t="image" check_options_flag_err "-t=image" } @test "inspect" { _prefetch alpine run_buildah from --quiet --pull $WITH_POLICY_JSON alpine cid=$output run_buildah commit $WITH_POLICY_JSON "$cid" alpine-image # e.g. { map[] [PATH=/....] [] [/bin/sh] map[] map[] } run_buildah inspect --format '{{.OCIv1.Config}}' alpine expect_output --substring "map.*PATH=.*/bin/sh.*map" inspect_basic=$output # Now inspect the committed image. Output should be _mostly_ the same... run_buildah inspect --type image --format '{{.OCIv1.Config}}' alpine-image inspect_after_commit=$output # ...except that in #2510/#3036/#3829 we started adding a label with # buildah's version. Strip it out for comparison. run_buildah --version local -a output_fields=($output) buildah_version=${output_fields[2]} inspect_cleaned=$(echo "$inspect_after_commit" | sed "s/io.buildah.version:${buildah_version}//g") expect_output --from="$inspect_cleaned" "$inspect_basic" run_buildah images -q alpine-image imageid=$output run_buildah containers -q containerid=$output # This one should not include buildah version run_buildah inspect --format '{{.OCIv1.Config}}' $containerid expect_output "$inspect_basic" # This one should. run_buildah inspect --type image --format '{{.OCIv1.Config}}' $imageid expect_output "$inspect_after_commit" } @test "inspect-is-json" { _prefetch alpine run_buildah images -q --no-trunc imageid="$output" run_buildah inspect --type image "$imageid" inspectoutput="$output" # jq will complain if it's not valid JSON run jq . <<< "$inspectoutput" } @test "inspect-config-is-json" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah inspect alpine expect_output --substring 'Config.*\{' } @test "inspect-manifest-is-json" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah inspect alpine expect_output --substring 'Manifest.*\{' } @test "inspect-ociv1-is-json" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah inspect alpine expect_output --substring 'OCIv1.*\{' } @test "inspect-docker-is-json" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah inspect alpine expect_output --substring 'Docker.*\{' } @test "inspect-format-config-is-json" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah inspect --format "{{.Config}}" alpine expect_output --substring '\{' } @test "inspect-format-manifest-is-json" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah inspect --format "{{.Manifest}}" alpine expect_output --substring '\{' } @test "inspect-format-ociv1-is-json" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah inspect --format "{{.OCIv1}}" alpine expect_output --substring '\{' } @test "inspect-format-docker-is-json" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah inspect --format "{{.Docker}}" alpine expect_output --substring '\{' } @test "inspect-format-docker-variant" { # libimage.Normalize() converts Arch:"armhf" to Arch:"arm" and variant: "v7", # so check that platform normalization happens at least for that one run_buildah from --quiet --pull=false $WITH_POLICY_JSON --arch=armhf scratch cid=$output run_buildah inspect --format "{{.Docker.Architecture}}" $cid [[ "$output" == "arm" ]] run_buildah inspect --format "{{.Docker.Variant}}" $cid [[ "$output" == "v7" ]] } @test "inspect manifest and verify OCI annotation" { run_buildah manifest create foobar run_buildah manifest add foobar busybox # get digest of added instance sha=$(echo $output | awk '{print $2}') run_buildah manifest annotate --annotation hello=world foobar "$sha" run_buildah manifest inspect foobar # Must contain annotation key and value expect_output --substring "hello" expect_output --substring "world" } ================================================ FILE: tests/lists.bats ================================================ #!/usr/bin/env bats load helpers IMAGE_LIST=docker://registry.k8s.io/pause:3.1 IMAGE_LIST_DIGEST=docker://registry.k8s.io/pause@sha256:f78411e19d84a252e53bff71a4407a5686c46983a2c2eeed83929b888179acea IMAGE_LIST_INSTANCE=docker://registry.k8s.io/pause@sha256:f365626a556e58189fc21d099fc64603db0f440bff07f77c740989515c544a39 IMAGE_LIST_AMD64_INSTANCE_DIGEST=sha256:59eec8837a4d942cc19a52b8c09ea75121acc38114a2c68b98983ce9356b8610 IMAGE_LIST_ARM_INSTANCE_DIGEST=sha256:c84b0a3a07b628bc4d62e5047d0f8dff80f7c00979e1e28a821a033ecda8fe53 IMAGE_LIST_ARM64_INSTANCE_DIGEST=sha256:f365626a556e58189fc21d099fc64603db0f440bff07f77c740989515c544a39 IMAGE_LIST_PPC64LE_INSTANCE_DIGEST=sha256:bcf9771c0b505e68c65440474179592ffdfa98790eb54ffbf129969c5e429990 IMAGE_LIST_S390X_INSTANCE_DIGEST=sha256:882a20ee0df7399a445285361d38b711c299ca093af978217112c73803546d5e @test "manifest-create" { _prefetch busybox run_buildah inspect -f '{{ .FromImageDigest }}' busybox imagedigest="$output" run_buildah manifest create foo listid="$output" run_buildah 125 manifest create foo assert "$output" =~ "that name is already in use" run_buildah manifest create --amend foo assert "$output" == "$listid" run_buildah manifest create --amend --annotation red=blue foo busybox assert "$output" == "$listid" run_buildah manifest inspect foo assert "$output" =~ '"red": "blue"' assert "$output" =~ "${imagedigest}" # since manifest exists in local storage this should exit with `0` run_buildah manifest exists foo # since manifest does not exist in local storage this should exit with `1` run_buildah 1 manifest exists foo2 } @test "manifest-inspect-id" { run_buildah manifest create foo cid=$output run_buildah manifest inspect $cid } @test "manifest-add" { run_buildah manifest create foo run_buildah manifest add foo ${IMAGE_LIST} # since manifest exists in local storage this should exit with `0` run_buildah manifest exists foo # since manifest does not exist in local storage this should exit with `1` run_buildah 1 manifest exists foo2 run_buildah manifest rm foo } @test "manifest-add artifact" { _prefetch busybox createrandom $TEST_SCRATCH_DIR/randomfile2 createrandom $TEST_SCRATCH_DIR/randomfile run sha256sum $TEST_SCRATCH_DIR/randomfile blobencoded="${output%% *}" run_buildah manifest create foo run_buildah manifest add --artifact --artifact-type image/jpeg --artifact-layer-type image/not-validated --artifact-config-type text/x-not-really --artifact-subject busybox --artifact-annotation up=down foo $TEST_SCRATCH_DIR/randomfile2 run_buildah manifest add --artifact --artifact-type image/png --artifact-layer-type image/not-validated --artifact-config-type text/x-not-really --artifact-subject busybox --artifact-annotation left=right foo $TEST_SCRATCH_DIR/randomfile digest="${output##* }" alg="${digest%%:*}" encoded="${digest##*:}" run_buildah manifest annotate --annotation red=blue foo $TEST_SCRATCH_DIR/randomfile run_buildah manifest inspect foo assert "$output" =~ '"image/png"' assert "$output" =~ '"red": "blue"' run_buildah manifest push --all foo oci:$TEST_SCRATCH_DIR/pushed run cmp $TEST_SCRATCH_DIR/randomfile $TEST_SCRATCH_DIR/pushed/blobs/sha256/$blobencoded assert "$status" -eq 0 "pushed copy of random file did not match original" run cat $TEST_SCRATCH_DIR/pushed/blobs/$alg/$encoded assert "$status" -eq 0 "artifact manifest not found in expected location" assert "$output" =~ '"artifactType":"image/png"' "cat $TEST_SCRATCH_DIR/pushed/blobs/$alg/$encoded" assert "$output" =~ '"mediaType":"image/not-validated"' "cat $TEST_SCRATCH_DIR/pushed/blobs/$alg/$encoded" assert "$output" =~ '"mediaType":"text/x-not-really"' "cat $TEST_SCRATCH_DIR/pushed/blobs/$alg/$encoded" assert "$output" =~ '"annotations":\{"left":"right"\}' "cat $TEST_SCRATCH_DIR/pushed/blobs/$alg/$encoded" run_buildah manifest rm foo } @test "manifest-add-multiple-artifacts" { run_buildah manifest create foo createrandom $TEST_SCRATCH_DIR/randomfile4 createrandom $TEST_SCRATCH_DIR/randomfile3 run_buildah manifest add --artifact foo $TEST_SCRATCH_DIR/randomfile3 $TEST_SCRATCH_DIR/randomfile4 run_buildah manifest push --all foo oci:$TEST_SCRATCH_DIR/pushed } @test "manifest-add local image" { target=scratch-image run_buildah bud $WITH_POLICY_JSON -t ${target} $BUDFILES/from-scratch run_buildah manifest create foo run_buildah manifest add foo ${target} run_buildah manifest rm foo } @test "manifest-add-one" { # This test must be run on amd64 to get an error when pulling an image from a different platform skip_unless_arch amd64 run_buildah manifest create foo run_buildah manifest add --arch=arm64 foo ${IMAGE_LIST_INSTANCE} run_buildah manifest inspect foo expect_output --substring ${IMAGE_LIST_ARM64_INSTANCE_DIGEST} run_buildah 125 inspect --type image foo expect_output --substring "no image found" run_buildah inspect foo expect_output --substring ${IMAGE_LIST_ARM64_INSTANCE_DIGEST} } @test "manifest-add-all" { run_buildah manifest create foo run_buildah manifest add --all foo ${IMAGE_LIST} run_buildah manifest inspect foo expect_output --substring ${IMAGE_LIST_AMD64_INSTANCE_DIGEST} expect_output --substring ${IMAGE_LIST_ARM_INSTANCE_DIGEST} expect_output --substring ${IMAGE_LIST_ARM64_INSTANCE_DIGEST} expect_output --substring ${IMAGE_LIST_PPC64LE_INSTANCE_DIGEST} expect_output --substring ${IMAGE_LIST_S390X_INSTANCE_DIGEST} } @test "manifest-annotate global annotation" { _prefetch busybox run_buildah manifest create foo run_buildah manifest add foo busybox run_buildah manifest annotate --index --annotation red=blue foo run_buildah manifest inspect foo assert "$output" =~ '"red": "blue"' } @test "manifest-annotate instance annotation" { _prefetch busybox run_buildah manifest create foo run_buildah manifest add foo busybox instance="${output##* }" run_buildah manifest annotate --annotation red=blue foo "${instance}" run_buildah manifest annotate --os OperatingSystem foo "${instance}" run_buildah manifest annotate --arch aRCHITECTURE foo "${instance}" run_buildah manifest annotate --variant vARIANT foo "${instance}" run_buildah manifest annotate --features FEATURE1 --features FEATURE2 foo "${instance}" run_buildah manifest annotate --os-features OSFEATURE1 --os-features OSFEATURE2 foo "${instance}" run_buildah manifest inspect foo assert "$output" =~ '"red": "blue"' assert "$output" =~ '"os": "OperatingSystem"' assert "$output" =~ '"architecture": "aRCHITECTURE"' assert "$output" =~ '"variant": "vARIANT"' } @test "manifest-annotate subject" { _prefetch busybox "${IMAGE_LIST_INSTANCE##*://}" run_buildah manifest create foo run_buildah manifest add foo busybox run_buildah manifest annotate --subject "${IMAGE_LIST_INSTANCE##*://}" foo run_buildah inspect -f '{{ .FromImageDigest }}' "${IMAGE_LIST_INSTANCE##*://}" imagedigest="$output" run_buildah manifest inspect foo assert "$output" =~ "$imagedigest" } @test "manifest-remove" { run_buildah manifest create foo run_buildah manifest add --all foo ${IMAGE_LIST} run_buildah manifest inspect foo expect_output --substring ${IMAGE_LIST_ARM64_INSTANCE_DIGEST} run_buildah manifest remove foo ${IMAGE_LIST_ARM64_INSTANCE_DIGEST} run_buildah manifest inspect foo expect_output --substring ${IMAGE_LIST_AMD64_INSTANCE_DIGEST} expect_output --substring ${IMAGE_LIST_ARM_INSTANCE_DIGEST} expect_output --substring ${IMAGE_LIST_PPC64LE_INSTANCE_DIGEST} expect_output --substring ${IMAGE_LIST_S390X_INSTANCE_DIGEST} # ARM64 should now be gone arm64=$(grep ${IMAGE_LIST_ARM64_INSTANCE_DIGEST} <<< "$output" || true) assert "$arm64" = "" "arm64 instance digest found in manifest list" } @test "manifest-remove-not-found" { run_buildah manifest create foo run_buildah manifest add foo ${IMAGE_LIST} run_buildah 125 manifest remove foo sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef } @test "manifest-rm failures" { run_buildah 125 manifest rm foo1 expect_output --substring "foo1: image not known" } @test "manifest-push" { run_buildah manifest create foo run_buildah manifest add --all foo ${IMAGE_LIST} run_buildah manifest push $WITH_POLICY_JSON foo dir:${TEST_SCRATCH_DIR}/pushed case "$(go env GOARCH 2> /dev/null)" in amd64) IMAGE_LIST_EXPECTED_INSTANCE_DIGEST=${IMAGE_LIST_AMD64_INSTANCE_DIGEST} ;; arm64) IMAGE_LIST_EXPECTED_INSTANCE_DIGEST=${IMAGE_LIST_ARM64_INSTANCE_DIGEST} ;; arm) IMAGE_LIST_EXPECTED_INSTANCE_DIGEST=${IMAGE_LIST_ARM_INSTANCE_DIGEST} ;; ppc64le) IMAGE_LIST_EXPECTED_INSTANCE_DIGEST=${IMAGE_LIST_PPC64LE_INSTANCE_DIGEST} ;; s390x) IMAGE_LIST_EXPECTED_INSTANCE_DIGEST=${IMAGE_LIST_S390X_INSTANCE_DIGEST} ;; *) skip "current arch \"$(go env GOARCH 2> /dev/null)\" not present in manifest list" ;; esac run grep ${IMAGE_LIST_EXPECTED_INSTANCE_DIGEST##sha256} ${TEST_SCRATCH_DIR}/pushed/manifest.json assert "$status" -eq 0 "status code of grep for expected instance digest" } @test "manifest-push with retry" { run_buildah manifest create foo run_buildah manifest add --all foo ${IMAGE_LIST} run_buildah manifest push --retry 4 --retry-delay 4s $WITH_POLICY_JSON foo dir:${TEST_SCRATCH_DIR}/pushed case "$(go env GOARCH 2> /dev/null)" in amd64) IMAGE_LIST_EXPECTED_INSTANCE_DIGEST=${IMAGE_LIST_AMD64_INSTANCE_DIGEST} ;; arm64) IMAGE_LIST_EXPECTED_INSTANCE_DIGEST=${IMAGE_LIST_ARM64_INSTANCE_DIGEST} ;; arm) IMAGE_LIST_EXPECTED_INSTANCE_DIGEST=${IMAGE_LIST_ARM_INSTANCE_DIGEST} ;; ppc64le) IMAGE_LIST_EXPECTED_INSTANCE_DIGEST=${IMAGE_LIST_PPC64LE_INSTANCE_DIGEST} ;; s390x) IMAGE_LIST_EXPECTED_INSTANCE_DIGEST=${IMAGE_LIST_S390X_INSTANCE_DIGEST} ;; *) skip "current arch \"$(go env GOARCH 2> /dev/null)\" not present in manifest list" ;; esac run grep ${IMAGE_LIST_EXPECTED_INSTANCE_DIGEST##sha256} ${TEST_SCRATCH_DIR}/pushed/manifest.json assert "$status" -eq 0 "status code of grep for expected instance digest" } @test "manifest-push-all" { run_buildah manifest create foo run_buildah manifest add --all foo ${IMAGE_LIST} run_buildah manifest push $WITH_POLICY_JSON --all foo dir:${TEST_SCRATCH_DIR}/pushed run sha256sum ${TEST_SCRATCH_DIR}/pushed/* expect_output --substring ${IMAGE_LIST_AMD64_INSTANCE_DIGEST##sha256:} expect_output --substring ${IMAGE_LIST_ARM_INSTANCE_DIGEST##sha256:} expect_output --substring ${IMAGE_LIST_ARM64_INSTANCE_DIGEST##sha256:} expect_output --substring ${IMAGE_LIST_PPC64LE_INSTANCE_DIGEST##sha256:} expect_output --substring ${IMAGE_LIST_S390X_INSTANCE_DIGEST##sha256:} } @test "manifest-push-all-default-true" { run_buildah manifest push --help expect_output --substring "all.*\(default true\).*authfile" } @test "manifest-push-purge" { run_buildah manifest create foo run_buildah manifest add --arch=arm64 foo ${IMAGE_LIST} run_buildah manifest inspect foo run_buildah manifest push $WITH_POLICY_JSON --purge foo dir:${TEST_SCRATCH_DIR}/pushed run_buildah 125 manifest inspect foo } @test "manifest-push-rm" { run_buildah manifest create foo run_buildah manifest add --arch=arm64 foo ${IMAGE_LIST} run_buildah manifest inspect foo run_buildah manifest push $WITH_POLICY_JSON --rm foo dir:${TEST_SCRATCH_DIR}/pushed run_buildah 125 manifest inspect foo } @test "manifest-push should fail with nonexistent authfile" { run_buildah manifest create foo run_buildah manifest add --arch=arm64 foo ${IMAGE_LIST} run_buildah manifest inspect foo run_buildah 125 manifest push --authfile /tmp/nonexistent $WITH_POLICY_JSON --purge foo dir:${TEST_SCRATCH_DIR}/pushed } @test "manifest-from-tag" { run_buildah from $WITH_POLICY_JSON --name test-container ${IMAGE_LIST} run_buildah inspect --format '{{.OCIv1.Architecture}}' ${IMAGE_LIST#docker://} expect_output --substring amd64 run_buildah inspect --format '{{.OCIv1.Architecture}}' test-container expect_output --substring amd64 } @test "manifest-from-digest" { run_buildah from $WITH_POLICY_JSON --name test-container ${IMAGE_LIST_DIGEST} run_buildah inspect --format '{{.OCIv1.Architecture}}' ${IMAGE_LIST_DIGEST#docker://} expect_output --substring amd64 run_buildah inspect --format '{{.OCIv1.Architecture}}' test-container expect_output --substring amd64 } @test "manifest-from-instance" { run_buildah from $WITH_POLICY_JSON --name test-container ${IMAGE_LIST_INSTANCE} run_buildah inspect --format '{{.OCIv1.Architecture}}' ${IMAGE_LIST_INSTANCE#docker://} expect_output --substring amd64 run_buildah inspect --format '{{.OCIv1.Architecture}}' test-container expect_output --substring amd64 } @test "manifest-no-matching-instance" { # Check that local images which we can't load the config and history for # don't just break multi-layer builds. # # Create a test list with some stuff in it. run_buildah manifest create test-list run_buildah manifest add --all test-list ${IMAGE_LIST} # Remove the entry for the current arch from the list. arch=$(go env GOARCH) run_buildah manifest inspect test-list archinstance=$(jq -r '.manifests|map(select(.platform.architecture=="'$arch'"))[].digest' <<< "$output") run_buildah manifest remove test-list $archinstance # Try to build using the build cache. mkdir ${TEST_SCRATCH_DIR}/build echo 'much content, wow.' > ${TEST_SCRATCH_DIR}/build/content.txt echo 'FROM scratch' > ${TEST_SCRATCH_DIR}/build/Dockerfile echo 'ADD content.txt /' >> ${TEST_SCRATCH_DIR}/build/Dockerfile run_buildah bud --layers --iidfile ${TEST_SCRATCH_DIR}/image-id.txt ${TEST_SCRATCH_DIR}/build # Make sure we can add the new image to the list. run_buildah manifest add test-list $(< ${TEST_SCRATCH_DIR}/image-id.txt) } @test "manifest-add-to-list-from-storage" { run_buildah pull --arch=amd64 busybox run_buildah tag busybox test:amd64 run_buildah pull --arch=arm64 busybox run_buildah tag busybox test:arm64 run_buildah manifest create test run_buildah manifest add test test:amd64 run_buildah manifest add --variant=variant-something test test:arm64 run_buildah manifest inspect test # must contain amd64 expect_output --substring "amd64" # must contain arm64 expect_output --substring "arm64" # must contain variant v8 expect_output --substring "variant-something" } @test "manifest-create-list-from-storage" { run_buildah from --quiet --arch amd64 busybox cid=$output run_buildah commit $cid "$cid-committed:latest" run_buildah manifest create test:latest "$cid-committed:latest" run_buildah manifest inspect test # must contain amd64 expect_output --substring "amd64" # since manifest exists in local storage this should exit with `0` run_buildah manifest exists test:latest # since manifest does not exist in local storage this should exit with `1` run_buildah 1 manifest exists test2 } @test "manifest-skip-some-base-images-with-all-platforms" { start_registry run_buildah manifest create localhost:"${REGISTRY_PORT}"/base run_buildah manifest add --all localhost:"${REGISTRY_PORT}"/base ${IMAGE_LIST} # get a count of how many "real" base images there are run_buildah manifest inspect localhost:"${REGISTRY_PORT}"/base nbaseplatforms=$(grep '"platform"' <<< "$output" | wc -l) echo $nbaseplatforms base platforms # add some trash that we expect to skip in a --all-platforms build run_buildah build --manifest localhost:"${REGISTRY_PORT}"/base --platform unknown/unknown --no-cache -f $BUDFILES/from-scratch/Containerfile2 $BUDFILES/from-scratch run_buildah build --manifest localhost:"${REGISTRY_PORT}"/base --platform linux/unknown --no-cache -f $BUDFILES/from-scratch/Containerfile2 $BUDFILES/from-scratch run_buildah build --manifest localhost:"${REGISTRY_PORT}"/base --platform unknown/amd64p32 --no-cache -f $BUDFILES/from-scratch/Containerfile2 $BUDFILES/from-scratch # add a known combination of OS/arch that we can be pretty sure wasn't already there run_buildah build --manifest localhost:"${REGISTRY_PORT}"/base --platform linux/amd64p32 --no-cache -f $BUDFILES/from-scratch/Containerfile2 $BUDFILES/from-scratch # push the list to the local registry and clean up our local copy run_buildah manifest push --tls-verify=false --creds testuser:testpassword --all localhost:"${REGISTRY_PORT}"/base run_buildah rmi localhost:"${REGISTRY_PORT}"/base # build a new list based on the valid base images in the list we just pushed run_buildah build --tls-verify=false --creds testuser:testpassword --manifest derived --all-platforms --from localhost:"${REGISTRY_PORT}"/base $BUDFILES/from-base run_buildah manifest inspect derived nderivedplatforms=$(grep '"platform"' <<< "$output" | wc -l) echo $nderivedplatforms derived platforms nexpectedderivedplatforms=$((nbaseplatforms+1)) echo expected $nexpectedderivedplatforms derived platforms [[ $nderivedplatforms -eq $nexpectedderivedplatforms ]] } ================================================ FILE: tests/loglevel.bats ================================================ #!/usr/bin/env bats load helpers @test "log-level set to debug" { run_buildah --log-level=debug images -q expect_output --substring "level=debug " } @test "log-level set to info" { run_buildah --log-level=info images -q expect_output "" } @test "log-level set to warn" { run_buildah --log-level=warn images -q expect_output "" } @test "log-level set to error" { run_buildah --log-level=error images -q expect_output "" } @test "log-level set to invalid" { run_buildah 125 --log-level=invalid images -q expect_output --substring "unable to parse log level" } ================================================ FILE: tests/mkcw.bats ================================================ #!/usr/bin/env bats load helpers function mkcw_check_image() { local imageID="$1" # Mount the container and take a look at what it got from the image. run_buildah from "$imageID" local ctrID="$output" run_buildah mount "$ctrID" local mountpoint="$output" # Should have a /disk.img file. test -s "$mountpoint"/disk.img # Should have a krun-sev.json file. test -s "$mountpoint"/krun-sev.json # Should have an executable entrypoint binary. test -s "$mountpoint"/entrypoint test -x "$mountpoint"/entrypoint # Should have a sticky /tmp directory. test -d "$mountpoint"/tmp test -k "$mountpoint"/tmp # Decrypt, mount, and take a look around. uuid=$(cryptsetup luksUUID "$mountpoint"/disk.img) run_with_log cryptsetup luksOpen --key-file "$TEST_SCRATCH_DIR"/key "$mountpoint"/disk.img "$uuid" mkdir -p "$TEST_SCRATCH_DIR"/mount run_with_log mount /dev/mapper/"$uuid" "$TEST_SCRATCH_DIR"/mount # Should have a not-empty config file with parts of an image's config. test -s "$TEST_SCRATCH_DIR"/mount/.krun_config.json # Should have a /tmp directory, at least. test -d "$TEST_SCRATCH_DIR"/mount/tmp # Should have a /bin/sh file from the base image, at least. test -s "$TEST_SCRATCH_DIR"/mount/bin/sh || test -L "$TEST_SCRATCH_DIR"/mount/bin/sh if shift ; then if shift ; then for pair in "$@" ; do inner=${pair##*:} outer=${pair%%:*} cmp ${outer} "$TEST_SCRATCH_DIR"/mount/${inner} done fi fi # Clean up. run_with_log umount -f "$TEST_SCRATCH_DIR"/mount # `Retry` if `luksClose` fails with defaults of `run_with_log` because # when unmounting the filesystem mounted on the device /dev/mapper/"$uuid" # without `retry` somehow we end up in a state where mount is still being # used by the kernel because when we do `lsof /dev/mapper/"$uuid"` it # shows nothing but `dmsetup info -c $uuid` shows the device is still # under use. Adding `--retry` in between somehow fixes this. run_with_log --retry cryptsetup luksClose "$uuid" run_buildah umount "$ctrID" } @test "mkcw-convert" { skip_if_in_container skip_if_rootless_environment if ! which cryptsetup > /dev/null 2> /dev/null ; then skip "cryptsetup not found" fi _prefetch busybox # The important thing we need from $SAFEIMAGE is that it have >1 layer. # Per @nalind: # The error we were attempting to avoid was causing the disk image to lose # content from layers that weren't the last one (and as far as this test is # concerned, for images with one layer, the only layer is also the last layer), # and the presence of the second layer, empty as it is, means the image still # meets the test expectations. _prefetch $SAFEIMAGE createrandom ${TEST_SCRATCH_DIR}/randomfile1 createrandom ${TEST_SCRATCH_DIR}/randomfile2 echo -n mkcw-convert > "$TEST_SCRATCH_DIR"/key # image has one layer, check with all-lower-case TEE type name run_buildah mkcw --ignore-attestation-errors --type snp --passphrase=mkcw-convert --add-file ${TEST_SCRATCH_DIR}/randomfile1:/in-a-subdir/rnd1 busybox busybox-cw mkcw_check_image busybox-cw ${TEST_SCRATCH_DIR}/randomfile1:in-a-subdir/rnd1 # image has multiple layers, check with all-upper-case TEE type name run_buildah mkcw --ignore-attestation-errors --type SNP --passphrase=mkcw-convert --add-file ${TEST_SCRATCH_DIR}/randomfile2:rnd2 $SAFEIMAGE my-cw mkcw_check_image my-cw ${TEST_SCRATCH_DIR}/randomfile2:/rnd2 } @test "mkcw-commit" { skip_if_in_container skip_if_rootless_environment if ! which cryptsetup > /dev/null 2> /dev/null ; then skip "cryptsetup not found" fi _prefetch $SAFEIMAGE passphrase="mkcw commit $(random_string)" echo -n "$passphrase" > "$TEST_SCRATCH_DIR"/key run_buildah from $SAFEIMAGE ctrID="$output" iidfile="$TEST_SCRATCH_DIR/iid" run_buildah commit --iidfile $iidfile --cw type=SEV,ignore_attestation_errors,passphrase="$passphrase" "$ctrID" mkcw_check_image $(< $iidfile) run_buildah commit --iidfile $iidfile --cw type=sev,ignore_attestation_errors,passphrase="$passphrase" "$ctrID" mkcw_check_image $(< $iidfile) } @test "mkcw build" { skip_if_in_container skip_if_rootless_environment if ! which cryptsetup > /dev/null 2> /dev/null ; then skip "cryptsetup not found" fi _prefetch alpine echo -n "mkcw build" > "$TEST_SCRATCH_DIR"/key run_buildah build --iidfile "$TEST_SCRATCH_DIR"/iid --cw type=SEV,ignore_attestation_errors,passphrase="mkcw build" -f bud/env/Dockerfile.check-env bud/env mkcw_check_image $(< "$TEST_SCRATCH_DIR"/iid) run_buildah build --iidfile "$TEST_SCRATCH_DIR"/iid --cw type=sev,ignore_attestation_errors,passphrase="mkcw build" -f bud/env/Dockerfile.check-env bud/env mkcw_check_image $(< "$TEST_SCRATCH_DIR"/iid) # the key thing about this next bit is mixing --layers with a final # instruction in the Dockerfile that normally wouldn't produce a layer echo -n "mkcw build --layers" > "$TEST_SCRATCH_DIR"/key run_buildah build --iidfile "$TEST_SCRATCH_DIR"/iid --cw type=SEV,ignore_attestation_errors,passphrase="mkcw build --layers" --layers -f bud/env/Dockerfile.check-env bud/env mkcw_check_image $(< "$TEST_SCRATCH_DIR"/iid) } ================================================ FILE: tests/mount.bats ================================================ #!/usr/bin/env bats load helpers @test "mount-flags-order-verification" { run_buildah 125 mount cnt1 --notruncate path1 check_options_flag_err "--notruncate" run_buildah 125 mount cnt1 --notruncate check_options_flag_err "--notruncate" run_buildah 125 mount cnt1 path1 --notruncate check_options_flag_err "--notruncate" } @test "mount one container" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah mount "$cid" } @test "mount bad container" { run_buildah 125 mount badcontainer } @test "mount multi images" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid1=$output run_buildah from --quiet --pull-never $WITH_POLICY_JSON alpine cid2=$output run_buildah from --quiet --pull-never $WITH_POLICY_JSON alpine cid3=$output run_buildah mount "$cid1" "$cid2" "$cid3" } @test "mount multi images one bad" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid1=$output run_buildah from --quiet --pull-never $WITH_POLICY_JSON alpine cid2=$output run_buildah from --quiet --pull-never $WITH_POLICY_JSON alpine cid3=$output run_buildah 125 mount "$cid1" badcontainer "$cid2" "$cid3" } @test "list currently mounted containers" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid1=$output run_buildah mount "$cid1" run_buildah from --quiet --pull-never $WITH_POLICY_JSON alpine cid2=$output run_buildah mount "$cid2" run_buildah from --quiet --pull-never $WITH_POLICY_JSON alpine cid3=$output run_buildah mount "$cid3" run_buildah mount expect_line_count 3 expect_output --from="${lines[0]}" --substring "/tmp" "mount line 1 of 3" expect_output --from="${lines[1]}" --substring "/tmp" "mount line 2 of 3" expect_output --from="${lines[2]}" --substring "/tmp" "mount line 3 of 3" } ================================================ FILE: tests/namespaces.bats ================================================ #!/usr/bin/env bats load helpers @test "already-in-userns" { if test "$BUILDAH_ISOLATION" != "rootless" -o $UID == 0 ; then skip "BUILDAH_ISOLATION = $BUILDAH_ISOLATION" fi _prefetch alpine run_buildah from $WITH_POLICY_JSON --quiet alpine expect_output "alpine-working-container" ctr="$output" run_buildah unshare buildah run --isolation=oci "$ctr" echo hello expect_output "hello" } @test "user-and-network-namespace" { skip_if_rootless_environment skip_if_chroot skip_if_rootless RUNOPTS="${RUNC_BINARY:+--runtime $RUNC_BINARY}" # Check if we're running in an environment that can even test this. run readlink /proc/self/ns/user echo "readlink /proc/self/ns/user -> $output" [ $status -eq 0 ] || skip "user namespaces not supported" run readlink /proc/self/ns/net echo "readlink /proc/self/ns/net -> $output" [ $status -eq 0 ] || skip "network namespaces not supported" mynetns="$output" # Generate the mappings to use for using-a-user-namespace cases. uidbase=$((${RANDOM}+1024)) gidbase=$((${RANDOM}+1024)) uidsize=$((${RANDOM}+1024)) gidsize=$((${RANDOM}+1024)) # Create a container that uses that mapping. _prefetch alpine run_buildah from $WITH_POLICY_JSON --quiet --userns-uid-map 0:$uidbase:$uidsize --userns-gid-map 0:$gidbase:$gidsize alpine ctr="$output" # Check that with settings that require a user namespace, we also get a new network namespace by default. run_buildah run $RUNOPTS "$ctr" readlink /proc/self/ns/net assert "$output" != "$mynetns" "we should get a new network namespace" # Check that with settings that require a user namespace, we can still try to use the host's network namespace. run_buildah run $RUNOPTS --net=host "$ctr" readlink /proc/self/ns/net expect_output "$mynetns" # Check that we are not bind mounting /sys from the host with --net=container host_sys=$(grep "/sys " /proc/self/mountinfo | cut -d ' ' -f 3) run_buildah run $RUNOPTS --net=container "$ctr" sh -c 'grep "/sys " /proc/self/mountinfo | cut -d " " -f 3' assert "$output" != "$host_sys" # Create a container that doesn't use that mapping. run_buildah from $WITH_POLICY_JSON --quiet alpine ctr="$output" run_buildah run $RUNOPTS --net=host "$ctr" readlink /proc/self/ns/net expect_output "$mynetns" # Check that with settings that don't require a user namespace, we can request to use a per-container network namespace. run_buildah run $RUNOPTS --net=container "$ctr" readlink /proc/self/ns/net assert "$output" != "$mynetns" \ "[/proc/self/ns/net (--net=container) should not be '$mynetns']" run_buildah run $RUNOPTS --net=private "$ctr" readlink /proc/self/ns/net assert "$output" != "$mynetns" \ "[/proc/self/ns/net (--net=private) should not be '$mynetns']" run_buildah run $RUNOPTS "$ctr" readlink /proc/self/ns/net assert "$output" != "$mynetns" \ "[/proc/self/ns/net (--net="") should not be '$mynetns']" } # Helper for idmapping test: check UID or GID mapping # NOTE SIDE EFFECT: sets $rootxid for possible use by caller idmapping_check_map() { local _output_idmap=$1 local _expect_idmap=$2 local _testname=$3 assert "$_output_idmap" != "" "Internal error: output_idmap is empty" local _idmap=$(sed -E -e 's, +, ,g' -e 's,^ +,,g' <<< "${_output_idmap}") expect_output --from="$_idmap" "${_expect_idmap}" "$_testname" # SIDE EFFECT: Global: our caller may want this rootxid=$(sed -E -e 's,^([^ ]*) (.*) ([^ ]*),\2,' <<< "$_idmap") } # Helper for idmapping test: check file permissions idmapping_check_permission() { local _output_file_stat=$1 local _output_dir_stat=$2 expect_output --from="${_output_file_stat}" "1:1" "Check if a copied file gets the right permissions" expect_output --from="${_output_dir_stat}" "0:0" "Check if a copied directory gets the right permissions" } @test "idmapping" { skip_if_rootless_environment RUNOPTS="${RUNC_BINARY:+--runtime $RUNC_BINARY}" # Check if we're running in an environment that can even test this. run readlink /proc/self/ns/user echo "readlink /proc/self/ns/user -> $output" [ $status -eq 0 ] || skip "user namespaces not supported" mynamespace="$output" # Generate the mappings to use. uidbase=$((${RANDOM}+1024)) gidbase=$((${RANDOM}+1024)) uidsize=$((${RANDOM}+1024)) gidsize=$((${RANDOM}+1024)) # Test with no mappings. uidmapargs[0]= gidmapargs[0]= uidmaps[0]="0 0 4294967295" gidmaps[0]="0 0 4294967295" # Test with both UID and GID maps specified. uidmapargs[1]="--userns-uid-map=0:$uidbase:$uidsize" gidmapargs[1]="--userns-gid-map=0:$gidbase:$gidsize" uidmaps[1]="0 $uidbase $uidsize" gidmaps[1]="0 $gidbase $gidsize" # Conditionalize some tests on the subuid and subgid files being present. if test -s /etc/subuid ; then if test -s /etc/subgid ; then # Look for a name that's in both the subuid and subgid files. for candidate in $(sed -e 's,:.*,,g' /etc/subuid); do if test $(sed -e 's,:.*,,g' -e "/$candidate/!d" /etc/subgid) == "$candidate"; then # Read the start of the subuid/subgid ranges. Assume length=65536. userbase=$(sed -e "/^${candidate}:/!d" -e 's,^[^:]*:,,g' -e 's,:[^:]*,,g' /etc/subuid) groupbase=$(sed -e "/^${candidate}:/!d" -e 's,^[^:]*:,,g' -e 's,:[^:]*,,g' /etc/subgid) # Test specifying both the user and group names. uidmapargs[${#uidmaps[*]}]=--userns-uid-map-user=$candidate gidmapargs[${#gidmaps[*]}]=--userns-gid-map-group=$candidate uidmaps[${#uidmaps[*]}]="0 $userbase 65536" gidmaps[${#gidmaps[*]}]="0 $groupbase 65536" # Test specifying just the user name. uidmapargs[${#uidmaps[*]}]=--userns-uid-map-user=$candidate uidmaps[${#uidmaps[*]}]="0 $userbase 65536" gidmaps[${#gidmaps[*]}]="0 $groupbase 65536" # Test specifying just the group name. gidmapargs[${#gidmaps[*]}]=--userns-gid-map-group=$candidate uidmaps[${#uidmaps[*]}]="0 $userbase 65536" gidmaps[${#gidmaps[*]}]="0 $groupbase 65536" break fi done # Choose different names from the files. for candidateuser in $(sed -e 's,:.*,,g' /etc/subuid); do for candidategroup in $(sed -e 's,:.*,,g' /etc/subgid); do if test "$candidateuser" == "$candidate" ; then continue fi if test "$candidategroup" == "$candidate" ; then continue fi if test "$candidateuser" == "$candidategroup" ; then continue fi # Read the start of the ranges. Assume length=65536. userbase=$(sed -e "/^${candidateuser}:/!d" -e 's,^[^:]*:,,g' -e 's,:[^:]*,,g' /etc/subuid) groupbase=$(sed -e "/^${candidategroup}:/!d" -e 's,^[^:]*:,,g' -e 's,:[^:]*,,g' /etc/subgid) # Test specifying both the user and group names. uidmapargs[${#uidmaps[*]}]=--userns-uid-map-user=$candidateuser gidmapargs[${#gidmaps[*]}]=--userns-gid-map-group=$candidategroup uidmaps[${#uidmaps[*]}]="0 $userbase 65536" gidmaps[${#gidmaps[*]}]="0 $groupbase 65536" break done done fi fi touch ${TEST_SCRATCH_DIR}/somefile mkdir ${TEST_SCRATCH_DIR}/somedir touch ${TEST_SCRATCH_DIR}/somedir/someotherfile chmod 700 ${TEST_SCRATCH_DIR}/somedir/someotherfile chmod u+s ${TEST_SCRATCH_DIR}/somedir/someotherfile for i in $(seq 0 "$((${#uidmaps[*]}-1))") ; do # local helper function for checking /proc/self/ns/user function idmapping_check_namespace() { local _output=$1 local _testname=$2 assert "$_output" != "" "Internal error: _output is empty" if [ -z "${uidmapargs[$i]}${gidmapargs[$i]}" ]; then if test "$BUILDAH_ISOLATION" != "chroot" -a "$BUILDAH_ISOLATION" != "rootless" ; then expect_output --from="$_output" "$mynamespace" "/proc/self/ns/user ($_testname)" fi else assert "$_output" != "$mynamespace" "_output vs mynamespace" fi } # Create a container using these mappings. echo "Building container with $WITH_POLICY_JSON --quiet ${uidmapargs[$i]} ${gidmapargs[$i]} alpine" _prefetch alpine run_buildah from $WITH_POLICY_JSON --quiet ${uidmapargs[$i]} ${gidmapargs[$i]} alpine ctr="$output" # If we specified mappings, expect to be in a different namespace by default. run_buildah run $RUNOPTS "$ctr" readlink /proc/self/ns/user idmapping_check_namespace "$output" "container" # Check that we got the UID and GID mappings that we expected. # rootuid/rootgid are obtained (side effect) from helper function run_buildah run $RUNOPTS "$ctr" cat /proc/self/uid_map idmapping_check_map "$output" "${uidmaps[$i]}" "uid_map" rootuid=$rootxid run_buildah run $RUNOPTS "$ctr" cat /proc/self/gid_map idmapping_check_map "$output" "${gidmaps[$i]}" "gid_map" rootgid=$rootxid # Check that if we copy a file into the container, it gets the right permissions. run_buildah copy --chown 1:1 "$ctr" ${TEST_SCRATCH_DIR}/somefile / run_buildah run $RUNOPTS "$ctr" stat -c '%u:%g' /somefile output_file_stat="$output" # Check that if we copy a directory into the container, its contents get the right permissions. run_buildah copy "$ctr" ${TEST_SCRATCH_DIR}/somedir /somedir run_buildah run $RUNOPTS "$ctr" stat -c '%u:%g' /somedir output_dir_stat="$output" idmapping_check_permission "$output_file_stat" "$output_dir_stat" run_buildah run $RUNOPTS "$ctr" stat -c '%u:%g %a' /somedir/someotherfile expect_output "0:0 4700" "stat(someotherfile), in container test" # Check that the copied file has the right permissions on host. run_buildah mount "$ctr" mnt="$output" run stat -c '%u:%g %a' "$mnt"/somedir/someotherfile assert "$status" -eq 0 "status of stat $mnt/somedir/someotherfile" expect_output "$rootuid:$rootgid 4700" # Check that a container with mapped-layer can be committed. run_buildah commit "$ctr" localhost/alpine-working:$i # Also test bud command # Build an image using these mappings. echo "Building image with ${uidmapargs[$i]} ${gidmapargs[$i]}" run_buildah bud ${uidmapargs[$i]} ${gidmapargs[$i]} $RUNOPTS $WITH_POLICY_JSON \ -t localhost/alpine-bud:$i -f $BUDFILES/namespaces/Containerfile $TEST_SCRATCH_DIR # If we specified mappings, expect to be in a different namespace by default. output_namespace="$(grep -A1 'ReadlinkResult' <<< "$output" | tail -n1)" idmapping_check_namespace "${output_namespace}" "bud" # Check that we got the mappings that we expected. output_uidmap="$(grep -A1 'UidMapResult' <<< "$output" | tail -n1)" output_gidmap="$(grep -A1 'GidMapResult' <<< "$output" | tail -n1)" idmapping_check_map "$output_uidmap" "${uidmaps[$i]}" "UidMapResult" idmapping_check_map "$output_gidmap" "${gidmaps[$i]}" "GidMapResult" # Check that if we copy a file into the container, it gets the right permissions. output_file_stat="$(grep -A1 'StatSomefileResult' <<< "$output" | tail -n1)" # Check that if we copy a directory into the container, its contents get the right permissions. output_dir_stat="$(grep -A1 'StatSomedirResult' <<< "$output" | tail -n1)" output_otherfile_stat="$(grep -A1 'StatSomeotherfileResult' <<< "$output" | tail -n1)" output_workdir_stat="$(grep -A1 'StatNewWorkdir' <<< "$output" | tail -n1)" # bud strips suid. idmapping_check_permission "$output_file_stat" "$output_dir_stat" expect_output --from="${output_otherfile_stat}" "0:0 700" "stat(someotherfile), in bud test" expect_output --from="${output_workdir_stat}" "guest:users" "stat(new-workdir), in bud test" done } general_namespace() { RUNOPTS="${RUNC_BINARY:+--runtime $RUNC_BINARY}" mytmpdir=$TEST_SCRATCH_DIR/my-dir mkdir -p ${mytmpdir} # The name of the /proc/self/ns/$link. nstype="$1" # The flag to use, if it's not the same as the namespace name. nsflag="${2:-$1}" # Check if we're running in an environment that can even test this. run readlink /proc/self/ns/"$nstype" echo "readlink /proc/self/ns/$nstype -> $output" [ $status -eq 0 ] || skip "$nstype namespaces not supported" mynamespace="$output" # Settings to test. types[0]= types[1]=container types[2]=host types[3]=/proc/$$/ns/$nstype types[4]=private types[5]=ns:/proc/$$/ns/$nstype _prefetch alpine for namespace in "${types[@]}" ; do # Specify the setting for this namespace for this container. run_buildah from $WITH_POLICY_JSON --quiet --"$nsflag"=$namespace alpine assert "$output" != "" "Internal error: buildah-from produced no output" ctr="$output" # Check that, unless we override it, we get that setting in "run". run_buildah run $RUNOPTS "$ctr" readlink /proc/self/ns/"$nstype" assert "$output" != "" "readlink /proc/self/ns/$nstype must not be empty" case "$namespace" in ""|container|private) assert "$output" != "$mynamespace" \ "readlink /proc/self/ns/$nstype, with namespace=$namespace" ;; host) expect_output "$mynamespace" ;; /*) expect_output "$(readlink $namespace)" ;; esac # "run" doesn't have --userns option. if [ "$nsflag" != "userns" ]; then for different in ${types[@]} ; do # Check that, if we override it, we get what we specify for "run". run_buildah run $RUNOPTS --"$nsflag"=$different "$ctr" readlink /proc/self/ns/"$nstype" assert "$output" != "" "readlink /proc/self/ns/$nstype must not be empty" case "$different" in ""|container|private) assert "$output" != "$mynamespace" \ "readlink /proc/self/ns/$nstype, with different=$different" ;; host) expect_output "$mynamespace" ;; /*) expect_output "$(readlink $different)" ;; esac done fi # Also check "from" command cat > $mytmpdir/Containerfile << _EOF FROM alpine RUN echo "TargetOutput" && readlink /proc/self/ns/$nstype _EOF run_buildah bud --"$nsflag"=$namespace $RUNOPTS $WITH_POLICY_JSON --file ${mytmpdir}/Containerfile . result=$(grep -A1 "TargetOutput" <<< "$output" | tail -n1) case "$namespace" in ""|container|private) assert "$result" != "$mynamespace" "readlink /proc/self/ns/$nstype" ;; host) expect_output --from="$result" "$mynamespace" ;; /*) expect_output --from="$result" "$(readlink $namespace)" ;; esac done } @test "ipc-namespace" { skip_if_chroot skip_if_rootless skip_if_rootless_environment general_namespace ipc } @test "net-namespace" { skip_if_chroot skip_if_rootless skip_if_rootless_environment general_namespace net } @test "network-namespace" { skip_if_chroot skip_if_rootless skip_if_rootless_environment general_namespace net network } @test "pid-namespace" { skip_if_chroot skip_if_rootless skip_if_rootless_environment general_namespace pid } @test "user-namespace" { skip_if_chroot skip_if_rootless skip_if_rootless_environment general_namespace user userns } @test "uts-namespace" { skip_if_chroot skip_if_rootless skip_if_rootless_environment general_namespace uts } @test "combination-namespaces" { skip_if_chroot skip_if_rootless _prefetch alpine # mnt is always per-container, cgroup isn't a thing OCI runtime lets us configure for ipc in host private; do for net in host private; do for pid in host private; do for userns in host private; do for uts in host private; do for cgroupns in host private; do echo "buildah from $WITH_POLICY_JSON --ipc=$ipc --net=$net --pid=$pid --userns=$userns --uts=$uts --cgroupns=$cgroupns alpine" run_buildah from $WITH_POLICY_JSON --quiet --ipc=$ipc --net=$net --pid=$pid --userns=$userns --uts=$uts --cgroupns=$cgroupns alpine assert "$output" != "" "output from buildah-from" ctr="$output" run_buildah run $ctr pwd assert "$output" != "" "output from pwd" run_buildah run --tty=true $ctr pwd assert "$output" != "" "output from pwd, with --tty=true" run_buildah run --terminal=false $ctr pwd assert "$output" != "" "output from pwd, with --terminal=false" done done done done done done } @test "idmapping-and-squash" { skip_if_rootless_environment createrandom ${TEST_SCRATCH_DIR}/randomfile run_buildah from --userns-uid-map 0:32:16 --userns-gid-map 0:48:16 scratch cid=$output run_buildah copy "$cid" ${TEST_SCRATCH_DIR}/randomfile / run_buildah copy --chown 1:1 "$cid" ${TEST_SCRATCH_DIR}/randomfile /randomfile2 run_buildah commit --squash $WITH_POLICY_JSON --rm "$cid" squashed run_buildah from --quiet squashed cid=$output run_buildah mount $cid mountpoint=$output run stat -c %u:%g $mountpoint/randomfile [ "$status" -eq 0 ] expect_output "0:0" run stat -c %u:%g $mountpoint/randomfile2 [ "$status" -eq 0 ] expect_output "1:1" } @test "invalid userns-uid-map userns-gid-map" { run_buildah 125 from --userns-uid-map 16 --userns-gid-map 0:48:16 scratch expect_output 'Error: initializing ID mappings: userns-uid-map setting is malformed expected ["uint32:uint32:uint32"]: ["16"]' run_buildah 125 from --userns-uid-map 0:32:16 --userns-gid-map 16 scratch expect_output 'Error: initializing ID mappings: userns-gid-map setting is malformed expected ["uint32:uint32:uint32"]: ["16"]' run_buildah 125 bud --userns-uid-map a --userns-gid-map bogus bud/from-scratch expect_output 'Error: initializing ID mappings: userns-uid-map setting is malformed expected ["uint32:uint32:uint32"]: ["a"]' run_buildah 125 bud --userns-uid-map 0:32:16 --userns-gid-map bogus bud/from-scratch expect_output 'Error: initializing ID mappings: userns-gid-map setting is malformed expected ["uint32:uint32:uint32"]: ["bogus"]' run_buildah from --userns-uid-map 0:32:16 scratch } @test "idmapping-syntax" { run_buildah from $WITH_POLICY_JSON --quiet --userns-uid-map=0:10000:65536 alpine run_buildah 125 from $WITH_POLICY_JSON --quiet --userns-gid-map=0:10000:65536 alpine expect_output --substring "userns-gid-map can not be used without --userns-uid-map" } @test "use containers.conf namespace settings" { skip_if_chroot _prefetch alpine containers_conf_file="$TEST_SCRATCH_DIR/containers-namespaces.conf" for mode in host private; do cat > "$containers_conf_file" << EOF [containers] cgroupns = "$mode" netns = "$mode" pidns = "$mode" ipcns = "$mode" utsns = "$mode" EOF CONTAINERS_CONF="$containers_conf_file" run_buildah from $WITH_POLICY_JSON --quiet alpine assert "$output" != "" "output from buildah-from" ctr="$output" local op="==" if [[ "$mode" == "private" ]]; then op="!=" fi for nstype in cgroup ipc net pid uts; do run readlink /proc/self/ns/"$nstype" ns="$output" run_buildah run $ctr readlink /proc/self/ns/"$nstype" assert "$output" $op "$ns" "namespace matches expected ($mode)" done done rm "$containers_conf_file" } ================================================ FILE: tests/overlay.bats ================================================ #!/usr/bin/env bats load helpers @test "overlay specific level" { if test \! -e /usr/bin/fuse-overlayfs -a "$BUILDAH_ISOLATION" = "rootless"; then skip "BUILDAH_ISOLATION = $BUILDAH_ISOLATION" and no /usr/bin/fuse-overlayfs present elif test "$STORAGE_DRIVER" = "vfs"; then skip "skipping overlay test because \$STORAGE_DRIVER = $STORAGE_DRIVER" fi image=alpine _prefetch $image mkdir ${TEST_SCRATCH_DIR}/lower touch ${TEST_SCRATCH_DIR}/lower/foo run_buildah from --quiet -v ${TEST_SCRATCH_DIR}/lower:/lower:O --quiet $WITH_POLICY_JSON $image cid=$output # This should succeed run_buildah run $cid ls /lower/foo # Create and remove content in the overlay directory, should succeed, # resetting the contents between each run. run_buildah run $cid touch /lower/bar run_buildah run $cid rm /lower/foo # This should fail, second runs of containers go back to original run_buildah 1 run $cid ls /lower/bar # This should fail run ls ${TEST_SCRATCH_DIR}/lower/bar assert "$status" -ne 0 "status of ls ${TEST_SCRATCH_DIR}/lower/bar" } @test "overlay source permissions and owners" { if test \! -e /usr/bin/fuse-overlayfs -a "$BUILDAH_ISOLATION" = "rootless"; then skip "BUILDAH_ISOLATION = $BUILDAH_ISOLATION" and no /usr/bin/fuse-overlayfs present elif test "$STORAGE_DRIVER" = "vfs"; then skip "skipping overlay test because \$STORAGE_DRIVER = $STORAGE_DRIVER" fi image=alpine _prefetch $image mkdir -m 770 ${TEST_SCRATCH_DIR}/lower chown 1:1 ${TEST_SCRATCH_DIR}/lower permission=$(stat -c "%a %u %g" ${TEST_SCRATCH_DIR}/lower) run_buildah from --quiet -v ${TEST_SCRATCH_DIR}/lower:/tmp/test:O --quiet $WITH_POLICY_JSON $image cid=$output # This should succeed run_buildah run $cid sh -c 'stat -c "%a %u %g" /tmp/test' expect_output "$permission" # Create and remove content in the overlay directory, should succeed touch ${TEST_SCRATCH_DIR}/lower/foo run_buildah run $cid touch /tmp/test/bar run_buildah run $cid rm /tmp/test/foo # This should fail, second runs of containers go back to original run_buildah 1 run $cid ls /tmp/test/bar # This should fail since /tmp/test was an overlay, not a bind mount run ls ${TEST_SCRATCH_DIR}/lower/bar assert "$status" -ne 0 "status of ls ${TEST_SCRATCH_DIR}/lower/bar" } @test "overlay path contains colon" { if test \! -e /usr/bin/fuse-overlayfs -a "$BUILDAH_ISOLATION" = "rootless"; then skip "BUILDAH_ISOLATION = $BUILDAH_ISOLATION" and no /usr/bin/fuse-overlayfs present elif test "$STORAGE_DRIVER" = "vfs"; then skip "skipping overlay test because \$STORAGE_DRIVER = $STORAGE_DRIVER" fi image=alpine _prefetch $image mkdir ${TEST_SCRATCH_DIR}/a:lower touch ${TEST_SCRATCH_DIR}/a:lower/foo # This should succeed. # Add double backslash, because shell will escape. run_buildah from --quiet -v ${TEST_SCRATCH_DIR}/a\\:lower:/a\\:lower:O --quiet $WITH_POLICY_JSON $image cid=$output # This should succeed run_buildah run $cid ls /a:lower/foo # Mount volume when run run_buildah run -v ${TEST_SCRATCH_DIR}/a\\:lower:/b\\:lower:O $cid ls /b:lower/foo # Create and remove content in the overlay directory, should succeed, # resetting the contents between each run. run_buildah run $cid touch /a:lower/bar run_buildah run $cid rm /a:lower/foo # This should fail, second runs of containers go back to original run_buildah 1 run $cid ls /a:lower/bar # This should fail run ls ${TEST_SCRATCH_DIR}/a:lower/bar assert "$status" -ne 0 "status of ls ${TEST_SCRATCH_DIR}/a:lower/bar" } ================================================ FILE: tests/passwd/README.md ================================================ # passwd A standalone password hashing tool for buildah tests. ## Purpose This tool generates bcrypt password hashes and is used exclusively for testing purposes. It was previously part of the main buildah command as a hidden `passwd` subcommand but has been split out into a separate tool to: - Keep the main buildah command clean of test-only functionality - Allow tests to use password hashing independently - Follow the same pattern as other test tools like `imgtype` ## Usage ```bash passwd ``` ## Example ```bash $ passwd testpassword $2a$10$ZamosnV9dfpTJn4Uk.Xix.5nwbKNiLw8xpP/6g2z83jhY.WKZuRjG ``` The tool outputs a bcrypt hash of the input password to stdout, which can be used in test scenarios that require password hashing (such as setting up test registries with HTTP basic authentication). ## Building The tool is built automatically when running `make all` or can be built individually with: ```bash make bin/passwd ``` ================================================ FILE: tests/passwd/passwd.go ================================================ package main import ( "fmt" "os" "golang.org/x/crypto/bcrypt" ) func main() { if len(os.Args) != 2 { fmt.Fprintf(os.Stderr, "Usage: %s \n", os.Args[0]) fmt.Fprintf(os.Stderr, "Generate a password hash using golang.org/x/crypto/bcrypt.\n") os.Exit(1) } passwd, err := bcrypt.GenerateFromPassword([]byte(os.Args[1]), bcrypt.DefaultCost) if err != nil { fmt.Fprintf(os.Stderr, "Error generating password hash: %v\n", err) os.Exit(1) } fmt.Println(string(passwd)) } ================================================ FILE: tests/platforms.bats ================================================ #!/usr/bin/env bats load helpers # read the platform information from the configuration of the main image for # the oci layout in $1 read_oci_layout_platform() { run jq -r '.manifests[0].digest' "$1"/index.json assert $status -eq 0 local alg="${output%%:*}" local hex="${output##*:}" run jq -r '.config.digest' "$1"/blobs/"$alg"/"$hex" assert $status -eq 0 alg="${output%%:*}" hex="${output##*:}" run jq -r '.os' "$1"/blobs/"$alg"/"$hex" assert $status -eq 0 local os="$output" run jq -r '.architecture' "$1"/blobs/"$alg"/"$hex" assert $status -eq 0 local arch="$output" run jq -r '.variant' "$1"/blobs/"$alg"/"$hex" assert $status -eq 0 local variant="$output" if test "$variant" = null ; then variant= fi echo "$os"/"$arch""${variant:+/$variant}" } @test "implicit-and-explicit-platforms" { _prefetch busybox local context="$TEST_SCRATCH_DIR"/context mkdir -p "$context" cat > "$context"/Dockerfile.scratch << EOF FROM scratch COPY . . EOF cat > "$context"/Dockerfile.base << EOF FROM busybox EOF cat > "$context"/Dockerfile.derived << EOF FROM busybox COPY . . EOF run_buildah version --json run jq -r .buildPlatform <<< "$output" assert $status -eq 0 local buildplatform="$output" # these should either get the default determined at runtime, or the value passed for platform in "" linux/amd64 linux/arm64 linux/arm64/v8 ; do local arch="${platform##*/}" run_buildah build --layers --no-cache -t oci:"$TEST_SCRATCH_DIR"/scratch-"${arch:-default}" ${platform:+--platform "$platform"} -f "$context"/Dockerfile.scratch "$context" run read_oci_layout_platform "$TEST_SCRATCH_DIR"/scratch-"${arch:-default}" assert $status -eq 0 assert "$output" = "${platform:-${buildplatform}}" "for build based on scratch for ${platform:-default platform}" done # these should inherit the values from the base image that we used for the given platform for platform in "" linux/amd64 linux/arm64 linux/arm64/v8 ; do arch="${platform##*/}" for base in base derived ; do run_buildah build --layers --no-cache -t "$base"-"${arch:-default}" ${platform:+--platform "$platform"} -f "$context"/Dockerfile."$base" "$context" run_buildah push "$base"-"${arch:-default}" oci:"$TEST_SCRATCH_DIR"/"$base"-"${arch:-default}" done run read_oci_layout_platform "$TEST_SCRATCH_DIR"/base-"${arch:-default}" assert $status -eq 0 baseplatform="$output" run read_oci_layout_platform "$TEST_SCRATCH_DIR"/derived-"${arch:-default}" assert $status -eq 0 derivedplatform="$output" assert "$baseplatform" = "$derivedplatform" "for build based for ${platform:-default platform}" done } ================================================ FILE: tests/policy.json ================================================ { "default": [ { "type": "insecureAcceptAnything" } ] } ================================================ FILE: tests/pull.bats ================================================ #!/usr/bin/env bats load helpers # Regression test for #2904 @test "local-image resolution" { run_buildah pull -q busybox iid=$output run_buildah tag ${iid} localhost/image # We want to make sure that "image" will always resolve to "localhost/image" # (given a local image with that name exists). The trick we're using is to # force a failed pull and look at the error message which *must* include the # the resolved image name (localhost/image:latest). run_buildah 125 pull --policy=always image assert "$output" =~ "initializing source docker://localhost/image:latest" run_buildah rmi localhost/image ${iid} } @test "pull-flags-order-verification" { run_buildah 125 pull --retry 4 --retry-delay 4s image1 --tls-verify check_options_flag_err "--tls-verify" run_buildah 125 pull image1 --authfile=/tmp/somefile check_options_flag_err "--authfile=/tmp/somefile" run_buildah 125 pull image1 -q --cred bla:bla --authfile=/tmp/somefile check_options_flag_err "-q" } @test "pull-blocked" { run_buildah 125 --registries-conf ${TEST_SOURCES}/registries.conf.block pull $WITH_POLICY_JSON docker.io/alpine expect_output --substring "registry docker.io is blocked in" run_buildah --retry pull $WITH_POLICY_JSON docker.io/alpine } @test "pull-from-registry" { run_buildah --retry pull $WITH_POLICY_JSON busybox:glibc run_buildah pull $WITH_POLICY_JSON busybox:latest run_buildah images --format "{{.Name}}:{{.Tag}}" expect_output --substring "busybox:glibc" expect_output --substring "busybox:latest" run_buildah --retry pull $WITH_POLICY_JSON quay.io/libpod/alpine_nginx:latest run_buildah images --format "{{.Name}}:{{.Tag}}" expect_output --substring "alpine_nginx:latest" run_buildah rmi quay.io/libpod/alpine_nginx:latest run_buildah --retry pull $WITH_POLICY_JSON quay.io/libpod/alpine_nginx run_buildah images --format "{{.Name}}:{{.Tag}}" expect_output --substring "alpine_nginx:latest" run_buildah --retry pull $WITH_POLICY_JSON alpine@sha256:634a8f35b5f16dcf4aaa0822adc0b1964bb786fca12f6831de8ddc45e5986a00 run_buildah 125 pull $WITH_POLICY_JSON fakeimage/fortest run_buildah images --format "{{.Name}}:{{.Tag}}" assert "$output" !~ "fakeimage/fortest" "fakeimage/fortest found in buildah images" } @test "pull-from-docker-archive" { run_buildah --retry pull $WITH_POLICY_JSON alpine run_buildah push $WITH_POLICY_JSON docker.io/library/alpine:latest docker-archive:${TEST_SCRATCH_DIR}/alp.tar:alpine:latest run_buildah rmi alpine run_buildah --retry pull $WITH_POLICY_JSON docker-archive:${TEST_SCRATCH_DIR}/alp.tar run_buildah images --format "{{.Name}}:{{.Tag}}" expect_output --substring "alpine" run_buildah 125 pull --all-tags $WITH_POLICY_JSON docker-archive:${TEST_SCRATCH_DIR}/alp.tar expect_output --substring "pulling all tags is not supported for docker-archive transport" } @test "pull-from-oci-archive" { run_buildah --retry pull $WITH_POLICY_JSON alpine run_buildah push $WITH_POLICY_JSON docker.io/library/alpine:latest oci-archive:${TEST_SCRATCH_DIR}/alp.tar:alpine run_buildah rmi alpine run_buildah pull $WITH_POLICY_JSON oci-archive:${TEST_SCRATCH_DIR}/alp.tar run_buildah images --format "{{.Name}}:{{.Tag}}" expect_output --substring "alpine" run_buildah 125 pull --all-tags $WITH_POLICY_JSON oci-archive:${TEST_SCRATCH_DIR}/alp.tar expect_output --substring "pulling all tags is not supported for oci-archive transport" } @test "pull-from-local-directory" { mkdir ${TEST_SCRATCH_DIR}/buildahtest run_buildah --retry pull $WITH_POLICY_JSON alpine run_buildah push $WITH_POLICY_JSON docker.io/library/alpine:latest dir:${TEST_SCRATCH_DIR}/buildahtest run_buildah rmi alpine run_buildah pull --quiet $WITH_POLICY_JSON dir:${TEST_SCRATCH_DIR}/buildahtest imageID="$output" # Images pulled via the dir transport are untagged. run_buildah images --format "{{.Name}}:{{.Tag}}" expect_output --substring ":" run_buildah 125 pull --all-tags $WITH_POLICY_JSON dir:$imageID expect_output --substring "pulling all tags is not supported for dir transport" } @test "pull-from-docker-daemon" { skip_if_no_docker run docker pull alpine echo "$output" assert "$status" -eq 0 "status of docker (yes, docker) pull alpine" run_buildah pull $WITH_POLICY_JSON docker-daemon:docker.io/library/alpine:latest run_buildah images --format "{{.Name}}:{{.Tag}}" expect_output --substring "alpine:latest" run_buildah rmi alpine run_buildah 125 pull --all-tags $WITH_POLICY_JSON docker-daemon:docker.io/library/alpine:latest expect_output --substring "pulling all tags is not supported for docker-daemon transport" } @test "pull-all-tags" { start_registry declare -a tags=(0.9 0.9.1 1.1 alpha beta gamma2.0 latest) # setup: pull alpine, and push it repeatedly to localhost using those tags opts="--signature-policy ${TEST_SOURCES}/policy.json --tls-verify=false --creds testuser:testpassword" run_buildah --retry pull --quiet $WITH_POLICY_JSON alpine for tag in "${tags[@]}"; do run_buildah push $opts alpine localhost:${REGISTRY_PORT}/myalpine:$tag done run_buildah images -q expect_line_count 1 "There's only one actual image ID" alpine_iid=$output # Remove it, and confirm. run_buildah rmi alpine run_buildah images -q expect_output "" "After buildah rmi, there are no locally stored images" # Now pull with --all-tags, and confirm that we see all expected tag strings run_buildah pull $opts --all-tags localhost:${REGISTRY_PORT}/myalpine for tag in "${tags[@]}"; do expect_output --substring "Trying to pull localhost:${REGISTRY_PORT}/myalpine:$tag" done # Confirm that 'images -a' lists all of them. help confirm # that tag names are exact, e.g we don't confuse 0.9 and 0.9.1 run_buildah images -a --format '<{{.Tag}}>' expect_line_count "${#tags[@]}" "number of tagged images" for tag in "${tags[@]}"; do expect_output --substring "<$tag>" done # Finally, make sure that there's actually one and exactly one image run_buildah images -q expect_output $alpine_iid "Pulled image has the same IID as original alpine" } @test "pull-from-oci-directory" { run_buildah --retry pull $WITH_POLICY_JSON alpine run_buildah 125 pull --all-tags $WITH_POLICY_JSON oci:${TEST_SCRATCH_DIR}/alpine expect_output --substring "pulling all tags is not supported for oci transport" # Create on OCI image with reference and one without. The first is expected # to preserve the reference while the latter should be unnamed. name="foo.com/name" tag="tag" withref="oci:${TEST_SCRATCH_DIR}/withref:$name:$tag" noref="oci:${TEST_SCRATCH_DIR}/noref" run_buildah push $WITH_POLICY_JSON docker.io/library/alpine:latest $withref run_buildah push $WITH_POLICY_JSON docker.io/library/alpine:latest $noref run_buildah rmi alpine # Image without optional reference is unnamed. run_buildah pull -q $WITH_POLICY_JSON $noref run_buildah images --format "{{.Name}}:{{.Tag}}" $output expect_output ":" # Image with optional reference is named. run_buildah pull -q $WITH_POLICY_JSON $withref run_buildah images --format "{{.Name}}:{{.Tag}}" $output expect_output "$name:$tag" } @test "pull-denied-by-registry-sources" { export BUILD_REGISTRY_SOURCES='{"blockedRegistries": ["docker.io"]}' run_buildah 125 pull $WITH_POLICY_JSON --registries-conf ${TEST_SOURCES}/registries.conf.hub --quiet busybox expect_output --substring 'registry "docker.io" denied by policy: it is in the blocked registries list' run_buildah 125 pull $WITH_POLICY_JSON --registries-conf ${TEST_SOURCES}/registries.conf.hub --quiet busybox expect_output --substring 'registry "docker.io" denied by policy: it is in the blocked registries list' export BUILD_REGISTRY_SOURCES='{"allowedRegistries": ["some-other-registry.example.com"]}' run_buildah 125 pull $WITH_POLICY_JSON --registries-conf ${TEST_SOURCES}/registries.conf.hub --quiet busybox expect_output --substring 'registry "docker.io" denied by policy: not in allowed registries list' run_buildah 125 pull $WITH_POLICY_JSON --registries-conf ${TEST_SOURCES}/registries.conf.hub --quiet busybox expect_output --substring 'registry "docker.io" denied by policy: not in allowed registries list' } @test "pull should fail with nonexistent authfile" { run_buildah 125 pull --authfile /tmp/nonexistent $WITH_POLICY_JSON alpine } @test "pull encrypted local image" { _prefetch busybox mkdir ${TEST_SCRATCH_DIR}/tmp openssl genrsa -out ${TEST_SCRATCH_DIR}/tmp/mykey.pem 1024 openssl genrsa -out ${TEST_SCRATCH_DIR}/tmp/mykey2.pem 1024 openssl rsa -in ${TEST_SCRATCH_DIR}/tmp/mykey.pem -pubout > ${TEST_SCRATCH_DIR}/tmp/mykey.pub run_buildah push $WITH_POLICY_JSON --encryption-key jwe:${TEST_SCRATCH_DIR}/tmp/mykey.pub busybox oci:${TEST_SCRATCH_DIR}/tmp/busybox_enc # Try to pull encrypted image without key should fail run_buildah 125 pull $WITH_POLICY_JSON oci:${TEST_SCRATCH_DIR}/tmp/busybox_enc expect_output --substring "does not match config's DiffID" # Try to pull encrypted image with wrong key should fail run_buildah 125 pull $WITH_POLICY_JSON --decryption-key ${TEST_SCRATCH_DIR}/tmp/mykey2.pem oci:${TEST_SCRATCH_DIR}/tmp/busybox_enc expect_output --substring "decrypting layer .* no suitable key unwrapper found or none of the private keys could be used for decryption" # Providing the right key should succeed run_buildah pull $WITH_POLICY_JSON --decryption-key ${TEST_SCRATCH_DIR}/tmp/mykey.pem oci:${TEST_SCRATCH_DIR}/tmp/busybox_enc rm -rf ${TEST_SCRATCH_DIR}/tmp } @test "pull encrypted registry image" { _prefetch busybox start_registry mkdir ${TEST_SCRATCH_DIR}/tmp openssl genrsa -out ${TEST_SCRATCH_DIR}/tmp/mykey.pem 1024 openssl genrsa -out ${TEST_SCRATCH_DIR}/tmp/mykey2.pem 1024 openssl rsa -in ${TEST_SCRATCH_DIR}/tmp/mykey.pem -pubout > ${TEST_SCRATCH_DIR}/tmp/mykey.pub run_buildah push $WITH_POLICY_JSON --tls-verify=false --creds testuser:testpassword --encryption-key jwe:${TEST_SCRATCH_DIR}/tmp/mykey.pub busybox docker://localhost:${REGISTRY_PORT}/buildah/busybox_encrypted:latest # Try to pull encrypted image without key should fail run_buildah 125 pull $WITH_POLICY_JSON --tls-verify=false --creds testuser:testpassword docker://localhost:${REGISTRY_PORT}/buildah/busybox_encrypted:latest expect_output --substring "does not match config's DiffID" # Try to pull encrypted image with wrong key should fail, with diff. msg run_buildah 125 pull $WITH_POLICY_JSON --tls-verify=false --creds testuser:testpassword --decryption-key ${TEST_SCRATCH_DIR}/tmp/mykey2.pem docker://localhost:${REGISTRY_PORT}/buildah/busybox_encrypted:latest expect_output --substring "decrypting layer .* no suitable key unwrapper found or none of the private keys could be used for decryption" # Providing the right key should succeed run_buildah pull $WITH_POLICY_JSON --tls-verify=false --creds testuser:testpassword --decryption-key ${TEST_SCRATCH_DIR}/tmp/mykey.pem docker://localhost:${REGISTRY_PORT}/buildah/busybox_encrypted:latest run_buildah rmi localhost:${REGISTRY_PORT}/buildah/busybox_encrypted:latest rm -rf ${TEST_SCRATCH_DIR}/tmp } @test "pull encrypted registry image from commit" { _prefetch busybox start_registry mkdir ${TEST_SCRATCH_DIR}/tmp openssl genrsa -out ${TEST_SCRATCH_DIR}/tmp/mykey.pem 1024 openssl genrsa -out ${TEST_SCRATCH_DIR}/tmp/mykey2.pem 1024 openssl rsa -in ${TEST_SCRATCH_DIR}/tmp/mykey.pem -pubout > ${TEST_SCRATCH_DIR}/tmp/mykey.pub run_buildah from --quiet --pull=false $WITH_POLICY_JSON busybox cid=$output run_buildah commit --iidfile /dev/null --tls-verify=false --creds testuser:testpassword $WITH_POLICY_JSON --encryption-key jwe:${TEST_SCRATCH_DIR}/tmp/mykey.pub -q $cid docker://localhost:${REGISTRY_PORT}/buildah/busybox_encrypted:latest # Try to pull encrypted image without key should fail run_buildah 125 pull $WITH_POLICY_JSON --tls-verify=false --creds testuser:testpassword docker://localhost:${REGISTRY_PORT}/buildah/busybox_encrypted:latest expect_output --substring "does not match config's DiffID" # Try to pull encrypted image with wrong key should fail run_buildah 125 pull $WITH_POLICY_JSON --tls-verify=false --creds testuser:testpassword --decryption-key ${TEST_SCRATCH_DIR}/tmp/mykey2.pem docker://localhost:${REGISTRY_PORT}/buildah/busybox_encrypted:latest expect_output --substring "decrypting layer .* no suitable key unwrapper found or none of the private keys could be used for decryption" # Providing the right key should succeed run_buildah pull $WITH_POLICY_JSON --tls-verify=false --creds testuser:testpassword --decryption-key ${TEST_SCRATCH_DIR}/tmp/mykey.pem docker://localhost:${REGISTRY_PORT}/buildah/busybox_encrypted:latest run_buildah rmi localhost:${REGISTRY_PORT}/buildah/busybox_encrypted:latest rm -rf ${TEST_SCRATCH_DIR}/tmp } @test "pull image into a full storage" { skip_if_rootless_environment mkdir /tmp/buildah-test mount -t tmpfs -o size=5M tmpfs /tmp/buildah-test run dd if=/dev/urandom of=/tmp/buildah-test/full run_buildah 125 --root=/tmp/buildah-test pull $WITH_POLICY_JSON alpine expect_output --substring "no space left on device" umount /tmp/buildah-test rm -rf /tmp/buildah-test } @test "pull with authfile" { _prefetch busybox start_registry mkdir ${TEST_SCRATCH_DIR}/tmp run_buildah push --creds testuser:testpassword --tls-verify=false busybox docker://localhost:${REGISTRY_PORT}/buildah/busybox:latest run_buildah login --authfile ${TEST_SCRATCH_DIR}/tmp/test.auth --username testuser --password testpassword --tls-verify=false localhost:${REGISTRY_PORT} run_buildah pull --authfile ${TEST_SCRATCH_DIR}/tmp/test.auth --tls-verify=false docker://localhost:${REGISTRY_PORT}/buildah/busybox:latest run_buildah rmi localhost:${REGISTRY_PORT}/buildah/busybox:latest rm -rf ${TEST_SCRATCH_DIR}/tmp } @test "pull quietly" { run_buildah pull -q busybox iid=$output run_buildah rmi ${iid} } @test "pull-policy" { mkdir ${TEST_SCRATCH_DIR}/buildahtest run_buildah 125 pull $WITH_POLICY_JSON --policy bogus alpine expect_output --substring "unsupported pull policy \"bogus\"" # If image does not exist the never will fail run_buildah 125 pull -q $WITH_POLICY_JSON --policy never alpine expect_output --substring "image not known" run_buildah 125 inspect --type image alpine expect_output --substring "image not known" # create bogus alpine image run_buildah from $WITH_POLICY_JSON scratch cid=$output run_buildah commit -q $cid docker.io/library/alpine iid=$output # If image does not exist the never will succeed, but iid should not change run_buildah pull -q $WITH_POLICY_JSON --policy never alpine expect_output $iid # Pull image by default should change the image id run_buildah pull -q --policy always $WITH_POLICY_JSON alpine assert "$output" != "$iid" "pulled image should have a new IID" # Recreate image run_buildah commit -q $cid docker.io/library/alpine iid=$output # Make sure missing image works run_buildah pull -q $WITH_POLICY_JSON --policy missing alpine expect_output $iid run_buildah rmi alpine run_buildah pull -q $WITH_POLICY_JSON alpine run_buildah inspect alpine run_buildah rmi alpine run_buildah pull -q $WITH_POLICY_JSON --policy missing alpine run_buildah inspect alpine run_buildah rmi alpine } @test "pull --arch" { mkdir ${TEST_SCRATCH_DIR}/buildahtest run_buildah 125 pull $WITH_POLICY_JSON --arch bogus alpine expect_output --substring "no image found in manifest list" # Make sure missing image works run_buildah pull -q $WITH_POLICY_JSON --arch arm64 alpine run_buildah inspect --format "{{ .Docker.Architecture }}" alpine expect_output arm64 run_buildah inspect --format "{{ .OCIv1.Architecture }}" alpine expect_output arm64 run_buildah rmi alpine } @test "pull --platform" { mkdir ${TEST_SCRATCH_DIR}/buildahtest run_buildah 125 pull $WITH_POLICY_JSON --platform linux/bogus alpine expect_output --substring "no image found in manifest list" # Make sure missing image works run_buildah pull -q $WITH_POLICY_JSON --platform linux/arm64 alpine run_buildah inspect --format "{{ .Docker.Architecture }}" alpine expect_output arm64 run_buildah inspect --format "{{ .OCIv1.Architecture }}" alpine expect_output arm64 run_buildah rmi alpine } @test "pull image with TMPDIR set" { skip_if_rootless_environment testdir=${TEST_SCRATCH_DIR}/buildah-test mkdir -p $testdir mount -t tmpfs -o size=1M tmpfs $testdir TMPDIR=$testdir run_buildah 125 pull --policy always $WITH_POLICY_JSON quay.io/libpod/alpine_nginx:latest expect_output --substring "no space left on device" run_buildah pull --policy always $WITH_POLICY_JSON quay.io/libpod/alpine_nginx:latest umount $testdir rm -rf $testdir } @test "pull-policy --missing --arch" { # Make sure missing image works run_buildah pull -q $WITH_POLICY_JSON --policy missing --arch amd64 alpine amdiid=$output run_buildah pull -q $WITH_POLICY_JSON --policy missing --arch arm64 alpine armiid=$output assert "$amdiid" != "$armiid" "AMD and ARM ids should differ" } ================================================ FILE: tests/push.bats ================================================ #!/usr/bin/env bats load helpers @test "push-flags-order-verification" { run_buildah 125 push img1 dest1 -q check_options_flag_err "-q" run_buildah 125 push img1 --tls-verify dest1 check_options_flag_err "--tls-verify" run_buildah 125 push img1 dest1 arg3 --creds user1:pass1 check_options_flag_err "--creds" run_buildah 125 push img1 --creds=user1:pass1 dest1 check_options_flag_err "--creds=user1:pass1" } @test "push" { skip_if_rootless_environment touch ${TEST_SCRATCH_DIR}/reference-time-file for source in scratch scratch-image; do run_buildah from --quiet --pull=false $WITH_POLICY_JSON ${source} cid=$output for format in "" docker oci ; do mkdir -p ${TEST_SCRATCH_DIR}/committed${format:+.${format}} # Force no compression to generate what we push. run_buildah commit -D ${format:+--format ${format}} --reference-time ${TEST_SCRATCH_DIR}/reference-time-file $WITH_POLICY_JSON "$cid" scratch-image${format:+-${format}} run_buildah commit -D ${format:+--format ${format}} --reference-time ${TEST_SCRATCH_DIR}/reference-time-file $WITH_POLICY_JSON "$cid" dir:${TEST_SCRATCH_DIR}/committed${format:+.${format}} mkdir -p ${TEST_SCRATCH_DIR}/pushed${format:+.${format}} run_buildah push -D $WITH_POLICY_JSON scratch-image${format:+-${format}} dir:${TEST_SCRATCH_DIR}/pushed${format:+.${format}} # Re-encode the manifest to lose variations due to different encoders or definitions of structures. imgtype -expected-manifest-type "*" -rebuild-manifest -show-manifest dir:${TEST_SCRATCH_DIR}/committed${format:+.${format}} > ${TEST_SCRATCH_DIR}/manifest.committed${format:+.${format}} imgtype -expected-manifest-type "*" -rebuild-manifest -show-manifest dir:${TEST_SCRATCH_DIR}/pushed${format:+.${format}} > ${TEST_SCRATCH_DIR}/manifest.pushed${format:+.${format}} diff -u ${TEST_SCRATCH_DIR}/manifest.committed${format:+.${format}} ${TEST_SCRATCH_DIR}/manifest.pushed${format:+.${format}} done run_buildah rm "$cid" done } @test "push with manifest type conversion" { mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p $mytmpdir _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah push --retry 4 --retry-delay 4s $WITH_POLICY_JSON --format oci alpine dir:$mytmpdir run cat $mytmpdir/manifest.json expect_output --substring "application/vnd.oci.image.config.v1\\+json" run_buildah push $WITH_POLICY_JSON --format v2s2 alpine dir:$mytmpdir run cat $mytmpdir/manifest.json expect_output --substring "application/vnd.docker.distribution.manifest.v2\\+json" } @test "push with imageid" { mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p $mytmpdir _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah images -q imageid=$output run_buildah push $WITH_POLICY_JSON $imageid dir:$mytmpdir } @test "push with imageid and digest file" { mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p $mytmpdir _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah images -q imageid=$output run_buildah push --digestfile=${TEST_SCRATCH_DIR}/digest.txt $WITH_POLICY_JSON $imageid dir:$mytmpdir cat ${TEST_SCRATCH_DIR}/digest.txt test -s ${TEST_SCRATCH_DIR}/digest.txt } @test "push without destination" { _prefetch busybox run_buildah pull $WITH_POLICY_JSON busybox run_buildah 125 push $WITH_POLICY_JSON busybox expect_output --substring "busybox" } @test "push should fail with nonexistent authfile" { _prefetch alpine run_buildah from --quiet --pull $WITH_POLICY_JSON alpine cid=$output run_buildah images -q imageid=$output run_buildah 125 push $WITH_POLICY_JSON --authfile /tmp/nonexistent $imageid dir:${TEST_SCRATCH_DIR}/my-tmp-dir } @test "push-denied-by-registry-sources" { _prefetch busybox export BUILD_REGISTRY_SOURCES='{"blockedRegistries": ["registry.example.com"]}' run_buildah from --quiet $WITH_POLICY_JSON --quiet busybox cid=$output run_buildah 125 commit $WITH_POLICY_JSON ${cid} docker://registry.example.com/busierbox expect_output --substring 'commit to registry at "registry.example.com" denied by policy: it is in the blocked registries list' run_buildah pull $WITH_POLICY_JSON --quiet busybox run_buildah 125 push $WITH_POLICY_JSON busybox docker://registry.example.com/evenbusierbox export BUILD_REGISTRY_SOURCES='{"allowedRegistries": ["some-other-registry.example.com"]}' run_buildah from --quiet $WITH_POLICY_JSON --quiet busybox cid=$output run_buildah 125 commit $WITH_POLICY_JSON ${cid} docker://registry.example.com/busierbox expect_output --substring 'commit to registry at "registry.example.com" denied by policy: not in allowed registries list' run_buildah pull $WITH_POLICY_JSON --quiet busybox run_buildah 125 push $WITH_POLICY_JSON busybox docker://registry.example.com/evenbusierbox expect_output --substring 'registry "registry.example.com" denied by policy: not in allowed registries list' } @test "buildah push image to containers-storage" { _prefetch busybox run_buildah push $WITH_POLICY_JSON busybox containers-storage:newimage:latest run_buildah images expect_output --substring "newimage" } @test "buildah push image to docker-archive and oci-archive" { _prefetch busybox for dest in docker-archive oci-archive; do mkdir ${TEST_SCRATCH_DIR}/tmp run_buildah push $WITH_POLICY_JSON busybox $dest:${TEST_SCRATCH_DIR}/tmp/busybox.tar:latest ls ${TEST_SCRATCH_DIR}/tmp/busybox.tar rm -rf ${TEST_SCRATCH_DIR}/tmp done } @test "buildah push image to docker and docker registry" { skip_if_no_docker _prefetch busybox run_buildah push $WITH_POLICY_JSON busybox docker-daemon:buildah/busybox:latest run docker images expect_output --substring "buildah/busybox" docker rmi buildah/busybox start_registry run_buildah push $WITH_POLICY_JSON --tls-verify=false --creds testuser:testpassword docker.io/busybox:latest docker://localhost:${REGISTRY_PORT}/buildah/busybox:latest docker login localhost:${REGISTRY_PORT} --username testuser --password-stdin << ${TEST_SCRATCH_DIR}/tmp/mykey.pub run_buildah push $WITH_POLICY_JSON --encryption-key jwe:${TEST_SCRATCH_DIR}/tmp/mykey.pub busybox oci:${TEST_SCRATCH_DIR}/tmp/busybox_enc imgtype -show-manifest oci:${TEST_SCRATCH_DIR}/tmp/busybox_enc | grep "+encrypted" rm -rf ${TEST_SCRATCH_DIR}/tmp } @test "buildah oci encrypt and push registry" { _prefetch busybox mkdir ${TEST_SCRATCH_DIR}/tmp start_registry openssl genrsa -out ${TEST_SCRATCH_DIR}/tmp/mykey.pem 1024 openssl rsa -in ${TEST_SCRATCH_DIR}/tmp/mykey.pem -pubout > ${TEST_SCRATCH_DIR}/tmp/mykey.pub run_buildah push $WITH_POLICY_JSON --tls-verify=false --creds testuser:testpassword --encryption-key jwe:${TEST_SCRATCH_DIR}/tmp/mykey.pub busybox docker://localhost:${REGISTRY_PORT}/buildah/busybox_encrypted:latest # this test, just checks the ability to push an image # there is no good way to test the details of the image unless with ./buildah pull, test will be in pull.bats rm -rf ${TEST_SCRATCH_DIR}/tmp } @test "buildah push to registry allowed by BUILD_REGISTRY_SOURCES" { _prefetch busybox start_registry export BUILD_REGISTRY_SOURCES='{"insecureRegistries": ["localhost:${REGISTRY_PORT}"]}' run_buildah 125 push --creds testuser:testpassword $WITH_POLICY_JSON --tls-verify=true busybox docker://localhost:${REGISTRY_PORT}/buildah/busybox:latest expect_output --substring "certificate signed by unknown authority" run_buildah push --creds testuser:testpassword $WITH_POLICY_JSON --cert-dir ${TEST_SCRATCH_DIR}/registry busybox docker://localhost:${REGISTRY_PORT}/buildah/busybox:latest } @test "push with authfile" { _prefetch busybox mkdir ${TEST_SCRATCH_DIR}/tmp start_registry run_buildah login --authfile ${TEST_SCRATCH_DIR}/tmp/test.auth --username testuser --password testpassword --tls-verify=false localhost:${REGISTRY_PORT} run_buildah push --authfile ${TEST_SCRATCH_DIR}/tmp/test.auth $WITH_POLICY_JSON --tls-verify=false busybox docker://localhost:${REGISTRY_PORT}/buildah/busybox:latest expect_output --substring "Copying" run_buildah manifest create localhost:${REGISTRY_PORT}/testmanifest run_buildah manifest push --authfile ${TEST_SCRATCH_DIR}/tmp/test.auth $WITH_POLICY_JSON --tls-verify=false localhost:${REGISTRY_PORT}/testmanifest expect_output --substring "Writing manifest list to image destination" } @test "push with --quiet" { mytmpdir=${TEST_SCRATCH_DIR}/my-dir mkdir -p $mytmpdir _prefetch alpine run_buildah push --quiet $WITH_POLICY_JSON alpine dir:$mytmpdir expect_output "" } @test "push with --compression-format" { _prefetch alpine run_buildah from --quiet --pull alpine cid=$output run_buildah images -q imageid=$output run_buildah push --format oci --compression-format zstd:chunked $imageid dir:${TEST_SCRATCH_DIR}/zstd # Verify there is some zstd compressed layer. grep application/vnd.oci.image.layer.v1.tar+zstd ${TEST_SCRATCH_DIR}/zstd/manifest.json } ================================================ FILE: tests/registries-cached.conf ================================================ # Note that changing the order here may break tests. unqualified-search-registries = ['docker.io', 'quay.io', 'registry.fedoraproject.org'] [[registry]] # As of July 2024, all CI VMs come with a local registry prepopulated # with all images needed by tests. prefix="docker.io" location="127.0.0.1:60333" insecure=true [[registry]] prefix="quay.io" location="127.0.0.1:60333" insecure=true [[registry]] prefix="docker.io/library" location="127.0.0.1:60333/libpod" insecure=true ================================================ FILE: tests/registries.bats ================================================ #!/usr/bin/env bats load helpers @test "registries" { registrypair() { image1=$1 image2=$2 # Create a container by specifying the image with one name. run_buildah --retry from --quiet --pull=ifmissing $WITH_POLICY_JSON $image1 cid1=$output # Create a container by specifying the image with another name. run_buildah --retry from --quiet --pull=ifmissing $WITH_POLICY_JSON $image2 cid2=$output # Get their image IDs. They should be the same one. run_buildah inspect -f "{{.FromImageID}}" $cid1 iid1=$output run_buildah inspect -f "{{.FromImageID}}" $cid2 expect_output $iid1 "$image2.FromImageID == $image1.FromImageID" # Clean up. run_buildah rm -a run_buildah rmi -a } # Test with pairs of short and fully-qualified names that should be the same image. registrypair busybox docker.io/busybox registrypair busybox docker.io/library/busybox } ================================================ FILE: tests/registries.conf ================================================ # Note that changing the order here may break tests. unqualified-search-registries = ['docker.io', 'quay.io', 'registry.fedoraproject.org'] [[registry]] # In Nov. 2020, Docker rate-limits image pulling. To avoid hitting these # limits while testing, always use the google mirror for qualified and # unqualified `docker.io` images. # Ref: https://cloud.google.com/container-registry/docs/pulling-cached-images prefix="docker.io" location="mirror.gcr.io" # 2020-10-27 a number of images are not present in gcr.io, and podman # barfs spectacularly when trying to fetch them. We've hand-copied # those to quay, using skopeo copy --all ... [[registry]] location="docker.io/library" mirror=[{location="quay.io/libpod"}] # 2021-03-23 these are used in buildah system tests, but not (yet?) # listed in the global shortnames.conf. [aliases] busybox="docker.io/library/busybox" ubuntu="docker.io/library/ubuntu" php="docker.io/library/php" alpine="docker.io/library/alpine" debian="docker.io/library/debian" ================================================ FILE: tests/registries.conf.block ================================================ # This is a system-wide configuration file used to # keep track of registries for various container backends. # It adheres to TOML format and does not support recursive # lists of registries. # The default location for this configuration file is /etc/containers/registries.conf. # The only valid categories are: 'registries.search', 'registries.insecure', # and 'registries.block'. [registries.search] registries = ['docker.io', 'registry.fedoraproject.org', 'registry.access.redhat.com'] # If you need to access insecure registries, add the registry's fully-qualified name. # An insecure registry is one that does not have a valid SSL certificate or only does HTTP. [registries.insecure] registries = [] # If you need to block pull access from a registry, uncomment the section below # and add the registries fully-qualified name. # # Docker only [registries.block] registries = ['docker.io', 'registry.fedoraproject.org', 'registry.access.redhat.com'] ================================================ FILE: tests/registries.conf.hub ================================================ # This is a system-wide configuration file used to # keep track of registries for various container backends. # It adheres to TOML format and does not support recursive # lists of registries. # The default location for this configuration file is /etc/containers/registries.conf. # The only valid categories are: 'registries.search', 'registries.insecure', # and 'registries.block'. [registries.search] registries = ['docker.io'] # If you need to access insecure registries, add the registry's fully-qualified name. # An insecure registry is one that does not have a valid SSL certificate or only does HTTP. [registries.insecure] registries = [] # If you need to block pull access from a registry, uncomment the section below # and add the registries fully-qualified name. # # Docker only [registries.block] registries = [] ================================================ FILE: tests/rename.bats ================================================ #!/usr/bin/env bats load helpers @test "rename" { _prefetch alpine new_name=test-container run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah containers --format "{{.ContainerName}}" old_name=$output run_buildah rename ${cid} ${new_name} run_buildah containers --format "{{.ContainerName}}" expect_output --substring "test-container" run_buildah containers --quiet -f name=${old_name} expect_output "" } @test "rename same name as current name" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah 125 rename ${cid} ${cid} expect_output 'Error: renaming a container with the same name as its current name' } @test "rename same name as other container name" { _prefetch alpine busybox run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid1=$output run_buildah from --quiet --pull=false $WITH_POLICY_JSON busybox cid2=$output run_buildah 125 rename ${cid1} ${cid2} expect_output --substring " already in use by " } ================================================ FILE: tests/rm.bats ================================================ #!/usr/bin/env bats load helpers @test "rm-flags-order-verification" { run_buildah 125 rm cnt1 -a check_options_flag_err "-a" run_buildah 125 rm cnt1 --all cnt2 check_options_flag_err "--all" } @test "remove multiple containers errors" { run_buildah 125 rm mycontainer1 mycontainer2 mycontainer3 expect_output --from="${lines[0]}" "removing container \"mycontainer1\": container not known" "output line 1" expect_output --from="${lines[1]}" "removing container \"mycontainer2\": container not known" "output line 2" expect_output --from="${lines[2]}" "Error: removing container \"mycontainer3\": container not known" "output line 3" expect_line_count 3 } @test "remove one container" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah rm "$cid" } @test "remove multiple containers" { _prefetch alpine busybox run_buildah from --quiet $WITH_POLICY_JSON alpine cid2=$output run_buildah from --quiet $WITH_POLICY_JSON busybox cid3=$output run_buildah rm "$cid2" "$cid3" } @test "remove all containers" { _prefetch alpine busybox run_buildah from $WITH_POLICY_JSON scratch cid1=$output run_buildah from --quiet $WITH_POLICY_JSON alpine cid2=$output run_buildah from --quiet $WITH_POLICY_JSON busybox cid3=$output run_buildah rm -a } @test "use conflicting commands to remove containers" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah 125 rm -a "$cid" expect_output --substring "when using the --all switch, you may not pass any containers names or IDs" } @test "remove a single tagged manifest list" { _prefetch busybox run_buildah manifest create manifestsample run_buildah manifest add manifestsample busybox run_buildah tag manifestsample manifestsample2 run_buildah manifest rm manifestsample2 # Output should only untag the listed manifest nothing else expect_output "untagged: localhost/manifestsample2:latest" run_buildah manifest rm manifestsample # Since actual list is getting removed it will also print the image id of list # So check for substring instead of exact match expect_output --substring "untagged: localhost/manifestsample:latest" # Check if busybox is still there run_buildah images expect_output --substring "busybox" } ================================================ FILE: tests/rmi.bats ================================================ #!/usr/bin/env bats load helpers @test "rmi-flags-order-verification" { run_buildah 125 rmi img1 -f check_options_flag_err "-f" run_buildah 125 rmi img1 --all img2 check_options_flag_err "--all" run_buildah 125 rmi img1 img2 --force check_options_flag_err "--force" } @test "remove one image" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah rm "$cid" run_buildah rmi alpine run_buildah images -q expect_output "" } @test "remove multiple images" { _prefetch alpine busybox run_buildah from --pull=false --quiet $WITH_POLICY_JSON alpine cid2=$output run_buildah from --pull=false --quiet $WITH_POLICY_JSON busybox cid3=$output run_buildah 125 rmi alpine busybox run_buildah images -q assert "$output" != "" "images -q" run_buildah rmi -f alpine busybox run_buildah images -q expect_output "" } @test "remove multiple non-existent images errors" { run_buildah 125 rmi image1 image2 image3 expect_output --from="${lines[1]}" --substring " image1: image not known" expect_output --from="${lines[2]}" --substring " image2: image not known" expect_output --from="${lines[3]}" --substring " image3: image not known" } @test "remove all images" { _prefetch alpine busybox run_buildah from $WITH_POLICY_JSON scratch cid1=$output run_buildah from --quiet $WITH_POLICY_JSON alpine cid2=$output run_buildah from --quiet $WITH_POLICY_JSON busybox cid3=$output run_buildah rmi -a -f run_buildah images -q expect_output "" _prefetch alpine busybox run_buildah from $WITH_POLICY_JSON scratch cid1=$output run_buildah from --quiet $WITH_POLICY_JSON alpine cid2=$output run_buildah from --quiet $WITH_POLICY_JSON busybox cid3=$output run_buildah 125 rmi --all run_buildah images -q assert "$output" != "" "images -q" run_buildah rmi --all --force run_buildah images -q expect_output "" } @test "use prune to remove dangling images" { _prefetch busybox createrandom ${TEST_SCRATCH_DIR}/randomfile createrandom ${TEST_SCRATCH_DIR}/other-randomfile run_buildah from --pull=false --quiet $WITH_POLICY_JSON busybox cid=$output run_buildah images -q expect_line_count 1 run_buildah mount $cid root=$output cp ${TEST_SCRATCH_DIR}/randomfile $root/randomfile run_buildah unmount $cid run_buildah commit $WITH_POLICY_JSON $cid containers-storage:new-image run_buildah images -q expect_line_count 2 run_buildah mount $cid root=$output cp ${TEST_SCRATCH_DIR}/other-randomfile $root/other-randomfile run_buildah unmount $cid run_buildah commit $WITH_POLICY_JSON $cid containers-storage:new-image run_buildah images -q expect_line_count 3 run_buildah rmi --prune run_buildah images -q expect_line_count 2 run_buildah rmi --all --force run_buildah images -q expect_output "" } @test "use prune to remove dangling images with parent" { createrandom ${TEST_SCRATCH_DIR}/randomfile createrandom ${TEST_SCRATCH_DIR}/other-randomfile run_buildah from --quiet $WITH_POLICY_JSON scratch cid=$output run_buildah images -q -a expect_line_count 0 run_buildah mount $cid root=$output cp ${TEST_SCRATCH_DIR}/randomfile $root/randomfile run_buildah unmount $cid run_buildah commit --quiet $WITH_POLICY_JSON $cid image=$output run_buildah rm $cid run_buildah images -q -a expect_line_count 1 run_buildah from --quiet $WITH_POLICY_JSON $image cid=$output run_buildah mount $cid root=$output cp ${TEST_SCRATCH_DIR}/other-randomfile $root/other-randomfile run_buildah unmount $cid run_buildah commit $WITH_POLICY_JSON $cid run_buildah rm $cid run_buildah images -q -a expect_line_count 2 run_buildah rmi --prune run_buildah images -q -a expect_line_count 0 run_buildah images -q -a expect_output "" } @test "attempt to prune non-dangling empty images" { # Regression test for containers/podman/issues/10832 ctxdir=${TEST_SCRATCH_DIR}/bud mkdir -p $ctxdir cat >$ctxdir/Dockerfile < ${contentdir}/README.md starthttpd ${contentdir} run_buildah bud $WITH_POLICY_JSON --build-arg=HTTP_SERVER_PORT=${HTTP_SERVER_PORT} --layers -t test1 $BUDFILES/use-layers run_buildah images -a -q expect_line_count 8 run_buildah bud $WITH_POLICY_JSON --layers -t test2 -f Dockerfile.2 $BUDFILES/use-layers run_buildah images -a -q expect_line_count 10 run_buildah rmi test2 run_buildah images -a -q expect_line_count 8 run_buildah rmi test1 run_buildah images -a -q expect_line_count 1 run_buildah bud $WITH_POLICY_JSON --layers -t test3 -f Dockerfile.2 $BUDFILES/use-layers run_buildah rmi alpine run_buildah rmi test3 run_buildah images -a -q expect_output "" } @test "rmi image that is created from another named image" { _prefetch alpine run_buildah from --quiet --pull=true $WITH_POLICY_JSON alpine cid=$output run_buildah config --entrypoint '[ "/ENTRYPOINT" ]' $cid run_buildah commit $WITH_POLICY_JSON $cid new-image run_buildah from --quiet $WITH_POLICY_JSON new-image cid=$output run_buildah config --env 'foo=bar' $cid run_buildah commit $WITH_POLICY_JSON $cid new-image-2 run_buildah rm -a run_buildah rmi new-image-2 run_buildah images -q expect_line_count 2 } ================================================ FILE: tests/rpc/noop/noop.go ================================================ package main import ( "context" "errors" "fmt" "os" "os/exec" "strings" "syscall" "github.com/containers/buildah" "github.com/containers/buildah/define" noop "github.com/containers/buildah/internal/rpc/noop/pb" "github.com/containers/buildah/pkg/cli" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) func main() { if buildah.InitReexec() { return } rootCmd := cobra.Command{ Use: "rpc", Short: "poke a no-op GRPC endpoint", Long: "poke a no-op GRPC endpoint", Args: cobra.MinimumNArgs(0), RunE: poke, Version: define.Version, SilenceUsage: true, } rootCmd.Flags().StringP("env", "e", "", "connect to location set in environment `variable`") rootCmd.Flags().StringP("connect", "c", "", "connect to `location`") rootCmd.Flags().SetInterspersed(true) rootCmd.Flags().Usage = func() { fmt.Println("[-e|--env var] [-c|--connect socket] endpoint json") } var exitCode int if err := rootCmd.Execute(); err != nil { if logrus.IsLevelEnabled(logrus.TraceLevel) { fmt.Fprintf(os.Stderr, "Error: %+v\n", err) } else { fmt.Fprintf(os.Stderr, "Error: %v\n", err) } exitCode = cli.ExecErrorCodeGeneric var ee *exec.ExitError if errors.As(err, &ee) { if w, ok := ee.Sys().(syscall.WaitStatus); ok { exitCode = w.ExitStatus() } } } os.Exit(exitCode) } func poke(c *cobra.Command, args []string) error { ctx := context.TODO() socketPath := c.Flag("connect").Value.String() if socketPath == "" { envVar := c.Flag("env").Value.String() if envVar == "" { return errors.New("neither --connect nor --env were specified") } var ok bool socketPath, ok = os.LookupEnv(envVar) if !ok { return fmt.Errorf("environment variable %q not set", envVar) } } if socketPath == "" { return errors.New("configured server location is empty") } cc, err := grpc.NewClient(socketPath, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { return fmt.Errorf("connecting to service: %w", err) } noopClient := noop.NewNoopClient(cc) response, err := noopClient.Noop(ctx, &noop.NoopRequest{Ignored: strings.Join(args[:], ",")}) if err != nil { return fmt.Errorf("server responded with error: %w", err) } fmt.Println(response.String()) return err } ================================================ FILE: tests/rpc.bats ================================================ #!/usr/bin/env bats load helpers @test "rpc noop" { run_buildah rpc -l $TEST_SCRATCH_DIR/socket pwd assert "$output" = $(pwd) run_buildah rpc --env LISTENER ${GRPCNOOP_BINARY} --env LISTENER first-arg second-arg assert "$output" = 'ignored:"first-arg,second-arg"' } ================================================ FILE: tests/run.bats ================================================ #!/usr/bin/env bats load helpers @test "run" { skip_if_no_runtime _prefetch alpine ${OCI} --version createrandom ${TEST_SCRATCH_DIR}/randomfile run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah mount $cid root=$output run_buildah config --workingdir /tmp $cid run_buildah run $cid pwd expect_output "/tmp" run_buildah config --workingdir /root $cid run_buildah run $cid pwd expect_output "/root" cp ${TEST_SCRATCH_DIR}/randomfile $root/tmp/ run_buildah run $cid cp /tmp/randomfile /tmp/other-randomfile test -s $root/tmp/other-randomfile cmp ${TEST_SCRATCH_DIR}/randomfile $root/tmp/other-randomfile seq 100000 | buildah run $cid -- sh -c 'while read i; do echo $i; done' } @test "run--args" { skip_if_no_runtime _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output # This should fail, because buildah run doesn't have a -n flag. run_buildah 125 run -n $cid echo test # This should succeed, because buildah run stops caring at the --, which is preserved as part of the command. run_buildah run $cid echo -- -n test expect_output -- "-- -n test" # This should succeed, because buildah run stops caring at the --, which is not part of the command. run_buildah run $cid -- echo -n -- test expect_output -- "-- test" # This should succeed, because buildah run stops caring at the --. run_buildah run $cid -- echo -- -n test -- expect_output -- "-- -n test --" # This should succeed, because buildah run stops caring at the --. run_buildah run $cid -- echo -n "test" expect_output "test" } @test "run-cmd" { skip_if_no_runtime _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah config --workingdir /tmp $cid # Configured entrypoint/cmd shouldn't modify behaviour of run with no arguments # empty entrypoint, configured cmd, empty run arguments run_buildah config --entrypoint "" $cid run_buildah config --cmd pwd $cid run_buildah 125 run $cid expect_output --substring "command must be specified" "empty entrypoint, cmd, no args" # empty entrypoint, configured cmd, empty run arguments, end parsing option run_buildah config --entrypoint "" $cid run_buildah config --cmd pwd $cid run_buildah 125 run $cid -- expect_output --substring "command must be specified" "empty entrypoint, cmd, no args, --" # configured entrypoint, empty cmd, empty run arguments run_buildah config --entrypoint pwd $cid run_buildah config --cmd "" $cid run_buildah 125 run $cid expect_output --substring "command must be specified" "entrypoint, empty cmd, no args" # configured entrypoint, empty cmd, empty run arguments, end parsing option run_buildah config --entrypoint pwd $cid run_buildah config --cmd "" $cid run_buildah 125 run $cid -- expect_output --substring "command must be specified" "entrypoint, empty cmd, no args, --" # configured entrypoint only, empty run arguments run_buildah config --entrypoint pwd $cid run_buildah 125 run $cid expect_output --substring "command must be specified" "entrypoint, no args" # configured entrypoint only, empty run arguments, end parsing option run_buildah config --entrypoint pwd $cid run_buildah 125 run $cid -- expect_output --substring "command must be specified" "entrypoint, no args, --" # configured cmd only, empty run arguments run_buildah config --cmd pwd $cid run_buildah 125 run $cid expect_output --substring "command must be specified" "cmd, no args" # configured cmd only, empty run arguments, end parsing option run_buildah config --cmd pwd $cid run_buildah 125 run $cid -- expect_output --substring "command must be specified" "cmd, no args, --" # configured entrypoint, configured cmd, empty run arguments run_buildah config --entrypoint "pwd" $cid run_buildah config --cmd "whoami" $cid run_buildah 125 run $cid expect_output --substring "command must be specified" "entrypoint, cmd, no args" # configured entrypoint, configured cmd, empty run arguments, end parsing option run_buildah config --entrypoint "pwd" $cid run_buildah config --cmd "whoami" $cid run_buildah 125 run $cid -- expect_output --substring "command must be specified" "entrypoint, cmd, no args" # Configured entrypoint/cmd shouldn't modify behaviour of run with argument # Note: entrypoint and cmd can be invalid in below tests as they should never execute # empty entrypoint, configured cmd, configured run arguments run_buildah config --entrypoint "" $cid run_buildah config --cmd "/invalid/cmd" $cid run_buildah run $cid -- pwd expect_output "/tmp" "empty entrypoint, invalid cmd, pwd" # configured entrypoint, empty cmd, configured run arguments run_buildah config --entrypoint "/invalid/entrypoint" $cid run_buildah config --cmd "" $cid run_buildah run $cid -- pwd expect_output "/tmp" "invalid entrypoint, empty cmd, pwd" # configured entrypoint only, configured run arguments run_buildah config --entrypoint "/invalid/entrypoint" $cid run_buildah run $cid -- pwd expect_output "/tmp" "invalid entrypoint, no cmd(??), pwd" # configured cmd only, configured run arguments run_buildah config --cmd "/invalid/cmd" $cid run_buildah run $cid -- pwd expect_output "/tmp" "invalid cmd, no entrypoint(??), pwd" # configured entrypoint, configured cmd, configured run arguments run_buildah config --entrypoint "/invalid/entrypoint" $cid run_buildah config --cmd "/invalid/cmd" $cid run_buildah run $cid -- pwd expect_output "/tmp" "invalid cmd & entrypoint, pwd" } # Helper for run-user test. Generates a UID or GID that is not present # in the given idfile (mounted /etc/passwd or /etc/group) function random_unused_id() { local idfile=$1 while :;do id=$RANDOM if ! fgrep -q :$id: $idfile; then echo $id return fi done } function configure_and_check_user() { local setting=$1 local expect_u=$2 local expect_g=$3 run_buildah config -u "$setting" $cid run_buildah run -- $cid id -u expect_output "$expect_u" "id -u ($setting)" run_buildah run -- $cid id -g expect_output "$expect_g" "id -g ($setting)" } @test "run-user" { skip_if_no_runtime eval $(go env) echo CGO_ENABLED=${CGO_ENABLED} if test "$CGO_ENABLED" -ne 1; then skip "CGO_ENABLED = '$CGO_ENABLED'" fi _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah mount $cid root=$output testuser=jimbo testbogususer=nosuchuser testgroup=jimbogroup testuid=$(random_unused_id $root/etc/passwd) testotheruid=$(random_unused_id $root/etc/passwd) testgid=$(random_unused_id $root/etc/group) testgroupid=$(random_unused_id $root/etc/group) echo "$testuser:x:$testuid:$testgid:Jimbo Jenkins:/home/$testuser:/bin/sh" >> $root/etc/passwd echo "$testgroup:x:$testgroupid:" >> $root/etc/group configure_and_check_user "" 0 0 configure_and_check_user "${testuser}" $testuid $testgid configure_and_check_user "${testuid}" $testuid $testgid configure_and_check_user "${testuser}:${testgroup}" $testuid $testgroupid configure_and_check_user "${testuid}:${testgroup}" $testuid $testgroupid configure_and_check_user "${testotheruid}:${testgroup}" $testotheruid $testgroupid configure_and_check_user "${testotheruid}" $testotheruid 0 configure_and_check_user "${testuser}:${testgroupid}" $testuid $testgroupid configure_and_check_user "${testuid}:${testgroupid}" $testuid $testgroupid run_buildah config -u ${testbogususer} $cid run_buildah 125 run -- $cid id -u expect_output --substring "unknown user" "id -u (bogus user)" run_buildah 125 run -- $cid id -g expect_output --substring "unknown user" "id -g (bogus user)" ln -vsf /etc/passwd $root/etc/passwd run_buildah config -u ${testuser}:${testgroup} $cid run_buildah 125 run -- $cid id -u echo "$output" expect_output --substring "unknown user" "run as unknown user" } @test "run --env" { skip_if_no_runtime _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah config --env foo=foo $cid # Ensure foo=foo from `buildah config` run_buildah run $cid -- /bin/sh -c 'echo $foo' expect_output "foo" # Ensure foo=bar from --env override run_buildah run --env foo=bar $cid -- /bin/sh -c 'echo $foo' expect_output "bar" # Reference foo=baz from process environment foo=baz run_buildah run --env foo $cid -- /bin/sh -c 'echo $foo' expect_output "baz" # Ensure that the --env override did not persist run_buildah run $cid -- /bin/sh -c 'echo $foo' expect_output "foo" } @test "run --group-add" { skip_if_no_runtime id=$RANDOM _prefetch alpine run_buildah from --group-add $id --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah run $cid id -G expect_output --substring "$id" if is_rootless && has_supplemental_groups && ! [[ $OCI =~ runc ]]; then run_buildah from --group-add keep-groups --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah run $cid id -G expect_output --substring "65534" else skip "test is being run by root or without any supplemental groups ($(id))" fi } @test "run --hostname" { skip_if_no_runtime _prefetch alpine ${OCI} --version run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah run $cid hostname [ "$output" != "foobar" ] run_buildah run --hostname foobar $cid hostname expect_output "foobar" } @test "run should also override /etc/hostname" { skip_if_no_runtime _prefetch alpine ${OCI} --version run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah run --hostname foobar $cid hostname expect_output "foobar" hostname=$output run_buildah run --hostname foobar $cid cat /etc/hostname expect_output $hostname run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah inspect --format "{{ .ContainerID }}" $cid id=$output run_buildah run $cid cat /etc/hostname expect_output "${id:0:12}" run_buildah run --no-hostname $cid cat /etc/hostname expect_output 'localhost' } @test "run --volume" { skip_if_no_runtime zflag= if which selinuxenabled > /dev/null 2> /dev/null ; then if selinuxenabled ; then zflag=z fi fi ${OCI} --version _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output mkdir -p ${TEST_SCRATCH_DIR}/was-empty # As a baseline, this should succeed. run_buildah run -v ${TEST_SCRATCH_DIR}/was-empty:/var/not-empty${zflag:+:${zflag}} $cid touch /var/not-empty/testfile # Parsing options that with comma, this should succeed. run_buildah run -v ${TEST_SCRATCH_DIR}/was-empty:/var/not-empty:rw,rshared${zflag:+,${zflag}} $cid touch /var/not-empty/testfile # If we're parsing the options at all, this should be read-only, so it should fail. run_buildah 1 run -v ${TEST_SCRATCH_DIR}/was-empty:/var/not-empty:ro${zflag:+,${zflag}} $cid touch /var/not-empty/testfile # Even if the parent directory doesn't exist yet, this should succeed. run_buildah run -v ${TEST_SCRATCH_DIR}/was-empty:/var/multi-level/subdirectory $cid touch /var/multi-level/subdirectory/testfile # And check the same for file volumes. run_buildah run -v ${TEST_SCRATCH_DIR}/was-empty/testfile:/var/different-multi-level/subdirectory/testfile $cid touch /var/different-multi-level/subdirectory/testfile # And check the same for file volumes. # Make sure directories show up inside of container on builtin mounts run_buildah run -v ${TEST_SCRATCH_DIR}/was-empty:/run/secrets/testdir $cid ls -ld /run/secrets/testdir } @test "run overlay --volume with custom upper and workdir" { skip_if_no_runtime zflag= if which selinuxenabled > /dev/null 2> /dev/null ; then if selinuxenabled ; then zflag=z fi fi ${OCI} --version _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output mkdir -p ${TEST_SCRATCH_DIR}/upperdir mkdir -p ${TEST_SCRATCH_DIR}/workdir mkdir -p ${TEST_SCRATCH_DIR}/lower echo 'hello' >> ${TEST_SCRATCH_DIR}/lower/hello # As a baseline, this should succeed. run_buildah run -v ${TEST_SCRATCH_DIR}/lower:/test:O,upperdir=${TEST_SCRATCH_DIR}/upperdir,workdir=${TEST_SCRATCH_DIR}/workdir${zflag:+:${zflag}} $cid cat /test/hello expect_output "hello" run_buildah run -v ${TEST_SCRATCH_DIR}/lower:/test:O,upperdir=${TEST_SCRATCH_DIR}/upperdir,workdir=${TEST_SCRATCH_DIR}/workdir${zflag:+:${zflag}} $cid sh -c 'echo "world" > /test/world' #upper dir should persist content result="$(< ${TEST_SCRATCH_DIR}/upperdir/world)" test "$result" == "world" } @test "run --volume with U flag" { skip_if_no_runtime # Create source volume. mkdir ${TEST_SCRATCH_DIR}/testdata # Create the container. _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine ctr="$output" # Test user can create file in the mounted volume. run_buildah run --user 888:888 --volume ${TEST_SCRATCH_DIR}/testdata:/mnt:z,U "$ctr" touch /mnt/testfile1.txt # Test created file has correct UID and GID ownership. run_buildah run --user 888:888 --volume ${TEST_SCRATCH_DIR}/testdata:/mnt:z,U "$ctr" stat -c "%u:%g" /mnt/testfile1.txt expect_output "888:888" } @test "run --user and verify gid in supplemental groups" { skip_if_no_runtime # Create the container. _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine ctr="$output" # Run with uid:gid 1000:1000 and verify if gid is present in additional groups run_buildah run --user 1000:1000 "$ctr" cat /proc/self/status # gid 1000 must be in additional/supplemental groups expect_output --substring "Groups: 1000 " } @test "run --workingdir" { skip_if_no_runtime _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah run $cid pwd expect_output "/" run_buildah run --workingdir /bin $cid pwd expect_output "/bin" # Ensure the /bin workingdir override did not persist run_buildah run $cid pwd expect_output "/" } @test "run --mount" { skip_if_no_runtime zflag= if which selinuxenabled > /dev/null 2> /dev/null ; then if selinuxenabled ; then zflag=,z fi fi ${OCI} --version _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output mkdir -p ${TEST_SCRATCH_DIR}/was:empty # As a baseline, this should succeed. run_buildah run --mount type=tmpfs,dst=/var/tmpfs-not-empty $cid touch /var/tmpfs-not-empty/testfile # This should succeed, but the writes should effectively be discarded run_buildah run --mount type=bind,src=${TEST_SCRATCH_DIR}/was:empty,dst=/var/not-empty,rw${zflag} $cid touch /var/not-empty/testfile if test -r ${TEST_SCRATCH_DIR}/was:empty/testfile ; then die write to mounted type=bind was not discarded, ${TEST_SCRATCH_DIR}/was:empty/testfile exists fi # If we're parsing the options at all, this should be read-only, so it should fail. run_buildah 1 run --mount type=bind,src=${TEST_SCRATCH_DIR}/was:empty,dst=/var/not-empty,ro${zflag} $cid touch /var/not-empty/testfile # Even if the parent directory doesn't exist yet, this should succeed, but again the write should be discarded. run_buildah run --mount type=bind,src=${TEST_SCRATCH_DIR}/was:empty,dst=/var/multi-level/subdirectory,rw${zflag} $cid touch /var/multi-level/subdirectory/testfile if test -r ${TEST_SCRATCH_DIR}/was:empty/testfile ; then die write to mounted type=bind was not discarded, ${TEST_SCRATCH_DIR}/was:empty/testfile exists fi # And check the same for file volumes, which make life harder because the kernel's overlay # filesystem really only wants to be dealing with directories. : > ${TEST_SCRATCH_DIR}/was:empty/testfile run_buildah run --mount type=bind,src=${TEST_SCRATCH_DIR}/was:empty/testfile,dst=/var/different-multi-level/subdirectory/testfile,rw${zflag} $cid sh -c 'echo wrote > /var/different-multi-level/subdirectory/testfile' if test -s ${TEST_SCRATCH_DIR}/was:empty/testfile ; then die write to mounted type=bind was not discarded, ${TEST_SCRATCH_DIR}/was:empty/testfile was written to fi } @test "run --mount=type=bind with from like buildkit" { skip_if_no_runtime zflag= if which selinuxenabled > /dev/null 2> /dev/null ; then if selinuxenabled ; then zflag=,z fi fi run_buildah build -t buildkitbase $WITH_POLICY_JSON -f $BUDFILES/buildkit-mount-from/Dockerfilebuildkitbase $BUDFILES/buildkit-mount-from/ _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah run --mount type=bind,source=.,from=buildkitbase,target=/test${zflag} $cid cat /test/hello1 expect_output --substring "hello1" run_buildah run --mount type=bind,source=subdir,from=buildkitbase,target=/test${zflag} $cid cat /test/hello expect_output --substring "hello2" } @test "run --mount=type=cache like buildkit" { skip_if_no_runtime zflag= if which selinuxenabled > /dev/null 2> /dev/null ; then if selinuxenabled ; then zflag=,z fi fi _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah run --mount type=cache,target=/test${zflag} $cid sh -c 'mkdir -p /test/subdir && echo "hello" > /test/subdir/h.txt && cat /test/subdir/h.txt' run_buildah run --mount type=cache,src=subdir,target=/test${zflag} $cid cat /test/h.txt expect_output --substring "hello" } @test "run symlinks" { skip_if_no_runtime ${OCI} --version _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output mkdir -p ${TEST_SCRATCH_DIR}/tmp ln -s tmp ${TEST_SCRATCH_DIR}/tmp2 export TMPDIR=${TEST_SCRATCH_DIR}/tmp2 run_buildah run $cid id } @test "run --cap-add/--cap-drop" { skip_if_no_runtime ${OCI} --version _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output # Try with default caps. run_buildah run $cid grep ^CapEff /proc/self/status defaultcaps="$output" # Try adding DAC_OVERRIDE. run_buildah run --cap-add CAP_DAC_OVERRIDE $cid grep ^CapEff /proc/self/status addedcaps="$output" # Try dropping DAC_OVERRIDE. run_buildah run --cap-drop CAP_DAC_OVERRIDE $cid grep ^CapEff /proc/self/status droppedcaps="$output" # Okay, now the "dropped" and "added" should be different. test "$addedcaps" != "$droppedcaps" # And one or the other should be different from the default, with the other being the same. if test "$defaultcaps" == "$addedcaps" ; then test "$defaultcaps" != "$droppedcaps" fi if test "$defaultcaps" == "$droppedcaps" ; then test "$defaultcaps" != "$addedcaps" fi } @test "Check if containers run with correct open files/processes limits" { skip_if_no_runtime skip "FIXME: we cannot rely on podman-run ulimits matching buildah-bud (see #5820)" # we need to not use the list of limits that are set in our default # ${TEST_SOURCES}/containers.conf for the sake of other tests, and override # any that might be picked up from system-wide configuration echo '[containers]' > ${TEST_SCRATCH_DIR}/containers.conf echo 'default_ulimits = []' >> ${TEST_SCRATCH_DIR}/containers.conf export CONTAINERS_CONF=${TEST_SCRATCH_DIR}/containers.conf _prefetch alpine run podman run --rm alpine sh -c "awk '/open files/{print \$4 \"/\" \$5}' /proc/self/limits" podman_files=$output run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah run $cid awk '/open files/{print $4 "/" $5}' /proc/self/limits expect_output "${podman_files}" "limits: podman and buildah should agree on open files" run podman run --rm alpine sh -c "awk '/processes/{print \$3 \"/\" \$4}' /proc/self/limits" podman_processes=$output run_buildah run $cid awk '/processes/{print $3 "/" $4}' /proc/self/limits expect_output ${podman_processes} "processes should match podman" run_buildah rm $cid run_buildah from --quiet --ulimit nofile=300:400 --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah run $cid awk '/open files/{print $4}' /proc/self/limits expect_output "300" "limits: open files (w/file limit)" run_buildah rm $cid run_buildah from --quiet --ulimit nproc=100:200 --ulimit nofile=300:400 --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah run $cid awk '/open files/{print $4}' /proc/self/limits expect_output "300" "limits: open files (w/file & proc limits)" run_buildah run $cid awk '/processes/{print $3}' /proc/self/limits expect_output "100" "limits: processes (w/file & proc limits)" unset CONTAINERS_CONF } @test "run-builtin-volume-omitted" { # This image is known to include a volume, but not include the mountpoint # in the image. _prefetch quay.io/libpod/registry:volume_omitted run_buildah from --quiet --cidfile ${TEST_SCRATCH_DIR}/cid --pull=ifmissing $WITH_POLICY_JSON quay.io/libpod/registry:volume_omitted cid="$(< ${TEST_SCRATCH_DIR}/cid)" run_buildah mount $cid mnt=$output # By default, the mountpoint should not be there. run test -d "$mnt"/var/lib/registry echo "$output" [ "$status" -ne 0 ] # We'll create the mountpoint for "run". run_buildah run $cid ls -1 /var/lib expect_output --substring "registry" # Double-check that the mountpoint is there. test -d "$mnt"/var/lib/registry } @test "run-exit-status" { skip_if_no_runtime _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah 42 run ${cid} sh -c 'exit 42' } @test "run-exit-status on non executable" { skip_if_no_runtime _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah 1 run ${cid} /etc } @test "Verify /run/.containerenv exist" { skip_if_rootless_environment skip_if_no_runtime _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output # test a standard mount to /run/.containerenv run_buildah run $cid ls -1 /run/.containerenv expect_output --substring "/run/.containerenv" run_buildah run $cid sh -c '. /run/.containerenv; echo $engine' expect_output --substring "buildah" run_buildah run $cid sh -c '. /run/.containerenv; echo $name' expect_output "alpine-working-container" run_buildah run $cid sh -c '. /run/.containerenv; echo $image' expect_output --substring "alpine:latest" rootless=0 if ["$(id -u)" -ne 0 ]; then rootless=1 fi run_buildah run $cid sh -c '. /run/.containerenv; echo $rootless' expect_output ${rootless} } @test "run-device" { skip_if_no_runtime _prefetch alpine run_buildah from --quiet --pull=false --device /dev/fuse $WITH_POLICY_JSON alpine cid=$output run_buildah 0 run ${cid} ls /dev/fuse run_buildah from --quiet --pull=false --device /dev/fuse:/dev/fuse:rm $WITH_POLICY_JSON alpine cid=$output run_buildah 0 run ${cid} ls /dev/fuse run_buildah from --quiet --pull=false --device /dev/fuse:/dev/fuse:rwm $WITH_POLICY_JSON alpine cid=$output run_buildah 0 run ${cid} ls /dev/fuse } @test "run-device-Rename" { skip_if_rootless_environment skip_if_no_runtime skip_if_chroot skip_if_rootless _prefetch alpine run_buildah from --quiet --pull=false --device /dev/fuse:/dev/fuse1 $WITH_POLICY_JSON alpine cid=$output run_buildah 0 run ${cid} ls /dev/fuse1 } @test "run check /etc/hosts" { skip_if_rootless_environment skip_if_no_runtime skip_if_in_container ${OCI} --version # We use ubuntu image because it has no /etc/hosts file. This # allows the fake_host test below to be an equality check, # not a substring check. _prefetch ubuntu local hostname=h-$(random_string) run_buildah from --quiet --pull=false $WITH_POLICY_JSON ubuntu cid=$output run_buildah 125 run --network=bogus $cid cat /etc/hosts expect_output --substring "unable to find network with name or ID bogus: network not found" run_buildah run --hostname $hostname $cid cat /etc/hosts expect_output --substring "(10.88.*|10.0.2.100)[[:blank:]]$hostname $cid" ip=$(hostname -I | cut -f 1 -d " ") expect_output --substring "$ip.*host.containers.internal" hosts="127.0.0.5 host1 127.0.0.6 host2" base_hosts_file="$TEST_SCRATCH_DIR/base_hosts" echo "$hosts" > "$base_hosts_file" containers_conf_file="$TEST_SCRATCH_DIR/containers.conf" echo -e "[containers]\nbase_hosts_file = \"$base_hosts_file\"" > "$containers_conf_file" CONTAINERS_CONF="$containers_conf_file" run_buildah run --hostname $hostname $cid cat /etc/hosts expect_output --substring "127.0.0.5[[:blank:]]host1" expect_output --substring "127.0.0.6[[:blank:]]host2" expect_output --substring "(10.88.*|10.0.2.100)[[:blank:]]$hostname $cid" # now check that hostname from base file is not overwritten CONTAINERS_CONF="$containers_conf_file" run_buildah run --hostname host1 $cid cat /etc/hosts expect_output --substring "127.0.0.5[[:blank:]]host1" expect_output --substring "127.0.0.6[[:blank:]]host2" expect_output --substring "(10.88.*|10.0.2.100)[[:blank:]]$cid" assert "$output" !~ "(10.88.*|10.0.2.100)[[:blank:]]host1 $cid" "Container IP should not contain host1" # check slirp4netns sets correct hostname with another cidr run_buildah run --network slirp4netns:cidr=192.168.2.0/24 --hostname $hostname $cid cat /etc/hosts expect_output --substring "192.168.2.100[[:blank:]]$hostname $cid" run_buildah run --network=container $cid cat /etc/hosts m=$(buildah mount $cid) run cat $m/etc/hosts [ "$status" -eq 0 ] expect_output --substring "" run_buildah rm -a run_buildah from --quiet --pull=false $WITH_POLICY_JSON ubuntu cid=$output run_buildah run --network=host --hostname $hostname $cid cat /etc/hosts assert "$output" =~ "$ip[[:blank:]]$hostname" hostOutput=$output m=$(buildah mount $cid) run cat $m/etc/hosts [ "$status" -eq 0 ] expect_output --substring "" run_buildah run --network=host --no-hosts $cid cat /etc/hosts [ "$output" != "$hostOutput" ] # --isolation chroot implies host networking so check for the correct hosts entry run_buildah run --isolation chroot --hostname $hostname $cid cat /etc/hosts assert "$output" =~ "$ip[[:blank:]]$hostname" run_buildah rm -a run_buildah from --quiet --pull=false $WITH_POLICY_JSON ubuntu cid=$output run_buildah run --network=none $cid sh -c 'echo "110.110.110.0 fake_host" >> /etc/hosts; cat /etc/hosts' expect_output "110.110.110.0 fake_host" m=$(buildah mount $cid) run cat $m/etc/hosts [ "$status" -eq 0 ] expect_output "110.110.110.0 fake_host" run_buildah rm -a } @test "run check /etc/hosts and resolv.conf with --network pasta" { skip_if_no_runtime skip_if_chroot skip_if_root_environment "pasta only works rootless" _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output local hostname=h-$(random_string) ip=$(hostname -I | cut -f 1 -d " ") run_buildah run --network pasta --hostname $hostname $cid cat /etc/hosts assert "$output" =~ "$ip[[:blank:]]$hostname $cid" "--network pasta adds correct hostname" # FIXME we need pasta 20240814 or newer in the VMs to enable this # assert "$output" =~ "169.254.1.2[[:blank:]]host.containers.internal" "--network pasta adds correct internal entry" # check with containers.conf setting echo -e "[network]\ndefault_rootless_network_cmd = \"pasta\"" > ${TEST_SCRATCH_DIR}/containers.conf CONTAINERS_CONF_OVERRIDE=${TEST_SCRATCH_DIR}/containers.conf run_buildah run --hostname $hostname $cid cat /etc/hosts assert "$output" =~ "$ip[[:blank:]]$hostname $cid" "default_rootless_network_cmd = \"pasta\" works" # resolv.conf checks run_buildah run --network pasta $cid grep nameserver /etc/resolv.conf assert "${lines[0]}" == "nameserver 169.254.1.1" "first pasta nameserver should be stub forwarder" run_buildah run --network pasta:--dns-forward,192.168.0.1 $cid grep nameserver /etc/resolv.conf assert "${lines[0]}" == "nameserver 192.168.0.1" "pasta nameserver with --dns-forward" } @test "run check /etc/resolv.conf" { skip_if_rootless_environment skip_if_no_runtime ${OCI} --version _prefetch alpine # Make sure to read the correct /etc/resolv.conf file in case of systemd-resolved. resolve_file=$(readlink -f /etc/resolv.conf) if [[ "$resolve_file" == "/run/systemd/resolve/stub-resolv.conf" ]]; then resolve_file="/run/systemd/resolve/resolv.conf" fi run grep nameserver $resolve_file # filter out 127... nameservers run grep -v "nameserver 127." <<< "$output" nameservers="$output" # in case of rootless add extra slirp4netns nameserver if is_rootless; then nameservers="nameserver 10.0.2.3 $output" fi run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah run --network=private $cid grep nameserver /etc/resolv.conf # check that no 127... nameserver is in resolv.conf assert "$output" !~ "^nameserver 127." "Container contains local nameserver" assert "$nameservers" "Container nameservers match correct host nameservers" if ! is_rootless; then run_buildah mount $cid assert "$output" != "" if test -e "$output/etc/resolv.conf" ; then assert "$(< $output/etc/resolv.conf)" = "" "resolv.conf is empty" else : # not there, like in the base image, which is what we really want fi fi run_buildah rm -a run grep nameserver /etc/resolv.conf nameservers="$output" run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah run --isolation=chroot --network=host $cid grep nameserver /etc/resolv.conf assert "$nameservers" "Container nameservers match the host nameservers" if ! is_rootless; then run_buildah mount $cid assert "$output" != "" if test -e "$output/etc/resolv.conf" ; then assert "$(< $output/etc/resolv.conf)" = "" "resolv.conf is empty" else : # not there, like in the base image, which is what we really want fi fi run_buildah rm -a run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah 125 run --isolation=chroot --network=none $cid sh -c 'echo "nameserver 110.110.0.110" >> /etc/resolv.conf; cat /etc/resolv.conf' expect_output --substring "cannot set --network other than host with --isolation chroot" run_buildah rm -a } @test "run --network=none and --isolation chroot must conflict" { skip_if_no_runtime _prefetch alpine run_buildah from --quiet --pull=ifmissing $WITH_POLICY_JSON alpine cid=$output # should fail by default run_buildah 125 run --isolation=chroot --network=none $cid wget google.com expect_output --substring "cannot set --network other than host with --isolation chroot" } @test "run --network=private must mount a fresh /sys" { skip_if_no_runtime _prefetch alpine run_buildah from --quiet --pull=ifmissing $WITH_POLICY_JSON alpine cid=$output # verify there is no /sys/kernel/security in the container, that would mean /sys # was bind mounted from the host. run_buildah 1 run --network=private $cid grep /sys/kernel/security /proc/self/mountinfo } @test "run --network should override build --network" { skip_if_no_runtime _prefetch alpine run_buildah from --network=none --quiet --pull=ifmissing $WITH_POLICY_JSON alpine cid=$output # should fail by default run_buildah 1 run $cid wget google.com expect_output --substring "bad" # try pinging external website run_buildah run --network=private $cid wget google.com expect_output --substring "index.html" run_buildah rm -a } @test "run --user" { skip_if_no_runtime _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah run --user sync $cid whoami expect_output "sync" run_buildah 125 run --user noexist $cid whoami expect_output --substring "unknown user error" } @test "run --runtime --runtime-flag" { skip_if_in_container skip_if_no_runtime _prefetch alpine # Use seccomp to make crun output a warning message because crun writes few logs. cat > ${TEST_SCRATCH_DIR}/seccomp.json << _EOF { "defaultAction": "SCMP_ACT_ALLOW", "syscalls": [ { "name": "unknown", "action": "SCMP_ACT_KILL" } ] } _EOF run_buildah from --security-opt seccomp=${TEST_SCRATCH_DIR}/seccomp.json --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output local found_runtime= if [ -n "$(command -v runc)" ]; then found_runtime=y run_buildah run --runtime=runc --runtime-flag=debug $cid true assert "$output" != "" "Output from running 'true' with --runtime-flag=debug" fi if [ -n "$(command -v crun)" ]; then found_runtime=y run_buildah run --runtime=crun --runtime-flag=log=${TEST_SCRATCH_DIR}/oci-log $cid true if test \! -e ${TEST_SCRATCH_DIR}/oci-log; then die "the expected file ${TEST_SCRATCH_DIR}/oci-log was not created" fi fi if [ -z "${found_runtime}" ]; then skip "Did not find 'runc' nor 'crun' in \$PATH - could not run this test!" fi } @test "run --terminal" { skip_if_no_runtime _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah run --terminal=true $cid ls --color=auto colored="$output" run_buildah run --terminal=false $cid ls --color=auto uncolored="$output" [ "$colored" != "$uncolored" ] } @test "rootless on cgroupv2 and systemd runs under user.slice" { skip_if_no_runtime skip_if_in_container skip_if_root_environment if test "$DBUS_SESSION_BUS_ADDRESS" = ""; then skip "$test does not work when DBUS_SESSION_BUS_ADDRESS is not defined" fi _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah run --cgroupns=host $cid cat /proc/self/cgroup expect_output --substring "/user.slice/" } @test "run-inheritable-capabilities" { skip_if_no_runtime _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah run $cid grep ^CapInh: /proc/self/status expect_output "CapInh: 0000000000000000" run_buildah run --cap-add=ALL $cid grep ^CapInh: /proc/self/status expect_output "CapInh: 0000000000000000" } @test "run masks" { skip_if_no_runtime _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output for mask in /proc/acpi /proc/interrupts /proc/kcore /proc/keys /proc/latency_stats /proc/sched_debug /proc/scsi /proc/timer_list /proc/timer_stats /sys/devices/virtual/powercap /sys/firmware /sys/fs/selinux; do if test -d $mask; then run_buildah run $cid sh -c "echo $mask/*" # globbing will fail whether it's simply unreadable, or readable but empty expect_output "$mask/*" "Directories should be empty" fi if test -f $mask; then run_buildah run $cid cat $mask expect_output "" "Directories should be empty" fi done } @test "empty run statement doesn't crash" { skip_if_no_runtime _prefetch alpine cd ${TEST_SCRATCH_DIR} printf 'FROM alpine\nRUN \\\n echo && echo' > Dockerfile run_buildah bud --pull=false --layers . printf 'FROM alpine\nRUN\n echo && echo' > Dockerfile run_buildah ? bud --pull=false --layers . expect_output --substring -- "-c requires an argument" } @test "container_name_as_hostname" { skip_if_no_runtime _prefetch alpine echo '[containers]' > ${TEST_SCRATCH_DIR}/containers.conf echo container_name_as_hostname = true >> ${TEST_SCRATCH_DIR}/containers.conf name=alpine-working_containeR-4-test sanitizedname=alpine-workingcontaineR-4-test run_buildah from --name :"$name": --cidfile ${TEST_SCRATCH_DIR}/cid alpine cname="$output" run_buildah run "$cname" hostname assert "$output" = $(cut -c1-12 < ${TEST_SCRATCH_DIR}/cid) CONTAINERS_CONF=${TEST_SCRATCH_DIR}/containers.conf run_buildah run "$cname" hostname expect_output "$sanitizedname" CONTAINERS_CONF=${TEST_SCRATCH_DIR}/containers.conf run_buildah run "$(< ${TEST_SCRATCH_DIR}/cid)" hostname expect_output "$sanitizedname" } @test "root fs only mounted once" { if test `uname` != Linux ; then skip "not meaningful except on Linux" fi _prefetch busybox run_buildah from --pull=never --quiet busybox cid="$output" run_buildah run $cid cat /proc/self/mountinfo echo "$output" > ${TEST_SCRATCH_DIR}/mountinfo1 echo "# mountinfo unfiltered:" cat ${TEST_SCRATCH_DIR}/mountinfo1 grep ' / rw[, ]' ${TEST_SCRATCH_DIR}/mountinfo1 > ${TEST_SCRATCH_DIR}/mountinfo2 echo "# mountinfo grepped:" cat ${TEST_SCRATCH_DIR}/mountinfo2 wc -l < ${TEST_SCRATCH_DIR}/mountinfo2 > ${TEST_SCRATCH_DIR}/mountinfo3 echo "# mountinfo count:" cat ${TEST_SCRATCH_DIR}/mountinfo3 assert $(< ${TEST_SCRATCH_DIR}/mountinfo3) -eq 1 } @test "run reaps stray processes" { if test `uname` != Linux ; then skip "not meaningful except on Linux" fi _prefetch busybox run_buildah from --pull=never --quiet busybox local cid="$output" echo '$' ${WAIT_BINARY} ${BUILDAH_BINARY} ${BUILDAH_REGISTRY_OPTS} ${ROOTDIR_OPTS} run --runtime=${CRASH_BINARY} "$cid" pwd run ${WAIT_BINARY} ${BUILDAH_BINARY} ${BUILDAH_REGISTRY_OPTS} ${ROOTDIR_OPTS} run --runtime=${CRASH_BINARY} "$cid" pwd echo "$output" assert "$output" !~ "caught reparented child process" } ================================================ FILE: tests/sbom.bats ================================================ #!/usr/bin/env bats load helpers @test "commit-sbom-types" { _prefetch alpine ghcr.io/anchore/syft ghcr.io/aquasecurity/trivy run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output for squash in "--squash" "" ; do for sbomtype in syft syft-cyclonedx syft-spdx trivy trivy-cyclonedx trivy-spdx; do echo "[sbom type $sbomtype${squash:+, $squash}]" # clear out one file that we might need to overwrite, but leave the other to # ensure that we don't accidentally append content to files that are already # present rm -f ${TEST_SCRATCH_DIR}/localpurl.json # write to both the image and the local filesystem run_buildah commit $WITH_POLICY_JSON --sbom ${sbomtype} \ --sbom-output=${TEST_SCRATCH_DIR}/localsbom.json \ --sbom-purl-output=${TEST_SCRATCH_DIR}/localpurl.json \ --sbom-image-output=/root/sbom.json \ --sbom-image-purl-output=/root/purl.json \ $squash $cid alpine-derived-image # both files should exist now, and neither should be empty test -s ${TEST_SCRATCH_DIR}/localsbom.json test -s ${TEST_SCRATCH_DIR}/localpurl.json # compare them to their equivalents in the image run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine-derived-image dcid=$output run_buildah mount $dcid mountpoint=$output cmp $mountpoint/root/purl.json ${TEST_SCRATCH_DIR}/localpurl.json cmp $mountpoint/root/sbom.json ${TEST_SCRATCH_DIR}/localsbom.json done done } @test "bud-sbom-types" { _prefetch alpine ghcr.io/anchore/syft ghcr.io/aquasecurity/trivy for layers in --layers=true --layers=false --squash ; do for sbomtype in syft syft-cyclonedx syft-spdx trivy trivy-cyclonedx trivy-spdx; do echo "[sbom type $sbomtype with $layers]" # clear out one file that we might need to overwrite, but leave the other to # ensure that we don't accidentally append content to files that are already # present rm -f ${TEST_SCRATCH_DIR}/localpurl.json # write to both the image and the local filesystem run_buildah build $WITH_POLICY_JSON --sbom ${sbomtype} \ --sbom-output=${TEST_SCRATCH_DIR}/localsbom.json \ --sbom-purl-output=${TEST_SCRATCH_DIR}/localpurl.json \ --sbom-image-output=/root/sbom.json \ --sbom-image-purl-output=/root/purl.json \ $layers -t alpine-derived-image $BUDFILES/simple-multi-step # both files should exist now, and neither should be empty test -s ${TEST_SCRATCH_DIR}/localsbom.json test -s ${TEST_SCRATCH_DIR}/localpurl.json # compare them to their equivalents in the image run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine-derived-image dcid=$output run_buildah mount $dcid mountpoint=$output cmp $mountpoint/root/purl.json ${TEST_SCRATCH_DIR}/localpurl.json cmp $mountpoint/root/sbom.json ${TEST_SCRATCH_DIR}/localsbom.json done done } @test "bud-sbom-with-no-changes" { _prefetch alpine ghcr.io/anchore/syft ghcr.io/aquasecurity/trivy for sbomtype in syft syft-cyclonedx syft-spdx trivy trivy-cyclonedx trivy-spdx; do echo "[sbom type $sbomtype with $layers]" run_buildah build $WITH_POLICY_JSON --sbom ${sbomtype} \ --sbom-output=${TEST_SCRATCH_DIR}/localsbom.json \ --sbom-purl-output=${TEST_SCRATCH_DIR}/localpurl.json \ --sbom-image-output=/root/sbom.json \ --sbom-image-purl-output=/root/purl.json \ -t busybox-derived-image $BUDFILES/pull # both files should exist now, and neither should be empty test -s ${TEST_SCRATCH_DIR}/localsbom.json test -s ${TEST_SCRATCH_DIR}/localpurl.json done } @test "bud-sbom-with-only-config-changes" { _prefetch alpine ghcr.io/anchore/syft ghcr.io/aquasecurity/trivy for layers in --layers=true --layers=false ; do for sbomtype in syft syft-cyclonedx syft-spdx trivy trivy-cyclonedx trivy-spdx; do echo "[sbom type $sbomtype with $layers]" # clear out one file that we might need to overwrite, but leave the other to # ensure that we don't accidentally append content to files that are already # present rm -f ${TEST_SCRATCH_DIR}/localpurl.json run_buildah build $WITH_POLICY_JSON --sbom ${sbomtype} \ --sbom-output=${TEST_SCRATCH_DIR}/localsbom.json \ --sbom-purl-output=${TEST_SCRATCH_DIR}/localpurl.json \ --sbom-image-output=/root/sbom.json \ --sbom-image-purl-output=/root/purl.json \ $layers -t alpine-derived-image -f $BUDFILES/env/Dockerfile.check-env $BUDFILES/env # both files should exist now, and neither should be empty test -s ${TEST_SCRATCH_DIR}/localsbom.json test -s ${TEST_SCRATCH_DIR}/localpurl.json done done } @test "bud-sbom-with-non-presets" { _prefetch alpine busybox run_buildah build --debug $WITH_POLICY_JSON \ --sbom-output=${TEST_SCRATCH_DIR}/localsbom.txt \ --sbom-purl-output=${TEST_SCRATCH_DIR}/localpurl.txt \ --sbom-image-output=/root/sbom.txt \ --sbom-image-purl-output=/root/purl.txt \ --sbom-scanner-image=alpine \ --sbom-scanner-command='echo SCANNED ROOT {ROOTFS} > {OUTPUT}' \ --sbom-scanner-command='echo SCANNED BUILD CONTEXT {CONTEXT} > {OUTPUT}' \ --sbom-merge-strategy=cat \ -t busybox-derived-image $BUDFILES/pull # both files should exist now, and neither should be empty test -s ${TEST_SCRATCH_DIR}/localsbom.txt test -s ${TEST_SCRATCH_DIR}/localpurl.txt } ================================================ FILE: tests/selinux.bats ================================================ #!/usr/bin/env bats load helpers @test "selinux test" { if ! which selinuxenabled > /dev/null 2> /dev/null ; then skip 'selinuxenabled command not found in $PATH' elif ! selinuxenabled ; then skip "selinux is disabled" fi image=alpine _prefetch $image # Create a container and read its context as a baseline. run_buildah from --quiet --quiet $WITH_POLICY_JSON $image cid=$output run_buildah run $cid sh -c 'tr \\0 \\n < /proc/self/attr/current' assert "$output" != "" "/proc/self/attr/current cannot be empty" firstlabel="$output" # Ensure that we label the same container consistently across multiple "run" instructions. run_buildah run $cid sh -c 'tr \\0 \\n < /proc/self/attr/current' expect_output "$firstlabel" "label of second container == first" # Ensure that different containers get different labels. run_buildah from --quiet --quiet $WITH_POLICY_JSON $image cid1=$output run_buildah run $cid1 sh -c 'tr \\0 \\n < /proc/self/attr/current' assert "$output" != "$firstlabel" \ "Second container has the same label as first (both '$output')" } @test "selinux spc" { if ! which selinuxenabled > /dev/null 2> /dev/null ; then skip "No selinuxenabled" elif ! selinuxenabled ; then skip "selinux is disabled" fi image=alpine _prefetch $image # Create a container and read its context as a baseline. run_buildah from --quiet --security-opt label=disable --quiet $WITH_POLICY_JSON $image cid=$output run_buildah run $cid sh -c 'tr \\0 \\n < /proc/self/attr/current' context=$output run id -Z crole=$(secon -r $output) # Role and Type should always be constant. (We don't check user) role=$(awk -F: '{print $2}' <<<$context) expect_output --from="$role" "${crole}" "SELinux role" type=$(awk -F: '{print $3}' <<<$context) expect_output --from="$type" "spc_t" "SELinux type" # Range should match that of the invoking process my_range=$(id -Z |awk -F: '{print $4 ":" $5}') container_range=$(awk -F: '{print $4 ":" $5}' <<<$context) expect_output --from="$container_range" "$my_range" "SELinux range: container matches process" } @test "selinux specific level" { if ! which selinuxenabled > /dev/null 2> /dev/null ; then skip "No selinuxenabled" elif ! selinuxenabled ; then skip "selinux is disabled" fi image=alpine _prefetch $image firstlabel="system_u:system_r:container_t:s0:c1,c2" # Create a container and read its context as a baseline. run_buildah from --quiet --security-opt label="level:s0:c1,c2" --quiet $WITH_POLICY_JSON $image cid=$output # Inspect image run_buildah inspect --format '{{.ProcessLabel}}' $cid expect_output "$firstlabel" # Check actual running context run_buildah run $cid sh -c 'tr \\0 \\n < /proc/self/attr/current' expect_output "$firstlabel" "running container context" } ================================================ FILE: tests/serve/serve.go ================================================ package main import ( "context" "errors" "fmt" "html" "io" "io/fs" "log" "net" "net/http" "net/http/cgi" "os" "os/exec" "path" "path/filepath" "strconv" "strings" ) func sendThatFile(basepath string) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { filename := filepath.Join(basepath, filepath.Clean(string([]rune{filepath.Separator})+filepath.FromSlash(r.URL.Path))) f, err := os.Open(filename) if err != nil { if errors.Is(err, os.ErrNotExist) { http.NotFound(w, r) return } http.Error(w, err.Error(), http.StatusInternalServerError) return } defer f.Close() finfo, err := f.Stat() if err != nil { http.Error(w, fmt.Sprintf("checking file info: %v", err), http.StatusInternalServerError) return } content := io.ReadSeeker(f) if finfo.IsDir() { names, err := f.ReadDir(-1) if err != nil { http.Error(w, fmt.Sprintf("reading directory: %v", err), http.StatusInternalServerError) } var builder strings.Builder builder.WriteString("") for _, name := range names { suffix := "" if name.IsDir() { suffix = "/" } fmt.Fprintf(&builder, "%s
\n", path.Join(r.URL.Path, name.Name())+suffix, html.EscapeString(fs.FormatDirEntry(name))) } fmt.Fprint(&builder, "") content = strings.NewReader(builder.String()) } http.ServeContent(w, r, filename, finfo.ModTime(), content) } } func runThatCGI(baseLocation, basePath, handlerAndEnvs string) func(w http.ResponseWriter, r *http.Request) { handler, envList, _ := strings.Cut(handlerAndEnvs, ":") handlerCmd := strings.Fields(handler) // github.com/mattn/go-shellwords.Parse() would be better, but we want to not require anything outside of the standard library, so this will have to do if !filepath.IsAbs(handlerCmd[0]) { abs, err := exec.LookPath(handlerCmd[0]) if err != nil { log.Fatalf("finding %q: %v", handler, err) } handlerCmd[0] = abs } envSlice := strings.Split(envList, ":") var envs, inherits []string for _, env := range envSlice { if strings.Contains(env, "=") { envs = append(envs, env) } else { inherits = append(inherits, env) } } cgiBackend := cgi.Handler{ Path: handlerCmd[0], Args: handlerCmd[1:], Root: baseLocation, Dir: basePath, Env: envs, InheritEnv: inherits, Stderr: os.Stderr, PathLocationHandler: http.DefaultServeMux, } log.Printf("cgi: %q -> %q started in %q %+v", baseLocation, handler, basePath, cgiBackend.Env) return cgiBackend.ServeHTTP } func main() { args := os.Args if len(args) < 2 { log.Fatal("requires subdirectory path [and optional port [and optional port file name [and optional TLS cert and key file names [and optional pid file name]]]]") } spec := args[1] port := "0" if len(args) > 2 { port = args[2] } certs, key := "", "" if len(args) > 5 && args[4] != "" && args[5] != "" { certs = args[4] key = args[5] } if len(args) > 6 && args[6] != "" { err := os.WriteFile(args[6], []byte(strconv.Itoa(os.Getpid())), 0o644) if err != nil { log.Fatalf("%v", err) } } roots := false for specPart := range strings.SplitSeq(spec, ",") { if content, subdirectoryAndHandlerAndEnvs, ok := strings.Cut(specPart, "="); ok { if subdirectory, handlerAndEnvs, ok := strings.Cut(subdirectoryAndHandlerAndEnvs, ":"); ok { http.HandleFunc(content, runThatCGI(content, subdirectory, handlerAndEnvs)) } else { http.HandleFunc(content, sendThatFile(subdirectory)) } } else { if roots { log.Fatal("only one content root allowed") } http.HandleFunc("/", sendThatFile(content)) roots = true } } server := http.Server{ Addr: ":" + port, BaseContext: func(l net.Listener) context.Context { if tcp, ok := l.Addr().(*net.TCPAddr); ok { if len(args) > 3 { f, err := os.CreateTemp(filepath.Dir(args[3]), filepath.Base(args[3])) if err != nil { log.Fatalf("%v", err) } tempName := f.Name() port := strconv.Itoa(tcp.Port) if n, err := f.WriteString(port); err != nil || n != len(port) { if err != nil { log.Fatalf("%v", err) } log.Fatalf("short write: %d != %d", n, len(port)) } f.Close() if err := os.Rename(tempName, args[3]); err != nil { log.Fatalf("rename: %v", err) } } } return context.Background() }, } if certs != "" && key != "" { log.Fatal(server.ListenAndServeTLS(certs, key)) } log.Fatal(server.ListenAndServe()) } ================================================ FILE: tests/sign.bats ================================================ #!/usr/bin/env bats load helpers function _gpg_setup() { if ! which gpg > /dev/null 2> /dev/null ; then skip 'gpg command not found in $PATH' fi export GNUPGHOME=${TEST_SCRATCH_DIR}/.gnupg mkdir -p --mode=0700 $GNUPGHOME # gpg on f30 and above needs this, otherwise: # gpg: agent_genkey failed: Inappropriate ioctl for device # ...but gpg on f29 (and, probably, Ubuntu) doesn't grok this GPGOPTS='--pinentry-mode loopback' if gpg --pinentry-mode asdf 2>&1 | grep -qi 'Invalid option'; then GPGOPTS= fi cat > ${TEST_SCRATCH_DIR}/genkey-answers <<- EOF %echo Generating a basic OpenPGP key Key-Type: RSA Key-Length: 2048 Name-Real: Amanda Lorian Name-Comment: Mandy to her friends Name-Email: amanda@localhost %commit %echo done EOF gpg --batch $GPGOPTS --gen-key --passphrase '' < ${TEST_SCRATCH_DIR}/genkey-answers } @test "commit-pull-push-signatures" { _gpg_setup _prefetch alpine mkdir -p ${TEST_SCRATCH_DIR}/signed-image ${TEST_SCRATCH_DIR}/unsigned-image run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah commit $WITH_POLICY_JSON --sign-by amanda@localhost $cid signed-alpine-image # Pushing should preserve the signature. run_buildah push $WITH_POLICY_JSON signed-alpine-image dir:${TEST_SCRATCH_DIR}/signed-image ls -l ${TEST_SCRATCH_DIR}/signed-image/ cat ${TEST_SCRATCH_DIR}/signed-image/version cat ${TEST_SCRATCH_DIR}/signed-image/manifest.json test -s ${TEST_SCRATCH_DIR}/signed-image/signature-1 # Pushing with --remove-signatures should remove the signature. run_buildah push $WITH_POLICY_JSON --remove-signatures signed-alpine-image dir:${TEST_SCRATCH_DIR}/unsigned-image ls -l ${TEST_SCRATCH_DIR}/unsigned-image/ cat ${TEST_SCRATCH_DIR}/unsigned-image/version cat ${TEST_SCRATCH_DIR}/unsigned-image/manifest.json ! test -s ${TEST_SCRATCH_DIR}/unsigned-image/signature-1 run_buildah commit $WITH_POLICY_JSON $cid unsigned-alpine-image # Pushing with --sign-by should fail add the signature to a dir: location, if it tries to add them. run_buildah 125 push $WITH_POLICY_JSON --sign-by amanda@localhost unsigned-alpine-image dir:${TEST_SCRATCH_DIR}/signed-image expect_output --substring "Cannot determine canonical Docker reference" ls -l ${TEST_SCRATCH_DIR}/signed-image/ # Clear out images, so that we don't have leftover signatures when we pull in an image that will end up # causing us to merge its contents with the image with the same ID. run_buildah rmi -a -f # Pulling with --remove-signatures should remove signatures, and pushing should have none to keep. run_buildah pull $WITH_POLICY_JSON --quiet dir:${TEST_SCRATCH_DIR}/signed-image imageID="$output" run_buildah push $WITH_POLICY_JSON "$imageID" dir:${TEST_SCRATCH_DIR}/unsigned-image ls -l ${TEST_SCRATCH_DIR}/unsigned-image/ cat ${TEST_SCRATCH_DIR}/unsigned-image/version cat ${TEST_SCRATCH_DIR}/unsigned-image/manifest.json ! test -s ${TEST_SCRATCH_DIR}/unsigned-image/signature-1 # Build a manifest list and try to push the list with signatures. run_buildah manifest create list run_buildah manifest add list $imageID run_buildah 125 manifest push $WITH_POLICY_JSON --sign-by amanda@localhost --all list dir:${TEST_SCRATCH_DIR}/signed-list expect_output --substring "Cannot determine canonical Docker reference" run_buildah manifest push $WITH_POLICY_JSON --all list dir:${TEST_SCRATCH_DIR}/unsigned-list ls -l ${TEST_SCRATCH_DIR}/unsigned-list/ cat ${TEST_SCRATCH_DIR}/unsigned-list/version cat ${TEST_SCRATCH_DIR}/unsigned-list/manifest.json } @test "build-with-dockerfile-signatures" { _gpg_setup builddir=${TEST_SCRATCH_DIR}/builddir mkdir -p $builddir cat > ${builddir}/Dockerfile <<- EOF FROM scratch ADD Dockerfile / EOF # We should be able to sign at build-time. run_buildah bud $WITH_POLICY_JSON --sign-by amanda@localhost -t signed-scratch-image ${builddir} mkdir -p ${TEST_SCRATCH_DIR}/signed-image # Pushing should preserve the signature. run_buildah push $WITH_POLICY_JSON signed-scratch-image dir:${TEST_SCRATCH_DIR}/signed-image ls -l ${TEST_SCRATCH_DIR}/signed-image/ test -s ${TEST_SCRATCH_DIR}/signed-image/signature-1 } ================================================ FILE: tests/source-policy.bats ================================================ #!/usr/bin/env bats load helpers @test "source-policy: DENY rule blocks base image" { # Create a policy that denies alpine policyfile=${TEST_SCRATCH_DIR}/policy.json cat > $policyfile << 'EOF' { "rules": [ { "action": "DENY", "selector": { "identifier": "docker-image://docker.io/library/alpine:latest" } } ] } EOF # Create a simple Dockerfile dockerfile=${TEST_SCRATCH_DIR}/Dockerfile cat > $dockerfile << 'EOF' FROM alpine:latest RUN echo hello EOF # Build should fail with source policy denial run_buildah 125 build $WITH_POLICY_JSON --source-policy-file $policyfile -f $dockerfile ${TEST_SCRATCH_DIR} expect_output --substring "denied by source policy" } @test "source-policy: DENY rule with WILDCARD match" { # Create a policy that denies all ubuntu images policyfile=${TEST_SCRATCH_DIR}/policy.json cat > $policyfile << 'EOF' { "rules": [ { "action": "DENY", "selector": { "identifier": "docker-image://docker.io/library/ubuntu:*", "matchType": "WILDCARD" } } ] } EOF # Create a simple Dockerfile using ubuntu dockerfile=${TEST_SCRATCH_DIR}/Dockerfile cat > $dockerfile << 'EOF' FROM ubuntu:22.04 RUN echo hello EOF # Build should fail with source policy denial run_buildah 125 build $WITH_POLICY_JSON --source-policy-file $policyfile -f $dockerfile ${TEST_SCRATCH_DIR} expect_output --substring "denied by source policy" } @test "source-policy: CONVERT rule rewrites base image to pinned digest" { _prefetch alpine # Get the digest of the alpine image run_buildah inspect --format '{{.FromImageDigest}}' alpine alpine_digest="$output" # Create a policy that converts alpine:latest to the pinned digest policyfile=${TEST_SCRATCH_DIR}/policy.json cat > $policyfile << EOF { "rules": [ { "action": "CONVERT", "selector": { "identifier": "docker-image://docker.io/library/alpine:latest" }, "updates": { "identifier": "docker-image://docker.io/library/alpine@${alpine_digest}" } } ] } EOF # Create a simple Dockerfile dockerfile=${TEST_SCRATCH_DIR}/Dockerfile cat > $dockerfile << 'EOF' FROM alpine:latest RUN echo converted EOF imgname="img-$(safename)" # Build should succeed with the converted reference run_buildah build $WITH_POLICY_JSON --source-policy-file $policyfile -t $imgname -f $dockerfile ${TEST_SCRATCH_DIR} # Verify the image was built run_buildah inspect $imgname } @test "source-policy: ALLOW rule explicitly allows source" { _prefetch alpine # Create a policy that explicitly allows alpine policyfile=${TEST_SCRATCH_DIR}/policy.json cat > $policyfile << 'EOF' { "rules": [ { "action": "ALLOW", "selector": { "identifier": "docker-image://docker.io/library/alpine:latest" } } ] } EOF # Create a simple Dockerfile dockerfile=${TEST_SCRATCH_DIR}/Dockerfile cat > $dockerfile << 'EOF' FROM alpine:latest RUN echo allowed EOF imgname="img-$(safename)" # Build should succeed run_buildah build $WITH_POLICY_JSON --source-policy-file $policyfile -t $imgname -f $dockerfile ${TEST_SCRATCH_DIR} # Verify the image was built run_buildah inspect $imgname } @test "source-policy: first matching rule wins" { _prefetch alpine # Create a policy where ALLOW comes before DENY for the same image policyfile=${TEST_SCRATCH_DIR}/policy.json cat > $policyfile << 'EOF' { "rules": [ { "action": "ALLOW", "selector": { "identifier": "docker-image://docker.io/library/alpine:latest" } }, { "action": "DENY", "selector": { "identifier": "docker-image://docker.io/library/alpine:latest" } } ] } EOF # Create a simple Dockerfile dockerfile=${TEST_SCRATCH_DIR}/Dockerfile cat > $dockerfile << 'EOF' FROM alpine:latest RUN echo first-match-wins EOF imgname="img-$(safename)" # Build should succeed because ALLOW matches first run_buildah build $WITH_POLICY_JSON --source-policy-file $policyfile -t $imgname -f $dockerfile ${TEST_SCRATCH_DIR} # Verify the image was built run_buildah inspect $imgname } @test "source-policy: invalid policy file fails build" { # Create an invalid policy file (bad JSON) policyfile=${TEST_SCRATCH_DIR}/policy.json cat > $policyfile << 'EOF' {invalid json} EOF # Create a simple Dockerfile dockerfile=${TEST_SCRATCH_DIR}/Dockerfile cat > $dockerfile << 'EOF' FROM alpine:latest EOF # Build should fail with parsing error run_buildah 125 build $WITH_POLICY_JSON --source-policy-file $policyfile -f $dockerfile ${TEST_SCRATCH_DIR} expect_output --substring "loading source policy" } @test "source-policy: missing policy file fails build" { # Create a simple Dockerfile dockerfile=${TEST_SCRATCH_DIR}/Dockerfile cat > $dockerfile << 'EOF' FROM alpine:latest EOF # Build should fail with file not found error run_buildah 125 build $WITH_POLICY_JSON --source-policy-file /nonexistent/policy.json -f $dockerfile ${TEST_SCRATCH_DIR} expect_output --substring "loading source policy" } @test "source-policy: scratch base image is not evaluated" { # Create a policy that would deny everything policyfile=${TEST_SCRATCH_DIR}/policy.json cat > $policyfile << 'EOF' { "rules": [ { "action": "DENY", "selector": { "identifier": "docker-image://*", "matchType": "WILDCARD" } } ] } EOF # Create a simple file to copy into the image echo "test content" > ${TEST_SCRATCH_DIR}/testfile.txt # Create a Dockerfile using scratch that doesn't reference external images dockerfile=${TEST_SCRATCH_DIR}/Dockerfile cat > $dockerfile << 'EOF' FROM scratch COPY testfile.txt /testfile.txt EOF imgname="img-$(safename)" # Build from scratch should succeed - scratch is not evaluated against policy run_buildah build $WITH_POLICY_JSON --source-policy-file $policyfile -t $imgname -f $dockerfile ${TEST_SCRATCH_DIR} # Verify the image was built run_buildah inspect $imgname } @test "source-policy: multi-stage build with stage reference not evaluated" { _prefetch alpine # Create a policy that denies everything except alpine policyfile=${TEST_SCRATCH_DIR}/policy.json cat > $policyfile << 'EOF' { "rules": [ { "action": "ALLOW", "selector": { "identifier": "docker-image://docker.io/library/alpine:latest" } }, { "action": "DENY", "selector": { "identifier": "docker-image://*", "matchType": "WILDCARD" } } ] } EOF # Create a multi-stage Dockerfile where stage references another stage by name dockerfile=${TEST_SCRATCH_DIR}/Dockerfile cat > $dockerfile << 'EOF' FROM alpine:latest AS builder RUN echo "building" FROM builder AS final RUN echo "final" EOF imgname="img-$(safename)" # Build should succeed - stage reference "builder" should not be evaluated against policy run_buildah build $WITH_POLICY_JSON --source-policy-file $policyfile -t $imgname -f $dockerfile ${TEST_SCRATCH_DIR} # Verify the image was built run_buildah inspect $imgname } @test "source-policy: no policy file means no restrictions" { _prefetch alpine # Create a simple Dockerfile dockerfile=${TEST_SCRATCH_DIR}/Dockerfile cat > $dockerfile << 'EOF' FROM alpine:latest RUN echo "no policy" EOF imgname="img-$(safename)" # Build without --source-policy-file should work normally run_buildah build $WITH_POLICY_JSON -t $imgname -f $dockerfile ${TEST_SCRATCH_DIR} # Verify the image was built run_buildah inspect $imgname } @test "source-policy: CONVERT with normalized image references" { _prefetch alpine # Get the digest of the alpine image run_buildah inspect --format '{{.FromImageDigest}}' alpine alpine_digest="$output" # Create a policy that converts short name "alpine" to full reference with digest policyfile=${TEST_SCRATCH_DIR}/policy.json cat > $policyfile << EOF { "rules": [ { "action": "CONVERT", "selector": { "identifier": "docker-image://docker.io/library/alpine" }, "updates": { "identifier": "docker-image://docker.io/library/alpine@${alpine_digest}" } } ] } EOF # Create a Dockerfile with short name (no tag) dockerfile=${TEST_SCRATCH_DIR}/Dockerfile cat > $dockerfile << 'EOF' FROM alpine RUN echo converted EOF imgname="img-$(safename)" # Build should succeed with the converted reference run_buildah build $WITH_POLICY_JSON --source-policy-file $policyfile -t $imgname -f $dockerfile ${TEST_SCRATCH_DIR} # Verify the image was built run_buildah inspect $imgname } @test "source-policy: policy validation - missing action" { # Create a policy missing required action field policyfile=${TEST_SCRATCH_DIR}/policy.json cat > $policyfile << 'EOF' { "rules": [ { "selector": { "identifier": "docker-image://test" } } ] } EOF # Create a simple Dockerfile dockerfile=${TEST_SCRATCH_DIR}/Dockerfile cat > $dockerfile << 'EOF' FROM alpine:latest EOF # Build should fail with validation error run_buildah 125 build $WITH_POLICY_JSON --source-policy-file $policyfile -f $dockerfile ${TEST_SCRATCH_DIR} expect_output --substring "action is required" } @test "source-policy: policy validation - missing selector identifier" { # Create a policy missing selector identifier policyfile=${TEST_SCRATCH_DIR}/policy.json cat > $policyfile << 'EOF' { "rules": [ { "action": "DENY", "selector": {} } ] } EOF # Create a simple Dockerfile dockerfile=${TEST_SCRATCH_DIR}/Dockerfile cat > $dockerfile << 'EOF' FROM alpine:latest EOF # Build should fail with validation error run_buildah 125 build $WITH_POLICY_JSON --source-policy-file $policyfile -f $dockerfile ${TEST_SCRATCH_DIR} expect_output --substring "selector.identifier is required" } @test "source-policy: policy validation - CONVERT without updates" { # Create a CONVERT policy without updates field policyfile=${TEST_SCRATCH_DIR}/policy.json cat > $policyfile << 'EOF' { "rules": [ { "action": "CONVERT", "selector": { "identifier": "docker-image://test" } } ] } EOF # Create a simple Dockerfile dockerfile=${TEST_SCRATCH_DIR}/Dockerfile cat > $dockerfile << 'EOF' FROM alpine:latest EOF # Build should fail with validation error run_buildah 125 build $WITH_POLICY_JSON --source-policy-file $policyfile -f $dockerfile ${TEST_SCRATCH_DIR} expect_output --substring "updates.identifier is required for CONVERT" } ================================================ FILE: tests/source.bats ================================================ #!/usr/bin/env bats load helpers @test "source create" { # Create an empty source image and make sure it's properly initialized srcdir=${TEST_SCRATCH_DIR}/newsource run_buildah source create --author="Buildah authors" $srcdir # Inspect the index.json run jq -r .manifests[0].mediaType $srcdir/index.json expect_output "application/vnd.oci.image.manifest.v1+json" run jq -r .mediaType $srcdir/index.json expect_output "application/vnd.oci.image.index.v1+json" # Digest of manifest run jq -r .manifests[0].digest $srcdir/index.json manifestDigest=${output//sha256:/} # strip off the sha256 prefix run stat $srcdir/blobs/sha256/$manifestDigest assert "$status" -eq 0 "status of stat(manifestDigest)" # Inspect the manifest run jq -r .schemaVersion $srcdir/blobs/sha256/$manifestDigest expect_output "2" run jq -r .layers $srcdir/blobs/sha256/$manifestDigest expect_output "null" run jq -r .config.mediaType $srcdir/blobs/sha256/$manifestDigest expect_output "application/vnd.oci.source.image.config.v1+json" run jq -r .mediaType $srcdir/blobs/sha256/$manifestDigest expect_output "application/vnd.oci.image.manifest.v1+json" run jq -r .config.size $srcdir/blobs/sha256/$manifestDigest # let's not check the size (afraid of time-stamp impacts) assert "$status" -eq 0 "status of jq .config.size" # Digest of config run jq -r .config.digest $srcdir/blobs/sha256/$manifestDigest configDigest=${output//sha256:/} # strip off the sha256 prefix run stat $srcdir/blobs/sha256/$configDigest assert "$status" -eq 0 "status of stat(configDigest)" # Inspect the config run jq -r .created $srcdir/blobs/sha256/$configDigest assert "$status" -eq 0 "status of jq .created on configDigest" run date --date="$output" assert "$status" -eq 0 "status of date (this should never ever fail)" run jq -r .author $srcdir/blobs/sha256/$configDigest expect_output "Buildah authors" # Directory mustn't exist run_buildah 125 source create $srcdir expect_output --substring "creating source image: " expect_output --substring " already exists" } @test "source add" { # Create an empty source image and make sure it's properly initialized. srcdir=${TEST_SCRATCH_DIR}/newsource run_buildah source create $srcdir # Digest of initial manifest run jq -r .manifests[0].digest $srcdir/index.json manifestDigestEmpty=${output//sha256:/} # strip off the sha256 prefix run stat $srcdir/blobs/sha256/$manifestDigestEmpty assert "$status" -eq 0 "status of stat(manifestDigestEmpty)" # Add layer 1 echo 111 > ${TEST_SCRATCH_DIR}/file1 run_buildah source add $srcdir ${TEST_SCRATCH_DIR}/file1 # Make sure the digest of the manifest changed run jq -r .manifests[0].digest $srcdir/index.json manifestDigestFile1=${output//sha256:/} # strip off the sha256 prefix assert "$manifestDigestEmpty" != "$manifestDigestFile1" \ "manifestDigestEmpty should differ from manifestDigestFile1" # Inspect layer 1 run jq -r .layers[0].mediaType $srcdir/blobs/sha256/$manifestDigestFile1 expect_output "application/vnd.oci.image.layer.v1.tar+gzip" run jq -r .layers[0].digest $srcdir/blobs/sha256/$manifestDigestFile1 layer1Digest=${output//sha256:/} # strip off the sha256 prefix # Now make sure the reported size matches the actual one run jq -r .layers[0].size $srcdir/blobs/sha256/$manifestDigestFile1 assert "$status" -eq 0 "status of jq .layers[0].size on manifestDigestFile1" layer1Size=$output run du -b $srcdir/blobs/sha256/$layer1Digest expect_output --substring "$layer1Size" # Add layer 2 echo 222222aBitLongerForAdifferentSize > ${TEST_SCRATCH_DIR}/file2 run_buildah source add $srcdir ${TEST_SCRATCH_DIR}/file2 # Make sure the digest of the manifest changed run jq -r .manifests[0].digest $srcdir/index.json manifestDigestFile2=${output//sha256:/} # strip off the sha256 prefix assert "$manifestDigestEmpty" != "$manifestDigestFile2" \ "manifestDigestEmpty should differ from manifestDigestFile2" assert "$manifestDigestFile1" != "$manifestDigestFile2" \ "manifestDigestFile1 should differ from manifestDigestFile2" # Make sure layer 1 is still in the manifest and remains unchanged run jq -r .layers[0].digest $srcdir/blobs/sha256/$manifestDigestFile2 expect_output "sha256:$layer1Digest" run jq -r .layers[0].size $srcdir/blobs/sha256/$manifestDigestFile2 expect_output "$layer1Size" # Inspect layer 2 run jq -r .layers[1].mediaType $srcdir/blobs/sha256/$manifestDigestFile2 expect_output "application/vnd.oci.image.layer.v1.tar+gzip" run jq -r .layers[1].digest $srcdir/blobs/sha256/$manifestDigestFile2 layer2Digest=${output//sha256:/} # strip off the sha256 prefix # Now make sure the reported size matches the actual one run jq -r .layers[1].size $srcdir/blobs/sha256/$manifestDigestFile2 assert "$status" -eq 0 "status of jq .layers[1].size on manifestDigestFile2" layer2Size=$output run du -b $srcdir/blobs/sha256/$layer2Digest expect_output --substring "$layer2Size" # Last but not least, make sure the two layers differ assert "$layer1Digest" != "$layer2Digest" "layer1Digest vs layer2Digest" assert "$layer1Size" != "$layer2Size" "layer1Size vs layer2Size" } @test "source push/pull" { # Create an empty source image and make sure it's properly initialized. srcdir=${TEST_SCRATCH_DIR}/newsource run_buildah source create $srcdir # Add two layers echo 111 > ${TEST_SCRATCH_DIR}/file1 run_buildah source add $srcdir ${TEST_SCRATCH_DIR}/file1 echo 222... > ${TEST_SCRATCH_DIR}/file2 run_buildah source add $srcdir ${TEST_SCRATCH_DIR}/file2 start_registry # --quiet=true run_buildah source push --quiet --tls-verify=false --creds testuser:testpassword $srcdir localhost:${REGISTRY_PORT}/source:test expect_output "" # --quiet=false (implicit) run_buildah source push --digestfile=${TEST_SCRATCH_DIR}/digest.txt --tls-verify=false --creds testuser:testpassword $srcdir localhost:${REGISTRY_PORT}/source:test expect_output --substring "Copying blob" expect_output --substring "Copying config" cat ${TEST_SCRATCH_DIR}/digest.txt test -s ${TEST_SCRATCH_DIR}/digest.txt pulldir=${TEST_SCRATCH_DIR}/pulledsource # --quiet=true run_buildah source pull --quiet --tls-verify=false --creds testuser:testpassword localhost:${REGISTRY_PORT}/source:test $pulldir expect_output "" # --quiet=false (implicit) rm -rf $pulldir run_buildah source pull --tls-verify=false --creds testuser:testpassword localhost:${REGISTRY_PORT}/source:test $pulldir expect_output --substring "Copying blob" expect_output --substring "Copying config" run diff -r $srcdir $pulldir # FIXME: if there's a nonzero chance of this failing, include actual diffs assert "$status" -eq 0 "status from diff of srcdir vs pulldir" } ================================================ FILE: tests/squash.bats ================================================ #!/usr/bin/env bats load helpers function check_lengths() { local image=$1 local expect=$2 # matrix test: check given .Docker.* and .OCIv1.* fields in image for which in Docker OCIv1; do for field in RootFS.DiffIDs History; do run_buildah inspect -t image -f "{{len .$which.$field}}" $image expect_output "$expect" done done } @test "squash" { createrandom ${TEST_SCRATCH_DIR}/randomfile run_buildah from scratch cid=$output image=stage0 remove=(8 5) for stage in $(seq 10) ; do run_buildah copy "$cid" ${TEST_SCRATCH_DIR}/randomfile /layer${stage} image=stage${stage} if test $stage -eq ${remove[0]} ; then run_buildah mount "$cid" mountpoint=$output rm -f ${mountpoint}/layer${remove[1]} fi run_buildah commit $WITH_POLICY_JSON --rm "$cid" ${image} check_lengths $image $stage run_buildah from --quiet ${image} cid=$output done run_buildah commit $WITH_POLICY_JSON --rm --squash "$cid" squashed check_lengths squashed 1 run_buildah from --quiet squashed cid=$output run_buildah mount $cid mountpoint=$output for stage in $(seq 10) ; do if test $stage -eq ${remove[1]} ; then if test -e $mountpoint/layer${remove[1]} ; then echo file /layer${remove[1]} should not be there exit 1 fi continue fi cmp $mountpoint/layer${stage} ${TEST_SCRATCH_DIR}/randomfile done } @test "squash-using-dockerfile" { createrandom ${TEST_SCRATCH_DIR}/randomfile image=stage0 from=scratch for stage in $(seq 10) ; do mkdir -p ${TEST_SCRATCH_DIR}/stage${stage} echo FROM ${from} > ${TEST_SCRATCH_DIR}/stage${stage}/Dockerfile cp ${TEST_SCRATCH_DIR}/randomfile ${TEST_SCRATCH_DIR}/stage${stage}/ echo COPY randomfile /layer${stage} >> ${TEST_SCRATCH_DIR}/stage${stage}/Dockerfile image=stage${stage} from=${image} run_buildah build-using-dockerfile $WITH_POLICY_JSON -t ${image} ${TEST_SCRATCH_DIR}/stage${stage} check_lengths $image $stage done mkdir -p ${TEST_SCRATCH_DIR}/squashed echo FROM ${from} > ${TEST_SCRATCH_DIR}/squashed/Dockerfile cp ${TEST_SCRATCH_DIR}/randomfile ${TEST_SCRATCH_DIR}/squashed/ echo COPY randomfile /layer-squashed >> ${TEST_SCRATCH_DIR}/stage${stage}/Dockerfile run_buildah build-using-dockerfile $WITH_POLICY_JSON --squash -t squashed ${TEST_SCRATCH_DIR}/squashed check_lengths squashed 1 run_buildah from --quiet squashed cid=$output run_buildah mount $cid mountpoint=$output for stage in $(seq 10) ; do cmp $mountpoint/layer${stage} ${TEST_SCRATCH_DIR}/randomfile done run_buildah build-using-dockerfile $WITH_POLICY_JSON --squash --layers -t squashed ${TEST_SCRATCH_DIR}/squashed run_buildah inspect -t image -f '{{len .Docker.RootFS.DiffIDs}}' squashed expect_output "1" "len(DiffIDs) - simple image" echo FROM ${from} > ${TEST_SCRATCH_DIR}/squashed/Dockerfile run_buildah build-using-dockerfile $WITH_POLICY_JSON --squash -t squashed ${TEST_SCRATCH_DIR}/squashed run_buildah inspect -t image -f '{{len .Docker.RootFS.DiffIDs}}' squashed expect_output "1" "len(DiffIDs) - image with FROM" echo USER root >> ${TEST_SCRATCH_DIR}/squashed/Dockerfile run_buildah build-using-dockerfile $WITH_POLICY_JSON --squash -t squashed ${TEST_SCRATCH_DIR}/squashed run_buildah inspect -t image -f '{{len .Docker.RootFS.DiffIDs}}' squashed expect_output "1" "len(DiffIDs) - image with FROM and USER" echo COPY file / >> ${TEST_SCRATCH_DIR}/squashed/Dockerfile echo COPY file / > ${TEST_SCRATCH_DIR}/squashed/file run_buildah build-using-dockerfile $WITH_POLICY_JSON --squash -t squashed ${TEST_SCRATCH_DIR}/squashed run_buildah inspect -t image -f '{{len .Docker.RootFS.DiffIDs}}' squashed expect_output "1" "len(DiffIDs) - image with FROM, USER, and 2xCOPY" echo FROM ${from} > ${TEST_SCRATCH_DIR}/squashed/Dockerfile run_buildah build-using-dockerfile $WITH_POLICY_JSON --squash --layers -t squashed ${TEST_SCRATCH_DIR}/squashed run_buildah inspect -t image -f '{{len .Docker.RootFS.DiffIDs}}' squashed expect_output "1" "len(DiffIDs) - image with FROM (--layers)" echo USER root >> ${TEST_SCRATCH_DIR}/squashed/Dockerfile run_buildah build-using-dockerfile $WITH_POLICY_JSON --squash -t squashed ${TEST_SCRATCH_DIR}/squashed run_buildah inspect -t image -f '{{len .Docker.RootFS.DiffIDs}}' squashed expect_output "1" "len(DiffIDs) - image with FROM and USER (--layers)" echo COPY file / >> ${TEST_SCRATCH_DIR}/squashed/Dockerfile echo COPY file / > ${TEST_SCRATCH_DIR}/squashed/file run_buildah build-using-dockerfile $WITH_POLICY_JSON --squash -t squashed ${TEST_SCRATCH_DIR}/squashed run_buildah inspect -t image -f '{{len .Docker.RootFS.DiffIDs}}' squashed expect_output "1" "len(DiffIDs) - image with FROM, USER, and 2xCOPY (--layers)" run_buildah build-using-dockerfile $WITH_POLICY_JSON --squash --format docker -t squashed ${TEST_SCRATCH_DIR}/squashed run_buildah inspect -t image -f '{{.Docker.Parent}}' squashed expect_output "" "should have no parent image set" } @test "bud-squash-should-use-cache" { _prefetch alpine # populate cache from simple build run_buildah build --layers -t test $WITH_POLICY_JSON -f $BUDFILES/layers-squash/Dockerfile.multi-stage # create another squashed build and check if we are using cache for everything. # instead of last instruction in last stage run_buildah build --layers --squash -t testsquash $WITH_POLICY_JSON -f $BUDFILES/layers-squash/Dockerfile.multi-stage expect_output --substring "Using cache" run_buildah inspect -t image -f '{{len .Docker.RootFS.DiffIDs}}' testsquash expect_output "1" "image built with --squash should only include 1 layer" run_buildah rmi -f testsquash run_buildah rmi -f test } # Test build with --squash and --layers and verify number of layers and content inside image @test "bud-squash-should-use-cache and verify content inside image" { mkdir -p ${TEST_SCRATCH_DIR}/bud/platform cat > ${TEST_SCRATCH_DIR}/bud/platform/Dockerfile << _EOF FROM busybox RUN touch hello ADD . /data RUN echo hey && mkdir water _EOF # Build a first image with --layers and --squash and populate build cache run_buildah build $WITH_POLICY_JSON --squash --layers -t one -f ${TEST_SCRATCH_DIR}/bud/platform/Dockerfile ${TEST_SCRATCH_DIR}/bud/platform run_buildah inspect -t image -f '{{len .Docker.RootFS.DiffIDs}}' one expect_output "1" "image built with --squash should only include 1 layer" # Build again and verify if cache is being used run_buildah build $WITH_POLICY_JSON --squash --layers -t two -f ${TEST_SCRATCH_DIR}/bud/platform/Dockerfile ${TEST_SCRATCH_DIR}/bud/platform expect_output --substring "Using cache" run_buildah inspect -t image -f '{{len .Docker.RootFS.DiffIDs}}' two expect_output "1" "image built with --squash should only include 1 layer" run_buildah from two run_buildah run two-working-container ls expect_output --substring "water" expect_output --substring "data" expect_output --substring "hello" } ================================================ FILE: tests/ssh.bats ================================================ #!/usr/bin/env bats load helpers function setup() { setup_tests unset SSH_AUTH_SOCK } function teardown(){ if [[ -n "$SSH_AUTH_SOCK" ]]; then ssh-agent -k;fi teardown_tests } @test "bud with ssh key" { # quay.io/hummingbird/git is only available on these arches: skip_unless_arch amd64 arm64 _prefetch quay.io/hummingbird/git mytmpdir=${TEST_SCRATCH_DIR}/my-dir1 mkdir -p ${mytmpdir} ssh-keygen -b 2048 -t rsa -f $mytmpdir/sshkey -q -N "" fingerprint=$(ssh-keygen -l -f $mytmpdir/sshkey -E md5 | awk '{ print $2; }') run_buildah bud --ssh default=$mytmpdir/sshkey $WITH_POLICY_JSON -t sshimg -f $BUDFILES/run-mounts/Dockerfile.ssh $BUDFILES/run-mounts expect_output --substring $fingerprint run_buildah from sshimg run_buildah 1 run sshimg-working-container cat /run/buildkit/ssh_agent.0 expect_output --substring "cat: /run/buildkit/ssh_agent.0: No such file or directory" run_buildah rm -a } @test "bud with ssh key secret accessed on second RUN" { # quay.io/hummingbird/git is only available on these arches: skip_unless_arch amd64 arm64 _prefetch quay.io/hummingbird/git mytmpdir=${TEST_SCRATCH_DIR}/my-dir1 mkdir -p ${mytmpdir} ssh-keygen -b 2048 -t rsa -f $mytmpdir/sshkey -q -N "" fingerprint=$(ssh-keygen -l -f $mytmpdir/sshkey -E md5 | awk '{ print $2; }') run_buildah 2 bud --ssh default=$mytmpdir/sshkey $WITH_POLICY_JSON -t sshimg -f $BUDFILES/run-mounts/Dockerfile.ssh_access $BUDFILES/run-mounts expect_output --substring "Could not open a connection to your authentication agent." } @test "bud with containerfile ssh options" { # quay.io/hummingbird/git is only available on these arches: skip_unless_arch amd64 arm64 _prefetch quay.io/hummingbird/git mytmpdir=${TEST_SCRATCH_DIR}/my-dir1 mkdir -p ${mytmpdir} ssh-keygen -b 2048 -t rsa -f $mytmpdir/sshkey -q -N "" fingerprint=$(ssh-keygen -l -f $mytmpdir/sshkey -E md5 | awk '{ print $2; }') run_buildah bud --ssh default=$mytmpdir/sshkey $WITH_POLICY_JSON -t secretopts -f $BUDFILES/run-mounts/Dockerfile.ssh_options $BUDFILES/run-mounts expect_output --substring "444" expect_output --substring "1000" expect_output --substring "1001" } @test "bud with ssh sock" { # quay.io/hummingbird/git is only available on these arches: skip_unless_arch amd64 arm64 _prefetch quay.io/hummingbird/git mytmpdir=${TEST_SCRATCH_DIR}/my-dir1 mkdir -p ${mytmpdir} ssh-keygen -b 2048 -t rsa -f $mytmpdir/sshkey -q -N "" fingerprint=$(ssh-keygen -l -f $mytmpdir/sshkey -E md5 | awk '{ print $2; }') eval "$(ssh-agent -s)" ssh-add $mytmpdir/sshkey run_buildah bud --ssh default=$mytmpdir/sshkey $WITH_POLICY_JSON -t sshimg -f $BUDFILES/run-mounts/Dockerfile.ssh $BUDFILES/run-mounts expect_output --substring $fingerprint run_buildah from sshimg run_buildah 1 run sshimg-working-container cat /run/buildkit/ssh_agent.0 expect_output --substring "cat: /run/buildkit/ssh_agent.0: No such file or directory" run_buildah rm -a } ================================================ FILE: tests/subscriptions.bats ================================================ #!/usr/bin/env bats load helpers @test "bind secrets mounts to container" { skip_if_no_runtime # Setup SECRETS_DIR=$TEST_SCRATCH_DIR/rhel/secrets mkdir -p $SECRETS_DIR TESTFILE1=$SECRETS_DIR/test.txt TESTFILE_CONTENT="Testing secrets mounts. I am mounted!" echo $TESTFILE_CONTENT > $TESTFILE1 TESTFILE2=$SECRETS_DIR/file.txt touch $TESTFILE2 chmod 604 $TESTFILE2 TEST_SCRATCH_DIR1=$SECRETS_DIR/test-dir mkdir -m704 $TEST_SCRATCH_DIR1 TESTFILE3=$TEST_SCRATCH_DIR1/file.txt touch $TESTFILE3 chmod 777 $TESTFILE3 mkdir -p $TEST_SCRATCH_DIR/symlink/target touch $TEST_SCRATCH_DIR/symlink/target/key.pem ln -s $TEST_SCRATCH_DIR/symlink/target $SECRETS_DIR/mysymlink # prepare the test mounts file mkdir $TEST_SCRATCH_DIR/containers MOUNTS_PATH=$TEST_SCRATCH_DIR/containers/mounts.conf # add the mounts entries echo "$SECRETS_DIR:/run/secrets" > $MOUNTS_PATH echo "$SECRETS_DIR" >> $MOUNTS_PATH echo "$TESTFILE1:/test.txt" >> $MOUNTS_PATH # setup the test container _prefetch alpine run_buildah --default-mounts-file "$MOUNTS_PATH" \ from --quiet --pull $WITH_POLICY_JSON alpine cid=$output # test a standard mount to /run/secrets run_buildah run $cid ls /run/secrets expect_output --substring "test.txt" # test a mount without destination run_buildah run $cid ls "$TEST_SCRATCH_DIR"/rhel/secrets expect_output --substring "test.txt" # test a file-based mount run_buildah run $cid cat /test.txt expect_output "$TESTFILE_CONTENT" # test permissions for a file-based mount run_buildah run $cid stat -c %a /run/secrets/file.txt expect_output 604 # test permissions for a directory-based mount run_buildah run $cid stat -c %a /run/secrets/test-dir expect_output 704 # test permissions for a file-based mount within a sub-directory run_buildah run $cid stat -c %a /run/secrets/test-dir/file.txt expect_output 777 cat > $TEST_SCRATCH_DIR/Containerfile << _EOF from alpine run stat -c %a /run/secrets/file.txt run stat -c %a /run/secrets/test-dir run stat -c %a /run/secrets/test-dir/file.txt _EOF run_buildah --default-mounts-file "$MOUNTS_PATH" bud $TEST_SCRATCH_DIR expect_output --substring "604" expect_output --substring "704" expect_output --substring "777" # test a symlink run_buildah run $cid ls /run/secrets/mysymlink expect_output --substring "key.pem" } ================================================ FILE: tests/tag.bats ================================================ #!/usr/bin/env bats load helpers @test "tag by name" { run_buildah from --pull=false $WITH_POLICY_JSON scratch cid=$output run_buildah commit $WITH_POLICY_JSON "$cid" scratch-image run_buildah 125 inspect --type image tagged-image run_buildah tag scratch-image tagged-image tagged-also-image named-image run_buildah inspect --type image tagged-image run_buildah inspect --type image tagged-also-image run_buildah inspect --type image named-image } @test "tag by id" { _prefetch busybox run_buildah pull --quiet $WITH_POLICY_JSON busybox id=$output # Tag by ID, then make a container from that tag run_buildah tag $id busybox1 run_buildah from busybox1 # gives us busybox1-working-container # The from-name should be busybox1, but ID should be same as pulled image run_buildah inspect --format '{{ .FromImage }}' busybox1-working-container expect_output "localhost/busybox1:latest" run_buildah inspect --format '{{ .FromImageID }}' busybox1-working-container expect_output $id } # Tagging a manifest list should tag manifest list instead of resolved image @test "tag a manifest list" { run_buildah manifest create foobar run_buildah manifest add foobar busybox run_buildah tag foobar foobar2 run_buildah manifest inspect foobar foobar_inspect=$output run_buildah manifest inspect foobar2 # Output of tagged manifest list should be same expect_output "$foobar_inspect" } ================================================ FILE: tests/test_buildah_authentication.sh ================================================ #!/usr/bin/env bash # test_buildah_authentication # A script to be run at the command line with Buildah installed. # This currently needs to be run as root and Docker must be # installed on the system. # This will test the code and should be run with this command: # # /bin/bash -v test_buildah_authentication.sh ######## # System setup - Create dir for creds and start Docker ######## mkdir -p /root/auth systemctl restart docker ######## # Create creds and store in /root/auth/htpasswd ######## registry=$(buildah from registry:2) buildah run $registry -- htpasswd -Bbn testuser testpassword > /root/auth/htpasswd ######## # Create certificate via openssl ######## openssl req -newkey rsa:4096 -nodes -sha256 -keyout /root/auth/domain.key -x509 -days 2 -out /root/auth/domain.crt -subj "/C=US/ST=Foo/L=Bar/O=Red Hat, Inc./CN=localhost" ######## # Skopeo and buildah both require *.cert file ######## cp /root/auth/domain.crt /root/auth/domain.cert ######## # Create a private registry that uses certificate and creds file ######## docker run -d -p 5000:5000 --name registry -v /root/auth:/root/auth:Z -e "REGISTRY_AUTH=htpasswd" -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" -e REGISTRY_AUTH_HTPASSWD_PATH=/root/auth/htpasswd -e REGISTRY_HTTP_TLS_CERTIFICATE=/root/auth/domain.crt -e REGISTRY_HTTP_TLS_KEY=/root/auth/domain.key registry:2 ######## # Pull alpine ######## buildah from alpine buildah containers buildah images ######## # Log into docker on local repo ######## docker login localhost:5000 --username testuser --password testpassword ######## # Push to the local repo using cached Docker creds. ######## buildah push --cert-dir /root/auth alpine docker://localhost:5000/my-alpine ######## # Show stuff ######## docker ps --all docker images buildah containers buildah images ######## # Buildah from (pull) using certs and cached Docker creds. # Should show two alpine images and containers when done. ######## ctrid=$(buildah from --cert-dir /root/auth localhost:5000/my-alpine) buildah containers buildah images ######## # Clean up Buildah ######## buildah rm $ctrid buildah rmi -f localhost:5000/my-alpine:latest ######## # Show stuff ######## docker ps --all docker images buildah containers buildah images ######## # Log out of local repo ######## docker logout localhost:5000 ######## # Push using only certs, this should FAIL. ######## buildah push --cert-dir /root/auth --tls-verify=true alpine docker://localhost:5000/my-alpine ######## # Push using creds, certs and no transport (docker://), this should work. ######## buildah push --cert-dir ~/auth --tls-verify=true --creds=testuser:testpassword alpine localhost:5000/my-alpine ######## # Push using a bad password , this should FAIL. ######## buildah push --cert-dir ~/auth --tls-verify=true --creds=testuser:badpassword alpine localhost:5000/my-alpine ######## # No creds anywhere, only the certificate, this should FAIL. ######## buildah from --cert-dir /root/auth --tls-verify=true localhost:5000/my-alpine ######## # From with creds and certs, this should work ######## ctrid=$(buildah from --cert-dir /root/auth --tls-verify=true --creds=testuser:testpassword localhost:5000/my-alpine) ######## # Show stuff ######## docker ps --all docker images buildah containers buildah images ######## # Clean up Buildah ######## buildah rm $ctrid buildah rmi -f $(buildah images -q) ######## # Pull alpine ######## buildah from alpine ######## # Show stuff ######## docker ps --all docker images buildah containers buildah images ######## # Let's test commit ######## ######## # No credentials, this should FAIL. ######## buildah commit --cert-dir /root/auth --tls-verify=true alpine-working-container docker://localhost:5000/my-commit-alpine ######## # This should work, writing image in registry. Will not create an image locally. ######## buildah commit --cert-dir /root/auth --tls-verify=true --creds=testuser:testpassword alpine-working-container docker://localhost:5000/my-commit-alpine ######## # Use bad password on from/pull, this should FAIL ######## buildah from --pull-always --cert-dir /root/auth --tls-verify=true --creds=testuser:badpassword localhost:5000/my-commit-alpine ######## # Pull the new image that we just committed ######## buildah from --pull-always --cert-dir /root/auth --tls-verify=true --creds=testuser:testpassword localhost:5000/my-commit-alpine ######## # Show stuff ######## docker ps --all docker images buildah containers buildah images ######## # Create Dockerfile ######## FILE=./Dockerfile /bin/cat <$FILE FROM localhost:5000/my-commit-alpine EOM chmod +x $FILE ######## # Clean up Buildah ######## buildah rm --all buildah rmi -f $(buildah images -q) ######## # Try Buildah bud with creds but no auth, this should FAIL ######## buildah bud -f ./Dockerfile --tls-verify=true --creds=testuser:testpassword ######## # Try Buildah bud with creds and auth, this should work ######## buildah bud -f ./Dockerfile --cert-dir /root/auth --tls-verify=true --creds=testuser:testpassword ######## # Show stuff ######## docker ps --all docker images buildah containers buildah images ######## # Clean up ######## read -p "Press enter to continue and clean up all" rm -f ./Dockerfile rm -rf ${TESTDIR}/auth docker rm -f $(docker ps --all -q) docker rmi -f $(docker images -q) buildah rm $(buildah containers -q) buildah rmi -f $(buildah images -q) ================================================ FILE: tests/test_buildah_baseline.sh ================================================ #!/usr/bin/env bash # test_buildah_baseline.sh # A script to be run at the command line with Buildah installed. # This should be run against a new kit to provide base level testing # on a freshly installed machine with no images or containers in # play. This currently needs to be run as root. # # Commands based on the tutorial provided by William Henry. # # To run this command: # # /bin/bash -v test_buildah_baseline.sh ######## # Next two commands should return blanks ######## buildah images buildah containers ######## # Run ls in redis container, this should work ######## ctrid=$(buildah from registry.redhat.io/rhscl/redis-6-rhel7) buildah run $ctrid ls / ######## # Validate touch works after installing httpd, solved selinux # issue that should now work. ######## ctr=$(buildah from scratch) mnt=$(buildah mount $ctr) dnf -y install --installroot=$mnt --releasever=42 --use-host-config --setopt "*.countme=false" httpd buildah run $ctr touch /test ######## # Create Fedora based container ######## container=$(buildah from fedora) echo $container ######## # Run container and display contents in /etc ######## buildah run $container -- ls -alF /etc ######## # Run Java in the container - should FAIL ######## buildah run $container java ######## # Install java onto the container ######## buildah run $container -- dnf -y install java ######## # Run Java in the container - should show java usage ######## buildah run $container java ######## # Create a scratch container ######## newcontainer=$(buildah from scratch) ######## # Check and find two containers ######## buildah containers ######## # Check images, no "scratch" image ######## buildah images ######## # Run the container - should FAIL ######## buildah run $newcontainer bash ######## # Mount the container's root file system ######## scratchmnt=$(buildah mount $newcontainer) ######## # Show the location, should be /var/lib/containers/storage/overlay/{id}/dif ######## echo $scratchmnt ######## # Install Fedora 30 bash and coreutils ######## dnf install --installroot $scratchmnt --releasever 42 bash coreutils --use-host-config --setopt "*.countme=false" --setopt install_weak_deps=false -y ######## # Check /usr/bin on the new container ######## buildah run $newcontainer -- ls -alF /usr/bin ######## # Create shell script to test on ######## FILE=./runecho.sh /bin/cat <$FILE #!/usr/bin/env bash for i in {1..9}; do echo "This is a new container from ipbabble [" \$i "]" done EOM chmod +x $FILE ######## # Copy and run file on scratch container ######## buildah copy $newcontainer $FILE /usr/bin buildah config --cmd /usr/bin/runecho.sh $newcontainer buildah run $newcontainer /usr/bin/runecho.sh ######## # Add configuration information ######## buildah config --created-by "ipbabble" $newcontainer buildah config --author "wgh at redhat.com @ipbabble" --label name=fedora42-bashecho $newcontainer ######## # Inspect the container, verifying above was put into it ######## buildah inspect $newcontainer ######## # Unmount the container ######## buildah unmount $newcontainer ######## # Commit the image ######## buildah commit $newcontainer fedora-bashecho ######## # Check the images there should be a fedora-bashecho:latest image ######## buildah images ######## # Inspect the fedora-bashecho image ######## buildah inspect --type=image fedora-bashecho ######## # Remove the container ######## buildah rm $newcontainer ######## # Install Docker, but not for long! ######## dnf -y install docker systemctl start docker ######## # Push fedora-bashecho to the Docker daemon ######## buildah push fedora-bashecho docker-daemon:fedora-bashecho:latest ######## # Run fedora-bashecho from Docker ######## docker run fedora-bashecho ######## # Time to remove Docker ######## dnf -y remove docker ######## # Build Dockerfiles for OnBuild Test # (Thanks @clcollins!) ######## FILE=./Dockerfile /bin/cat <$FILE FROM alpine RUN touch /foo ONBUILD RUN touch /bar EOM chmod +x $FILE FILE=./Dockerfile-2 /bin/cat <$FILE FROM onbuild-image RUN touch /baz EOM chmod +x $FILE ######## # Build with Dockerfiles ######## buildah bud -f ./Dockerfile --format=docker -t onbuild-image . buildah bud -f ./Dockerfile-2 --format=docker -t result-image . ######## # Build a container to see if the /bar file has been created. ######## ctr=$(buildah from result-image) ######## # Validate that the /bar file has been created in the container. ######## buildah run $ctr ls -alF /bar /foo /baz ######## # Build Dockerfile for WhaleSays ######## FILE=./Dockerfile /bin/cat <$FILE FROM docker.io/docker/whalesay:latest RUN apt-get -y update && apt-get install -y fortunes CMD /usr/games/fortune -a | cowsay EOM chmod +x $FILE ######## # Build with the Dockerfile ######## buildah bud -f Dockerfile -t whale-says . ######## # Create a whalesays container ######## whalesays=$(buildah from whale-says) ######## # Run the container to see what the whale says ######## buildah run $whalesays bash -c '/usr/games/fortune -a | cowsay' ######## # Clean up Buildah ######## buildah rm --all buildah rmi --all ================================================ FILE: tests/test_buildah_build_rpm.sh ================================================ #!/usr/bin/env bash # # test_buildah_build_rpm.sh # # Meant to run on a freshly installed VM. # Installs the latest Git and Buildah and then # Builds and installs Buildah's RPM in a Buildah Container. # The baseline test is then run on this vm and then the # newly created BUILDAH rpm is installed and the baseline # test is rerun. # ######## # Setup ######## IMAGE=registry.fedoraproject.org/fedora SBOX=/tmp/sandbox PACKAGES=/tmp/packages mkdir -p ${SBOX}/buildah GITROOT=${SBOX}/buildah TEST_SOURCES=${GITROOT}/tests # Change packager as appropriate for the platform PACKAGER=dnf ${PACKAGER} install -y git ${PACKAGER} install -y buildah ######## # Clone buildah from GitHub.com ######## cd $SBOX git clone https://github.com/containers/buildah.git cd $GITROOT ######## # Build a container to use for building the binaries. ######## CTRID=$(buildah from --pull --signature-policy ${TEST_SOURCES}/policy.json $IMAGE) ROOTMNT=$(buildah mount $CTRID) COMMIT=$(git log --format=%H -n 1) SHORTCOMMIT=$(echo ${COMMIT} | cut -c-7) mkdir -p ${ROOTMNT}/rpmbuild/{SOURCES,SPECS} ######## # Build the tarball. ######## (git archive --format tar.gz --prefix=buildah-${COMMIT}/ ${COMMIT}) > ${ROOTMNT}/rpmbuild/SOURCES/buildah-${SHORTCOMMIT}.tar.gz ######## # Update the .spec file with the commit ID. ######## sed s:REPLACEWITHCOMMITID:${COMMIT}:g ${GITROOT}/contrib/rpm/buildah.spec > ${ROOTMNT}/rpmbuild/SPECS/buildah.spec ######## # Install build dependencies and build binary packages. ######## buildah run $CTRID -- dnf -y install 'dnf-command(builddep)' rpm-build buildah run $CTRID -- dnf -y builddep --spec /rpmbuild/SPECS/buildah.spec buildah run $CTRID -- rpmbuild --define "_topdir /rpmbuild" -ba /rpmbuild/SPECS/buildah.spec ######## # Build a second new container. ######## CTRID2=$(buildah from --pull --signature-policy ${TEST_SOURCES}/policy.json $IMAGE) ROOTMNT2=$(buildah mount $CTRID2) ######## # Copy the binary packages from the first container to the second one and to # /tmp. Also build a list of their filenames. ######## rpms= mkdir -p ${ROOTMNT2}/${PACKAGES} mkdir -p ${PACKAGES} for rpm in ${ROOTMNT}/rpmbuild/RPMS/*/*.rpm ; do cp $rpm ${ROOTMNT2}/${PACKAGES} cp $rpm ${PACKAGES} rpms="$rpms "${PACKAGES}/$(basename $rpm) done ######## # Install the binary packages into the second container. ######## buildah run $CTRID2 -- dnf -y install $rpms ######## # Run the binary package and compare its self-identified version to the one we tried to build. ######## id=$(buildah run $CTRID2 -- buildah version | awk '/^Git Commit:/ { print $NF }') bv=$(buildah run $CTRID2 -- buildah version | awk '/^Version:/ { print $NF }') rv=$(buildah run $CTRID2 -- rpm -q --queryformat '%{version}' buildah) echo "short commit: $SHORTCOMMIT" echo "id: $id" echo "buildah version: $bv" echo "buildah rpm version: $rv" test $SHORTCOMMIT = $id test $bv = $rv ######## # Clean up Buildah ######## buildah rm $(buildah containers -q) buildah rmi -f $(buildah images -q) ######## # Kick off baseline testing against the installed Buildah ######## /bin/bash -v ${TEST_SOURCES}/test_buildah_baseline.sh ######## # Install the Buildah we just built locally and run # the baseline tests again. ######## ${PACKAGER} -y install ${PACKAGES}/*.rpm /bin/bash -v ${TEST_SOURCES}/test_buildah_baseline.sh ######## # Clean up ######## rm -rf ${SBOX} rm -rf ${PACKAGES} buildah rm $(buildah containers -q) buildah rmi -f $(buildah images -q) ${PACKAGER} remove -y buildah ================================================ FILE: tests/test_buildah_rpm.sh ================================================ #!/usr/bin/env bats load helpers # Ensure that any updated/pushed rpm .spec files don't clobber the commit placeholder @test "rpm REPLACEWITHCOMMITID placeholder exists in .spec file" { run grep -q "^%global[ ]\+commit[ ]\+REPLACEWITHCOMMITID$" ${TEST_SOURCES}/../contrib/rpm/buildah.spec [ "$status" -eq 0 ] } @test "rpm-build CentOS 7" { skip_if_no_runtime # Build a container to use for building the binaries. image=quay.io/libpod/centos:7 cid=$(buildah from --pull --signature-policy ${TEST_SOURCES}/policy.json $image) root=$(buildah mount $cid) commit=$(git log --format=%H -n 1) shortcommit=$(echo ${commit} | cut -c-7) mkdir -p ${root}/rpmbuild/{SOURCES,SPECS} # Build the tarball. (cd ..; git archive --format tar.gz --prefix=buildah-${commit}/ ${commit}) > ${root}/rpmbuild/SOURCES/buildah-${shortcommit}.tar.gz # Update the .spec file with the commit ID. sed s:REPLACEWITHCOMMITID:${commit}:g ${TEST_SOURCES}/../contrib/rpm/buildah.spec > ${root}/rpmbuild/SPECS/buildah.spec # Install build dependencies and build binary packages. buildah run $cid -- yum -y install rpm-build yum-utils buildah run $cid -- yum-builddep -y rpmbuild/SPECS/buildah.spec buildah run $cid -- rpmbuild --define "_topdir /rpmbuild" -ba /rpmbuild/SPECS/buildah.spec # Build a second new container. cid2=$(buildah from --pull --signature-policy ${TEST_SOURCES}/policy.json $image) root2=$(buildah mount $cid2) # Copy the binary packages from the first container to the second one, and build a list of # their filenames relative to the root of the second container. rpms= mkdir -p ${root2}/packages for rpm in ${root}/rpmbuild/RPMS/*/*.rpm ; do cp $rpm ${root2}/packages/ rpms="$rpms "/packages/$(basename $rpm) done # Install the binary packages into the second container. buildah run $cid2 -- yum -y install $rpms # Run the binary package and compare its self-identified version to the one we tried to build. id=$(buildah run $cid2 -- buildah version | awk '/^Git Commit:/ { print $NF }') bv=$(buildah run $cid2 -- buildah version | awk '/^Version:/ { print $NF }') rv=$(buildah run $cid2 -- rpm -q --queryformat '%{version}' buildah) echo "short commit: $shortcommit" echo "id: $id" echo "buildah version: $bv" echo "buildah rpm version: $rv" test $shortcommit = $id test $bv = ${rv} -o $bv = ${rv}-dev # Clean up. buildah rm $cid $cid2 } @test "rpm-build Fedora latest" { skip_if_no_runtime # Build a container to use for building the binaries. image=registry.fedoraproject.org/fedora:latest cid=$(buildah from --pull --signature-policy ${TEST_SOURCES}/policy.json $image) root=$(buildah mount $cid) commit=$(git log --format=%H -n 1) shortcommit=$(echo ${commit} | cut -c-7) mkdir -p ${root}/rpmbuild/{SOURCES,SPECS} # Build the tarball. (cd ..; git archive --format tar.gz --prefix=buildah-${commit}/ ${commit}) > ${root}/rpmbuild/SOURCES/buildah-${shortcommit}.tar.gz # Update the .spec file with the commit ID. sed s:REPLACEWITHCOMMITID:${commit}:g ${TEST_SOURCES}/../contrib/rpm/buildah.spec > ${root}/rpmbuild/SPECS/buildah.spec # Install build dependencies and build binary packages. buildah run $cid -- dnf -y install 'dnf-command(builddep)' rpm-build buildah run $cid -- dnf -y builddep --spec rpmbuild/SPECS/buildah.spec buildah run $cid -- rpmbuild --define "_topdir /rpmbuild" -ba /rpmbuild/SPECS/buildah.spec # Build a second new container. cid2=$(buildah from --pull --signature-policy ${TEST_SOURCES}/policy.json $image) root2=$(buildah mount $cid2) # Copy the binary packages from the first container to the second one, and build a list of # their filenames relative to the root of the second container. rpms= mkdir -p ${root2}/packages for rpm in ${root}/rpmbuild/RPMS/*/*.rpm ; do cp $rpm ${root2}/packages/ rpms="$rpms "/packages/$(basename $rpm) done # Install the binary packages into the second container. buildah run $cid2 -- dnf -y install $rpms # Run the binary package and compare its self-identified version to the one we tried to build. id=$(buildah run $cid2 -- buildah version | awk '/^Git Commit:/ { print $NF }') bv=$(buildah run $cid2 -- buildah version | awk '/^Version:/ { print $NF }') rv=$(buildah run $cid2 -- rpm -q --queryformat '%{version}' buildah) echo "short commit: $shortcommit" echo "id: $id" echo "buildah version: $bv" echo "buildah rpm version: $rv" test $shortcommit = $id test $bv = ${rv} -o $bv = ${rv}-dev # Clean up. buildah rm $cid $cid2 } ================================================ FILE: tests/test_runner.sh ================================================ #!/usr/bin/env bash set -e cd "$(dirname "$(readlink -f "$BASH_SOURCE")")" # Default to using /tmp for test space. export TMPDIR=${TMPDIR:-/tmp} function execute() { >&2 echo "++ $@" eval "$@" } # Run the tests. execute time bats -j $(nproc) --tap "${@:-.}" ================================================ FILE: tests/testreport/testreport.go ================================================ package main import ( "bufio" "encoding/json" "fmt" "os" "path/filepath" "slices" "strconv" "strings" "syscall" types "github.com/containers/buildah/tests/testreport/types" "github.com/moby/sys/capability" "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" "go.podman.io/storage/pkg/mount" "golang.org/x/sys/unix" "golang.org/x/term" ) func getVersion(r *types.TestReport) { r.Spec.Version = fmt.Sprintf("%d.%d.%d%s", specs.VersionMajor, specs.VersionMinor, specs.VersionPatch, specs.VersionDev) } func getHostname(r *types.TestReport) error { hostname, err := os.Hostname() if err != nil { return fmt.Errorf("reading hostname: %w", err) } r.Spec.Hostname = hostname return nil } func getProcessTerminal(r *types.TestReport) error { r.Spec.Process.Terminal = term.IsTerminal(unix.Stdin) return nil } func getProcessConsoleSize(r *types.TestReport) error { if term.IsTerminal(unix.Stdin) { winsize, err := unix.IoctlGetWinsize(unix.Stdin, unix.TIOCGWINSZ) if err != nil { return fmt.Errorf("reading size of terminal on stdin: %w", err) } if r.Spec.Process.ConsoleSize == nil { r.Spec.Process.ConsoleSize = new(specs.Box) } r.Spec.Process.ConsoleSize.Height = uint(winsize.Row) r.Spec.Process.ConsoleSize.Width = uint(winsize.Col) } return nil } func getProcessUser(r *types.TestReport) error { r.Spec.Process.User.UID = uint32(unix.Getuid()) r.Spec.Process.User.GID = uint32(unix.Getgid()) groups, err := unix.Getgroups() if err != nil { return fmt.Errorf("reading supplemental groups list: %w", err) } for _, gid := range groups { r.Spec.Process.User.AdditionalGids = append(r.Spec.Process.User.AdditionalGids, uint32(gid)) } return nil } func getProcessArgs(r *types.TestReport) error { r.Spec.Process.Args = slices.Clone(os.Args) return nil } func getProcessEnv(r *types.TestReport) error { r.Spec.Process.Env = os.Environ() return nil } func getProcessCwd(r *types.TestReport) error { cwd := make([]byte, 8192) n, err := unix.Getcwd(cwd) if err != nil { return fmt.Errorf("determining current working directory: %w", err) } for n > 0 && cwd[n-1] == 0 { n-- } r.Spec.Process.Cwd = string(cwd[:n]) return nil } func getProcessCapabilities(r *types.TestReport) error { capabilities, err := capability.NewPid2(0) if err != nil { return fmt.Errorf("reading current capabilities: %w", err) } if err := capabilities.Load(); err != nil { return fmt.Errorf("loading capabilities: %w", err) } if r.Spec.Process.Capabilities == nil { r.Spec.Process.Capabilities = new(specs.LinuxCapabilities) } caplistMap := map[capability.CapType]*[]string{ capability.EFFECTIVE: &r.Spec.Process.Capabilities.Effective, capability.PERMITTED: &r.Spec.Process.Capabilities.Permitted, capability.INHERITABLE: &r.Spec.Process.Capabilities.Inheritable, capability.BOUNDING: &r.Spec.Process.Capabilities.Bounding, capability.AMBIENT: &r.Spec.Process.Capabilities.Ambient, } for capType, capList := range caplistMap { for _, cap := range capability.ListKnown() { if capabilities.Get(capType, cap) { *capList = append(*capList, strings.ToUpper("cap_"+cap.String())) } } } return nil } func getProcessRLimits(r *types.TestReport) error { limitsMap := map[string]int{ "RLIMIT_AS": unix.RLIMIT_AS, "RLIMIT_CORE": unix.RLIMIT_CORE, "RLIMIT_CPU": unix.RLIMIT_CPU, "RLIMIT_DATA": unix.RLIMIT_DATA, "RLIMIT_FSIZE": unix.RLIMIT_FSIZE, "RLIMIT_LOCKS": unix.RLIMIT_LOCKS, "RLIMIT_MEMLOCK": unix.RLIMIT_MEMLOCK, "RLIMIT_MSGQUEUE": unix.RLIMIT_MSGQUEUE, "RLIMIT_NICE": unix.RLIMIT_NICE, "RLIMIT_NOFILE": unix.RLIMIT_NOFILE, "RLIMIT_NPROC": unix.RLIMIT_NPROC, "RLIMIT_RSS": unix.RLIMIT_RSS, "RLIMIT_RTPRIO": unix.RLIMIT_RTPRIO, "RLIMIT_RTTIME": unix.RLIMIT_RTTIME, "RLIMIT_SIGPENDING": unix.RLIMIT_SIGPENDING, "RLIMIT_STACK": unix.RLIMIT_STACK, } for resourceName, resource := range limitsMap { var rlim unix.Rlimit if err := unix.Getrlimit(resource, &rlim); err != nil { return fmt.Errorf("reading %s limit: %w", resourceName, err) } if rlim.Cur == unix.RLIM_INFINITY && rlim.Max == unix.RLIM_INFINITY { continue } rlimit := specs.POSIXRlimit{ Type: resourceName, Soft: rlim.Cur, Hard: rlim.Max, } found := false for i := range r.Spec.Process.Rlimits { if r.Spec.Process.Rlimits[i].Type == resourceName { r.Spec.Process.Rlimits[i] = rlimit found = true } } if !found { r.Spec.Process.Rlimits = append(r.Spec.Process.Rlimits, rlimit) } } return nil } func getProcessNoNewPrivileges(r *types.TestReport) error { // We'd scan /proc/self/status here, but the "NoNewPrivs" line wasn't added until 4.10, // and we want to succeed on older kernels. r1, err := unix.PrctlRetInt(unix.PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0) if err != nil { return fmt.Errorf("reading no-new-privs bit: %w", err) } r.Spec.Process.NoNewPrivileges = (r1 != 0) return nil } func getProcessAppArmorProfile(_ *types.TestReport) error { // TODO return nil } func getProcessOOMScoreAdjust(r *types.TestReport) error { node := "/proc/self/oom_score_adj" score, err := os.ReadFile(node) if err != nil { return fmt.Errorf("reading %q: %w", node, err) } fields := strings.Fields(string(score)) if len(fields) != 1 { return fmt.Errorf("badly formatted line %q in %q: expected to find only one field", string(score), node) } oom, err := strconv.Atoi(fields[0]) if err != nil { return fmt.Errorf("parsing %q in line %q in %q: %w", fields[0], string(score), node, err) } if oom != 0 { r.Spec.Process.OOMScoreAdj = &oom } return nil } func getProcessSeLinuxLabel(_ *types.TestReport) error { // TODO return nil } func getProcess(r *types.TestReport) error { if r.Spec.Process == nil { r.Spec.Process = new(specs.Process) } if err := getProcessTerminal(r); err != nil { return err } if err := getProcessConsoleSize(r); err != nil { return err } if err := getProcessUser(r); err != nil { return err } if err := getProcessArgs(r); err != nil { return err } if err := getProcessEnv(r); err != nil { return err } if err := getProcessCwd(r); err != nil { return err } if err := getProcessCapabilities(r); err != nil { return err } if err := getProcessRLimits(r); err != nil { return err } if err := getProcessNoNewPrivileges(r); err != nil { return err } if err := getProcessAppArmorProfile(r); err != nil { return err } if err := getProcessOOMScoreAdjust(r); err != nil { return err } return getProcessSeLinuxLabel(r) } func getMounts(r *types.TestReport) error { infos, err := mount.GetMounts() if err != nil { return fmt.Errorf("reading current list of mounts: %w", err) } for _, info := range infos { mount := specs.Mount{ Destination: info.Mountpoint, Type: info.FSType, Source: info.Source, Options: strings.Split(info.Options, ","), } r.Spec.Mounts = append(r.Spec.Mounts, mount) } return nil } func getLinuxIDMappings(r *types.TestReport) error { getIDMapping := func(node string) ([]specs.LinuxIDMapping, error) { var mappings []specs.LinuxIDMapping mapfile, err := os.Open(node) if err != nil { return nil, fmt.Errorf("opening %q: %w", node, err) } defer mapfile.Close() scanner := bufio.NewScanner(mapfile) for scanner.Scan() { line := scanner.Text() fields := strings.Fields(line) if len(fields) != 3 { return nil, fmt.Errorf("badly formatted line %q in %q: expected to find exactly three fields", line, node) } cid, err := strconv.ParseUint(fields[0], 10, 32) if err != nil { return nil, fmt.Errorf("parsing %q in line %q in %q: %w", fields[0], line, node, err) } hid, err := strconv.ParseUint(fields[1], 10, 32) if err != nil { return nil, fmt.Errorf("parsing %q in line %q in %q: %w", fields[1], line, node, err) } size, err := strconv.ParseUint(fields[2], 10, 32) if err != nil { return nil, fmt.Errorf("parsing %q in line %q in %q: %w", fields[2], line, node, err) } mappings = append(mappings, specs.LinuxIDMapping{ContainerID: uint32(cid), HostID: uint32(hid), Size: uint32(size)}) } return mappings, nil } uidmap, err := getIDMapping("/proc/self/uid_map") if err != nil { return err } gidmap, err := getIDMapping("/proc/self/gid_map") if err != nil { return err } r.Spec.Linux.UIDMappings = uidmap r.Spec.Linux.GIDMappings = gidmap return nil } func getLinuxSysctl(r *types.TestReport) error { if r.Spec.Linux.Sysctl == nil { r.Spec.Linux.Sysctl = make(map[string]string) } walk := func(path string, info os.FileInfo, _ error) error { if info.IsDir() { return nil } value, err := os.ReadFile(path) if err != nil { if pe, ok := err.(*os.PathError); ok { if errno, ok := pe.Err.(syscall.Errno); ok { switch errno { case syscall.EACCES, syscall.EINVAL, syscall.EIO, syscall.EPERM: return nil } } } return fmt.Errorf("reading sysctl %q: %w", path, err) } path = strings.TrimPrefix(path, "/proc/sys/") sysctl := strings.ReplaceAll(path, "/", ".") val := strings.TrimRight(string(value), "\r\n") if strings.ContainsAny(val, "\r\n") { val = string(value) } r.Spec.Linux.Sysctl[sysctl] = val return nil } return filepath.Walk("/proc/sys", walk) } func getLinuxResources(_ *types.TestReport) error { // TODO return nil } func getLinuxCgroupsPath(_ *types.TestReport) error { // TODO return nil } func getLinuxNamespaces(_ *types.TestReport) error { // TODO return nil } func getLinuxDevices(_ *types.TestReport) error { // TODO return nil } func getLinuxRootfsPropagation(_ *types.TestReport) error { // TODO return nil } func getLinuxMaskedPaths(_ *types.TestReport) error { // TODO return nil } func getLinuxReadOnlyPaths(_ *types.TestReport) error { // TODO return nil } func getLinuxMountLabel(_ *types.TestReport) error { // TODO return nil } func getLinuxIntelRdt(_ *types.TestReport) error { // TODO return nil } func getLinux(r *types.TestReport) error { if r.Spec.Linux == nil { r.Spec.Linux = new(specs.Linux) } if err := getLinuxIDMappings(r); err != nil { return err } if err := getLinuxSysctl(r); err != nil { return err } if err := getLinuxResources(r); err != nil { return err } if err := getLinuxCgroupsPath(r); err != nil { return err } if err := getLinuxNamespaces(r); err != nil { return err } if err := getLinuxDevices(r); err != nil { return err } if err := getLinuxRootfsPropagation(r); err != nil { return err } if err := getLinuxMaskedPaths(r); err != nil { return err } if err := getLinuxReadOnlyPaths(r); err != nil { return err } if err := getLinuxMountLabel(r); err != nil { return err } return getLinuxIntelRdt(r) } func main() { var r types.TestReport if r.Spec == nil { r.Spec = new(specs.Spec) } getVersion(&r) if err := getProcess(&r); err != nil { logrus.Errorf("%v", err) os.Exit(1) } if err := getHostname(&r); err != nil { logrus.Errorf("%v", err) os.Exit(1) } if err := getMounts(&r); err != nil { logrus.Errorf("%v", err) os.Exit(1) } if err := getLinux(&r); err != nil { logrus.Errorf("%v", err) os.Exit(1) } if err := json.NewEncoder(os.Stdout).Encode(r); err != nil { logrus.Errorf("%v", err) os.Exit(1) } } ================================================ FILE: tests/testreport/types/types.go ================================================ package testreporttypes import ( "github.com/opencontainers/runtime-spec/specs-go" ) // TestReport is an internal type used for testing. type TestReport struct { Spec *specs.Spec } ================================================ FILE: tests/tmt/system.fmf ================================================ require: - buildah-tests - git-daemon - slirp4netns environment: BUILDAH_BINARY: /usr/bin/buildah IMGTYPE_BINARY: /usr/bin/buildah-imgtype INET_BINARY: /usr/bin/buildah-inet COPY_BINARY: /usr/bin/buildah-copy TUTORIAL_BINARY: /usr/bin/buildah-tutorial DUMPSPEC_BINARY: /usr/bin/buildah-dumpspec PASSWD_BINARY: /usr/bin/buildah-passwd GRPCNOOP_BINARY: /usr/bin/buildah-grpcnoop WAIT_BINARY: /usr/bin/buildah-wait CRASH_BINARY: /usr/bin/buildah-crash TMPDIR: /var/tmp adjust: - when: initiator != "packit" environment+: RELEASE_TESTING: true /local/root: summary: System test test: bash ./system.sh duration: 60m ================================================ FILE: tests/tmt/system.sh ================================================ #!/usr/bin/env bash set -exo pipefail uname -r rpm -q \ aardvark-dns \ buildah \ buildah-tests \ conmon \ container-selinux \ containers-common \ crun \ netavark \ systemd bats /usr/share/buildah/test/system ================================================ FILE: tests/tools/Makefile ================================================ GO := go GO_BUILD=$(GO) build BUILDDIR := build SOURCES = go.mod go.sum all: $(BUILDDIR) .PHONY: vendor vendor: $(GO) mod tidy $(GO) mod vendor $(GO) mod verify if test -n "$(strip $(shell go env GOTOOLCHAIN))"; then go mod edit -toolchain none ; fi vendor-in-container: if test -d `go env GOCACHE` && test -w `go env GOCACHE` ; then \ podman run --privileged --rm --env HOME=/root -v `go env GOCACHE`:/root/.cache/go-build --env GOCACHE=/root/.cache/go-build -v `pwd`:/src -w /src docker.io/library/golang:1.21 make vendor ; \ else \ podman run --privileged --rm --env HOME=/root -v `pwd`:/src -w /src docker.io/library/golang:1.21 make vendor ; \ fi .PHONY: clean clean: rm -rf $(BUILDDIR) .PHONY: $(BUILDDIR) $(BUILDDIR): \ $(BUILDDIR)/go-md2man \ $(BUILDDIR)/golangci-lint $(BUILDDIR)/go-md2man: $(SOURCES) $(GO_BUILD) -o $@ ./vendor/github.com/cpuguy83/go-md2man/v2 # Use GOLANGCI_LINT_VERSION exported in top-level Makefile, # or, if called directly, use "latest". $(BUILDDIR)/golangci-lint: VERSION=$(if $(GOLANGCI_LINT_VERSION),v$(GOLANGCI_LINT_VERSION),latest) $(BUILDDIR)/golangci-lint: check-lint-version @test -f $(BUILDDIR)/golangci-lint || \ curl -fsSL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b ./$(BUILDDIR) $(VERSION) .PHONY: check-lint-version check-lint-version: VERSION=$(GOLANGCI_LINT_VERSION) check-lint-version: @test -n "$(VERSION)" && \ $(BUILDDIR)/golangci-lint version 2>/dev/null | grep -F $(VERSION) || \ rm -f $(BUILDDIR)/golangci-lint ================================================ FILE: tests/tools/go.mod ================================================ module github.com/containers/buildah/tests/tools go 1.24.3 require github.com/cpuguy83/go-md2man/v2 v2.0.4 require github.com/russross/blackfriday/v2 v2.1.0 // indirect ================================================ FILE: tests/tools/go.sum ================================================ github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= ================================================ FILE: tests/tools/tools.go ================================================ //go:build tools package tools // Importing the packages here will allow to vendor those via // `go mod vendor`. import ( _ "github.com/cpuguy83/go-md2man/v2" ) ================================================ FILE: tests/tools/vendor/github.com/russross/blackfriday/v2/.gitignore ================================================ *.out *.swp *.8 *.6 _obj _test* markdown tags ================================================ FILE: tests/tools/vendor/github.com/russross/blackfriday/v2/.travis.yml ================================================ sudo: false language: go go: - "1.10.x" - "1.11.x" - tip matrix: fast_finish: true allow_failures: - go: tip install: - # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step). script: - go get -t -v ./... - diff -u <(echo -n) <(gofmt -d -s .) - go tool vet . - go test -v ./... ================================================ FILE: tests/tools/vendor/github.com/russross/blackfriday/v2/LICENSE.txt ================================================ Blackfriday is distributed under the Simplified BSD License: > Copyright © 2011 Russ Ross > All rights reserved. > > Redistribution and use in source and binary forms, with or without > modification, are permitted provided that the following conditions > are met: > > 1. Redistributions of source code must retain the above copyright > notice, this list of conditions and the following disclaimer. > > 2. Redistributions in binary form must reproduce the above > copyright notice, this list of conditions and the following > disclaimer in the documentation and/or other materials provided with > the distribution. > > THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS > "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT > LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS > FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE > COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, > INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, > BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; > LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER > CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT > LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN > ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE > POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: tests/tools/vendor/github.com/russross/blackfriday/v2/README.md ================================================ Blackfriday [![Build Status][BuildV2SVG]][BuildV2URL] [![PkgGoDev][PkgGoDevV2SVG]][PkgGoDevV2URL] =========== Blackfriday is a [Markdown][1] processor implemented in [Go][2]. It is paranoid about its input (so you can safely feed it user-supplied data), it is fast, it supports common extensions (tables, smart punctuation substitutions, etc.), and it is safe for all utf-8 (unicode) input. HTML output is currently supported, along with Smartypants extensions. It started as a translation from C of [Sundown][3]. Installation ------------ Blackfriday is compatible with modern Go releases in module mode. With Go installed: go get github.com/russross/blackfriday/v2 will resolve and add the package to the current development module, then build and install it. Alternatively, you can achieve the same if you import it in a package: import "github.com/russross/blackfriday/v2" and `go get` without parameters. Legacy GOPATH mode is unsupported. Versions -------- Currently maintained and recommended version of Blackfriday is `v2`. It's being developed on its own branch: https://github.com/russross/blackfriday/tree/v2 and the documentation is available at https://pkg.go.dev/github.com/russross/blackfriday/v2. It is `go get`-able in module mode at `github.com/russross/blackfriday/v2`. Version 2 offers a number of improvements over v1: * Cleaned up API * A separate call to [`Parse`][4], which produces an abstract syntax tree for the document * Latest bug fixes * Flexibility to easily add your own rendering extensions Potential drawbacks: * Our benchmarks show v2 to be slightly slower than v1. Currently in the ballpark of around 15%. * API breakage. If you can't afford modifying your code to adhere to the new API and don't care too much about the new features, v2 is probably not for you. * Several bug fixes are trailing behind and still need to be forward-ported to v2. See issue [#348](https://github.com/russross/blackfriday/issues/348) for tracking. If you are still interested in the legacy `v1`, you can import it from `github.com/russross/blackfriday`. Documentation for the legacy v1 can be found here: https://pkg.go.dev/github.com/russross/blackfriday. Usage ----- For the most sensible markdown processing, it is as simple as getting your input into a byte slice and calling: ```go output := blackfriday.Run(input) ``` Your input will be parsed and the output rendered with a set of most popular extensions enabled. If you want the most basic feature set, corresponding with the bare Markdown specification, use: ```go output := blackfriday.Run(input, blackfriday.WithNoExtensions()) ``` ### Sanitize untrusted content Blackfriday itself does nothing to protect against malicious content. If you are dealing with user-supplied markdown, we recommend running Blackfriday's output through HTML sanitizer such as [Bluemonday][5]. Here's an example of simple usage of Blackfriday together with Bluemonday: ```go import ( "github.com/microcosm-cc/bluemonday" "github.com/russross/blackfriday/v2" ) // ... unsafe := blackfriday.Run(input) html := bluemonday.UGCPolicy().SanitizeBytes(unsafe) ``` ### Custom options If you want to customize the set of options, use `blackfriday.WithExtensions`, `blackfriday.WithRenderer` and `blackfriday.WithRefOverride`. ### `blackfriday-tool` You can also check out `blackfriday-tool` for a more complete example of how to use it. Download and install it using: go get github.com/russross/blackfriday-tool This is a simple command-line tool that allows you to process a markdown file using a standalone program. You can also browse the source directly on github if you are just looking for some example code: * Note that if you have not already done so, installing `blackfriday-tool` will be sufficient to download and install blackfriday in addition to the tool itself. The tool binary will be installed in `$GOPATH/bin`. This is a statically-linked binary that can be copied to wherever you need it without worrying about dependencies and library versions. ### Sanitized anchor names Blackfriday includes an algorithm for creating sanitized anchor names corresponding to a given input text. This algorithm is used to create anchors for headings when `AutoHeadingIDs` extension is enabled. The algorithm has a specification, so that other packages can create compatible anchor names and links to those anchors. The specification is located at https://pkg.go.dev/github.com/russross/blackfriday/v2#hdr-Sanitized_Anchor_Names. [`SanitizedAnchorName`](https://pkg.go.dev/github.com/russross/blackfriday/v2#SanitizedAnchorName) exposes this functionality, and can be used to create compatible links to the anchor names generated by blackfriday. This algorithm is also implemented in a small standalone package at [`github.com/shurcooL/sanitized_anchor_name`](https://pkg.go.dev/github.com/shurcooL/sanitized_anchor_name). It can be useful for clients that want a small package and don't need full functionality of blackfriday. Features -------- All features of Sundown are supported, including: * **Compatibility**. The Markdown v1.0.3 test suite passes with the `--tidy` option. Without `--tidy`, the differences are mostly in whitespace and entity escaping, where blackfriday is more consistent and cleaner. * **Common extensions**, including table support, fenced code blocks, autolinks, strikethroughs, non-strict emphasis, etc. * **Safety**. Blackfriday is paranoid when parsing, making it safe to feed untrusted user input without fear of bad things happening. The test suite stress tests this and there are no known inputs that make it crash. If you find one, please let me know and send me the input that does it. NOTE: "safety" in this context means *runtime safety only*. In order to protect yourself against JavaScript injection in untrusted content, see [this example](https://github.com/russross/blackfriday#sanitize-untrusted-content). * **Fast processing**. It is fast enough to render on-demand in most web applications without having to cache the output. * **Thread safety**. You can run multiple parsers in different goroutines without ill effect. There is no dependence on global shared state. * **Minimal dependencies**. Blackfriday only depends on standard library packages in Go. The source code is pretty self-contained, so it is easy to add to any project, including Google App Engine projects. * **Standards compliant**. Output successfully validates using the W3C validation tool for HTML 4.01 and XHTML 1.0 Transitional. Extensions ---------- In addition to the standard markdown syntax, this package implements the following extensions: * **Intra-word emphasis supression**. The `_` character is commonly used inside words when discussing code, so having markdown interpret it as an emphasis command is usually the wrong thing. Blackfriday lets you treat all emphasis markers as normal characters when they occur inside a word. * **Tables**. Tables can be created by drawing them in the input using a simple syntax: ``` Name | Age --------|------ Bob | 27 Alice | 23 ``` * **Fenced code blocks**. In addition to the normal 4-space indentation to mark code blocks, you can explicitly mark them and supply a language (to make syntax highlighting simple). Just mark it like this: ```go func getTrue() bool { return true } ``` You can use 3 or more backticks to mark the beginning of the block, and the same number to mark the end of the block. To preserve classes of fenced code blocks while using the bluemonday HTML sanitizer, use the following policy: ```go p := bluemonday.UGCPolicy() p.AllowAttrs("class").Matching(regexp.MustCompile("^language-[a-zA-Z0-9]+$")).OnElements("code") html := p.SanitizeBytes(unsafe) ``` * **Definition lists**. A simple definition list is made of a single-line term followed by a colon and the definition for that term. Cat : Fluffy animal everyone likes Internet : Vector of transmission for pictures of cats Terms must be separated from the previous definition by a blank line. * **Footnotes**. A marker in the text that will become a superscript number; a footnote definition that will be placed in a list of footnotes at the end of the document. A footnote looks like this: This is a footnote.[^1] [^1]: the footnote text. * **Autolinking**. Blackfriday can find URLs that have not been explicitly marked as links and turn them into links. * **Strikethrough**. Use two tildes (`~~`) to mark text that should be crossed out. * **Hard line breaks**. With this extension enabled newlines in the input translate into line breaks in the output. This extension is off by default. * **Smart quotes**. Smartypants-style punctuation substitution is supported, turning normal double- and single-quote marks into curly quotes, etc. * **LaTeX-style dash parsing** is an additional option, where `--` is translated into `–`, and `---` is translated into `—`. This differs from most smartypants processors, which turn a single hyphen into an ndash and a double hyphen into an mdash. * **Smart fractions**, where anything that looks like a fraction is translated into suitable HTML (instead of just a few special cases like most smartypant processors). For example, `4/5` becomes `45`, which renders as 45. Other renderers --------------- Blackfriday is structured to allow alternative rendering engines. Here are a few of note: * [github_flavored_markdown](https://pkg.go.dev/github.com/shurcooL/github_flavored_markdown): provides a GitHub Flavored Markdown renderer with fenced code block highlighting, clickable heading anchor links. It's not customizable, and its goal is to produce HTML output equivalent to the [GitHub Markdown API endpoint](https://developer.github.com/v3/markdown/#render-a-markdown-document-in-raw-mode), except the rendering is performed locally. * [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt, but for markdown. * [LaTeX output](https://gitlab.com/ambrevar/blackfriday-latex): renders output as LaTeX. * [bfchroma](https://github.com/Depado/bfchroma/): provides convenience integration with the [Chroma](https://github.com/alecthomas/chroma) code highlighting library. bfchroma is only compatible with v2 of Blackfriday and provides a drop-in renderer ready to use with Blackfriday, as well as options and means for further customization. * [Blackfriday-Confluence](https://github.com/kentaro-m/blackfriday-confluence): provides a [Confluence Wiki Markup](https://confluence.atlassian.com/doc/confluence-wiki-markup-251003035.html) renderer. * [Blackfriday-Slack](https://github.com/karriereat/blackfriday-slack): converts markdown to slack message style TODO ---- * More unit testing * Improve Unicode support. It does not understand all Unicode rules (about what constitutes a letter, a punctuation symbol, etc.), so it may fail to detect word boundaries correctly in some instances. It is safe on all UTF-8 input. License ------- [Blackfriday is distributed under the Simplified BSD License](LICENSE.txt) [1]: https://daringfireball.net/projects/markdown/ "Markdown" [2]: https://golang.org/ "Go Language" [3]: https://github.com/vmg/sundown "Sundown" [4]: https://pkg.go.dev/github.com/russross/blackfriday/v2#Parse "Parse func" [5]: https://github.com/microcosm-cc/bluemonday "Bluemonday" [BuildV2SVG]: https://travis-ci.org/russross/blackfriday.svg?branch=v2 [BuildV2URL]: https://travis-ci.org/russross/blackfriday [PkgGoDevV2SVG]: https://pkg.go.dev/badge/github.com/russross/blackfriday/v2 [PkgGoDevV2URL]: https://pkg.go.dev/github.com/russross/blackfriday/v2 ================================================ FILE: tests/tools/vendor/github.com/russross/blackfriday/v2/block.go ================================================ // // Blackfriday Markdown Processor // Available at http://github.com/russross/blackfriday // // Copyright © 2011 Russ Ross . // Distributed under the Simplified BSD License. // See README.md for details. // // // Functions to parse block-level elements. // package blackfriday import ( "bytes" "html" "regexp" "strings" "unicode" ) const ( charEntity = "&(?:#x[a-f0-9]{1,8}|#[0-9]{1,8}|[a-z][a-z0-9]{1,31});" escapable = "[!\"#$%&'()*+,./:;<=>?@[\\\\\\]^_`{|}~-]" ) var ( reBackslashOrAmp = regexp.MustCompile("[\\&]") reEntityOrEscapedChar = regexp.MustCompile("(?i)\\\\" + escapable + "|" + charEntity) ) // Parse block-level data. // Note: this function and many that it calls assume that // the input buffer ends with a newline. func (p *Markdown) block(data []byte) { // this is called recursively: enforce a maximum depth if p.nesting >= p.maxNesting { return } p.nesting++ // parse out one block-level construct at a time for len(data) > 0 { // prefixed heading: // // # Heading 1 // ## Heading 2 // ... // ###### Heading 6 if p.isPrefixHeading(data) { data = data[p.prefixHeading(data):] continue } // block of preformatted HTML: // //
// ... //
if data[0] == '<' { if i := p.html(data, true); i > 0 { data = data[i:] continue } } // title block // // % stuff // % more stuff // % even more stuff if p.extensions&Titleblock != 0 { if data[0] == '%' { if i := p.titleBlock(data, true); i > 0 { data = data[i:] continue } } } // blank lines. note: returns the # of bytes to skip if i := p.isEmpty(data); i > 0 { data = data[i:] continue } // indented code block: // // func max(a, b int) int { // if a > b { // return a // } // return b // } if p.codePrefix(data) > 0 { data = data[p.code(data):] continue } // fenced code block: // // ``` go // func fact(n int) int { // if n <= 1 { // return n // } // return n * fact(n-1) // } // ``` if p.extensions&FencedCode != 0 { if i := p.fencedCodeBlock(data, true); i > 0 { data = data[i:] continue } } // horizontal rule: // // ------ // or // ****** // or // ______ if p.isHRule(data) { p.addBlock(HorizontalRule, nil) var i int for i = 0; i < len(data) && data[i] != '\n'; i++ { } data = data[i:] continue } // block quote: // // > A big quote I found somewhere // > on the web if p.quotePrefix(data) > 0 { data = data[p.quote(data):] continue } // table: // // Name | Age | Phone // ------|-----|--------- // Bob | 31 | 555-1234 // Alice | 27 | 555-4321 if p.extensions&Tables != 0 { if i := p.table(data); i > 0 { data = data[i:] continue } } // an itemized/unordered list: // // * Item 1 // * Item 2 // // also works with + or - if p.uliPrefix(data) > 0 { data = data[p.list(data, 0):] continue } // a numbered/ordered list: // // 1. Item 1 // 2. Item 2 if p.oliPrefix(data) > 0 { data = data[p.list(data, ListTypeOrdered):] continue } // definition lists: // // Term 1 // : Definition a // : Definition b // // Term 2 // : Definition c if p.extensions&DefinitionLists != 0 { if p.dliPrefix(data) > 0 { data = data[p.list(data, ListTypeDefinition):] continue } } // anything else must look like a normal paragraph // note: this finds underlined headings, too data = data[p.paragraph(data):] } p.nesting-- } func (p *Markdown) addBlock(typ NodeType, content []byte) *Node { p.closeUnmatchedBlocks() container := p.addChild(typ, 0) container.content = content return container } func (p *Markdown) isPrefixHeading(data []byte) bool { if data[0] != '#' { return false } if p.extensions&SpaceHeadings != 0 { level := 0 for level < 6 && level < len(data) && data[level] == '#' { level++ } if level == len(data) || data[level] != ' ' { return false } } return true } func (p *Markdown) prefixHeading(data []byte) int { level := 0 for level < 6 && level < len(data) && data[level] == '#' { level++ } i := skipChar(data, level, ' ') end := skipUntilChar(data, i, '\n') skip := end id := "" if p.extensions&HeadingIDs != 0 { j, k := 0, 0 // find start/end of heading id for j = i; j < end-1 && (data[j] != '{' || data[j+1] != '#'); j++ { } for k = j + 1; k < end && data[k] != '}'; k++ { } // extract heading id iff found if j < end && k < end { id = string(data[j+2 : k]) end = j skip = k + 1 for end > 0 && data[end-1] == ' ' { end-- } } } for end > 0 && data[end-1] == '#' { if isBackslashEscaped(data, end-1) { break } end-- } for end > 0 && data[end-1] == ' ' { end-- } if end > i { if id == "" && p.extensions&AutoHeadingIDs != 0 { id = SanitizedAnchorName(string(data[i:end])) } block := p.addBlock(Heading, data[i:end]) block.HeadingID = id block.Level = level } return skip } func (p *Markdown) isUnderlinedHeading(data []byte) int { // test of level 1 heading if data[0] == '=' { i := skipChar(data, 1, '=') i = skipChar(data, i, ' ') if i < len(data) && data[i] == '\n' { return 1 } return 0 } // test of level 2 heading if data[0] == '-' { i := skipChar(data, 1, '-') i = skipChar(data, i, ' ') if i < len(data) && data[i] == '\n' { return 2 } return 0 } return 0 } func (p *Markdown) titleBlock(data []byte, doRender bool) int { if data[0] != '%' { return 0 } splitData := bytes.Split(data, []byte("\n")) var i int for idx, b := range splitData { if !bytes.HasPrefix(b, []byte("%")) { i = idx // - 1 break } } data = bytes.Join(splitData[0:i], []byte("\n")) consumed := len(data) data = bytes.TrimPrefix(data, []byte("% ")) data = bytes.Replace(data, []byte("\n% "), []byte("\n"), -1) block := p.addBlock(Heading, data) block.Level = 1 block.IsTitleblock = true return consumed } func (p *Markdown) html(data []byte, doRender bool) int { var i, j int // identify the opening tag if data[0] != '<' { return 0 } curtag, tagfound := p.htmlFindTag(data[1:]) // handle special cases if !tagfound { // check for an HTML comment if size := p.htmlComment(data, doRender); size > 0 { return size } // check for an
tag if size := p.htmlHr(data, doRender); size > 0 { return size } // no special case recognized return 0 } // look for an unindented matching closing tag // followed by a blank line found := false /* closetag := []byte("\n") j = len(curtag) + 1 for !found { // scan for a closing tag at the beginning of a line if skip := bytes.Index(data[j:], closetag); skip >= 0 { j += skip + len(closetag) } else { break } // see if it is the only thing on the line if skip := p.isEmpty(data[j:]); skip > 0 { // see if it is followed by a blank line/eof j += skip if j >= len(data) { found = true i = j } else { if skip := p.isEmpty(data[j:]); skip > 0 { j += skip found = true i = j } } } } */ // if not found, try a second pass looking for indented match // but not if tag is "ins" or "del" (following original Markdown.pl) if !found && curtag != "ins" && curtag != "del" { i = 1 for i < len(data) { i++ for i < len(data) && !(data[i-1] == '<' && data[i] == '/') { i++ } if i+2+len(curtag) >= len(data) { break } j = p.htmlFindEnd(curtag, data[i-1:]) if j > 0 { i += j - 1 found = true break } } } if !found { return 0 } // the end of the block has been found if doRender { // trim newlines end := i for end > 0 && data[end-1] == '\n' { end-- } finalizeHTMLBlock(p.addBlock(HTMLBlock, data[:end])) } return i } func finalizeHTMLBlock(block *Node) { block.Literal = block.content block.content = nil } // HTML comment, lax form func (p *Markdown) htmlComment(data []byte, doRender bool) int { i := p.inlineHTMLComment(data) // needs to end with a blank line if j := p.isEmpty(data[i:]); j > 0 { size := i + j if doRender { // trim trailing newlines end := size for end > 0 && data[end-1] == '\n' { end-- } block := p.addBlock(HTMLBlock, data[:end]) finalizeHTMLBlock(block) } return size } return 0 } // HR, which is the only self-closing block tag considered func (p *Markdown) htmlHr(data []byte, doRender bool) int { if len(data) < 4 { return 0 } if data[0] != '<' || (data[1] != 'h' && data[1] != 'H') || (data[2] != 'r' && data[2] != 'R') { return 0 } if data[3] != ' ' && data[3] != '/' && data[3] != '>' { // not an
tag after all; at least not a valid one return 0 } i := 3 for i < len(data) && data[i] != '>' && data[i] != '\n' { i++ } if i < len(data) && data[i] == '>' { i++ if j := p.isEmpty(data[i:]); j > 0 { size := i + j if doRender { // trim newlines end := size for end > 0 && data[end-1] == '\n' { end-- } finalizeHTMLBlock(p.addBlock(HTMLBlock, data[:end])) } return size } } return 0 } func (p *Markdown) htmlFindTag(data []byte) (string, bool) { i := 0 for i < len(data) && isalnum(data[i]) { i++ } key := string(data[:i]) if _, ok := blockTags[key]; ok { return key, true } return "", false } func (p *Markdown) htmlFindEnd(tag string, data []byte) int { // assume data[0] == '<' && data[1] == '/' already tested if tag == "hr" { return 2 } // check if tag is a match closetag := []byte("") if !bytes.HasPrefix(data, closetag) { return 0 } i := len(closetag) // check that the rest of the line is blank skip := 0 if skip = p.isEmpty(data[i:]); skip == 0 { return 0 } i += skip skip = 0 if i >= len(data) { return i } if p.extensions&LaxHTMLBlocks != 0 { return i } if skip = p.isEmpty(data[i:]); skip == 0 { // following line must be blank return 0 } return i + skip } func (*Markdown) isEmpty(data []byte) int { // it is okay to call isEmpty on an empty buffer if len(data) == 0 { return 0 } var i int for i = 0; i < len(data) && data[i] != '\n'; i++ { if data[i] != ' ' && data[i] != '\t' { return 0 } } if i < len(data) && data[i] == '\n' { i++ } return i } func (*Markdown) isHRule(data []byte) bool { i := 0 // skip up to three spaces for i < 3 && data[i] == ' ' { i++ } // look at the hrule char if data[i] != '*' && data[i] != '-' && data[i] != '_' { return false } c := data[i] // the whole line must be the char or whitespace n := 0 for i < len(data) && data[i] != '\n' { switch { case data[i] == c: n++ case data[i] != ' ': return false } i++ } return n >= 3 } // isFenceLine checks if there's a fence line (e.g., ``` or ``` go) at the beginning of data, // and returns the end index if so, or 0 otherwise. It also returns the marker found. // If info is not nil, it gets set to the syntax specified in the fence line. func isFenceLine(data []byte, info *string, oldmarker string) (end int, marker string) { i, size := 0, 0 // skip up to three spaces for i < len(data) && i < 3 && data[i] == ' ' { i++ } // check for the marker characters: ~ or ` if i >= len(data) { return 0, "" } if data[i] != '~' && data[i] != '`' { return 0, "" } c := data[i] // the whole line must be the same char or whitespace for i < len(data) && data[i] == c { size++ i++ } // the marker char must occur at least 3 times if size < 3 { return 0, "" } marker = string(data[i-size : i]) // if this is the end marker, it must match the beginning marker if oldmarker != "" && marker != oldmarker { return 0, "" } // TODO(shurcooL): It's probably a good idea to simplify the 2 code paths here // into one, always get the info string, and discard it if the caller doesn't care. if info != nil { infoLength := 0 i = skipChar(data, i, ' ') if i >= len(data) { if i == len(data) { return i, marker } return 0, "" } infoStart := i if data[i] == '{' { i++ infoStart++ for i < len(data) && data[i] != '}' && data[i] != '\n' { infoLength++ i++ } if i >= len(data) || data[i] != '}' { return 0, "" } // strip all whitespace at the beginning and the end // of the {} block for infoLength > 0 && isspace(data[infoStart]) { infoStart++ infoLength-- } for infoLength > 0 && isspace(data[infoStart+infoLength-1]) { infoLength-- } i++ i = skipChar(data, i, ' ') } else { for i < len(data) && !isverticalspace(data[i]) { infoLength++ i++ } } *info = strings.TrimSpace(string(data[infoStart : infoStart+infoLength])) } if i == len(data) { return i, marker } if i > len(data) || data[i] != '\n' { return 0, "" } return i + 1, marker // Take newline into account. } // fencedCodeBlock returns the end index if data contains a fenced code block at the beginning, // or 0 otherwise. It writes to out if doRender is true, otherwise it has no side effects. // If doRender is true, a final newline is mandatory to recognize the fenced code block. func (p *Markdown) fencedCodeBlock(data []byte, doRender bool) int { var info string beg, marker := isFenceLine(data, &info, "") if beg == 0 || beg >= len(data) { return 0 } fenceLength := beg - 1 var work bytes.Buffer work.Write([]byte(info)) work.WriteByte('\n') for { // safe to assume beg < len(data) // check for the end of the code block fenceEnd, _ := isFenceLine(data[beg:], nil, marker) if fenceEnd != 0 { beg += fenceEnd break } // copy the current line end := skipUntilChar(data, beg, '\n') + 1 // did we reach the end of the buffer without a closing marker? if end >= len(data) { return 0 } // verbatim copy to the working buffer if doRender { work.Write(data[beg:end]) } beg = end } if doRender { block := p.addBlock(CodeBlock, work.Bytes()) // TODO: get rid of temp buffer block.IsFenced = true block.FenceLength = fenceLength finalizeCodeBlock(block) } return beg } func unescapeChar(str []byte) []byte { if str[0] == '\\' { return []byte{str[1]} } return []byte(html.UnescapeString(string(str))) } func unescapeString(str []byte) []byte { if reBackslashOrAmp.Match(str) { return reEntityOrEscapedChar.ReplaceAllFunc(str, unescapeChar) } return str } func finalizeCodeBlock(block *Node) { if block.IsFenced { newlinePos := bytes.IndexByte(block.content, '\n') firstLine := block.content[:newlinePos] rest := block.content[newlinePos+1:] block.Info = unescapeString(bytes.Trim(firstLine, "\n")) block.Literal = rest } else { block.Literal = block.content } block.content = nil } func (p *Markdown) table(data []byte) int { table := p.addBlock(Table, nil) i, columns := p.tableHeader(data) if i == 0 { p.tip = table.Parent table.Unlink() return 0 } p.addBlock(TableBody, nil) for i < len(data) { pipes, rowStart := 0, i for ; i < len(data) && data[i] != '\n'; i++ { if data[i] == '|' { pipes++ } } if pipes == 0 { i = rowStart break } // include the newline in data sent to tableRow if i < len(data) && data[i] == '\n' { i++ } p.tableRow(data[rowStart:i], columns, false) } return i } // check if the specified position is preceded by an odd number of backslashes func isBackslashEscaped(data []byte, i int) bool { backslashes := 0 for i-backslashes-1 >= 0 && data[i-backslashes-1] == '\\' { backslashes++ } return backslashes&1 == 1 } func (p *Markdown) tableHeader(data []byte) (size int, columns []CellAlignFlags) { i := 0 colCount := 1 for i = 0; i < len(data) && data[i] != '\n'; i++ { if data[i] == '|' && !isBackslashEscaped(data, i) { colCount++ } } // doesn't look like a table header if colCount == 1 { return } // include the newline in the data sent to tableRow j := i if j < len(data) && data[j] == '\n' { j++ } header := data[:j] // column count ignores pipes at beginning or end of line if data[0] == '|' { colCount-- } if i > 2 && data[i-1] == '|' && !isBackslashEscaped(data, i-1) { colCount-- } columns = make([]CellAlignFlags, colCount) // move on to the header underline i++ if i >= len(data) { return } if data[i] == '|' && !isBackslashEscaped(data, i) { i++ } i = skipChar(data, i, ' ') // each column header is of form: / *:?-+:? *|/ with # dashes + # colons >= 3 // and trailing | optional on last column col := 0 for i < len(data) && data[i] != '\n' { dashes := 0 if data[i] == ':' { i++ columns[col] |= TableAlignmentLeft dashes++ } for i < len(data) && data[i] == '-' { i++ dashes++ } if i < len(data) && data[i] == ':' { i++ columns[col] |= TableAlignmentRight dashes++ } for i < len(data) && data[i] == ' ' { i++ } if i == len(data) { return } // end of column test is messy switch { case dashes < 3: // not a valid column return case data[i] == '|' && !isBackslashEscaped(data, i): // marker found, now skip past trailing whitespace col++ i++ for i < len(data) && data[i] == ' ' { i++ } // trailing junk found after last column if col >= colCount && i < len(data) && data[i] != '\n' { return } case (data[i] != '|' || isBackslashEscaped(data, i)) && col+1 < colCount: // something else found where marker was required return case data[i] == '\n': // marker is optional for the last column col++ default: // trailing junk found after last column return } } if col != colCount { return } p.addBlock(TableHead, nil) p.tableRow(header, columns, true) size = i if size < len(data) && data[size] == '\n' { size++ } return } func (p *Markdown) tableRow(data []byte, columns []CellAlignFlags, header bool) { p.addBlock(TableRow, nil) i, col := 0, 0 if data[i] == '|' && !isBackslashEscaped(data, i) { i++ } for col = 0; col < len(columns) && i < len(data); col++ { for i < len(data) && data[i] == ' ' { i++ } cellStart := i for i < len(data) && (data[i] != '|' || isBackslashEscaped(data, i)) && data[i] != '\n' { i++ } cellEnd := i // skip the end-of-cell marker, possibly taking us past end of buffer i++ for cellEnd > cellStart && cellEnd-1 < len(data) && data[cellEnd-1] == ' ' { cellEnd-- } cell := p.addBlock(TableCell, data[cellStart:cellEnd]) cell.IsHeader = header cell.Align = columns[col] } // pad it out with empty columns to get the right number for ; col < len(columns); col++ { cell := p.addBlock(TableCell, nil) cell.IsHeader = header cell.Align = columns[col] } // silently ignore rows with too many cells } // returns blockquote prefix length func (p *Markdown) quotePrefix(data []byte) int { i := 0 for i < 3 && i < len(data) && data[i] == ' ' { i++ } if i < len(data) && data[i] == '>' { if i+1 < len(data) && data[i+1] == ' ' { return i + 2 } return i + 1 } return 0 } // blockquote ends with at least one blank line // followed by something without a blockquote prefix func (p *Markdown) terminateBlockquote(data []byte, beg, end int) bool { if p.isEmpty(data[beg:]) <= 0 { return false } if end >= len(data) { return true } return p.quotePrefix(data[end:]) == 0 && p.isEmpty(data[end:]) == 0 } // parse a blockquote fragment func (p *Markdown) quote(data []byte) int { block := p.addBlock(BlockQuote, nil) var raw bytes.Buffer beg, end := 0, 0 for beg < len(data) { end = beg // Step over whole lines, collecting them. While doing that, check for // fenced code and if one's found, incorporate it altogether, // irregardless of any contents inside it for end < len(data) && data[end] != '\n' { if p.extensions&FencedCode != 0 { if i := p.fencedCodeBlock(data[end:], false); i > 0 { // -1 to compensate for the extra end++ after the loop: end += i - 1 break } } end++ } if end < len(data) && data[end] == '\n' { end++ } if pre := p.quotePrefix(data[beg:]); pre > 0 { // skip the prefix beg += pre } else if p.terminateBlockquote(data, beg, end) { break } // this line is part of the blockquote raw.Write(data[beg:end]) beg = end } p.block(raw.Bytes()) p.finalize(block) return end } // returns prefix length for block code func (p *Markdown) codePrefix(data []byte) int { if len(data) >= 1 && data[0] == '\t' { return 1 } if len(data) >= 4 && data[0] == ' ' && data[1] == ' ' && data[2] == ' ' && data[3] == ' ' { return 4 } return 0 } func (p *Markdown) code(data []byte) int { var work bytes.Buffer i := 0 for i < len(data) { beg := i for i < len(data) && data[i] != '\n' { i++ } if i < len(data) && data[i] == '\n' { i++ } blankline := p.isEmpty(data[beg:i]) > 0 if pre := p.codePrefix(data[beg:i]); pre > 0 { beg += pre } else if !blankline { // non-empty, non-prefixed line breaks the pre i = beg break } // verbatim copy to the working buffer if blankline { work.WriteByte('\n') } else { work.Write(data[beg:i]) } } // trim all the \n off the end of work workbytes := work.Bytes() eol := len(workbytes) for eol > 0 && workbytes[eol-1] == '\n' { eol-- } if eol != len(workbytes) { work.Truncate(eol) } work.WriteByte('\n') block := p.addBlock(CodeBlock, work.Bytes()) // TODO: get rid of temp buffer block.IsFenced = false finalizeCodeBlock(block) return i } // returns unordered list item prefix func (p *Markdown) uliPrefix(data []byte) int { i := 0 // start with up to 3 spaces for i < len(data) && i < 3 && data[i] == ' ' { i++ } if i >= len(data)-1 { return 0 } // need one of {'*', '+', '-'} followed by a space or a tab if (data[i] != '*' && data[i] != '+' && data[i] != '-') || (data[i+1] != ' ' && data[i+1] != '\t') { return 0 } return i + 2 } // returns ordered list item prefix func (p *Markdown) oliPrefix(data []byte) int { i := 0 // start with up to 3 spaces for i < 3 && i < len(data) && data[i] == ' ' { i++ } // count the digits start := i for i < len(data) && data[i] >= '0' && data[i] <= '9' { i++ } if start == i || i >= len(data)-1 { return 0 } // we need >= 1 digits followed by a dot and a space or a tab if data[i] != '.' || !(data[i+1] == ' ' || data[i+1] == '\t') { return 0 } return i + 2 } // returns definition list item prefix func (p *Markdown) dliPrefix(data []byte) int { if len(data) < 2 { return 0 } i := 0 // need a ':' followed by a space or a tab if data[i] != ':' || !(data[i+1] == ' ' || data[i+1] == '\t') { return 0 } for i < len(data) && data[i] == ' ' { i++ } return i + 2 } // parse ordered or unordered list block func (p *Markdown) list(data []byte, flags ListType) int { i := 0 flags |= ListItemBeginningOfList block := p.addBlock(List, nil) block.ListFlags = flags block.Tight = true for i < len(data) { skip := p.listItem(data[i:], &flags) if flags&ListItemContainsBlock != 0 { block.ListData.Tight = false } i += skip if skip == 0 || flags&ListItemEndOfList != 0 { break } flags &= ^ListItemBeginningOfList } above := block.Parent finalizeList(block) p.tip = above return i } // Returns true if the list item is not the same type as its parent list func (p *Markdown) listTypeChanged(data []byte, flags *ListType) bool { if p.dliPrefix(data) > 0 && *flags&ListTypeDefinition == 0 { return true } else if p.oliPrefix(data) > 0 && *flags&ListTypeOrdered == 0 { return true } else if p.uliPrefix(data) > 0 && (*flags&ListTypeOrdered != 0 || *flags&ListTypeDefinition != 0) { return true } return false } // Returns true if block ends with a blank line, descending if needed // into lists and sublists. func endsWithBlankLine(block *Node) bool { // TODO: figure this out. Always false now. for block != nil { //if block.lastLineBlank { //return true //} t := block.Type if t == List || t == Item { block = block.LastChild } else { break } } return false } func finalizeList(block *Node) { block.open = false item := block.FirstChild for item != nil { // check for non-final list item ending with blank line: if endsWithBlankLine(item) && item.Next != nil { block.ListData.Tight = false break } // recurse into children of list item, to see if there are spaces // between any of them: subItem := item.FirstChild for subItem != nil { if endsWithBlankLine(subItem) && (item.Next != nil || subItem.Next != nil) { block.ListData.Tight = false break } subItem = subItem.Next } item = item.Next } } // Parse a single list item. // Assumes initial prefix is already removed if this is a sublist. func (p *Markdown) listItem(data []byte, flags *ListType) int { // keep track of the indentation of the first line itemIndent := 0 if data[0] == '\t' { itemIndent += 4 } else { for itemIndent < 3 && data[itemIndent] == ' ' { itemIndent++ } } var bulletChar byte = '*' i := p.uliPrefix(data) if i == 0 { i = p.oliPrefix(data) } else { bulletChar = data[i-2] } if i == 0 { i = p.dliPrefix(data) // reset definition term flag if i > 0 { *flags &= ^ListTypeTerm } } if i == 0 { // if in definition list, set term flag and continue if *flags&ListTypeDefinition != 0 { *flags |= ListTypeTerm } else { return 0 } } // skip leading whitespace on first line for i < len(data) && data[i] == ' ' { i++ } // find the end of the line line := i for i > 0 && i < len(data) && data[i-1] != '\n' { i++ } // get working buffer var raw bytes.Buffer // put the first line into the working buffer raw.Write(data[line:i]) line = i // process the following lines containsBlankLine := false sublist := 0 codeBlockMarker := "" gatherlines: for line < len(data) { i++ // find the end of this line for i < len(data) && data[i-1] != '\n' { i++ } // if it is an empty line, guess that it is part of this item // and move on to the next line if p.isEmpty(data[line:i]) > 0 { containsBlankLine = true line = i continue } // calculate the indentation indent := 0 indentIndex := 0 if data[line] == '\t' { indentIndex++ indent += 4 } else { for indent < 4 && line+indent < i && data[line+indent] == ' ' { indent++ indentIndex++ } } chunk := data[line+indentIndex : i] if p.extensions&FencedCode != 0 { // determine if in or out of codeblock // if in codeblock, ignore normal list processing _, marker := isFenceLine(chunk, nil, codeBlockMarker) if marker != "" { if codeBlockMarker == "" { // start of codeblock codeBlockMarker = marker } else { // end of codeblock. codeBlockMarker = "" } } // we are in a codeblock, write line, and continue if codeBlockMarker != "" || marker != "" { raw.Write(data[line+indentIndex : i]) line = i continue gatherlines } } // evaluate how this line fits in switch { // is this a nested list item? case (p.uliPrefix(chunk) > 0 && !p.isHRule(chunk)) || p.oliPrefix(chunk) > 0 || p.dliPrefix(chunk) > 0: // to be a nested list, it must be indented more // if not, it is either a different kind of list // or the next item in the same list if indent <= itemIndent { if p.listTypeChanged(chunk, flags) { *flags |= ListItemEndOfList } else if containsBlankLine { *flags |= ListItemContainsBlock } break gatherlines } if containsBlankLine { *flags |= ListItemContainsBlock } // is this the first item in the nested list? if sublist == 0 { sublist = raw.Len() } // is this a nested prefix heading? case p.isPrefixHeading(chunk): // if the heading is not indented, it is not nested in the list // and thus ends the list if containsBlankLine && indent < 4 { *flags |= ListItemEndOfList break gatherlines } *flags |= ListItemContainsBlock // anything following an empty line is only part // of this item if it is indented 4 spaces // (regardless of the indentation of the beginning of the item) case containsBlankLine && indent < 4: if *flags&ListTypeDefinition != 0 && i < len(data)-1 { // is the next item still a part of this list? next := i for next < len(data) && data[next] != '\n' { next++ } for next < len(data)-1 && data[next] == '\n' { next++ } if i < len(data)-1 && data[i] != ':' && data[next] != ':' { *flags |= ListItemEndOfList } } else { *flags |= ListItemEndOfList } break gatherlines // a blank line means this should be parsed as a block case containsBlankLine: raw.WriteByte('\n') *flags |= ListItemContainsBlock } // if this line was preceded by one or more blanks, // re-introduce the blank into the buffer if containsBlankLine { containsBlankLine = false raw.WriteByte('\n') } // add the line into the working buffer without prefix raw.Write(data[line+indentIndex : i]) line = i } rawBytes := raw.Bytes() block := p.addBlock(Item, nil) block.ListFlags = *flags block.Tight = false block.BulletChar = bulletChar block.Delimiter = '.' // Only '.' is possible in Markdown, but ')' will also be possible in CommonMark // render the contents of the list item if *flags&ListItemContainsBlock != 0 && *flags&ListTypeTerm == 0 { // intermediate render of block item, except for definition term if sublist > 0 { p.block(rawBytes[:sublist]) p.block(rawBytes[sublist:]) } else { p.block(rawBytes) } } else { // intermediate render of inline item if sublist > 0 { child := p.addChild(Paragraph, 0) child.content = rawBytes[:sublist] p.block(rawBytes[sublist:]) } else { child := p.addChild(Paragraph, 0) child.content = rawBytes } } return line } // render a single paragraph that has already been parsed out func (p *Markdown) renderParagraph(data []byte) { if len(data) == 0 { return } // trim leading spaces beg := 0 for data[beg] == ' ' { beg++ } end := len(data) // trim trailing newline if data[len(data)-1] == '\n' { end-- } // trim trailing spaces for end > beg && data[end-1] == ' ' { end-- } p.addBlock(Paragraph, data[beg:end]) } func (p *Markdown) paragraph(data []byte) int { // prev: index of 1st char of previous line // line: index of 1st char of current line // i: index of cursor/end of current line var prev, line, i int tabSize := TabSizeDefault if p.extensions&TabSizeEight != 0 { tabSize = TabSizeDouble } // keep going until we find something to mark the end of the paragraph for i < len(data) { // mark the beginning of the current line prev = line current := data[i:] line = i // did we find a reference or a footnote? If so, end a paragraph // preceding it and report that we have consumed up to the end of that // reference: if refEnd := isReference(p, current, tabSize); refEnd > 0 { p.renderParagraph(data[:i]) return i + refEnd } // did we find a blank line marking the end of the paragraph? if n := p.isEmpty(current); n > 0 { // did this blank line followed by a definition list item? if p.extensions&DefinitionLists != 0 { if i < len(data)-1 && data[i+1] == ':' { return p.list(data[prev:], ListTypeDefinition) } } p.renderParagraph(data[:i]) return i + n } // an underline under some text marks a heading, so our paragraph ended on prev line if i > 0 { if level := p.isUnderlinedHeading(current); level > 0 { // render the paragraph p.renderParagraph(data[:prev]) // ignore leading and trailing whitespace eol := i - 1 for prev < eol && data[prev] == ' ' { prev++ } for eol > prev && data[eol-1] == ' ' { eol-- } id := "" if p.extensions&AutoHeadingIDs != 0 { id = SanitizedAnchorName(string(data[prev:eol])) } block := p.addBlock(Heading, data[prev:eol]) block.Level = level block.HeadingID = id // find the end of the underline for i < len(data) && data[i] != '\n' { i++ } return i } } // if the next line starts a block of HTML, then the paragraph ends here if p.extensions&LaxHTMLBlocks != 0 { if data[i] == '<' && p.html(current, false) > 0 { // rewind to before the HTML block p.renderParagraph(data[:i]) return i } } // if there's a prefixed heading or a horizontal rule after this, paragraph is over if p.isPrefixHeading(current) || p.isHRule(current) { p.renderParagraph(data[:i]) return i } // if there's a fenced code block, paragraph is over if p.extensions&FencedCode != 0 { if p.fencedCodeBlock(current, false) > 0 { p.renderParagraph(data[:i]) return i } } // if there's a definition list item, prev line is a definition term if p.extensions&DefinitionLists != 0 { if p.dliPrefix(current) != 0 { ret := p.list(data[prev:], ListTypeDefinition) return ret } } // if there's a list after this, paragraph is over if p.extensions&NoEmptyLineBeforeBlock != 0 { if p.uliPrefix(current) != 0 || p.oliPrefix(current) != 0 || p.quotePrefix(current) != 0 || p.codePrefix(current) != 0 { p.renderParagraph(data[:i]) return i } } // otherwise, scan to the beginning of the next line nl := bytes.IndexByte(data[i:], '\n') if nl >= 0 { i += nl + 1 } else { i += len(data[i:]) } } p.renderParagraph(data[:i]) return i } func skipChar(data []byte, start int, char byte) int { i := start for i < len(data) && data[i] == char { i++ } return i } func skipUntilChar(text []byte, start int, char byte) int { i := start for i < len(text) && text[i] != char { i++ } return i } // SanitizedAnchorName returns a sanitized anchor name for the given text. // // It implements the algorithm specified in the package comment. func SanitizedAnchorName(text string) string { var anchorName []rune futureDash := false for _, r := range text { switch { case unicode.IsLetter(r) || unicode.IsNumber(r): if futureDash && len(anchorName) > 0 { anchorName = append(anchorName, '-') } futureDash = false anchorName = append(anchorName, unicode.ToLower(r)) default: futureDash = true } } return string(anchorName) } ================================================ FILE: tests/tools/vendor/github.com/russross/blackfriday/v2/doc.go ================================================ // Package blackfriday is a markdown processor. // // It translates plain text with simple formatting rules into an AST, which can // then be further processed to HTML (provided by Blackfriday itself) or other // formats (provided by the community). // // The simplest way to invoke Blackfriday is to call the Run function. It will // take a text input and produce a text output in HTML (or other format). // // A slightly more sophisticated way to use Blackfriday is to create a Markdown // processor and to call Parse, which returns a syntax tree for the input // document. You can leverage Blackfriday's parsing for content extraction from // markdown documents. You can assign a custom renderer and set various options // to the Markdown processor. // // If you're interested in calling Blackfriday from command line, see // https://github.com/russross/blackfriday-tool. // // # Sanitized Anchor Names // // Blackfriday includes an algorithm for creating sanitized anchor names // corresponding to a given input text. This algorithm is used to create // anchors for headings when AutoHeadingIDs extension is enabled. The // algorithm is specified below, so that other packages can create // compatible anchor names and links to those anchors. // // The algorithm iterates over the input text, interpreted as UTF-8, // one Unicode code point (rune) at a time. All runes that are letters (category L) // or numbers (category N) are considered valid characters. They are mapped to // lower case, and included in the output. All other runes are considered // invalid characters. Invalid characters that precede the first valid character, // as well as invalid character that follow the last valid character // are dropped completely. All other sequences of invalid characters // between two valid characters are replaced with a single dash character '-'. // // SanitizedAnchorName exposes this functionality, and can be used to // create compatible links to the anchor names generated by blackfriday. // This algorithm is also implemented in a small standalone package at // github.com/shurcooL/sanitized_anchor_name. It can be useful for clients // that want a small package and don't need full functionality of blackfriday. package blackfriday // NOTE: Keep Sanitized Anchor Name algorithm in sync with package // github.com/shurcooL/sanitized_anchor_name. // Otherwise, users of sanitized_anchor_name will get anchor names // that are incompatible with those generated by blackfriday. ================================================ FILE: tests/tools/vendor/github.com/russross/blackfriday/v2/entities.go ================================================ package blackfriday // Extracted from https://html.spec.whatwg.org/multipage/entities.json var entities = map[string]bool{ "Æ": true, "Æ": true, "&": true, "&": true, "Á": true, "Á": true, "Ă": true, "Â": true, "Â": true, "А": true, "𝔄": true, "À": true, "À": true, "Α": true, "Ā": true, "⩓": true, "Ą": true, "𝔸": true, "⁡": true, "Å": true, "Å": true, "𝒜": true, "≔": true, "Ã": true, "Ã": true, "Ä": true, "Ä": true, "∖": true, "⫧": true, "⌆": true, "Б": true, "∵": true, "ℬ": true, "Β": true, "𝔅": true, "𝔹": true, "˘": true, "ℬ": true, "≎": true, "Ч": true, "©": true, "©": true, "Ć": true, "⋒": true, "ⅅ": true, "ℭ": true, "Č": true, "Ç": true, "Ç": true, "Ĉ": true, "∰": true, "Ċ": true, "¸": true, "·": true, "ℭ": true, "Χ": true, "⊙": true, "⊖": true, "⊕": true, "⊗": true, "∲": true, "”": true, "’": true, "∷": true, "⩴": true, "≡": true, "∯": true, "∮": true, "ℂ": true, "∐": true, "∳": true, "⨯": true, "𝒞": true, "⋓": true, "≍": true, "ⅅ": true, "⤑": true, "Ђ": true, "Ѕ": true, "Џ": true, "‡": true, "↡": true, "⫤": true, "Ď": true, "Д": true, "∇": true, "Δ": true, "𝔇": true, "´": true, "˙": true, "˝": true, "`": true, "˜": true, "⋄": true, "ⅆ": true, "𝔻": true, "¨": true, "⃜": true, "≐": true, "∯": true, "¨": true, "⇓": true, "⇐": true, "⇔": true, "⫤": true, "⟸": true, "⟺": true, "⟹": true, "⇒": true, "⊨": true, "⇑": true, "⇕": true, "∥": true, "↓": true, "⤓": true, "⇵": true, "̑": true, "⥐": true, "⥞": true, "↽": true, "⥖": true, "⥟": true, "⇁": true, "⥗": true, "⊤": true, "↧": true, "⇓": true, "𝒟": true, "Đ": true, "Ŋ": true, "Ð": true, "Ð": true, "É": true, "É": true, "Ě": true, "Ê": true, "Ê": true, "Э": true, "Ė": true, "𝔈": true, "È": true, "È": true, "∈": true, "Ē": true, "◻": true, "▫": true, "Ę": true, "𝔼": true, "Ε": true, "⩵": true, "≂": true, "⇌": true, "ℰ": true, "⩳": true, "Η": true, "Ë": true, "Ë": true, "∃": true, "ⅇ": true, "Ф": true, "𝔉": true, "◼": true, "▪": true, "𝔽": true, "∀": true, "ℱ": true, "ℱ": true, "Ѓ": true, ">": true, ">": true, "Γ": true, "Ϝ": true, "Ğ": true, "Ģ": true, "Ĝ": true, "Г": true, "Ġ": true, "𝔊": true, "⋙": true, "𝔾": true, "≥": true, "⋛": true, "≧": true, "⪢": true, "≷": true, "⩾": true, "≳": true, "𝒢": true, "≫": true, "Ъ": true, "ˇ": true, "^": true, "Ĥ": true, "ℌ": true, "ℋ": true, "ℍ": true, "─": true, "ℋ": true, "Ħ": true, "≎": true, "≏": true, "Е": true, "IJ": true, "Ё": true, "Í": true, "Í": true, "Î": true, "Î": true, "И": true, "İ": true, "ℑ": true, "Ì": true, "Ì": true, "ℑ": true, "Ī": true, "ⅈ": true, "⇒": true, "∬": true, "∫": true, "⋂": true, "⁣": true, "⁢": true, "Į": true, "𝕀": true, "Ι": true, "ℐ": true, "Ĩ": true, "І": true, "Ï": true, "Ï": true, "Ĵ": true, "Й": true, "𝔍": true, "𝕁": true, "𝒥": true, "Ј": true, "Є": true, "Х": true, "Ќ": true, "Κ": true, "Ķ": true, "К": true, "𝔎": true, "𝕂": true, "𝒦": true, "Љ": true, "<": true, "<": true, "Ĺ": true, "Λ": true, "⟪": true, "ℒ": true, "↞": true, "Ľ": true, "Ļ": true, "Л": true, "⟨": true, "←": true, "⇤": true, "⇆": true, "⌈": true, "⟦": true, "⥡": true, "⇃": true, "⥙": true, "⌊": true, "↔": true, "⥎": true, "⊣": true, "↤": true, "⥚": true, "⊲": true, "⧏": true, "⊴": true, "⥑": true, "⥠": true, "↿": true, "⥘": true, "↼": true, "⥒": true, "⇐": true, "⇔": true, "⋚": true, "≦": true, "≶": true, "⪡": true, "⩽": true, "≲": true, "𝔏": true, "⋘": true, "⇚": true, "Ŀ": true, "⟵": true, "⟷": true, "⟶": true, "⟸": true, "⟺": true, "⟹": true, "𝕃": true, "↙": true, "↘": true, "ℒ": true, "↰": true, "Ł": true, "≪": true, "⤅": true, "М": true, " ": true, "ℳ": true, "𝔐": true, "∓": true, "𝕄": true, "ℳ": true, "Μ": true, "Њ": true, "Ń": true, "Ň": true, "Ņ": true, "Н": true, "​": true, "​": true, "​": true, "​": true, "≫": true, "≪": true, " ": true, "𝔑": true, "⁠": true, " ": true, "ℕ": true, "⫬": true, "≢": true, "≭": true, "∦": true, "∉": true, "≠": true, "≂̸": true, "∄": true, "≯": true, "≱": true, "≧̸": true, "≫̸": true, "≹": true, "⩾̸": true, "≵": true, "≎̸": true, "≏̸": true, "⋪": true, "⧏̸": true, "⋬": true, "≮": true, "≰": true, "≸": true, "≪̸": true, "⩽̸": true, "≴": true, "⪢̸": true, "⪡̸": true, "⊀": true, "⪯̸": true, "⋠": true, "∌": true, "⋫": true, "⧐̸": true, "⋭": true, "⊏̸": true, "⋢": true, "⊐̸": true, "⋣": true, "⊂⃒": true, "⊈": true, "⊁": true, "⪰̸": true, "⋡": true, "≿̸": true, "⊃⃒": true, "⊉": true, "≁": true, "≄": true, "≇": true, "≉": true, "∤": true, "𝒩": true, "Ñ": true, "Ñ": true, "Ν": true, "Œ": true, "Ó": true, "Ó": true, "Ô": true, "Ô": true, "О": true, "Ő": true, "𝔒": true, "Ò": true, "Ò": true, "Ō": true, "Ω": true, "Ο": true, "𝕆": true, "“": true, "‘": true, "⩔": true, "𝒪": true, "Ø": true, "Ø": true, "Õ": true, "Õ": true, "⨷": true, "Ö": true, "Ö": true, "‾": true, "⏞": true, "⎴": true, "⏜": true, "∂": true, "П": true, "𝔓": true, "Φ": true, "Π": true, "±": true, "ℌ": true, "ℙ": true, "⪻": true, "≺": true, "⪯": true, "≼": true, "≾": true, "″": true, "∏": true, "∷": true, "∝": true, "𝒫": true, "Ψ": true, """: true, """: true, "𝔔": true, "ℚ": true, "𝒬": true, "⤐": true, "®": true, "®": true, "Ŕ": true, "⟫": true, "↠": true, "⤖": true, "Ř": true, "Ŗ": true, "Р": true, "ℜ": true, "∋": true, "⇋": true, "⥯": true, "ℜ": true, "Ρ": true, "⟩": true, "→": true, "⇥": true, "⇄": true, "⌉": true, "⟧": true, "⥝": true, "⇂": true, "⥕": true, "⌋": true, "⊢": true, "↦": true, "⥛": true, "⊳": true, "⧐": true, "⊵": true, "⥏": true, "⥜": true, "↾": true, "⥔": true, "⇀": true, "⥓": true, "⇒": true, "ℝ": true, "⥰": true, "⇛": true, "ℛ": true, "↱": true, "⧴": true, "Щ": true, "Ш": true, "Ь": true, "Ś": true, "⪼": true, "Š": true, "Ş": true, "Ŝ": true, "С": true, "𝔖": true, "↓": true, "←": true, "→": true, "↑": true, "Σ": true, "∘": true, "𝕊": true, "√": true, "□": true, "⊓": true, "⊏": true, "⊑": true, "⊐": true, "⊒": true, "⊔": true, "𝒮": true, "⋆": true, "⋐": true, "⋐": true, "⊆": true, "≻": true, "⪰": true, "≽": true, "≿": true, "∋": true, "∑": true, "⋑": true, "⊃": true, "⊇": true, "⋑": true, "Þ": true, "Þ": true, "™": true, "Ћ": true, "Ц": true, " ": true, "Τ": true, "Ť": true, "Ţ": true, "Т": true, "𝔗": true, "∴": true, "Θ": true, "  ": true, " ": true, "∼": true, "≃": true, "≅": true, "≈": true, "𝕋": true, "⃛": true, "𝒯": true, "Ŧ": true, "Ú": true, "Ú": true, "↟": true, "⥉": true, "Ў": true, "Ŭ": true, "Û": true, "Û": true, "У": true, "Ű": true, "𝔘": true, "Ù": true, "Ù": true, "Ū": true, "_": true, "⏟": true, "⎵": true, "⏝": true, "⋃": true, "⊎": true, "Ų": true, "𝕌": true, "↑": true, "⤒": true, "⇅": true, "↕": true, "⥮": true, "⊥": true, "↥": true, "⇑": true, "⇕": true, "↖": true, "↗": true, "ϒ": true, "Υ": true, "Ů": true, "𝒰": true, "Ũ": true, "Ü": true, "Ü": true, "⊫": true, "⫫": true, "В": true, "⊩": true, "⫦": true, "⋁": true, "‖": true, "‖": true, "∣": true, "|": true, "❘": true, "≀": true, " ": true, "𝔙": true, "𝕍": true, "𝒱": true, "⊪": true, "Ŵ": true, "⋀": true, "𝔚": true, "𝕎": true, "𝒲": true, "𝔛": true, "Ξ": true, "𝕏": true, "𝒳": true, "Я": true, "Ї": true, "Ю": true, "Ý": true, "Ý": true, "Ŷ": true, "Ы": true, "𝔜": true, "𝕐": true, "𝒴": true, "Ÿ": true, "Ж": true, "Ź": true, "Ž": true, "З": true, "Ż": true, "​": true, "Ζ": true, "ℨ": true, "ℤ": true, "𝒵": true, "á": true, "á": true, "ă": true, "∾": true, "∾̳": true, "∿": true, "â": true, "â": true, "´": true, "´": true, "а": true, "æ": true, "æ": true, "⁡": true, "𝔞": true, "à": true, "à": true, "ℵ": true, "ℵ": true, "α": true, "ā": true, "⨿": true, "&": true, "&": true, "∧": true, "⩕": true, "⩜": true, "⩘": true, "⩚": true, "∠": true, "⦤": true, "∠": true, "∡": true, "⦨": true, "⦩": true, "⦪": true, "⦫": true, "⦬": true, "⦭": true, "⦮": true, "⦯": true, "∟": true, "⊾": true, "⦝": true, "∢": true, "Å": true, "⍼": true, "ą": true, "𝕒": true, "≈": true, "⩰": true, "⩯": true, "≊": true, "≋": true, "'": true, "≈": true, "≊": true, "å": true, "å": true, "𝒶": true, "*": true, "≈": true, "≍": true, "ã": true, "ã": true, "ä": true, "ä": true, "∳": true, "⨑": true, "⫭": true, "≌": true, "϶": true, "‵": true, "∽": true, "⋍": true, "⊽": true, "⌅": true, "⌅": true, "⎵": true, "⎶": true, "≌": true, "б": true, "„": true, "∵": true, "∵": true, "⦰": true, "϶": true, "ℬ": true, "β": true, "ℶ": true, "≬": true, "𝔟": true, "⋂": true, "◯": true, "⋃": true, "⨀": true, "⨁": true, "⨂": true, "⨆": true, "★": true, "▽": true, "△": true, "⨄": true, "⋁": true, "⋀": true, "⤍": true, "⧫": true, "▪": true, "▴": true, "▾": true, "◂": true, "▸": true, "␣": true, "▒": true, "░": true, "▓": true, "█": true, "=⃥": true, "≡⃥": true, "⌐": true, "𝕓": true, "⊥": true, "⊥": true, "⋈": true, "╗": true, "╔": true, "╖": true, "╓": true, "═": true, "╦": true, "╩": true, "╤": true, "╧": true, "╝": true, "╚": true, "╜": true, "╙": true, "║": true, "╬": true, "╣": true, "╠": true, "╫": true, "╢": true, "╟": true, "⧉": true, "╕": true, "╒": true, "┐": true, "┌": true, "─": true, "╥": true, "╨": true, "┬": true, "┴": true, "⊟": true, "⊞": true, "⊠": true, "╛": true, "╘": true, "┘": true, "└": true, "│": true, "╪": true, "╡": true, "╞": true, "┼": true, "┤": true, "├": true, "‵": true, "˘": true, "¦": true, "¦": true, "𝒷": true, "⁏": true, "∽": true, "⋍": true, "\": true, "⧅": true, "⟈": true, "•": true, "•": true, "≎": true, "⪮": true, "≏": true, "≏": true, "ć": true, "∩": true, "⩄": true, "⩉": true, "⩋": true, "⩇": true, "⩀": true, "∩︀": true, "⁁": true, "ˇ": true, "⩍": true, "č": true, "ç": true, "ç": true, "ĉ": true, "⩌": true, "⩐": true, "ċ": true, "¸": true, "¸": true, "⦲": true, "¢": true, "¢": true, "·": true, "𝔠": true, "ч": true, "✓": true, "✓": true, "χ": true, "○": true, "⧃": true, "ˆ": true, "≗": true, "↺": true, "↻": true, "®": true, "Ⓢ": true, "⊛": true, "⊚": true, "⊝": true, "≗": true, "⨐": true, "⫯": true, "⧂": true, "♣": true, "♣": true, ":": true, "≔": true, "≔": true, ",": true, "@": true, "∁": true, "∘": true, "∁": true, "ℂ": true, "≅": true, "⩭": true, "∮": true, "𝕔": true, "∐": true, "©": true, "©": true, "℗": true, "↵": true, "✗": true, "𝒸": true, "⫏": true, "⫑": true, "⫐": true, "⫒": true, "⋯": true, "⤸": true, "⤵": true, "⋞": true, "⋟": true, "↶": true, "⤽": true, "∪": true, "⩈": true, "⩆": true, "⩊": true, "⊍": true, "⩅": true, "∪︀": true, "↷": true, "⤼": true, "⋞": true, "⋟": true, "⋎": true, "⋏": true, "¤": true, "¤": true, "↶": true, "↷": true, "⋎": true, "⋏": true, "∲": true, "∱": true, "⌭": true, "⇓": true, "⥥": true, "†": true, "ℸ": true, "↓": true, "‐": true, "⊣": true, "⤏": true, "˝": true, "ď": true, "д": true, "ⅆ": true, "‡": true, "⇊": true, "⩷": true, "°": true, "°": true, "δ": true, "⦱": true, "⥿": true, "𝔡": true, "⇃": true, "⇂": true, "⋄": true, "⋄": true, "♦": true, "♦": true, "¨": true, "ϝ": true, "⋲": true, "÷": true, "÷": true, "÷": true, "⋇": true, "⋇": true, "ђ": true, "⌞": true, "⌍": true, "$": true, "𝕕": true, "˙": true, "≐": true, "≑": true, "∸": true, "∔": true, "⊡": true, "⌆": true, "↓": true, "⇊": true, "⇃": true, "⇂": true, "⤐": true, "⌟": true, "⌌": true, "𝒹": true, "ѕ": true, "⧶": true, "đ": true, "⋱": true, "▿": true, "▾": true, "⇵": true, "⥯": true, "⦦": true, "џ": true, "⟿": true, "⩷": true, "≑": true, "é": true, "é": true, "⩮": true, "ě": true, "≖": true, "ê": true, "ê": true, "≕": true, "э": true, "ė": true, "ⅇ": true, "≒": true, "𝔢": true, "⪚": true, "è": true, "è": true, "⪖": true, "⪘": true, "⪙": true, "⏧": true, "ℓ": true, "⪕": true, "⪗": true, "ē": true, "∅": true, "∅": true, "∅": true, " ": true, " ": true, " ": true, "ŋ": true, " ": true, "ę": true, "𝕖": true, "⋕": true, "⧣": true, "⩱": true, "ε": true, "ε": true, "ϵ": true, "≖": true, "≕": true, "≂": true, "⪖": true, "⪕": true, "=": true, "≟": true, "≡": true, "⩸": true, "⧥": true, "≓": true, "⥱": true, "ℯ": true, "≐": true, "≂": true, "η": true, "ð": true, "ð": true, "ë": true, "ë": true, "€": true, "!": true, "∃": true, "ℰ": true, "ⅇ": true, "≒": true, "ф": true, "♀": true, "ffi": true, "ff": true, "ffl": true, "𝔣": true, "fi": true, "fj": true, "♭": true, "fl": true, "▱": true, "ƒ": true, "𝕗": true, "∀": true, "⋔": true, "⫙": true, "⨍": true, "½": true, "½": true, "⅓": true, "¼": true, "¼": true, "⅕": true, "⅙": true, "⅛": true, "⅔": true, "⅖": true, "¾": true, "¾": true, "⅗": true, "⅜": true, "⅘": true, "⅚": true, "⅝": true, "⅞": true, "⁄": true, "⌢": true, "𝒻": true, "≧": true, "⪌": true, "ǵ": true, "γ": true, "ϝ": true, "⪆": true, "ğ": true, "ĝ": true, "г": true, "ġ": true, "≥": true, "⋛": true, "≥": true, "≧": true, "⩾": true, "⩾": true, "⪩": true, "⪀": true, "⪂": true, "⪄": true, "⋛︀": true, "⪔": true, "𝔤": true, "≫": true, "⋙": true, "ℷ": true, "ѓ": true, "≷": true, "⪒": true, "⪥": true, "⪤": true, "≩": true, "⪊": true, "⪊": true, "⪈": true, "⪈": true, "≩": true, "⋧": true, "𝕘": true, "`": true, "ℊ": true, "≳": true, "⪎": true, "⪐": true, ">": true, ">": true, "⪧": true, "⩺": true, "⋗": true, "⦕": true, "⩼": true, "⪆": true, "⥸": true, "⋗": true, "⋛": true, "⪌": true, "≷": true, "≳": true, "≩︀": true, "≩︀": true, "⇔": true, " ": true, "½": true, "ℋ": true, "ъ": true, "↔": true, "⥈": true, "↭": true, "ℏ": true, "ĥ": true, "♥": true, "♥": true, "…": true, "⊹": true, "𝔥": true, "⤥": true, "⤦": true, "⇿": true, "∻": true, "↩": true, "↪": true, "𝕙": true, "―": true, "𝒽": true, "ℏ": true, "ħ": true, "⁃": true, "‐": true, "í": true, "í": true, "⁣": true, "î": true, "î": true, "и": true, "е": true, "¡": true, "¡": true, "⇔": true, "𝔦": true, "ì": true, "ì": true, "ⅈ": true, "⨌": true, "∭": true, "⧜": true, "℩": true, "ij": true, "ī": true, "ℑ": true, "ℐ": true, "ℑ": true, "ı": true, "⊷": true, "Ƶ": true, "∈": true, "℅": true, "∞": true, "⧝": true, "ı": true, "∫": true, "⊺": true, "ℤ": true, "⊺": true, "⨗": true, "⨼": true, "ё": true, "į": true, "𝕚": true, "ι": true, "⨼": true, "¿": true, "¿": true, "𝒾": true, "∈": true, "⋹": true, "⋵": true, "⋴": true, "⋳": true, "∈": true, "⁢": true, "ĩ": true, "і": true, "ï": true, "ï": true, "ĵ": true, "й": true, "𝔧": true, "ȷ": true, "𝕛": true, "𝒿": true, "ј": true, "є": true, "κ": true, "ϰ": true, "ķ": true, "к": true, "𝔨": true, "ĸ": true, "х": true, "ќ": true, "𝕜": true, "𝓀": true, "⇚": true, "⇐": true, "⤛": true, "⤎": true, "≦": true, "⪋": true, "⥢": true, "ĺ": true, "⦴": true, "ℒ": true, "λ": true, "⟨": true, "⦑": true, "⟨": true, "⪅": true, "«": true, "«": true, "←": true, "⇤": true, "⤟": true, "⤝": true, "↩": true, "↫": true, "⤹": true, "⥳": true, "↢": true, "⪫": true, "⤙": true, "⪭": true, "⪭︀": true, "⤌": true, "❲": true, "{": true, "[": true, "⦋": true, "⦏": true, "⦍": true, "ľ": true, "ļ": true, "⌈": true, "{": true, "л": true, "⤶": true, "“": true, "„": true, "⥧": true, "⥋": true, "↲": true, "≤": true, "←": true, "↢": true, "↽": true, "↼": true, "⇇": true, "↔": true, "⇆": true, "⇋": true, "↭": true, "⋋": true, "⋚": true, "≤": true, "≦": true, "⩽": true, "⩽": true, "⪨": true, "⩿": true, "⪁": true, "⪃": true, "⋚︀": true, "⪓": true, "⪅": true, "⋖": true, "⋚": true, "⪋": true, "≶": true, "≲": true, "⥼": true, "⌊": true, "𝔩": true, "≶": true, "⪑": true, "↽": true, "↼": true, "⥪": true, "▄": true, "љ": true, "≪": true, "⇇": true, "⌞": true, "⥫": true, "◺": true, "ŀ": true, "⎰": true, "⎰": true, "≨": true, "⪉": true, "⪉": true, "⪇": true, "⪇": true, "≨": true, "⋦": true, "⟬": true, "⇽": true, "⟦": true, "⟵": true, "⟷": true, "⟼": true, "⟶": true, "↫": true, "↬": true, "⦅": true, "𝕝": true, "⨭": true, "⨴": true, "∗": true, "_": true, "◊": true, "◊": true, "⧫": true, "(": true, "⦓": true, "⇆": true, "⌟": true, "⇋": true, "⥭": true, "‎": true, "⊿": true, "‹": true, "𝓁": true, "↰": true, "≲": true, "⪍": true, "⪏": true, "[": true, "‘": true, "‚": true, "ł": true, "<": true, "<": true, "⪦": true, "⩹": true, "⋖": true, "⋋": true, "⋉": true, "⥶": true, "⩻": true, "⦖": true, "◃": true, "⊴": true, "◂": true, "⥊": true, "⥦": true, "≨︀": true, "≨︀": true, "∺": true, "¯": true, "¯": true, "♂": true, "✠": true, "✠": true, "↦": true, "↦": true, "↧": true, "↤": true, "↥": true, "▮": true, "⨩": true, "м": true, "—": true, "∡": true, "𝔪": true, "℧": true, "µ": true, "µ": true, "∣": true, "*": true, "⫰": true, "·": true, "·": true, "−": true, "⊟": true, "∸": true, "⨪": true, "⫛": true, "…": true, "∓": true, "⊧": true, "𝕞": true, "∓": true, "𝓂": true, "∾": true, "μ": true, "⊸": true, "⊸": true, "⋙̸": true, "≫⃒": true, "≫̸": true, "⇍": true, "⇎": true, "⋘̸": true, "≪⃒": true, "≪̸": true, "⇏": true, "⊯": true, "⊮": true, "∇": true, "ń": true, "∠⃒": true, "≉": true, "⩰̸": true, "≋̸": true, "ʼn": true, "≉": true, "♮": true, "♮": true, "ℕ": true, " ": true, " ": true, "≎̸": true, "≏̸": true, "⩃": true, "ň": true, "ņ": true, "≇": true, "⩭̸": true, "⩂": true, "н": true, "–": true, "≠": true, "⇗": true, "⤤": true, "↗": true, "↗": true, "≐̸": true, "≢": true, "⤨": true, "≂̸": true, "∄": true, "∄": true, "𝔫": true, "≧̸": true, "≱": true, "≱": true, "≧̸": true, "⩾̸": true, "⩾̸": true, "≵": true, "≯": true, "≯": true, "⇎": true, "↮": true, "⫲": true, "∋": true, "⋼": true, "⋺": true, "∋": true, "њ": true, "⇍": true, "≦̸": true, "↚": true, "‥": true, "≰": true, "↚": true, "↮": true, "≰": true, "≦̸": true, "⩽̸": true, "⩽̸": true, "≮": true, "≴": true, "≮": true, "⋪": true, "⋬": true, "∤": true, "𝕟": true, "¬": true, "¬": true, "∉": true, "⋹̸": true, "⋵̸": true, "∉": true, "⋷": true, "⋶": true, "∌": true, "∌": true, "⋾": true, "⋽": true, "∦": true, "∦": true, "⫽⃥": true, "∂̸": true, "⨔": true, "⊀": true, "⋠": true, "⪯̸": true, "⊀": true, "⪯̸": true, "⇏": true, "↛": true, "⤳̸": true, "↝̸": true, "↛": true, "⋫": true, "⋭": true, "⊁": true, "⋡": true, "⪰̸": true, "𝓃": true, "∤": true, "∦": true, "≁": true, "≄": true, "≄": true, "∤": true, "∦": true, "⋢": true, "⋣": true, "⊄": true, "⫅̸": true, "⊈": true, "⊂⃒": true, "⊈": true, "⫅̸": true, "⊁": true, "⪰̸": true, "⊅": true, "⫆̸": true, "⊉": true, "⊃⃒": true, "⊉": true, "⫆̸": true, "≹": true, "ñ": true, "ñ": true, "≸": true, "⋪": true, "⋬": true, "⋫": true, "⋭": true, "ν": true, "#": true, "№": true, " ": true, "⊭": true, "⤄": true, "≍⃒": true, "⊬": true, "≥⃒": true, ">⃒": true, "⧞": true, "⤂": true, "≤⃒": true, "<⃒": true, "⊴⃒": true, "⤃": true, "⊵⃒": true, "∼⃒": true, "⇖": true, "⤣": true, "↖": true, "↖": true, "⤧": true, "Ⓢ": true, "ó": true, "ó": true, "⊛": true, "⊚": true, "ô": true, "ô": true, "о": true, "⊝": true, "ő": true, "⨸": true, "⊙": true, "⦼": true, "œ": true, "⦿": true, "𝔬": true, "˛": true, "ò": true, "ò": true, "⧁": true, "⦵": true, "Ω": true, "∮": true, "↺": true, "⦾": true, "⦻": true, "‾": true, "⧀": true, "ō": true, "ω": true, "ο": true, "⦶": true, "⊖": true, "𝕠": true, "⦷": true, "⦹": true, "⊕": true, "∨": true, "↻": true, "⩝": true, "ℴ": true, "ℴ": true, "ª": true, "ª": true, "º": true, "º": true, "⊶": true, "⩖": true, "⩗": true, "⩛": true, "ℴ": true, "ø": true, "ø": true, "⊘": true, "õ": true, "õ": true, "⊗": true, "⨶": true, "ö": true, "ö": true, "⌽": true, "∥": true, "¶": true, "¶": true, "∥": true, "⫳": true, "⫽": true, "∂": true, "п": true, "%": true, ".": true, "‰": true, "⊥": true, "‱": true, "𝔭": true, "φ": true, "ϕ": true, "ℳ": true, "☎": true, "π": true, "⋔": true, "ϖ": true, "ℏ": true, "ℎ": true, "ℏ": true, "+": true, "⨣": true, "⊞": true, "⨢": true, "∔": true, "⨥": true, "⩲": true, "±": true, "±": true, "⨦": true, "⨧": true, "±": true, "⨕": true, "𝕡": true, "£": true, "£": true, "≺": true, "⪳": true, "⪷": true, "≼": true, "⪯": true, "≺": true, "⪷": true, "≼": true, "⪯": true, "⪹": true, "⪵": true, "⋨": true, "≾": true, "′": true, "ℙ": true, "⪵": true, "⪹": true, "⋨": true, "∏": true, "⌮": true, "⌒": true, "⌓": true, "∝": true, "∝": true, "≾": true, "⊰": true, "𝓅": true, "ψ": true, " ": true, "𝔮": true, "⨌": true, "𝕢": true, "⁗": true, "𝓆": true, "ℍ": true, "⨖": true, "?": true, "≟": true, """: true, """: true, "⇛": true, "⇒": true, "⤜": true, "⤏": true, "⥤": true, "∽̱": true, "ŕ": true, "√": true, "⦳": true, "⟩": true, "⦒": true, "⦥": true, "⟩": true, "»": true, "»": true, "→": true, "⥵": true, "⇥": true, "⤠": true, "⤳": true, "⤞": true, "↪": true, "↬": true, "⥅": true, "⥴": true, "↣": true, "↝": true, "⤚": true, "∶": true, "ℚ": true, "⤍": true, "❳": true, "}": true, "]": true, "⦌": true, "⦎": true, "⦐": true, "ř": true, "ŗ": true, "⌉": true, "}": true, "р": true, "⤷": true, "⥩": true, "”": true, "”": true, "↳": true, "ℜ": true, "ℛ": true, "ℜ": true, "ℝ": true, "▭": true, "®": true, "®": true, "⥽": true, "⌋": true, "𝔯": true, "⇁": true, "⇀": true, "⥬": true, "ρ": true, "ϱ": true, "→": true, "↣": true, "⇁": true, "⇀": true, "⇄": true, "⇌": true, "⇉": true, "↝": true, "⋌": true, "˚": true, "≓": true, "⇄": true, "⇌": true, "‏": true, "⎱": true, "⎱": true, "⫮": true, "⟭": true, "⇾": true, "⟧": true, "⦆": true, "𝕣": true, "⨮": true, "⨵": true, ")": true, "⦔": true, "⨒": true, "⇉": true, "›": true, "𝓇": true, "↱": true, "]": true, "’": true, "’": true, "⋌": true, "⋊": true, "▹": true, "⊵": true, "▸": true, "⧎": true, "⥨": true, "℞": true, "ś": true, "‚": true, "≻": true, "⪴": true, "⪸": true, "š": true, "≽": true, "⪰": true, "ş": true, "ŝ": true, "⪶": true, "⪺": true, "⋩": true, "⨓": true, "≿": true, "с": true, "⋅": true, "⊡": true, "⩦": true, "⇘": true, "⤥": true, "↘": true, "↘": true, "§": true, "§": true, ";": true, "⤩": true, "∖": true, "∖": true, "✶": true, "𝔰": true, "⌢": true, "♯": true, "щ": true, "ш": true, "∣": true, "∥": true, "­": true, "­": true, "σ": true, "ς": true, "ς": true, "∼": true, "⩪": true, "≃": true, "≃": true, "⪞": true, "⪠": true, "⪝": true, "⪟": true, "≆": true, "⨤": true, "⥲": true, "←": true, "∖": true, "⨳": true, "⧤": true, "∣": true, "⌣": true, "⪪": true, "⪬": true, "⪬︀": true, "ь": true, "/": true, "⧄": true, "⌿": true, "𝕤": true, "♠": true, "♠": true, "∥": true, "⊓": true, "⊓︀": true, "⊔": true, "⊔︀": true, "⊏": true, "⊑": true, "⊏": true, "⊑": true, "⊐": true, "⊒": true, "⊐": true, "⊒": true, "□": true, "□": true, "▪": true, "▪": true, "→": true, "𝓈": true, "∖": true, "⌣": true, "⋆": true, "☆": true, "★": true, "ϵ": true, "ϕ": true, "¯": true, "⊂": true, "⫅": true, "⪽": true, "⊆": true, "⫃": true, "⫁": true, "⫋": true, "⊊": true, "⪿": true, "⥹": true, "⊂": true, "⊆": true, "⫅": true, "⊊": true, "⫋": true, "⫇": true, "⫕": true, "⫓": true, "≻": true, "⪸": true, "≽": true, "⪰": true, "⪺": true, "⪶": true, "⋩": true, "≿": true, "∑": true, "♪": true, "¹": true, "¹": true, "²": true, "²": true, "³": true, "³": true, "⊃": true, "⫆": true, "⪾": true, "⫘": true, "⊇": true, "⫄": true, "⟉": true, "⫗": true, "⥻": true, "⫂": true, "⫌": true, "⊋": true, "⫀": true, "⊃": true, "⊇": true, "⫆": true, "⊋": true, "⫌": true, "⫈": true, "⫔": true, "⫖": true, "⇙": true, "⤦": true, "↙": true, "↙": true, "⤪": true, "ß": true, "ß": true, "⌖": true, "τ": true, "⎴": true, "ť": true, "ţ": true, "т": true, "⃛": true, "⌕": true, "𝔱": true, "∴": true, "∴": true, "θ": true, "ϑ": true, "ϑ": true, "≈": true, "∼": true, " ": true, "≈": true, "∼": true, "þ": true, "þ": true, "˜": true, "×": true, "×": true, "⊠": true, "⨱": true, "⨰": true, "∭": true, "⤨": true, "⊤": true, "⌶": true, "⫱": true, "𝕥": true, "⫚": true, "⤩": true, "‴": true, "™": true, "▵": true, "▿": true, "◃": true, "⊴": true, "≜": true, "▹": true, "⊵": true, "◬": true, "≜": true, "⨺": true, "⨹": true, "⧍": true, "⨻": true, "⏢": true, "𝓉": true, "ц": true, "ћ": true, "ŧ": true, "≬": true, "↞": true, "↠": true, "⇑": true, "⥣": true, "ú": true, "ú": true, "↑": true, "ў": true, "ŭ": true, "û": true, "û": true, "у": true, "⇅": true, "ű": true, "⥮": true, "⥾": true, "𝔲": true, "ù": true, "ù": true, "↿": true, "↾": true, "▀": true, "⌜": true, "⌜": true, "⌏": true, "◸": true, "ū": true, "¨": true, "¨": true, "ų": true, "𝕦": true, "↑": true, "↕": true, "↿": true, "↾": true, "⊎": true, "υ": true, "ϒ": true, "υ": true, "⇈": true, "⌝": true, "⌝": true, "⌎": true, "ů": true, "◹": true, "𝓊": true, "⋰": true, "ũ": true, "▵": true, "▴": true, "⇈": true, "ü": true, "ü": true, "⦧": true, "⇕": true, "⫨": true, "⫩": true, "⊨": true, "⦜": true, "ϵ": true, "ϰ": true, "∅": true, "ϕ": true, "ϖ": true, "∝": true, "↕": true, "ϱ": true, "ς": true, "⊊︀": true, "⫋︀": true, "⊋︀": true, "⫌︀": true, "ϑ": true, "⊲": true, "⊳": true, "в": true, "⊢": true, "∨": true, "⊻": true, "≚": true, "⋮": true, "|": true, "|": true, "𝔳": true, "⊲": true, "⊂⃒": true, "⊃⃒": true, "𝕧": true, "∝": true, "⊳": true, "𝓋": true, "⫋︀": true, "⊊︀": true, "⫌︀": true, "⊋︀": true, "⦚": true, "ŵ": true, "⩟": true, "∧": true, "≙": true, "℘": true, "𝔴": true, "𝕨": true, "℘": true, "≀": true, "≀": true, "𝓌": true, "⋂": true, "◯": true, "⋃": true, "▽": true, "𝔵": true, "⟺": true, "⟷": true, "ξ": true, "⟸": true, "⟵": true, "⟼": true, "⋻": true, "⨀": true, "𝕩": true, "⨁": true, "⨂": true, "⟹": true, "⟶": true, "𝓍": true, "⨆": true, "⨄": true, "△": true, "⋁": true, "⋀": true, "ý": true, "ý": true, "я": true, "ŷ": true, "ы": true, "¥": true, "¥": true, "𝔶": true, "ї": true, "𝕪": true, "𝓎": true, "ю": true, "ÿ": true, "ÿ": true, "ź": true, "ž": true, "з": true, "ż": true, "ℨ": true, "ζ": true, "𝔷": true, "ж": true, "⇝": true, "𝕫": true, "𝓏": true, "‍": true, "‌": true, } ================================================ FILE: tests/tools/vendor/github.com/russross/blackfriday/v2/esc.go ================================================ package blackfriday import ( "html" "io" ) var htmlEscaper = [256][]byte{ '&': []byte("&"), '<': []byte("<"), '>': []byte(">"), '"': []byte("""), } func escapeHTML(w io.Writer, s []byte) { escapeEntities(w, s, false) } func escapeAllHTML(w io.Writer, s []byte) { escapeEntities(w, s, true) } func escapeEntities(w io.Writer, s []byte, escapeValidEntities bool) { var start, end int for end < len(s) { escSeq := htmlEscaper[s[end]] if escSeq != nil { isEntity, entityEnd := nodeIsEntity(s, end) if isEntity && !escapeValidEntities { w.Write(s[start : entityEnd+1]) start = entityEnd + 1 } else { w.Write(s[start:end]) w.Write(escSeq) start = end + 1 } } end++ } if start < len(s) && end <= len(s) { w.Write(s[start:end]) } } func nodeIsEntity(s []byte, end int) (isEntity bool, endEntityPos int) { isEntity = false endEntityPos = end + 1 if s[end] == '&' { for endEntityPos < len(s) { if s[endEntityPos] == ';' { if entities[string(s[end:endEntityPos+1])] { isEntity = true break } } if !isalnum(s[endEntityPos]) && s[endEntityPos] != '&' && s[endEntityPos] != '#' { break } endEntityPos++ } } return isEntity, endEntityPos } func escLink(w io.Writer, text []byte) { unesc := html.UnescapeString(string(text)) escapeHTML(w, []byte(unesc)) } ================================================ FILE: tests/tools/vendor/github.com/russross/blackfriday/v2/html.go ================================================ // // Blackfriday Markdown Processor // Available at http://github.com/russross/blackfriday // // Copyright © 2011 Russ Ross . // Distributed under the Simplified BSD License. // See README.md for details. // // // // HTML rendering backend // // package blackfriday import ( "bytes" "fmt" "io" "regexp" "strings" ) // HTMLFlags control optional behavior of HTML renderer. type HTMLFlags int // HTML renderer configuration options. const ( HTMLFlagsNone HTMLFlags = 0 SkipHTML HTMLFlags = 1 << iota // Skip preformatted HTML blocks SkipImages // Skip embedded images SkipLinks // Skip all links Safelink // Only link to trusted protocols NofollowLinks // Only link with rel="nofollow" NoreferrerLinks // Only link with rel="noreferrer" NoopenerLinks // Only link with rel="noopener" HrefTargetBlank // Add a blank target CompletePage // Generate a complete HTML page UseXHTML // Generate XHTML output instead of HTML FootnoteReturnLinks // Generate a link at the end of a footnote to return to the source Smartypants // Enable smart punctuation substitutions SmartypantsFractions // Enable smart fractions (with Smartypants) SmartypantsDashes // Enable smart dashes (with Smartypants) SmartypantsLatexDashes // Enable LaTeX-style dashes (with Smartypants) SmartypantsAngledQuotes // Enable angled double quotes (with Smartypants) for double quotes rendering SmartypantsQuotesNBSP // Enable « French guillemets » (with Smartypants) TOC // Generate a table of contents ) var ( htmlTagRe = regexp.MustCompile("(?i)^" + htmlTag) ) const ( htmlTag = "(?:" + openTag + "|" + closeTag + "|" + htmlComment + "|" + processingInstruction + "|" + declaration + "|" + cdata + ")" closeTag = "]" openTag = "<" + tagName + attribute + "*" + "\\s*/?>" attribute = "(?:" + "\\s+" + attributeName + attributeValueSpec + "?)" attributeValue = "(?:" + unquotedValue + "|" + singleQuotedValue + "|" + doubleQuotedValue + ")" attributeValueSpec = "(?:" + "\\s*=" + "\\s*" + attributeValue + ")" attributeName = "[a-zA-Z_:][a-zA-Z0-9:._-]*" cdata = "" declaration = "]*>" doubleQuotedValue = "\"[^\"]*\"" htmlComment = "|" processingInstruction = "[<][?].*?[?][>]" singleQuotedValue = "'[^']*'" tagName = "[A-Za-z][A-Za-z0-9-]*" unquotedValue = "[^\"'=<>`\\x00-\\x20]+" ) // HTMLRendererParameters is a collection of supplementary parameters tweaking // the behavior of various parts of HTML renderer. type HTMLRendererParameters struct { // Prepend this text to each relative URL. AbsolutePrefix string // Add this text to each footnote anchor, to ensure uniqueness. FootnoteAnchorPrefix string // Show this text inside the tag for a footnote return link, if the // HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string // [return] is used. FootnoteReturnLinkContents string // If set, add this text to the front of each Heading ID, to ensure // uniqueness. HeadingIDPrefix string // If set, add this text to the back of each Heading ID, to ensure uniqueness. HeadingIDSuffix string // Increase heading levels: if the offset is 1,

becomes

etc. // Negative offset is also valid. // Resulting levels are clipped between 1 and 6. HeadingLevelOffset int Title string // Document title (used if CompletePage is set) CSS string // Optional CSS file URL (used if CompletePage is set) Icon string // Optional icon file URL (used if CompletePage is set) Flags HTMLFlags // Flags allow customizing this renderer's behavior } // HTMLRenderer is a type that implements the Renderer interface for HTML output. // // Do not create this directly, instead use the NewHTMLRenderer function. type HTMLRenderer struct { HTMLRendererParameters closeTag string // how to end singleton tags: either " />" or ">" // Track heading IDs to prevent ID collision in a single generation. headingIDs map[string]int lastOutputLen int disableTags int sr *SPRenderer } const ( xhtmlClose = " />" htmlClose = ">" ) // NewHTMLRenderer creates and configures an HTMLRenderer object, which // satisfies the Renderer interface. func NewHTMLRenderer(params HTMLRendererParameters) *HTMLRenderer { // configure the rendering engine closeTag := htmlClose if params.Flags&UseXHTML != 0 { closeTag = xhtmlClose } if params.FootnoteReturnLinkContents == "" { // U+FE0E is VARIATION SELECTOR-15. // It suppresses automatic emoji presentation of the preceding // U+21A9 LEFTWARDS ARROW WITH HOOK on iOS and iPadOS. params.FootnoteReturnLinkContents = "↩\ufe0e" } return &HTMLRenderer{ HTMLRendererParameters: params, closeTag: closeTag, headingIDs: make(map[string]int), sr: NewSmartypantsRenderer(params.Flags), } } func isHTMLTag(tag []byte, tagname string) bool { found, _ := findHTMLTagPos(tag, tagname) return found } // Look for a character, but ignore it when it's in any kind of quotes, it // might be JavaScript func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int { inSingleQuote := false inDoubleQuote := false inGraveQuote := false i := start for i < len(html) { switch { case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote: return i case html[i] == '\'': inSingleQuote = !inSingleQuote case html[i] == '"': inDoubleQuote = !inDoubleQuote case html[i] == '`': inGraveQuote = !inGraveQuote } i++ } return start } func findHTMLTagPos(tag []byte, tagname string) (bool, int) { i := 0 if i < len(tag) && tag[0] != '<' { return false, -1 } i++ i = skipSpace(tag, i) if i < len(tag) && tag[i] == '/' { i++ } i = skipSpace(tag, i) j := 0 for ; i < len(tag); i, j = i+1, j+1 { if j >= len(tagname) { break } if strings.ToLower(string(tag[i]))[0] != tagname[j] { return false, -1 } } if i == len(tag) { return false, -1 } rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>') if rightAngle >= i { return true, rightAngle } return false, -1 } func skipSpace(tag []byte, i int) int { for i < len(tag) && isspace(tag[i]) { i++ } return i } func isRelativeLink(link []byte) (yes bool) { // a tag begin with '#' if link[0] == '#' { return true } // link begin with '/' but not '//', the second maybe a protocol relative link if len(link) >= 2 && link[0] == '/' && link[1] != '/' { return true } // only the root '/' if len(link) == 1 && link[0] == '/' { return true } // current directory : begin with "./" if bytes.HasPrefix(link, []byte("./")) { return true } // parent directory : begin with "../" if bytes.HasPrefix(link, []byte("../")) { return true } return false } func (r *HTMLRenderer) ensureUniqueHeadingID(id string) string { for count, found := r.headingIDs[id]; found; count, found = r.headingIDs[id] { tmp := fmt.Sprintf("%s-%d", id, count+1) if _, tmpFound := r.headingIDs[tmp]; !tmpFound { r.headingIDs[id] = count + 1 id = tmp } else { id = id + "-1" } } if _, found := r.headingIDs[id]; !found { r.headingIDs[id] = 0 } return id } func (r *HTMLRenderer) addAbsPrefix(link []byte) []byte { if r.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' { newDest := r.AbsolutePrefix if link[0] != '/' { newDest += "/" } newDest += string(link) return []byte(newDest) } return link } func appendLinkAttrs(attrs []string, flags HTMLFlags, link []byte) []string { if isRelativeLink(link) { return attrs } val := []string{} if flags&NofollowLinks != 0 { val = append(val, "nofollow") } if flags&NoreferrerLinks != 0 { val = append(val, "noreferrer") } if flags&NoopenerLinks != 0 { val = append(val, "noopener") } if flags&HrefTargetBlank != 0 { attrs = append(attrs, "target=\"_blank\"") } if len(val) == 0 { return attrs } attr := fmt.Sprintf("rel=%q", strings.Join(val, " ")) return append(attrs, attr) } func isMailto(link []byte) bool { return bytes.HasPrefix(link, []byte("mailto:")) } func needSkipLink(flags HTMLFlags, dest []byte) bool { if flags&SkipLinks != 0 { return true } return flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest) } func isSmartypantable(node *Node) bool { pt := node.Parent.Type return pt != Link && pt != CodeBlock && pt != Code } func appendLanguageAttr(attrs []string, info []byte) []string { if len(info) == 0 { return attrs } endOfLang := bytes.IndexAny(info, "\t ") if endOfLang < 0 { endOfLang = len(info) } return append(attrs, fmt.Sprintf("class=\"language-%s\"", info[:endOfLang])) } func (r *HTMLRenderer) tag(w io.Writer, name []byte, attrs []string) { w.Write(name) if len(attrs) > 0 { w.Write(spaceBytes) w.Write([]byte(strings.Join(attrs, " "))) } w.Write(gtBytes) r.lastOutputLen = 1 } func footnoteRef(prefix string, node *Node) []byte { urlFrag := prefix + string(slugify(node.Destination)) anchor := fmt.Sprintf(`%d`, urlFrag, node.NoteID) return []byte(fmt.Sprintf(`%s`, urlFrag, anchor)) } func footnoteItem(prefix string, slug []byte) []byte { return []byte(fmt.Sprintf(`
  • `, prefix, slug)) } func footnoteReturnLink(prefix, returnLink string, slug []byte) []byte { const format = ` %s` return []byte(fmt.Sprintf(format, prefix, slug, returnLink)) } func itemOpenCR(node *Node) bool { if node.Prev == nil { return false } ld := node.Parent.ListData return !ld.Tight && ld.ListFlags&ListTypeDefinition == 0 } func skipParagraphTags(node *Node) bool { grandparent := node.Parent.Parent if grandparent == nil || grandparent.Type != List { return false } tightOrTerm := grandparent.Tight || node.Parent.ListFlags&ListTypeTerm != 0 return grandparent.Type == List && tightOrTerm } func cellAlignment(align CellAlignFlags) string { switch align { case TableAlignmentLeft: return "left" case TableAlignmentRight: return "right" case TableAlignmentCenter: return "center" default: return "" } } func (r *HTMLRenderer) out(w io.Writer, text []byte) { if r.disableTags > 0 { w.Write(htmlTagRe.ReplaceAll(text, []byte{})) } else { w.Write(text) } r.lastOutputLen = len(text) } func (r *HTMLRenderer) cr(w io.Writer) { if r.lastOutputLen > 0 { r.out(w, nlBytes) } } var ( nlBytes = []byte{'\n'} gtBytes = []byte{'>'} spaceBytes = []byte{' '} ) var ( brTag = []byte("
    ") brXHTMLTag = []byte("
    ") emTag = []byte("") emCloseTag = []byte("") strongTag = []byte("") strongCloseTag = []byte("") delTag = []byte("") delCloseTag = []byte("") ttTag = []byte("") ttCloseTag = []byte("") aTag = []byte("") preTag = []byte("
    ")
    	preCloseTag        = []byte("
    ") codeTag = []byte("") codeCloseTag = []byte("") pTag = []byte("

    ") pCloseTag = []byte("

    ") blockquoteTag = []byte("
    ") blockquoteCloseTag = []byte("
    ") hrTag = []byte("
    ") hrXHTMLTag = []byte("
    ") ulTag = []byte("
      ") ulCloseTag = []byte("
    ") olTag = []byte("
      ") olCloseTag = []byte("
    ") dlTag = []byte("
    ") dlCloseTag = []byte("
    ") liTag = []byte("
  • ") liCloseTag = []byte("
  • ") ddTag = []byte("
    ") ddCloseTag = []byte("
    ") dtTag = []byte("
    ") dtCloseTag = []byte("
    ") tableTag = []byte("") tableCloseTag = []byte("
    ") tdTag = []byte("") thTag = []byte("") theadTag = []byte("") theadCloseTag = []byte("") tbodyTag = []byte("") tbodyCloseTag = []byte("") trTag = []byte("") trCloseTag = []byte("") h1Tag = []byte("") h2Tag = []byte("") h3Tag = []byte("") h4Tag = []byte("") h5Tag = []byte("") h6Tag = []byte("") footnotesDivBytes = []byte("\n
    \n\n") footnotesCloseDivBytes = []byte("\n
    \n") ) func headingTagsFromLevel(level int) ([]byte, []byte) { if level <= 1 { return h1Tag, h1CloseTag } switch level { case 2: return h2Tag, h2CloseTag case 3: return h3Tag, h3CloseTag case 4: return h4Tag, h4CloseTag case 5: return h5Tag, h5CloseTag } return h6Tag, h6CloseTag } func (r *HTMLRenderer) outHRTag(w io.Writer) { if r.Flags&UseXHTML == 0 { r.out(w, hrTag) } else { r.out(w, hrXHTMLTag) } } // RenderNode is a default renderer of a single node of a syntax tree. For // block nodes it will be called twice: first time with entering=true, second // time with entering=false, so that it could know when it's working on an open // tag and when on close. It writes the result to w. // // The return value is a way to tell the calling walker to adjust its walk // pattern: e.g. it can terminate the traversal by returning Terminate. Or it // can ask the walker to skip a subtree of this node by returning SkipChildren. // The typical behavior is to return GoToNext, which asks for the usual // traversal to the next node. func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkStatus { attrs := []string{} switch node.Type { case Text: if r.Flags&Smartypants != 0 { var tmp bytes.Buffer escapeHTML(&tmp, node.Literal) r.sr.Process(w, tmp.Bytes()) } else { if node.Parent.Type == Link { escLink(w, node.Literal) } else { escapeHTML(w, node.Literal) } } case Softbreak: r.cr(w) // TODO: make it configurable via out(renderer.softbreak) case Hardbreak: if r.Flags&UseXHTML == 0 { r.out(w, brTag) } else { r.out(w, brXHTMLTag) } r.cr(w) case Emph: if entering { r.out(w, emTag) } else { r.out(w, emCloseTag) } case Strong: if entering { r.out(w, strongTag) } else { r.out(w, strongCloseTag) } case Del: if entering { r.out(w, delTag) } else { r.out(w, delCloseTag) } case HTMLSpan: if r.Flags&SkipHTML != 0 { break } r.out(w, node.Literal) case Link: // mark it but don't link it if it is not a safe link: no smartypants dest := node.LinkData.Destination if needSkipLink(r.Flags, dest) { if entering { r.out(w, ttTag) } else { r.out(w, ttCloseTag) } } else { if entering { dest = r.addAbsPrefix(dest) var hrefBuf bytes.Buffer hrefBuf.WriteString("href=\"") escLink(&hrefBuf, dest) hrefBuf.WriteByte('"') attrs = append(attrs, hrefBuf.String()) if node.NoteID != 0 { r.out(w, footnoteRef(r.FootnoteAnchorPrefix, node)) break } attrs = appendLinkAttrs(attrs, r.Flags, dest) if len(node.LinkData.Title) > 0 { var titleBuff bytes.Buffer titleBuff.WriteString("title=\"") escapeHTML(&titleBuff, node.LinkData.Title) titleBuff.WriteByte('"') attrs = append(attrs, titleBuff.String()) } r.tag(w, aTag, attrs) } else { if node.NoteID != 0 { break } r.out(w, aCloseTag) } } case Image: if r.Flags&SkipImages != 0 { return SkipChildren } if entering { dest := node.LinkData.Destination dest = r.addAbsPrefix(dest) if r.disableTags == 0 { //if options.safe && potentiallyUnsafe(dest) { //out(w, ``)
				//} else {
				r.out(w, []byte(`<img src=`)) } } case Code: r.out(w, codeTag) escapeAllHTML(w, node.Literal) r.out(w, codeCloseTag) case Document: break case Paragraph: if skipParagraphTags(node) { break } if entering { // TODO: untangle this clusterfuck about when the newlines need // to be added and when not. if node.Prev != nil { switch node.Prev.Type { case HTMLBlock, List, Paragraph, Heading, CodeBlock, BlockQuote, HorizontalRule: r.cr(w) } } if node.Parent.Type == BlockQuote && node.Prev == nil { r.cr(w) } r.out(w, pTag) } else { r.out(w, pCloseTag) if !(node.Parent.Type == Item && node.Next == nil) { r.cr(w) } } case BlockQuote: if entering { r.cr(w) r.out(w, blockquoteTag) } else { r.out(w, blockquoteCloseTag) r.cr(w) } case HTMLBlock: if r.Flags&SkipHTML != 0 { break } r.cr(w) r.out(w, node.Literal) r.cr(w) case Heading: headingLevel := r.HTMLRendererParameters.HeadingLevelOffset + node.Level openTag, closeTag := headingTagsFromLevel(headingLevel) if entering { if node.IsTitleblock { attrs = append(attrs, `class="title"`) } if node.HeadingID != "" { id := r.ensureUniqueHeadingID(node.HeadingID) if r.HeadingIDPrefix != "" { id = r.HeadingIDPrefix + id } if r.HeadingIDSuffix != "" { id = id + r.HeadingIDSuffix } attrs = append(attrs, fmt.Sprintf(`id="%s"`, id)) } r.cr(w) r.tag(w, openTag, attrs) } else { r.out(w, closeTag) if !(node.Parent.Type == Item && node.Next == nil) { r.cr(w) } } case HorizontalRule: r.cr(w) r.outHRTag(w) r.cr(w) case List: openTag := ulTag closeTag := ulCloseTag if node.ListFlags&ListTypeOrdered != 0 { openTag = olTag closeTag = olCloseTag } if node.ListFlags&ListTypeDefinition != 0 { openTag = dlTag closeTag = dlCloseTag } if entering { if node.IsFootnotesList { r.out(w, footnotesDivBytes) r.outHRTag(w) r.cr(w) } r.cr(w) if node.Parent.Type == Item && node.Parent.Parent.Tight { r.cr(w) } r.tag(w, openTag[:len(openTag)-1], attrs) r.cr(w) } else { r.out(w, closeTag) //cr(w) //if node.parent.Type != Item { // cr(w) //} if node.Parent.Type == Item && node.Next != nil { r.cr(w) } if node.Parent.Type == Document || node.Parent.Type == BlockQuote { r.cr(w) } if node.IsFootnotesList { r.out(w, footnotesCloseDivBytes) } } case Item: openTag := liTag closeTag := liCloseTag if node.ListFlags&ListTypeDefinition != 0 { openTag = ddTag closeTag = ddCloseTag } if node.ListFlags&ListTypeTerm != 0 { openTag = dtTag closeTag = dtCloseTag } if entering { if itemOpenCR(node) { r.cr(w) } if node.ListData.RefLink != nil { slug := slugify(node.ListData.RefLink) r.out(w, footnoteItem(r.FootnoteAnchorPrefix, slug)) break } r.out(w, openTag) } else { if node.ListData.RefLink != nil { slug := slugify(node.ListData.RefLink) if r.Flags&FootnoteReturnLinks != 0 { r.out(w, footnoteReturnLink(r.FootnoteAnchorPrefix, r.FootnoteReturnLinkContents, slug)) } } r.out(w, closeTag) r.cr(w) } case CodeBlock: attrs = appendLanguageAttr(attrs, node.Info) r.cr(w) r.out(w, preTag) r.tag(w, codeTag[:len(codeTag)-1], attrs) escapeAllHTML(w, node.Literal) r.out(w, codeCloseTag) r.out(w, preCloseTag) if node.Parent.Type != Item { r.cr(w) } case Table: if entering { r.cr(w) r.out(w, tableTag) } else { r.out(w, tableCloseTag) r.cr(w) } case TableCell: openTag := tdTag closeTag := tdCloseTag if node.IsHeader { openTag = thTag closeTag = thCloseTag } if entering { align := cellAlignment(node.Align) if align != "" { attrs = append(attrs, fmt.Sprintf(`align="%s"`, align)) } if node.Prev == nil { r.cr(w) } r.tag(w, openTag, attrs) } else { r.out(w, closeTag) r.cr(w) } case TableHead: if entering { r.cr(w) r.out(w, theadTag) } else { r.out(w, theadCloseTag) r.cr(w) } case TableBody: if entering { r.cr(w) r.out(w, tbodyTag) // XXX: this is to adhere to a rather silly test. Should fix test. if node.FirstChild == nil { r.cr(w) } } else { r.out(w, tbodyCloseTag) r.cr(w) } case TableRow: if entering { r.cr(w) r.out(w, trTag) } else { r.out(w, trCloseTag) r.cr(w) } default: panic("Unknown node type " + node.Type.String()) } return GoToNext } // RenderHeader writes HTML document preamble and TOC if requested. func (r *HTMLRenderer) RenderHeader(w io.Writer, ast *Node) { r.writeDocumentHeader(w) if r.Flags&TOC != 0 { r.writeTOC(w, ast) } } // RenderFooter writes HTML document footer. func (r *HTMLRenderer) RenderFooter(w io.Writer, ast *Node) { if r.Flags&CompletePage == 0 { return } io.WriteString(w, "\n\n\n") } func (r *HTMLRenderer) writeDocumentHeader(w io.Writer) { if r.Flags&CompletePage == 0 { return } ending := "" if r.Flags&UseXHTML != 0 { io.WriteString(w, "\n") io.WriteString(w, "\n") ending = " /" } else { io.WriteString(w, "\n") io.WriteString(w, "\n") } io.WriteString(w, "\n") io.WriteString(w, " ") if r.Flags&Smartypants != 0 { r.sr.Process(w, []byte(r.Title)) } else { escapeHTML(w, []byte(r.Title)) } io.WriteString(w, "\n") io.WriteString(w, " \n") io.WriteString(w, " \n") if r.CSS != "" { io.WriteString(w, " \n") } if r.Icon != "" { io.WriteString(w, " \n") } io.WriteString(w, "\n") io.WriteString(w, "\n\n") } func (r *HTMLRenderer) writeTOC(w io.Writer, ast *Node) { buf := bytes.Buffer{} inHeading := false tocLevel := 0 headingCount := 0 ast.Walk(func(node *Node, entering bool) WalkStatus { if node.Type == Heading && !node.HeadingData.IsTitleblock { inHeading = entering if entering { node.HeadingID = fmt.Sprintf("toc_%d", headingCount) if node.Level == tocLevel { buf.WriteString("\n\n
  • ") } else if node.Level < tocLevel { for node.Level < tocLevel { tocLevel-- buf.WriteString("
  • \n") } buf.WriteString("\n\n
  • ") } else { for node.Level > tocLevel { tocLevel++ buf.WriteString("\n") } if buf.Len() > 0 { io.WriteString(w, "\n") } r.lastOutputLen = buf.Len() } ================================================ FILE: tests/tools/vendor/github.com/russross/blackfriday/v2/inline.go ================================================ // // Blackfriday Markdown Processor // Available at http://github.com/russross/blackfriday // // Copyright © 2011 Russ Ross . // Distributed under the Simplified BSD License. // See README.md for details. // // // Functions to parse inline elements. // package blackfriday import ( "bytes" "regexp" "strconv" ) var ( urlRe = `((https?|ftp):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+` anchorRe = regexp.MustCompile(`^(]+")?\s?>` + urlRe + `<\/a>)`) // https://www.w3.org/TR/html5/syntax.html#character-references // highest unicode code point in 17 planes (2^20): 1,114,112d = // 7 dec digits or 6 hex digits // named entity references can be 2-31 characters with stuff like < // at one end and ∳ at the other. There // are also sometimes numbers at the end, although this isn't inherent // in the specification; there are never numbers anywhere else in // current character references, though; see ¾ and ▒, etc. // https://www.w3.org/TR/html5/syntax.html#named-character-references // // entity := "&" (named group | number ref) ";" // named group := [a-zA-Z]{2,31}[0-9]{0,2} // number ref := "#" (dec ref | hex ref) // dec ref := [0-9]{1,7} // hex ref := ("x" | "X") [0-9a-fA-F]{1,6} htmlEntityRe = regexp.MustCompile(`&([a-zA-Z]{2,31}[0-9]{0,2}|#([0-9]{1,7}|[xX][0-9a-fA-F]{1,6}));`) ) // Functions to parse text within a block // Each function returns the number of chars taken care of // data is the complete block being rendered // offset is the number of valid chars before the current cursor func (p *Markdown) inline(currBlock *Node, data []byte) { // handlers might call us recursively: enforce a maximum depth if p.nesting >= p.maxNesting || len(data) == 0 { return } p.nesting++ beg, end := 0, 0 for end < len(data) { handler := p.inlineCallback[data[end]] if handler != nil { if consumed, node := handler(p, data, end); consumed == 0 { // No action from the callback. end++ } else { // Copy inactive chars into the output. currBlock.AppendChild(text(data[beg:end])) if node != nil { currBlock.AppendChild(node) } // Skip past whatever the callback used. beg = end + consumed end = beg } } else { end++ } } if beg < len(data) { if data[end-1] == '\n' { end-- } currBlock.AppendChild(text(data[beg:end])) } p.nesting-- } // single and double emphasis parsing func emphasis(p *Markdown, data []byte, offset int) (int, *Node) { data = data[offset:] c := data[0] if len(data) > 2 && data[1] != c { // whitespace cannot follow an opening emphasis; // strikethrough only takes two characters '~~' if c == '~' || isspace(data[1]) { return 0, nil } ret, node := helperEmphasis(p, data[1:], c) if ret == 0 { return 0, nil } return ret + 1, node } if len(data) > 3 && data[1] == c && data[2] != c { if isspace(data[2]) { return 0, nil } ret, node := helperDoubleEmphasis(p, data[2:], c) if ret == 0 { return 0, nil } return ret + 2, node } if len(data) > 4 && data[1] == c && data[2] == c && data[3] != c { if c == '~' || isspace(data[3]) { return 0, nil } ret, node := helperTripleEmphasis(p, data, 3, c) if ret == 0 { return 0, nil } return ret + 3, node } return 0, nil } func codeSpan(p *Markdown, data []byte, offset int) (int, *Node) { data = data[offset:] nb := 0 // count the number of backticks in the delimiter for nb < len(data) && data[nb] == '`' { nb++ } // find the next delimiter i, end := 0, 0 for end = nb; end < len(data) && i < nb; end++ { if data[end] == '`' { i++ } else { i = 0 } } // no matching delimiter? if i < nb && end >= len(data) { return 0, nil } // trim outside whitespace fBegin := nb for fBegin < end && data[fBegin] == ' ' { fBegin++ } fEnd := end - nb for fEnd > fBegin && data[fEnd-1] == ' ' { fEnd-- } // render the code span if fBegin != fEnd { code := NewNode(Code) code.Literal = data[fBegin:fEnd] return end, code } return end, nil } // newline preceded by two spaces becomes
    func maybeLineBreak(p *Markdown, data []byte, offset int) (int, *Node) { origOffset := offset for offset < len(data) && data[offset] == ' ' { offset++ } if offset < len(data) && data[offset] == '\n' { if offset-origOffset >= 2 { return offset - origOffset + 1, NewNode(Hardbreak) } return offset - origOffset, nil } return 0, nil } // newline without two spaces works when HardLineBreak is enabled func lineBreak(p *Markdown, data []byte, offset int) (int, *Node) { if p.extensions&HardLineBreak != 0 { return 1, NewNode(Hardbreak) } return 0, nil } type linkType int const ( linkNormal linkType = iota linkImg linkDeferredFootnote linkInlineFootnote ) func isReferenceStyleLink(data []byte, pos int, t linkType) bool { if t == linkDeferredFootnote { return false } return pos < len(data)-1 && data[pos] == '[' && data[pos+1] != '^' } func maybeImage(p *Markdown, data []byte, offset int) (int, *Node) { if offset < len(data)-1 && data[offset+1] == '[' { return link(p, data, offset) } return 0, nil } func maybeInlineFootnote(p *Markdown, data []byte, offset int) (int, *Node) { if offset < len(data)-1 && data[offset+1] == '[' { return link(p, data, offset) } return 0, nil } // '[': parse a link or an image or a footnote func link(p *Markdown, data []byte, offset int) (int, *Node) { // no links allowed inside regular links, footnote, and deferred footnotes if p.insideLink && (offset > 0 && data[offset-1] == '[' || len(data)-1 > offset && data[offset+1] == '^') { return 0, nil } var t linkType switch { // special case: ![^text] == deferred footnote (that follows something with // an exclamation point) case p.extensions&Footnotes != 0 && len(data)-1 > offset && data[offset+1] == '^': t = linkDeferredFootnote // ![alt] == image case offset >= 0 && data[offset] == '!': t = linkImg offset++ // ^[text] == inline footnote // [^refId] == deferred footnote case p.extensions&Footnotes != 0: if offset >= 0 && data[offset] == '^' { t = linkInlineFootnote offset++ } else if len(data)-1 > offset && data[offset+1] == '^' { t = linkDeferredFootnote } // [text] == regular link default: t = linkNormal } data = data[offset:] var ( i = 1 noteID int title, link, altContent []byte textHasNl = false ) if t == linkDeferredFootnote { i++ } // look for the matching closing bracket for level := 1; level > 0 && i < len(data); i++ { switch { case data[i] == '\n': textHasNl = true case isBackslashEscaped(data, i): continue case data[i] == '[': level++ case data[i] == ']': level-- if level <= 0 { i-- // compensate for extra i++ in for loop } } } if i >= len(data) { return 0, nil } txtE := i i++ var footnoteNode *Node // skip any amount of whitespace or newline // (this is much more lax than original markdown syntax) for i < len(data) && isspace(data[i]) { i++ } // inline style link switch { case i < len(data) && data[i] == '(': // skip initial whitespace i++ for i < len(data) && isspace(data[i]) { i++ } linkB := i // look for link end: ' " ) findlinkend: for i < len(data) { switch { case data[i] == '\\': i += 2 case data[i] == ')' || data[i] == '\'' || data[i] == '"': break findlinkend default: i++ } } if i >= len(data) { return 0, nil } linkE := i // look for title end if present titleB, titleE := 0, 0 if data[i] == '\'' || data[i] == '"' { i++ titleB = i findtitleend: for i < len(data) { switch { case data[i] == '\\': i += 2 case data[i] == ')': break findtitleend default: i++ } } if i >= len(data) { return 0, nil } // skip whitespace after title titleE = i - 1 for titleE > titleB && isspace(data[titleE]) { titleE-- } // check for closing quote presence if data[titleE] != '\'' && data[titleE] != '"' { titleB, titleE = 0, 0 linkE = i } } // remove whitespace at the end of the link for linkE > linkB && isspace(data[linkE-1]) { linkE-- } // remove optional angle brackets around the link if data[linkB] == '<' { linkB++ } if data[linkE-1] == '>' { linkE-- } // build escaped link and title if linkE > linkB { link = data[linkB:linkE] } if titleE > titleB { title = data[titleB:titleE] } i++ // reference style link case isReferenceStyleLink(data, i, t): var id []byte altContentConsidered := false // look for the id i++ linkB := i for i < len(data) && data[i] != ']' { i++ } if i >= len(data) { return 0, nil } linkE := i // find the reference if linkB == linkE { if textHasNl { var b bytes.Buffer for j := 1; j < txtE; j++ { switch { case data[j] != '\n': b.WriteByte(data[j]) case data[j-1] != ' ': b.WriteByte(' ') } } id = b.Bytes() } else { id = data[1:txtE] altContentConsidered = true } } else { id = data[linkB:linkE] } // find the reference with matching id lr, ok := p.getRef(string(id)) if !ok { return 0, nil } // keep link and title from reference link = lr.link title = lr.title if altContentConsidered { altContent = lr.text } i++ // shortcut reference style link or reference or inline footnote default: var id []byte // craft the id if textHasNl { var b bytes.Buffer for j := 1; j < txtE; j++ { switch { case data[j] != '\n': b.WriteByte(data[j]) case data[j-1] != ' ': b.WriteByte(' ') } } id = b.Bytes() } else { if t == linkDeferredFootnote { id = data[2:txtE] // get rid of the ^ } else { id = data[1:txtE] } } footnoteNode = NewNode(Item) if t == linkInlineFootnote { // create a new reference noteID = len(p.notes) + 1 var fragment []byte if len(id) > 0 { if len(id) < 16 { fragment = make([]byte, len(id)) } else { fragment = make([]byte, 16) } copy(fragment, slugify(id)) } else { fragment = append([]byte("footnote-"), []byte(strconv.Itoa(noteID))...) } ref := &reference{ noteID: noteID, hasBlock: false, link: fragment, title: id, footnote: footnoteNode, } p.notes = append(p.notes, ref) link = ref.link title = ref.title } else { // find the reference with matching id lr, ok := p.getRef(string(id)) if !ok { return 0, nil } if t == linkDeferredFootnote { lr.noteID = len(p.notes) + 1 lr.footnote = footnoteNode p.notes = append(p.notes, lr) } // keep link and title from reference link = lr.link // if inline footnote, title == footnote contents title = lr.title noteID = lr.noteID } // rewind the whitespace i = txtE + 1 } var uLink []byte if t == linkNormal || t == linkImg { if len(link) > 0 { var uLinkBuf bytes.Buffer unescapeText(&uLinkBuf, link) uLink = uLinkBuf.Bytes() } // links need something to click on and somewhere to go if len(uLink) == 0 || (t == linkNormal && txtE <= 1) { return 0, nil } } // call the relevant rendering function var linkNode *Node switch t { case linkNormal: linkNode = NewNode(Link) linkNode.Destination = normalizeURI(uLink) linkNode.Title = title if len(altContent) > 0 { linkNode.AppendChild(text(altContent)) } else { // links cannot contain other links, so turn off link parsing // temporarily and recurse insideLink := p.insideLink p.insideLink = true p.inline(linkNode, data[1:txtE]) p.insideLink = insideLink } case linkImg: linkNode = NewNode(Image) linkNode.Destination = uLink linkNode.Title = title linkNode.AppendChild(text(data[1:txtE])) i++ case linkInlineFootnote, linkDeferredFootnote: linkNode = NewNode(Link) linkNode.Destination = link linkNode.Title = title linkNode.NoteID = noteID linkNode.Footnote = footnoteNode if t == linkInlineFootnote { i++ } default: return 0, nil } return i, linkNode } func (p *Markdown) inlineHTMLComment(data []byte) int { if len(data) < 5 { return 0 } if data[0] != '<' || data[1] != '!' || data[2] != '-' || data[3] != '-' { return 0 } i := 5 // scan for an end-of-comment marker, across lines if necessary for i < len(data) && !(data[i-2] == '-' && data[i-1] == '-' && data[i] == '>') { i++ } // no end-of-comment marker if i >= len(data) { return 0 } return i + 1 } func stripMailto(link []byte) []byte { if bytes.HasPrefix(link, []byte("mailto://")) { return link[9:] } else if bytes.HasPrefix(link, []byte("mailto:")) { return link[7:] } else { return link } } // autolinkType specifies a kind of autolink that gets detected. type autolinkType int // These are the possible flag values for the autolink renderer. const ( notAutolink autolinkType = iota normalAutolink emailAutolink ) // '<' when tags or autolinks are allowed func leftAngle(p *Markdown, data []byte, offset int) (int, *Node) { data = data[offset:] altype, end := tagLength(data) if size := p.inlineHTMLComment(data); size > 0 { end = size } if end > 2 { if altype != notAutolink { var uLink bytes.Buffer unescapeText(&uLink, data[1:end+1-2]) if uLink.Len() > 0 { link := uLink.Bytes() node := NewNode(Link) node.Destination = link if altype == emailAutolink { node.Destination = append([]byte("mailto:"), link...) } node.AppendChild(text(stripMailto(link))) return end, node } } else { htmlTag := NewNode(HTMLSpan) htmlTag.Literal = data[:end] return end, htmlTag } } return end, nil } // '\\' backslash escape var escapeChars = []byte("\\`*_{}[]()#+-.!:|&<>~") func escape(p *Markdown, data []byte, offset int) (int, *Node) { data = data[offset:] if len(data) > 1 { if p.extensions&BackslashLineBreak != 0 && data[1] == '\n' { return 2, NewNode(Hardbreak) } if bytes.IndexByte(escapeChars, data[1]) < 0 { return 0, nil } return 2, text(data[1:2]) } return 2, nil } func unescapeText(ob *bytes.Buffer, src []byte) { i := 0 for i < len(src) { org := i for i < len(src) && src[i] != '\\' { i++ } if i > org { ob.Write(src[org:i]) } if i+1 >= len(src) { break } ob.WriteByte(src[i+1]) i += 2 } } // '&' escaped when it doesn't belong to an entity // valid entities are assumed to be anything matching &#?[A-Za-z0-9]+; func entity(p *Markdown, data []byte, offset int) (int, *Node) { data = data[offset:] end := 1 if end < len(data) && data[end] == '#' { end++ } for end < len(data) && isalnum(data[end]) { end++ } if end < len(data) && data[end] == ';' { end++ // real entity } else { return 0, nil // lone '&' } ent := data[:end] // undo & escaping or it will be converted to &amp; by another // escaper in the renderer if bytes.Equal(ent, []byte("&")) { ent = []byte{'&'} } return end, text(ent) } func linkEndsWithEntity(data []byte, linkEnd int) bool { entityRanges := htmlEntityRe.FindAllIndex(data[:linkEnd], -1) return entityRanges != nil && entityRanges[len(entityRanges)-1][1] == linkEnd } // hasPrefixCaseInsensitive is a custom implementation of // // strings.HasPrefix(strings.ToLower(s), prefix) // // we rolled our own because ToLower pulls in a huge machinery of lowercasing // anything from Unicode and that's very slow. Since this func will only be // used on ASCII protocol prefixes, we can take shortcuts. func hasPrefixCaseInsensitive(s, prefix []byte) bool { if len(s) < len(prefix) { return false } delta := byte('a' - 'A') for i, b := range prefix { if b != s[i] && b != s[i]+delta { return false } } return true } var protocolPrefixes = [][]byte{ []byte("http://"), []byte("https://"), []byte("ftp://"), []byte("file://"), []byte("mailto:"), } const shortestPrefix = 6 // len("ftp://"), the shortest of the above func maybeAutoLink(p *Markdown, data []byte, offset int) (int, *Node) { // quick check to rule out most false hits if p.insideLink || len(data) < offset+shortestPrefix { return 0, nil } for _, prefix := range protocolPrefixes { endOfHead := offset + 8 // 8 is the len() of the longest prefix if endOfHead > len(data) { endOfHead = len(data) } if hasPrefixCaseInsensitive(data[offset:endOfHead], prefix) { return autoLink(p, data, offset) } } return 0, nil } func autoLink(p *Markdown, data []byte, offset int) (int, *Node) { // Now a more expensive check to see if we're not inside an anchor element anchorStart := offset offsetFromAnchor := 0 for anchorStart > 0 && data[anchorStart] != '<' { anchorStart-- offsetFromAnchor++ } anchorStr := anchorRe.Find(data[anchorStart:]) if anchorStr != nil { anchorClose := NewNode(HTMLSpan) anchorClose.Literal = anchorStr[offsetFromAnchor:] return len(anchorStr) - offsetFromAnchor, anchorClose } // scan backward for a word boundary rewind := 0 for offset-rewind > 0 && rewind <= 7 && isletter(data[offset-rewind-1]) { rewind++ } if rewind > 6 { // longest supported protocol is "mailto" which has 6 letters return 0, nil } origData := data data = data[offset-rewind:] if !isSafeLink(data) { return 0, nil } linkEnd := 0 for linkEnd < len(data) && !isEndOfLink(data[linkEnd]) { linkEnd++ } // Skip punctuation at the end of the link if (data[linkEnd-1] == '.' || data[linkEnd-1] == ',') && data[linkEnd-2] != '\\' { linkEnd-- } // But don't skip semicolon if it's a part of escaped entity: if data[linkEnd-1] == ';' && data[linkEnd-2] != '\\' && !linkEndsWithEntity(data, linkEnd) { linkEnd-- } // See if the link finishes with a punctuation sign that can be closed. var copen byte switch data[linkEnd-1] { case '"': copen = '"' case '\'': copen = '\'' case ')': copen = '(' case ']': copen = '[' case '}': copen = '{' default: copen = 0 } if copen != 0 { bufEnd := offset - rewind + linkEnd - 2 openDelim := 1 /* Try to close the final punctuation sign in this same line; * if we managed to close it outside of the URL, that means that it's * not part of the URL. If it closes inside the URL, that means it * is part of the URL. * * Examples: * * foo http://www.pokemon.com/Pikachu_(Electric) bar * => http://www.pokemon.com/Pikachu_(Electric) * * foo (http://www.pokemon.com/Pikachu_(Electric)) bar * => http://www.pokemon.com/Pikachu_(Electric) * * foo http://www.pokemon.com/Pikachu_(Electric)) bar * => http://www.pokemon.com/Pikachu_(Electric)) * * (foo http://www.pokemon.com/Pikachu_(Electric)) bar * => foo http://www.pokemon.com/Pikachu_(Electric) */ for bufEnd >= 0 && origData[bufEnd] != '\n' && openDelim != 0 { if origData[bufEnd] == data[linkEnd-1] { openDelim++ } if origData[bufEnd] == copen { openDelim-- } bufEnd-- } if openDelim == 0 { linkEnd-- } } var uLink bytes.Buffer unescapeText(&uLink, data[:linkEnd]) if uLink.Len() > 0 { node := NewNode(Link) node.Destination = uLink.Bytes() node.AppendChild(text(uLink.Bytes())) return linkEnd, node } return linkEnd, nil } func isEndOfLink(char byte) bool { return isspace(char) || char == '<' } var validUris = [][]byte{[]byte("http://"), []byte("https://"), []byte("ftp://"), []byte("mailto://")} var validPaths = [][]byte{[]byte("/"), []byte("./"), []byte("../")} func isSafeLink(link []byte) bool { for _, path := range validPaths { if len(link) >= len(path) && bytes.Equal(link[:len(path)], path) { if len(link) == len(path) { return true } else if isalnum(link[len(path)]) { return true } } } for _, prefix := range validUris { // TODO: handle unicode here // case-insensitive prefix test if len(link) > len(prefix) && bytes.Equal(bytes.ToLower(link[:len(prefix)]), prefix) && isalnum(link[len(prefix)]) { return true } } return false } // return the length of the given tag, or 0 is it's not valid func tagLength(data []byte) (autolink autolinkType, end int) { var i, j int // a valid tag can't be shorter than 3 chars if len(data) < 3 { return notAutolink, 0 } // begins with a '<' optionally followed by '/', followed by letter or number if data[0] != '<' { return notAutolink, 0 } if data[1] == '/' { i = 2 } else { i = 1 } if !isalnum(data[i]) { return notAutolink, 0 } // scheme test autolink = notAutolink // try to find the beginning of an URI for i < len(data) && (isalnum(data[i]) || data[i] == '.' || data[i] == '+' || data[i] == '-') { i++ } if i > 1 && i < len(data) && data[i] == '@' { if j = isMailtoAutoLink(data[i:]); j != 0 { return emailAutolink, i + j } } if i > 2 && i < len(data) && data[i] == ':' { autolink = normalAutolink i++ } // complete autolink test: no whitespace or ' or " switch { case i >= len(data): autolink = notAutolink case autolink != notAutolink: j = i for i < len(data) { if data[i] == '\\' { i += 2 } else if data[i] == '>' || data[i] == '\'' || data[i] == '"' || isspace(data[i]) { break } else { i++ } } if i >= len(data) { return autolink, 0 } if i > j && data[i] == '>' { return autolink, i + 1 } // one of the forbidden chars has been found autolink = notAutolink } i += bytes.IndexByte(data[i:], '>') if i < 0 { return autolink, 0 } return autolink, i + 1 } // look for the address part of a mail autolink and '>' // this is less strict than the original markdown e-mail address matching func isMailtoAutoLink(data []byte) int { nb := 0 // address is assumed to be: [-@._a-zA-Z0-9]+ with exactly one '@' for i := 0; i < len(data); i++ { if isalnum(data[i]) { continue } switch data[i] { case '@': nb++ case '-', '.', '_': break case '>': if nb == 1 { return i + 1 } return 0 default: return 0 } } return 0 } // look for the next emph char, skipping other constructs func helperFindEmphChar(data []byte, c byte) int { i := 0 for i < len(data) { for i < len(data) && data[i] != c && data[i] != '`' && data[i] != '[' { i++ } if i >= len(data) { return 0 } // do not count escaped chars if i != 0 && data[i-1] == '\\' { i++ continue } if data[i] == c { return i } if data[i] == '`' { // skip a code span tmpI := 0 i++ for i < len(data) && data[i] != '`' { if tmpI == 0 && data[i] == c { tmpI = i } i++ } if i >= len(data) { return tmpI } i++ } else if data[i] == '[' { // skip a link tmpI := 0 i++ for i < len(data) && data[i] != ']' { if tmpI == 0 && data[i] == c { tmpI = i } i++ } i++ for i < len(data) && (data[i] == ' ' || data[i] == '\n') { i++ } if i >= len(data) { return tmpI } if data[i] != '[' && data[i] != '(' { // not a link if tmpI > 0 { return tmpI } continue } cc := data[i] i++ for i < len(data) && data[i] != cc { if tmpI == 0 && data[i] == c { return i } i++ } if i >= len(data) { return tmpI } i++ } } return 0 } func helperEmphasis(p *Markdown, data []byte, c byte) (int, *Node) { i := 0 // skip one symbol if coming from emph3 if len(data) > 1 && data[0] == c && data[1] == c { i = 1 } for i < len(data) { length := helperFindEmphChar(data[i:], c) if length == 0 { return 0, nil } i += length if i >= len(data) { return 0, nil } if i+1 < len(data) && data[i+1] == c { i++ continue } if data[i] == c && !isspace(data[i-1]) { if p.extensions&NoIntraEmphasis != 0 { if !(i+1 == len(data) || isspace(data[i+1]) || ispunct(data[i+1])) { continue } } emph := NewNode(Emph) p.inline(emph, data[:i]) return i + 1, emph } } return 0, nil } func helperDoubleEmphasis(p *Markdown, data []byte, c byte) (int, *Node) { i := 0 for i < len(data) { length := helperFindEmphChar(data[i:], c) if length == 0 { return 0, nil } i += length if i+1 < len(data) && data[i] == c && data[i+1] == c && i > 0 && !isspace(data[i-1]) { nodeType := Strong if c == '~' { nodeType = Del } node := NewNode(nodeType) p.inline(node, data[:i]) return i + 2, node } i++ } return 0, nil } func helperTripleEmphasis(p *Markdown, data []byte, offset int, c byte) (int, *Node) { i := 0 origData := data data = data[offset:] for i < len(data) { length := helperFindEmphChar(data[i:], c) if length == 0 { return 0, nil } i += length // skip whitespace preceded symbols if data[i] != c || isspace(data[i-1]) { continue } switch { case i+2 < len(data) && data[i+1] == c && data[i+2] == c: // triple symbol found strong := NewNode(Strong) em := NewNode(Emph) strong.AppendChild(em) p.inline(em, data[:i]) return i + 3, strong case (i+1 < len(data) && data[i+1] == c): // double symbol found, hand over to emph1 length, node := helperEmphasis(p, origData[offset-2:], c) if length == 0 { return 0, nil } return length - 2, node default: // single symbol found, hand over to emph2 length, node := helperDoubleEmphasis(p, origData[offset-1:], c) if length == 0 { return 0, nil } return length - 1, node } } return 0, nil } func text(s []byte) *Node { node := NewNode(Text) node.Literal = s return node } func normalizeURI(s []byte) []byte { return s // TODO: implement } ================================================ FILE: tests/tools/vendor/github.com/russross/blackfriday/v2/markdown.go ================================================ // Blackfriday Markdown Processor // Available at http://github.com/russross/blackfriday // // Copyright © 2011 Russ Ross . // Distributed under the Simplified BSD License. // See README.md for details. package blackfriday import ( "bytes" "fmt" "io" "strings" "unicode/utf8" ) // // Markdown parsing and processing // // Version string of the package. Appears in the rendered document when // CompletePage flag is on. const Version = "2.0" // Extensions is a bitwise or'ed collection of enabled Blackfriday's // extensions. type Extensions int // These are the supported markdown parsing extensions. // OR these values together to select multiple extensions. const ( NoExtensions Extensions = 0 NoIntraEmphasis Extensions = 1 << iota // Ignore emphasis markers inside words Tables // Render tables FencedCode // Render fenced code blocks Autolink // Detect embedded URLs that are not explicitly marked Strikethrough // Strikethrough text using ~~test~~ LaxHTMLBlocks // Loosen up HTML block parsing rules SpaceHeadings // Be strict about prefix heading rules HardLineBreak // Translate newlines into line breaks TabSizeEight // Expand tabs to eight spaces instead of four Footnotes // Pandoc-style footnotes NoEmptyLineBeforeBlock // No need to insert an empty line to start a (code, quote, ordered list, unordered list) block HeadingIDs // specify heading IDs with {#id} Titleblock // Titleblock ala pandoc AutoHeadingIDs // Create the heading ID from the text BackslashLineBreak // Translate trailing backslashes into line breaks DefinitionLists // Render definition lists CommonHTMLFlags HTMLFlags = UseXHTML | Smartypants | SmartypantsFractions | SmartypantsDashes | SmartypantsLatexDashes CommonExtensions Extensions = NoIntraEmphasis | Tables | FencedCode | Autolink | Strikethrough | SpaceHeadings | HeadingIDs | BackslashLineBreak | DefinitionLists ) // ListType contains bitwise or'ed flags for list and list item objects. type ListType int // These are the possible flag values for the ListItem renderer. // Multiple flag values may be ORed together. // These are mostly of interest if you are writing a new output format. const ( ListTypeOrdered ListType = 1 << iota ListTypeDefinition ListTypeTerm ListItemContainsBlock ListItemBeginningOfList // TODO: figure out if this is of any use now ListItemEndOfList ) // CellAlignFlags holds a type of alignment in a table cell. type CellAlignFlags int // These are the possible flag values for the table cell renderer. // Only a single one of these values will be used; they are not ORed together. // These are mostly of interest if you are writing a new output format. const ( TableAlignmentLeft CellAlignFlags = 1 << iota TableAlignmentRight TableAlignmentCenter = (TableAlignmentLeft | TableAlignmentRight) ) // The size of a tab stop. const ( TabSizeDefault = 4 TabSizeDouble = 8 ) // blockTags is a set of tags that are recognized as HTML block tags. // Any of these can be included in markdown text without special escaping. var blockTags = map[string]struct{}{ "blockquote": {}, "del": {}, "div": {}, "dl": {}, "fieldset": {}, "form": {}, "h1": {}, "h2": {}, "h3": {}, "h4": {}, "h5": {}, "h6": {}, "iframe": {}, "ins": {}, "math": {}, "noscript": {}, "ol": {}, "pre": {}, "p": {}, "script": {}, "style": {}, "table": {}, "ul": {}, // HTML5 "address": {}, "article": {}, "aside": {}, "canvas": {}, "figcaption": {}, "figure": {}, "footer": {}, "header": {}, "hgroup": {}, "main": {}, "nav": {}, "output": {}, "progress": {}, "section": {}, "video": {}, } // Renderer is the rendering interface. This is mostly of interest if you are // implementing a new rendering format. // // Only an HTML implementation is provided in this repository, see the README // for external implementations. type Renderer interface { // RenderNode is the main rendering method. It will be called once for // every leaf node and twice for every non-leaf node (first with // entering=true, then with entering=false). The method should write its // rendition of the node to the supplied writer w. RenderNode(w io.Writer, node *Node, entering bool) WalkStatus // RenderHeader is a method that allows the renderer to produce some // content preceding the main body of the output document. The header is // understood in the broad sense here. For example, the default HTML // renderer will write not only the HTML document preamble, but also the // table of contents if it was requested. // // The method will be passed an entire document tree, in case a particular // implementation needs to inspect it to produce output. // // The output should be written to the supplied writer w. If your // implementation has no header to write, supply an empty implementation. RenderHeader(w io.Writer, ast *Node) // RenderFooter is a symmetric counterpart of RenderHeader. RenderFooter(w io.Writer, ast *Node) } // Callback functions for inline parsing. One such function is defined // for each character that triggers a response when parsing inline data. type inlineParser func(p *Markdown, data []byte, offset int) (int, *Node) // Markdown is a type that holds extensions and the runtime state used by // Parse, and the renderer. You can not use it directly, construct it with New. type Markdown struct { renderer Renderer referenceOverride ReferenceOverrideFunc refs map[string]*reference inlineCallback [256]inlineParser extensions Extensions nesting int maxNesting int insideLink bool // Footnotes need to be ordered as well as available to quickly check for // presence. If a ref is also a footnote, it's stored both in refs and here // in notes. Slice is nil if footnotes not enabled. notes []*reference doc *Node tip *Node // = doc oldTip *Node lastMatchedContainer *Node // = doc allClosed bool } func (p *Markdown) getRef(refid string) (ref *reference, found bool) { if p.referenceOverride != nil { r, overridden := p.referenceOverride(refid) if overridden { if r == nil { return nil, false } return &reference{ link: []byte(r.Link), title: []byte(r.Title), noteID: 0, hasBlock: false, text: []byte(r.Text)}, true } } // refs are case insensitive ref, found = p.refs[strings.ToLower(refid)] return ref, found } func (p *Markdown) finalize(block *Node) { above := block.Parent block.open = false p.tip = above } func (p *Markdown) addChild(node NodeType, offset uint32) *Node { return p.addExistingChild(NewNode(node), offset) } func (p *Markdown) addExistingChild(node *Node, offset uint32) *Node { for !p.tip.canContain(node.Type) { p.finalize(p.tip) } p.tip.AppendChild(node) p.tip = node return node } func (p *Markdown) closeUnmatchedBlocks() { if !p.allClosed { for p.oldTip != p.lastMatchedContainer { parent := p.oldTip.Parent p.finalize(p.oldTip) p.oldTip = parent } p.allClosed = true } } // // // Public interface // // // Reference represents the details of a link. // See the documentation in Options for more details on use-case. type Reference struct { // Link is usually the URL the reference points to. Link string // Title is the alternate text describing the link in more detail. Title string // Text is the optional text to override the ref with if the syntax used was // [refid][] Text string } // ReferenceOverrideFunc is expected to be called with a reference string and // return either a valid Reference type that the reference string maps to or // nil. If overridden is false, the default reference logic will be executed. // See the documentation in Options for more details on use-case. type ReferenceOverrideFunc func(reference string) (ref *Reference, overridden bool) // New constructs a Markdown processor. You can use the same With* functions as // for Run() to customize parser's behavior and the renderer. func New(opts ...Option) *Markdown { var p Markdown for _, opt := range opts { opt(&p) } p.refs = make(map[string]*reference) p.maxNesting = 16 p.insideLink = false docNode := NewNode(Document) p.doc = docNode p.tip = docNode p.oldTip = docNode p.lastMatchedContainer = docNode p.allClosed = true // register inline parsers p.inlineCallback[' '] = maybeLineBreak p.inlineCallback['*'] = emphasis p.inlineCallback['_'] = emphasis if p.extensions&Strikethrough != 0 { p.inlineCallback['~'] = emphasis } p.inlineCallback['`'] = codeSpan p.inlineCallback['\n'] = lineBreak p.inlineCallback['['] = link p.inlineCallback['<'] = leftAngle p.inlineCallback['\\'] = escape p.inlineCallback['&'] = entity p.inlineCallback['!'] = maybeImage p.inlineCallback['^'] = maybeInlineFootnote if p.extensions&Autolink != 0 { p.inlineCallback['h'] = maybeAutoLink p.inlineCallback['m'] = maybeAutoLink p.inlineCallback['f'] = maybeAutoLink p.inlineCallback['H'] = maybeAutoLink p.inlineCallback['M'] = maybeAutoLink p.inlineCallback['F'] = maybeAutoLink } if p.extensions&Footnotes != 0 { p.notes = make([]*reference, 0) } return &p } // Option customizes the Markdown processor's default behavior. type Option func(*Markdown) // WithRenderer allows you to override the default renderer. func WithRenderer(r Renderer) Option { return func(p *Markdown) { p.renderer = r } } // WithExtensions allows you to pick some of the many extensions provided by // Blackfriday. You can bitwise OR them. func WithExtensions(e Extensions) Option { return func(p *Markdown) { p.extensions = e } } // WithNoExtensions turns off all extensions and custom behavior. func WithNoExtensions() Option { return func(p *Markdown) { p.extensions = NoExtensions p.renderer = NewHTMLRenderer(HTMLRendererParameters{ Flags: HTMLFlagsNone, }) } } // WithRefOverride sets an optional function callback that is called every // time a reference is resolved. // // In Markdown, the link reference syntax can be made to resolve a link to // a reference instead of an inline URL, in one of the following ways: // // - [link text][refid] // - [refid][] // // Usually, the refid is defined at the bottom of the Markdown document. If // this override function is provided, the refid is passed to the override // function first, before consulting the defined refids at the bottom. If // the override function indicates an override did not occur, the refids at // the bottom will be used to fill in the link details. func WithRefOverride(o ReferenceOverrideFunc) Option { return func(p *Markdown) { p.referenceOverride = o } } // Run is the main entry point to Blackfriday. It parses and renders a // block of markdown-encoded text. // // The simplest invocation of Run takes one argument, input: // // output := Run(input) // // This will parse the input with CommonExtensions enabled and render it with // the default HTMLRenderer (with CommonHTMLFlags). // // Variadic arguments opts can customize the default behavior. Since Markdown // type does not contain exported fields, you can not use it directly. Instead, // use the With* functions. For example, this will call the most basic // functionality, with no extensions: // // output := Run(input, WithNoExtensions()) // // You can use any number of With* arguments, even contradicting ones. They // will be applied in order of appearance and the latter will override the // former: // // output := Run(input, WithNoExtensions(), WithExtensions(exts), // WithRenderer(yourRenderer)) func Run(input []byte, opts ...Option) []byte { r := NewHTMLRenderer(HTMLRendererParameters{ Flags: CommonHTMLFlags, }) optList := []Option{WithRenderer(r), WithExtensions(CommonExtensions)} optList = append(optList, opts...) parser := New(optList...) ast := parser.Parse(input) var buf bytes.Buffer parser.renderer.RenderHeader(&buf, ast) ast.Walk(func(node *Node, entering bool) WalkStatus { return parser.renderer.RenderNode(&buf, node, entering) }) parser.renderer.RenderFooter(&buf, ast) return buf.Bytes() } // Parse is an entry point to the parsing part of Blackfriday. It takes an // input markdown document and produces a syntax tree for its contents. This // tree can then be rendered with a default or custom renderer, or // analyzed/transformed by the caller to whatever non-standard needs they have. // The return value is the root node of the syntax tree. func (p *Markdown) Parse(input []byte) *Node { p.block(input) // Walk the tree and finish up some of unfinished blocks for p.tip != nil { p.finalize(p.tip) } // Walk the tree again and process inline markdown in each block p.doc.Walk(func(node *Node, entering bool) WalkStatus { if node.Type == Paragraph || node.Type == Heading || node.Type == TableCell { p.inline(node, node.content) node.content = nil } return GoToNext }) p.parseRefsToAST() return p.doc } func (p *Markdown) parseRefsToAST() { if p.extensions&Footnotes == 0 || len(p.notes) == 0 { return } p.tip = p.doc block := p.addBlock(List, nil) block.IsFootnotesList = true block.ListFlags = ListTypeOrdered flags := ListItemBeginningOfList // Note: this loop is intentionally explicit, not range-form. This is // because the body of the loop will append nested footnotes to p.notes and // we need to process those late additions. Range form would only walk over // the fixed initial set. for i := 0; i < len(p.notes); i++ { ref := p.notes[i] p.addExistingChild(ref.footnote, 0) block := ref.footnote block.ListFlags = flags | ListTypeOrdered block.RefLink = ref.link if ref.hasBlock { flags |= ListItemContainsBlock p.block(ref.title) } else { p.inline(block, ref.title) } flags &^= ListItemBeginningOfList | ListItemContainsBlock } above := block.Parent finalizeList(block) p.tip = above block.Walk(func(node *Node, entering bool) WalkStatus { if node.Type == Paragraph || node.Type == Heading { p.inline(node, node.content) node.content = nil } return GoToNext }) } // // Link references // // This section implements support for references that (usually) appear // as footnotes in a document, and can be referenced anywhere in the document. // The basic format is: // // [1]: http://www.google.com/ "Google" // [2]: http://www.github.com/ "Github" // // Anywhere in the document, the reference can be linked by referring to its // label, i.e., 1 and 2 in this example, as in: // // This library is hosted on [Github][2], a git hosting site. // // Actual footnotes as specified in Pandoc and supported by some other Markdown // libraries such as php-markdown are also taken care of. They look like this: // // This sentence needs a bit of further explanation.[^note] // // [^note]: This is the explanation. // // Footnotes should be placed at the end of the document in an ordered list. // Finally, there are inline footnotes such as: // // Inline footnotes^[Also supported.] provide a quick inline explanation, // but are rendered at the bottom of the document. // // reference holds all information necessary for a reference-style links or // footnotes. // // Consider this markdown with reference-style links: // // [link][ref] // // [ref]: /url/ "tooltip title" // // It will be ultimately converted to this HTML: // //

    link

    // // And a reference structure will be populated as follows: // // p.refs["ref"] = &reference{ // link: "/url/", // title: "tooltip title", // } // // Alternatively, reference can contain information about a footnote. Consider // this markdown: // // Text needing a footnote.[^a] // // [^a]: This is the note // // A reference structure will be populated as follows: // // p.refs["a"] = &reference{ // link: "a", // title: "This is the note", // noteID: , // } // // TODO: As you can see, it begs for splitting into two dedicated structures // for refs and for footnotes. type reference struct { link []byte title []byte noteID int // 0 if not a footnote ref hasBlock bool footnote *Node // a link to the Item node within a list of footnotes text []byte // only gets populated by refOverride feature with Reference.Text } func (r *reference) String() string { return fmt.Sprintf("{link: %q, title: %q, text: %q, noteID: %d, hasBlock: %v}", r.link, r.title, r.text, r.noteID, r.hasBlock) } // Check whether or not data starts with a reference link. // If so, it is parsed and stored in the list of references // (in the render struct). // Returns the number of bytes to skip to move past it, // or zero if the first line is not a reference. func isReference(p *Markdown, data []byte, tabSize int) int { // up to 3 optional leading spaces if len(data) < 4 { return 0 } i := 0 for i < 3 && data[i] == ' ' { i++ } noteID := 0 // id part: anything but a newline between brackets if data[i] != '[' { return 0 } i++ if p.extensions&Footnotes != 0 { if i < len(data) && data[i] == '^' { // we can set it to anything here because the proper noteIds will // be assigned later during the second pass. It just has to be != 0 noteID = 1 i++ } } idOffset := i for i < len(data) && data[i] != '\n' && data[i] != '\r' && data[i] != ']' { i++ } if i >= len(data) || data[i] != ']' { return 0 } idEnd := i // footnotes can have empty ID, like this: [^], but a reference can not be // empty like this: []. Break early if it's not a footnote and there's no ID if noteID == 0 && idOffset == idEnd { return 0 } // spacer: colon (space | tab)* newline? (space | tab)* i++ if i >= len(data) || data[i] != ':' { return 0 } i++ for i < len(data) && (data[i] == ' ' || data[i] == '\t') { i++ } if i < len(data) && (data[i] == '\n' || data[i] == '\r') { i++ if i < len(data) && data[i] == '\n' && data[i-1] == '\r' { i++ } } for i < len(data) && (data[i] == ' ' || data[i] == '\t') { i++ } if i >= len(data) { return 0 } var ( linkOffset, linkEnd int titleOffset, titleEnd int lineEnd int raw []byte hasBlock bool ) if p.extensions&Footnotes != 0 && noteID != 0 { linkOffset, linkEnd, raw, hasBlock = scanFootnote(p, data, i, tabSize) lineEnd = linkEnd } else { linkOffset, linkEnd, titleOffset, titleEnd, lineEnd = scanLinkRef(p, data, i) } if lineEnd == 0 { return 0 } // a valid ref has been found ref := &reference{ noteID: noteID, hasBlock: hasBlock, } if noteID > 0 { // reusing the link field for the id since footnotes don't have links ref.link = data[idOffset:idEnd] // if footnote, it's not really a title, it's the contained text ref.title = raw } else { ref.link = data[linkOffset:linkEnd] ref.title = data[titleOffset:titleEnd] } // id matches are case-insensitive id := string(bytes.ToLower(data[idOffset:idEnd])) p.refs[id] = ref return lineEnd } func scanLinkRef(p *Markdown, data []byte, i int) (linkOffset, linkEnd, titleOffset, titleEnd, lineEnd int) { // link: whitespace-free sequence, optionally between angle brackets if data[i] == '<' { i++ } linkOffset = i for i < len(data) && data[i] != ' ' && data[i] != '\t' && data[i] != '\n' && data[i] != '\r' { i++ } linkEnd = i if data[linkOffset] == '<' && data[linkEnd-1] == '>' { linkOffset++ linkEnd-- } // optional spacer: (space | tab)* (newline | '\'' | '"' | '(' ) for i < len(data) && (data[i] == ' ' || data[i] == '\t') { i++ } if i < len(data) && data[i] != '\n' && data[i] != '\r' && data[i] != '\'' && data[i] != '"' && data[i] != '(' { return } // compute end-of-line if i >= len(data) || data[i] == '\r' || data[i] == '\n' { lineEnd = i } if i+1 < len(data) && data[i] == '\r' && data[i+1] == '\n' { lineEnd++ } // optional (space|tab)* spacer after a newline if lineEnd > 0 { i = lineEnd + 1 for i < len(data) && (data[i] == ' ' || data[i] == '\t') { i++ } } // optional title: any non-newline sequence enclosed in '"() alone on its line if i+1 < len(data) && (data[i] == '\'' || data[i] == '"' || data[i] == '(') { i++ titleOffset = i // look for EOL for i < len(data) && data[i] != '\n' && data[i] != '\r' { i++ } if i+1 < len(data) && data[i] == '\n' && data[i+1] == '\r' { titleEnd = i + 1 } else { titleEnd = i } // step back i-- for i > titleOffset && (data[i] == ' ' || data[i] == '\t') { i-- } if i > titleOffset && (data[i] == '\'' || data[i] == '"' || data[i] == ')') { lineEnd = titleEnd titleEnd = i } } return } // The first bit of this logic is the same as Parser.listItem, but the rest // is much simpler. This function simply finds the entire block and shifts it // over by one tab if it is indeed a block (just returns the line if it's not). // blockEnd is the end of the section in the input buffer, and contents is the // extracted text that was shifted over one tab. It will need to be rendered at // the end of the document. func scanFootnote(p *Markdown, data []byte, i, indentSize int) (blockStart, blockEnd int, contents []byte, hasBlock bool) { if i == 0 || len(data) == 0 { return } // skip leading whitespace on first line for i < len(data) && data[i] == ' ' { i++ } blockStart = i // find the end of the line blockEnd = i for i < len(data) && data[i-1] != '\n' { i++ } // get working buffer var raw bytes.Buffer // put the first line into the working buffer raw.Write(data[blockEnd:i]) blockEnd = i // process the following lines containsBlankLine := false gatherLines: for blockEnd < len(data) { i++ // find the end of this line for i < len(data) && data[i-1] != '\n' { i++ } // if it is an empty line, guess that it is part of this item // and move on to the next line if p.isEmpty(data[blockEnd:i]) > 0 { containsBlankLine = true blockEnd = i continue } n := 0 if n = isIndented(data[blockEnd:i], indentSize); n == 0 { // this is the end of the block. // we don't want to include this last line in the index. break gatherLines } // if there were blank lines before this one, insert a new one now if containsBlankLine { raw.WriteByte('\n') containsBlankLine = false } // get rid of that first tab, write to buffer raw.Write(data[blockEnd+n : i]) hasBlock = true blockEnd = i } if data[blockEnd-1] != '\n' { raw.WriteByte('\n') } contents = raw.Bytes() return } // // // Miscellaneous helper functions // // // Test if a character is a punctuation symbol. // Taken from a private function in regexp in the stdlib. func ispunct(c byte) bool { for _, r := range []byte("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~") { if c == r { return true } } return false } // Test if a character is a whitespace character. func isspace(c byte) bool { return ishorizontalspace(c) || isverticalspace(c) } // Test if a character is a horizontal whitespace character. func ishorizontalspace(c byte) bool { return c == ' ' || c == '\t' } // Test if a character is a vertical character. func isverticalspace(c byte) bool { return c == '\n' || c == '\r' || c == '\f' || c == '\v' } // Test if a character is letter. func isletter(c byte) bool { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') } // Test if a character is a letter or a digit. // TODO: check when this is looking for ASCII alnum and when it should use unicode func isalnum(c byte) bool { return (c >= '0' && c <= '9') || isletter(c) } // Replace tab characters with spaces, aligning to the next TAB_SIZE column. // always ends output with a newline func expandTabs(out *bytes.Buffer, line []byte, tabSize int) { // first, check for common cases: no tabs, or only tabs at beginning of line i, prefix := 0, 0 slowcase := false for i = 0; i < len(line); i++ { if line[i] == '\t' { if prefix == i { prefix++ } else { slowcase = true break } } } // no need to decode runes if all tabs are at the beginning of the line if !slowcase { for i = 0; i < prefix*tabSize; i++ { out.WriteByte(' ') } out.Write(line[prefix:]) return } // the slow case: we need to count runes to figure out how // many spaces to insert for each tab column := 0 i = 0 for i < len(line) { start := i for i < len(line) && line[i] != '\t' { _, size := utf8.DecodeRune(line[i:]) i += size column++ } if i > start { out.Write(line[start:i]) } if i >= len(line) { break } for { out.WriteByte(' ') column++ if column%tabSize == 0 { break } } i++ } } // Find if a line counts as indented or not. // Returns number of characters the indent is (0 = not indented). func isIndented(data []byte, indentSize int) int { if len(data) == 0 { return 0 } if data[0] == '\t' { return 1 } if len(data) < indentSize { return 0 } for i := 0; i < indentSize; i++ { if data[i] != ' ' { return 0 } } return indentSize } // Create a url-safe slug for fragments func slugify(in []byte) []byte { if len(in) == 0 { return in } out := make([]byte, 0, len(in)) sym := false for _, ch := range in { if isalnum(ch) { sym = false out = append(out, ch) } else if sym { continue } else { out = append(out, '-') sym = true } } var a, b int var ch byte for a, ch = range out { if ch != '-' { break } } for b = len(out) - 1; b > 0; b-- { if out[b] != '-' { break } } return out[a : b+1] } ================================================ FILE: tests/tools/vendor/github.com/russross/blackfriday/v2/node.go ================================================ package blackfriday import ( "bytes" "fmt" ) // NodeType specifies a type of a single node of a syntax tree. Usually one // node (and its type) corresponds to a single markdown feature, e.g. emphasis // or code block. type NodeType int // Constants for identifying different types of nodes. See NodeType. const ( Document NodeType = iota BlockQuote List Item Paragraph Heading HorizontalRule Emph Strong Del Link Image Text HTMLBlock CodeBlock Softbreak Hardbreak Code HTMLSpan Table TableCell TableHead TableBody TableRow ) var nodeTypeNames = []string{ Document: "Document", BlockQuote: "BlockQuote", List: "List", Item: "Item", Paragraph: "Paragraph", Heading: "Heading", HorizontalRule: "HorizontalRule", Emph: "Emph", Strong: "Strong", Del: "Del", Link: "Link", Image: "Image", Text: "Text", HTMLBlock: "HTMLBlock", CodeBlock: "CodeBlock", Softbreak: "Softbreak", Hardbreak: "Hardbreak", Code: "Code", HTMLSpan: "HTMLSpan", Table: "Table", TableCell: "TableCell", TableHead: "TableHead", TableBody: "TableBody", TableRow: "TableRow", } func (t NodeType) String() string { return nodeTypeNames[t] } // ListData contains fields relevant to a List and Item node type. type ListData struct { ListFlags ListType Tight bool // Skip

    s around list item data if true BulletChar byte // '*', '+' or '-' in bullet lists Delimiter byte // '.' or ')' after the number in ordered lists RefLink []byte // If not nil, turns this list item into a footnote item and triggers different rendering IsFootnotesList bool // This is a list of footnotes } // LinkData contains fields relevant to a Link node type. type LinkData struct { Destination []byte // Destination is what goes into a href Title []byte // Title is the tooltip thing that goes in a title attribute NoteID int // NoteID contains a serial number of a footnote, zero if it's not a footnote Footnote *Node // If it's a footnote, this is a direct link to the footnote Node. Otherwise nil. } // CodeBlockData contains fields relevant to a CodeBlock node type. type CodeBlockData struct { IsFenced bool // Specifies whether it's a fenced code block or an indented one Info []byte // This holds the info string FenceChar byte FenceLength int FenceOffset int } // TableCellData contains fields relevant to a TableCell node type. type TableCellData struct { IsHeader bool // This tells if it's under the header row Align CellAlignFlags // This holds the value for align attribute } // HeadingData contains fields relevant to a Heading node type. type HeadingData struct { Level int // This holds the heading level number HeadingID string // This might hold heading ID, if present IsTitleblock bool // Specifies whether it's a title block } // Node is a single element in the abstract syntax tree of the parsed document. // It holds connections to the structurally neighboring nodes and, for certain // types of nodes, additional information that might be needed when rendering. type Node struct { Type NodeType // Determines the type of the node Parent *Node // Points to the parent FirstChild *Node // Points to the first child, if any LastChild *Node // Points to the last child, if any Prev *Node // Previous sibling; nil if it's the first child Next *Node // Next sibling; nil if it's the last child Literal []byte // Text contents of the leaf nodes HeadingData // Populated if Type is Heading ListData // Populated if Type is List CodeBlockData // Populated if Type is CodeBlock LinkData // Populated if Type is Link TableCellData // Populated if Type is TableCell content []byte // Markdown content of the block nodes open bool // Specifies an open block node that has not been finished to process yet } // NewNode allocates a node of a specified type. func NewNode(typ NodeType) *Node { return &Node{ Type: typ, open: true, } } func (n *Node) String() string { ellipsis := "" snippet := n.Literal if len(snippet) > 16 { snippet = snippet[:16] ellipsis = "..." } return fmt.Sprintf("%s: '%s%s'", n.Type, snippet, ellipsis) } // Unlink removes node 'n' from the tree. // It panics if the node is nil. func (n *Node) Unlink() { if n.Prev != nil { n.Prev.Next = n.Next } else if n.Parent != nil { n.Parent.FirstChild = n.Next } if n.Next != nil { n.Next.Prev = n.Prev } else if n.Parent != nil { n.Parent.LastChild = n.Prev } n.Parent = nil n.Next = nil n.Prev = nil } // AppendChild adds a node 'child' as a child of 'n'. // It panics if either node is nil. func (n *Node) AppendChild(child *Node) { child.Unlink() child.Parent = n if n.LastChild != nil { n.LastChild.Next = child child.Prev = n.LastChild n.LastChild = child } else { n.FirstChild = child n.LastChild = child } } // InsertBefore inserts 'sibling' immediately before 'n'. // It panics if either node is nil. func (n *Node) InsertBefore(sibling *Node) { sibling.Unlink() sibling.Prev = n.Prev if sibling.Prev != nil { sibling.Prev.Next = sibling } sibling.Next = n n.Prev = sibling sibling.Parent = n.Parent if sibling.Prev == nil { sibling.Parent.FirstChild = sibling } } // IsContainer returns true if 'n' can contain children. func (n *Node) IsContainer() bool { switch n.Type { case Document: fallthrough case BlockQuote: fallthrough case List: fallthrough case Item: fallthrough case Paragraph: fallthrough case Heading: fallthrough case Emph: fallthrough case Strong: fallthrough case Del: fallthrough case Link: fallthrough case Image: fallthrough case Table: fallthrough case TableHead: fallthrough case TableBody: fallthrough case TableRow: fallthrough case TableCell: return true default: return false } } // IsLeaf returns true if 'n' is a leaf node. func (n *Node) IsLeaf() bool { return !n.IsContainer() } func (n *Node) canContain(t NodeType) bool { if n.Type == List { return t == Item } if n.Type == Document || n.Type == BlockQuote || n.Type == Item { return t != Item } if n.Type == Table { return t == TableHead || t == TableBody } if n.Type == TableHead || n.Type == TableBody { return t == TableRow } if n.Type == TableRow { return t == TableCell } return false } // WalkStatus allows NodeVisitor to have some control over the tree traversal. // It is returned from NodeVisitor and different values allow Node.Walk to // decide which node to go to next. type WalkStatus int const ( // GoToNext is the default traversal of every node. GoToNext WalkStatus = iota // SkipChildren tells walker to skip all children of current node. SkipChildren // Terminate tells walker to terminate the traversal. Terminate ) // NodeVisitor is a callback to be called when traversing the syntax tree. // Called twice for every node: once with entering=true when the branch is // first visited, then with entering=false after all the children are done. type NodeVisitor func(node *Node, entering bool) WalkStatus // Walk is a convenience method that instantiates a walker and starts a // traversal of subtree rooted at n. func (n *Node) Walk(visitor NodeVisitor) { w := newNodeWalker(n) for w.current != nil { status := visitor(w.current, w.entering) switch status { case GoToNext: w.next() case SkipChildren: w.entering = false w.next() case Terminate: return } } } type nodeWalker struct { current *Node root *Node entering bool } func newNodeWalker(root *Node) *nodeWalker { return &nodeWalker{ current: root, root: root, entering: true, } } func (nw *nodeWalker) next() { if (!nw.current.IsContainer() || !nw.entering) && nw.current == nw.root { nw.current = nil return } if nw.entering && nw.current.IsContainer() { if nw.current.FirstChild != nil { nw.current = nw.current.FirstChild nw.entering = true } else { nw.entering = false } } else if nw.current.Next == nil { nw.current = nw.current.Parent nw.entering = false } else { nw.current = nw.current.Next nw.entering = true } } func dump(ast *Node) { fmt.Println(dumpString(ast)) } func dumpR(ast *Node, depth int) string { if ast == nil { return "" } indent := bytes.Repeat([]byte("\t"), depth) content := ast.Literal if content == nil { content = ast.content } result := fmt.Sprintf("%s%s(%q)\n", indent, ast.Type, content) for n := ast.FirstChild; n != nil; n = n.Next { result += dumpR(n, depth+1) } return result } func dumpString(ast *Node) string { return dumpR(ast, 0) } ================================================ FILE: tests/tools/vendor/github.com/russross/blackfriday/v2/smartypants.go ================================================ // // Blackfriday Markdown Processor // Available at http://github.com/russross/blackfriday // // Copyright © 2011 Russ Ross . // Distributed under the Simplified BSD License. // See README.md for details. // // // // SmartyPants rendering // // package blackfriday import ( "bytes" "io" ) // SPRenderer is a struct containing state of a Smartypants renderer. type SPRenderer struct { inSingleQuote bool inDoubleQuote bool callbacks [256]smartCallback } func wordBoundary(c byte) bool { return c == 0 || isspace(c) || ispunct(c) } func tolower(c byte) byte { if c >= 'A' && c <= 'Z' { return c - 'A' + 'a' } return c } func isdigit(c byte) bool { return c >= '0' && c <= '9' } func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote byte, isOpen *bool, addNBSP bool) bool { // edge of the buffer is likely to be a tag that we don't get to see, // so we treat it like text sometimes // enumerate all sixteen possibilities for (previousChar, nextChar) // each can be one of {0, space, punct, other} switch { case previousChar == 0 && nextChar == 0: // context is not any help here, so toggle *isOpen = !*isOpen case isspace(previousChar) && nextChar == 0: // [ "] might be [ "foo...] *isOpen = true case ispunct(previousChar) && nextChar == 0: // [!"] hmm... could be [Run!"] or [("...] *isOpen = false case /* isnormal(previousChar) && */ nextChar == 0: // [a"] is probably a close *isOpen = false case previousChar == 0 && isspace(nextChar): // [" ] might be [...foo" ] *isOpen = false case isspace(previousChar) && isspace(nextChar): // [ " ] context is not any help here, so toggle *isOpen = !*isOpen case ispunct(previousChar) && isspace(nextChar): // [!" ] is probably a close *isOpen = false case /* isnormal(previousChar) && */ isspace(nextChar): // [a" ] this is one of the easy cases *isOpen = false case previousChar == 0 && ispunct(nextChar): // ["!] hmm... could be ["$1.95] or ["!...] *isOpen = false case isspace(previousChar) && ispunct(nextChar): // [ "!] looks more like [ "$1.95] *isOpen = true case ispunct(previousChar) && ispunct(nextChar): // [!"!] context is not any help here, so toggle *isOpen = !*isOpen case /* isnormal(previousChar) && */ ispunct(nextChar): // [a"!] is probably a close *isOpen = false case previousChar == 0 /* && isnormal(nextChar) */ : // ["a] is probably an open *isOpen = true case isspace(previousChar) /* && isnormal(nextChar) */ : // [ "a] this is one of the easy cases *isOpen = true case ispunct(previousChar) /* && isnormal(nextChar) */ : // [!"a] is probably an open *isOpen = true default: // [a'b] maybe a contraction? *isOpen = false } // Note that with the limited lookahead, this non-breaking // space will also be appended to single double quotes. if addNBSP && !*isOpen { out.WriteString(" ") } out.WriteByte('&') if *isOpen { out.WriteByte('l') } else { out.WriteByte('r') } out.WriteByte(quote) out.WriteString("quo;") if addNBSP && *isOpen { out.WriteString(" ") } return true } func (r *SPRenderer) smartSingleQuote(out *bytes.Buffer, previousChar byte, text []byte) int { if len(text) >= 2 { t1 := tolower(text[1]) if t1 == '\'' { nextChar := byte(0) if len(text) >= 3 { nextChar = text[2] } if smartQuoteHelper(out, previousChar, nextChar, 'd', &r.inDoubleQuote, false) { return 1 } } if (t1 == 's' || t1 == 't' || t1 == 'm' || t1 == 'd') && (len(text) < 3 || wordBoundary(text[2])) { out.WriteString("’") return 0 } if len(text) >= 3 { t2 := tolower(text[2]) if ((t1 == 'r' && t2 == 'e') || (t1 == 'l' && t2 == 'l') || (t1 == 'v' && t2 == 'e')) && (len(text) < 4 || wordBoundary(text[3])) { out.WriteString("’") return 0 } } } nextChar := byte(0) if len(text) > 1 { nextChar = text[1] } if smartQuoteHelper(out, previousChar, nextChar, 's', &r.inSingleQuote, false) { return 0 } out.WriteByte(text[0]) return 0 } func (r *SPRenderer) smartParens(out *bytes.Buffer, previousChar byte, text []byte) int { if len(text) >= 3 { t1 := tolower(text[1]) t2 := tolower(text[2]) if t1 == 'c' && t2 == ')' { out.WriteString("©") return 2 } if t1 == 'r' && t2 == ')' { out.WriteString("®") return 2 } if len(text) >= 4 && t1 == 't' && t2 == 'm' && text[3] == ')' { out.WriteString("™") return 3 } } out.WriteByte(text[0]) return 0 } func (r *SPRenderer) smartDash(out *bytes.Buffer, previousChar byte, text []byte) int { if len(text) >= 2 { if text[1] == '-' { out.WriteString("—") return 1 } if wordBoundary(previousChar) && wordBoundary(text[1]) { out.WriteString("–") return 0 } } out.WriteByte(text[0]) return 0 } func (r *SPRenderer) smartDashLatex(out *bytes.Buffer, previousChar byte, text []byte) int { if len(text) >= 3 && text[1] == '-' && text[2] == '-' { out.WriteString("—") return 2 } if len(text) >= 2 && text[1] == '-' { out.WriteString("–") return 1 } out.WriteByte(text[0]) return 0 } func (r *SPRenderer) smartAmpVariant(out *bytes.Buffer, previousChar byte, text []byte, quote byte, addNBSP bool) int { if bytes.HasPrefix(text, []byte(""")) { nextChar := byte(0) if len(text) >= 7 { nextChar = text[6] } if smartQuoteHelper(out, previousChar, nextChar, quote, &r.inDoubleQuote, addNBSP) { return 5 } } if bytes.HasPrefix(text, []byte("�")) { return 3 } out.WriteByte('&') return 0 } func (r *SPRenderer) smartAmp(angledQuotes, addNBSP bool) func(*bytes.Buffer, byte, []byte) int { var quote byte = 'd' if angledQuotes { quote = 'a' } return func(out *bytes.Buffer, previousChar byte, text []byte) int { return r.smartAmpVariant(out, previousChar, text, quote, addNBSP) } } func (r *SPRenderer) smartPeriod(out *bytes.Buffer, previousChar byte, text []byte) int { if len(text) >= 3 && text[1] == '.' && text[2] == '.' { out.WriteString("…") return 2 } if len(text) >= 5 && text[1] == ' ' && text[2] == '.' && text[3] == ' ' && text[4] == '.' { out.WriteString("…") return 4 } out.WriteByte(text[0]) return 0 } func (r *SPRenderer) smartBacktick(out *bytes.Buffer, previousChar byte, text []byte) int { if len(text) >= 2 && text[1] == '`' { nextChar := byte(0) if len(text) >= 3 { nextChar = text[2] } if smartQuoteHelper(out, previousChar, nextChar, 'd', &r.inDoubleQuote, false) { return 1 } } out.WriteByte(text[0]) return 0 } func (r *SPRenderer) smartNumberGeneric(out *bytes.Buffer, previousChar byte, text []byte) int { if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 { // is it of the form digits/digits(word boundary)?, i.e., \d+/\d+\b // note: check for regular slash (/) or fraction slash (⁄, 0x2044, or 0xe2 81 84 in utf-8) // and avoid changing dates like 1/23/2005 into fractions. numEnd := 0 for len(text) > numEnd && isdigit(text[numEnd]) { numEnd++ } if numEnd == 0 { out.WriteByte(text[0]) return 0 } denStart := numEnd + 1 if len(text) > numEnd+3 && text[numEnd] == 0xe2 && text[numEnd+1] == 0x81 && text[numEnd+2] == 0x84 { denStart = numEnd + 3 } else if len(text) < numEnd+2 || text[numEnd] != '/' { out.WriteByte(text[0]) return 0 } denEnd := denStart for len(text) > denEnd && isdigit(text[denEnd]) { denEnd++ } if denEnd == denStart { out.WriteByte(text[0]) return 0 } if len(text) == denEnd || wordBoundary(text[denEnd]) && text[denEnd] != '/' { out.WriteString("") out.Write(text[:numEnd]) out.WriteString("") out.Write(text[denStart:denEnd]) out.WriteString("") return denEnd - 1 } } out.WriteByte(text[0]) return 0 } func (r *SPRenderer) smartNumber(out *bytes.Buffer, previousChar byte, text []byte) int { if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 { if text[0] == '1' && text[1] == '/' && text[2] == '2' { if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' { out.WriteString("½") return 2 } } if text[0] == '1' && text[1] == '/' && text[2] == '4' { if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 5 && tolower(text[3]) == 't' && tolower(text[4]) == 'h') { out.WriteString("¼") return 2 } } if text[0] == '3' && text[1] == '/' && text[2] == '4' { if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 6 && tolower(text[3]) == 't' && tolower(text[4]) == 'h' && tolower(text[5]) == 's') { out.WriteString("¾") return 2 } } } out.WriteByte(text[0]) return 0 } func (r *SPRenderer) smartDoubleQuoteVariant(out *bytes.Buffer, previousChar byte, text []byte, quote byte) int { nextChar := byte(0) if len(text) > 1 { nextChar = text[1] } if !smartQuoteHelper(out, previousChar, nextChar, quote, &r.inDoubleQuote, false) { out.WriteString(""") } return 0 } func (r *SPRenderer) smartDoubleQuote(out *bytes.Buffer, previousChar byte, text []byte) int { return r.smartDoubleQuoteVariant(out, previousChar, text, 'd') } func (r *SPRenderer) smartAngledDoubleQuote(out *bytes.Buffer, previousChar byte, text []byte) int { return r.smartDoubleQuoteVariant(out, previousChar, text, 'a') } func (r *SPRenderer) smartLeftAngle(out *bytes.Buffer, previousChar byte, text []byte) int { i := 0 for i < len(text) && text[i] != '>' { i++ } out.Write(text[:i+1]) return i } type smartCallback func(out *bytes.Buffer, previousChar byte, text []byte) int // NewSmartypantsRenderer constructs a Smartypants renderer object. func NewSmartypantsRenderer(flags HTMLFlags) *SPRenderer { var ( r SPRenderer smartAmpAngled = r.smartAmp(true, false) smartAmpAngledNBSP = r.smartAmp(true, true) smartAmpRegular = r.smartAmp(false, false) smartAmpRegularNBSP = r.smartAmp(false, true) addNBSP = flags&SmartypantsQuotesNBSP != 0 ) if flags&SmartypantsAngledQuotes == 0 { r.callbacks['"'] = r.smartDoubleQuote if !addNBSP { r.callbacks['&'] = smartAmpRegular } else { r.callbacks['&'] = smartAmpRegularNBSP } } else { r.callbacks['"'] = r.smartAngledDoubleQuote if !addNBSP { r.callbacks['&'] = smartAmpAngled } else { r.callbacks['&'] = smartAmpAngledNBSP } } r.callbacks['\''] = r.smartSingleQuote r.callbacks['('] = r.smartParens if flags&SmartypantsDashes != 0 { if flags&SmartypantsLatexDashes == 0 { r.callbacks['-'] = r.smartDash } else { r.callbacks['-'] = r.smartDashLatex } } r.callbacks['.'] = r.smartPeriod if flags&SmartypantsFractions == 0 { r.callbacks['1'] = r.smartNumber r.callbacks['3'] = r.smartNumber } else { for ch := '1'; ch <= '9'; ch++ { r.callbacks[ch] = r.smartNumberGeneric } } r.callbacks['<'] = r.smartLeftAngle r.callbacks['`'] = r.smartBacktick return &r } // Process is the entry point of the Smartypants renderer. func (r *SPRenderer) Process(w io.Writer, text []byte) { mark := 0 for i := 0; i < len(text); i++ { if action := r.callbacks[text[i]]; action != nil { if i > mark { w.Write(text[mark:i]) } previousChar := byte(0) if i > 0 { previousChar = text[i-1] } var tmp bytes.Buffer i += action(&tmp, previousChar, text[i:]) w.Write(tmp.Bytes()) mark = i + 1 } } if mark < len(text) { w.Write(text[mark:]) } } ================================================ FILE: tests/tools/vendor/modules.txt ================================================ # github.com/cpuguy83/go-md2man/v2 v2.0.4 ## explicit; go 1.11 github.com/cpuguy83/go-md2man/v2 github.com/cpuguy83/go-md2man/v2/md2man # github.com/russross/blackfriday/v2 v2.1.0 ## explicit github.com/russross/blackfriday/v2 ================================================ FILE: tests/tutorial/tutorial.go ================================================ package main import ( "context" "errors" "fmt" "os" "path/filepath" "github.com/containers/buildah" "github.com/containers/buildah/define" "github.com/containers/buildah/imagebuildah" specs "github.com/opencontainers/runtime-spec/specs-go" "go.podman.io/storage" "go.podman.io/storage/pkg/unshare" ) func main() { if buildah.InitReexec() { return } unshare.MaybeReexecUsingUserNamespace(false) buildStoreOptions, err := storage.DefaultStoreOptions() if err != nil { panic(err) } buildStore, err := storage.GetStore(buildStoreOptions) if err != nil { panic(err) } defer func() { if _, err := buildStore.Shutdown(false); err != nil { if !errors.Is(err, storage.ErrLayerUsedByContainer) { fmt.Printf("failed to shutdown storage: %q", err) } } }() d, err := os.MkdirTemp("", "") if err != nil { panic(err) } defer os.RemoveAll(d) dockerfile := filepath.Join(d, "Dockerfile") f, err := os.Create(dockerfile) if err != nil { panic(err) } fmt.Fprintf(f, "FROM quay.io/libpod/alpine\nRUN echo CUT START; find /sys/fs/cgroup -print | sort ; echo CUT END") f.Close() buildOptions := define.BuildOptions{ ContextDirectory: d, NamespaceOptions: []define.NamespaceOption{ {Name: string(specs.NetworkNamespace), Host: true}, }, } _, _, err = imagebuildah.BuildDockerfiles(context.TODO(), buildStore, buildOptions, dockerfile) if err != nil { panic(err) } } ================================================ FILE: tests/tutorial.bats ================================================ #!/usr/bin/env bats load helpers @test "tutorial-cgroups" { # confidence check for the sake of packages that consume our library skip_if_no_runtime skip_if_rootless_environment skip_if_chroot _prefetch quay.io/libpod/alpine run ${TUTORIAL_BINARY} buildoutput="$output" # shouldn't have the "root" scope in our cgroups echo "build output:" echo "${output}" ! grep -q init.scope <<< "$buildoutput" run sed -e '0,/^CUT START/d' -e '/^CUT END/,//d' <<< "$buildoutput" # should've found a /sys/fs/cgroup with stuff in it echo "contents of /sys/fs/cgroup:" echo "${output}" echo "number of lines: ${#lines[@]}" test "${#lines[@]}" -gt 2 } ================================================ FILE: tests/umount.bats ================================================ #!/usr/bin/env bats load helpers @test "umount-flags-order-verification" { run_buildah 125 umount cnt1 -a check_options_flag_err "-a" run_buildah 125 umount cnt1 --all cnt2 check_options_flag_err "--all" run_buildah 125 umount cnt1 cnt2 --all check_options_flag_err "--all" } @test "umount one image" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid=$output run_buildah mount "$cid" run_buildah umount "$cid" } @test "umount bad image" { run_buildah 125 umount badcontainer } @test "umount multi images" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid1=$output run_buildah mount "$cid1" run_buildah from --quiet --pull-never $WITH_POLICY_JSON alpine cid2=$output run_buildah mount "$cid2" run_buildah from --quiet --pull-never $WITH_POLICY_JSON alpine cid3=$output run_buildah mount "$cid3" run_buildah umount "$cid1" "$cid2" "$cid3" } @test "umount all images" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid1=$output run_buildah mount "$cid1" run_buildah from --quiet --pull-never $WITH_POLICY_JSON alpine cid2=$output run_buildah mount "$cid2" run_buildah from --quiet --pull-never $WITH_POLICY_JSON alpine cid3=$output run_buildah mount "$cid3" run_buildah umount --all } @test "umount multi images one bad" { _prefetch alpine run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine cid1=$output run_buildah mount "$cid1" run_buildah from --quiet --pull-never $WITH_POLICY_JSON alpine cid2=$output run_buildah mount "$cid2" run_buildah from --quiet --pull-never $WITH_POLICY_JSON alpine cid3=$output run_buildah mount "$cid3" run_buildah 125 umount "$cid1" badcontainer "$cid2" "$cid3" } ================================================ FILE: tests/validate/buildahimages-are-sane ================================================ #!/bin/bash # # buildahimages-are-sane - validate changes against buildah-images Dockerfiles # ME=$(basename $0) # HEAD should be good enough, but the CIRRUS envariable allows us to test head=${CIRRUS_CHANGE_IN_REPO:-HEAD} # Base of this PR. Here we absolutely rely on cirrus. base=$(git merge-base ${GITVALIDATE_EPOCH:-main} $head) # Sanity check: if [[ -z "$base" ]]; then echo "$(basename $0): internal error: could not determine merge-base" echo " head = $head" echo " CIRRUS_CHANGE_IN_REPO = $CIRRUS_CHANGE_IN_REPO" echo " GITVALIDATE_EPOCH = $GITVALIDATE_EPOCH" exit 1 fi # Helper function: confirms that shadow-utils is sane in the built image function build_and_check() { local dir=$1 echo "$ME: Checking $dir" # Clean up preexisting image bin/buildah rmi -f buildah &>/dev/null || true # Quiet by default, but show logs if anything fails. logfile=$(mktemp --tmpdir $ME.build.XXXXXXX) bin/buildah bud -t buildah $dir > $logfile 2>&1 if [[ $? -ne 0 ]]; then echo "$ME: buildah-bud failed:" sed -e 's/^/ /' <$logfile exit 1 fi ctr=$(/usr/bin/buildah from buildah) rpmqv=$(/usr/bin/buildah run $ctr rpm -qV shadow-utils) if [[ -n "$rpmqv" ]]; then echo "$ME: rpm-qv failed on $dir:" echo " $rpmqv" exit 1 fi owner=$(buildah run $ctr stat -c "%U:%G" /home/build/.local/share/containers) if [[ "${owner}" != "build:build" ]]; then echo "$ME: ownership of /home/build/.local/share/containers failed on $dir:" echo " ${owner}" exit 1 fi bin/buildah rm $ctr &>/dev/null bin/buildah rmi buildah &>/dev/null } # This gives us a list of files touched in all commits, e.g. # A file1 # M subdir/file2 # We look for Added or Modified files under contrib/buildahimage; if there # aren't any, we have nothing to do. # # Notes: # --no-renames ensures that renamed files show up as 'A'dded. # we omit 'stablebyhand' because it does not have a Containerfile touched=$(git diff --name-status --no-renames $base $head |\ grep -v /stablebyhand |\ sed -n -E -e 's;^[AM][[:space:]]+(contrib/buildahimage/[^/]+)/.*;\1;p' |\ uniq) for dir in $touched; do build_and_check $dir done ================================================ FILE: tests/validate/pr-should-include-tests ================================================ #!/bin/bash # # Intended for use in CI: check git commits, barf if no tests added. # ME=$(basename $0) # Github label which allows overriding this check OVERRIDE_LABEL="No New Tests" # Docs-only changes are excused if [[ "${CIRRUS_CHANGE_TITLE}" =~ CI:DOCS ]]; then exit 0 fi # HEAD should be good enough, but the CIRRUS envariable allows us to test head=${CIRRUS_CHANGE_IN_REPO:-HEAD} # Base of this PR. Here we absolutely rely on cirrus. base=$(git merge-base ${GITVALIDATE_EPOCH:-main} $head) # Sanity check: if [[ -z "$base" ]]; then echo "$(basename $0): internal error: could not determine merge-base" echo " head = $head" echo " CIRRUS_CHANGE_IN_REPO = $CIRRUS_CHANGE_IN_REPO" echo " GITVALIDATE_EPOCH = $GITVALIDATE_EPOCH" exit 1 fi # This gives us a list of files touched in all commits, e.g. # A foo.c # M bar.c # We look for Added or Modified (not Deleted!) files under 'tests'. # --no-renames ensures that renamed tests show up as 'A'dded. if git diff --name-status --no-renames $base $head | grep -E -q '^[AM]\s+(tests/|.*_test\.go)'; then exit 0 fi # Nothing changed under test subdirectory. # # This is OK if the only files being touched are "safe" ones. filtered_changes=$(git diff --name-status $base $head | awk '{print $2}' | grep -F -vx .cirrus.yml | grep -F -vx .packit.yaml | grep -F -vx .gitignore | grep -F -vx changelog.txt | grep -F -vx go.mod | grep -F -vx go.sum | grep -F -vx buildah.spec.rpkg | grep -F -vx .golangci.yml | grep -F -vx Makefile | grep -E -v '^[^/]+\.md$' | grep -E -v '^\.github/' | grep -E -v '^contrib/' | grep -E -v '^docs/' | grep -E -v '^hack/' | grep -E -v '^nix/' | grep -E -v '^vendor/') if [[ -z "$filtered_changes" ]]; then exit 0 fi # Nope. Only allow if the github 'no-tests-needed' label is set if [[ -z "$CIRRUS_PR" ]]; then echo "$ME: cannot query github: \$CIRRUS_PR is undefined" >&2 exit 1 fi if [[ -z "$CIRRUS_REPO_CLONE_TOKEN" ]]; then echo "$ME: cannot query github: \$CIRRUS_REPO_CLONE_TOKEN is undefined" >&2 exit 1 fi query="{ \"query\": \"query { repository(owner: \\\"containers\\\", name: \\\"buildah\\\") { pullRequest(number: $CIRRUS_PR) { labels(first: 100) { nodes { name } } } } }\" }" result=$(curl -s -H "Authorization: bearer $CIRRUS_REPO_CLONE_TOKEN" -H "Accept: application/vnd.github.antiope-preview+json" -H "Content-Type: application/json" -X POST --data @- https://api.github.com/graphql <<<"$query") labels=$(jq -r '.data.repository.pullRequest.labels.nodes[].name' <<<"$result") if grep -F -x -q "$OVERRIDE_LABEL" <<<"$labels"; then # PR has the label set exit 0 fi cat <&2 exit 1 fi export CIRRUS_REPO_CLONE_TOKEN="$GITHUB_TOKEN" ############################################################################### # BEGIN test cases # # Feel free to add as needed. Syntax is: # = # comments # # Where: # exit status is the expected exit status of the script # sha of merge base is the SHA of the branch point of the commit # sha of commit is the SHA of a real commit in the podman repo # # We need the actual sha of the merge base because once a branch is # merged 'git merge-base' (used in our test script) becomes useless. # # # FIXME: as of 2021-01-07 we don't have "no tests needed" in our git # commit history, but once we do, please add a new '0' test here. # tests=" 0 f466086d 88bc27df 2955 two commits, includes tests 1 f466086d 4026fa96 2973 single commit, no tests 0 d460e2ed 371e4ca6 2886 .cirrus.yml and contrib/cirrus/* 0 88bc27df c5870ff8 2972 vendor only 0 d4c696af faa86c4f 2470 CI:DOCS as well as only a .md change 0 d460e2ed f52762a9 2927 .md only, without CI:DOCS 0 d80ec964 8a1bcd51 5366 no tests, allowed due to No New Tests label " # The script we're testing test_script=$(dirname $0)/$(basename $0 .t) # END test cases ############################################################################### # BEGIN test-script runner and status checker function run_test_script() { local expected_rc=$1 local testname=$2 testnum=$(( testnum + 1 )) # DO NOT COMBINE 'local output=...' INTO ONE LINE. If you do, you lose $? local output output=$( $test_script ) local actual_rc=$? if [[ $actual_rc != $expected_rc ]]; then echo "not ok $testnum $testname" echo "# expected rc $expected_rc" echo "# actual rc $actual_rc" if [[ -n "$output" ]]; then echo "# script output: $output" fi rc=1 else if [[ $expected_rc == 1 ]]; then # Confirm we get an error message if [[ ! "$output" =~ "Please write a regression test" ]]; then echo "not ok $testnum $testname" echo "# Expected: ~ 'Please write a regression test'" echo "# Actual: $output" rc=1 else echo "ok $testnum $testname - rc=$expected_rc" fi else echo "ok $testnum $testname - rc=$expected_rc" fi fi # If we expect an error, confirm that we can override it. We only need # to do this once. if [[ $expected_rc == 1 ]]; then if [[ -z "$tested_override" ]]; then testnum=$(( testnum + 1 )) CIRRUS_CHANGE_TITLE="[CI:DOCS] hi there" $test_script &>/dev/null if [[ $? -ne 0 ]]; then echo "not ok $testnum $testname (override with CI:DOCS)" rc=1 else echo "ok $testnum $testname (override with CI:DOCS)" fi tested_override=1 fi fi } # END test-script runner and status checker ############################################################################### # BEGIN test-case parsing rc=0 testnum=0 tested_override= while read expected_rc parent_sha commit_sha pr rest; do # Skip blank lines test -z "$expected_rc" && continue export GITVALIDATE_EPOCH=$parent_sha export CIRRUS_CHANGE_IN_REPO=$commit_sha export CIRRUS_CHANGE_TITLE=$(git log -1 --format=%s $commit_sha) export CIRRUS_CHANGE_MESSAGE= export CIRRUS_PR=$pr run_test_script $expected_rc "PR $pr - $rest" done <<<"$tests" echo "1..$testnum" exit $rc # END Test-case parsing ############################################################################### ================================================ FILE: tests/validate/whitespace.sh ================================================ #!/bin/sh # # Check for one or more whitespace characters at the end of a line in a markdown or text file. # gofmt is already going to be doing the same for source code. # status=0 if find * -name '*.md' -o -name "*.txt" | grep -v vendor/ | xargs grep -E -q '[[:space:]]+$' ; then echo "** ERROR: dangling whitespace found in these files: **" find * -name '*.md' -o -name "*.txt" | grep -v vendor/ | xargs grep -E -n '[[:space:]]+$' echo "** ERROR: try running \"sed -i -E -e 's,[[:space:]]+$,,'\" on the affected files **" status=1 fi exit $status ================================================ FILE: tests/wait/wait_notunix.go ================================================ //go:build !unix package main // This is really only here to prevent complaints about the source directory // for a helper that's used in a Unix-specific test not having something that // will compile on non-Unix platforms. func main() { } ================================================ FILE: tests/wait/wait_unix.go ================================================ package main import ( "fmt" "os" "os/exec" "path/filepath" "time" "github.com/sirupsen/logrus" "golang.org/x/sys/unix" ) func main() { if err := unix.Prctl(unix.PR_SET_CHILD_SUBREAPER, uintptr(1), 0, 0, 0); err != nil { fmt.Fprintf(os.Stderr, "%v", err) os.Exit(1) } if len(os.Args) < 2 { fmt.Fprintf(os.Stderr, "usage: %s [CMD ...]\n", filepath.Base(os.Args[0])) os.Exit(1) } cmd := exec.Command(os.Args[1], os.Args[2:]...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err := cmd.Run() caught := false for range 100 { wpid, err := unix.Wait4(-1, nil, unix.WNOHANG, nil) if err != nil { break } if wpid == 0 { time.Sleep(100 * time.Millisecond) } else { // log an error: the child process was expected to reap // its own reparented child processes; we shouldn't // have had to clean them up on its behalf logrus.Errorf("caught reparented child process %d", wpid) caught = true } } if !caught { if err == nil { return } fmt.Fprintf(os.Stderr, "%v", err) } os.Exit(1) } ================================================ FILE: troubleshooting.md ================================================ ![buildah logo](https://cdn.rawgit.com/containers/buildah/main/logos/buildah-logo_large.png) # Troubleshooting ## A list of common issues and solutions for Buildah --- ### 1) No such image When doing a `buildah pull` or `buildah build` command and a "common" image can not be pulled, it is likely that the `/etc/containers/registries.conf` file is either not installed or possibly misconfigured. This issue might also indicate that other required files as listed in the [Configuration Files](https://github.com/containers/buildah/blob/main/install.md#configuration-files) section of the Installation Instructions are also not installed. #### Symptom ```console $ sudo buildah build -f Dockerfile . STEP 1: FROM alpine error creating build container: 2 errors occurred: * Error determining manifest MIME type for docker://localhost/alpine:latest: pinging docker registry returned: Get https://localhost/v2/: dial tcp [::1]:443: connect: connection refused * Error determining manifest MIME type for docker://registry.access.redhat.com/alpine:latest: Error reading manifest latest in registry.access.redhat.com/alpine: unknown: Not Found error building: error creating build container: no such image "alpine" in registry: image not known ``` #### Solution * Verify that the `/etc/containers/registries.conf` file exists. If not, verify that the containers-common package is installed. * Verify that the entries in the `[registries.search]` section of the /etc/containers/registries file are valid and reachable. * Verify that the image you requested is either fully qualified, or that it exists on one of your search registries. * Verify that the image is public or that you have logged in to at least one search registry which contains the private image. * Verify that the other required [Configuration Files](https://github.com/containers/buildah/blob/main/install.md#configuration-files) are installed. --- ### 2) http: server gave HTTP response to HTTPS client When doing a Buildah command such as `build`, `commit`, `from`, or `push` to a registry, tls verification is turned on by default. If authentication is not used with those commands, this error can occur. #### Symptom ```console # buildah push alpine docker://localhost:5000/myalpine:latest Getting image source signatures Get https://localhost:5000/v2/: http: server gave HTTP response to HTTPS client ``` #### Solution By default tls verification is turned on when communicating to registries from Buildah. If the registry does not require authentication the Buildah commands such as `build`, `commit`, `from` and `pull` will fail unless tls verification is turned off using the `--tls-verify` option. **NOTE:** It is not at all recommended to communicate with a registry and not use tls verification. * Turn off tls verification by passing false to the tls-verification option. * I.e. `buildah push --tls-verify=false alpine docker://localhost:5000/myalpine:latest` --- ### 3) `buildah run` command fails with pipe or output redirection When doing a `buildah run` command while using a pipe ('|') or output redirection ('>>'), the command will fail, often times with a `command not found` type of error. #### Symptom When executing a `buildah run` command with a pipe or output redirection such as the following commands: ```console # buildah run $whalecontainer /usr/games/fortune -a | cowsay # buildah run $newcontainer echo "daemon off;" >> /etc/nginx/nginx.conf # buildah run $newcontainer echo "nginx on Fedora" > /usr/share/nginx/html/index.html ``` the `buildah run` command will not complete and an error will be raised. #### Solution There are two solutions to this problem. The [`podman run`](https://github.com/containers/podman/blob/main/docs/podman-run.1.md) command can be used in place of `buildah run`. To still use `buildah run`, surround the command with single quotes and use `bash -c`. The previous examples would be changed to: ```console # buildah run $whalecontainer bash -c '/usr/games/fortune -a | cowsay' # buildah run $newcontainer bash -c 'echo "daemon off;" >> /etc/nginx/nginx.conf' # buildah run $newcontainer bash -c 'echo "nginx on Fedora" > /usr/share/nginx/html/index.html' ``` --- ### 4) `buildah push alpine oci:~/myalpine:latest` fails with lstat error When doing a `buildah push` command and the target image has a tilde (`~`) character in it, an lstat error will be raised stating there is no such file or directory. This is expected behavior for shell expansion of the tilde character as it is only expanded at the start of a word. This behavior is documented [here](https://www.gnu.org/software/libc/manual/html_node/Tilde-Expansion.html). #### Symptom ```console $ sudo pull alpine $ sudo buildah push alpine oci:~/myalpine:latest lstat /home/myusername/~: no such file or directory ``` #### Solution * Replace `~` with `$HOME` or the fully specified directory `/home/myusername`. * `$ sudo buildah push alpine oci:${HOME}/myalpine:latest` --- ### 5) Rootless buildah build fails EPERM on NFS: NFS enforces file creation on different UIDs on the server side and does not understand user namespace, which rootless Podman requires. When a container root process like YUM attempts to create a file owned by a different UID, NFS Server denies the creation. NFS is also a problem for the file locks when the storage is on it. Other distributed file systems (for example: Lustre, Spectrum Scale, the General Parallel File System (GPFS)) are also not supported when running in rootless mode as these file systems do not understand user namespace. #### Symptom ```console $ buildah build . ERRO[0014] Error while applying layer: ApplyLayer exit status 1 stdout: stderr: open /root/.bash_logout: permission denied error creating build container: Error committing the finished image: error adding layer with blob "sha256:a02a4930cb5d36f3290eb84f4bfa30668ef2e9fe3a1fb73ec015fc58b9958b17": ApplyLayer exit status 1 stdout: stderr: open /root/.bash_logout: permission denied ``` #### Solution Choose one of the following: * Setup containers/storage in a different directory, not on an NFS share. * Otherwise just run buildah as root, via `sudo buildah` --- ### 6) Rootless buildah build fails when using OverlayFS: The Overlay file system (OverlayFS) requires the ability to call the `mknod` command when creating whiteout files when extracting an image. However, a rootless user does not have the privileges to use `mknod` in this capacity. #### Symptom ```console buildah build --storage-driver overlay . STEP 1: FROM docker.io/ubuntu:xenial Getting image source signatures Copying blob edf72af6d627 done Copying blob 3e4f86211d23 done Copying blob 8d3eac894db4 done Copying blob f7277927d38a done Copying config 5e13f8dd4c done Writing manifest to image destination Storing signatures Error: error creating build container: Error committing the finished image: error adding layer with blob "sha256:8d3eac894db4dc4154377ad28643dfe6625ff0e54bcfa63e0d04921f1a8ef7f8": Error processing tar file(exit status 1): operation not permitted $ buildah build . ERRO[0014] Error while applying layer: ApplyLayer exit status 1 stdout: stderr: open /root/.bash_logout: permission denied error creating build container: Error committing the finished image: error adding layer with blob "sha256:a02a4930cb5d36f3290eb84f4bfa30668ef2e9fe3a1fb73ec015fc58b9958b17": ApplyLayer exit status 1 stdout: stderr: open /root/.bash_logout: permission denied ``` #### Solution Choose one of the following: * Complete the build operation as a privileged user. * Install and configure fuse-overlayfs. * Install the fuse-overlayfs package for your Linux Distribution. * Add `mount_program = "/usr/bin/fuse-overlayfs"` under `[storage.options]` in your `~/.config/containers/storage.conf` file. --- ================================================ FILE: unmount.go ================================================ package buildah import "fmt" // Unmount unmounts a build container. func (b *Builder) Unmount() error { _, err := b.store.Unmount(b.ContainerID, false) if err != nil { return fmt.Errorf("unmounting build container %q: %w", b.ContainerID, err) } b.MountPoint = "" err = b.Save() if err != nil { return fmt.Errorf("saving updated state for build container %q: %w", b.ContainerID, err) } return nil } ================================================ FILE: util/types.go ================================================ package util //nolint:revive,nolintlint import ( "github.com/containers/buildah/define" ) const ( // DefaultRuntime if containers.conf fails. DefaultRuntime = define.DefaultRuntime ) var ( // Deprecated: DefaultCapabilities values should be retrieved from // github.com/containers/common/pkg/config DefaultCapabilities = define.DefaultCapabilities //nolint // Deprecated: DefaultNetworkSysctl values should be retrieved from // github.com/containers/common/pkg/config DefaultNetworkSysctl = define.DefaultNetworkSysctl //nolint ) ================================================ FILE: util/util.go ================================================ package util //nolint:revive,nolintlint import ( "errors" "fmt" "io" "net/url" "os" "path/filepath" "slices" "sort" "strings" "syscall" "github.com/containers/buildah/define" "github.com/docker/distribution/registry/api/errcode" "github.com/opencontainers/go-digest" specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" "go.podman.io/common/libimage" "go.podman.io/common/pkg/config" "go.podman.io/image/v5/docker/reference" "go.podman.io/image/v5/pkg/shortnames" "go.podman.io/image/v5/signature" "go.podman.io/image/v5/transports/alltransports" "go.podman.io/image/v5/types" "go.podman.io/storage" ) const ( minimumTruncatedIDLength = 3 // DefaultTransport is a prefix that we apply to an image name if we // can't find one in the local Store, in order to generate a source // reference for the image that we can then copy to the local Store. DefaultTransport = "docker://" ) // RegistryDefaultPathPrefix contains a per-registry listing of default prefixes // to prepend to image names that only contain a single path component. var RegistryDefaultPathPrefix = map[string]string{ "index.docker.io": "library", "docker.io": "library", } // StringInSlice is deprecated, use slices.Contains func StringInSlice(s string, slice []string) bool { return slices.Contains(slice, s) } // resolveName checks if name is a valid image name, and if that name doesn't // include a domain portion, returns a list of the names which it might // correspond to in the set of configured registries, and the transport used to // pull the image. // // The returned image names never include a transport: prefix, and if transport != "", // (transport, image) should be a valid input to alltransports.ParseImageName. // transport == "" indicates that image that already exists in a local storage, // and the name is valid for store.Image() / storage.Transport.ParseStoreReference(). // // NOTE: The "list of search registries is empty" check does not count blocked registries, // and neither the implied "localhost" nor a possible firstRegistry are counted func resolveName(name string, sc *types.SystemContext, store storage.Store) ([]string, string, error) { if name == "" { return nil, "", nil } // Maybe it's a truncated image ID. Don't prepend a registry name, then. if len(name) >= minimumTruncatedIDLength { if img, err := store.Image(name); err == nil && img != nil && strings.HasPrefix(img.ID, name) { // It's a truncated version of the ID of an image that's present in local storage; // we need only expand the ID. return []string{img.ID}, "", nil } } // If we're referring to an image by digest, it *must* be local and we // should not have any fall through/back logic. if strings.HasPrefix(name, "sha256:") { d, err := digest.Parse(name) if err != nil { return nil, "", err } img, err := store.Image(d.Encoded()) if err != nil { return nil, "", err } return []string{img.ID}, "", nil } // Transports are not supported for local image look ups. srcRef, err := alltransports.ParseImageName(name) if err == nil { return []string{srcRef.StringWithinTransport()}, srcRef.Transport().Name(), nil } var candidates []string // Local short-name resolution. namedCandidates, err := shortnames.ResolveLocally(sc, name) if err != nil { return nil, "", err } for _, named := range namedCandidates { candidates = append(candidates, named.String()) } return candidates, DefaultTransport, nil } // ExpandNames takes unqualified names, parses them as image names, and returns // the fully expanded result, including a tag. Names which don't include a registry // name will be marked for the most-preferred registry (i.e., the first one in our // configuration). func ExpandNames(names []string, systemContext *types.SystemContext, store storage.Store) ([]string, error) { expanded := make([]string, 0, len(names)) for _, n := range names { var name reference.Named nameList, _, err := resolveName(n, systemContext, store) if err != nil { return nil, fmt.Errorf("parsing name %q: %w", n, err) } if len(nameList) == 0 { named, err := reference.ParseNormalizedNamed(n) if err != nil { return nil, fmt.Errorf("parsing name %q: %w", n, err) } name = named } else { named, err := reference.ParseNormalizedNamed(nameList[0]) if err != nil { return nil, fmt.Errorf("parsing name %q: %w", nameList[0], err) } name = named } name = reference.TagNameOnly(name) expanded = append(expanded, name.String()) } return expanded, nil } // FindImage locates the locally-stored image which corresponds to a given // name. Please note that the second argument has been deprecated and has no // effect anymore. func FindImage(store storage.Store, _ string, systemContext *types.SystemContext, image string) (types.ImageReference, *storage.Image, error) { runtime, err := libimage.RuntimeFromStore(store, &libimage.RuntimeOptions{SystemContext: systemContext}) if err != nil { return nil, nil, err } localImage, _, err := runtime.LookupImage(image, nil) if err != nil { return nil, nil, err } ref, err := localImage.StorageReference() if err != nil { return nil, nil, err } return ref, localImage.StorageImage(), nil } // resolveNameToReferences tries to create a list of possible references // (including their transports) from the provided image name. func ResolveNameToReferences( store storage.Store, systemContext *types.SystemContext, image string, ) (refs []types.ImageReference, err error) { names, transport, err := resolveName(image, systemContext, store) if err != nil { return nil, fmt.Errorf("parsing name %q: %w", image, err) } if transport != DefaultTransport { transport += ":" } for _, name := range names { ref, err := alltransports.ParseImageName(transport + name) if err != nil { logrus.Debugf("error parsing reference to image %q: %v", name, err) continue } refs = append(refs, ref) } if len(refs) == 0 { return nil, fmt.Errorf("locating images with names %v", names) } return refs, nil } // AddImageNames adds the specified names to the specified image. Please note // that the second argument has been deprecated and has no effect anymore. func AddImageNames(store storage.Store, _ string, systemContext *types.SystemContext, image *storage.Image, addNames []string) error { runtime, err := libimage.RuntimeFromStore(store, &libimage.RuntimeOptions{SystemContext: systemContext}) if err != nil { return err } localImage, _, err := runtime.LookupImage(image.ID, nil) if err != nil { return err } for _, tag := range addNames { if err := localImage.Tag(tag); err != nil { return fmt.Errorf("tagging image %s: %w", image.ID, err) } } return nil } // GetFailureCause checks the type of the error "err" and returns a new // error message that reflects the reason of the failure. // In case err type is not a familiar one the error "defaultError" is returned. func GetFailureCause(err, defaultError error) error { switch nErr := err.(type) { case errcode.Errors: return err case errcode.Error, *url.Error: return nErr default: return defaultError } } // WriteError writes `lastError` into `w` if not nil and return the next error `err` func WriteError(w io.Writer, err error, lastError error) error { if lastError != nil { fmt.Fprintln(w, lastError) } return err } // Runtime is the default command to use to run the container. func Runtime() string { runtime := os.Getenv("BUILDAH_RUNTIME") if runtime != "" { return runtime } conf, err := config.Default() if err != nil { logrus.Warnf("Error loading default container config when searching for local runtime: %v", err) return define.DefaultRuntime } return conf.Engine.OCIRuntime } // GetContainerIDs uses ID mappings to compute the container-level IDs that will // correspond to a UID/GID pair on the host. func GetContainerIDs(uidmap, gidmap []specs.LinuxIDMapping, uid, gid uint32) (uint32, uint32, error) { uidMapped := true for _, m := range uidmap { uidMapped = false if uid >= m.HostID && uid < m.HostID+m.Size { uid = (uid - m.HostID) + m.ContainerID uidMapped = true break } } if !uidMapped { return 0, 0, fmt.Errorf("container uses ID mappings (%#v), but doesn't map UID %d", uidmap, uid) } gidMapped := true for _, m := range gidmap { gidMapped = false if gid >= m.HostID && gid < m.HostID+m.Size { gid = (gid - m.HostID) + m.ContainerID gidMapped = true break } } if !gidMapped { return 0, 0, fmt.Errorf("container uses ID mappings (%#v), but doesn't map GID %d", gidmap, gid) } return uid, gid, nil } // GetHostIDs uses ID mappings to compute the host-level IDs that will // correspond to a UID/GID pair in the container. func GetHostIDs(uidmap, gidmap []specs.LinuxIDMapping, uid, gid uint32) (uint32, uint32, error) { uidMapped := true for _, m := range uidmap { uidMapped = false if uid >= m.ContainerID && uid < m.ContainerID+m.Size { uid = (uid - m.ContainerID) + m.HostID uidMapped = true break } } if !uidMapped { return 0, 0, fmt.Errorf("container uses ID mappings (%#v), but doesn't map UID %d", uidmap, uid) } gidMapped := true for _, m := range gidmap { gidMapped = false if gid >= m.ContainerID && gid < m.ContainerID+m.Size { gid = (gid - m.ContainerID) + m.HostID gidMapped = true break } } if !gidMapped { return 0, 0, fmt.Errorf("container uses ID mappings (%#v), but doesn't map GID %d", gidmap, gid) } return uid, gid, nil } // GetHostRootIDs uses ID mappings in spec to compute the host-level IDs that will // correspond to UID/GID 0/0 in the container. func GetHostRootIDs(spec *specs.Spec) (uint32, uint32, error) { if spec == nil || spec.Linux == nil { return 0, 0, nil } return GetHostIDs(spec.Linux.UIDMappings, spec.Linux.GIDMappings, 0, 0) } // GetPolicyContext sets up, initializes and returns a new context for the specified policy func GetPolicyContext(ctx *types.SystemContext) (*signature.PolicyContext, error) { policy, err := signature.DefaultPolicy(ctx) if err != nil { return nil, err } policyContext, err := signature.NewPolicyContext(policy) if err != nil { return nil, err } return policyContext, nil } // logIfNotErrno logs the error message unless err is either nil or one of the // listed syscall.Errno values. It returns true if it logged an error. func logIfNotErrno(err error, what string, ignores ...syscall.Errno) (logged bool) { if err == nil { return false } if errno, ok := err.(syscall.Errno); ok && slices.Contains(ignores, errno) { return false } logrus.Error(what) return true } // LogIfNotRetryable logs "what" if err is set and is not an EINTR or EAGAIN // syscall.Errno. Returns "true" if we can continue. func LogIfNotRetryable(err error, what string) (retry bool) { return !logIfNotErrno(err, what, syscall.EINTR, syscall.EAGAIN) } // LogIfUnexpectedWhileDraining logs "what" if err is set and is not an EINTR // or EAGAIN or EIO syscall.Errno. func LogIfUnexpectedWhileDraining(err error, what string) { logIfNotErrno(err, what, syscall.EINTR, syscall.EAGAIN, syscall.EIO) } // TruncateString trims the given string to the provided maximum amount of // characters and shortens it with `...`. func TruncateString(str string, to int) string { newStr := str if len(str) > to { const tr = "..." if to > len(tr) { to -= len(tr) } newStr = str[0:to] + tr } return newStr } // fileExistsAndNotADir - Check to see if a file exists // and that it is not a directory. func fileExistsAndNotADir(path string) (bool, error) { file, err := os.Stat(path) if err != nil { if errors.Is(err, os.ErrNotExist) { return false, nil } return false, err } return !file.IsDir(), nil } // FindLocalRuntime find the local runtime of the // system searching through the config file for // possible locations. func FindLocalRuntime(runtime string) string { var localRuntime string conf, err := config.Default() if err != nil { logrus.Debugf("Error loading container config when searching for local runtime.") return localRuntime } for _, val := range conf.Engine.OCIRuntimes[runtime] { exists, err := fileExistsAndNotADir(val) if err != nil { logrus.Errorf("Failed to determine if file exists and is not a directory: %v", err) } if exists { localRuntime = val break } } return localRuntime } // MergeEnv merges two lists of environment variables, avoiding duplicates. func MergeEnv(defaults, overrides []string) []string { s := make([]string, 0, len(defaults)+len(overrides)) index := make(map[string]int) for _, envSpec := range append(defaults, overrides...) { envVar := strings.SplitN(envSpec, "=", 2) if i, ok := index[envVar[0]]; ok { s[i] = envSpec continue } s = append(s, envSpec) index[envVar[0]] = len(s) - 1 } return s } type byDestination []specs.Mount func (m byDestination) Len() int { return len(m) } func (m byDestination) Less(i, j int) bool { iparts, jparts := m.parts(i), m.parts(j) switch { case iparts < jparts: return true case iparts > jparts: return false } return filepath.Clean(m[i].Destination) < filepath.Clean(m[j].Destination) } func (m byDestination) Swap(i, j int) { m[i], m[j] = m[j], m[i] } func (m byDestination) parts(i int) int { return strings.Count(filepath.Clean(m[i].Destination), string(os.PathSeparator)) } func SortMounts(m []specs.Mount) []specs.Mount { sort.Stable(byDestination(m)) return m } func VerifyTagName(imageSpec string) (types.ImageReference, error) { ref, err := alltransports.ParseImageName(imageSpec) if err != nil { if ref, err = alltransports.ParseImageName(DefaultTransport + imageSpec); err != nil { return nil, err } } return ref, nil } ================================================ FILE: util/util_test.go ================================================ package util //nolint:revive,nolintlint import ( "os" "strconv" "testing" specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/stretchr/testify/assert" "go.podman.io/common/pkg/config" ) func TestMergeEnv(t *testing.T) { t.Parallel() tests := [][3][]string{ { []string{"A=B", "B=C", "C=D"}, nil, []string{"A=B", "B=C", "C=D"}, }, { nil, []string{"A=B", "B=C", "C=D"}, []string{"A=B", "B=C", "C=D"}, }, { []string{"A=B", "B=C", "C=D", "E=F"}, []string{"B=O", "F=G"}, []string{"A=B", "B=O", "C=D", "E=F", "F=G"}, }, } for i, test := range tests { t.Run(strconv.Itoa(i), func(t *testing.T) { result := MergeEnv(test[0], test[1]) if len(result) != len(test[2]) { t.Fatalf("expected %v, got %v", test[2], result) } for i := range result { if result[i] != test[2][i] { t.Fatalf("expected %v, got %v", test[2], result) } } }) } } func TestRuntime(t *testing.T) { t.Parallel() os.Setenv("CONTAINERS_CONF", "/dev/null") conf, _ := config.Default() defaultRuntime := conf.Engine.OCIRuntime runtime := Runtime() if runtime != defaultRuntime { t.Fatalf("expected %v, got %v", runtime, defaultRuntime) } defaultRuntime = "myoci" os.Setenv("BUILDAH_RUNTIME", defaultRuntime) runtime = Runtime() if runtime != defaultRuntime { t.Fatalf("expected %v, got %v", runtime, defaultRuntime) } } func TestMountsSort(t *testing.T) { t.Parallel() mounts1a := []specs.Mount{ { Source: "/a/bb/c", Destination: "/a/bb/c", }, { Source: "/a/b/c", Destination: "/a/b/c", }, { Source: "/a", Destination: "/a", }, { Source: "/a/b", Destination: "/a/b", }, { Source: "/d/e", Destination: "/a/c", }, { Source: "/b", Destination: "/b", }, { Source: "/", Destination: "/", }, { Source: "/a/b/c", Destination: "/aa/b/c", }, } mounts1b := []string{ "/", "/a", "/b", "/a/b", "/a/c", "/a/b/c", "/a/bb/c", "/aa/b/c", } sorted := SortMounts(mounts1a) sortedDests := make([]string, len(mounts1a)) for i := range sorted { sortedDests[i] = sorted[i].Destination } assert.Equalf(t, mounts1b, sortedDests, "sort returned results in unexpected by-destination order") } ================================================ FILE: util/util_unix.go ================================================ //go:build linux || darwin || freebsd || netbsd package util //nolint:revive,nolintlint import ( "os" "syscall" ) func UID(st os.FileInfo) int { return int(st.Sys().(*syscall.Stat_t).Uid) } func GID(st os.FileInfo) int { return int(st.Sys().(*syscall.Stat_t).Gid) } ================================================ FILE: util/util_unsupported.go ================================================ //go:build !linux package util //nolint:revive,nolintlint // IsCgroup2UnifiedMode returns whether we are running in cgroup 2 cgroup2 mode. func IsCgroup2UnifiedMode() (bool, error) { return false, nil } ================================================ FILE: util/util_windows.go ================================================ //go:build !linux && !darwin package util //nolint:revive,nolintlint import ( "os" ) func UID(st os.FileInfo) int { return 0 } func GID(st os.FileInfo) int { return 0 } ================================================ FILE: util.go ================================================ package buildah import ( "errors" "fmt" "io" "os" "path/filepath" "sync" "github.com/containers/buildah/copier" v1 "github.com/opencontainers/image-spec/specs-go/v1" rspec "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/selinux/go-selinux" "github.com/sirupsen/logrus" "go.podman.io/image/v5/docker/reference" "go.podman.io/image/v5/pkg/sysregistriesv2" "go.podman.io/image/v5/types" "go.podman.io/storage" "go.podman.io/storage/pkg/idtools" "go.podman.io/storage/pkg/reexec" ) // InitReexec is a wrapper for reexec.Init(). It should be called at // the start of main(), and if it returns true, main() should return // immediately. func InitReexec() bool { return reexec.Init() } func copyHistory(history []v1.History) []v1.History { if len(history) == 0 { return nil } h := make([]v1.History, 0, len(history)) for _, entry := range history { created := entry.Created if created != nil { timestamp := *created created = ×tamp } h = append(h, v1.History{ Created: created, CreatedBy: entry.CreatedBy, Author: entry.Author, Comment: entry.Comment, EmptyLayer: entry.EmptyLayer, }) } return h } func convertStorageIDMaps(UIDMap, GIDMap []idtools.IDMap) ([]rspec.LinuxIDMapping, []rspec.LinuxIDMapping) { uidmap := make([]rspec.LinuxIDMapping, 0, len(UIDMap)) gidmap := make([]rspec.LinuxIDMapping, 0, len(GIDMap)) for _, m := range UIDMap { uidmap = append(uidmap, rspec.LinuxIDMapping{ HostID: uint32(m.HostID), ContainerID: uint32(m.ContainerID), Size: uint32(m.Size), }) } for _, m := range GIDMap { gidmap = append(gidmap, rspec.LinuxIDMapping{ HostID: uint32(m.HostID), ContainerID: uint32(m.ContainerID), Size: uint32(m.Size), }) } return uidmap, gidmap } func convertRuntimeIDMaps(UIDMap, GIDMap []rspec.LinuxIDMapping) ([]idtools.IDMap, []idtools.IDMap) { uidmap := make([]idtools.IDMap, 0, len(UIDMap)) gidmap := make([]idtools.IDMap, 0, len(GIDMap)) for _, m := range UIDMap { uidmap = append(uidmap, idtools.IDMap{ HostID: int(m.HostID), ContainerID: int(m.ContainerID), Size: int(m.Size), }) } for _, m := range GIDMap { gidmap = append(gidmap, idtools.IDMap{ HostID: int(m.HostID), ContainerID: int(m.ContainerID), Size: int(m.Size), }) } return uidmap, gidmap } // isRegistryBlocked checks if the named registry is marked as blocked func isRegistryBlocked(registry string, sc *types.SystemContext) (bool, error) { reginfo, err := sysregistriesv2.FindRegistry(sc, registry) if err != nil { return false, fmt.Errorf("unable to parse the registries configuration (%s): %w", sysregistriesv2.ConfigurationSourceDescription(sc), err) } if reginfo != nil { if reginfo.Blocked { logrus.Debugf("registry %q is marked as blocked in registries configuration %q", registry, sysregistriesv2.ConfigurationSourceDescription(sc)) } else { logrus.Debugf("registry %q is not marked as blocked in registries configuration %q", registry, sysregistriesv2.ConfigurationSourceDescription(sc)) } return reginfo.Blocked, nil } logrus.Debugf("registry %q is not listed in registries configuration %q, assuming it's not blocked", registry, sysregistriesv2.ConfigurationSourceDescription(sc)) return false, nil } // isReferenceSomething checks if the registry part of a reference is insecure or blocked func isReferenceSomething(ref types.ImageReference, sc *types.SystemContext, what func(string, *types.SystemContext) (bool, error)) (bool, error) { if ref != nil { if named := ref.DockerReference(); named != nil { if domain := reference.Domain(named); domain != "" { return what(domain, sc) } } } return false, nil } // isReferenceBlocked checks if the registry part of a reference is blocked func isReferenceBlocked(ref types.ImageReference, sc *types.SystemContext) (bool, error) { if ref != nil && ref.Transport() != nil { switch ref.Transport().Name() { case "docker": return isReferenceSomething(ref, sc, isRegistryBlocked) } } return false, nil } // ReserveSELinuxLabels reads containers storage and reserves SELinux contexts // which are already being used by buildah containers. func ReserveSELinuxLabels(store storage.Store, id string) error { if selinuxGetEnabled() { containers, err := store.Containers() if err != nil { return fmt.Errorf("getting list of containers: %w", err) } for _, c := range containers { if id == c.ID { continue } b, err := OpenBuilder(store, c.ID) if err != nil { if errors.Is(err, os.ErrNotExist) { // Ignore not exist errors since containers probably created by other tool // TODO, we need to read other containers json data to reserve their SELinux labels continue } return err } // Prevent different containers from using same MCS label selinux.ReserveLabel(b.ProcessLabel) } } return nil } // IsContainer identifies if the specified container id is a buildah container // in the specified store. func IsContainer(id string, store storage.Store) (bool, error) { cdir, err := store.ContainerDirectory(id) if err != nil { return false, err } // Assuming that if the stateFile exists, that this is a Buildah // container. if _, err = os.Stat(filepath.Join(cdir, stateFile)); err != nil { if errors.Is(err, os.ErrNotExist) { return false, nil } return false, err } return true, nil } // Copy content from the directory "src" to the directory "dest", ensuring that // content from outside of "root" (which is a parent of "src" or "src" itself) // isn't read. func extractWithTar(root, src, dest string) error { var getErr, putErr error var wg sync.WaitGroup pipeReader, pipeWriter := io.Pipe() wg.Add(1) go func() { getErr = copier.Get(root, src, copier.GetOptions{}, []string{"."}, pipeWriter) pipeWriter.Close() wg.Done() }() wg.Add(1) go func() { putErr = copier.Put(dest, dest, copier.PutOptions{}, pipeReader) pipeReader.Close() wg.Done() }() wg.Wait() if getErr != nil { return fmt.Errorf("reading %q: %w", src, getErr) } if putErr != nil { return fmt.Errorf("copying contents of %q to %q: %w", src, dest, putErr) } return nil } ================================================ FILE: vendor/cyphar.com/go-pathrs/.golangci.yml ================================================ # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 SUSE LLC # Copyright (C) 2026 Aleksa Sarai # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. version: "2" linters: enable: - bidichk - cyclop - errname - errorlint - exhaustive - goconst - godot - gomoddirectives - gosec - mirror - misspell - mnd - nilerr - nilnil - perfsprint - prealloc - reassign - revive - unconvert - unparam - usestdlibvars - wastedassign formatters: enable: - gofumpt - goimports settings: goimports: local-prefixes: - cyphar.com/go-pathrs ================================================ FILE: vendor/cyphar.com/go-pathrs/COPYING ================================================ Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. ================================================ FILE: vendor/cyphar.com/go-pathrs/doc.go ================================================ // SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 SUSE LLC * Copyright (C) 2026 Aleksa Sarai * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ // Package pathrs provides bindings for libpathrs, a library for safe path // resolution on Linux. package pathrs ================================================ FILE: vendor/cyphar.com/go-pathrs/handle_linux.go ================================================ //go:build linux // SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 SUSE LLC * Copyright (C) 2026 Aleksa Sarai * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ package pathrs import ( "fmt" "os" "cyphar.com/go-pathrs/internal/fdutils" "cyphar.com/go-pathrs/internal/libpathrs" ) // Handle is a handle for a path within a given [Root]. This handle references // an already-resolved path which can be used for only one purpose -- to // "re-open" the handle and get an actual [os.File] which can be used for // ordinary operations. // // If you wish to open a file without having an intermediate [Handle] object, // you can try to use [Root.Open] or [Root.OpenFile]. // // It is critical that perform all relevant operations through this [Handle] // (rather than fetching the underlying [os.File] yourself with [Handle.IntoFile]), // because the security properties of libpathrs depend on users doing all // relevant filesystem operations through libpathrs. type Handle struct { inner *os.File } // HandleFromFile creates a new [Handle] from an existing file handle. The // handle will be copied by this method, so the original handle should still be // freed by the caller. // // This is effectively the inverse operation of [Handle.IntoFile], and is used // for "deserialising" pathrs root handles. func HandleFromFile(file *os.File) (*Handle, error) { newFile, err := fdutils.DupFile(file) if err != nil { return nil, fmt.Errorf("duplicate handle fd: %w", err) } return &Handle{inner: newFile}, nil } // Open creates an "upgraded" file handle to the file referenced by the // [Handle]. Note that the original [Handle] is not consumed by this operation, // and can be opened multiple times. // // The handle returned is only usable for reading, and this is method is // shorthand for [Handle.OpenFile] with os.O_RDONLY. // // TODO: Rename these to "Reopen" or something. func (h *Handle) Open() (*os.File, error) { return h.OpenFile(os.O_RDONLY) } // OpenFile creates an "upgraded" file handle to the file referenced by the // [Handle]. Note that the original [Handle] is not consumed by this operation, // and can be opened multiple times. // // The provided flags indicate which open(2) flags are used to create the new // handle. // // TODO: Rename these to "Reopen" or something. func (h *Handle) OpenFile(flags int) (*os.File, error) { return fdutils.WithFileFd(h.inner, func(fd uintptr) (*os.File, error) { newFd, err := libpathrs.Reopen(fd, flags) if err != nil { return nil, err } return os.NewFile(newFd, h.inner.Name()), nil }) } // IntoFile unwraps the [Handle] into its underlying [os.File]. // // You almost certainly want to use [Handle.OpenFile] to get a non-O_PATH // version of this [Handle]. // // This operation returns the internal [os.File] of the [Handle] directly, so // calling [Handle.Close] will also close any copies of the returned [os.File]. // If you want to get an independent copy, use [Handle.Clone] followed by // [Handle.IntoFile] on the cloned [Handle]. func (h *Handle) IntoFile() *os.File { // TODO: Figure out if we really don't want to make a copy. // TODO: We almost certainly want to clear r.inner here, but we can't do // that easily atomically (we could use atomic.Value but that'll make // things quite a bit uglier). return h.inner } // Clone creates a copy of a [Handle], such that it has a separate lifetime to // the original (while referring to the same underlying file). func (h *Handle) Clone() (*Handle, error) { return HandleFromFile(h.inner) } // Close frees all of the resources used by the [Handle]. func (h *Handle) Close() error { return h.inner.Close() } ================================================ FILE: vendor/cyphar.com/go-pathrs/internal/fdutils/fd_linux.go ================================================ //go:build linux // SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 SUSE LLC * Copyright (C) 2026 Aleksa Sarai * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ // Package fdutils contains a few helper methods when dealing with *os.File and // file descriptors. package fdutils import ( "fmt" "os" "golang.org/x/sys/unix" "cyphar.com/go-pathrs/internal/libpathrs" ) // DupFd makes a duplicate of the given fd. func DupFd(fd uintptr, name string) (*os.File, error) { newFd, err := unix.FcntlInt(fd, unix.F_DUPFD_CLOEXEC, 0) if err != nil { return nil, fmt.Errorf("fcntl(F_DUPFD_CLOEXEC): %w", err) } return os.NewFile(uintptr(newFd), name), nil } // WithFileFd is a more ergonomic wrapper around file.SyscallConn().Control(). func WithFileFd[T any](file *os.File, fn func(fd uintptr) (T, error)) (T, error) { conn, err := file.SyscallConn() if err != nil { return *new(T), err } var ( ret T innerErr error ) if err := conn.Control(func(fd uintptr) { ret, innerErr = fn(fd) }); err != nil { return *new(T), err } return ret, innerErr } // DupFile makes a duplicate of the given file. func DupFile(file *os.File) (*os.File, error) { return WithFileFd(file, func(fd uintptr) (*os.File, error) { return DupFd(fd, file.Name()) }) } // MkFile creates a new *os.File from the provided file descriptor. However, // unlike os.NewFile, the file's Name is based on the real path (provided by // /proc/self/fd/$n). func MkFile(fd uintptr) (*os.File, error) { fdPath := fmt.Sprintf("fd/%d", fd) fdName, err := libpathrs.ProcReadlinkat(libpathrs.ProcDefaultRootFd, libpathrs.ProcThreadSelf, fdPath) if err != nil { _ = unix.Close(int(fd)) return nil, fmt.Errorf("failed to fetch real name of fd %d: %w", fd, err) } // TODO: Maybe we should prefix this name with something to indicate to // users that they must not use this path as a "safe" path. Something like // "//pathrs-handle:/foo/bar"? return os.NewFile(fd, fdName), nil } ================================================ FILE: vendor/cyphar.com/go-pathrs/internal/libpathrs/error_unix.go ================================================ //go:build linux // TODO: Use "go:build unix" once we bump the minimum Go version 1.19. // SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 SUSE LLC * Copyright (C) 2026 Aleksa Sarai * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ package libpathrs import ( "syscall" ) // Error represents an underlying libpathrs error. type Error struct { description string errno syscall.Errno } // Error returns a textual description of the error. func (err *Error) Error() string { return err.description } // Unwrap returns the underlying error which was wrapped by this error (if // applicable). func (err *Error) Unwrap() error { if err.errno != 0 { return err.errno } return nil } ================================================ FILE: vendor/cyphar.com/go-pathrs/internal/libpathrs/libpathrs_linux.go ================================================ //go:build linux // SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 SUSE LLC * Copyright (C) 2026 Aleksa Sarai * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ // Package libpathrs is an internal thin wrapper around the libpathrs C API. package libpathrs import ( "fmt" "syscall" "unsafe" ) /* // TODO: Figure out if we need to add support for linking against libpathrs // statically even if in dynamically linked builds in order to make // packaging a bit easier (using "-Wl,-Bstatic -lpathrs -Wl,-Bdynamic" or // "-l:pathrs.a"). #cgo pkg-config: pathrs #include // This is a workaround for unsafe.Pointer() not working for non-void pointers. char *cast_ptr(void *ptr) { return ptr; } */ import "C" func fetchError(errID C.int) error { if errID >= C.__PATHRS_MAX_ERR_VALUE { return nil } cErr := C.pathrs_errorinfo(errID) defer C.pathrs_errorinfo_free(cErr) var err error if cErr != nil { err = &Error{ errno: syscall.Errno(cErr.saved_errno), description: C.GoString(cErr.description), } } return err } // OpenRoot wraps pathrs_open_root. func OpenRoot(path string) (uintptr, error) { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) fd := C.pathrs_open_root(cPath) return uintptr(fd), fetchError(fd) } // Reopen wraps pathrs_reopen. func Reopen(fd uintptr, flags int) (uintptr, error) { newFd := C.pathrs_reopen(C.int(fd), C.int(flags)) return uintptr(newFd), fetchError(newFd) } // InRootResolve wraps pathrs_inroot_resolve. func InRootResolve(rootFd uintptr, path string) (uintptr, error) { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) fd := C.pathrs_inroot_resolve(C.int(rootFd), cPath) return uintptr(fd), fetchError(fd) } // InRootResolveNoFollow wraps pathrs_inroot_resolve_nofollow. func InRootResolveNoFollow(rootFd uintptr, path string) (uintptr, error) { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) fd := C.pathrs_inroot_resolve_nofollow(C.int(rootFd), cPath) return uintptr(fd), fetchError(fd) } // InRootOpen wraps pathrs_inroot_open. func InRootOpen(rootFd uintptr, path string, flags int) (uintptr, error) { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) fd := C.pathrs_inroot_open(C.int(rootFd), cPath, C.int(flags)) return uintptr(fd), fetchError(fd) } // InRootReadlink wraps pathrs_inroot_readlink. func InRootReadlink(rootFd uintptr, path string) (string, error) { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) size := 128 for { linkBuf := make([]byte, size) n := C.pathrs_inroot_readlink(C.int(rootFd), cPath, C.cast_ptr(unsafe.Pointer(&linkBuf[0])), C.size_t(len(linkBuf))) switch { case int(n) < C.__PATHRS_MAX_ERR_VALUE: return "", fetchError(n) case int(n) <= len(linkBuf): return string(linkBuf[:int(n)]), nil default: // The contents were truncated. Unlike readlinkat, pathrs returns // the size of the link when it checked. So use the returned size // as a basis for the reallocated size (but in order to avoid a DoS // where a magic-link is growing by a single byte each iteration, // make sure we are a fair bit larger). size += int(n) } } } // InRootRmdir wraps pathrs_inroot_rmdir. func InRootRmdir(rootFd uintptr, path string) error { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) err := C.pathrs_inroot_rmdir(C.int(rootFd), cPath) return fetchError(err) } // InRootUnlink wraps pathrs_inroot_unlink. func InRootUnlink(rootFd uintptr, path string) error { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) err := C.pathrs_inroot_unlink(C.int(rootFd), cPath) return fetchError(err) } // InRootRemoveAll wraps pathrs_inroot_remove_all. func InRootRemoveAll(rootFd uintptr, path string) error { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) err := C.pathrs_inroot_remove_all(C.int(rootFd), cPath) return fetchError(err) } // InRootCreat wraps pathrs_inroot_creat. func InRootCreat(rootFd uintptr, path string, flags int, mode uint32) (uintptr, error) { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) fd := C.pathrs_inroot_creat(C.int(rootFd), cPath, C.int(flags), C.uint(mode)) return uintptr(fd), fetchError(fd) } // InRootRename wraps pathrs_inroot_rename. func InRootRename(rootFd uintptr, src, dst string, flags uint) error { cSrc := C.CString(src) defer C.free(unsafe.Pointer(cSrc)) cDst := C.CString(dst) defer C.free(unsafe.Pointer(cDst)) err := C.pathrs_inroot_rename(C.int(rootFd), cSrc, cDst, C.uint(flags)) return fetchError(err) } // InRootMkdir wraps pathrs_inroot_mkdir. func InRootMkdir(rootFd uintptr, path string, mode uint32) error { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) err := C.pathrs_inroot_mkdir(C.int(rootFd), cPath, C.uint(mode)) return fetchError(err) } // InRootMkdirAll wraps pathrs_inroot_mkdir_all. func InRootMkdirAll(rootFd uintptr, path string, mode uint32) (uintptr, error) { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) fd := C.pathrs_inroot_mkdir_all(C.int(rootFd), cPath, C.uint(mode)) return uintptr(fd), fetchError(fd) } // InRootMknod wraps pathrs_inroot_mknod. func InRootMknod(rootFd uintptr, path string, mode uint32, dev uint64) error { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) err := C.pathrs_inroot_mknod(C.int(rootFd), cPath, C.uint(mode), C.dev_t(dev)) return fetchError(err) } // InRootSymlink wraps pathrs_inroot_symlink. func InRootSymlink(rootFd uintptr, path, target string) error { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) cTarget := C.CString(target) defer C.free(unsafe.Pointer(cTarget)) err := C.pathrs_inroot_symlink(C.int(rootFd), cPath, cTarget) return fetchError(err) } // InRootHardlink wraps pathrs_inroot_hardlink. func InRootHardlink(rootFd uintptr, path, target string) error { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) cTarget := C.CString(target) defer C.free(unsafe.Pointer(cTarget)) err := C.pathrs_inroot_hardlink(C.int(rootFd), cPath, cTarget) return fetchError(err) } // ProcBase is pathrs_proc_base_t (uint64_t). type ProcBase C.pathrs_proc_base_t // FIXME: We need to open-code the constants because CGo unfortunately will // implicitly convert any non-literal constants (i.e. those resolved using gcc) // to signed integers. See for some // more information on the underlying issue (though. const ( // ProcRoot is PATHRS_PROC_ROOT. ProcRoot ProcBase = 0xFFFF_FFFE_7072_6F63 // C.PATHRS_PROC_ROOT // ProcSelf is PATHRS_PROC_SELF. ProcSelf ProcBase = 0xFFFF_FFFE_091D_5E1F // C.PATHRS_PROC_SELF // ProcThreadSelf is PATHRS_PROC_THREAD_SELF. ProcThreadSelf ProcBase = 0xFFFF_FFFE_3EAD_5E1F // C.PATHRS_PROC_THREAD_SELF // ProcBaseTypeMask is __PATHRS_PROC_TYPE_MASK. ProcBaseTypeMask ProcBase = 0xFFFF_FFFF_0000_0000 // C.__PATHRS_PROC_TYPE_MASK // ProcBaseTypePid is __PATHRS_PROC_TYPE_PID. ProcBaseTypePid ProcBase = 0x8000_0000_0000_0000 // C.__PATHRS_PROC_TYPE_PID // ProcDefaultRootFd is PATHRS_PROC_DEFAULT_ROOTFD. ProcDefaultRootFd = -int(syscall.EBADF) // C.PATHRS_PROC_DEFAULT_ROOTFD ) func assertEqual[T comparable](a, b T, msg string) { if a != b { panic(fmt.Sprintf("%s ((%T) %#v != (%T) %#v)", msg, a, a, b, b)) } } // Verify that the values above match the actual C values. Unfortunately, Go // only allows us to forcefully cast int64 to uint64 if you use a temporary // variable, which means we cannot do it in a const context and thus need to do // it at runtime (even though it is a check that fundamentally could be done at // compile-time)... func init() { var ( actualProcRoot int64 = C.PATHRS_PROC_ROOT actualProcSelf int64 = C.PATHRS_PROC_SELF actualProcThreadSelf int64 = C.PATHRS_PROC_THREAD_SELF ) assertEqual(ProcRoot, ProcBase(actualProcRoot), "PATHRS_PROC_ROOT") assertEqual(ProcSelf, ProcBase(actualProcSelf), "PATHRS_PROC_SELF") assertEqual(ProcThreadSelf, ProcBase(actualProcThreadSelf), "PATHRS_PROC_THREAD_SELF") var ( actualProcBaseTypeMask uint64 = C.__PATHRS_PROC_TYPE_MASK actualProcBaseTypePid uint64 = C.__PATHRS_PROC_TYPE_PID ) assertEqual(ProcBaseTypeMask, ProcBase(actualProcBaseTypeMask), "__PATHRS_PROC_TYPE_MASK") assertEqual(ProcBaseTypePid, ProcBase(actualProcBaseTypePid), "__PATHRS_PROC_TYPE_PID") assertEqual(ProcDefaultRootFd, int(C.PATHRS_PROC_DEFAULT_ROOTFD), "PATHRS_PROC_DEFAULT_ROOTFD") } // ProcPid reimplements the PROC_PID(x) conversion. func ProcPid(pid uint32) ProcBase { return ProcBaseTypePid | ProcBase(pid) } // ProcOpenat wraps pathrs_proc_openat. func ProcOpenat(procRootFd int, base ProcBase, path string, flags int) (uintptr, error) { cBase := C.pathrs_proc_base_t(base) cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) fd := C.pathrs_proc_openat(C.int(procRootFd), cBase, cPath, C.int(flags)) return uintptr(fd), fetchError(fd) } // ProcReadlinkat wraps pathrs_proc_readlinkat. func ProcReadlinkat(procRootFd int, base ProcBase, path string) (string, error) { // TODO: See if we can unify this code with InRootReadlink. cBase := C.pathrs_proc_base_t(base) cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) size := 128 for { linkBuf := make([]byte, size) n := C.pathrs_proc_readlinkat( C.int(procRootFd), cBase, cPath, C.cast_ptr(unsafe.Pointer(&linkBuf[0])), C.size_t(len(linkBuf))) switch { case int(n) < C.__PATHRS_MAX_ERR_VALUE: return "", fetchError(n) case int(n) <= len(linkBuf): return string(linkBuf[:int(n)]), nil default: // The contents were truncated. Unlike readlinkat, pathrs returns // the size of the link when it checked. So use the returned size // as a basis for the reallocated size (but in order to avoid a DoS // where a magic-link is growing by a single byte each iteration, // make sure we are a fair bit larger). size += int(n) } } } // ProcfsOpenHow is pathrs_procfs_open_how (struct). type ProcfsOpenHow C.pathrs_procfs_open_how const ( // ProcfsNewUnmasked is PATHRS_PROCFS_NEW_UNMASKED. ProcfsNewUnmasked = C.PATHRS_PROCFS_NEW_UNMASKED ) // Flags returns a pointer to the internal flags field to allow other packages // to modify structure fields that are internal due to Go's visibility model. func (how *ProcfsOpenHow) Flags() *C.uint64_t { return &how.flags } // ProcfsOpen is pathrs_procfs_open (sizeof(*how) is passed automatically). func ProcfsOpen(how *ProcfsOpenHow) (uintptr, error) { fd := C.pathrs_procfs_open((*C.pathrs_procfs_open_how)(how), C.size_t(unsafe.Sizeof(*how))) return uintptr(fd), fetchError(fd) } ================================================ FILE: vendor/cyphar.com/go-pathrs/procfs/procfs_linux.go ================================================ //go:build linux // SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 SUSE LLC * Copyright (C) 2026 Aleksa Sarai * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ // Package procfs provides a safe API for operating on /proc on Linux. package procfs import ( "os" "runtime" "cyphar.com/go-pathrs/internal/fdutils" "cyphar.com/go-pathrs/internal/libpathrs" ) // ProcBase is used with [ProcReadlink] and related functions to indicate what // /proc subpath path operations should be done relative to. type ProcBase struct { inner libpathrs.ProcBase } var ( // ProcRoot indicates to use /proc. Note that this mode may be more // expensive because we have to take steps to try to avoid leaking unmasked // procfs handles, so you should use [ProcBaseSelf] if you can. ProcRoot = ProcBase{inner: libpathrs.ProcRoot} // ProcSelf indicates to use /proc/self. For most programs, this is the // standard choice. ProcSelf = ProcBase{inner: libpathrs.ProcSelf} // ProcThreadSelf indicates to use /proc/thread-self. In multi-threaded // programs where one thread has a different CLONE_FS, it is possible for // /proc/self to point the wrong thread and so /proc/thread-self may be // necessary. ProcThreadSelf = ProcBase{inner: libpathrs.ProcThreadSelf} ) // ProcPid returns a ProcBase which indicates to use /proc/$pid for the given // PID (or TID). Be aware that due to PID recycling, using this is generally // not safe except in certain circumstances. Namely: // // - PID 1 (the init process), as that PID cannot ever get recycled. // - Your current PID (though you should just use [ProcBaseSelf]). // - Your current TID if you have used [runtime.LockOSThread] (though you // should just use [ProcBaseThreadSelf]). // - PIDs of child processes (as long as you are sure that no other part of // your program incorrectly catches or ignores SIGCHLD, and that you do it // *before* you call wait(2)or any equivalent method that could reap // zombies). func ProcPid(pid int) ProcBase { if pid < 0 || uint64(pid) >= 1<<31 { panic("invalid ProcBasePid value") // TODO: should this be an error? } pid32 := uint32(pid) //nolint:gosec // G115 false positive return ProcBase{inner: libpathrs.ProcPid(pid32)} } // ThreadCloser is a callback that needs to be called when you are done // operating on an [os.File] fetched using [Handle.OpenThreadSelf]. type ThreadCloser func() // Handle is a wrapper around an *os.File handle to "/proc", which can be // used to do further procfs-related operations in a safe way. type Handle struct { inner *os.File } // Close releases all internal resources for this [Handle]. // // Note that if the handle is actually the global cached handle, this operation // is a no-op. func (proc *Handle) Close() error { var err error if proc.inner != nil { err = proc.inner.Close() } return err } // OpenOption is a configuration function passed as an argument to [Open]. type OpenOption func(*libpathrs.ProcfsOpenHow) error // UnmaskedProcRoot can be passed to [Open] to request an unmasked procfs // handle be created. // // procfs, err := procfs.OpenRoot(procfs.UnmaskedProcRoot) func UnmaskedProcRoot(how *libpathrs.ProcfsOpenHow) error { *how.Flags() |= libpathrs.ProcfsNewUnmasked return nil } // Open creates a new [Handle] to a safe "/proc", based on the passed // configuration options (in the form of a series of [OpenOption]s). func Open(opts ...OpenOption) (*Handle, error) { var how libpathrs.ProcfsOpenHow for _, opt := range opts { if err := opt(&how); err != nil { return nil, err } } fd, err := libpathrs.ProcfsOpen(&how) if err != nil { return nil, err } var procFile *os.File if int(fd) >= 0 { procFile = os.NewFile(fd, "/proc") } // TODO: Check that fd == PATHRS_PROC_DEFAULT_ROOTFD in the <0 case? return &Handle{inner: procFile}, nil } // TODO: Switch to something fdutils.WithFileFd-like. func (proc *Handle) fd() int { if proc.inner != nil { return int(proc.inner.Fd()) } return libpathrs.ProcDefaultRootFd } // TODO: Should we expose open? func (proc *Handle) open(base ProcBase, path string, flags int) (_ *os.File, Closer ThreadCloser, Err error) { var closer ThreadCloser if base == ProcThreadSelf { runtime.LockOSThread() closer = runtime.UnlockOSThread } defer func() { if closer != nil && Err != nil { closer() Closer = nil } }() fd, err := libpathrs.ProcOpenat(proc.fd(), base.inner, path, flags) if err != nil { return nil, nil, err } file, err := fdutils.MkFile(fd) return file, closer, err } // OpenRoot safely opens a given path from inside /proc/. // // This function must only be used for accessing global information from procfs // (such as /proc/cpuinfo) or information about other processes (such as // /proc/1). Accessing your own process information should be done using // [Handle.OpenSelf] or [Handle.OpenThreadSelf]. func (proc *Handle) OpenRoot(path string, flags int) (*os.File, error) { file, closer, err := proc.open(ProcRoot, path, flags) if closer != nil { // should not happen panic("non-zero closer returned from procOpen(ProcRoot)") } return file, err } // OpenSelf safely opens a given path from inside /proc/self/. // // This method is recommend for getting process information about the current // process for almost all Go processes *except* for cases where there are // [runtime.LockOSThread] threads that have changed some aspect of their state // (such as through unshare(CLONE_FS) or changing namespaces). // // For such non-heterogeneous processes, /proc/self may reference to a task // that has different state from the current goroutine and so it may be // preferable to use [Handle.OpenThreadSelf]. The same is true if a user // really wants to inspect the current OS thread's information (such as // /proc/thread-self/stack or /proc/thread-self/status which is always uniquely // per-thread). // // Unlike [Handle.OpenThreadSelf], this method does not involve locking // the goroutine to the current OS thread and so is simpler to use and // theoretically has slightly less overhead. func (proc *Handle) OpenSelf(path string, flags int) (*os.File, error) { file, closer, err := proc.open(ProcSelf, path, flags) if closer != nil { // should not happen panic("non-zero closer returned from procOpen(ProcSelf)") } return file, err } // OpenPid safely opens a given path from inside /proc/$pid/, where pid can be // either a PID or TID. // // This is effectively equivalent to calling [Handle.OpenRoot] with the // pid prefixed to the subpath. // // Be aware that due to PID recycling, using this is generally not safe except // in certain circumstances. See the documentation of [ProcPid] for more // details. func (proc *Handle) OpenPid(pid int, path string, flags int) (*os.File, error) { file, closer, err := proc.open(ProcPid(pid), path, flags) if closer != nil { // should not happen panic("non-zero closer returned from procOpen(ProcPidOpen)") } return file, err } // OpenThreadSelf safely opens a given path from inside /proc/thread-self/. // // Most Go processes have heterogeneous threads (all threads have most of the // same kernel state such as CLONE_FS) and so [Handle.OpenSelf] is // preferable for most users. // // For non-heterogeneous threads, or users that actually want thread-specific // information (such as /proc/thread-self/stack or /proc/thread-self/status), // this method is necessary. // // Because Go can change the running OS thread of your goroutine without notice // (and then subsequently kill the old thread), this method will lock the // current goroutine to the OS thread (with [runtime.LockOSThread]) and the // caller is responsible for unlocking the the OS thread with the // [ThreadCloser] callback once they are done using the returned file. This // callback MUST be called AFTER you have finished using the returned // [os.File]. This callback is completely separate to [os.File.Close], so it // must be called regardless of how you close the handle. func (proc *Handle) OpenThreadSelf(path string, flags int) (*os.File, ThreadCloser, error) { return proc.open(ProcThreadSelf, path, flags) } // Readlink safely reads the contents of a symlink from the given procfs base. // // This is effectively equivalent to doing an Open*(O_PATH|O_NOFOLLOW) of the // path and then doing unix.Readlinkat(fd, ""), but with the benefit that // thread locking is not necessary for [ProcThreadSelf]. func (proc *Handle) Readlink(base ProcBase, path string) (string, error) { return libpathrs.ProcReadlinkat(proc.fd(), base.inner, path) } ================================================ FILE: vendor/cyphar.com/go-pathrs/root_linux.go ================================================ //go:build linux // SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 SUSE LLC * Copyright (C) 2026 Aleksa Sarai * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ package pathrs import ( "errors" "fmt" "os" "syscall" "cyphar.com/go-pathrs/internal/fdutils" "cyphar.com/go-pathrs/internal/libpathrs" ) // Root is a handle to the root of a directory tree to resolve within. The only // purpose of this "root handle" is to perform operations within the directory // tree, or to get a [Handle] to inodes within the directory tree. // // At time of writing, it is considered a *VERY BAD IDEA* to open a [Root] // inside a possibly-attacker-controlled directory tree. While we do have // protections that should defend against it, it's far more dangerous than just // opening a directory tree which is not inside a potentially-untrusted // directory. type Root struct { inner *os.File } // OpenRoot creates a new [Root] handle to the directory at the given path. func OpenRoot(path string) (*Root, error) { fd, err := libpathrs.OpenRoot(path) if err != nil { return nil, err } file, err := fdutils.MkFile(fd) if err != nil { return nil, err } return &Root{inner: file}, nil } // RootFromFile creates a new [Root] handle from an [os.File] referencing a // directory. The provided file will be duplicated, so the original file should // still be closed by the caller. // // This is effectively the inverse operation of [Root.IntoFile]. func RootFromFile(file *os.File) (*Root, error) { newFile, err := fdutils.DupFile(file) if err != nil { return nil, fmt.Errorf("duplicate root fd: %w", err) } return &Root{inner: newFile}, nil } // Resolve resolves the given path within the [Root]'s directory tree, and // returns a [Handle] to the resolved path. The path must already exist, // otherwise an error will occur. // // All symlinks (including trailing symlinks) are followed, but they are // resolved within the rootfs. If you wish to open a handle to the symlink // itself, use [ResolveNoFollow]. func (r *Root) Resolve(path string) (*Handle, error) { return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (*Handle, error) { handleFd, err := libpathrs.InRootResolve(rootFd, path) if err != nil { return nil, err } handleFile, err := fdutils.MkFile(handleFd) if err != nil { return nil, err } return &Handle{inner: handleFile}, nil }) } // ResolveNoFollow is effectively an O_NOFOLLOW version of [Resolve]. Their // behaviour is identical, except that *trailing* symlinks will not be // followed. If the final component is a trailing symlink, an O_PATH|O_NOFOLLOW // handle to the symlink itself is returned. func (r *Root) ResolveNoFollow(path string) (*Handle, error) { return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (*Handle, error) { handleFd, err := libpathrs.InRootResolveNoFollow(rootFd, path) if err != nil { return nil, err } handleFile, err := fdutils.MkFile(handleFd) if err != nil { return nil, err } return &Handle{inner: handleFile}, nil }) } // Open is effectively shorthand for [Resolve] followed by [Handle.Open], but // can be slightly more efficient (it reduces CGo overhead and the number of // syscalls used when using the openat2-based resolver) and is arguably more // ergonomic to use. // // This is effectively equivalent to [os.Open]. func (r *Root) Open(path string) (*os.File, error) { return r.OpenFile(path, os.O_RDONLY) } // OpenFile is effectively shorthand for [Resolve] followed by // [Handle.OpenFile], but can be slightly more efficient (it reduces CGo // overhead and the number of syscalls used when using the openat2-based // resolver) and is arguably more ergonomic to use. // // However, if flags contains os.O_NOFOLLOW and the path is a symlink, then // OpenFile's behaviour will match that of openat2. In most cases an error will // be returned, but if os.O_PATH is provided along with os.O_NOFOLLOW then a // file equivalent to [ResolveNoFollow] will be returned instead. // // This is effectively equivalent to [os.OpenFile], except that os.O_CREAT is // not supported. func (r *Root) OpenFile(path string, flags int) (*os.File, error) { return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (*os.File, error) { fd, err := libpathrs.InRootOpen(rootFd, path, flags) if err != nil { return nil, err } return fdutils.MkFile(fd) }) } // Create creates a file within the [Root]'s directory tree at the given path, // and returns a handle to the file. The provided mode is used for the new file // (the process's umask applies). // // Unlike [os.Create], if the file already exists an error is created rather // than the file being opened and truncated. func (r *Root) Create(path string, flags int, mode os.FileMode) (*os.File, error) { unixMode, err := toUnixMode(mode, false) if err != nil { return nil, err } return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (*os.File, error) { handleFd, err := libpathrs.InRootCreat(rootFd, path, flags, unixMode) if err != nil { return nil, err } return fdutils.MkFile(handleFd) }) } // Rename two paths within a [Root]'s directory tree. The flags argument is // identical to the RENAME_* flags to the renameat2(2) system call. func (r *Root) Rename(src, dst string, flags uint) error { _, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) { err := libpathrs.InRootRename(rootFd, src, dst, flags) return struct{}{}, err }) return err } // RemoveDir removes the named empty directory within a [Root]'s directory // tree. func (r *Root) RemoveDir(path string) error { _, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) { err := libpathrs.InRootRmdir(rootFd, path) return struct{}{}, err }) return err } // RemoveFile removes the named file within a [Root]'s directory tree. func (r *Root) RemoveFile(path string) error { _, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) { err := libpathrs.InRootUnlink(rootFd, path) return struct{}{}, err }) return err } // Remove removes the named file or (empty) directory within a [Root]'s // directory tree. // // This is effectively equivalent to [os.Remove]. func (r *Root) Remove(path string) error { // In order to match os.Remove's implementation we need to also do both // syscalls unconditionally and adjust the error based on whether // pathrs_inroot_rmdir() returned ENOTDIR. unlinkErr := r.RemoveFile(path) if unlinkErr == nil { return nil } rmdirErr := r.RemoveDir(path) if rmdirErr == nil { return nil } // Both failed, adjust the error in the same way that os.Remove does. err := rmdirErr if errors.Is(err, syscall.ENOTDIR) { err = unlinkErr } return err } // RemoveAll recursively deletes a path and all of its children. // // This is effectively equivalent to [os.RemoveAll]. func (r *Root) RemoveAll(path string) error { _, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) { err := libpathrs.InRootRemoveAll(rootFd, path) return struct{}{}, err }) return err } // Mkdir creates a directory within a [Root]'s directory tree. The provided // mode is used for the new directory (the process's umask applies). // // This is effectively equivalent to [os.Mkdir]. func (r *Root) Mkdir(path string, mode os.FileMode) error { unixMode, err := toUnixMode(mode, false) if err != nil { return err } _, err = fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) { err := libpathrs.InRootMkdir(rootFd, path, unixMode) return struct{}{}, err }) return err } // MkdirAll creates a directory (and any parent path components if they don't // exist) within a [Root]'s directory tree. The provided mode is used for any // directories created by this function (the process's umask applies). // // This is effectively equivalent to [os.MkdirAll]. func (r *Root) MkdirAll(path string, mode os.FileMode) (*Handle, error) { unixMode, err := toUnixMode(mode, false) if err != nil { return nil, err } return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (*Handle, error) { handleFd, err := libpathrs.InRootMkdirAll(rootFd, path, unixMode) if err != nil { return nil, err } handleFile, err := fdutils.MkFile(handleFd) if err != nil { return nil, err } return &Handle{inner: handleFile}, err }) } // Mknod creates a new device inode of the given type within a [Root]'s // directory tree. The provided mode is used for the new directory (the // process's umask applies). // // This is effectively equivalent to [golang.org/x/sys/unix.Mknod]. func (r *Root) Mknod(path string, mode os.FileMode, dev uint64) error { unixMode, err := toUnixMode(mode, true) if err != nil { return err } _, err = fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) { err := libpathrs.InRootMknod(rootFd, path, unixMode, dev) return struct{}{}, err }) return err } // Symlink creates a symlink within a [Root]'s directory tree. The symlink is // created at path and is a link to target. // // This is effectively equivalent to [os.Symlink]. func (r *Root) Symlink(path, target string) error { _, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) { err := libpathrs.InRootSymlink(rootFd, path, target) return struct{}{}, err }) return err } // Hardlink creates a hardlink within a [Root]'s directory tree. The hardlink // is created at path and is a link to target. Both paths are within the // [Root]'s directory tree (you cannot hardlink to a different [Root] or the // host). // // This is effectively equivalent to [os.Link]. func (r *Root) Hardlink(path, target string) error { _, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) { err := libpathrs.InRootHardlink(rootFd, path, target) return struct{}{}, err }) return err } // Readlink returns the target of a symlink with a [Root]'s directory tree. // // This is effectively equivalent to [os.Readlink]. func (r *Root) Readlink(path string) (string, error) { return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (string, error) { return libpathrs.InRootReadlink(rootFd, path) }) } // IntoFile unwraps the [Root] into its underlying [os.File]. // // It is critical that you do not operate on this file descriptor yourself, // because the security properties of libpathrs depend on users doing all // relevant filesystem operations through libpathrs. // // This operation returns the internal [os.File] of the [Root] directly, so // calling [Root.Close] will also close any copies of the returned [os.File]. // If you want to get an independent copy, use [Root.Clone] followed by // [Root.IntoFile] on the cloned [Root]. func (r *Root) IntoFile() *os.File { // TODO: Figure out if we really don't want to make a copy. // TODO: We almost certainly want to clear r.inner here, but we can't do // that easily atomically (we could use atomic.Value but that'll make // things quite a bit uglier). return r.inner } // Clone creates a copy of a [Root] handle, such that it has a separate // lifetime to the original (while referring to the same underlying directory). func (r *Root) Clone() (*Root, error) { return RootFromFile(r.inner) } // Close frees all of the resources used by the [Root] handle. func (r *Root) Close() error { return r.inner.Close() } ================================================ FILE: vendor/cyphar.com/go-pathrs/utils_linux.go ================================================ //go:build linux // SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 SUSE LLC * Copyright (C) 2026 Aleksa Sarai * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ package pathrs import ( "fmt" "os" "golang.org/x/sys/unix" ) //nolint:cyclop // this function needs to handle a lot of cases func toUnixMode(mode os.FileMode, needsType bool) (uint32, error) { sysMode := uint32(mode.Perm()) switch mode & os.ModeType { //nolint:exhaustive // we only care about ModeType bits case 0: if needsType { sysMode |= unix.S_IFREG } case os.ModeDir: sysMode |= unix.S_IFDIR case os.ModeSymlink: sysMode |= unix.S_IFLNK case os.ModeCharDevice | os.ModeDevice: sysMode |= unix.S_IFCHR case os.ModeDevice: sysMode |= unix.S_IFBLK case os.ModeNamedPipe: sysMode |= unix.S_IFIFO case os.ModeSocket: sysMode |= unix.S_IFSOCK default: return 0, fmt.Errorf("invalid mode filetype %+o", mode) } if mode&os.ModeSetuid != 0 { sysMode |= unix.S_ISUID } if mode&os.ModeSetgid != 0 { sysMode |= unix.S_ISGID } if mode&os.ModeSticky != 0 { sysMode |= unix.S_ISVTX } return sysMode, nil } ================================================ FILE: vendor/dario.cat/mergo/.deepsource.toml ================================================ version = 1 test_patterns = [ "*_test.go" ] [[analyzers]] name = "go" enabled = true [analyzers.meta] import_path = "dario.cat/mergo" ================================================ FILE: vendor/dario.cat/mergo/.gitignore ================================================ #### joe made this: http://goel.io/joe #### go #### # Binaries for programs and plugins *.exe *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Golang/Intellij .idea # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ #### vim #### # Swap [._]*.s[a-v][a-z] [._]*.sw[a-p] [._]s[a-v][a-z] [._]sw[a-p] # Session Session.vim # Temporary .netrwhist *~ # Auto-generated tag files tags ================================================ FILE: vendor/dario.cat/mergo/.travis.yml ================================================ language: go arch: - amd64 - ppc64le install: - go get -t - go get golang.org/x/tools/cmd/cover - go get github.com/mattn/goveralls script: - go test -race -v ./... after_script: - $HOME/gopath/bin/goveralls -service=travis-ci -repotoken $COVERALLS_TOKEN ================================================ FILE: vendor/dario.cat/mergo/CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at i@dario.im. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: vendor/dario.cat/mergo/CONTRIBUTING.md ================================================ # Contributing to mergo First off, thanks for taking the time to contribute! ❤️ All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉 > And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about: > - Star the project > - Tweet about it > - Refer this project in your project's readme > - Mention the project at local meetups and tell your friends/colleagues ## Table of Contents - [Code of Conduct](#code-of-conduct) - [I Have a Question](#i-have-a-question) - [I Want To Contribute](#i-want-to-contribute) - [Reporting Bugs](#reporting-bugs) - [Suggesting Enhancements](#suggesting-enhancements) ## Code of Conduct This project and everyone participating in it is governed by the [mergo Code of Conduct](https://github.com/imdario/mergoblob/master/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to <>. ## I Have a Question > If you want to ask a question, we assume that you have read the available [Documentation](https://pkg.go.dev/github.com/imdario/mergo). Before you ask a question, it is best to search for existing [Issues](https://github.com/imdario/mergo/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first. If you then still feel the need to ask a question and need clarification, we recommend the following: - Open an [Issue](https://github.com/imdario/mergo/issues/new). - Provide as much context as you can about what you're running into. - Provide project and platform versions (nodejs, npm, etc), depending on what seems relevant. We will then take care of the issue as soon as possible. ## I Want To Contribute > ### Legal Notice > When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license. ### Reporting Bugs #### Before Submitting a Bug Report A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible. - Make sure that you are using the latest version. - Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](). If you are looking for support, you might want to check [this section](#i-have-a-question)). - To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/imdario/mergoissues?q=label%3Abug). - Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue. - Collect information about the bug: - Stack trace (Traceback) - OS, Platform and Version (Windows, Linux, macOS, x86, ARM) - Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant. - Possibly your input and the output - Can you reliably reproduce the issue? And can you also reproduce it with older versions? #### How Do I Submit a Good Bug Report? > You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to . We use GitHub issues to track bugs and errors. If you run into an issue with the project: - Open an [Issue](https://github.com/imdario/mergo/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.) - Explain the behavior you would expect and the actual behavior. - Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case. - Provide the information you collected in the previous section. Once it's filed: - The project team will label the issue accordingly. - A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced. - If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be implemented by someone. ### Suggesting Enhancements This section guides you through submitting an enhancement suggestion for mergo, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions. #### Before Submitting an Enhancement - Make sure that you are using the latest version. - Read the [documentation]() carefully and find out if the functionality is already covered, maybe by an individual configuration. - Perform a [search](https://github.com/imdario/mergo/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. - Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library. #### How Do I Submit a Good Enhancement Suggestion? Enhancement suggestions are tracked as [GitHub issues](https://github.com/imdario/mergo/issues). - Use a **clear and descriptive title** for the issue to identify the suggestion. - Provide a **step-by-step description of the suggested enhancement** in as many details as possible. - **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you. - You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. - **Explain why this enhancement would be useful** to most mergo users. You may also want to point out the other projects that solved it better and which could serve as inspiration. ## Attribution This guide is based on the **contributing-gen**. [Make your own](https://github.com/bttger/contributing-gen)! ================================================ FILE: vendor/dario.cat/mergo/FUNDING.json ================================================ { "drips": { "ethereum": { "ownedBy": "0x6160020e7102237aC41bdb156e94401692D76930" } } } ================================================ FILE: vendor/dario.cat/mergo/LICENSE ================================================ Copyright (c) 2013 Dario Castañé. All rights reserved. Copyright (c) 2012 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: vendor/dario.cat/mergo/README.md ================================================ # Mergo [![GitHub release][5]][6] [![GoCard][7]][8] [![Test status][1]][2] [![OpenSSF Scorecard][21]][22] [![OpenSSF Best Practices][19]][20] [![Coverage status][9]][10] [![Sourcegraph][11]][12] [![FOSSA status][13]][14] [![GoDoc][3]][4] [![Become my sponsor][15]][16] [![Tidelift][17]][18] [1]: https://github.com/imdario/mergo/workflows/tests/badge.svg?branch=master [2]: https://github.com/imdario/mergo/actions/workflows/tests.yml [3]: https://godoc.org/github.com/imdario/mergo?status.svg [4]: https://godoc.org/github.com/imdario/mergo [5]: https://img.shields.io/github/release/imdario/mergo.svg [6]: https://github.com/imdario/mergo/releases [7]: https://goreportcard.com/badge/imdario/mergo [8]: https://goreportcard.com/report/github.com/imdario/mergo [9]: https://coveralls.io/repos/github/imdario/mergo/badge.svg?branch=master [10]: https://coveralls.io/github/imdario/mergo?branch=master [11]: https://sourcegraph.com/github.com/imdario/mergo/-/badge.svg [12]: https://sourcegraph.com/github.com/imdario/mergo?badge [13]: https://app.fossa.io/api/projects/git%2Bgithub.com%2Fimdario%2Fmergo.svg?type=shield [14]: https://app.fossa.io/projects/git%2Bgithub.com%2Fimdario%2Fmergo?ref=badge_shield [15]: https://img.shields.io/github/sponsors/imdario [16]: https://github.com/sponsors/imdario [17]: https://tidelift.com/badges/package/go/github.com%2Fimdario%2Fmergo [18]: https://tidelift.com/subscription/pkg/go-github.com-imdario-mergo [19]: https://bestpractices.coreinfrastructure.org/projects/7177/badge [20]: https://bestpractices.coreinfrastructure.org/projects/7177 [21]: https://api.securityscorecards.dev/projects/github.com/imdario/mergo/badge [22]: https://api.securityscorecards.dev/projects/github.com/imdario/mergo A helper to merge structs and maps in Golang. Useful for configuration default values, avoiding messy if-statements. Mergo merges same-type structs and maps by setting default values in zero-value fields. Mergo won't merge unexported (private) fields. It will do recursively any exported one. It also won't merge structs inside maps (because they are not addressable using Go reflection). Also a lovely [comune](http://en.wikipedia.org/wiki/Mergo) (municipality) in the Province of Ancona in the Italian region of Marche. ## Status Mergo is stable and frozen, ready for production. Check a short list of the projects using at large scale it [here](https://github.com/imdario/mergo#mergo-in-the-wild). No new features are accepted. They will be considered for a future v2 that improves the implementation and fixes bugs for corner cases. ### Important notes #### 1.0.0 In [1.0.0](//github.com/imdario/mergo/releases/tag/1.0.0) Mergo moves to a vanity URL `dario.cat/mergo`. No more v1 versions will be released. If the vanity URL is causing issues in your project due to a dependency pulling Mergo - it isn't a direct dependency in your project - it is recommended to use [replace](https://github.com/golang/go/wiki/Modules#when-should-i-use-the-replace-directive) to pin the version to the last one with the old import URL: ``` replace github.com/imdario/mergo => github.com/imdario/mergo v0.3.16 ``` #### 0.3.9 Please keep in mind that a problematic PR broke [0.3.9](//github.com/imdario/mergo/releases/tag/0.3.9). I reverted it in [0.3.10](//github.com/imdario/mergo/releases/tag/0.3.10), and I consider it stable but not bug-free. Also, this version adds support for go modules. Keep in mind that in [0.3.2](//github.com/imdario/mergo/releases/tag/0.3.2), Mergo changed `Merge()`and `Map()` signatures to support [transformers](#transformers). I added an optional/variadic argument so that it won't break the existing code. If you were using Mergo before April 6th, 2015, please check your project works as intended after updating your local copy with ```go get -u dario.cat/mergo```. I apologize for any issue caused by its previous behavior and any future bug that Mergo could cause in existing projects after the change (release 0.2.0). ### Donations If Mergo is useful to you, consider buying me a coffee, a beer, or making a monthly donation to allow me to keep building great free software. :heart_eyes: Donate using Liberapay Become my sponsor ### Mergo in the wild Mergo is used by [thousands](https://deps.dev/go/dario.cat%2Fmergo/v1.0.0/dependents) [of](https://deps.dev/go/github.com%2Fimdario%2Fmergo/v0.3.16/dependents) [projects](https://deps.dev/go/github.com%2Fimdario%2Fmergo/v0.3.12), including: * [containerd/containerd](https://github.com/containerd/containerd) * [datadog/datadog-agent](https://github.com/datadog/datadog-agent) * [docker/cli/](https://github.com/docker/cli/) * [goreleaser/goreleaser](https://github.com/goreleaser/goreleaser) * [go-micro/go-micro](https://github.com/go-micro/go-micro) * [grafana/loki](https://github.com/grafana/loki) * [masterminds/sprig](github.com/Masterminds/sprig) * [moby/moby](https://github.com/moby/moby) * [slackhq/nebula](https://github.com/slackhq/nebula) * [volcano-sh/volcano](https://github.com/volcano-sh/volcano) ## Install go get dario.cat/mergo // use in your .go code import ( "dario.cat/mergo" ) ## Usage You can only merge same-type structs with exported fields initialized as zero value of their type and same-types maps. Mergo won't merge unexported (private) fields but will do recursively any exported one. It won't merge empty structs value as [they are zero values](https://golang.org/ref/spec#The_zero_value) too. Also, maps will be merged recursively except for structs inside maps (because they are not addressable using Go reflection). ```go if err := mergo.Merge(&dst, src); err != nil { // ... } ``` Also, you can merge overwriting values using the transformer `WithOverride`. ```go if err := mergo.Merge(&dst, src, mergo.WithOverride); err != nil { // ... } ``` If you need to override pointers, so the source pointer's value is assigned to the destination's pointer, you must use `WithoutDereference`: ```go package main import ( "fmt" "dario.cat/mergo" ) type Foo struct { A *string B int64 } func main() { first := "first" second := "second" src := Foo{ A: &first, B: 2, } dest := Foo{ A: &second, B: 1, } mergo.Merge(&dest, src, mergo.WithOverride, mergo.WithoutDereference) } ``` Additionally, you can map a `map[string]interface{}` to a struct (and otherwise, from struct to map), following the same restrictions as in `Merge()`. Keys are capitalized to find each corresponding exported field. ```go if err := mergo.Map(&dst, srcMap); err != nil { // ... } ``` Warning: if you map a struct to map, it won't do it recursively. Don't expect Mergo to map struct members of your struct as `map[string]interface{}`. They will be just assigned as values. Here is a nice example: ```go package main import ( "fmt" "dario.cat/mergo" ) type Foo struct { A string B int64 } func main() { src := Foo{ A: "one", B: 2, } dest := Foo{ A: "two", } mergo.Merge(&dest, src) fmt.Println(dest) // Will print // {two 2} } ``` ### Transformers Transformers allow to merge specific types differently than in the default behavior. In other words, now you can customize how some types are merged. For example, `time.Time` is a struct; it doesn't have zero value but IsZero can return true because it has fields with zero value. How can we merge a non-zero `time.Time`? ```go package main import ( "fmt" "dario.cat/mergo" "reflect" "time" ) type timeTransformer struct { } func (t timeTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error { if typ == reflect.TypeOf(time.Time{}) { return func(dst, src reflect.Value) error { if dst.CanSet() { isZero := dst.MethodByName("IsZero") result := isZero.Call([]reflect.Value{}) if result[0].Bool() { dst.Set(src) } } return nil } } return nil } type Snapshot struct { Time time.Time // ... } func main() { src := Snapshot{time.Now()} dest := Snapshot{} mergo.Merge(&dest, src, mergo.WithTransformers(timeTransformer{})) fmt.Println(dest) // Will print // { 2018-01-12 01:15:00 +0000 UTC m=+0.000000001 } } ``` ## Contact me If I can help you, you have an idea or you are using Mergo in your projects, don't hesitate to drop me a line (or a pull request): [@im_dario](https://twitter.com/im_dario) ## About Written by [Dario Castañé](http://dario.im). ## License [BSD 3-Clause](http://opensource.org/licenses/BSD-3-Clause) license, as [Go language](http://golang.org/LICENSE). [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fimdario%2Fmergo.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fimdario%2Fmergo?ref=badge_large) ================================================ FILE: vendor/dario.cat/mergo/SECURITY.md ================================================ # Security Policy ## Supported Versions | Version | Supported | | ------- | ------------------ | | 1.x.x | :white_check_mark: | | < 1.0 | :x: | ## Security contact information To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. ================================================ FILE: vendor/dario.cat/mergo/doc.go ================================================ // Copyright 2013 Dario Castañé. All rights reserved. // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. /* A helper to merge structs and maps in Golang. Useful for configuration default values, avoiding messy if-statements. Mergo merges same-type structs and maps by setting default values in zero-value fields. Mergo won't merge unexported (private) fields. It will do recursively any exported one. It also won't merge structs inside maps (because they are not addressable using Go reflection). # Status It is ready for production use. It is used in several projects by Docker, Google, The Linux Foundation, VMWare, Shopify, etc. # Important notes 1.0.0 In 1.0.0 Mergo moves to a vanity URL `dario.cat/mergo`. 0.3.9 Please keep in mind that a problematic PR broke 0.3.9. We reverted it in 0.3.10. We consider 0.3.10 as stable but not bug-free. . Also, this version adds suppot for go modules. Keep in mind that in 0.3.2, Mergo changed Merge() and Map() signatures to support transformers. We added an optional/variadic argument so that it won't break the existing code. If you were using Mergo before April 6th, 2015, please check your project works as intended after updating your local copy with go get -u dario.cat/mergo. I apologize for any issue caused by its previous behavior and any future bug that Mergo could cause in existing projects after the change (release 0.2.0). # Install Do your usual installation procedure: go get dario.cat/mergo // use in your .go code import ( "dario.cat/mergo" ) # Usage You can only merge same-type structs with exported fields initialized as zero value of their type and same-types maps. Mergo won't merge unexported (private) fields but will do recursively any exported one. It won't merge empty structs value as they are zero values too. Also, maps will be merged recursively except for structs inside maps (because they are not addressable using Go reflection). if err := mergo.Merge(&dst, src); err != nil { // ... } Also, you can merge overwriting values using the transformer WithOverride. if err := mergo.Merge(&dst, src, mergo.WithOverride); err != nil { // ... } Additionally, you can map a map[string]interface{} to a struct (and otherwise, from struct to map), following the same restrictions as in Merge(). Keys are capitalized to find each corresponding exported field. if err := mergo.Map(&dst, srcMap); err != nil { // ... } Warning: if you map a struct to map, it won't do it recursively. Don't expect Mergo to map struct members of your struct as map[string]interface{}. They will be just assigned as values. Here is a nice example: package main import ( "fmt" "dario.cat/mergo" ) type Foo struct { A string B int64 } func main() { src := Foo{ A: "one", B: 2, } dest := Foo{ A: "two", } mergo.Merge(&dest, src) fmt.Println(dest) // Will print // {two 2} } # Transformers Transformers allow to merge specific types differently than in the default behavior. In other words, now you can customize how some types are merged. For example, time.Time is a struct; it doesn't have zero value but IsZero can return true because it has fields with zero value. How can we merge a non-zero time.Time? package main import ( "fmt" "dario.cat/mergo" "reflect" "time" ) type timeTransformer struct { } func (t timeTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error { if typ == reflect.TypeOf(time.Time{}) { return func(dst, src reflect.Value) error { if dst.CanSet() { isZero := dst.MethodByName("IsZero") result := isZero.Call([]reflect.Value{}) if result[0].Bool() { dst.Set(src) } } return nil } } return nil } type Snapshot struct { Time time.Time // ... } func main() { src := Snapshot{time.Now()} dest := Snapshot{} mergo.Merge(&dest, src, mergo.WithTransformers(timeTransformer{})) fmt.Println(dest) // Will print // { 2018-01-12 01:15:00 +0000 UTC m=+0.000000001 } } # Contact me If I can help you, you have an idea or you are using Mergo in your projects, don't hesitate to drop me a line (or a pull request): https://twitter.com/im_dario # About Written by Dario Castañé: https://da.rio.hn # License BSD 3-Clause license, as Go language. */ package mergo ================================================ FILE: vendor/dario.cat/mergo/map.go ================================================ // Copyright 2014 Dario Castañé. All rights reserved. // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Based on src/pkg/reflect/deepequal.go from official // golang's stdlib. package mergo import ( "fmt" "reflect" "unicode" "unicode/utf8" ) func changeInitialCase(s string, mapper func(rune) rune) string { if s == "" { return s } r, n := utf8.DecodeRuneInString(s) return string(mapper(r)) + s[n:] } func isExported(field reflect.StructField) bool { r, _ := utf8.DecodeRuneInString(field.Name) return r >= 'A' && r <= 'Z' } // Traverses recursively both values, assigning src's fields values to dst. // The map argument tracks comparisons that have already been seen, which allows // short circuiting on recursive types. func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int, config *Config) (err error) { overwrite := config.Overwrite if dst.CanAddr() { addr := dst.UnsafeAddr() h := 17 * addr seen := visited[h] typ := dst.Type() for p := seen; p != nil; p = p.next { if p.ptr == addr && p.typ == typ { return nil } } // Remember, remember... visited[h] = &visit{typ, seen, addr} } zeroValue := reflect.Value{} switch dst.Kind() { case reflect.Map: dstMap := dst.Interface().(map[string]interface{}) for i, n := 0, src.NumField(); i < n; i++ { srcType := src.Type() field := srcType.Field(i) if !isExported(field) { continue } fieldName := field.Name fieldName = changeInitialCase(fieldName, unicode.ToLower) if _, ok := dstMap[fieldName]; !ok || (!isEmptyValue(reflect.ValueOf(src.Field(i).Interface()), !config.ShouldNotDereference) && overwrite) || config.overwriteWithEmptyValue { dstMap[fieldName] = src.Field(i).Interface() } } case reflect.Ptr: if dst.IsNil() { v := reflect.New(dst.Type().Elem()) dst.Set(v) } dst = dst.Elem() fallthrough case reflect.Struct: srcMap := src.Interface().(map[string]interface{}) for key := range srcMap { config.overwriteWithEmptyValue = true srcValue := srcMap[key] fieldName := changeInitialCase(key, unicode.ToUpper) dstElement := dst.FieldByName(fieldName) if dstElement == zeroValue { // We discard it because the field doesn't exist. continue } srcElement := reflect.ValueOf(srcValue) dstKind := dstElement.Kind() srcKind := srcElement.Kind() if srcKind == reflect.Ptr && dstKind != reflect.Ptr { srcElement = srcElement.Elem() srcKind = reflect.TypeOf(srcElement.Interface()).Kind() } else if dstKind == reflect.Ptr { // Can this work? I guess it can't. if srcKind != reflect.Ptr && srcElement.CanAddr() { srcPtr := srcElement.Addr() srcElement = reflect.ValueOf(srcPtr) srcKind = reflect.Ptr } } if !srcElement.IsValid() { continue } if srcKind == dstKind { if err = deepMerge(dstElement, srcElement, visited, depth+1, config); err != nil { return } } else if dstKind == reflect.Interface && dstElement.Kind() == reflect.Interface { if err = deepMerge(dstElement, srcElement, visited, depth+1, config); err != nil { return } } else if srcKind == reflect.Map { if err = deepMap(dstElement, srcElement, visited, depth+1, config); err != nil { return } } else { return fmt.Errorf("type mismatch on %s field: found %v, expected %v", fieldName, srcKind, dstKind) } } } return } // Map sets fields' values in dst from src. // src can be a map with string keys or a struct. dst must be the opposite: // if src is a map, dst must be a valid pointer to struct. If src is a struct, // dst must be map[string]interface{}. // It won't merge unexported (private) fields and will do recursively // any exported field. // If dst is a map, keys will be src fields' names in lower camel case. // Missing key in src that doesn't match a field in dst will be skipped. This // doesn't apply if dst is a map. // This is separated method from Merge because it is cleaner and it keeps sane // semantics: merging equal types, mapping different (restricted) types. func Map(dst, src interface{}, opts ...func(*Config)) error { return _map(dst, src, opts...) } // MapWithOverwrite will do the same as Map except that non-empty dst attributes will be overridden by // non-empty src attribute values. // Deprecated: Use Map(…) with WithOverride func MapWithOverwrite(dst, src interface{}, opts ...func(*Config)) error { return _map(dst, src, append(opts, WithOverride)...) } func _map(dst, src interface{}, opts ...func(*Config)) error { if dst != nil && reflect.ValueOf(dst).Kind() != reflect.Ptr { return ErrNonPointerArgument } var ( vDst, vSrc reflect.Value err error ) config := &Config{} for _, opt := range opts { opt(config) } if vDst, vSrc, err = resolveValues(dst, src); err != nil { return err } // To be friction-less, we redirect equal-type arguments // to deepMerge. Only because arguments can be anything. if vSrc.Kind() == vDst.Kind() { return deepMerge(vDst, vSrc, make(map[uintptr]*visit), 0, config) } switch vSrc.Kind() { case reflect.Struct: if vDst.Kind() != reflect.Map { return ErrExpectedMapAsDestination } case reflect.Map: if vDst.Kind() != reflect.Struct { return ErrExpectedStructAsDestination } default: return ErrNotSupported } return deepMap(vDst, vSrc, make(map[uintptr]*visit), 0, config) } ================================================ FILE: vendor/dario.cat/mergo/merge.go ================================================ // Copyright 2013 Dario Castañé. All rights reserved. // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Based on src/pkg/reflect/deepequal.go from official // golang's stdlib. package mergo import ( "fmt" "reflect" ) func hasMergeableFields(dst reflect.Value) (exported bool) { for i, n := 0, dst.NumField(); i < n; i++ { field := dst.Type().Field(i) if field.Anonymous && dst.Field(i).Kind() == reflect.Struct { exported = exported || hasMergeableFields(dst.Field(i)) } else if isExportedComponent(&field) { exported = exported || len(field.PkgPath) == 0 } } return } func isExportedComponent(field *reflect.StructField) bool { pkgPath := field.PkgPath if len(pkgPath) > 0 { return false } c := field.Name[0] if 'a' <= c && c <= 'z' || c == '_' { return false } return true } type Config struct { Transformers Transformers Overwrite bool ShouldNotDereference bool AppendSlice bool TypeCheck bool overwriteWithEmptyValue bool overwriteSliceWithEmptyValue bool sliceDeepCopy bool debug bool } type Transformers interface { Transformer(reflect.Type) func(dst, src reflect.Value) error } // Traverses recursively both values, assigning src's fields values to dst. // The map argument tracks comparisons that have already been seen, which allows // short circuiting on recursive types. func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int, config *Config) (err error) { overwrite := config.Overwrite typeCheck := config.TypeCheck overwriteWithEmptySrc := config.overwriteWithEmptyValue overwriteSliceWithEmptySrc := config.overwriteSliceWithEmptyValue sliceDeepCopy := config.sliceDeepCopy if !src.IsValid() { return } if dst.CanAddr() { addr := dst.UnsafeAddr() h := 17 * addr seen := visited[h] typ := dst.Type() for p := seen; p != nil; p = p.next { if p.ptr == addr && p.typ == typ { return nil } } // Remember, remember... visited[h] = &visit{typ, seen, addr} } if config.Transformers != nil && !isReflectNil(dst) && dst.IsValid() { if fn := config.Transformers.Transformer(dst.Type()); fn != nil { err = fn(dst, src) return } } switch dst.Kind() { case reflect.Struct: if hasMergeableFields(dst) { for i, n := 0, dst.NumField(); i < n; i++ { if err = deepMerge(dst.Field(i), src.Field(i), visited, depth+1, config); err != nil { return } } } else { if dst.CanSet() && (isReflectNil(dst) || overwrite) && (!isEmptyValue(src, !config.ShouldNotDereference) || overwriteWithEmptySrc) { dst.Set(src) } } case reflect.Map: if dst.IsNil() && !src.IsNil() { if dst.CanSet() { dst.Set(reflect.MakeMap(dst.Type())) } else { dst = src return } } if src.Kind() != reflect.Map { if overwrite && dst.CanSet() { dst.Set(src) } return } for _, key := range src.MapKeys() { srcElement := src.MapIndex(key) if !srcElement.IsValid() { continue } dstElement := dst.MapIndex(key) switch srcElement.Kind() { case reflect.Chan, reflect.Func, reflect.Map, reflect.Interface, reflect.Slice: if srcElement.IsNil() { if overwrite { dst.SetMapIndex(key, srcElement) } continue } fallthrough default: if !srcElement.CanInterface() { continue } switch reflect.TypeOf(srcElement.Interface()).Kind() { case reflect.Struct: fallthrough case reflect.Ptr: fallthrough case reflect.Map: srcMapElm := srcElement dstMapElm := dstElement if srcMapElm.CanInterface() { srcMapElm = reflect.ValueOf(srcMapElm.Interface()) if dstMapElm.IsValid() { dstMapElm = reflect.ValueOf(dstMapElm.Interface()) } } if err = deepMerge(dstMapElm, srcMapElm, visited, depth+1, config); err != nil { return } case reflect.Slice: srcSlice := reflect.ValueOf(srcElement.Interface()) var dstSlice reflect.Value if !dstElement.IsValid() || dstElement.IsNil() { dstSlice = reflect.MakeSlice(srcSlice.Type(), 0, srcSlice.Len()) } else { dstSlice = reflect.ValueOf(dstElement.Interface()) } if (!isEmptyValue(src, !config.ShouldNotDereference) || overwriteWithEmptySrc || overwriteSliceWithEmptySrc) && (overwrite || isEmptyValue(dst, !config.ShouldNotDereference)) && !config.AppendSlice && !sliceDeepCopy { if typeCheck && srcSlice.Type() != dstSlice.Type() { return fmt.Errorf("cannot override two slices with different type (%s, %s)", srcSlice.Type(), dstSlice.Type()) } dstSlice = srcSlice } else if config.AppendSlice { if srcSlice.Type() != dstSlice.Type() { return fmt.Errorf("cannot append two slices with different type (%s, %s)", srcSlice.Type(), dstSlice.Type()) } dstSlice = reflect.AppendSlice(dstSlice, srcSlice) } else if sliceDeepCopy { i := 0 for ; i < srcSlice.Len() && i < dstSlice.Len(); i++ { srcElement := srcSlice.Index(i) dstElement := dstSlice.Index(i) if srcElement.CanInterface() { srcElement = reflect.ValueOf(srcElement.Interface()) } if dstElement.CanInterface() { dstElement = reflect.ValueOf(dstElement.Interface()) } if err = deepMerge(dstElement, srcElement, visited, depth+1, config); err != nil { return } } } dst.SetMapIndex(key, dstSlice) } } if dstElement.IsValid() && !isEmptyValue(dstElement, !config.ShouldNotDereference) { if reflect.TypeOf(srcElement.Interface()).Kind() == reflect.Slice { continue } if reflect.TypeOf(srcElement.Interface()).Kind() == reflect.Map && reflect.TypeOf(dstElement.Interface()).Kind() == reflect.Map { continue } } if srcElement.IsValid() && ((srcElement.Kind() != reflect.Ptr && overwrite) || !dstElement.IsValid() || isEmptyValue(dstElement, !config.ShouldNotDereference)) { if dst.IsNil() { dst.Set(reflect.MakeMap(dst.Type())) } dst.SetMapIndex(key, srcElement) } } // Ensure that all keys in dst are deleted if they are not in src. if overwriteWithEmptySrc { for _, key := range dst.MapKeys() { srcElement := src.MapIndex(key) if !srcElement.IsValid() { dst.SetMapIndex(key, reflect.Value{}) } } } case reflect.Slice: if !dst.CanSet() { break } if (!isEmptyValue(src, !config.ShouldNotDereference) || overwriteWithEmptySrc || overwriteSliceWithEmptySrc) && (overwrite || isEmptyValue(dst, !config.ShouldNotDereference)) && !config.AppendSlice && !sliceDeepCopy { dst.Set(src) } else if config.AppendSlice { if src.Type() != dst.Type() { return fmt.Errorf("cannot append two slice with different type (%s, %s)", src.Type(), dst.Type()) } dst.Set(reflect.AppendSlice(dst, src)) } else if sliceDeepCopy { for i := 0; i < src.Len() && i < dst.Len(); i++ { srcElement := src.Index(i) dstElement := dst.Index(i) if srcElement.CanInterface() { srcElement = reflect.ValueOf(srcElement.Interface()) } if dstElement.CanInterface() { dstElement = reflect.ValueOf(dstElement.Interface()) } if err = deepMerge(dstElement, srcElement, visited, depth+1, config); err != nil { return } } } case reflect.Ptr: fallthrough case reflect.Interface: if isReflectNil(src) { if overwriteWithEmptySrc && dst.CanSet() && src.Type().AssignableTo(dst.Type()) { dst.Set(src) } break } if src.Kind() != reflect.Interface { if dst.IsNil() || (src.Kind() != reflect.Ptr && overwrite) { if dst.CanSet() && (overwrite || isEmptyValue(dst, !config.ShouldNotDereference)) { dst.Set(src) } } else if src.Kind() == reflect.Ptr { if !config.ShouldNotDereference { if err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1, config); err != nil { return } } else if src.Elem().Kind() != reflect.Struct { if overwriteWithEmptySrc || (overwrite && !src.IsNil()) || dst.IsNil() { dst.Set(src) } } } else if dst.Elem().Type() == src.Type() { if err = deepMerge(dst.Elem(), src, visited, depth+1, config); err != nil { return } } else { return ErrDifferentArgumentsTypes } break } if dst.IsNil() || overwrite { if dst.CanSet() && (overwrite || isEmptyValue(dst, !config.ShouldNotDereference)) { dst.Set(src) } break } if dst.Elem().Kind() == src.Elem().Kind() { if err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1, config); err != nil { return } break } default: mustSet := (isEmptyValue(dst, !config.ShouldNotDereference) || overwrite) && (!isEmptyValue(src, !config.ShouldNotDereference) || overwriteWithEmptySrc) if mustSet { if dst.CanSet() { dst.Set(src) } else { dst = src } } } return } // Merge will fill any empty for value type attributes on the dst struct using corresponding // src attributes if they themselves are not empty. dst and src must be valid same-type structs // and dst must be a pointer to struct. // It won't merge unexported (private) fields and will do recursively any exported field. func Merge(dst, src interface{}, opts ...func(*Config)) error { return merge(dst, src, opts...) } // MergeWithOverwrite will do the same as Merge except that non-empty dst attributes will be overridden by // non-empty src attribute values. // Deprecated: use Merge(…) with WithOverride func MergeWithOverwrite(dst, src interface{}, opts ...func(*Config)) error { return merge(dst, src, append(opts, WithOverride)...) } // WithTransformers adds transformers to merge, allowing to customize the merging of some types. func WithTransformers(transformers Transformers) func(*Config) { return func(config *Config) { config.Transformers = transformers } } // WithOverride will make merge override non-empty dst attributes with non-empty src attributes values. func WithOverride(config *Config) { config.Overwrite = true } // WithOverwriteWithEmptyValue will make merge override non empty dst attributes with empty src attributes values. func WithOverwriteWithEmptyValue(config *Config) { config.Overwrite = true config.overwriteWithEmptyValue = true } // WithOverrideEmptySlice will make merge override empty dst slice with empty src slice. func WithOverrideEmptySlice(config *Config) { config.overwriteSliceWithEmptyValue = true } // WithoutDereference prevents dereferencing pointers when evaluating whether they are empty // (i.e. a non-nil pointer is never considered empty). func WithoutDereference(config *Config) { config.ShouldNotDereference = true } // WithAppendSlice will make merge append slices instead of overwriting it. func WithAppendSlice(config *Config) { config.AppendSlice = true } // WithTypeCheck will make merge check types while overwriting it (must be used with WithOverride). func WithTypeCheck(config *Config) { config.TypeCheck = true } // WithSliceDeepCopy will merge slice element one by one with Overwrite flag. func WithSliceDeepCopy(config *Config) { config.sliceDeepCopy = true config.Overwrite = true } func merge(dst, src interface{}, opts ...func(*Config)) error { if dst != nil && reflect.ValueOf(dst).Kind() != reflect.Ptr { return ErrNonPointerArgument } var ( vDst, vSrc reflect.Value err error ) config := &Config{} for _, opt := range opts { opt(config) } if vDst, vSrc, err = resolveValues(dst, src); err != nil { return err } if vDst.Type() != vSrc.Type() { return ErrDifferentArgumentsTypes } return deepMerge(vDst, vSrc, make(map[uintptr]*visit), 0, config) } // IsReflectNil is the reflect value provided nil func isReflectNil(v reflect.Value) bool { k := v.Kind() switch k { case reflect.Interface, reflect.Slice, reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr: // Both interface and slice are nil if first word is 0. // Both are always bigger than a word; assume flagIndir. return v.IsNil() default: return false } } ================================================ FILE: vendor/dario.cat/mergo/mergo.go ================================================ // Copyright 2013 Dario Castañé. All rights reserved. // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Based on src/pkg/reflect/deepequal.go from official // golang's stdlib. package mergo import ( "errors" "reflect" ) // Errors reported by Mergo when it finds invalid arguments. var ( ErrNilArguments = errors.New("src and dst must not be nil") ErrDifferentArgumentsTypes = errors.New("src and dst must be of same type") ErrNotSupported = errors.New("only structs, maps, and slices are supported") ErrExpectedMapAsDestination = errors.New("dst was expected to be a map") ErrExpectedStructAsDestination = errors.New("dst was expected to be a struct") ErrNonPointerArgument = errors.New("dst must be a pointer") ) // During deepMerge, must keep track of checks that are // in progress. The comparison algorithm assumes that all // checks in progress are true when it reencounters them. // Visited are stored in a map indexed by 17 * a1 + a2; type visit struct { typ reflect.Type next *visit ptr uintptr } // From src/pkg/encoding/json/encode.go. func isEmptyValue(v reflect.Value, shouldDereference bool) bool { switch v.Kind() { case reflect.Array, reflect.Map, reflect.Slice, reflect.String: return v.Len() == 0 case reflect.Bool: return !v.Bool() case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return v.Int() == 0 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return v.Uint() == 0 case reflect.Float32, reflect.Float64: return v.Float() == 0 case reflect.Interface, reflect.Ptr: if v.IsNil() { return true } if shouldDereference { return isEmptyValue(v.Elem(), shouldDereference) } return false case reflect.Func: return v.IsNil() case reflect.Invalid: return true } return false } func resolveValues(dst, src interface{}) (vDst, vSrc reflect.Value, err error) { if dst == nil || src == nil { err = ErrNilArguments return } vDst = reflect.ValueOf(dst).Elem() if vDst.Kind() != reflect.Struct && vDst.Kind() != reflect.Map && vDst.Kind() != reflect.Slice { err = ErrNotSupported return } vSrc = reflect.ValueOf(src) // We check if vSrc is a pointer to dereference it. if vSrc.Kind() == reflect.Ptr { vSrc = vSrc.Elem() } return } ================================================ FILE: vendor/github.com/Azure/go-ansiterm/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 Microsoft Corporation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: vendor/github.com/Azure/go-ansiterm/README.md ================================================ # go-ansiterm This is a cross platform Ansi Terminal Emulation library. It reads a stream of Ansi characters and produces the appropriate function calls. The results of the function calls are platform dependent. For example the parser might receive "ESC, [, A" as a stream of three characters. This is the code for Cursor Up (http://www.vt100.net/docs/vt510-rm/CUU). The parser then calls the cursor up function (CUU()) on an event handler. The event handler determines what platform specific work must be done to cause the cursor to move up one position. The parser (parser.go) is a partial implementation of this state machine (http://vt100.net/emu/vt500_parser.png). There are also two event handler implementations, one for tests (test_event_handler.go) to validate that the expected events are being produced and called, the other is a Windows implementation (winterm/win_event_handler.go). See parser_test.go for examples exercising the state machine and generating appropriate function calls. ----- This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. ================================================ FILE: vendor/github.com/Azure/go-ansiterm/SECURITY.md ================================================ ## Security Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. ## Reporting Security Issues **Please do not report security vulnerabilities through public GitHub issues.** Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. ## Preferred Languages We prefer all communications to be in English. ## Policy Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). ================================================ FILE: vendor/github.com/Azure/go-ansiterm/constants.go ================================================ package ansiterm const LogEnv = "DEBUG_TERMINAL" // ANSI constants // References: // -- http://www.ecma-international.org/publications/standards/Ecma-048.htm // -- http://man7.org/linux/man-pages/man4/console_codes.4.html // -- http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html // -- http://en.wikipedia.org/wiki/ANSI_escape_code // -- http://vt100.net/emu/dec_ansi_parser // -- http://vt100.net/emu/vt500_parser.svg // -- http://invisible-island.net/xterm/ctlseqs/ctlseqs.html // -- http://www.inwap.com/pdp10/ansicode.txt const ( // ECMA-48 Set Graphics Rendition // Note: // -- Constants leading with an underscore (e.g., _ANSI_xxx) are unsupported or reserved // -- Fonts could possibly be supported via SetCurrentConsoleFontEx // -- Windows does not expose the per-window cursor (i.e., caret) blink times ANSI_SGR_RESET = 0 ANSI_SGR_BOLD = 1 ANSI_SGR_DIM = 2 _ANSI_SGR_ITALIC = 3 ANSI_SGR_UNDERLINE = 4 _ANSI_SGR_BLINKSLOW = 5 _ANSI_SGR_BLINKFAST = 6 ANSI_SGR_REVERSE = 7 _ANSI_SGR_INVISIBLE = 8 _ANSI_SGR_LINETHROUGH = 9 _ANSI_SGR_FONT_00 = 10 _ANSI_SGR_FONT_01 = 11 _ANSI_SGR_FONT_02 = 12 _ANSI_SGR_FONT_03 = 13 _ANSI_SGR_FONT_04 = 14 _ANSI_SGR_FONT_05 = 15 _ANSI_SGR_FONT_06 = 16 _ANSI_SGR_FONT_07 = 17 _ANSI_SGR_FONT_08 = 18 _ANSI_SGR_FONT_09 = 19 _ANSI_SGR_FONT_10 = 20 _ANSI_SGR_DOUBLEUNDERLINE = 21 ANSI_SGR_BOLD_DIM_OFF = 22 _ANSI_SGR_ITALIC_OFF = 23 ANSI_SGR_UNDERLINE_OFF = 24 _ANSI_SGR_BLINK_OFF = 25 _ANSI_SGR_RESERVED_00 = 26 ANSI_SGR_REVERSE_OFF = 27 _ANSI_SGR_INVISIBLE_OFF = 28 _ANSI_SGR_LINETHROUGH_OFF = 29 ANSI_SGR_FOREGROUND_BLACK = 30 ANSI_SGR_FOREGROUND_RED = 31 ANSI_SGR_FOREGROUND_GREEN = 32 ANSI_SGR_FOREGROUND_YELLOW = 33 ANSI_SGR_FOREGROUND_BLUE = 34 ANSI_SGR_FOREGROUND_MAGENTA = 35 ANSI_SGR_FOREGROUND_CYAN = 36 ANSI_SGR_FOREGROUND_WHITE = 37 _ANSI_SGR_RESERVED_01 = 38 ANSI_SGR_FOREGROUND_DEFAULT = 39 ANSI_SGR_BACKGROUND_BLACK = 40 ANSI_SGR_BACKGROUND_RED = 41 ANSI_SGR_BACKGROUND_GREEN = 42 ANSI_SGR_BACKGROUND_YELLOW = 43 ANSI_SGR_BACKGROUND_BLUE = 44 ANSI_SGR_BACKGROUND_MAGENTA = 45 ANSI_SGR_BACKGROUND_CYAN = 46 ANSI_SGR_BACKGROUND_WHITE = 47 _ANSI_SGR_RESERVED_02 = 48 ANSI_SGR_BACKGROUND_DEFAULT = 49 // 50 - 65: Unsupported ANSI_MAX_CMD_LENGTH = 4096 MAX_INPUT_EVENTS = 128 DEFAULT_WIDTH = 80 DEFAULT_HEIGHT = 24 ANSI_BEL = 0x07 ANSI_BACKSPACE = 0x08 ANSI_TAB = 0x09 ANSI_LINE_FEED = 0x0A ANSI_VERTICAL_TAB = 0x0B ANSI_FORM_FEED = 0x0C ANSI_CARRIAGE_RETURN = 0x0D ANSI_ESCAPE_PRIMARY = 0x1B ANSI_ESCAPE_SECONDARY = 0x5B ANSI_OSC_STRING_ENTRY = 0x5D ANSI_COMMAND_FIRST = 0x40 ANSI_COMMAND_LAST = 0x7E DCS_ENTRY = 0x90 CSI_ENTRY = 0x9B OSC_STRING = 0x9D ANSI_PARAMETER_SEP = ";" ANSI_CMD_G0 = '(' ANSI_CMD_G1 = ')' ANSI_CMD_G2 = '*' ANSI_CMD_G3 = '+' ANSI_CMD_DECPNM = '>' ANSI_CMD_DECPAM = '=' ANSI_CMD_OSC = ']' ANSI_CMD_STR_TERM = '\\' KEY_CONTROL_PARAM_2 = ";2" KEY_CONTROL_PARAM_3 = ";3" KEY_CONTROL_PARAM_4 = ";4" KEY_CONTROL_PARAM_5 = ";5" KEY_CONTROL_PARAM_6 = ";6" KEY_CONTROL_PARAM_7 = ";7" KEY_CONTROL_PARAM_8 = ";8" KEY_ESC_CSI = "\x1B[" KEY_ESC_N = "\x1BN" KEY_ESC_O = "\x1BO" FILL_CHARACTER = ' ' ) func getByteRange(start byte, end byte) []byte { bytes := make([]byte, 0, 32) for i := start; i <= end; i++ { bytes = append(bytes, byte(i)) } return bytes } var toGroundBytes = getToGroundBytes() var executors = getExecuteBytes() // SPACE 20+A0 hex Always and everywhere a blank space // Intermediate 20-2F hex !"#$%&'()*+,-./ var intermeds = getByteRange(0x20, 0x2F) // Parameters 30-3F hex 0123456789:;<=>? // CSI Parameters 30-39, 3B hex 0123456789; var csiParams = getByteRange(0x30, 0x3F) var csiCollectables = append(getByteRange(0x30, 0x39), getByteRange(0x3B, 0x3F)...) // Uppercase 40-5F hex @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_ var upperCase = getByteRange(0x40, 0x5F) // Lowercase 60-7E hex `abcdefghijlkmnopqrstuvwxyz{|}~ var lowerCase = getByteRange(0x60, 0x7E) // Alphabetics 40-7E hex (all of upper and lower case) var alphabetics = append(upperCase, lowerCase...) var printables = getByteRange(0x20, 0x7F) var escapeIntermediateToGroundBytes = getByteRange(0x30, 0x7E) var escapeToGroundBytes = getEscapeToGroundBytes() // See http://www.vt100.net/emu/vt500_parser.png for description of the complex // byte ranges below func getEscapeToGroundBytes() []byte { escapeToGroundBytes := getByteRange(0x30, 0x4F) escapeToGroundBytes = append(escapeToGroundBytes, getByteRange(0x51, 0x57)...) escapeToGroundBytes = append(escapeToGroundBytes, 0x59) escapeToGroundBytes = append(escapeToGroundBytes, 0x5A) escapeToGroundBytes = append(escapeToGroundBytes, 0x5C) escapeToGroundBytes = append(escapeToGroundBytes, getByteRange(0x60, 0x7E)...) return escapeToGroundBytes } func getExecuteBytes() []byte { executeBytes := getByteRange(0x00, 0x17) executeBytes = append(executeBytes, 0x19) executeBytes = append(executeBytes, getByteRange(0x1C, 0x1F)...) return executeBytes } func getToGroundBytes() []byte { groundBytes := []byte{0x18} groundBytes = append(groundBytes, 0x1A) groundBytes = append(groundBytes, getByteRange(0x80, 0x8F)...) groundBytes = append(groundBytes, getByteRange(0x91, 0x97)...) groundBytes = append(groundBytes, 0x99) groundBytes = append(groundBytes, 0x9A) groundBytes = append(groundBytes, 0x9C) return groundBytes } // Delete 7F hex Always and everywhere ignored // C1 Control 80-9F hex 32 additional control characters // G1 Displayable A1-FE hex 94 additional displayable characters // Special A0+FF hex Same as SPACE and DELETE ================================================ FILE: vendor/github.com/Azure/go-ansiterm/context.go ================================================ package ansiterm type ansiContext struct { currentChar byte paramBuffer []byte interBuffer []byte } ================================================ FILE: vendor/github.com/Azure/go-ansiterm/csi_entry_state.go ================================================ package ansiterm type csiEntryState struct { baseState } func (csiState csiEntryState) Handle(b byte) (s state, e error) { csiState.parser.logf("CsiEntry::Handle %#x", b) nextState, err := csiState.baseState.Handle(b) if nextState != nil || err != nil { return nextState, err } switch { case sliceContains(alphabetics, b): return csiState.parser.ground, nil case sliceContains(csiCollectables, b): return csiState.parser.csiParam, nil case sliceContains(executors, b): return csiState, csiState.parser.execute() } return csiState, nil } func (csiState csiEntryState) Transition(s state) error { csiState.parser.logf("CsiEntry::Transition %s --> %s", csiState.Name(), s.Name()) csiState.baseState.Transition(s) switch s { case csiState.parser.ground: return csiState.parser.csiDispatch() case csiState.parser.csiParam: switch { case sliceContains(csiParams, csiState.parser.context.currentChar): csiState.parser.collectParam() case sliceContains(intermeds, csiState.parser.context.currentChar): csiState.parser.collectInter() } } return nil } func (csiState csiEntryState) Enter() error { csiState.parser.clear() return nil } ================================================ FILE: vendor/github.com/Azure/go-ansiterm/csi_param_state.go ================================================ package ansiterm type csiParamState struct { baseState } func (csiState csiParamState) Handle(b byte) (s state, e error) { csiState.parser.logf("CsiParam::Handle %#x", b) nextState, err := csiState.baseState.Handle(b) if nextState != nil || err != nil { return nextState, err } switch { case sliceContains(alphabetics, b): return csiState.parser.ground, nil case sliceContains(csiCollectables, b): csiState.parser.collectParam() return csiState, nil case sliceContains(executors, b): return csiState, csiState.parser.execute() } return csiState, nil } func (csiState csiParamState) Transition(s state) error { csiState.parser.logf("CsiParam::Transition %s --> %s", csiState.Name(), s.Name()) csiState.baseState.Transition(s) switch s { case csiState.parser.ground: return csiState.parser.csiDispatch() } return nil } ================================================ FILE: vendor/github.com/Azure/go-ansiterm/escape_intermediate_state.go ================================================ package ansiterm type escapeIntermediateState struct { baseState } func (escState escapeIntermediateState) Handle(b byte) (s state, e error) { escState.parser.logf("escapeIntermediateState::Handle %#x", b) nextState, err := escState.baseState.Handle(b) if nextState != nil || err != nil { return nextState, err } switch { case sliceContains(intermeds, b): return escState, escState.parser.collectInter() case sliceContains(executors, b): return escState, escState.parser.execute() case sliceContains(escapeIntermediateToGroundBytes, b): return escState.parser.ground, nil } return escState, nil } func (escState escapeIntermediateState) Transition(s state) error { escState.parser.logf("escapeIntermediateState::Transition %s --> %s", escState.Name(), s.Name()) escState.baseState.Transition(s) switch s { case escState.parser.ground: return escState.parser.escDispatch() } return nil } ================================================ FILE: vendor/github.com/Azure/go-ansiterm/escape_state.go ================================================ package ansiterm type escapeState struct { baseState } func (escState escapeState) Handle(b byte) (s state, e error) { escState.parser.logf("escapeState::Handle %#x", b) nextState, err := escState.baseState.Handle(b) if nextState != nil || err != nil { return nextState, err } switch { case b == ANSI_ESCAPE_SECONDARY: return escState.parser.csiEntry, nil case b == ANSI_OSC_STRING_ENTRY: return escState.parser.oscString, nil case sliceContains(executors, b): return escState, escState.parser.execute() case sliceContains(escapeToGroundBytes, b): return escState.parser.ground, nil case sliceContains(intermeds, b): return escState.parser.escapeIntermediate, nil } return escState, nil } func (escState escapeState) Transition(s state) error { escState.parser.logf("Escape::Transition %s --> %s", escState.Name(), s.Name()) escState.baseState.Transition(s) switch s { case escState.parser.ground: return escState.parser.escDispatch() case escState.parser.escapeIntermediate: return escState.parser.collectInter() } return nil } func (escState escapeState) Enter() error { escState.parser.clear() return nil } ================================================ FILE: vendor/github.com/Azure/go-ansiterm/event_handler.go ================================================ package ansiterm type AnsiEventHandler interface { // Print Print(b byte) error // Execute C0 commands Execute(b byte) error // CUrsor Up CUU(int) error // CUrsor Down CUD(int) error // CUrsor Forward CUF(int) error // CUrsor Backward CUB(int) error // Cursor to Next Line CNL(int) error // Cursor to Previous Line CPL(int) error // Cursor Horizontal position Absolute CHA(int) error // Vertical line Position Absolute VPA(int) error // CUrsor Position CUP(int, int) error // Horizontal and Vertical Position (depends on PUM) HVP(int, int) error // Text Cursor Enable Mode DECTCEM(bool) error // Origin Mode DECOM(bool) error // 132 Column Mode DECCOLM(bool) error // Erase in Display ED(int) error // Erase in Line EL(int) error // Insert Line IL(int) error // Delete Line DL(int) error // Insert Character ICH(int) error // Delete Character DCH(int) error // Set Graphics Rendition SGR([]int) error // Pan Down SU(int) error // Pan Up SD(int) error // Device Attributes DA([]string) error // Set Top and Bottom Margins DECSTBM(int, int) error // Index IND() error // Reverse Index RI() error // Flush updates from previous commands Flush() error } ================================================ FILE: vendor/github.com/Azure/go-ansiterm/ground_state.go ================================================ package ansiterm type groundState struct { baseState } func (gs groundState) Handle(b byte) (s state, e error) { gs.parser.context.currentChar = b nextState, err := gs.baseState.Handle(b) if nextState != nil || err != nil { return nextState, err } switch { case sliceContains(printables, b): return gs, gs.parser.print() case sliceContains(executors, b): return gs, gs.parser.execute() } return gs, nil } ================================================ FILE: vendor/github.com/Azure/go-ansiterm/osc_string_state.go ================================================ package ansiterm type oscStringState struct { baseState } func (oscState oscStringState) Handle(b byte) (s state, e error) { oscState.parser.logf("OscString::Handle %#x", b) nextState, err := oscState.baseState.Handle(b) if nextState != nil || err != nil { return nextState, err } // There are several control characters and sequences which can // terminate an OSC string. Most of them are handled by the baseState // handler. The ANSI_BEL character is a special case which behaves as a // terminator only for an OSC string. if b == ANSI_BEL { return oscState.parser.ground, nil } return oscState, nil } ================================================ FILE: vendor/github.com/Azure/go-ansiterm/parser.go ================================================ package ansiterm import ( "errors" "log" "os" ) type AnsiParser struct { currState state eventHandler AnsiEventHandler context *ansiContext csiEntry state csiParam state dcsEntry state escape state escapeIntermediate state error state ground state oscString state stateMap []state logf func(string, ...interface{}) } type Option func(*AnsiParser) func WithLogf(f func(string, ...interface{})) Option { return func(ap *AnsiParser) { ap.logf = f } } func CreateParser(initialState string, evtHandler AnsiEventHandler, opts ...Option) *AnsiParser { ap := &AnsiParser{ eventHandler: evtHandler, context: &ansiContext{}, } for _, o := range opts { o(ap) } if isDebugEnv := os.Getenv(LogEnv); isDebugEnv == "1" { logFile, _ := os.Create("ansiParser.log") logger := log.New(logFile, "", log.LstdFlags) if ap.logf != nil { l := ap.logf ap.logf = func(s string, v ...interface{}) { l(s, v...) logger.Printf(s, v...) } } else { ap.logf = logger.Printf } } if ap.logf == nil { ap.logf = func(string, ...interface{}) {} } ap.csiEntry = csiEntryState{baseState{name: "CsiEntry", parser: ap}} ap.csiParam = csiParamState{baseState{name: "CsiParam", parser: ap}} ap.dcsEntry = dcsEntryState{baseState{name: "DcsEntry", parser: ap}} ap.escape = escapeState{baseState{name: "Escape", parser: ap}} ap.escapeIntermediate = escapeIntermediateState{baseState{name: "EscapeIntermediate", parser: ap}} ap.error = errorState{baseState{name: "Error", parser: ap}} ap.ground = groundState{baseState{name: "Ground", parser: ap}} ap.oscString = oscStringState{baseState{name: "OscString", parser: ap}} ap.stateMap = []state{ ap.csiEntry, ap.csiParam, ap.dcsEntry, ap.escape, ap.escapeIntermediate, ap.error, ap.ground, ap.oscString, } ap.currState = getState(initialState, ap.stateMap) ap.logf("CreateParser: parser %p", ap) return ap } func getState(name string, states []state) state { for _, el := range states { if el.Name() == name { return el } } return nil } func (ap *AnsiParser) Parse(bytes []byte) (int, error) { for i, b := range bytes { if err := ap.handle(b); err != nil { return i, err } } return len(bytes), ap.eventHandler.Flush() } func (ap *AnsiParser) handle(b byte) error { ap.context.currentChar = b newState, err := ap.currState.Handle(b) if err != nil { return err } if newState == nil { ap.logf("WARNING: newState is nil") return errors.New("New state of 'nil' is invalid.") } if newState != ap.currState { if err := ap.changeState(newState); err != nil { return err } } return nil } func (ap *AnsiParser) changeState(newState state) error { ap.logf("ChangeState %s --> %s", ap.currState.Name(), newState.Name()) // Exit old state if err := ap.currState.Exit(); err != nil { ap.logf("Exit state '%s' failed with : '%v'", ap.currState.Name(), err) return err } // Perform transition action if err := ap.currState.Transition(newState); err != nil { ap.logf("Transition from '%s' to '%s' failed with: '%v'", ap.currState.Name(), newState.Name, err) return err } // Enter new state if err := newState.Enter(); err != nil { ap.logf("Enter state '%s' failed with: '%v'", newState.Name(), err) return err } ap.currState = newState return nil } ================================================ FILE: vendor/github.com/Azure/go-ansiterm/parser_action_helpers.go ================================================ package ansiterm import ( "strconv" ) func parseParams(bytes []byte) ([]string, error) { paramBuff := make([]byte, 0, 0) params := []string{} for _, v := range bytes { if v == ';' { if len(paramBuff) > 0 { // Completed parameter, append it to the list s := string(paramBuff) params = append(params, s) paramBuff = make([]byte, 0, 0) } } else { paramBuff = append(paramBuff, v) } } // Last parameter may not be terminated with ';' if len(paramBuff) > 0 { s := string(paramBuff) params = append(params, s) } return params, nil } func parseCmd(context ansiContext) (string, error) { return string(context.currentChar), nil } func getInt(params []string, dflt int) int { i := getInts(params, 1, dflt)[0] return i } func getInts(params []string, minCount int, dflt int) []int { ints := []int{} for _, v := range params { i, _ := strconv.Atoi(v) // Zero is mapped to the default value in VT100. if i == 0 { i = dflt } ints = append(ints, i) } if len(ints) < minCount { remaining := minCount - len(ints) for i := 0; i < remaining; i++ { ints = append(ints, dflt) } } return ints } func (ap *AnsiParser) modeDispatch(param string, set bool) error { switch param { case "?3": return ap.eventHandler.DECCOLM(set) case "?6": return ap.eventHandler.DECOM(set) case "?25": return ap.eventHandler.DECTCEM(set) } return nil } func (ap *AnsiParser) hDispatch(params []string) error { if len(params) == 1 { return ap.modeDispatch(params[0], true) } return nil } func (ap *AnsiParser) lDispatch(params []string) error { if len(params) == 1 { return ap.modeDispatch(params[0], false) } return nil } func getEraseParam(params []string) int { param := getInt(params, 0) if param < 0 || 3 < param { param = 0 } return param } ================================================ FILE: vendor/github.com/Azure/go-ansiterm/parser_actions.go ================================================ package ansiterm func (ap *AnsiParser) collectParam() error { currChar := ap.context.currentChar ap.logf("collectParam %#x", currChar) ap.context.paramBuffer = append(ap.context.paramBuffer, currChar) return nil } func (ap *AnsiParser) collectInter() error { currChar := ap.context.currentChar ap.logf("collectInter %#x", currChar) ap.context.paramBuffer = append(ap.context.interBuffer, currChar) return nil } func (ap *AnsiParser) escDispatch() error { cmd, _ := parseCmd(*ap.context) intermeds := ap.context.interBuffer ap.logf("escDispatch currentChar: %#x", ap.context.currentChar) ap.logf("escDispatch: %v(%v)", cmd, intermeds) switch cmd { case "D": // IND return ap.eventHandler.IND() case "E": // NEL, equivalent to CRLF err := ap.eventHandler.Execute(ANSI_CARRIAGE_RETURN) if err == nil { err = ap.eventHandler.Execute(ANSI_LINE_FEED) } return err case "M": // RI return ap.eventHandler.RI() } return nil } func (ap *AnsiParser) csiDispatch() error { cmd, _ := parseCmd(*ap.context) params, _ := parseParams(ap.context.paramBuffer) ap.logf("Parsed params: %v with length: %d", params, len(params)) ap.logf("csiDispatch: %v(%v)", cmd, params) switch cmd { case "@": return ap.eventHandler.ICH(getInt(params, 1)) case "A": return ap.eventHandler.CUU(getInt(params, 1)) case "B": return ap.eventHandler.CUD(getInt(params, 1)) case "C": return ap.eventHandler.CUF(getInt(params, 1)) case "D": return ap.eventHandler.CUB(getInt(params, 1)) case "E": return ap.eventHandler.CNL(getInt(params, 1)) case "F": return ap.eventHandler.CPL(getInt(params, 1)) case "G": return ap.eventHandler.CHA(getInt(params, 1)) case "H": ints := getInts(params, 2, 1) x, y := ints[0], ints[1] return ap.eventHandler.CUP(x, y) case "J": param := getEraseParam(params) return ap.eventHandler.ED(param) case "K": param := getEraseParam(params) return ap.eventHandler.EL(param) case "L": return ap.eventHandler.IL(getInt(params, 1)) case "M": return ap.eventHandler.DL(getInt(params, 1)) case "P": return ap.eventHandler.DCH(getInt(params, 1)) case "S": return ap.eventHandler.SU(getInt(params, 1)) case "T": return ap.eventHandler.SD(getInt(params, 1)) case "c": return ap.eventHandler.DA(params) case "d": return ap.eventHandler.VPA(getInt(params, 1)) case "f": ints := getInts(params, 2, 1) x, y := ints[0], ints[1] return ap.eventHandler.HVP(x, y) case "h": return ap.hDispatch(params) case "l": return ap.lDispatch(params) case "m": return ap.eventHandler.SGR(getInts(params, 1, 0)) case "r": ints := getInts(params, 2, 1) top, bottom := ints[0], ints[1] return ap.eventHandler.DECSTBM(top, bottom) default: ap.logf("ERROR: Unsupported CSI command: '%s', with full context: %v", cmd, ap.context) return nil } } func (ap *AnsiParser) print() error { return ap.eventHandler.Print(ap.context.currentChar) } func (ap *AnsiParser) clear() error { ap.context = &ansiContext{} return nil } func (ap *AnsiParser) execute() error { return ap.eventHandler.Execute(ap.context.currentChar) } ================================================ FILE: vendor/github.com/Azure/go-ansiterm/states.go ================================================ package ansiterm type stateID int type state interface { Enter() error Exit() error Handle(byte) (state, error) Name() string Transition(state) error } type baseState struct { name string parser *AnsiParser } func (base baseState) Enter() error { return nil } func (base baseState) Exit() error { return nil } func (base baseState) Handle(b byte) (s state, e error) { switch { case b == CSI_ENTRY: return base.parser.csiEntry, nil case b == DCS_ENTRY: return base.parser.dcsEntry, nil case b == ANSI_ESCAPE_PRIMARY: return base.parser.escape, nil case b == OSC_STRING: return base.parser.oscString, nil case sliceContains(toGroundBytes, b): return base.parser.ground, nil } return nil, nil } func (base baseState) Name() string { return base.name } func (base baseState) Transition(s state) error { if s == base.parser.ground { execBytes := []byte{0x18} execBytes = append(execBytes, 0x1A) execBytes = append(execBytes, getByteRange(0x80, 0x8F)...) execBytes = append(execBytes, getByteRange(0x91, 0x97)...) execBytes = append(execBytes, 0x99) execBytes = append(execBytes, 0x9A) if sliceContains(execBytes, base.parser.context.currentChar) { return base.parser.execute() } } return nil } type dcsEntryState struct { baseState } type errorState struct { baseState } ================================================ FILE: vendor/github.com/Azure/go-ansiterm/utilities.go ================================================ package ansiterm import ( "strconv" ) func sliceContains(bytes []byte, b byte) bool { for _, v := range bytes { if v == b { return true } } return false } func convertBytesToInteger(bytes []byte) int { s := string(bytes) i, _ := strconv.Atoi(s) return i } ================================================ FILE: vendor/github.com/Azure/go-ansiterm/winterm/ansi.go ================================================ // +build windows package winterm import ( "fmt" "os" "strconv" "strings" "syscall" "github.com/Azure/go-ansiterm" windows "golang.org/x/sys/windows" ) // Windows keyboard constants // See https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx. const ( VK_PRIOR = 0x21 // PAGE UP key VK_NEXT = 0x22 // PAGE DOWN key VK_END = 0x23 // END key VK_HOME = 0x24 // HOME key VK_LEFT = 0x25 // LEFT ARROW key VK_UP = 0x26 // UP ARROW key VK_RIGHT = 0x27 // RIGHT ARROW key VK_DOWN = 0x28 // DOWN ARROW key VK_SELECT = 0x29 // SELECT key VK_PRINT = 0x2A // PRINT key VK_EXECUTE = 0x2B // EXECUTE key VK_SNAPSHOT = 0x2C // PRINT SCREEN key VK_INSERT = 0x2D // INS key VK_DELETE = 0x2E // DEL key VK_HELP = 0x2F // HELP key VK_F1 = 0x70 // F1 key VK_F2 = 0x71 // F2 key VK_F3 = 0x72 // F3 key VK_F4 = 0x73 // F4 key VK_F5 = 0x74 // F5 key VK_F6 = 0x75 // F6 key VK_F7 = 0x76 // F7 key VK_F8 = 0x77 // F8 key VK_F9 = 0x78 // F9 key VK_F10 = 0x79 // F10 key VK_F11 = 0x7A // F11 key VK_F12 = 0x7B // F12 key RIGHT_ALT_PRESSED = 0x0001 LEFT_ALT_PRESSED = 0x0002 RIGHT_CTRL_PRESSED = 0x0004 LEFT_CTRL_PRESSED = 0x0008 SHIFT_PRESSED = 0x0010 NUMLOCK_ON = 0x0020 SCROLLLOCK_ON = 0x0040 CAPSLOCK_ON = 0x0080 ENHANCED_KEY = 0x0100 ) type ansiCommand struct { CommandBytes []byte Command string Parameters []string IsSpecial bool } func newAnsiCommand(command []byte) *ansiCommand { if isCharacterSelectionCmdChar(command[1]) { // Is Character Set Selection commands return &ansiCommand{ CommandBytes: command, Command: string(command), IsSpecial: true, } } // last char is command character lastCharIndex := len(command) - 1 ac := &ansiCommand{ CommandBytes: command, Command: string(command[lastCharIndex]), IsSpecial: false, } // more than a single escape if lastCharIndex != 0 { start := 1 // skip if double char escape sequence if command[0] == ansiterm.ANSI_ESCAPE_PRIMARY && command[1] == ansiterm.ANSI_ESCAPE_SECONDARY { start++ } // convert this to GetNextParam method ac.Parameters = strings.Split(string(command[start:lastCharIndex]), ansiterm.ANSI_PARAMETER_SEP) } return ac } func (ac *ansiCommand) paramAsSHORT(index int, defaultValue int16) int16 { if index < 0 || index >= len(ac.Parameters) { return defaultValue } param, err := strconv.ParseInt(ac.Parameters[index], 10, 16) if err != nil { return defaultValue } return int16(param) } func (ac *ansiCommand) String() string { return fmt.Sprintf("0x%v \"%v\" (\"%v\")", bytesToHex(ac.CommandBytes), ac.Command, strings.Join(ac.Parameters, "\",\"")) } // isAnsiCommandChar returns true if the passed byte falls within the range of ANSI commands. // See http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html. func isAnsiCommandChar(b byte) bool { switch { case ansiterm.ANSI_COMMAND_FIRST <= b && b <= ansiterm.ANSI_COMMAND_LAST && b != ansiterm.ANSI_ESCAPE_SECONDARY: return true case b == ansiterm.ANSI_CMD_G1 || b == ansiterm.ANSI_CMD_OSC || b == ansiterm.ANSI_CMD_DECPAM || b == ansiterm.ANSI_CMD_DECPNM: // non-CSI escape sequence terminator return true case b == ansiterm.ANSI_CMD_STR_TERM || b == ansiterm.ANSI_BEL: // String escape sequence terminator return true } return false } func isXtermOscSequence(command []byte, current byte) bool { return (len(command) >= 2 && command[0] == ansiterm.ANSI_ESCAPE_PRIMARY && command[1] == ansiterm.ANSI_CMD_OSC && current != ansiterm.ANSI_BEL) } func isCharacterSelectionCmdChar(b byte) bool { return (b == ansiterm.ANSI_CMD_G0 || b == ansiterm.ANSI_CMD_G1 || b == ansiterm.ANSI_CMD_G2 || b == ansiterm.ANSI_CMD_G3) } // bytesToHex converts a slice of bytes to a human-readable string. func bytesToHex(b []byte) string { hex := make([]string, len(b)) for i, ch := range b { hex[i] = fmt.Sprintf("%X", ch) } return strings.Join(hex, "") } // ensureInRange adjusts the passed value, if necessary, to ensure it is within // the passed min / max range. func ensureInRange(n int16, min int16, max int16) int16 { if n < min { return min } else if n > max { return max } else { return n } } func GetStdFile(nFile int) (*os.File, uintptr) { var file *os.File // syscall uses negative numbers // windows package uses very big uint32 // Keep these switches split so we don't have to convert ints too much. switch uint32(nFile) { case windows.STD_INPUT_HANDLE: file = os.Stdin case windows.STD_OUTPUT_HANDLE: file = os.Stdout case windows.STD_ERROR_HANDLE: file = os.Stderr default: switch nFile { case syscall.STD_INPUT_HANDLE: file = os.Stdin case syscall.STD_OUTPUT_HANDLE: file = os.Stdout case syscall.STD_ERROR_HANDLE: file = os.Stderr default: panic(fmt.Errorf("Invalid standard handle identifier: %v", nFile)) } } fd, err := syscall.GetStdHandle(nFile) if err != nil { panic(fmt.Errorf("Invalid standard handle identifier: %v -- %v", nFile, err)) } return file, uintptr(fd) } ================================================ FILE: vendor/github.com/Azure/go-ansiterm/winterm/api.go ================================================ // +build windows package winterm import ( "fmt" "syscall" "unsafe" ) //=========================================================================================================== // IMPORTANT NOTE: // // The methods below make extensive use of the "unsafe" package to obtain the required pointers. // Beginning in Go 1.3, the garbage collector may release local variables (e.g., incoming arguments, stack // variables) the pointers reference *before* the API completes. // // As a result, in those cases, the code must hint that the variables remain in active by invoking the // dummy method "use" (see below). Newer versions of Go are planned to change the mechanism to no longer // require unsafe pointers. // // If you add or modify methods, ENSURE protection of local variables through the "use" builtin to inform // the garbage collector the variables remain in use if: // // -- The value is not a pointer (e.g., int32, struct) // -- The value is not referenced by the method after passing the pointer to Windows // // See http://golang.org/doc/go1.3. //=========================================================================================================== var ( kernel32DLL = syscall.NewLazyDLL("kernel32.dll") getConsoleCursorInfoProc = kernel32DLL.NewProc("GetConsoleCursorInfo") setConsoleCursorInfoProc = kernel32DLL.NewProc("SetConsoleCursorInfo") setConsoleCursorPositionProc = kernel32DLL.NewProc("SetConsoleCursorPosition") setConsoleModeProc = kernel32DLL.NewProc("SetConsoleMode") getConsoleScreenBufferInfoProc = kernel32DLL.NewProc("GetConsoleScreenBufferInfo") setConsoleScreenBufferSizeProc = kernel32DLL.NewProc("SetConsoleScreenBufferSize") scrollConsoleScreenBufferProc = kernel32DLL.NewProc("ScrollConsoleScreenBufferA") setConsoleTextAttributeProc = kernel32DLL.NewProc("SetConsoleTextAttribute") setConsoleWindowInfoProc = kernel32DLL.NewProc("SetConsoleWindowInfo") writeConsoleOutputProc = kernel32DLL.NewProc("WriteConsoleOutputW") readConsoleInputProc = kernel32DLL.NewProc("ReadConsoleInputW") waitForSingleObjectProc = kernel32DLL.NewProc("WaitForSingleObject") ) // Windows Console constants const ( // Console modes // See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx. ENABLE_PROCESSED_INPUT = 0x0001 ENABLE_LINE_INPUT = 0x0002 ENABLE_ECHO_INPUT = 0x0004 ENABLE_WINDOW_INPUT = 0x0008 ENABLE_MOUSE_INPUT = 0x0010 ENABLE_INSERT_MODE = 0x0020 ENABLE_QUICK_EDIT_MODE = 0x0040 ENABLE_EXTENDED_FLAGS = 0x0080 ENABLE_AUTO_POSITION = 0x0100 ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200 ENABLE_PROCESSED_OUTPUT = 0x0001 ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002 ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 DISABLE_NEWLINE_AUTO_RETURN = 0x0008 ENABLE_LVB_GRID_WORLDWIDE = 0x0010 // Character attributes // Note: // -- The attributes are combined to produce various colors (e.g., Blue + Green will create Cyan). // Clearing all foreground or background colors results in black; setting all creates white. // See https://msdn.microsoft.com/en-us/library/windows/desktop/ms682088(v=vs.85).aspx#_win32_character_attributes. FOREGROUND_BLUE uint16 = 0x0001 FOREGROUND_GREEN uint16 = 0x0002 FOREGROUND_RED uint16 = 0x0004 FOREGROUND_INTENSITY uint16 = 0x0008 FOREGROUND_MASK uint16 = 0x000F BACKGROUND_BLUE uint16 = 0x0010 BACKGROUND_GREEN uint16 = 0x0020 BACKGROUND_RED uint16 = 0x0040 BACKGROUND_INTENSITY uint16 = 0x0080 BACKGROUND_MASK uint16 = 0x00F0 COMMON_LVB_MASK uint16 = 0xFF00 COMMON_LVB_REVERSE_VIDEO uint16 = 0x4000 COMMON_LVB_UNDERSCORE uint16 = 0x8000 // Input event types // See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683499(v=vs.85).aspx. KEY_EVENT = 0x0001 MOUSE_EVENT = 0x0002 WINDOW_BUFFER_SIZE_EVENT = 0x0004 MENU_EVENT = 0x0008 FOCUS_EVENT = 0x0010 // WaitForSingleObject return codes WAIT_ABANDONED = 0x00000080 WAIT_FAILED = 0xFFFFFFFF WAIT_SIGNALED = 0x0000000 WAIT_TIMEOUT = 0x00000102 // WaitForSingleObject wait duration WAIT_INFINITE = 0xFFFFFFFF WAIT_ONE_SECOND = 1000 WAIT_HALF_SECOND = 500 WAIT_QUARTER_SECOND = 250 ) // Windows API Console types // -- See https://msdn.microsoft.com/en-us/library/windows/desktop/ms682101(v=vs.85).aspx for Console specific types (e.g., COORD) // -- See https://msdn.microsoft.com/en-us/library/aa296569(v=vs.60).aspx for comments on alignment type ( CHAR_INFO struct { UnicodeChar uint16 Attributes uint16 } CONSOLE_CURSOR_INFO struct { Size uint32 Visible int32 } CONSOLE_SCREEN_BUFFER_INFO struct { Size COORD CursorPosition COORD Attributes uint16 Window SMALL_RECT MaximumWindowSize COORD } COORD struct { X int16 Y int16 } SMALL_RECT struct { Left int16 Top int16 Right int16 Bottom int16 } // INPUT_RECORD is a C/C++ union of which KEY_EVENT_RECORD is one case, it is also the largest // See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683499(v=vs.85).aspx. INPUT_RECORD struct { EventType uint16 KeyEvent KEY_EVENT_RECORD } KEY_EVENT_RECORD struct { KeyDown int32 RepeatCount uint16 VirtualKeyCode uint16 VirtualScanCode uint16 UnicodeChar uint16 ControlKeyState uint32 } WINDOW_BUFFER_SIZE struct { Size COORD } ) // boolToBOOL converts a Go bool into a Windows int32. func boolToBOOL(f bool) int32 { if f { return int32(1) } else { return int32(0) } } // GetConsoleCursorInfo retrieves information about the size and visiblity of the console cursor. // See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683163(v=vs.85).aspx. func GetConsoleCursorInfo(handle uintptr, cursorInfo *CONSOLE_CURSOR_INFO) error { r1, r2, err := getConsoleCursorInfoProc.Call(handle, uintptr(unsafe.Pointer(cursorInfo)), 0) return checkError(r1, r2, err) } // SetConsoleCursorInfo sets the size and visiblity of the console cursor. // See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686019(v=vs.85).aspx. func SetConsoleCursorInfo(handle uintptr, cursorInfo *CONSOLE_CURSOR_INFO) error { r1, r2, err := setConsoleCursorInfoProc.Call(handle, uintptr(unsafe.Pointer(cursorInfo)), 0) return checkError(r1, r2, err) } // SetConsoleCursorPosition location of the console cursor. // See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686025(v=vs.85).aspx. func SetConsoleCursorPosition(handle uintptr, coord COORD) error { r1, r2, err := setConsoleCursorPositionProc.Call(handle, coordToPointer(coord)) use(coord) return checkError(r1, r2, err) } // GetConsoleMode gets the console mode for given file descriptor // See http://msdn.microsoft.com/en-us/library/windows/desktop/ms683167(v=vs.85).aspx. func GetConsoleMode(handle uintptr) (mode uint32, err error) { err = syscall.GetConsoleMode(syscall.Handle(handle), &mode) return mode, err } // SetConsoleMode sets the console mode for given file descriptor // See http://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx. func SetConsoleMode(handle uintptr, mode uint32) error { r1, r2, err := setConsoleModeProc.Call(handle, uintptr(mode), 0) use(mode) return checkError(r1, r2, err) } // GetConsoleScreenBufferInfo retrieves information about the specified console screen buffer. // See http://msdn.microsoft.com/en-us/library/windows/desktop/ms683171(v=vs.85).aspx. func GetConsoleScreenBufferInfo(handle uintptr) (*CONSOLE_SCREEN_BUFFER_INFO, error) { info := CONSOLE_SCREEN_BUFFER_INFO{} err := checkError(getConsoleScreenBufferInfoProc.Call(handle, uintptr(unsafe.Pointer(&info)), 0)) if err != nil { return nil, err } return &info, nil } func ScrollConsoleScreenBuffer(handle uintptr, scrollRect SMALL_RECT, clipRect SMALL_RECT, destOrigin COORD, char CHAR_INFO) error { r1, r2, err := scrollConsoleScreenBufferProc.Call(handle, uintptr(unsafe.Pointer(&scrollRect)), uintptr(unsafe.Pointer(&clipRect)), coordToPointer(destOrigin), uintptr(unsafe.Pointer(&char))) use(scrollRect) use(clipRect) use(destOrigin) use(char) return checkError(r1, r2, err) } // SetConsoleScreenBufferSize sets the size of the console screen buffer. // See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686044(v=vs.85).aspx. func SetConsoleScreenBufferSize(handle uintptr, coord COORD) error { r1, r2, err := setConsoleScreenBufferSizeProc.Call(handle, coordToPointer(coord)) use(coord) return checkError(r1, r2, err) } // SetConsoleTextAttribute sets the attributes of characters written to the // console screen buffer by the WriteFile or WriteConsole function. // See http://msdn.microsoft.com/en-us/library/windows/desktop/ms686047(v=vs.85).aspx. func SetConsoleTextAttribute(handle uintptr, attribute uint16) error { r1, r2, err := setConsoleTextAttributeProc.Call(handle, uintptr(attribute), 0) use(attribute) return checkError(r1, r2, err) } // SetConsoleWindowInfo sets the size and position of the console screen buffer's window. // Note that the size and location must be within and no larger than the backing console screen buffer. // See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686125(v=vs.85).aspx. func SetConsoleWindowInfo(handle uintptr, isAbsolute bool, rect SMALL_RECT) error { r1, r2, err := setConsoleWindowInfoProc.Call(handle, uintptr(boolToBOOL(isAbsolute)), uintptr(unsafe.Pointer(&rect))) use(isAbsolute) use(rect) return checkError(r1, r2, err) } // WriteConsoleOutput writes the CHAR_INFOs from the provided buffer to the active console buffer. // See https://msdn.microsoft.com/en-us/library/windows/desktop/ms687404(v=vs.85).aspx. func WriteConsoleOutput(handle uintptr, buffer []CHAR_INFO, bufferSize COORD, bufferCoord COORD, writeRegion *SMALL_RECT) error { r1, r2, err := writeConsoleOutputProc.Call(handle, uintptr(unsafe.Pointer(&buffer[0])), coordToPointer(bufferSize), coordToPointer(bufferCoord), uintptr(unsafe.Pointer(writeRegion))) use(buffer) use(bufferSize) use(bufferCoord) return checkError(r1, r2, err) } // ReadConsoleInput reads (and removes) data from the console input buffer. // See https://msdn.microsoft.com/en-us/library/windows/desktop/ms684961(v=vs.85).aspx. func ReadConsoleInput(handle uintptr, buffer []INPUT_RECORD, count *uint32) error { r1, r2, err := readConsoleInputProc.Call(handle, uintptr(unsafe.Pointer(&buffer[0])), uintptr(len(buffer)), uintptr(unsafe.Pointer(count))) use(buffer) return checkError(r1, r2, err) } // WaitForSingleObject waits for the passed handle to be signaled. // It returns true if the handle was signaled; false otherwise. // See https://msdn.microsoft.com/en-us/library/windows/desktop/ms687032(v=vs.85).aspx. func WaitForSingleObject(handle uintptr, msWait uint32) (bool, error) { r1, _, err := waitForSingleObjectProc.Call(handle, uintptr(uint32(msWait))) switch r1 { case WAIT_ABANDONED, WAIT_TIMEOUT: return false, nil case WAIT_SIGNALED: return true, nil } use(msWait) return false, err } // String helpers func (info CONSOLE_SCREEN_BUFFER_INFO) String() string { return fmt.Sprintf("Size(%v) Cursor(%v) Window(%v) Max(%v)", info.Size, info.CursorPosition, info.Window, info.MaximumWindowSize) } func (coord COORD) String() string { return fmt.Sprintf("%v,%v", coord.X, coord.Y) } func (rect SMALL_RECT) String() string { return fmt.Sprintf("(%v,%v),(%v,%v)", rect.Left, rect.Top, rect.Right, rect.Bottom) } // checkError evaluates the results of a Windows API call and returns the error if it failed. func checkError(r1, r2 uintptr, err error) error { // Windows APIs return non-zero to indicate success if r1 != 0 { return nil } // Return the error if provided, otherwise default to EINVAL if err != nil { return err } return syscall.EINVAL } // coordToPointer converts a COORD into a uintptr (by fooling the type system). func coordToPointer(c COORD) uintptr { // Note: This code assumes the two SHORTs are correctly laid out; the "cast" to uint32 is just to get a pointer to pass. return uintptr(*((*uint32)(unsafe.Pointer(&c)))) } // use is a no-op, but the compiler cannot see that it is. // Calling use(p) ensures that p is kept live until that point. func use(p interface{}) {} ================================================ FILE: vendor/github.com/Azure/go-ansiterm/winterm/attr_translation.go ================================================ // +build windows package winterm import "github.com/Azure/go-ansiterm" const ( FOREGROUND_COLOR_MASK = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE BACKGROUND_COLOR_MASK = BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE ) // collectAnsiIntoWindowsAttributes modifies the passed Windows text mode flags to reflect the // request represented by the passed ANSI mode. func collectAnsiIntoWindowsAttributes(windowsMode uint16, inverted bool, baseMode uint16, ansiMode int16) (uint16, bool) { switch ansiMode { // Mode styles case ansiterm.ANSI_SGR_BOLD: windowsMode = windowsMode | FOREGROUND_INTENSITY case ansiterm.ANSI_SGR_DIM, ansiterm.ANSI_SGR_BOLD_DIM_OFF: windowsMode &^= FOREGROUND_INTENSITY case ansiterm.ANSI_SGR_UNDERLINE: windowsMode = windowsMode | COMMON_LVB_UNDERSCORE case ansiterm.ANSI_SGR_REVERSE: inverted = true case ansiterm.ANSI_SGR_REVERSE_OFF: inverted = false case ansiterm.ANSI_SGR_UNDERLINE_OFF: windowsMode &^= COMMON_LVB_UNDERSCORE // Foreground colors case ansiterm.ANSI_SGR_FOREGROUND_DEFAULT: windowsMode = (windowsMode &^ FOREGROUND_MASK) | (baseMode & FOREGROUND_MASK) case ansiterm.ANSI_SGR_FOREGROUND_BLACK: windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) case ansiterm.ANSI_SGR_FOREGROUND_RED: windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_RED case ansiterm.ANSI_SGR_FOREGROUND_GREEN: windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_GREEN case ansiterm.ANSI_SGR_FOREGROUND_YELLOW: windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_RED | FOREGROUND_GREEN case ansiterm.ANSI_SGR_FOREGROUND_BLUE: windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_BLUE case ansiterm.ANSI_SGR_FOREGROUND_MAGENTA: windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_RED | FOREGROUND_BLUE case ansiterm.ANSI_SGR_FOREGROUND_CYAN: windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_GREEN | FOREGROUND_BLUE case ansiterm.ANSI_SGR_FOREGROUND_WHITE: windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE // Background colors case ansiterm.ANSI_SGR_BACKGROUND_DEFAULT: // Black with no intensity windowsMode = (windowsMode &^ BACKGROUND_MASK) | (baseMode & BACKGROUND_MASK) case ansiterm.ANSI_SGR_BACKGROUND_BLACK: windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) case ansiterm.ANSI_SGR_BACKGROUND_RED: windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_RED case ansiterm.ANSI_SGR_BACKGROUND_GREEN: windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_GREEN case ansiterm.ANSI_SGR_BACKGROUND_YELLOW: windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_RED | BACKGROUND_GREEN case ansiterm.ANSI_SGR_BACKGROUND_BLUE: windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_BLUE case ansiterm.ANSI_SGR_BACKGROUND_MAGENTA: windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_RED | BACKGROUND_BLUE case ansiterm.ANSI_SGR_BACKGROUND_CYAN: windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_GREEN | BACKGROUND_BLUE case ansiterm.ANSI_SGR_BACKGROUND_WHITE: windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE } return windowsMode, inverted } // invertAttributes inverts the foreground and background colors of a Windows attributes value func invertAttributes(windowsMode uint16) uint16 { return (COMMON_LVB_MASK & windowsMode) | ((FOREGROUND_MASK & windowsMode) << 4) | ((BACKGROUND_MASK & windowsMode) >> 4) } ================================================ FILE: vendor/github.com/Azure/go-ansiterm/winterm/cursor_helpers.go ================================================ // +build windows package winterm const ( horizontal = iota vertical ) func (h *windowsAnsiEventHandler) getCursorWindow(info *CONSOLE_SCREEN_BUFFER_INFO) SMALL_RECT { if h.originMode { sr := h.effectiveSr(info.Window) return SMALL_RECT{ Top: sr.top, Bottom: sr.bottom, Left: 0, Right: info.Size.X - 1, } } else { return SMALL_RECT{ Top: info.Window.Top, Bottom: info.Window.Bottom, Left: 0, Right: info.Size.X - 1, } } } // setCursorPosition sets the cursor to the specified position, bounded to the screen size func (h *windowsAnsiEventHandler) setCursorPosition(position COORD, window SMALL_RECT) error { position.X = ensureInRange(position.X, window.Left, window.Right) position.Y = ensureInRange(position.Y, window.Top, window.Bottom) err := SetConsoleCursorPosition(h.fd, position) if err != nil { return err } h.logf("Cursor position set: (%d, %d)", position.X, position.Y) return err } func (h *windowsAnsiEventHandler) moveCursorVertical(param int) error { return h.moveCursor(vertical, param) } func (h *windowsAnsiEventHandler) moveCursorHorizontal(param int) error { return h.moveCursor(horizontal, param) } func (h *windowsAnsiEventHandler) moveCursor(moveMode int, param int) error { info, err := GetConsoleScreenBufferInfo(h.fd) if err != nil { return err } position := info.CursorPosition switch moveMode { case horizontal: position.X += int16(param) case vertical: position.Y += int16(param) } if err = h.setCursorPosition(position, h.getCursorWindow(info)); err != nil { return err } return nil } func (h *windowsAnsiEventHandler) moveCursorLine(param int) error { info, err := GetConsoleScreenBufferInfo(h.fd) if err != nil { return err } position := info.CursorPosition position.X = 0 position.Y += int16(param) if err = h.setCursorPosition(position, h.getCursorWindow(info)); err != nil { return err } return nil } func (h *windowsAnsiEventHandler) moveCursorColumn(param int) error { info, err := GetConsoleScreenBufferInfo(h.fd) if err != nil { return err } position := info.CursorPosition position.X = int16(param) - 1 if err = h.setCursorPosition(position, h.getCursorWindow(info)); err != nil { return err } return nil } ================================================ FILE: vendor/github.com/Azure/go-ansiterm/winterm/erase_helpers.go ================================================ // +build windows package winterm import "github.com/Azure/go-ansiterm" func (h *windowsAnsiEventHandler) clearRange(attributes uint16, fromCoord COORD, toCoord COORD) error { // Ignore an invalid (negative area) request if toCoord.Y < fromCoord.Y { return nil } var err error var coordStart = COORD{} var coordEnd = COORD{} xCurrent, yCurrent := fromCoord.X, fromCoord.Y xEnd, yEnd := toCoord.X, toCoord.Y // Clear any partial initial line if xCurrent > 0 { coordStart.X, coordStart.Y = xCurrent, yCurrent coordEnd.X, coordEnd.Y = xEnd, yCurrent err = h.clearRect(attributes, coordStart, coordEnd) if err != nil { return err } xCurrent = 0 yCurrent += 1 } // Clear intervening rectangular section if yCurrent < yEnd { coordStart.X, coordStart.Y = xCurrent, yCurrent coordEnd.X, coordEnd.Y = xEnd, yEnd-1 err = h.clearRect(attributes, coordStart, coordEnd) if err != nil { return err } xCurrent = 0 yCurrent = yEnd } // Clear remaining partial ending line coordStart.X, coordStart.Y = xCurrent, yCurrent coordEnd.X, coordEnd.Y = xEnd, yEnd err = h.clearRect(attributes, coordStart, coordEnd) if err != nil { return err } return nil } func (h *windowsAnsiEventHandler) clearRect(attributes uint16, fromCoord COORD, toCoord COORD) error { region := SMALL_RECT{Top: fromCoord.Y, Left: fromCoord.X, Bottom: toCoord.Y, Right: toCoord.X} width := toCoord.X - fromCoord.X + 1 height := toCoord.Y - fromCoord.Y + 1 size := uint32(width) * uint32(height) if size <= 0 { return nil } buffer := make([]CHAR_INFO, size) char := CHAR_INFO{ansiterm.FILL_CHARACTER, attributes} for i := 0; i < int(size); i++ { buffer[i] = char } err := WriteConsoleOutput(h.fd, buffer, COORD{X: width, Y: height}, COORD{X: 0, Y: 0}, ®ion) if err != nil { return err } return nil } ================================================ FILE: vendor/github.com/Azure/go-ansiterm/winterm/scroll_helper.go ================================================ // +build windows package winterm // effectiveSr gets the current effective scroll region in buffer coordinates func (h *windowsAnsiEventHandler) effectiveSr(window SMALL_RECT) scrollRegion { top := addInRange(window.Top, h.sr.top, window.Top, window.Bottom) bottom := addInRange(window.Top, h.sr.bottom, window.Top, window.Bottom) if top >= bottom { top = window.Top bottom = window.Bottom } return scrollRegion{top: top, bottom: bottom} } func (h *windowsAnsiEventHandler) scrollUp(param int) error { info, err := GetConsoleScreenBufferInfo(h.fd) if err != nil { return err } sr := h.effectiveSr(info.Window) return h.scroll(param, sr, info) } func (h *windowsAnsiEventHandler) scrollDown(param int) error { return h.scrollUp(-param) } func (h *windowsAnsiEventHandler) deleteLines(param int) error { info, err := GetConsoleScreenBufferInfo(h.fd) if err != nil { return err } start := info.CursorPosition.Y sr := h.effectiveSr(info.Window) // Lines cannot be inserted or deleted outside the scrolling region. if start >= sr.top && start <= sr.bottom { sr.top = start return h.scroll(param, sr, info) } else { return nil } } func (h *windowsAnsiEventHandler) insertLines(param int) error { return h.deleteLines(-param) } // scroll scrolls the provided scroll region by param lines. The scroll region is in buffer coordinates. func (h *windowsAnsiEventHandler) scroll(param int, sr scrollRegion, info *CONSOLE_SCREEN_BUFFER_INFO) error { h.logf("scroll: scrollTop: %d, scrollBottom: %d", sr.top, sr.bottom) h.logf("scroll: windowTop: %d, windowBottom: %d", info.Window.Top, info.Window.Bottom) // Copy from and clip to the scroll region (full buffer width) scrollRect := SMALL_RECT{ Top: sr.top, Bottom: sr.bottom, Left: 0, Right: info.Size.X - 1, } // Origin to which area should be copied destOrigin := COORD{ X: 0, Y: sr.top - int16(param), } char := CHAR_INFO{ UnicodeChar: ' ', Attributes: h.attributes, } if err := ScrollConsoleScreenBuffer(h.fd, scrollRect, scrollRect, destOrigin, char); err != nil { return err } return nil } func (h *windowsAnsiEventHandler) deleteCharacters(param int) error { info, err := GetConsoleScreenBufferInfo(h.fd) if err != nil { return err } return h.scrollLine(param, info.CursorPosition, info) } func (h *windowsAnsiEventHandler) insertCharacters(param int) error { return h.deleteCharacters(-param) } // scrollLine scrolls a line horizontally starting at the provided position by a number of columns. func (h *windowsAnsiEventHandler) scrollLine(columns int, position COORD, info *CONSOLE_SCREEN_BUFFER_INFO) error { // Copy from and clip to the scroll region (full buffer width) scrollRect := SMALL_RECT{ Top: position.Y, Bottom: position.Y, Left: position.X, Right: info.Size.X - 1, } // Origin to which area should be copied destOrigin := COORD{ X: position.X - int16(columns), Y: position.Y, } char := CHAR_INFO{ UnicodeChar: ' ', Attributes: h.attributes, } if err := ScrollConsoleScreenBuffer(h.fd, scrollRect, scrollRect, destOrigin, char); err != nil { return err } return nil } ================================================ FILE: vendor/github.com/Azure/go-ansiterm/winterm/utilities.go ================================================ // +build windows package winterm // AddInRange increments a value by the passed quantity while ensuring the values // always remain within the supplied min / max range. func addInRange(n int16, increment int16, min int16, max int16) int16 { return ensureInRange(n+increment, min, max) } ================================================ FILE: vendor/github.com/Azure/go-ansiterm/winterm/win_event_handler.go ================================================ // +build windows package winterm import ( "bytes" "log" "os" "strconv" "github.com/Azure/go-ansiterm" ) type windowsAnsiEventHandler struct { fd uintptr file *os.File infoReset *CONSOLE_SCREEN_BUFFER_INFO sr scrollRegion buffer bytes.Buffer attributes uint16 inverted bool wrapNext bool drewMarginByte bool originMode bool marginByte byte curInfo *CONSOLE_SCREEN_BUFFER_INFO curPos COORD logf func(string, ...interface{}) } type Option func(*windowsAnsiEventHandler) func WithLogf(f func(string, ...interface{})) Option { return func(w *windowsAnsiEventHandler) { w.logf = f } } func CreateWinEventHandler(fd uintptr, file *os.File, opts ...Option) ansiterm.AnsiEventHandler { infoReset, err := GetConsoleScreenBufferInfo(fd) if err != nil { return nil } h := &windowsAnsiEventHandler{ fd: fd, file: file, infoReset: infoReset, attributes: infoReset.Attributes, } for _, o := range opts { o(h) } if isDebugEnv := os.Getenv(ansiterm.LogEnv); isDebugEnv == "1" { logFile, _ := os.Create("winEventHandler.log") logger := log.New(logFile, "", log.LstdFlags) if h.logf != nil { l := h.logf h.logf = func(s string, v ...interface{}) { l(s, v...) logger.Printf(s, v...) } } else { h.logf = logger.Printf } } if h.logf == nil { h.logf = func(string, ...interface{}) {} } return h } type scrollRegion struct { top int16 bottom int16 } // simulateLF simulates a LF or CR+LF by scrolling if necessary to handle the // current cursor position and scroll region settings, in which case it returns // true. If no special handling is necessary, then it does nothing and returns // false. // // In the false case, the caller should ensure that a carriage return // and line feed are inserted or that the text is otherwise wrapped. func (h *windowsAnsiEventHandler) simulateLF(includeCR bool) (bool, error) { if h.wrapNext { if err := h.Flush(); err != nil { return false, err } h.clearWrap() } pos, info, err := h.getCurrentInfo() if err != nil { return false, err } sr := h.effectiveSr(info.Window) if pos.Y == sr.bottom { // Scrolling is necessary. Let Windows automatically scroll if the scrolling region // is the full window. if sr.top == info.Window.Top && sr.bottom == info.Window.Bottom { if includeCR { pos.X = 0 h.updatePos(pos) } return false, nil } // A custom scroll region is active. Scroll the window manually to simulate // the LF. if err := h.Flush(); err != nil { return false, err } h.logf("Simulating LF inside scroll region") if err := h.scrollUp(1); err != nil { return false, err } if includeCR { pos.X = 0 if err := SetConsoleCursorPosition(h.fd, pos); err != nil { return false, err } } return true, nil } else if pos.Y < info.Window.Bottom { // Let Windows handle the LF. pos.Y++ if includeCR { pos.X = 0 } h.updatePos(pos) return false, nil } else { // The cursor is at the bottom of the screen but outside the scroll // region. Skip the LF. h.logf("Simulating LF outside scroll region") if includeCR { if err := h.Flush(); err != nil { return false, err } pos.X = 0 if err := SetConsoleCursorPosition(h.fd, pos); err != nil { return false, err } } return true, nil } } // executeLF executes a LF without a CR. func (h *windowsAnsiEventHandler) executeLF() error { handled, err := h.simulateLF(false) if err != nil { return err } if !handled { // Windows LF will reset the cursor column position. Write the LF // and restore the cursor position. pos, _, err := h.getCurrentInfo() if err != nil { return err } h.buffer.WriteByte(ansiterm.ANSI_LINE_FEED) if pos.X != 0 { if err := h.Flush(); err != nil { return err } h.logf("Resetting cursor position for LF without CR") if err := SetConsoleCursorPosition(h.fd, pos); err != nil { return err } } } return nil } func (h *windowsAnsiEventHandler) Print(b byte) error { if h.wrapNext { h.buffer.WriteByte(h.marginByte) h.clearWrap() if _, err := h.simulateLF(true); err != nil { return err } } pos, info, err := h.getCurrentInfo() if err != nil { return err } if pos.X == info.Size.X-1 { h.wrapNext = true h.marginByte = b } else { pos.X++ h.updatePos(pos) h.buffer.WriteByte(b) } return nil } func (h *windowsAnsiEventHandler) Execute(b byte) error { switch b { case ansiterm.ANSI_TAB: h.logf("Execute(TAB)") // Move to the next tab stop, but preserve auto-wrap if already set. if !h.wrapNext { pos, info, err := h.getCurrentInfo() if err != nil { return err } pos.X = (pos.X + 8) - pos.X%8 if pos.X >= info.Size.X { pos.X = info.Size.X - 1 } if err := h.Flush(); err != nil { return err } if err := SetConsoleCursorPosition(h.fd, pos); err != nil { return err } } return nil case ansiterm.ANSI_BEL: h.buffer.WriteByte(ansiterm.ANSI_BEL) return nil case ansiterm.ANSI_BACKSPACE: if h.wrapNext { if err := h.Flush(); err != nil { return err } h.clearWrap() } pos, _, err := h.getCurrentInfo() if err != nil { return err } if pos.X > 0 { pos.X-- h.updatePos(pos) h.buffer.WriteByte(ansiterm.ANSI_BACKSPACE) } return nil case ansiterm.ANSI_VERTICAL_TAB, ansiterm.ANSI_FORM_FEED: // Treat as true LF. return h.executeLF() case ansiterm.ANSI_LINE_FEED: // Simulate a CR and LF for now since there is no way in go-ansiterm // to tell if the LF should include CR (and more things break when it's // missing than when it's incorrectly added). handled, err := h.simulateLF(true) if handled || err != nil { return err } return h.buffer.WriteByte(ansiterm.ANSI_LINE_FEED) case ansiterm.ANSI_CARRIAGE_RETURN: if h.wrapNext { if err := h.Flush(); err != nil { return err } h.clearWrap() } pos, _, err := h.getCurrentInfo() if err != nil { return err } if pos.X != 0 { pos.X = 0 h.updatePos(pos) h.buffer.WriteByte(ansiterm.ANSI_CARRIAGE_RETURN) } return nil default: return nil } } func (h *windowsAnsiEventHandler) CUU(param int) error { if err := h.Flush(); err != nil { return err } h.logf("CUU: [%v]", []string{strconv.Itoa(param)}) h.clearWrap() return h.moveCursorVertical(-param) } func (h *windowsAnsiEventHandler) CUD(param int) error { if err := h.Flush(); err != nil { return err } h.logf("CUD: [%v]", []string{strconv.Itoa(param)}) h.clearWrap() return h.moveCursorVertical(param) } func (h *windowsAnsiEventHandler) CUF(param int) error { if err := h.Flush(); err != nil { return err } h.logf("CUF: [%v]", []string{strconv.Itoa(param)}) h.clearWrap() return h.moveCursorHorizontal(param) } func (h *windowsAnsiEventHandler) CUB(param int) error { if err := h.Flush(); err != nil { return err } h.logf("CUB: [%v]", []string{strconv.Itoa(param)}) h.clearWrap() return h.moveCursorHorizontal(-param) } func (h *windowsAnsiEventHandler) CNL(param int) error { if err := h.Flush(); err != nil { return err } h.logf("CNL: [%v]", []string{strconv.Itoa(param)}) h.clearWrap() return h.moveCursorLine(param) } func (h *windowsAnsiEventHandler) CPL(param int) error { if err := h.Flush(); err != nil { return err } h.logf("CPL: [%v]", []string{strconv.Itoa(param)}) h.clearWrap() return h.moveCursorLine(-param) } func (h *windowsAnsiEventHandler) CHA(param int) error { if err := h.Flush(); err != nil { return err } h.logf("CHA: [%v]", []string{strconv.Itoa(param)}) h.clearWrap() return h.moveCursorColumn(param) } func (h *windowsAnsiEventHandler) VPA(param int) error { if err := h.Flush(); err != nil { return err } h.logf("VPA: [[%d]]", param) h.clearWrap() info, err := GetConsoleScreenBufferInfo(h.fd) if err != nil { return err } window := h.getCursorWindow(info) position := info.CursorPosition position.Y = window.Top + int16(param) - 1 return h.setCursorPosition(position, window) } func (h *windowsAnsiEventHandler) CUP(row int, col int) error { if err := h.Flush(); err != nil { return err } h.logf("CUP: [[%d %d]]", row, col) h.clearWrap() info, err := GetConsoleScreenBufferInfo(h.fd) if err != nil { return err } window := h.getCursorWindow(info) position := COORD{window.Left + int16(col) - 1, window.Top + int16(row) - 1} return h.setCursorPosition(position, window) } func (h *windowsAnsiEventHandler) HVP(row int, col int) error { if err := h.Flush(); err != nil { return err } h.logf("HVP: [[%d %d]]", row, col) h.clearWrap() return h.CUP(row, col) } func (h *windowsAnsiEventHandler) DECTCEM(visible bool) error { if err := h.Flush(); err != nil { return err } h.logf("DECTCEM: [%v]", []string{strconv.FormatBool(visible)}) h.clearWrap() return nil } func (h *windowsAnsiEventHandler) DECOM(enable bool) error { if err := h.Flush(); err != nil { return err } h.logf("DECOM: [%v]", []string{strconv.FormatBool(enable)}) h.clearWrap() h.originMode = enable return h.CUP(1, 1) } func (h *windowsAnsiEventHandler) DECCOLM(use132 bool) error { if err := h.Flush(); err != nil { return err } h.logf("DECCOLM: [%v]", []string{strconv.FormatBool(use132)}) h.clearWrap() if err := h.ED(2); err != nil { return err } info, err := GetConsoleScreenBufferInfo(h.fd) if err != nil { return err } targetWidth := int16(80) if use132 { targetWidth = 132 } if info.Size.X < targetWidth { if err := SetConsoleScreenBufferSize(h.fd, COORD{targetWidth, info.Size.Y}); err != nil { h.logf("set buffer failed: %v", err) return err } } window := info.Window window.Left = 0 window.Right = targetWidth - 1 if err := SetConsoleWindowInfo(h.fd, true, window); err != nil { h.logf("set window failed: %v", err) return err } if info.Size.X > targetWidth { if err := SetConsoleScreenBufferSize(h.fd, COORD{targetWidth, info.Size.Y}); err != nil { h.logf("set buffer failed: %v", err) return err } } return SetConsoleCursorPosition(h.fd, COORD{0, 0}) } func (h *windowsAnsiEventHandler) ED(param int) error { if err := h.Flush(); err != nil { return err } h.logf("ED: [%v]", []string{strconv.Itoa(param)}) h.clearWrap() // [J -- Erases from the cursor to the end of the screen, including the cursor position. // [1J -- Erases from the beginning of the screen to the cursor, including the cursor position. // [2J -- Erases the complete display. The cursor does not move. // Notes: // -- Clearing the entire buffer, versus just the Window, works best for Windows Consoles info, err := GetConsoleScreenBufferInfo(h.fd) if err != nil { return err } var start COORD var end COORD switch param { case 0: start = info.CursorPosition end = COORD{info.Size.X - 1, info.Size.Y - 1} case 1: start = COORD{0, 0} end = info.CursorPosition case 2: start = COORD{0, 0} end = COORD{info.Size.X - 1, info.Size.Y - 1} } err = h.clearRange(h.attributes, start, end) if err != nil { return err } // If the whole buffer was cleared, move the window to the top while preserving // the window-relative cursor position. if param == 2 { pos := info.CursorPosition window := info.Window pos.Y -= window.Top window.Bottom -= window.Top window.Top = 0 if err := SetConsoleCursorPosition(h.fd, pos); err != nil { return err } if err := SetConsoleWindowInfo(h.fd, true, window); err != nil { return err } } return nil } func (h *windowsAnsiEventHandler) EL(param int) error { if err := h.Flush(); err != nil { return err } h.logf("EL: [%v]", strconv.Itoa(param)) h.clearWrap() // [K -- Erases from the cursor to the end of the line, including the cursor position. // [1K -- Erases from the beginning of the line to the cursor, including the cursor position. // [2K -- Erases the complete line. info, err := GetConsoleScreenBufferInfo(h.fd) if err != nil { return err } var start COORD var end COORD switch param { case 0: start = info.CursorPosition end = COORD{info.Size.X, info.CursorPosition.Y} case 1: start = COORD{0, info.CursorPosition.Y} end = info.CursorPosition case 2: start = COORD{0, info.CursorPosition.Y} end = COORD{info.Size.X, info.CursorPosition.Y} } err = h.clearRange(h.attributes, start, end) if err != nil { return err } return nil } func (h *windowsAnsiEventHandler) IL(param int) error { if err := h.Flush(); err != nil { return err } h.logf("IL: [%v]", strconv.Itoa(param)) h.clearWrap() return h.insertLines(param) } func (h *windowsAnsiEventHandler) DL(param int) error { if err := h.Flush(); err != nil { return err } h.logf("DL: [%v]", strconv.Itoa(param)) h.clearWrap() return h.deleteLines(param) } func (h *windowsAnsiEventHandler) ICH(param int) error { if err := h.Flush(); err != nil { return err } h.logf("ICH: [%v]", strconv.Itoa(param)) h.clearWrap() return h.insertCharacters(param) } func (h *windowsAnsiEventHandler) DCH(param int) error { if err := h.Flush(); err != nil { return err } h.logf("DCH: [%v]", strconv.Itoa(param)) h.clearWrap() return h.deleteCharacters(param) } func (h *windowsAnsiEventHandler) SGR(params []int) error { if err := h.Flush(); err != nil { return err } strings := []string{} for _, v := range params { strings = append(strings, strconv.Itoa(v)) } h.logf("SGR: [%v]", strings) if len(params) <= 0 { h.attributes = h.infoReset.Attributes h.inverted = false } else { for _, attr := range params { if attr == ansiterm.ANSI_SGR_RESET { h.attributes = h.infoReset.Attributes h.inverted = false continue } h.attributes, h.inverted = collectAnsiIntoWindowsAttributes(h.attributes, h.inverted, h.infoReset.Attributes, int16(attr)) } } attributes := h.attributes if h.inverted { attributes = invertAttributes(attributes) } err := SetConsoleTextAttribute(h.fd, attributes) if err != nil { return err } return nil } func (h *windowsAnsiEventHandler) SU(param int) error { if err := h.Flush(); err != nil { return err } h.logf("SU: [%v]", []string{strconv.Itoa(param)}) h.clearWrap() return h.scrollUp(param) } func (h *windowsAnsiEventHandler) SD(param int) error { if err := h.Flush(); err != nil { return err } h.logf("SD: [%v]", []string{strconv.Itoa(param)}) h.clearWrap() return h.scrollDown(param) } func (h *windowsAnsiEventHandler) DA(params []string) error { h.logf("DA: [%v]", params) // DA cannot be implemented because it must send data on the VT100 input stream, // which is not available to go-ansiterm. return nil } func (h *windowsAnsiEventHandler) DECSTBM(top int, bottom int) error { if err := h.Flush(); err != nil { return err } h.logf("DECSTBM: [%d, %d]", top, bottom) // Windows is 0 indexed, Linux is 1 indexed h.sr.top = int16(top - 1) h.sr.bottom = int16(bottom - 1) // This command also moves the cursor to the origin. h.clearWrap() return h.CUP(1, 1) } func (h *windowsAnsiEventHandler) RI() error { if err := h.Flush(); err != nil { return err } h.logf("RI: []") h.clearWrap() info, err := GetConsoleScreenBufferInfo(h.fd) if err != nil { return err } sr := h.effectiveSr(info.Window) if info.CursorPosition.Y == sr.top { return h.scrollDown(1) } return h.moveCursorVertical(-1) } func (h *windowsAnsiEventHandler) IND() error { h.logf("IND: []") return h.executeLF() } func (h *windowsAnsiEventHandler) Flush() error { h.curInfo = nil if h.buffer.Len() > 0 { h.logf("Flush: [%s]", h.buffer.Bytes()) if _, err := h.buffer.WriteTo(h.file); err != nil { return err } } if h.wrapNext && !h.drewMarginByte { h.logf("Flush: drawing margin byte '%c'", h.marginByte) info, err := GetConsoleScreenBufferInfo(h.fd) if err != nil { return err } charInfo := []CHAR_INFO{{UnicodeChar: uint16(h.marginByte), Attributes: info.Attributes}} size := COORD{1, 1} position := COORD{0, 0} region := SMALL_RECT{Left: info.CursorPosition.X, Top: info.CursorPosition.Y, Right: info.CursorPosition.X, Bottom: info.CursorPosition.Y} if err := WriteConsoleOutput(h.fd, charInfo, size, position, ®ion); err != nil { return err } h.drewMarginByte = true } return nil } // cacheConsoleInfo ensures that the current console screen information has been queried // since the last call to Flush(). It must be called before accessing h.curInfo or h.curPos. func (h *windowsAnsiEventHandler) getCurrentInfo() (COORD, *CONSOLE_SCREEN_BUFFER_INFO, error) { if h.curInfo == nil { info, err := GetConsoleScreenBufferInfo(h.fd) if err != nil { return COORD{}, nil, err } h.curInfo = info h.curPos = info.CursorPosition } return h.curPos, h.curInfo, nil } func (h *windowsAnsiEventHandler) updatePos(pos COORD) { if h.curInfo == nil { panic("failed to call getCurrentInfo before calling updatePos") } h.curPos = pos } // clearWrap clears the state where the cursor is in the margin // waiting for the next character before wrapping the line. This must // be done before most operations that act on the cursor. func (h *windowsAnsiEventHandler) clearWrap() { h.wrapNext = false h.drewMarginByte = false } ================================================ FILE: vendor/github.com/BurntSushi/toml/.gitignore ================================================ /toml.test /toml-test ================================================ FILE: vendor/github.com/BurntSushi/toml/COPYING ================================================ The MIT License (MIT) Copyright (c) 2013 TOML authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: vendor/github.com/BurntSushi/toml/README.md ================================================ TOML stands for Tom's Obvious, Minimal Language. This Go package provides a reflection interface similar to Go's standard library `json` and `xml` packages. Compatible with TOML version [v1.1.0](https://toml.io/en/v1.1.0). Documentation: https://pkg.go.dev/github.com/BurntSushi/toml See the [releases page](https://github.com/BurntSushi/toml/releases) for a changelog; this information is also in the git tag annotations (e.g. `git show v0.4.0`). This library requires Go 1.18 or newer; add it to your go.mod with: % go get github.com/BurntSushi/toml@latest It also comes with a TOML validator CLI tool: % go install github.com/BurntSushi/toml/cmd/tomlv@latest % tomlv some-toml-file.toml ### Examples For the simplest example, consider some TOML file as just a list of keys and values: ```toml Age = 25 Cats = [ "Cauchy", "Plato" ] Pi = 3.14 Perfection = [ 6, 28, 496, 8128 ] DOB = 1987-07-05T05:45:00Z ``` Which can be decoded with: ```go type Config struct { Age int Cats []string Pi float64 Perfection []int DOB time.Time } var conf Config _, err := toml.Decode(tomlData, &conf) ``` You can also use struct tags if your struct field name doesn't map to a TOML key value directly: ```toml some_key_NAME = "wat" ``` ```go type TOML struct { ObscureKey string `toml:"some_key_NAME"` } ``` Beware that like other decoders **only exported fields** are considered when encoding and decoding; private fields are silently ignored. ### Using the `Marshaler` and `encoding.TextUnmarshaler` interfaces Here's an example that automatically parses values in a `mail.Address`: ```toml contacts = [ "Donald Duck ", "Scrooge McDuck ", ] ``` Can be decoded with: ```go // Create address type which satisfies the encoding.TextUnmarshaler interface. type address struct { *mail.Address } func (a *address) UnmarshalText(text []byte) error { var err error a.Address, err = mail.ParseAddress(string(text)) return err } // Decode it. func decode() { blob := ` contacts = [ "Donald Duck ", "Scrooge McDuck ", ] ` var contacts struct { Contacts []address } _, err := toml.Decode(blob, &contacts) if err != nil { log.Fatal(err) } for _, c := range contacts.Contacts { fmt.Printf("%#v\n", c.Address) } // Output: // &mail.Address{Name:"Donald Duck", Address:"donald@duckburg.com"} // &mail.Address{Name:"Scrooge McDuck", Address:"scrooge@duckburg.com"} } ``` To target TOML specifically you can implement `UnmarshalTOML` TOML interface in a similar way. ### More complex usage See the [`_example/`](/_example) directory for a more complex example. ================================================ FILE: vendor/github.com/BurntSushi/toml/decode.go ================================================ package toml import ( "bytes" "encoding" "encoding/json" "fmt" "io" "io/fs" "math" "os" "reflect" "strconv" "strings" "time" ) // Unmarshaler is the interface implemented by objects that can unmarshal a // TOML description of themselves. type Unmarshaler interface { UnmarshalTOML(any) error } // Unmarshal decodes the contents of data in TOML format into a pointer v. // // See [Decoder] for a description of the decoding process. func Unmarshal(data []byte, v any) error { _, err := NewDecoder(bytes.NewReader(data)).Decode(v) return err } // Decode the TOML data in to the pointer v. // // See [Decoder] for a description of the decoding process. func Decode(data string, v any) (MetaData, error) { return NewDecoder(strings.NewReader(data)).Decode(v) } // DecodeFile reads the contents of a file and decodes it with [Decode]. func DecodeFile(path string, v any) (MetaData, error) { fp, err := os.Open(path) if err != nil { return MetaData{}, err } defer fp.Close() return NewDecoder(fp).Decode(v) } // DecodeFS reads the contents of a file from [fs.FS] and decodes it with // [Decode]. func DecodeFS(fsys fs.FS, path string, v any) (MetaData, error) { fp, err := fsys.Open(path) if err != nil { return MetaData{}, err } defer fp.Close() return NewDecoder(fp).Decode(v) } // Primitive is a TOML value that hasn't been decoded into a Go value. // // This type can be used for any value, which will cause decoding to be delayed. // You can use [PrimitiveDecode] to "manually" decode these values. // // NOTE: The underlying representation of a `Primitive` value is subject to // change. Do not rely on it. // // NOTE: Primitive values are still parsed, so using them will only avoid the // overhead of reflection. They can be useful when you don't know the exact type // of TOML data until runtime. type Primitive struct { undecoded any context Key } // The significand precision for float32 and float64 is 24 and 53 bits; this is // the range a natural number can be stored in a float without loss of data. const ( maxSafeFloat32Int = 16777215 // 2^24-1 maxSafeFloat64Int = int64(9007199254740991) // 2^53-1 ) // Decoder decodes TOML data. // // TOML tables correspond to Go structs or maps; they can be used // interchangeably, but structs offer better type safety. // // TOML table arrays correspond to either a slice of structs or a slice of maps. // // TOML datetimes correspond to [time.Time]. Local datetimes are parsed in the // local timezone. // // [time.Duration] types are treated as nanoseconds if the TOML value is an // integer, or they're parsed with time.ParseDuration() if they're strings. // // All other TOML types (float, string, int, bool and array) correspond to the // obvious Go types. // // An exception to the above rules is if a type implements the TextUnmarshaler // interface, in which case any primitive TOML value (floats, strings, integers, // booleans, datetimes) will be converted to a []byte and given to the value's // UnmarshalText method. See the Unmarshaler example for a demonstration with // email addresses. // // # Key mapping // // TOML keys can map to either keys in a Go map or field names in a Go struct. // The special `toml` struct tag can be used to map TOML keys to struct fields // that don't match the key name exactly (see the example). A case insensitive // match to struct names will be tried if an exact match can't be found. // // The mapping between TOML values and Go values is loose. That is, there may // exist TOML values that cannot be placed into your representation, and there // may be parts of your representation that do not correspond to TOML values. // This loose mapping can be made stricter by using the IsDefined and/or // Undecoded methods on the MetaData returned. // // This decoder does not handle cyclic types. Decode will not terminate if a // cyclic type is passed. type Decoder struct { r io.Reader } // NewDecoder creates a new Decoder. func NewDecoder(r io.Reader) *Decoder { return &Decoder{r: r} } var ( unmarshalToml = reflect.TypeOf((*Unmarshaler)(nil)).Elem() unmarshalText = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() primitiveType = reflect.TypeOf((*Primitive)(nil)).Elem() ) // Decode TOML data in to the pointer `v`. func (dec *Decoder) Decode(v any) (MetaData, error) { rv := reflect.ValueOf(v) if rv.Kind() != reflect.Ptr { s := "%q" if reflect.TypeOf(v) == nil { s = "%v" } return MetaData{}, fmt.Errorf("toml: cannot decode to non-pointer "+s, reflect.TypeOf(v)) } if rv.IsNil() { return MetaData{}, fmt.Errorf("toml: cannot decode to nil value of %q", reflect.TypeOf(v)) } // Check if this is a supported type: struct, map, any, or something that // implements UnmarshalTOML or UnmarshalText. rv = indirect(rv) rt := rv.Type() if rv.Kind() != reflect.Struct && rv.Kind() != reflect.Map && !(rv.Kind() == reflect.Interface && rv.NumMethod() == 0) && !rt.Implements(unmarshalToml) && !rt.Implements(unmarshalText) { return MetaData{}, fmt.Errorf("toml: cannot decode to type %s", rt) } // TODO: parser should read from io.Reader? Or at the very least, make it // read from []byte rather than string data, err := io.ReadAll(dec.r) if err != nil { return MetaData{}, err } p, err := parse(string(data)) if err != nil { return MetaData{}, err } md := MetaData{ mapping: p.mapping, keyInfo: p.keyInfo, keys: p.ordered, decoded: make(map[string]struct{}, len(p.ordered)), context: nil, data: data, } return md, md.unify(p.mapping, rv) } // PrimitiveDecode is just like the other Decode* functions, except it decodes a // TOML value that has already been parsed. Valid primitive values can *only* be // obtained from values filled by the decoder functions, including this method. // (i.e., v may contain more [Primitive] values.) // // Meta data for primitive values is included in the meta data returned by the // Decode* functions with one exception: keys returned by the Undecoded method // will only reflect keys that were decoded. Namely, any keys hidden behind a // Primitive will be considered undecoded. Executing this method will update the // undecoded keys in the meta data. (See the example.) func (md *MetaData) PrimitiveDecode(primValue Primitive, v any) error { md.context = primValue.context defer func() { md.context = nil }() return md.unify(primValue.undecoded, rvalue(v)) } // markDecodedRecursive is a helper to mark any key under the given tmap as // decoded, recursing as needed func markDecodedRecursive(md *MetaData, tmap map[string]any) { for key := range tmap { md.decoded[md.context.add(key).String()] = struct{}{} if tmap, ok := tmap[key].(map[string]any); ok { md.context = append(md.context, key) markDecodedRecursive(md, tmap) md.context = md.context[0 : len(md.context)-1] } if tarr, ok := tmap[key].([]map[string]any); ok { for _, elm := range tarr { md.context = append(md.context, key) markDecodedRecursive(md, elm) md.context = md.context[0 : len(md.context)-1] } } } } // unify performs a sort of type unification based on the structure of `rv`, // which is the client representation. // // Any type mismatch produces an error. Finding a type that we don't know // how to handle produces an unsupported type error. func (md *MetaData) unify(data any, rv reflect.Value) error { // Special case. Look for a `Primitive` value. // TODO: #76 would make this superfluous after implemented. if rv.Type() == primitiveType { // Save the undecoded data and the key context into the primitive // value. context := make(Key, len(md.context)) copy(context, md.context) rv.Set(reflect.ValueOf(Primitive{ undecoded: data, context: context, })) return nil } rvi := rv.Interface() if v, ok := rvi.(Unmarshaler); ok { err := v.UnmarshalTOML(data) if err != nil { return md.parseErr(err) } // Assume the Unmarshaler decoded everything, so mark all keys under // this table as decoded. if tmap, ok := data.(map[string]any); ok { markDecodedRecursive(md, tmap) } if aot, ok := data.([]map[string]any); ok { for _, tmap := range aot { markDecodedRecursive(md, tmap) } } return nil } if v, ok := rvi.(encoding.TextUnmarshaler); ok { return md.unifyText(data, v) } // TODO: // The behavior here is incorrect whenever a Go type satisfies the // encoding.TextUnmarshaler interface but also corresponds to a TOML hash or // array. In particular, the unmarshaler should only be applied to primitive // TOML values. But at this point, it will be applied to all kinds of values // and produce an incorrect error whenever those values are hashes or arrays // (including arrays of tables). k := rv.Kind() if k >= reflect.Int && k <= reflect.Uint64 { return md.unifyInt(data, rv) } switch k { case reflect.Struct: return md.unifyStruct(data, rv) case reflect.Map: return md.unifyMap(data, rv) case reflect.Array: return md.unifyArray(data, rv) case reflect.Slice: return md.unifySlice(data, rv) case reflect.String: return md.unifyString(data, rv) case reflect.Bool: return md.unifyBool(data, rv) case reflect.Interface: if rv.NumMethod() > 0 { /// Only empty interfaces are supported. return md.e("unsupported type %s", rv.Type()) } return md.unifyAnything(data, rv) case reflect.Float32, reflect.Float64: return md.unifyFloat64(data, rv) } return md.e("unsupported type %s", rv.Kind()) } func (md *MetaData) unifyStruct(mapping any, rv reflect.Value) error { tmap, ok := mapping.(map[string]any) if !ok { if mapping == nil { return nil } return md.e("type mismatch for %s: expected table but found %s", rv.Type().String(), fmtType(mapping)) } for key, datum := range tmap { var f *field fields := cachedTypeFields(rv.Type()) for i := range fields { ff := &fields[i] if ff.name == key { f = ff break } if f == nil && strings.EqualFold(ff.name, key) { f = ff } } if f != nil { subv := rv for _, i := range f.index { subv = indirect(subv.Field(i)) } if isUnifiable(subv) { md.decoded[md.context.add(key).String()] = struct{}{} md.context = append(md.context, key) err := md.unify(datum, subv) if err != nil { return err } md.context = md.context[0 : len(md.context)-1] } else if f.name != "" { return md.e("cannot write unexported field %s.%s", rv.Type().String(), f.name) } } } return nil } func (md *MetaData) unifyMap(mapping any, rv reflect.Value) error { keyType := rv.Type().Key().Kind() if keyType != reflect.String && keyType != reflect.Interface { return fmt.Errorf("toml: cannot decode to a map with non-string key type (%s in %q)", keyType, rv.Type()) } tmap, ok := mapping.(map[string]any) if !ok { if tmap == nil { return nil } return md.badtype("map", mapping) } if rv.IsNil() { rv.Set(reflect.MakeMap(rv.Type())) } for k, v := range tmap { md.decoded[md.context.add(k).String()] = struct{}{} md.context = append(md.context, k) rvval := reflect.Indirect(reflect.New(rv.Type().Elem())) err := md.unify(v, indirect(rvval)) if err != nil { return err } md.context = md.context[0 : len(md.context)-1] rvkey := indirect(reflect.New(rv.Type().Key())) switch keyType { case reflect.Interface: rvkey.Set(reflect.ValueOf(k)) case reflect.String: rvkey.SetString(k) } rv.SetMapIndex(rvkey, rvval) } return nil } func (md *MetaData) unifyArray(data any, rv reflect.Value) error { datav := reflect.ValueOf(data) if datav.Kind() != reflect.Slice { if !datav.IsValid() { return nil } return md.badtype("slice", data) } if l := datav.Len(); l != rv.Len() { return md.e("expected array length %d; got TOML array of length %d", rv.Len(), l) } return md.unifySliceArray(datav, rv) } func (md *MetaData) unifySlice(data any, rv reflect.Value) error { datav := reflect.ValueOf(data) if datav.Kind() != reflect.Slice { if !datav.IsValid() { return nil } return md.badtype("slice", data) } n := datav.Len() if rv.IsNil() || rv.Cap() < n { rv.Set(reflect.MakeSlice(rv.Type(), n, n)) } rv.SetLen(n) return md.unifySliceArray(datav, rv) } func (md *MetaData) unifySliceArray(data, rv reflect.Value) error { l := data.Len() for i := 0; i < l; i++ { err := md.unify(data.Index(i).Interface(), indirect(rv.Index(i))) if err != nil { return err } } return nil } func (md *MetaData) unifyString(data any, rv reflect.Value) error { _, ok := rv.Interface().(json.Number) if ok { if i, ok := data.(int64); ok { rv.SetString(strconv.FormatInt(i, 10)) } else if f, ok := data.(float64); ok { rv.SetString(strconv.FormatFloat(f, 'g', -1, 64)) } else { return md.badtype("string", data) } return nil } if s, ok := data.(string); ok { rv.SetString(s) return nil } return md.badtype("string", data) } func (md *MetaData) unifyFloat64(data any, rv reflect.Value) error { rvk := rv.Kind() if num, ok := data.(float64); ok { switch rvk { case reflect.Float32: if num < -math.MaxFloat32 || num > math.MaxFloat32 { return md.parseErr(errParseRange{i: num, size: rvk.String()}) } fallthrough case reflect.Float64: rv.SetFloat(num) default: panic("bug") } return nil } if num, ok := data.(int64); ok { if (rvk == reflect.Float32 && (num < -maxSafeFloat32Int || num > maxSafeFloat32Int)) || (rvk == reflect.Float64 && (num < -maxSafeFloat64Int || num > maxSafeFloat64Int)) { return md.parseErr(errUnsafeFloat{i: num, size: rvk.String()}) } rv.SetFloat(float64(num)) return nil } return md.badtype("float", data) } func (md *MetaData) unifyInt(data any, rv reflect.Value) error { _, ok := rv.Interface().(time.Duration) if ok { // Parse as string duration, and fall back to regular integer parsing // (as nanosecond) if this is not a string. if s, ok := data.(string); ok { dur, err := time.ParseDuration(s) if err != nil { return md.parseErr(errParseDuration{s}) } rv.SetInt(int64(dur)) return nil } } num, ok := data.(int64) if !ok { return md.badtype("integer", data) } rvk := rv.Kind() switch { case rvk >= reflect.Int && rvk <= reflect.Int64: if (rvk == reflect.Int8 && (num < math.MinInt8 || num > math.MaxInt8)) || (rvk == reflect.Int16 && (num < math.MinInt16 || num > math.MaxInt16)) || (rvk == reflect.Int32 && (num < math.MinInt32 || num > math.MaxInt32)) { return md.parseErr(errParseRange{i: num, size: rvk.String()}) } rv.SetInt(num) case rvk >= reflect.Uint && rvk <= reflect.Uint64: unum := uint64(num) if rvk == reflect.Uint8 && (num < 0 || unum > math.MaxUint8) || rvk == reflect.Uint16 && (num < 0 || unum > math.MaxUint16) || rvk == reflect.Uint32 && (num < 0 || unum > math.MaxUint32) { return md.parseErr(errParseRange{i: num, size: rvk.String()}) } rv.SetUint(unum) default: panic("unreachable") } return nil } func (md *MetaData) unifyBool(data any, rv reflect.Value) error { if b, ok := data.(bool); ok { rv.SetBool(b) return nil } return md.badtype("boolean", data) } func (md *MetaData) unifyAnything(data any, rv reflect.Value) error { rv.Set(reflect.ValueOf(data)) return nil } func (md *MetaData) unifyText(data any, v encoding.TextUnmarshaler) error { var s string switch sdata := data.(type) { case Marshaler: text, err := sdata.MarshalTOML() if err != nil { return err } s = string(text) case encoding.TextMarshaler: text, err := sdata.MarshalText() if err != nil { return err } s = string(text) case fmt.Stringer: s = sdata.String() case string: s = sdata case bool: s = fmt.Sprintf("%v", sdata) case int64: s = fmt.Sprintf("%d", sdata) case float64: s = fmt.Sprintf("%f", sdata) default: return md.badtype("primitive (string-like)", data) } if err := v.UnmarshalText([]byte(s)); err != nil { return md.parseErr(err) } return nil } func (md *MetaData) badtype(dst string, data any) error { return md.e("incompatible types: TOML value has type %s; destination has type %s", fmtType(data), dst) } func (md *MetaData) parseErr(err error) error { k := md.context.String() d := string(md.data) return ParseError{ Message: err.Error(), err: err, LastKey: k, Position: md.keyInfo[k].pos.withCol(d), Line: md.keyInfo[k].pos.Line, input: d, } } func (md *MetaData) e(format string, args ...any) error { f := "toml: " if len(md.context) > 0 { f = fmt.Sprintf("toml: (last key %q): ", md.context) p := md.keyInfo[md.context.String()].pos if p.Line > 0 { f = fmt.Sprintf("toml: line %d (last key %q): ", p.Line, md.context) } } return fmt.Errorf(f+format, args...) } // rvalue returns a reflect.Value of `v`. All pointers are resolved. func rvalue(v any) reflect.Value { return indirect(reflect.ValueOf(v)) } // indirect returns the value pointed to by a pointer. // // Pointers are followed until the value is not a pointer. New values are // allocated for each nil pointer. // // An exception to this rule is if the value satisfies an interface of interest // to us (like encoding.TextUnmarshaler). func indirect(v reflect.Value) reflect.Value { if v.Kind() != reflect.Ptr { if v.CanSet() { pv := v.Addr() pvi := pv.Interface() if _, ok := pvi.(encoding.TextUnmarshaler); ok { return pv } if _, ok := pvi.(Unmarshaler); ok { return pv } } return v } if v.IsNil() { v.Set(reflect.New(v.Type().Elem())) } return indirect(reflect.Indirect(v)) } func isUnifiable(rv reflect.Value) bool { if rv.CanSet() { return true } rvi := rv.Interface() if _, ok := rvi.(encoding.TextUnmarshaler); ok { return true } if _, ok := rvi.(Unmarshaler); ok { return true } return false } // fmt %T with "interface {}" replaced with "any", which is far more readable. func fmtType(t any) string { return strings.ReplaceAll(fmt.Sprintf("%T", t), "interface {}", "any") } ================================================ FILE: vendor/github.com/BurntSushi/toml/deprecated.go ================================================ package toml import ( "encoding" "io" ) // TextMarshaler is an alias for encoding.TextMarshaler. // // Deprecated: use encoding.TextMarshaler type TextMarshaler encoding.TextMarshaler // TextUnmarshaler is an alias for encoding.TextUnmarshaler. // // Deprecated: use encoding.TextUnmarshaler type TextUnmarshaler encoding.TextUnmarshaler // DecodeReader is an alias for NewDecoder(r).Decode(v). // // Deprecated: use NewDecoder(reader).Decode(&value). func DecodeReader(r io.Reader, v any) (MetaData, error) { return NewDecoder(r).Decode(v) } // PrimitiveDecode is an alias for MetaData.PrimitiveDecode(). // // Deprecated: use MetaData.PrimitiveDecode. func PrimitiveDecode(primValue Primitive, v any) error { md := MetaData{decoded: make(map[string]struct{})} return md.unify(primValue.undecoded, rvalue(v)) } ================================================ FILE: vendor/github.com/BurntSushi/toml/doc.go ================================================ // Package toml implements decoding and encoding of TOML files. // // This package supports TOML v1.0.0, as specified at https://toml.io // // The github.com/BurntSushi/toml/cmd/tomlv package implements a TOML validator, // and can be used to verify if TOML document is valid. It can also be used to // print the type of each key. package toml ================================================ FILE: vendor/github.com/BurntSushi/toml/encode.go ================================================ package toml import ( "bufio" "bytes" "encoding" "encoding/json" "errors" "fmt" "io" "math" "reflect" "sort" "strconv" "strings" "time" "github.com/BurntSushi/toml/internal" ) type tomlEncodeError struct{ error } var ( errArrayNilElement = errors.New("toml: cannot encode array with nil element") errNonString = errors.New("toml: cannot encode a map with non-string key type") errNoKey = errors.New("toml: top-level values must be Go maps or structs") errAnything = errors.New("") // used in testing ) var dblQuotedReplacer = strings.NewReplacer( "\"", "\\\"", "\\", "\\\\", "\x00", `\u0000`, "\x01", `\u0001`, "\x02", `\u0002`, "\x03", `\u0003`, "\x04", `\u0004`, "\x05", `\u0005`, "\x06", `\u0006`, "\x07", `\u0007`, "\b", `\b`, "\t", `\t`, "\n", `\n`, "\x0b", `\u000b`, "\f", `\f`, "\r", `\r`, "\x0e", `\u000e`, "\x0f", `\u000f`, "\x10", `\u0010`, "\x11", `\u0011`, "\x12", `\u0012`, "\x13", `\u0013`, "\x14", `\u0014`, "\x15", `\u0015`, "\x16", `\u0016`, "\x17", `\u0017`, "\x18", `\u0018`, "\x19", `\u0019`, "\x1a", `\u001a`, "\x1b", `\u001b`, "\x1c", `\u001c`, "\x1d", `\u001d`, "\x1e", `\u001e`, "\x1f", `\u001f`, "\x7f", `\u007f`, ) var ( marshalToml = reflect.TypeOf((*Marshaler)(nil)).Elem() marshalText = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem() timeType = reflect.TypeOf((*time.Time)(nil)).Elem() ) // Marshaler is the interface implemented by types that can marshal themselves // into valid TOML. type Marshaler interface { MarshalTOML() ([]byte, error) } // Marshal returns a TOML representation of the Go value. // // See [Encoder] for a description of the encoding process. func Marshal(v any) ([]byte, error) { buff := new(bytes.Buffer) if err := NewEncoder(buff).Encode(v); err != nil { return nil, err } return buff.Bytes(), nil } // Encoder encodes a Go to a TOML document. // // The mapping between Go values and TOML values should be precisely the same as // for [Decode]. // // time.Time is encoded as a RFC 3339 string, and time.Duration as its string // representation. // // The [Marshaler] and [encoding.TextMarshaler] interfaces are supported to // encoding the value as custom TOML. // // If you want to write arbitrary binary data then you will need to use // something like base64 since TOML does not have any binary types. // // When encoding TOML hashes (Go maps or structs), keys without any sub-hashes // are encoded first. // // Go maps will be sorted alphabetically by key for deterministic output. // // The toml struct tag can be used to provide the key name; if omitted the // struct field name will be used. If the "omitempty" option is present the // following value will be skipped: // // - arrays, slices, maps, and string with len of 0 // - struct with all zero values // - bool false // // If omitzero is given all int and float types with a value of 0 will be // skipped. // // Encoding Go values without a corresponding TOML representation will return an // error. Examples of this includes maps with non-string keys, slices with nil // elements, embedded non-struct types, and nested slices containing maps or // structs. (e.g. [][]map[string]string is not allowed but []map[string]string // is okay, as is []map[string][]string). // // NOTE: only exported keys are encoded due to the use of reflection. Unexported // keys are silently discarded. type Encoder struct { Indent string // string for a single indentation level; default is two spaces. hasWritten bool // written any output to w yet? w *bufio.Writer } // NewEncoder create a new Encoder. func NewEncoder(w io.Writer) *Encoder { return &Encoder{w: bufio.NewWriter(w), Indent: " "} } // Encode writes a TOML representation of the Go value to the [Encoder]'s writer. // // An error is returned if the value given cannot be encoded to a valid TOML // document. func (enc *Encoder) Encode(v any) error { rv := eindirect(reflect.ValueOf(v)) err := enc.safeEncode(Key([]string{}), rv) if err != nil { return err } return enc.w.Flush() } func (enc *Encoder) safeEncode(key Key, rv reflect.Value) (err error) { defer func() { if r := recover(); r != nil { if terr, ok := r.(tomlEncodeError); ok { err = terr.error return } panic(r) } }() enc.encode(key, rv) return nil } func (enc *Encoder) encode(key Key, rv reflect.Value) { // If we can marshal the type to text, then we use that. This prevents the // encoder for handling these types as generic structs (or whatever the // underlying type of a TextMarshaler is). switch { case isMarshaler(rv): enc.writeKeyValue(key, rv, false) return case rv.Type() == primitiveType: // TODO: #76 would make this superfluous after implemented. enc.encode(key, reflect.ValueOf(rv.Interface().(Primitive).undecoded)) return } k := rv.Kind() switch k { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64, reflect.String, reflect.Bool: enc.writeKeyValue(key, rv, false) case reflect.Array, reflect.Slice: if typeEqual(tomlArrayHash, tomlTypeOfGo(rv)) { enc.eArrayOfTables(key, rv) } else { enc.writeKeyValue(key, rv, false) } case reflect.Interface: if rv.IsNil() { return } enc.encode(key, rv.Elem()) case reflect.Map: if rv.IsNil() { return } enc.eTable(key, rv) case reflect.Ptr: if rv.IsNil() { return } enc.encode(key, rv.Elem()) case reflect.Struct: enc.eTable(key, rv) default: encPanic(fmt.Errorf("unsupported type for key '%s': %s", key, k)) } } // eElement encodes any value that can be an array element. func (enc *Encoder) eElement(rv reflect.Value) { switch v := rv.Interface().(type) { case time.Time: // Using TextMarshaler adds extra quotes, which we don't want. format := time.RFC3339Nano switch v.Location() { case internal.LocalDatetime: format = "2006-01-02T15:04:05.999999999" case internal.LocalDate: format = "2006-01-02" case internal.LocalTime: format = "15:04:05.999999999" } switch v.Location() { default: enc.write(v.Format(format)) case internal.LocalDatetime, internal.LocalDate, internal.LocalTime: enc.write(v.In(time.UTC).Format(format)) } return case Marshaler: s, err := v.MarshalTOML() if err != nil { encPanic(err) } if s == nil { encPanic(errors.New("MarshalTOML returned nil and no error")) } enc.w.Write(s) return case encoding.TextMarshaler: s, err := v.MarshalText() if err != nil { encPanic(err) } if s == nil { encPanic(errors.New("MarshalText returned nil and no error")) } enc.writeQuoted(string(s)) return case time.Duration: enc.writeQuoted(v.String()) return case json.Number: n, _ := rv.Interface().(json.Number) if n == "" { /// Useful zero value. enc.w.WriteByte('0') return } else if v, err := n.Int64(); err == nil { enc.eElement(reflect.ValueOf(v)) return } else if v, err := n.Float64(); err == nil { enc.eElement(reflect.ValueOf(v)) return } encPanic(fmt.Errorf("unable to convert %q to int64 or float64", n)) } switch rv.Kind() { case reflect.Ptr: enc.eElement(rv.Elem()) return case reflect.String: enc.writeQuoted(rv.String()) case reflect.Bool: enc.write(strconv.FormatBool(rv.Bool())) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: enc.write(strconv.FormatInt(rv.Int(), 10)) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: enc.write(strconv.FormatUint(rv.Uint(), 10)) case reflect.Float32: f := rv.Float() if math.IsNaN(f) { if math.Signbit(f) { enc.write("-") } enc.write("nan") } else if math.IsInf(f, 0) { if math.Signbit(f) { enc.write("-") } enc.write("inf") } else { enc.write(floatAddDecimal(strconv.FormatFloat(f, 'g', -1, 32))) } case reflect.Float64: f := rv.Float() if math.IsNaN(f) { if math.Signbit(f) { enc.write("-") } enc.write("nan") } else if math.IsInf(f, 0) { if math.Signbit(f) { enc.write("-") } enc.write("inf") } else { enc.write(floatAddDecimal(strconv.FormatFloat(f, 'g', -1, 64))) } case reflect.Array, reflect.Slice: enc.eArrayOrSliceElement(rv) case reflect.Struct: enc.eStruct(nil, rv, true) case reflect.Map: enc.eMap(nil, rv, true) case reflect.Interface: enc.eElement(rv.Elem()) default: encPanic(fmt.Errorf("unexpected type: %s", fmtType(rv.Interface()))) } } // By the TOML spec, all floats must have a decimal with at least one number on // either side. func floatAddDecimal(fstr string) string { for _, c := range fstr { if c == 'e' { // Exponent syntax return fstr } if c == '.' { return fstr } } return fstr + ".0" } func (enc *Encoder) writeQuoted(s string) { enc.write(`"` + dblQuotedReplacer.Replace(s) + `"`) } func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) { length := rv.Len() enc.write("[") for i := 0; i < length; i++ { elem := eindirect(rv.Index(i)) enc.eElement(elem) if i != length-1 { enc.write(", ") } } enc.write("]") } func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) { if len(key) == 0 { encPanic(errNoKey) } for i := 0; i < rv.Len(); i++ { trv := eindirect(rv.Index(i)) if isNil(trv) { continue } enc.newline() enc.writef("%s[[%s]]", enc.indentStr(key), key) enc.newline() enc.eMapOrStruct(key, trv, false) } } func (enc *Encoder) eTable(key Key, rv reflect.Value) { if len(key) == 1 { // Output an extra newline between top-level tables. // (The newline isn't written if nothing else has been written though.) enc.newline() } if len(key) > 0 { enc.writef("%s[%s]", enc.indentStr(key), key) enc.newline() } enc.eMapOrStruct(key, rv, false) } func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value, inline bool) { switch rv.Kind() { case reflect.Map: enc.eMap(key, rv, inline) case reflect.Struct: enc.eStruct(key, rv, inline) default: // Should never happen? panic("eTable: unhandled reflect.Value Kind: " + rv.Kind().String()) } } func (enc *Encoder) eMap(key Key, rv reflect.Value, inline bool) { rt := rv.Type() if rt.Key().Kind() != reflect.String { encPanic(errNonString) } // Sort keys so that we have deterministic output. And write keys directly // underneath this key first, before writing sub-structs or sub-maps. var mapKeysDirect, mapKeysSub []reflect.Value for _, mapKey := range rv.MapKeys() { if typeIsTable(tomlTypeOfGo(eindirect(rv.MapIndex(mapKey)))) { mapKeysSub = append(mapKeysSub, mapKey) } else { mapKeysDirect = append(mapKeysDirect, mapKey) } } writeMapKeys := func(mapKeys []reflect.Value, trailC bool) { sort.Slice(mapKeys, func(i, j int) bool { return mapKeys[i].String() < mapKeys[j].String() }) for i, mapKey := range mapKeys { val := eindirect(rv.MapIndex(mapKey)) if isNil(val) { continue } if inline { enc.writeKeyValue(Key{mapKey.String()}, val, true) if trailC || i != len(mapKeys)-1 { enc.write(", ") } } else { enc.encode(key.add(mapKey.String()), val) } } } if inline { enc.write("{") } writeMapKeys(mapKeysDirect, len(mapKeysSub) > 0) writeMapKeys(mapKeysSub, false) if inline { enc.write("}") } } func pointerTo(t reflect.Type) reflect.Type { if t.Kind() == reflect.Ptr { return pointerTo(t.Elem()) } return t } func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) { // Write keys for fields directly under this key first, because if we write // a field that creates a new table then all keys under it will be in that // table (not the one we're writing here). // // Fields is a [][]int: for fieldsDirect this always has one entry (the // struct index). For fieldsSub it contains two entries: the parent field // index from tv, and the field indexes for the fields of the sub. var ( rt = rv.Type() fieldsDirect, fieldsSub [][]int addFields func(rt reflect.Type, rv reflect.Value, start []int) ) addFields = func(rt reflect.Type, rv reflect.Value, start []int) { for i := 0; i < rt.NumField(); i++ { f := rt.Field(i) isEmbed := f.Anonymous && pointerTo(f.Type).Kind() == reflect.Struct if f.PkgPath != "" && !isEmbed { /// Skip unexported fields. continue } opts := getOptions(f.Tag) if opts.skip { continue } frv := eindirect(rv.Field(i)) // Need to make a copy because ... ehm, I don't know why... I guess // allocating a new array can cause it to fail(?) // // Done for: https://github.com/BurntSushi/toml/issues/430 // Previously only on 32bit for: https://github.com/BurntSushi/toml/issues/314 copyStart := make([]int, len(start)) copy(copyStart, start) start = copyStart // Treat anonymous struct fields with tag names as though they are // not anonymous, like encoding/json does. // // Non-struct anonymous fields use the normal encoding logic. if isEmbed { if getOptions(f.Tag).name == "" && frv.Kind() == reflect.Struct { addFields(frv.Type(), frv, append(start, f.Index...)) continue } } if typeIsTable(tomlTypeOfGo(frv)) { fieldsSub = append(fieldsSub, append(start, f.Index...)) } else { fieldsDirect = append(fieldsDirect, append(start, f.Index...)) } } } addFields(rt, rv, nil) writeFields := func(fields [][]int, totalFields int) { for _, fieldIndex := range fields { fieldType := rt.FieldByIndex(fieldIndex) fieldVal := rv.FieldByIndex(fieldIndex) opts := getOptions(fieldType.Tag) if opts.skip { continue } if opts.omitempty && isEmpty(fieldVal) { continue } fieldVal = eindirect(fieldVal) if isNil(fieldVal) { /// Don't write anything for nil fields. continue } keyName := fieldType.Name if opts.name != "" { keyName = opts.name } if opts.omitzero && isZero(fieldVal) { continue } if inline { enc.writeKeyValue(Key{keyName}, fieldVal, true) if fieldIndex[0] != totalFields-1 { enc.write(", ") } } else { enc.encode(key.add(keyName), fieldVal) } } } if inline { enc.write("{") } l := len(fieldsDirect) + len(fieldsSub) writeFields(fieldsDirect, l) writeFields(fieldsSub, l) if inline { enc.write("}") } } // tomlTypeOfGo returns the TOML type name of the Go value's type. // // It is used to determine whether the types of array elements are mixed (which // is forbidden). If the Go value is nil, then it is illegal for it to be an // array element, and valueIsNil is returned as true. // // The type may be `nil`, which means no concrete TOML type could be found. func tomlTypeOfGo(rv reflect.Value) tomlType { if isNil(rv) || !rv.IsValid() { return nil } if rv.Kind() == reflect.Struct { if rv.Type() == timeType { return tomlDatetime } if isMarshaler(rv) { return tomlString } return tomlHash } if isMarshaler(rv) { return tomlString } switch rv.Kind() { case reflect.Bool: return tomlBool case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return tomlInteger case reflect.Float32, reflect.Float64: return tomlFloat case reflect.Array, reflect.Slice: if isTableArray(rv) { return tomlArrayHash } return tomlArray case reflect.Ptr, reflect.Interface: return tomlTypeOfGo(rv.Elem()) case reflect.String: return tomlString case reflect.Map: return tomlHash default: encPanic(errors.New("unsupported type: " + rv.Kind().String())) panic("unreachable") } } func isMarshaler(rv reflect.Value) bool { return rv.Type().Implements(marshalText) || rv.Type().Implements(marshalToml) } // isTableArray reports if all entries in the array or slice are a table. func isTableArray(arr reflect.Value) bool { if isNil(arr) || !arr.IsValid() || arr.Len() == 0 { return false } ret := true for i := 0; i < arr.Len(); i++ { tt := tomlTypeOfGo(eindirect(arr.Index(i))) // Don't allow nil. if tt == nil { encPanic(errArrayNilElement) } if ret && !typeEqual(tomlHash, tt) { ret = false } } return ret } type tagOptions struct { skip bool // "-" name string omitempty bool omitzero bool } func getOptions(tag reflect.StructTag) tagOptions { t := tag.Get("toml") if t == "-" { return tagOptions{skip: true} } var opts tagOptions parts := strings.Split(t, ",") opts.name = parts[0] for _, s := range parts[1:] { switch s { case "omitempty": opts.omitempty = true case "omitzero": opts.omitzero = true } } return opts } func isZero(rv reflect.Value) bool { switch rv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return rv.Int() == 0 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return rv.Uint() == 0 case reflect.Float32, reflect.Float64: return rv.Float() == 0.0 } return false } func isEmpty(rv reflect.Value) bool { switch rv.Kind() { case reflect.Array, reflect.Slice, reflect.Map, reflect.String: return rv.Len() == 0 case reflect.Struct: if rv.Type().Comparable() { return reflect.Zero(rv.Type()).Interface() == rv.Interface() } // Need to also check if all the fields are empty, otherwise something // like this with uncomparable types will always return true: // // type a struct{ field b } // type b struct{ s []string } // s := a{field: b{s: []string{"AAA"}}} for i := 0; i < rv.NumField(); i++ { if !isEmpty(rv.Field(i)) { return false } } return true case reflect.Bool: return !rv.Bool() case reflect.Ptr: return rv.IsNil() } return false } func (enc *Encoder) newline() { if enc.hasWritten { enc.write("\n") } } // Write a key/value pair: // // key = // // This is also used for "k = v" in inline tables; so something like this will // be written in three calls: // // ┌───────────────────┐ // │ ┌───┐ ┌────┐│ // v v v v vv // key = {k = 1, k2 = 2} func (enc *Encoder) writeKeyValue(key Key, val reflect.Value, inline bool) { /// Marshaler used on top-level document; call eElement() to just call /// Marshal{TOML,Text}. if len(key) == 0 { enc.eElement(val) return } enc.writef("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1)) enc.eElement(val) if !inline { enc.newline() } } func (enc *Encoder) write(s string) { _, err := enc.w.WriteString(s) if err != nil { encPanic(err) } enc.hasWritten = true } func (enc *Encoder) writef(format string, v ...any) { _, err := fmt.Fprintf(enc.w, format, v...) if err != nil { encPanic(err) } enc.hasWritten = true } func (enc *Encoder) indentStr(key Key) string { return strings.Repeat(enc.Indent, len(key)-1) } func encPanic(err error) { panic(tomlEncodeError{err}) } // Resolve any level of pointers to the actual value (e.g. **string → string). func eindirect(v reflect.Value) reflect.Value { if v.Kind() != reflect.Ptr && v.Kind() != reflect.Interface { if isMarshaler(v) { return v } if v.CanAddr() { /// Special case for marshalers; see #358. if pv := v.Addr(); isMarshaler(pv) { return pv } } return v } if v.IsNil() { return v } return eindirect(v.Elem()) } func isNil(rv reflect.Value) bool { switch rv.Kind() { case reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: return rv.IsNil() default: return false } } ================================================ FILE: vendor/github.com/BurntSushi/toml/error.go ================================================ package toml import ( "fmt" "strings" ) // ParseError is returned when there is an error parsing the TOML syntax such as // invalid syntax, duplicate keys, etc. // // In addition to the error message itself, you can also print detailed location // information with context by using [ErrorWithPosition]: // // toml: error: Key 'fruit' was already created and cannot be used as an array. // // At line 4, column 2-7: // // 2 | fruit = [] // 3 | // 4 | [[fruit]] # Not allowed // ^^^^^ // // [ErrorWithUsage] can be used to print the above with some more detailed usage // guidance: // // toml: error: newlines not allowed within inline tables // // At line 1, column 18: // // 1 | x = [{ key = 42 # // ^ // // Error help: // // Inline tables must always be on a single line: // // table = {key = 42, second = 43} // // It is invalid to split them over multiple lines like so: // // # INVALID // table = { // key = 42, // second = 43 // } // // Use regular for this: // // [table] // key = 42 // second = 43 type ParseError struct { Message string // Short technical message. Usage string // Longer message with usage guidance; may be blank. Position Position // Position of the error LastKey string // Last parsed key, may be blank. // Line the error occurred. // // Deprecated: use [Position]. Line int err error input string } // Position of an error. type Position struct { Line int // Line number, starting at 1. Col int // Error column, starting at 1. Start int // Start of error, as byte offset starting at 0. Len int // Length of the error in bytes. } func (p Position) withCol(tomlFile string) Position { var ( pos int lines = strings.Split(tomlFile, "\n") ) for i := range lines { ll := len(lines[i]) + 1 // +1 for the removed newline if pos+ll >= p.Start { p.Col = p.Start - pos + 1 if p.Col < 1 { // Should never happen, but just in case. p.Col = 1 } break } pos += ll } return p } func (pe ParseError) Error() string { if pe.LastKey == "" { return fmt.Sprintf("toml: line %d: %s", pe.Position.Line, pe.Message) } return fmt.Sprintf("toml: line %d (last key %q): %s", pe.Position.Line, pe.LastKey, pe.Message) } // ErrorWithPosition returns the error with detailed location context. // // See the documentation on [ParseError]. func (pe ParseError) ErrorWithPosition() string { if pe.input == "" { // Should never happen, but just in case. return pe.Error() } // TODO: don't show control characters as literals? This may not show up // well everywhere. var ( lines = strings.Split(pe.input, "\n") b = new(strings.Builder) ) if pe.Position.Len == 1 { fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d:\n\n", pe.Message, pe.Position.Line, pe.Position.Col) } else { fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d-%d:\n\n", pe.Message, pe.Position.Line, pe.Position.Col, pe.Position.Col+pe.Position.Len-1) } if pe.Position.Line > 2 { fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-2, expandTab(lines[pe.Position.Line-3])) } if pe.Position.Line > 1 { fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-1, expandTab(lines[pe.Position.Line-2])) } /// Expand tabs, so that the ^^^s are at the correct position, but leave /// "column 10-13" intact. Adjusting this to the visual column would be /// better, but we don't know the tabsize of the user in their editor, which /// can be 8, 4, 2, or something else. We can't know. So leaving it as the /// character index is probably the "most correct". expanded := expandTab(lines[pe.Position.Line-1]) diff := len(expanded) - len(lines[pe.Position.Line-1]) fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line, expanded) fmt.Fprintf(b, "% 10s%s%s\n", "", strings.Repeat(" ", pe.Position.Col-1+diff), strings.Repeat("^", pe.Position.Len)) return b.String() } // ErrorWithUsage returns the error with detailed location context and usage // guidance. // // See the documentation on [ParseError]. func (pe ParseError) ErrorWithUsage() string { m := pe.ErrorWithPosition() if u, ok := pe.err.(interface{ Usage() string }); ok && u.Usage() != "" { lines := strings.Split(strings.TrimSpace(u.Usage()), "\n") for i := range lines { if lines[i] != "" { lines[i] = " " + lines[i] } } return m + "Error help:\n\n" + strings.Join(lines, "\n") + "\n" } return m } func expandTab(s string) string { var ( b strings.Builder l int fill = func(n int) string { b := make([]byte, n) for i := range b { b[i] = ' ' } return string(b) } ) b.Grow(len(s)) for _, r := range s { switch r { case '\t': tw := 8 - l%8 b.WriteString(fill(tw)) l += tw default: b.WriteRune(r) l += 1 } } return b.String() } type ( errLexControl struct{ r rune } errLexEscape struct{ r rune } errLexUTF8 struct{ b byte } errParseDate struct{ v string } errLexInlineTableNL struct{} errLexStringNL struct{} errParseRange struct { i any // int or float size string // "int64", "uint16", etc. } errUnsafeFloat struct { i interface{} // float32 or float64 size string // "float32" or "float64" } errParseDuration struct{ d string } ) func (e errLexControl) Error() string { return fmt.Sprintf("TOML files cannot contain control characters: '0x%02x'", e.r) } func (e errLexControl) Usage() string { return "" } func (e errLexEscape) Error() string { return fmt.Sprintf(`invalid escape in string '\%c'`, e.r) } func (e errLexEscape) Usage() string { return usageEscape } func (e errLexUTF8) Error() string { return fmt.Sprintf("invalid UTF-8 byte: 0x%02x", e.b) } func (e errLexUTF8) Usage() string { return "" } func (e errParseDate) Error() string { return fmt.Sprintf("invalid datetime: %q", e.v) } func (e errParseDate) Usage() string { return usageDate } func (e errLexInlineTableNL) Error() string { return "newlines not allowed within inline tables" } func (e errLexInlineTableNL) Usage() string { return usageInlineNewline } func (e errLexStringNL) Error() string { return "strings cannot contain newlines" } func (e errLexStringNL) Usage() string { return usageStringNewline } func (e errParseRange) Error() string { return fmt.Sprintf("%v is out of range for %s", e.i, e.size) } func (e errParseRange) Usage() string { return usageIntOverflow } func (e errUnsafeFloat) Error() string { return fmt.Sprintf("%v is out of the safe %s range", e.i, e.size) } func (e errUnsafeFloat) Usage() string { return usageUnsafeFloat } func (e errParseDuration) Error() string { return fmt.Sprintf("invalid duration: %q", e.d) } func (e errParseDuration) Usage() string { return usageDuration } const usageEscape = ` A '\' inside a "-delimited string is interpreted as an escape character. The following escape sequences are supported: \b, \t, \n, \f, \r, \", \\, \uXXXX, and \UXXXXXXXX To prevent a '\' from being recognized as an escape character, use either: - a ' or '''-delimited string; escape characters aren't processed in them; or - write two backslashes to get a single backslash: '\\'. If you're trying to add a Windows path (e.g. "C:\Users\martin") then using '/' instead of '\' will usually also work: "C:/Users/martin". ` const usageInlineNewline = ` Inline tables must always be on a single line: table = {key = 42, second = 43} It is invalid to split them over multiple lines like so: # INVALID table = { key = 42, second = 43 } Use regular for this: [table] key = 42 second = 43 ` const usageStringNewline = ` Strings must always be on a single line, and cannot span more than one line: # INVALID string = "Hello, world!" Instead use """ or ''' to split strings over multiple lines: string = """Hello, world!""" ` const usageIntOverflow = ` This number is too large; this may be an error in the TOML, but it can also be a bug in the program that uses too small of an integer. The maximum and minimum values are: size │ lowest │ highest ───────┼────────────────┼────────────── int8 │ -128 │ 127 int16 │ -32,768 │ 32,767 int32 │ -2,147,483,648 │ 2,147,483,647 int64 │ -9.2 × 10¹⁷ │ 9.2 × 10¹⁷ uint8 │ 0 │ 255 uint16 │ 0 │ 65,535 uint32 │ 0 │ 4,294,967,295 uint64 │ 0 │ 1.8 × 10¹⁸ int refers to int32 on 32-bit systems and int64 on 64-bit systems. ` const usageUnsafeFloat = ` This number is outside of the "safe" range for floating point numbers; whole (non-fractional) numbers outside the below range can not always be represented accurately in a float, leading to some loss of accuracy. Explicitly mark a number as a fractional unit by adding ".0", which will incur some loss of accuracy; for example: f = 2_000_000_000.0 Accuracy ranges: float32 = 16,777,215 float64 = 9,007,199,254,740,991 ` const usageDuration = ` A duration must be as "number", without any spaces. Valid units are: ns nanoseconds (billionth of a second) us, µs microseconds (millionth of a second) ms milliseconds (thousands of a second) s seconds m minutes h hours You can combine multiple units; for example "5m10s" for 5 minutes and 10 seconds. ` const usageDate = ` A TOML datetime must be in one of the following formats: 2006-01-02T15:04:05Z07:00 Date and time, with timezone. 2006-01-02T15:04:05 Date and time, but without timezone. 2006-01-02 Date without a time or timezone. 15:04:05 Just a time, without any timezone. Seconds may optionally have a fraction, up to nanosecond precision: 15:04:05.123 15:04:05.856018510 ` // TOML 1.1: // The seconds part in times is optional, and may be omitted: // 2006-01-02T15:04Z07:00 // 2006-01-02T15:04 // 15:04 ================================================ FILE: vendor/github.com/BurntSushi/toml/internal/tz.go ================================================ package internal import "time" // Timezones used for local datetime, date, and time TOML types. // // The exact way times and dates without a timezone should be interpreted is not // well-defined in the TOML specification and left to the implementation. These // defaults to current local timezone offset of the computer, but this can be // changed by changing these variables before decoding. // // TODO: // Ideally we'd like to offer people the ability to configure the used timezone // by setting Decoder.Timezone and Encoder.Timezone; however, this is a bit // tricky: the reason we use three different variables for this is to support // round-tripping – without these specific TZ names we wouldn't know which // format to use. // // There isn't a good way to encode this right now though, and passing this sort // of information also ties in to various related issues such as string format // encoding, encoding of comments, etc. // // So, for the time being, just put this in internal until we can write a good // comprehensive API for doing all of this. // // The reason they're exported is because they're referred from in e.g. // internal/tag. // // Note that this behaviour is valid according to the TOML spec as the exact // behaviour is left up to implementations. var ( localOffset = func() int { _, o := time.Now().Zone(); return o }() LocalDatetime = time.FixedZone("datetime-local", localOffset) LocalDate = time.FixedZone("date-local", localOffset) LocalTime = time.FixedZone("time-local", localOffset) ) ================================================ FILE: vendor/github.com/BurntSushi/toml/lex.go ================================================ package toml import ( "fmt" "reflect" "runtime" "strings" "unicode" "unicode/utf8" ) type itemType int const ( itemError itemType = iota itemEOF itemText itemString itemStringEsc itemRawString itemMultilineString itemRawMultilineString itemBool itemInteger itemFloat itemDatetime itemArray // the start of an array itemArrayEnd itemTableStart itemTableEnd itemArrayTableStart itemArrayTableEnd itemKeyStart itemKeyEnd itemCommentStart itemInlineTableStart itemInlineTableEnd ) const eof = 0 type stateFn func(lx *lexer) stateFn func (p Position) String() string { return fmt.Sprintf("at line %d; start %d; length %d", p.Line, p.Start, p.Len) } type lexer struct { input string start int pos int line int state stateFn items chan item esc bool // Allow for backing up up to 4 runes. This is necessary because TOML // contains 3-rune tokens (""" and '''). prevWidths [4]int nprev int // how many of prevWidths are in use atEOF bool // If we emit an eof, we can still back up, but it is not OK to call next again. // A stack of state functions used to maintain context. // // The idea is to reuse parts of the state machine in various places. For // example, values can appear at the top level or within arbitrarily nested // arrays. The last state on the stack is used after a value has been lexed. // Similarly for comments. stack []stateFn } type item struct { typ itemType val string err error pos Position } func (lx *lexer) nextItem() item { for { select { case item := <-lx.items: return item default: lx.state = lx.state(lx) //fmt.Printf(" STATE %-24s current: %-10s stack: %s\n", lx.state, lx.current(), lx.stack) } } } func lex(input string) *lexer { lx := &lexer{ input: input, state: lexTop, items: make(chan item, 10), stack: make([]stateFn, 0, 10), line: 1, } return lx } func (lx *lexer) push(state stateFn) { lx.stack = append(lx.stack, state) } func (lx *lexer) pop() stateFn { if len(lx.stack) == 0 { panic("BUG in lexer: no states to pop") } last := lx.stack[len(lx.stack)-1] lx.stack = lx.stack[0 : len(lx.stack)-1] return last } func (lx *lexer) current() string { return lx.input[lx.start:lx.pos] } func (lx lexer) getPos() Position { p := Position{ Line: lx.line, Start: lx.start, Len: lx.pos - lx.start, } if p.Len <= 0 { p.Len = 1 } return p } func (lx *lexer) emit(typ itemType) { // Needed for multiline strings ending with an incomplete UTF-8 sequence. if lx.start > lx.pos { lx.error(errLexUTF8{lx.input[lx.pos]}) return } lx.items <- item{typ: typ, pos: lx.getPos(), val: lx.current()} lx.start = lx.pos } func (lx *lexer) emitTrim(typ itemType) { lx.items <- item{typ: typ, pos: lx.getPos(), val: strings.TrimSpace(lx.current())} lx.start = lx.pos } func (lx *lexer) next() (r rune) { if lx.atEOF { panic("BUG in lexer: next called after EOF") } if lx.pos >= len(lx.input) { lx.atEOF = true return eof } if lx.input[lx.pos] == '\n' { lx.line++ } lx.prevWidths[3] = lx.prevWidths[2] lx.prevWidths[2] = lx.prevWidths[1] lx.prevWidths[1] = lx.prevWidths[0] if lx.nprev < 4 { lx.nprev++ } r, w := utf8.DecodeRuneInString(lx.input[lx.pos:]) if r == utf8.RuneError && w == 1 { lx.error(errLexUTF8{lx.input[lx.pos]}) return utf8.RuneError } // Note: don't use peek() here, as this calls next(). if isControl(r) || (r == '\r' && (len(lx.input)-1 == lx.pos || lx.input[lx.pos+1] != '\n')) { lx.errorControlChar(r) return utf8.RuneError } lx.prevWidths[0] = w lx.pos += w return r } // ignore skips over the pending input before this point. func (lx *lexer) ignore() { lx.start = lx.pos } // backup steps back one rune. Can be called 4 times between calls to next. func (lx *lexer) backup() { if lx.atEOF { lx.atEOF = false return } if lx.nprev < 1 { panic("BUG in lexer: backed up too far") } w := lx.prevWidths[0] lx.prevWidths[0] = lx.prevWidths[1] lx.prevWidths[1] = lx.prevWidths[2] lx.prevWidths[2] = lx.prevWidths[3] lx.nprev-- lx.pos -= w if lx.pos < len(lx.input) && lx.input[lx.pos] == '\n' { lx.line-- } } // accept consumes the next rune if it's equal to `valid`. func (lx *lexer) accept(valid rune) bool { if lx.next() == valid { return true } lx.backup() return false } // peek returns but does not consume the next rune in the input. func (lx *lexer) peek() rune { r := lx.next() lx.backup() return r } // skip ignores all input that matches the given predicate. func (lx *lexer) skip(pred func(rune) bool) { for { r := lx.next() if pred(r) { continue } lx.backup() lx.ignore() return } } // error stops all lexing by emitting an error and returning `nil`. // // Note that any value that is a character is escaped if it's a special // character (newlines, tabs, etc.). func (lx *lexer) error(err error) stateFn { if lx.atEOF { return lx.errorPrevLine(err) } lx.items <- item{typ: itemError, pos: lx.getPos(), err: err} return nil } // errorfPrevline is like error(), but sets the position to the last column of // the previous line. // // This is so that unexpected EOF or NL errors don't show on a new blank line. func (lx *lexer) errorPrevLine(err error) stateFn { pos := lx.getPos() pos.Line-- pos.Len = 1 pos.Start = lx.pos - 1 lx.items <- item{typ: itemError, pos: pos, err: err} return nil } // errorPos is like error(), but allows explicitly setting the position. func (lx *lexer) errorPos(start, length int, err error) stateFn { pos := lx.getPos() pos.Start = start pos.Len = length lx.items <- item{typ: itemError, pos: pos, err: err} return nil } // errorf is like error, and creates a new error. func (lx *lexer) errorf(format string, values ...any) stateFn { if lx.atEOF { pos := lx.getPos() if lx.pos >= 1 && lx.input[lx.pos-1] == '\n' { pos.Line-- } pos.Len = 1 pos.Start = lx.pos - 1 lx.items <- item{typ: itemError, pos: pos, err: fmt.Errorf(format, values...)} return nil } lx.items <- item{typ: itemError, pos: lx.getPos(), err: fmt.Errorf(format, values...)} return nil } func (lx *lexer) errorControlChar(cc rune) stateFn { return lx.errorPos(lx.pos-1, 1, errLexControl{cc}) } // lexTop consumes elements at the top level of TOML data. func lexTop(lx *lexer) stateFn { r := lx.next() if isWhitespace(r) || isNL(r) { return lexSkip(lx, lexTop) } switch r { case '#': lx.push(lexTop) return lexCommentStart case '[': return lexTableStart case eof: if lx.pos > lx.start { // TODO: never reached? I think this can only occur on a bug in the // lexer(?) return lx.errorf("unexpected EOF") } lx.emit(itemEOF) return nil } // At this point, the only valid item can be a key, so we back up // and let the key lexer do the rest. lx.backup() lx.push(lexTopEnd) return lexKeyStart } // lexTopEnd is entered whenever a top-level item has been consumed. (A value // or a table.) It must see only whitespace, and will turn back to lexTop // upon a newline. If it sees EOF, it will quit the lexer successfully. func lexTopEnd(lx *lexer) stateFn { r := lx.next() switch { case r == '#': // a comment will read to a newline for us. lx.push(lexTop) return lexCommentStart case isWhitespace(r): return lexTopEnd case isNL(r): lx.ignore() return lexTop case r == eof: lx.emit(itemEOF) return nil } return lx.errorf("expected a top-level item to end with a newline, comment, or EOF, but got %q instead", r) } // lexTable lexes the beginning of a table. Namely, it makes sure that // it starts with a character other than '.' and ']'. // It assumes that '[' has already been consumed. // It also handles the case that this is an item in an array of tables. // e.g., '[[name]]'. func lexTableStart(lx *lexer) stateFn { if lx.peek() == '[' { lx.next() lx.emit(itemArrayTableStart) lx.push(lexArrayTableEnd) } else { lx.emit(itemTableStart) lx.push(lexTableEnd) } return lexTableNameStart } func lexTableEnd(lx *lexer) stateFn { lx.emit(itemTableEnd) return lexTopEnd } func lexArrayTableEnd(lx *lexer) stateFn { if r := lx.next(); r != ']' { return lx.errorf("expected end of table array name delimiter ']', but got %q instead", r) } lx.emit(itemArrayTableEnd) return lexTopEnd } func lexTableNameStart(lx *lexer) stateFn { lx.skip(isWhitespace) switch r := lx.peek(); { case r == ']' || r == eof: return lx.errorf("unexpected end of table name (table names cannot be empty)") case r == '.': return lx.errorf("unexpected table separator (table names cannot be empty)") case r == '"' || r == '\'': lx.ignore() lx.push(lexTableNameEnd) return lexQuotedName default: lx.push(lexTableNameEnd) return lexBareName } } // lexTableNameEnd reads the end of a piece of a table name, optionally // consuming whitespace. func lexTableNameEnd(lx *lexer) stateFn { lx.skip(isWhitespace) switch r := lx.next(); { case r == '.': lx.ignore() return lexTableNameStart case r == ']': return lx.pop() default: return lx.errorf("expected '.' or ']' to end table name, but got %q instead", r) } } // lexBareName lexes one part of a key or table. // // It assumes that at least one valid character for the table has already been // read. // // Lexes only one part, e.g. only 'a' inside 'a.b'. func lexBareName(lx *lexer) stateFn { r := lx.next() if isBareKeyChar(r) { return lexBareName } lx.backup() lx.emit(itemText) return lx.pop() } // lexQuotedName lexes one part of a quoted key or table name. It assumes that // it starts lexing at the quote itself (" or '). // // Lexes only one part, e.g. only '"a"' inside '"a".b'. func lexQuotedName(lx *lexer) stateFn { r := lx.next() switch { case r == '"': lx.ignore() // ignore the '"' return lexString case r == '\'': lx.ignore() // ignore the "'" return lexRawString // TODO: I don't think any of the below conditions can ever be reached? case isWhitespace(r): return lexSkip(lx, lexValue) case r == eof: return lx.errorf("unexpected EOF; expected value") default: return lx.errorf("expected value but found %q instead", r) } } // lexKeyStart consumes all key parts until a '='. func lexKeyStart(lx *lexer) stateFn { lx.skip(isWhitespace) switch r := lx.peek(); { case r == '=' || r == eof: return lx.errorf("unexpected '=': key name appears blank") case r == '.': return lx.errorf("unexpected '.': keys cannot start with a '.'") case r == '"' || r == '\'': lx.ignore() fallthrough default: // Bare key lx.emit(itemKeyStart) return lexKeyNameStart } } func lexKeyNameStart(lx *lexer) stateFn { lx.skip(isWhitespace) switch r := lx.peek(); { default: lx.push(lexKeyEnd) return lexBareName case r == '"' || r == '\'': lx.ignore() lx.push(lexKeyEnd) return lexQuotedName // TODO: I think these can never be reached? case r == '=' || r == eof: return lx.errorf("unexpected '='") case r == '.': return lx.errorf("unexpected '.'") } } // lexKeyEnd consumes the end of a key and trims whitespace (up to the key // separator). func lexKeyEnd(lx *lexer) stateFn { lx.skip(isWhitespace) switch r := lx.next(); { case isWhitespace(r): return lexSkip(lx, lexKeyEnd) case r == eof: // TODO: never reached return lx.errorf("unexpected EOF; expected key separator '='") case r == '.': lx.ignore() return lexKeyNameStart case r == '=': lx.emit(itemKeyEnd) return lexSkip(lx, lexValue) default: if r == '\n' { return lx.errorPrevLine(fmt.Errorf("expected '.' or '=', but got %q instead", r)) } return lx.errorf("expected '.' or '=', but got %q instead", r) } } // lexValue starts the consumption of a value anywhere a value is expected. // lexValue will ignore whitespace. // After a value is lexed, the last state on the next is popped and returned. func lexValue(lx *lexer) stateFn { // We allow whitespace to precede a value, but NOT newlines. // In array syntax, the array states are responsible for ignoring newlines. r := lx.next() switch { case isWhitespace(r): return lexSkip(lx, lexValue) case isDigit(r): lx.backup() // avoid an extra state and use the same as above return lexNumberOrDateStart } switch r { case '[': lx.ignore() lx.emit(itemArray) return lexArrayValue case '{': lx.ignore() lx.emit(itemInlineTableStart) return lexInlineTableValue case '"': if lx.accept('"') { if lx.accept('"') { lx.ignore() // Ignore """ return lexMultilineString } lx.backup() } lx.ignore() // ignore the '"' return lexString case '\'': if lx.accept('\'') { if lx.accept('\'') { lx.ignore() // Ignore """ return lexMultilineRawString } lx.backup() } lx.ignore() // ignore the "'" return lexRawString case '.': // special error case, be kind to users return lx.errorf("floats must start with a digit, not '.'") case 'i', 'n': if (lx.accept('n') && lx.accept('f')) || (lx.accept('a') && lx.accept('n')) { lx.emit(itemFloat) return lx.pop() } case '-', '+': return lexDecimalNumberStart } if unicode.IsLetter(r) { // Be permissive here; lexBool will give a nice error if the // user wrote something like // x = foo // (i.e. not 'true' or 'false' but is something else word-like.) lx.backup() return lexBool } if r == eof { return lx.errorf("unexpected EOF; expected value") } if r == '\n' { return lx.errorPrevLine(fmt.Errorf("expected value but found %q instead", r)) } return lx.errorf("expected value but found %q instead", r) } // lexArrayValue consumes one value in an array. It assumes that '[' or ',' // have already been consumed. All whitespace and newlines are ignored. func lexArrayValue(lx *lexer) stateFn { r := lx.next() switch { case isWhitespace(r) || isNL(r): return lexSkip(lx, lexArrayValue) case r == '#': lx.push(lexArrayValue) return lexCommentStart case r == ',': return lx.errorf("unexpected comma") case r == ']': return lexArrayEnd } lx.backup() lx.push(lexArrayValueEnd) return lexValue } // lexArrayValueEnd consumes everything between the end of an array value and // the next value (or the end of the array): it ignores whitespace and newlines // and expects either a ',' or a ']'. func lexArrayValueEnd(lx *lexer) stateFn { switch r := lx.next(); { case isWhitespace(r) || isNL(r): return lexSkip(lx, lexArrayValueEnd) case r == '#': lx.push(lexArrayValueEnd) return lexCommentStart case r == ',': lx.ignore() return lexArrayValue // move on to the next value case r == ']': return lexArrayEnd default: return lx.errorf("expected a comma (',') or array terminator (']'), but got %s", runeOrEOF(r)) } } // lexArrayEnd finishes the lexing of an array. // It assumes that a ']' has just been consumed. func lexArrayEnd(lx *lexer) stateFn { lx.ignore() lx.emit(itemArrayEnd) return lx.pop() } // lexInlineTableValue consumes one key/value pair in an inline table. // It assumes that '{' or ',' have already been consumed. Whitespace is ignored. func lexInlineTableValue(lx *lexer) stateFn { r := lx.next() switch { case isWhitespace(r): return lexSkip(lx, lexInlineTableValue) case isNL(r): return lexSkip(lx, lexInlineTableValue) case r == '#': lx.push(lexInlineTableValue) return lexCommentStart case r == ',': return lx.errorf("unexpected comma") case r == '}': return lexInlineTableEnd } lx.backup() lx.push(lexInlineTableValueEnd) return lexKeyStart } // lexInlineTableValueEnd consumes everything between the end of an inline table // key/value pair and the next pair (or the end of the table): // it ignores whitespace and expects either a ',' or a '}'. func lexInlineTableValueEnd(lx *lexer) stateFn { switch r := lx.next(); { case isWhitespace(r): return lexSkip(lx, lexInlineTableValueEnd) case isNL(r): return lexSkip(lx, lexInlineTableValueEnd) case r == '#': lx.push(lexInlineTableValueEnd) return lexCommentStart case r == ',': lx.ignore() lx.skip(isWhitespace) if lx.peek() == '}' { return lexInlineTableValueEnd } return lexInlineTableValue case r == '}': return lexInlineTableEnd default: return lx.errorf("expected a comma or an inline table terminator '}', but got %s instead", runeOrEOF(r)) } } func runeOrEOF(r rune) string { if r == eof { return "end of file" } return "'" + string(r) + "'" } // lexInlineTableEnd finishes the lexing of an inline table. // It assumes that a '}' has just been consumed. func lexInlineTableEnd(lx *lexer) stateFn { lx.ignore() lx.emit(itemInlineTableEnd) return lx.pop() } // lexString consumes the inner contents of a string. It assumes that the // beginning '"' has already been consumed and ignored. func lexString(lx *lexer) stateFn { r := lx.next() switch { case r == eof: return lx.errorf(`unexpected EOF; expected '"'`) case isNL(r): return lx.errorPrevLine(errLexStringNL{}) case r == '\\': lx.push(lexString) return lexStringEscape case r == '"': lx.backup() if lx.esc { lx.esc = false lx.emit(itemStringEsc) } else { lx.emit(itemString) } lx.next() lx.ignore() return lx.pop() } return lexString } // lexMultilineString consumes the inner contents of a string. It assumes that // the beginning '"""' has already been consumed and ignored. func lexMultilineString(lx *lexer) stateFn { r := lx.next() switch r { default: return lexMultilineString case eof: return lx.errorf(`unexpected EOF; expected '"""'`) case '\\': return lexMultilineStringEscape case '"': /// Found " → try to read two more "". if lx.accept('"') { if lx.accept('"') { /// Peek ahead: the string can contain " and "", including at the /// end: """str""""" /// 6 or more at the end, however, is an error. if lx.peek() == '"' { /// Check if we already lexed 5 's; if so we have 6 now, and /// that's just too many man! /// /// Second check is for the edge case: /// /// two quotes allowed. /// vv /// """lol \"""""" /// ^^ ^^^---- closing three /// escaped /// /// But ugly, but it works if strings.HasSuffix(lx.current(), `"""""`) && !strings.HasSuffix(lx.current(), `\"""""`) { return lx.errorf(`unexpected '""""""'`) } lx.backup() lx.backup() return lexMultilineString } lx.backup() /// backup: don't include the """ in the item. lx.backup() lx.backup() lx.esc = false lx.emit(itemMultilineString) lx.next() /// Read over ''' again and discard it. lx.next() lx.next() lx.ignore() return lx.pop() } lx.backup() } return lexMultilineString } } // lexRawString consumes a raw string. Nothing can be escaped in such a string. // It assumes that the beginning "'" has already been consumed and ignored. func lexRawString(lx *lexer) stateFn { r := lx.next() switch { default: return lexRawString case r == eof: return lx.errorf(`unexpected EOF; expected "'"`) case isNL(r): return lx.errorPrevLine(errLexStringNL{}) case r == '\'': lx.backup() lx.emit(itemRawString) lx.next() lx.ignore() return lx.pop() } } // lexMultilineRawString consumes a raw string. Nothing can be escaped in such a // string. It assumes that the beginning triple-' has already been consumed and // ignored. func lexMultilineRawString(lx *lexer) stateFn { r := lx.next() switch r { default: return lexMultilineRawString case eof: return lx.errorf(`unexpected EOF; expected "'''"`) case '\'': /// Found ' → try to read two more ''. if lx.accept('\'') { if lx.accept('\'') { /// Peek ahead: the string can contain ' and '', including at the /// end: '''str''''' /// 6 or more at the end, however, is an error. if lx.peek() == '\'' { /// Check if we already lexed 5 's; if so we have 6 now, and /// that's just too many man! if strings.HasSuffix(lx.current(), "'''''") { return lx.errorf(`unexpected "''''''"`) } lx.backup() lx.backup() return lexMultilineRawString } lx.backup() /// backup: don't include the ''' in the item. lx.backup() lx.backup() lx.emit(itemRawMultilineString) lx.next() /// Read over ''' again and discard it. lx.next() lx.next() lx.ignore() return lx.pop() } lx.backup() } return lexMultilineRawString } } // lexMultilineStringEscape consumes an escaped character. It assumes that the // preceding '\\' has already been consumed. func lexMultilineStringEscape(lx *lexer) stateFn { if isNL(lx.next()) { /// \ escaping newline. return lexMultilineString } lx.backup() lx.push(lexMultilineString) return lexStringEscape(lx) } func lexStringEscape(lx *lexer) stateFn { lx.esc = true r := lx.next() switch r { case 'e': fallthrough case 'b': fallthrough case 't': fallthrough case 'n': fallthrough case 'f': fallthrough case 'r': fallthrough case '"': fallthrough case ' ', '\t': // Inside """ .. """ strings you can use \ to escape newlines, and any // amount of whitespace can be between the \ and \n. fallthrough case '\\': return lx.pop() case 'x': return lexHexEscape case 'u': return lexShortUnicodeEscape case 'U': return lexLongUnicodeEscape } return lx.error(errLexEscape{r}) } func lexHexEscape(lx *lexer) stateFn { var r rune for i := 0; i < 2; i++ { r = lx.next() if !isHex(r) { return lx.errorf(`expected two hexadecimal digits after '\x', but got %q instead`, lx.current()) } } return lx.pop() } func lexShortUnicodeEscape(lx *lexer) stateFn { var r rune for i := 0; i < 4; i++ { r = lx.next() if !isHex(r) { return lx.errorf(`expected four hexadecimal digits after '\u', but got %q instead`, lx.current()) } } return lx.pop() } func lexLongUnicodeEscape(lx *lexer) stateFn { var r rune for i := 0; i < 8; i++ { r = lx.next() if !isHex(r) { return lx.errorf(`expected eight hexadecimal digits after '\U', but got %q instead`, lx.current()) } } return lx.pop() } // lexNumberOrDateStart processes the first character of a value which begins // with a digit. It exists to catch values starting with '0', so that // lexBaseNumberOrDate can differentiate base prefixed integers from other // types. func lexNumberOrDateStart(lx *lexer) stateFn { if lx.next() == '0' { return lexBaseNumberOrDate } return lexNumberOrDate } // lexNumberOrDate consumes either an integer, float or datetime. func lexNumberOrDate(lx *lexer) stateFn { r := lx.next() if isDigit(r) { return lexNumberOrDate } switch r { case '-', ':': return lexDatetime case '_': return lexDecimalNumber case '.', 'e', 'E': return lexFloat } lx.backup() lx.emit(itemInteger) return lx.pop() } // lexDatetime consumes a Datetime, to a first approximation. // The parser validates that it matches one of the accepted formats. func lexDatetime(lx *lexer) stateFn { r := lx.next() if isDigit(r) { return lexDatetime } switch r { case '-', ':', 'T', 't', ' ', '.', 'Z', 'z', '+': return lexDatetime } lx.backup() lx.emitTrim(itemDatetime) return lx.pop() } // lexHexInteger consumes a hexadecimal integer after seeing the '0x' prefix. func lexHexInteger(lx *lexer) stateFn { r := lx.next() if isHex(r) { return lexHexInteger } switch r { case '_': return lexHexInteger } lx.backup() lx.emit(itemInteger) return lx.pop() } // lexOctalInteger consumes an octal integer after seeing the '0o' prefix. func lexOctalInteger(lx *lexer) stateFn { r := lx.next() if isOctal(r) { return lexOctalInteger } switch r { case '_': return lexOctalInteger } lx.backup() lx.emit(itemInteger) return lx.pop() } // lexBinaryInteger consumes a binary integer after seeing the '0b' prefix. func lexBinaryInteger(lx *lexer) stateFn { r := lx.next() if isBinary(r) { return lexBinaryInteger } switch r { case '_': return lexBinaryInteger } lx.backup() lx.emit(itemInteger) return lx.pop() } // lexDecimalNumber consumes a decimal float or integer. func lexDecimalNumber(lx *lexer) stateFn { r := lx.next() if isDigit(r) { return lexDecimalNumber } switch r { case '.', 'e', 'E': return lexFloat case '_': return lexDecimalNumber } lx.backup() lx.emit(itemInteger) return lx.pop() } // lexDecimalNumber consumes the first digit of a number beginning with a sign. // It assumes the sign has already been consumed. Values which start with a sign // are only allowed to be decimal integers or floats. // // The special "nan" and "inf" values are also recognized. func lexDecimalNumberStart(lx *lexer) stateFn { r := lx.next() // Special error cases to give users better error messages switch r { case 'i': if !lx.accept('n') || !lx.accept('f') { return lx.errorf("invalid float: '%s'", lx.current()) } lx.emit(itemFloat) return lx.pop() case 'n': if !lx.accept('a') || !lx.accept('n') { return lx.errorf("invalid float: '%s'", lx.current()) } lx.emit(itemFloat) return lx.pop() case '0': p := lx.peek() switch p { case 'b', 'o', 'x': return lx.errorf("cannot use sign with non-decimal numbers: '%s%c'", lx.current(), p) } case '.': return lx.errorf("floats must start with a digit, not '.'") } if isDigit(r) { return lexDecimalNumber } return lx.errorf("expected a digit but got %q", r) } // lexBaseNumberOrDate differentiates between the possible values which // start with '0'. It assumes that before reaching this state, the initial '0' // has been consumed. func lexBaseNumberOrDate(lx *lexer) stateFn { r := lx.next() // Note: All datetimes start with at least two digits, so we don't // handle date characters (':', '-', etc.) here. if isDigit(r) { return lexNumberOrDate } switch r { case '_': // Can only be decimal, because there can't be an underscore // between the '0' and the base designator, and dates can't // contain underscores. return lexDecimalNumber case '.', 'e', 'E': return lexFloat case 'b': r = lx.peek() if !isBinary(r) { lx.errorf("not a binary number: '%s%c'", lx.current(), r) } return lexBinaryInteger case 'o': r = lx.peek() if !isOctal(r) { lx.errorf("not an octal number: '%s%c'", lx.current(), r) } return lexOctalInteger case 'x': r = lx.peek() if !isHex(r) { lx.errorf("not a hexadecimal number: '%s%c'", lx.current(), r) } return lexHexInteger } lx.backup() lx.emit(itemInteger) return lx.pop() } // lexFloat consumes the elements of a float. It allows any sequence of // float-like characters, so floats emitted by the lexer are only a first // approximation and must be validated by the parser. func lexFloat(lx *lexer) stateFn { r := lx.next() if isDigit(r) { return lexFloat } switch r { case '_', '.', '-', '+', 'e', 'E': return lexFloat } lx.backup() lx.emit(itemFloat) return lx.pop() } // lexBool consumes a bool string: 'true' or 'false. func lexBool(lx *lexer) stateFn { var rs []rune for { r := lx.next() if !unicode.IsLetter(r) { lx.backup() break } rs = append(rs, r) } s := string(rs) switch s { case "true", "false": lx.emit(itemBool) return lx.pop() } return lx.errorf("expected value but found %q instead", s) } // lexCommentStart begins the lexing of a comment. It will emit // itemCommentStart and consume no characters, passing control to lexComment. func lexCommentStart(lx *lexer) stateFn { lx.ignore() lx.emit(itemCommentStart) return lexComment } // lexComment lexes an entire comment. It assumes that '#' has been consumed. // It will consume *up to* the first newline character, and pass control // back to the last state on the stack. func lexComment(lx *lexer) stateFn { switch r := lx.next(); { case isNL(r) || r == eof: lx.backup() lx.emit(itemText) return lx.pop() default: return lexComment } } // lexSkip ignores all slurped input and moves on to the next state. func lexSkip(lx *lexer, nextState stateFn) stateFn { lx.ignore() return nextState } func (s stateFn) String() string { if s == nil { return "" } name := runtime.FuncForPC(reflect.ValueOf(s).Pointer()).Name() if i := strings.LastIndexByte(name, '.'); i > -1 { name = name[i+1:] } return name + "()" } func (itype itemType) String() string { switch itype { case itemError: return "Error" case itemEOF: return "EOF" case itemText: return "Text" case itemString, itemStringEsc, itemRawString, itemMultilineString, itemRawMultilineString: return "String" case itemBool: return "Bool" case itemInteger: return "Integer" case itemFloat: return "Float" case itemDatetime: return "DateTime" case itemArray: return "Array" case itemArrayEnd: return "ArrayEnd" case itemTableStart: return "TableStart" case itemTableEnd: return "TableEnd" case itemArrayTableStart: return "ArrayTableStart" case itemArrayTableEnd: return "ArrayTableEnd" case itemKeyStart: return "KeyStart" case itemKeyEnd: return "KeyEnd" case itemCommentStart: return "CommentStart" case itemInlineTableStart: return "InlineTableStart" case itemInlineTableEnd: return "InlineTableEnd" } panic(fmt.Sprintf("BUG: Unknown type '%d'.", int(itype))) } func (item item) String() string { return fmt.Sprintf("(%s, %s)", item.typ, item.val) } func isWhitespace(r rune) bool { return r == '\t' || r == ' ' } func isNL(r rune) bool { return r == '\n' || r == '\r' } func isControl(r rune) bool { // Control characters except \t, \r, \n switch r { case '\t', '\r', '\n': return false default: return (r >= 0x00 && r <= 0x1f) || r == 0x7f } } func isDigit(r rune) bool { return r >= '0' && r <= '9' } func isBinary(r rune) bool { return r == '0' || r == '1' } func isOctal(r rune) bool { return r >= '0' && r <= '7' } func isHex(r rune) bool { return (r >= '0' && r <= '9') || (r|0x20 >= 'a' && r|0x20 <= 'f') } func isBareKeyChar(r rune) bool { return (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' || r == '-' } ================================================ FILE: vendor/github.com/BurntSushi/toml/meta.go ================================================ package toml import ( "strings" ) // MetaData allows access to meta information about TOML data that's not // accessible otherwise. // // It allows checking if a key is defined in the TOML data, whether any keys // were undecoded, and the TOML type of a key. type MetaData struct { context Key // Used only during decoding. keyInfo map[string]keyInfo mapping map[string]any keys []Key decoded map[string]struct{} data []byte // Input file; for errors. } // IsDefined reports if the key exists in the TOML data. // // The key should be specified hierarchically, for example to access the TOML // key "a.b.c" you would use IsDefined("a", "b", "c"). Keys are case sensitive. // // Returns false for an empty key. func (md *MetaData) IsDefined(key ...string) bool { if len(key) == 0 { return false } var ( hash map[string]any ok bool hashOrVal any = md.mapping ) for _, k := range key { if hash, ok = hashOrVal.(map[string]any); !ok { return false } if hashOrVal, ok = hash[k]; !ok { return false } } return true } // Type returns a string representation of the type of the key specified. // // Type will return the empty string if given an empty key or a key that does // not exist. Keys are case sensitive. func (md *MetaData) Type(key ...string) string { if ki, ok := md.keyInfo[Key(key).String()]; ok { return ki.tomlType.typeString() } return "" } // Keys returns a slice of every key in the TOML data, including key groups. // // Each key is itself a slice, where the first element is the top of the // hierarchy and the last is the most specific. The list will have the same // order as the keys appeared in the TOML data. // // All keys returned are non-empty. func (md *MetaData) Keys() []Key { return md.keys } // Undecoded returns all keys that have not been decoded in the order in which // they appear in the original TOML document. // // This includes keys that haven't been decoded because of a [Primitive] value. // Once the Primitive value is decoded, the keys will be considered decoded. // // Also note that decoding into an empty interface will result in no decoding, // and so no keys will be considered decoded. // // In this sense, the Undecoded keys correspond to keys in the TOML document // that do not have a concrete type in your representation. func (md *MetaData) Undecoded() []Key { undecoded := make([]Key, 0, len(md.keys)) for _, key := range md.keys { if _, ok := md.decoded[key.String()]; !ok { undecoded = append(undecoded, key) } } return undecoded } // Key represents any TOML key, including key groups. Use [MetaData.Keys] to get // values of this type. type Key []string func (k Key) String() string { // This is called quite often, so it's a bit funky to make it faster. var b strings.Builder b.Grow(len(k) * 25) outer: for i, kk := range k { if i > 0 { b.WriteByte('.') } if kk == "" { b.WriteString(`""`) } else { for _, r := range kk { // "Inline" isBareKeyChar if !((r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' || r == '-') { b.WriteByte('"') b.WriteString(dblQuotedReplacer.Replace(kk)) b.WriteByte('"') continue outer } } b.WriteString(kk) } } return b.String() } func (k Key) maybeQuoted(i int) string { if k[i] == "" { return `""` } for _, r := range k[i] { if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' || r == '-' { continue } return `"` + dblQuotedReplacer.Replace(k[i]) + `"` } return k[i] } // Like append(), but only increase the cap by 1. func (k Key) add(piece string) Key { newKey := make(Key, len(k)+1) copy(newKey, k) newKey[len(k)] = piece return newKey } func (k Key) parent() Key { return k[:len(k)-1] } // all except the last piece. func (k Key) last() string { return k[len(k)-1] } // last piece of this key. ================================================ FILE: vendor/github.com/BurntSushi/toml/parse.go ================================================ package toml import ( "fmt" "math" "strconv" "strings" "time" "unicode/utf8" "github.com/BurntSushi/toml/internal" ) type parser struct { lx *lexer context Key // Full key for the current hash in scope. currentKey string // Base key name for everything except hashes. pos Position // Current position in the TOML file. ordered []Key // List of keys in the order that they appear in the TOML data. keyInfo map[string]keyInfo // Map keyname → info about the TOML key. mapping map[string]any // Map keyname → key value. implicits map[string]struct{} // Record implicit keys (e.g. "key.group.names"). } type keyInfo struct { pos Position tomlType tomlType } func parse(data string) (p *parser, err error) { defer func() { if r := recover(); r != nil { if pErr, ok := r.(ParseError); ok { pErr.input = data err = pErr return } panic(r) } }() // Read over BOM; do this here as the lexer calls utf8.DecodeRuneInString() // which mangles stuff. UTF-16 BOM isn't strictly valid, but some tools add // it anyway. if strings.HasPrefix(data, "\xff\xfe") || strings.HasPrefix(data, "\xfe\xff") { // UTF-16 data = data[2:] } else if strings.HasPrefix(data, "\xef\xbb\xbf") { // UTF-8 data = data[3:] } // Examine first few bytes for NULL bytes; this probably means it's a UTF-16 // file (second byte in surrogate pair being NULL). Again, do this here to // avoid having to deal with UTF-8/16 stuff in the lexer. ex := 6 if len(data) < 6 { ex = len(data) } if i := strings.IndexRune(data[:ex], 0); i > -1 { return nil, ParseError{ Message: "files cannot contain NULL bytes; probably using UTF-16; TOML files must be UTF-8", Position: Position{Line: 1, Col: 1, Start: i, Len: 1}, Line: 1, input: data, } } p = &parser{ keyInfo: make(map[string]keyInfo), mapping: make(map[string]any), lx: lex(data), ordered: make([]Key, 0), implicits: make(map[string]struct{}), } for { item := p.next() if item.typ == itemEOF { break } p.topLevel(item) } return p, nil } func (p *parser) panicErr(it item, err error) { panic(ParseError{ Message: err.Error(), err: err, Position: it.pos.withCol(p.lx.input), Line: it.pos.Len, LastKey: p.current(), }) } func (p *parser) panicItemf(it item, format string, v ...any) { panic(ParseError{ Message: fmt.Sprintf(format, v...), Position: it.pos.withCol(p.lx.input), Line: it.pos.Len, LastKey: p.current(), }) } func (p *parser) panicf(format string, v ...any) { panic(ParseError{ Message: fmt.Sprintf(format, v...), Position: p.pos.withCol(p.lx.input), Line: p.pos.Line, LastKey: p.current(), }) } func (p *parser) next() item { it := p.lx.nextItem() //fmt.Printf("ITEM %-18s line %-3d │ %q\n", it.typ, it.pos.Line, it.val) if it.typ == itemError { if it.err != nil { panic(ParseError{ Message: it.err.Error(), err: it.err, Position: it.pos.withCol(p.lx.input), Line: it.pos.Line, LastKey: p.current(), }) } p.panicItemf(it, "%s", it.val) } return it } func (p *parser) nextPos() item { it := p.next() p.pos = it.pos return it } func (p *parser) bug(format string, v ...any) { panic(fmt.Sprintf("BUG: "+format+"\n\n", v...)) } func (p *parser) expect(typ itemType) item { it := p.next() p.assertEqual(typ, it.typ) return it } func (p *parser) assertEqual(expected, got itemType) { if expected != got { p.bug("Expected '%s' but got '%s'.", expected, got) } } func (p *parser) topLevel(item item) { switch item.typ { case itemCommentStart: // # .. p.expect(itemText) case itemTableStart: // [ .. ] name := p.nextPos() var key Key for ; name.typ != itemTableEnd && name.typ != itemEOF; name = p.next() { key = append(key, p.keyString(name)) } p.assertEqual(itemTableEnd, name.typ) p.addContext(key, false) p.setType("", tomlHash, item.pos) p.ordered = append(p.ordered, key) case itemArrayTableStart: // [[ .. ]] name := p.nextPos() var key Key for ; name.typ != itemArrayTableEnd && name.typ != itemEOF; name = p.next() { key = append(key, p.keyString(name)) } p.assertEqual(itemArrayTableEnd, name.typ) p.addContext(key, true) p.setType("", tomlArrayHash, item.pos) p.ordered = append(p.ordered, key) case itemKeyStart: // key = .. outerContext := p.context /// Read all the key parts (e.g. 'a' and 'b' in 'a.b') k := p.nextPos() var key Key for ; k.typ != itemKeyEnd && k.typ != itemEOF; k = p.next() { key = append(key, p.keyString(k)) } p.assertEqual(itemKeyEnd, k.typ) /// The current key is the last part. p.currentKey = key.last() /// All the other parts (if any) are the context; need to set each part /// as implicit. context := key.parent() for i := range context { p.addImplicitContext(append(p.context, context[i:i+1]...)) } p.ordered = append(p.ordered, p.context.add(p.currentKey)) /// Set value. vItem := p.next() val, typ := p.value(vItem, false) p.setValue(p.currentKey, val) p.setType(p.currentKey, typ, vItem.pos) /// Remove the context we added (preserving any context from [tbl] lines). p.context = outerContext p.currentKey = "" default: p.bug("Unexpected type at top level: %s", item.typ) } } // Gets a string for a key (or part of a key in a table name). func (p *parser) keyString(it item) string { switch it.typ { case itemText: return it.val case itemString, itemStringEsc, itemMultilineString, itemRawString, itemRawMultilineString: s, _ := p.value(it, false) return s.(string) default: p.bug("Unexpected key type: %s", it.typ) } panic("unreachable") } var datetimeRepl = strings.NewReplacer( "z", "Z", "t", "T", " ", "T") // value translates an expected value from the lexer into a Go value wrapped // as an empty interface. func (p *parser) value(it item, parentIsArray bool) (any, tomlType) { switch it.typ { case itemString: return it.val, p.typeOfPrimitive(it) case itemStringEsc: return p.replaceEscapes(it, it.val), p.typeOfPrimitive(it) case itemMultilineString: return p.replaceEscapes(it, p.stripEscapedNewlines(stripFirstNewline(it.val))), p.typeOfPrimitive(it) case itemRawString: return it.val, p.typeOfPrimitive(it) case itemRawMultilineString: return stripFirstNewline(it.val), p.typeOfPrimitive(it) case itemInteger: return p.valueInteger(it) case itemFloat: return p.valueFloat(it) case itemBool: switch it.val { case "true": return true, p.typeOfPrimitive(it) case "false": return false, p.typeOfPrimitive(it) default: p.bug("Expected boolean value, but got '%s'.", it.val) } case itemDatetime: return p.valueDatetime(it) case itemArray: return p.valueArray(it) case itemInlineTableStart: return p.valueInlineTable(it, parentIsArray) default: p.bug("Unexpected value type: %s", it.typ) } panic("unreachable") } func (p *parser) valueInteger(it item) (any, tomlType) { if !numUnderscoresOK(it.val) { p.panicItemf(it, "Invalid integer %q: underscores must be surrounded by digits", it.val) } if numHasLeadingZero(it.val) { p.panicItemf(it, "Invalid integer %q: cannot have leading zeroes", it.val) } num, err := strconv.ParseInt(it.val, 0, 64) if err != nil { // Distinguish integer values. Normally, it'd be a bug if the lexer // provides an invalid integer, but it's possible that the number is // out of range of valid values (which the lexer cannot determine). // So mark the former as a bug but the latter as a legitimate user // error. if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange { p.panicErr(it, errParseRange{i: it.val, size: "int64"}) } else { p.bug("Expected integer value, but got '%s'.", it.val) } } return num, p.typeOfPrimitive(it) } func (p *parser) valueFloat(it item) (any, tomlType) { parts := strings.FieldsFunc(it.val, func(r rune) bool { switch r { case '.', 'e', 'E': return true } return false }) for _, part := range parts { if !numUnderscoresOK(part) { p.panicItemf(it, "Invalid float %q: underscores must be surrounded by digits", it.val) } } if len(parts) > 0 && numHasLeadingZero(parts[0]) { p.panicItemf(it, "Invalid float %q: cannot have leading zeroes", it.val) } if !numPeriodsOK(it.val) { // As a special case, numbers like '123.' or '1.e2', // which are valid as far as Go/strconv are concerned, // must be rejected because TOML says that a fractional // part consists of '.' followed by 1+ digits. p.panicItemf(it, "Invalid float %q: '.' must be followed by one or more digits", it.val) } val := strings.Replace(it.val, "_", "", -1) signbit := false if val == "+nan" || val == "-nan" { signbit = val == "-nan" val = "nan" } num, err := strconv.ParseFloat(val, 64) if err != nil { if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange { p.panicErr(it, errParseRange{i: it.val, size: "float64"}) } else { p.panicItemf(it, "Invalid float value: %q", it.val) } } if signbit { num = math.Copysign(num, -1) } return num, p.typeOfPrimitive(it) } var dtTypes = []struct { fmt string zone *time.Location }{ {time.RFC3339Nano, time.Local}, {"2006-01-02T15:04:05.999999999", internal.LocalDatetime}, {"2006-01-02", internal.LocalDate}, {"15:04:05.999999999", internal.LocalTime}, {"2006-01-02T15:04Z07:00", time.Local}, {"2006-01-02T15:04", internal.LocalDatetime}, {"15:04", internal.LocalTime}, } func (p *parser) valueDatetime(it item) (any, tomlType) { it.val = datetimeRepl.Replace(it.val) var ( t time.Time ok bool err error ) for _, dt := range dtTypes { t, err = time.ParseInLocation(dt.fmt, it.val, dt.zone) if err == nil { if missingLeadingZero(it.val, dt.fmt) { p.panicErr(it, errParseDate{it.val}) } ok = true break } } if !ok { p.panicErr(it, errParseDate{it.val}) } return t, p.typeOfPrimitive(it) } // Go's time.Parse() will accept numbers without a leading zero; there isn't any // way to require it. https://github.com/golang/go/issues/29911 // // Depend on the fact that the separators (- and :) should always be at the same // location. func missingLeadingZero(d, l string) bool { for i, c := range []byte(l) { if c == '.' || c == 'Z' { return false } if (c < '0' || c > '9') && d[i] != c { return true } } return false } func (p *parser) valueArray(it item) (any, tomlType) { p.setType(p.currentKey, tomlArray, it.pos) var ( // Initialize to a non-nil slice to make it consistent with how S = [] // decodes into a non-nil slice inside something like struct { S // []string }. See #338 array = make([]any, 0, 2) ) for it = p.next(); it.typ != itemArrayEnd; it = p.next() { if it.typ == itemCommentStart { p.expect(itemText) continue } val, typ := p.value(it, true) array = append(array, val) // XXX: type isn't used here, we need it to record the accurate type // information. // // Not entirely sure how to best store this; could use "key[0]", // "key[1]" notation, or maybe store it on the Array type? _ = typ } return array, tomlArray } func (p *parser) valueInlineTable(it item, parentIsArray bool) (any, tomlType) { var ( topHash = make(map[string]any) outerContext = p.context outerKey = p.currentKey ) p.context = append(p.context, p.currentKey) prevContext := p.context p.currentKey = "" p.addImplicit(p.context) p.addContext(p.context, parentIsArray) /// Loop over all table key/value pairs. for it := p.next(); it.typ != itemInlineTableEnd; it = p.next() { if it.typ == itemCommentStart { p.expect(itemText) continue } /// Read all key parts. k := p.nextPos() var key Key for ; k.typ != itemKeyEnd && k.typ != itemEOF; k = p.next() { key = append(key, p.keyString(k)) } p.assertEqual(itemKeyEnd, k.typ) /// The current key is the last part. p.currentKey = key.last() /// All the other parts (if any) are the context; need to set each part /// as implicit. context := key.parent() for i := range context { p.addImplicitContext(append(p.context, context[i:i+1]...)) } p.ordered = append(p.ordered, p.context.add(p.currentKey)) /// Set the value. val, typ := p.value(p.next(), false) p.setValue(p.currentKey, val) p.setType(p.currentKey, typ, it.pos) hash := topHash for _, c := range context { h, ok := hash[c] if !ok { h = make(map[string]any) hash[c] = h } hash, ok = h.(map[string]any) if !ok { p.panicf("%q is not a table", p.context) } } hash[p.currentKey] = val /// Restore context. p.context = prevContext } p.context = outerContext p.currentKey = outerKey return topHash, tomlHash } // numHasLeadingZero checks if this number has leading zeroes, allowing for '0', // +/- signs, and base prefixes. func numHasLeadingZero(s string) bool { if len(s) > 1 && s[0] == '0' && !(s[1] == 'b' || s[1] == 'o' || s[1] == 'x') { // Allow 0b, 0o, 0x return true } if len(s) > 2 && (s[0] == '-' || s[0] == '+') && s[1] == '0' { return true } return false } // numUnderscoresOK checks whether each underscore in s is surrounded by // characters that are not underscores. func numUnderscoresOK(s string) bool { switch s { case "nan", "+nan", "-nan", "inf", "-inf", "+inf": return true } accept := false for _, r := range s { if r == '_' { if !accept { return false } } // isHex is a superset of all the permissible characters surrounding an // underscore. accept = isHex(r) } return accept } // numPeriodsOK checks whether every period in s is followed by a digit. func numPeriodsOK(s string) bool { period := false for _, r := range s { if period && !isDigit(r) { return false } period = r == '.' } return !period } // Set the current context of the parser, where the context is either a hash or // an array of hashes, depending on the value of the `array` parameter. // // Establishing the context also makes sure that the key isn't a duplicate, and // will create implicit hashes automatically. func (p *parser) addContext(key Key, array bool) { /// Always start at the top level and drill down for our context. hashContext := p.mapping keyContext := make(Key, 0, len(key)-1) /// We only need implicit hashes for the parents. for _, k := range key.parent() { _, ok := hashContext[k] keyContext = append(keyContext, k) // No key? Make an implicit hash and move on. if !ok { p.addImplicit(keyContext) hashContext[k] = make(map[string]any) } // If the hash context is actually an array of tables, then set // the hash context to the last element in that array. // // Otherwise, it better be a table, since this MUST be a key group (by // virtue of it not being the last element in a key). switch t := hashContext[k].(type) { case []map[string]any: hashContext = t[len(t)-1] case map[string]any: hashContext = t default: p.panicf("Key '%s' was already created as a hash.", keyContext) } } p.context = keyContext if array { // If this is the first element for this array, then allocate a new // list of tables for it. k := key.last() if _, ok := hashContext[k]; !ok { hashContext[k] = make([]map[string]any, 0, 4) } // Add a new table. But make sure the key hasn't already been used // for something else. if hash, ok := hashContext[k].([]map[string]any); ok { hashContext[k] = append(hash, make(map[string]any)) } else { p.panicf("Key '%s' was already created and cannot be used as an array.", key) } } else { p.setValue(key.last(), make(map[string]any)) } p.context = append(p.context, key.last()) } // setValue sets the given key to the given value in the current context. // It will make sure that the key hasn't already been defined, account for // implicit key groups. func (p *parser) setValue(key string, value any) { var ( tmpHash any ok bool hash = p.mapping keyContext = make(Key, 0, len(p.context)+1) ) for _, k := range p.context { keyContext = append(keyContext, k) if tmpHash, ok = hash[k]; !ok { p.bug("Context for key '%s' has not been established.", keyContext) } switch t := tmpHash.(type) { case []map[string]any: // The context is a table of hashes. Pick the most recent table // defined as the current hash. hash = t[len(t)-1] case map[string]any: hash = t default: p.panicf("Key '%s' has already been defined.", keyContext) } } keyContext = append(keyContext, key) if _, ok := hash[key]; ok { // Normally redefining keys isn't allowed, but the key could have been // defined implicitly and it's allowed to be redefined concretely. (See // the `valid/implicit-and-explicit-after.toml` in toml-test) // // But we have to make sure to stop marking it as an implicit. (So that // another redefinition provokes an error.) // // Note that since it has already been defined (as a hash), we don't // want to overwrite it. So our business is done. if p.isArray(keyContext) { if !p.isImplicit(keyContext) { if _, ok := hash[key]; ok { p.panicf("Key '%s' has already been defined.", keyContext) } } p.removeImplicit(keyContext) hash[key] = value return } if p.isImplicit(keyContext) { p.removeImplicit(keyContext) return } // Otherwise, we have a concrete key trying to override a previous key, // which is *always* wrong. p.panicf("Key '%s' has already been defined.", keyContext) } hash[key] = value } // setType sets the type of a particular value at a given key. It should be // called immediately AFTER setValue. // // Note that if `key` is empty, then the type given will be applied to the // current context (which is either a table or an array of tables). func (p *parser) setType(key string, typ tomlType, pos Position) { keyContext := make(Key, 0, len(p.context)+1) keyContext = append(keyContext, p.context...) if len(key) > 0 { // allow type setting for hashes keyContext = append(keyContext, key) } // Special case to make empty keys ("" = 1) work. // Without it it will set "" rather than `""`. // TODO: why is this needed? And why is this only needed here? if len(keyContext) == 0 { keyContext = Key{""} } p.keyInfo[keyContext.String()] = keyInfo{tomlType: typ, pos: pos} } // Implicit keys need to be created when tables are implied in "a.b.c.d = 1" and // "[a.b.c]" (the "a", "b", and "c" hashes are never created explicitly). func (p *parser) addImplicit(key Key) { p.implicits[key.String()] = struct{}{} } func (p *parser) removeImplicit(key Key) { delete(p.implicits, key.String()) } func (p *parser) isImplicit(key Key) bool { _, ok := p.implicits[key.String()]; return ok } func (p *parser) isArray(key Key) bool { return p.keyInfo[key.String()].tomlType == tomlArray } func (p *parser) addImplicitContext(key Key) { p.addImplicit(key); p.addContext(key, false) } // current returns the full key name of the current context. func (p *parser) current() string { if len(p.currentKey) == 0 { return p.context.String() } if len(p.context) == 0 { return p.currentKey } return fmt.Sprintf("%s.%s", p.context, p.currentKey) } func stripFirstNewline(s string) string { if len(s) > 0 && s[0] == '\n' { return s[1:] } if len(s) > 1 && s[0] == '\r' && s[1] == '\n' { return s[2:] } return s } // stripEscapedNewlines removes whitespace after line-ending backslashes in // multiline strings. // // A line-ending backslash is an unescaped \ followed only by whitespace until // the next newline. After a line-ending backslash, all whitespace is removed // until the next non-whitespace character. func (p *parser) stripEscapedNewlines(s string) string { var ( b strings.Builder i int ) b.Grow(len(s)) for { ix := strings.Index(s[i:], `\`) if ix < 0 { b.WriteString(s) return b.String() } i += ix if len(s) > i+1 && s[i+1] == '\\' { // Escaped backslash. i += 2 continue } // Scan until the next non-whitespace. j := i + 1 whitespaceLoop: for ; j < len(s); j++ { switch s[j] { case ' ', '\t', '\r', '\n': default: break whitespaceLoop } } if j == i+1 { // Not a whitespace escape. i++ continue } if !strings.Contains(s[i:j], "\n") { // This is not a line-ending backslash. (It's a bad escape sequence, // but we can let replaceEscapes catch it.) i++ continue } b.WriteString(s[:i]) s = s[j:] i = 0 } } func (p *parser) replaceEscapes(it item, str string) string { var ( b strings.Builder skip = 0 ) b.Grow(len(str)) for i, c := range str { if skip > 0 { skip-- continue } if c != '\\' { b.WriteRune(c) continue } if i >= len(str) { p.bug("Escape sequence at end of string.") return "" } switch str[i+1] { default: p.bug("Expected valid escape code after \\, but got %q.", str[i+1]) case ' ', '\t': p.panicItemf(it, "invalid escape: '\\%c'", str[i+1]) case 'b': b.WriteByte(0x08) skip = 1 case 't': b.WriteByte(0x09) skip = 1 case 'n': b.WriteByte(0x0a) skip = 1 case 'f': b.WriteByte(0x0c) skip = 1 case 'r': b.WriteByte(0x0d) skip = 1 case 'e': b.WriteByte(0x1b) skip = 1 case '"': b.WriteByte(0x22) skip = 1 case '\\': b.WriteByte(0x5c) skip = 1 // The lexer guarantees the correct number of characters are present; // don't need to check here. case 'x': escaped := p.asciiEscapeToUnicode(it, str[i+2:i+4]) b.WriteRune(escaped) skip = 3 case 'u': escaped := p.asciiEscapeToUnicode(it, str[i+2:i+6]) b.WriteRune(escaped) skip = 5 case 'U': escaped := p.asciiEscapeToUnicode(it, str[i+2:i+10]) b.WriteRune(escaped) skip = 9 } } return b.String() } func (p *parser) asciiEscapeToUnicode(it item, s string) rune { hex, err := strconv.ParseUint(strings.ToLower(s), 16, 32) if err != nil { p.bug("Could not parse '%s' as a hexadecimal number, but the lexer claims it's OK: %s", s, err) } if !utf8.ValidRune(rune(hex)) { p.panicItemf(it, "Escaped character '\\u%s' is not valid UTF-8.", s) } return rune(hex) } ================================================ FILE: vendor/github.com/BurntSushi/toml/type_fields.go ================================================ package toml // Struct field handling is adapted from code in encoding/json: // // Copyright 2010 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the Go distribution. import ( "reflect" "sort" "sync" ) // A field represents a single field found in a struct. type field struct { name string // the name of the field (`toml` tag included) tag bool // whether field has a `toml` tag index []int // represents the depth of an anonymous field typ reflect.Type // the type of the field } // byName sorts field by name, breaking ties with depth, // then breaking ties with "name came from toml tag", then // breaking ties with index sequence. type byName []field func (x byName) Len() int { return len(x) } func (x byName) Swap(i, j int) { x[i], x[j] = x[j], x[i] } func (x byName) Less(i, j int) bool { if x[i].name != x[j].name { return x[i].name < x[j].name } if len(x[i].index) != len(x[j].index) { return len(x[i].index) < len(x[j].index) } if x[i].tag != x[j].tag { return x[i].tag } return byIndex(x).Less(i, j) } // byIndex sorts field by index sequence. type byIndex []field func (x byIndex) Len() int { return len(x) } func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] } func (x byIndex) Less(i, j int) bool { for k, xik := range x[i].index { if k >= len(x[j].index) { return false } if xik != x[j].index[k] { return xik < x[j].index[k] } } return len(x[i].index) < len(x[j].index) } // typeFields returns a list of fields that TOML should recognize for the given // type. The algorithm is breadth-first search over the set of structs to // include - the top struct and then any reachable anonymous structs. func typeFields(t reflect.Type) []field { // Anonymous fields to explore at the current level and the next. current := []field{} next := []field{{typ: t}} // Count of queued names for current level and the next. var count map[reflect.Type]int var nextCount map[reflect.Type]int // Types already visited at an earlier level. visited := map[reflect.Type]bool{} // Fields found. var fields []field for len(next) > 0 { current, next = next, current[:0] count, nextCount = nextCount, map[reflect.Type]int{} for _, f := range current { if visited[f.typ] { continue } visited[f.typ] = true // Scan f.typ for fields to include. for i := 0; i < f.typ.NumField(); i++ { sf := f.typ.Field(i) if sf.PkgPath != "" && !sf.Anonymous { // unexported continue } opts := getOptions(sf.Tag) if opts.skip { continue } index := make([]int, len(f.index)+1) copy(index, f.index) index[len(f.index)] = i ft := sf.Type if ft.Name() == "" && ft.Kind() == reflect.Ptr { // Follow pointer. ft = ft.Elem() } // Record found field and index sequence. if opts.name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct { tagged := opts.name != "" name := opts.name if name == "" { name = sf.Name } fields = append(fields, field{name, tagged, index, ft}) if count[f.typ] > 1 { // If there were multiple instances, add a second, // so that the annihilation code will see a duplicate. // It only cares about the distinction between 1 or 2, // so don't bother generating any more copies. fields = append(fields, fields[len(fields)-1]) } continue } // Record new anonymous struct to explore in next round. nextCount[ft]++ if nextCount[ft] == 1 { f := field{name: ft.Name(), index: index, typ: ft} next = append(next, f) } } } } sort.Sort(byName(fields)) // Delete all fields that are hidden by the Go rules for embedded fields, // except that fields with TOML tags are promoted. // The fields are sorted in primary order of name, secondary order // of field index length. Loop over names; for each name, delete // hidden fields by choosing the one dominant field that survives. out := fields[:0] for advance, i := 0, 0; i < len(fields); i += advance { // One iteration per name. // Find the sequence of fields with the name of this first field. fi := fields[i] name := fi.name for advance = 1; i+advance < len(fields); advance++ { fj := fields[i+advance] if fj.name != name { break } } if advance == 1 { // Only one field with this name out = append(out, fi) continue } dominant, ok := dominantField(fields[i : i+advance]) if ok { out = append(out, dominant) } } fields = out sort.Sort(byIndex(fields)) return fields } // dominantField looks through the fields, all of which are known to // have the same name, to find the single field that dominates the // others using Go's embedding rules, modified by the presence of // TOML tags. If there are multiple top-level fields, the boolean // will be false: This condition is an error in Go and we skip all // the fields. func dominantField(fields []field) (field, bool) { // The fields are sorted in increasing index-length order. The winner // must therefore be one with the shortest index length. Drop all // longer entries, which is easy: just truncate the slice. length := len(fields[0].index) tagged := -1 // Index of first tagged field. for i, f := range fields { if len(f.index) > length { fields = fields[:i] break } if f.tag { if tagged >= 0 { // Multiple tagged fields at the same level: conflict. // Return no field. return field{}, false } tagged = i } } if tagged >= 0 { return fields[tagged], true } // All remaining fields have the same length. If there's more than one, // we have a conflict (two fields named "X" at the same level) and we // return no field. if len(fields) > 1 { return field{}, false } return fields[0], true } var fieldCache struct { sync.RWMutex m map[reflect.Type][]field } // cachedTypeFields is like typeFields but uses a cache to avoid repeated work. func cachedTypeFields(t reflect.Type) []field { fieldCache.RLock() f := fieldCache.m[t] fieldCache.RUnlock() if f != nil { return f } // Compute fields without lock. // Might duplicate effort but won't hold other computations back. f = typeFields(t) if f == nil { f = []field{} } fieldCache.Lock() if fieldCache.m == nil { fieldCache.m = map[reflect.Type][]field{} } fieldCache.m[t] = f fieldCache.Unlock() return f } ================================================ FILE: vendor/github.com/BurntSushi/toml/type_toml.go ================================================ package toml // tomlType represents any Go type that corresponds to a TOML type. // While the first draft of the TOML spec has a simplistic type system that // probably doesn't need this level of sophistication, we seem to be militating // toward adding real composite types. type tomlType interface { typeString() string } // typeEqual accepts any two types and returns true if they are equal. func typeEqual(t1, t2 tomlType) bool { if t1 == nil || t2 == nil { return false } return t1.typeString() == t2.typeString() } func typeIsTable(t tomlType) bool { return typeEqual(t, tomlHash) || typeEqual(t, tomlArrayHash) } type tomlBaseType string func (btype tomlBaseType) typeString() string { return string(btype) } func (btype tomlBaseType) String() string { return btype.typeString() } var ( tomlInteger tomlBaseType = "Integer" tomlFloat tomlBaseType = "Float" tomlDatetime tomlBaseType = "Datetime" tomlString tomlBaseType = "String" tomlBool tomlBaseType = "Bool" tomlArray tomlBaseType = "Array" tomlHash tomlBaseType = "Hash" tomlArrayHash tomlBaseType = "ArrayHash" ) // typeOfPrimitive returns a tomlType of any primitive value in TOML. // Primitive values are: Integer, Float, Datetime, String and Bool. // // Passing a lexer item other than the following will cause a BUG message // to occur: itemString, itemBool, itemInteger, itemFloat, itemDatetime. func (p *parser) typeOfPrimitive(lexItem item) tomlType { switch lexItem.typ { case itemInteger: return tomlInteger case itemFloat: return tomlFloat case itemDatetime: return tomlDatetime case itemString, itemStringEsc: return tomlString case itemMultilineString: return tomlString case itemRawString: return tomlString case itemRawMultilineString: return tomlString case itemBool: return tomlBool } p.bug("Cannot infer primitive type of lex item '%s'.", lexItem) panic("unreachable") } ================================================ FILE: vendor/github.com/Microsoft/go-winio/.gitattributes ================================================ * text=auto eol=lf ================================================ FILE: vendor/github.com/Microsoft/go-winio/.gitignore ================================================ .vscode/ *.exe # testing testdata # go workspaces go.work go.work.sum ================================================ FILE: vendor/github.com/Microsoft/go-winio/.golangci.yml ================================================ linters: enable: # style - containedctx # struct contains a context - dupl # duplicate code - errname # erorrs are named correctly - nolintlint # "//nolint" directives are properly explained - revive # golint replacement - unconvert # unnecessary conversions - wastedassign # bugs, performance, unused, etc ... - contextcheck # function uses a non-inherited context - errorlint # errors not wrapped for 1.13 - exhaustive # check exhaustiveness of enum switch statements - gofmt # files are gofmt'ed - gosec # security - nilerr # returns nil even with non-nil error - thelper # test helpers without t.Helper() - unparam # unused function params issues: exclude-dirs: - pkg/etw/sample exclude-rules: # err is very often shadowed in nested scopes - linters: - govet text: '^shadow: declaration of "err" shadows declaration' # ignore long lines for skip autogen directives - linters: - revive text: "^line-length-limit: " source: "^//(go:generate|sys) " #TODO: remove after upgrading to go1.18 # ignore comment spacing for nolint and sys directives - linters: - revive text: "^comment-spacings: no space between comment delimiter and comment text" source: "//(cspell:|nolint:|sys |todo)" # not on go 1.18 yet, so no any - linters: - revive text: "^use-any: since GO 1.18 'interface{}' can be replaced by 'any'" # allow unjustified ignores of error checks in defer statements - linters: - nolintlint text: "^directive `//nolint:errcheck` should provide explanation" source: '^\s*defer ' # allow unjustified ignores of error lints for io.EOF - linters: - nolintlint text: "^directive `//nolint:errorlint` should provide explanation" source: '[=|!]= io.EOF' linters-settings: exhaustive: default-signifies-exhaustive: true govet: enable-all: true disable: # struct order is often for Win32 compat # also, ignore pointer bytes/GC issues for now until performance becomes an issue - fieldalignment nolintlint: require-explanation: true require-specific: true revive: # revive is more configurable than static check, so likely the preferred alternative to static-check # (once the perf issue is solved: https://github.com/golangci/golangci-lint/issues/2997) enable-all-rules: true # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md rules: # rules with required arguments - name: argument-limit disabled: true - name: banned-characters disabled: true - name: cognitive-complexity disabled: true - name: cyclomatic disabled: true - name: file-header disabled: true - name: function-length disabled: true - name: function-result-limit disabled: true - name: max-public-structs disabled: true # geneally annoying rules - name: add-constant # complains about any and all strings and integers disabled: true - name: confusing-naming # we frequently use "Foo()" and "foo()" together disabled: true - name: flag-parameter # excessive, and a common idiom we use disabled: true - name: unhandled-error # warns over common fmt.Print* and io.Close; rely on errcheck instead disabled: true # general config - name: line-length-limit arguments: - 140 - name: var-naming arguments: - [] - - CID - CRI - CTRD - DACL - DLL - DOS - ETW - FSCTL - GCS - GMSA - HCS - HV - IO - LCOW - LDAP - LPAC - LTSC - MMIO - NT - OCI - PMEM - PWSH - RX - SACl - SID - SMB - TX - VHD - VHDX - VMID - VPCI - WCOW - WIM ================================================ FILE: vendor/github.com/Microsoft/go-winio/CODEOWNERS ================================================ * @microsoft/containerplat ================================================ FILE: vendor/github.com/Microsoft/go-winio/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 Microsoft Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: vendor/github.com/Microsoft/go-winio/README.md ================================================ # go-winio [![Build Status](https://github.com/microsoft/go-winio/actions/workflows/ci.yml/badge.svg)](https://github.com/microsoft/go-winio/actions/workflows/ci.yml) This repository contains utilities for efficiently performing Win32 IO operations in Go. Currently, this is focused on accessing named pipes and other file handles, and for using named pipes as a net transport. This code relies on IO completion ports to avoid blocking IO on system threads, allowing Go to reuse the thread to schedule another goroutine. This limits support to Windows Vista and newer operating systems. This is similar to the implementation of network sockets in Go's net package. Please see the LICENSE file for licensing information. ## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit [Microsoft CLA](https://cla.microsoft.com). When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. Additionally, the pull request pipeline requires the following steps to be performed before mergining. ### Code Sign-Off We require that contributors sign their commits using [`git commit --signoff`][git-commit-s] to certify they either authored the work themselves or otherwise have permission to use it in this project. A range of commits can be signed off using [`git rebase --signoff`][git-rebase-s]. Please see [the developer certificate](https://developercertificate.org) for more info, as well as to make sure that you can attest to the rules listed. Our CI uses the DCO Github app to ensure that all commits in a given PR are signed-off. ### Linting Code must pass a linting stage, which uses [`golangci-lint`][lint]. The linting settings are stored in [`.golangci.yaml`](./.golangci.yaml), and can be run automatically with VSCode by adding the following to your workspace or folder settings: ```json "go.lintTool": "golangci-lint", "go.lintOnSave": "package", ``` Additional editor [integrations options are also available][lint-ide]. Alternatively, `golangci-lint` can be [installed locally][lint-install] and run from the repo root: ```shell # use . or specify a path to only lint a package # to show all lint errors, use flags "--max-issues-per-linter=0 --max-same-issues=0" > golangci-lint run ./... ``` ### Go Generate The pipeline checks that auto-generated code, via `go generate`, are up to date. This can be done for the entire repo: ```shell > go generate ./... ``` ## Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. ## Special Thanks Thanks to [natefinch][natefinch] for the inspiration for this library. See [npipe](https://github.com/natefinch/npipe) for another named pipe implementation. [lint]: https://golangci-lint.run/ [lint-ide]: https://golangci-lint.run/usage/integrations/#editor-integration [lint-install]: https://golangci-lint.run/usage/install/#local-installation [git-commit-s]: https://git-scm.com/docs/git-commit#Documentation/git-commit.txt--s [git-rebase-s]: https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---signoff [natefinch]: https://github.com/natefinch ================================================ FILE: vendor/github.com/Microsoft/go-winio/SECURITY.md ================================================ ## Security Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. ## Reporting Security Issues **Please do not report security vulnerabilities through public GitHub issues.** Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. ## Preferred Languages We prefer all communications to be in English. ## Policy Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). ================================================ FILE: vendor/github.com/Microsoft/go-winio/backup.go ================================================ //go:build windows // +build windows package winio import ( "encoding/binary" "errors" "fmt" "io" "os" "runtime" "unicode/utf16" "github.com/Microsoft/go-winio/internal/fs" "golang.org/x/sys/windows" ) //sys backupRead(h windows.Handle, b []byte, bytesRead *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupRead //sys backupWrite(h windows.Handle, b []byte, bytesWritten *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupWrite const ( BackupData = uint32(iota + 1) BackupEaData BackupSecurity BackupAlternateData BackupLink BackupPropertyData BackupObjectId //revive:disable-line:var-naming ID, not Id BackupReparseData BackupSparseBlock BackupTxfsData ) const ( StreamSparseAttributes = uint32(8) ) //nolint:revive // var-naming: ALL_CAPS const ( WRITE_DAC = windows.WRITE_DAC WRITE_OWNER = windows.WRITE_OWNER ACCESS_SYSTEM_SECURITY = windows.ACCESS_SYSTEM_SECURITY ) // BackupHeader represents a backup stream of a file. type BackupHeader struct { //revive:disable-next-line:var-naming ID, not Id Id uint32 // The backup stream ID Attributes uint32 // Stream attributes Size int64 // The size of the stream in bytes Name string // The name of the stream (for BackupAlternateData only). Offset int64 // The offset of the stream in the file (for BackupSparseBlock only). } type win32StreamID struct { StreamID uint32 Attributes uint32 Size uint64 NameSize uint32 } // BackupStreamReader reads from a stream produced by the BackupRead Win32 API and produces a series // of BackupHeader values. type BackupStreamReader struct { r io.Reader bytesLeft int64 } // NewBackupStreamReader produces a BackupStreamReader from any io.Reader. func NewBackupStreamReader(r io.Reader) *BackupStreamReader { return &BackupStreamReader{r, 0} } // Next returns the next backup stream and prepares for calls to Read(). It skips the remainder of the current stream if // it was not completely read. func (r *BackupStreamReader) Next() (*BackupHeader, error) { if r.bytesLeft > 0 { //nolint:nestif // todo: flatten this if s, ok := r.r.(io.Seeker); ok { // Make sure Seek on io.SeekCurrent sometimes succeeds // before trying the actual seek. if _, err := s.Seek(0, io.SeekCurrent); err == nil { if _, err = s.Seek(r.bytesLeft, io.SeekCurrent); err != nil { return nil, err } r.bytesLeft = 0 } } if _, err := io.Copy(io.Discard, r); err != nil { return nil, err } } var wsi win32StreamID if err := binary.Read(r.r, binary.LittleEndian, &wsi); err != nil { return nil, err } hdr := &BackupHeader{ Id: wsi.StreamID, Attributes: wsi.Attributes, Size: int64(wsi.Size), } if wsi.NameSize != 0 { name := make([]uint16, int(wsi.NameSize/2)) if err := binary.Read(r.r, binary.LittleEndian, name); err != nil { return nil, err } hdr.Name = windows.UTF16ToString(name) } if wsi.StreamID == BackupSparseBlock { if err := binary.Read(r.r, binary.LittleEndian, &hdr.Offset); err != nil { return nil, err } hdr.Size -= 8 } r.bytesLeft = hdr.Size return hdr, nil } // Read reads from the current backup stream. func (r *BackupStreamReader) Read(b []byte) (int, error) { if r.bytesLeft == 0 { return 0, io.EOF } if int64(len(b)) > r.bytesLeft { b = b[:r.bytesLeft] } n, err := r.r.Read(b) r.bytesLeft -= int64(n) if err == io.EOF { err = io.ErrUnexpectedEOF } else if r.bytesLeft == 0 && err == nil { err = io.EOF } return n, err } // BackupStreamWriter writes a stream compatible with the BackupWrite Win32 API. type BackupStreamWriter struct { w io.Writer bytesLeft int64 } // NewBackupStreamWriter produces a BackupStreamWriter on top of an io.Writer. func NewBackupStreamWriter(w io.Writer) *BackupStreamWriter { return &BackupStreamWriter{w, 0} } // WriteHeader writes the next backup stream header and prepares for calls to Write(). func (w *BackupStreamWriter) WriteHeader(hdr *BackupHeader) error { if w.bytesLeft != 0 { return fmt.Errorf("missing %d bytes", w.bytesLeft) } name := utf16.Encode([]rune(hdr.Name)) wsi := win32StreamID{ StreamID: hdr.Id, Attributes: hdr.Attributes, Size: uint64(hdr.Size), NameSize: uint32(len(name) * 2), } if hdr.Id == BackupSparseBlock { // Include space for the int64 block offset wsi.Size += 8 } if err := binary.Write(w.w, binary.LittleEndian, &wsi); err != nil { return err } if len(name) != 0 { if err := binary.Write(w.w, binary.LittleEndian, name); err != nil { return err } } if hdr.Id == BackupSparseBlock { if err := binary.Write(w.w, binary.LittleEndian, hdr.Offset); err != nil { return err } } w.bytesLeft = hdr.Size return nil } // Write writes to the current backup stream. func (w *BackupStreamWriter) Write(b []byte) (int, error) { if w.bytesLeft < int64(len(b)) { return 0, fmt.Errorf("too many bytes by %d", int64(len(b))-w.bytesLeft) } n, err := w.w.Write(b) w.bytesLeft -= int64(n) return n, err } // BackupFileReader provides an io.ReadCloser interface on top of the BackupRead Win32 API. type BackupFileReader struct { f *os.File includeSecurity bool ctx uintptr } // NewBackupFileReader returns a new BackupFileReader from a file handle. If includeSecurity is true, // Read will attempt to read the security descriptor of the file. func NewBackupFileReader(f *os.File, includeSecurity bool) *BackupFileReader { r := &BackupFileReader{f, includeSecurity, 0} return r } // Read reads a backup stream from the file by calling the Win32 API BackupRead(). func (r *BackupFileReader) Read(b []byte) (int, error) { var bytesRead uint32 err := backupRead(windows.Handle(r.f.Fd()), b, &bytesRead, false, r.includeSecurity, &r.ctx) if err != nil { return 0, &os.PathError{Op: "BackupRead", Path: r.f.Name(), Err: err} } runtime.KeepAlive(r.f) if bytesRead == 0 { return 0, io.EOF } return int(bytesRead), nil } // Close frees Win32 resources associated with the BackupFileReader. It does not close // the underlying file. func (r *BackupFileReader) Close() error { if r.ctx != 0 { _ = backupRead(windows.Handle(r.f.Fd()), nil, nil, true, false, &r.ctx) runtime.KeepAlive(r.f) r.ctx = 0 } return nil } // BackupFileWriter provides an io.WriteCloser interface on top of the BackupWrite Win32 API. type BackupFileWriter struct { f *os.File includeSecurity bool ctx uintptr } // NewBackupFileWriter returns a new BackupFileWriter from a file handle. If includeSecurity is true, // Write() will attempt to restore the security descriptor from the stream. func NewBackupFileWriter(f *os.File, includeSecurity bool) *BackupFileWriter { w := &BackupFileWriter{f, includeSecurity, 0} return w } // Write restores a portion of the file using the provided backup stream. func (w *BackupFileWriter) Write(b []byte) (int, error) { var bytesWritten uint32 err := backupWrite(windows.Handle(w.f.Fd()), b, &bytesWritten, false, w.includeSecurity, &w.ctx) if err != nil { return 0, &os.PathError{Op: "BackupWrite", Path: w.f.Name(), Err: err} } runtime.KeepAlive(w.f) if int(bytesWritten) != len(b) { return int(bytesWritten), errors.New("not all bytes could be written") } return len(b), nil } // Close frees Win32 resources associated with the BackupFileWriter. It does not // close the underlying file. func (w *BackupFileWriter) Close() error { if w.ctx != 0 { _ = backupWrite(windows.Handle(w.f.Fd()), nil, nil, true, false, &w.ctx) runtime.KeepAlive(w.f) w.ctx = 0 } return nil } // OpenForBackup opens a file or directory, potentially skipping access checks if the backup // or restore privileges have been acquired. // // If the file opened was a directory, it cannot be used with Readdir(). func OpenForBackup(path string, access uint32, share uint32, createmode uint32) (*os.File, error) { h, err := fs.CreateFile(path, fs.AccessMask(access), fs.FileShareMode(share), nil, fs.FileCreationDisposition(createmode), fs.FILE_FLAG_BACKUP_SEMANTICS|fs.FILE_FLAG_OPEN_REPARSE_POINT, 0, ) if err != nil { err = &os.PathError{Op: "open", Path: path, Err: err} return nil, err } return os.NewFile(uintptr(h), path), nil } ================================================ FILE: vendor/github.com/Microsoft/go-winio/doc.go ================================================ // This package provides utilities for efficiently performing Win32 IO operations in Go. // Currently, this package is provides support for genreal IO and management of // - named pipes // - files // - [Hyper-V sockets] // // This code is similar to Go's [net] package, and uses IO completion ports to avoid // blocking IO on system threads, allowing Go to reuse the thread to schedule other goroutines. // // This limits support to Windows Vista and newer operating systems. // // Additionally, this package provides support for: // - creating and managing GUIDs // - writing to [ETW] // - opening and manageing VHDs // - parsing [Windows Image files] // - auto-generating Win32 API code // // [Hyper-V sockets]: https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/make-integration-service // [ETW]: https://docs.microsoft.com/en-us/windows-hardware/drivers/devtest/event-tracing-for-windows--etw- // [Windows Image files]: https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/work-with-windows-images package winio ================================================ FILE: vendor/github.com/Microsoft/go-winio/ea.go ================================================ package winio import ( "bytes" "encoding/binary" "errors" ) type fileFullEaInformation struct { NextEntryOffset uint32 Flags uint8 NameLength uint8 ValueLength uint16 } var ( fileFullEaInformationSize = binary.Size(&fileFullEaInformation{}) errInvalidEaBuffer = errors.New("invalid extended attribute buffer") errEaNameTooLarge = errors.New("extended attribute name too large") errEaValueTooLarge = errors.New("extended attribute value too large") ) // ExtendedAttribute represents a single Windows EA. type ExtendedAttribute struct { Name string Value []byte Flags uint8 } func parseEa(b []byte) (ea ExtendedAttribute, nb []byte, err error) { var info fileFullEaInformation err = binary.Read(bytes.NewReader(b), binary.LittleEndian, &info) if err != nil { err = errInvalidEaBuffer return ea, nb, err } nameOffset := fileFullEaInformationSize nameLen := int(info.NameLength) valueOffset := nameOffset + int(info.NameLength) + 1 valueLen := int(info.ValueLength) nextOffset := int(info.NextEntryOffset) if valueLen+valueOffset > len(b) || nextOffset < 0 || nextOffset > len(b) { err = errInvalidEaBuffer return ea, nb, err } ea.Name = string(b[nameOffset : nameOffset+nameLen]) ea.Value = b[valueOffset : valueOffset+valueLen] ea.Flags = info.Flags if info.NextEntryOffset != 0 { nb = b[info.NextEntryOffset:] } return ea, nb, err } // DecodeExtendedAttributes decodes a list of EAs from a FILE_FULL_EA_INFORMATION // buffer retrieved from BackupRead, ZwQueryEaFile, etc. func DecodeExtendedAttributes(b []byte) (eas []ExtendedAttribute, err error) { for len(b) != 0 { ea, nb, err := parseEa(b) if err != nil { return nil, err } eas = append(eas, ea) b = nb } return eas, err } func writeEa(buf *bytes.Buffer, ea *ExtendedAttribute, last bool) error { if int(uint8(len(ea.Name))) != len(ea.Name) { return errEaNameTooLarge } if int(uint16(len(ea.Value))) != len(ea.Value) { return errEaValueTooLarge } entrySize := uint32(fileFullEaInformationSize + len(ea.Name) + 1 + len(ea.Value)) withPadding := (entrySize + 3) &^ 3 nextOffset := uint32(0) if !last { nextOffset = withPadding } info := fileFullEaInformation{ NextEntryOffset: nextOffset, Flags: ea.Flags, NameLength: uint8(len(ea.Name)), ValueLength: uint16(len(ea.Value)), } err := binary.Write(buf, binary.LittleEndian, &info) if err != nil { return err } _, err = buf.Write([]byte(ea.Name)) if err != nil { return err } err = buf.WriteByte(0) if err != nil { return err } _, err = buf.Write(ea.Value) if err != nil { return err } _, err = buf.Write([]byte{0, 0, 0}[0 : withPadding-entrySize]) if err != nil { return err } return nil } // EncodeExtendedAttributes encodes a list of EAs into a FILE_FULL_EA_INFORMATION // buffer for use with BackupWrite, ZwSetEaFile, etc. func EncodeExtendedAttributes(eas []ExtendedAttribute) ([]byte, error) { var buf bytes.Buffer for i := range eas { last := false if i == len(eas)-1 { last = true } err := writeEa(&buf, &eas[i], last) if err != nil { return nil, err } } return buf.Bytes(), nil } ================================================ FILE: vendor/github.com/Microsoft/go-winio/file.go ================================================ //go:build windows // +build windows package winio import ( "errors" "io" "runtime" "sync" "sync/atomic" "syscall" "time" "golang.org/x/sys/windows" ) //sys cancelIoEx(file windows.Handle, o *windows.Overlapped) (err error) = CancelIoEx //sys createIoCompletionPort(file windows.Handle, port windows.Handle, key uintptr, threadCount uint32) (newport windows.Handle, err error) = CreateIoCompletionPort //sys getQueuedCompletionStatus(port windows.Handle, bytes *uint32, key *uintptr, o **ioOperation, timeout uint32) (err error) = GetQueuedCompletionStatus //sys setFileCompletionNotificationModes(h windows.Handle, flags uint8) (err error) = SetFileCompletionNotificationModes //sys wsaGetOverlappedResult(h windows.Handle, o *windows.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) = ws2_32.WSAGetOverlappedResult var ( ErrFileClosed = errors.New("file has already been closed") ErrTimeout = &timeoutError{} ) type timeoutError struct{} func (*timeoutError) Error() string { return "i/o timeout" } func (*timeoutError) Timeout() bool { return true } func (*timeoutError) Temporary() bool { return true } type timeoutChan chan struct{} var ioInitOnce sync.Once var ioCompletionPort windows.Handle // ioResult contains the result of an asynchronous IO operation. type ioResult struct { bytes uint32 err error } // ioOperation represents an outstanding asynchronous Win32 IO. type ioOperation struct { o windows.Overlapped ch chan ioResult } func initIO() { h, err := createIoCompletionPort(windows.InvalidHandle, 0, 0, 0xffffffff) if err != nil { panic(err) } ioCompletionPort = h go ioCompletionProcessor(h) } // win32File implements Reader, Writer, and Closer on a Win32 handle without blocking in a syscall. // It takes ownership of this handle and will close it if it is garbage collected. type win32File struct { handle windows.Handle wg sync.WaitGroup wgLock sync.RWMutex closing atomic.Bool socket bool readDeadline deadlineHandler writeDeadline deadlineHandler } type deadlineHandler struct { setLock sync.Mutex channel timeoutChan channelLock sync.RWMutex timer *time.Timer timedout atomic.Bool } // makeWin32File makes a new win32File from an existing file handle. func makeWin32File(h windows.Handle) (*win32File, error) { f := &win32File{handle: h} ioInitOnce.Do(initIO) _, err := createIoCompletionPort(h, ioCompletionPort, 0, 0xffffffff) if err != nil { return nil, err } err = setFileCompletionNotificationModes(h, windows.FILE_SKIP_COMPLETION_PORT_ON_SUCCESS|windows.FILE_SKIP_SET_EVENT_ON_HANDLE) if err != nil { return nil, err } f.readDeadline.channel = make(timeoutChan) f.writeDeadline.channel = make(timeoutChan) return f, nil } // Deprecated: use NewOpenFile instead. func MakeOpenFile(h syscall.Handle) (io.ReadWriteCloser, error) { return NewOpenFile(windows.Handle(h)) } func NewOpenFile(h windows.Handle) (io.ReadWriteCloser, error) { // If we return the result of makeWin32File directly, it can result in an // interface-wrapped nil, rather than a nil interface value. f, err := makeWin32File(h) if err != nil { return nil, err } return f, nil } // closeHandle closes the resources associated with a Win32 handle. func (f *win32File) closeHandle() { f.wgLock.Lock() // Atomically set that we are closing, releasing the resources only once. if !f.closing.Swap(true) { f.wgLock.Unlock() // cancel all IO and wait for it to complete _ = cancelIoEx(f.handle, nil) f.wg.Wait() // at this point, no new IO can start windows.Close(f.handle) f.handle = 0 } else { f.wgLock.Unlock() } } // Close closes a win32File. func (f *win32File) Close() error { f.closeHandle() return nil } // IsClosed checks if the file has been closed. func (f *win32File) IsClosed() bool { return f.closing.Load() } // prepareIO prepares for a new IO operation. // The caller must call f.wg.Done() when the IO is finished, prior to Close() returning. func (f *win32File) prepareIO() (*ioOperation, error) { f.wgLock.RLock() if f.closing.Load() { f.wgLock.RUnlock() return nil, ErrFileClosed } f.wg.Add(1) f.wgLock.RUnlock() c := &ioOperation{} c.ch = make(chan ioResult) return c, nil } // ioCompletionProcessor processes completed async IOs forever. func ioCompletionProcessor(h windows.Handle) { for { var bytes uint32 var key uintptr var op *ioOperation err := getQueuedCompletionStatus(h, &bytes, &key, &op, windows.INFINITE) if op == nil { panic(err) } op.ch <- ioResult{bytes, err} } } // todo: helsaawy - create an asyncIO version that takes a context // asyncIO processes the return value from ReadFile or WriteFile, blocking until // the operation has actually completed. func (f *win32File) asyncIO(c *ioOperation, d *deadlineHandler, bytes uint32, err error) (int, error) { if err != windows.ERROR_IO_PENDING { //nolint:errorlint // err is Errno return int(bytes), err } if f.closing.Load() { _ = cancelIoEx(f.handle, &c.o) } var timeout timeoutChan if d != nil { d.channelLock.Lock() timeout = d.channel d.channelLock.Unlock() } var r ioResult select { case r = <-c.ch: err = r.err if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno if f.closing.Load() { err = ErrFileClosed } } else if err != nil && f.socket { // err is from Win32. Query the overlapped structure to get the winsock error. var bytes, flags uint32 err = wsaGetOverlappedResult(f.handle, &c.o, &bytes, false, &flags) } case <-timeout: _ = cancelIoEx(f.handle, &c.o) r = <-c.ch err = r.err if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno err = ErrTimeout } } // runtime.KeepAlive is needed, as c is passed via native // code to ioCompletionProcessor, c must remain alive // until the channel read is complete. // todo: (de)allocate *ioOperation via win32 heap functions, instead of needing to KeepAlive? runtime.KeepAlive(c) return int(r.bytes), err } // Read reads from a file handle. func (f *win32File) Read(b []byte) (int, error) { c, err := f.prepareIO() if err != nil { return 0, err } defer f.wg.Done() if f.readDeadline.timedout.Load() { return 0, ErrTimeout } var bytes uint32 err = windows.ReadFile(f.handle, b, &bytes, &c.o) n, err := f.asyncIO(c, &f.readDeadline, bytes, err) runtime.KeepAlive(b) // Handle EOF conditions. if err == nil && n == 0 && len(b) != 0 { return 0, io.EOF } else if err == windows.ERROR_BROKEN_PIPE { //nolint:errorlint // err is Errno return 0, io.EOF } return n, err } // Write writes to a file handle. func (f *win32File) Write(b []byte) (int, error) { c, err := f.prepareIO() if err != nil { return 0, err } defer f.wg.Done() if f.writeDeadline.timedout.Load() { return 0, ErrTimeout } var bytes uint32 err = windows.WriteFile(f.handle, b, &bytes, &c.o) n, err := f.asyncIO(c, &f.writeDeadline, bytes, err) runtime.KeepAlive(b) return n, err } func (f *win32File) SetReadDeadline(deadline time.Time) error { return f.readDeadline.set(deadline) } func (f *win32File) SetWriteDeadline(deadline time.Time) error { return f.writeDeadline.set(deadline) } func (f *win32File) Flush() error { return windows.FlushFileBuffers(f.handle) } func (f *win32File) Fd() uintptr { return uintptr(f.handle) } func (d *deadlineHandler) set(deadline time.Time) error { d.setLock.Lock() defer d.setLock.Unlock() if d.timer != nil { if !d.timer.Stop() { <-d.channel } d.timer = nil } d.timedout.Store(false) select { case <-d.channel: d.channelLock.Lock() d.channel = make(chan struct{}) d.channelLock.Unlock() default: } if deadline.IsZero() { return nil } timeoutIO := func() { d.timedout.Store(true) close(d.channel) } now := time.Now() duration := deadline.Sub(now) if deadline.After(now) { // Deadline is in the future, set a timer to wait d.timer = time.AfterFunc(duration, timeoutIO) } else { // Deadline is in the past. Cancel all pending IO now. timeoutIO() } return nil } ================================================ FILE: vendor/github.com/Microsoft/go-winio/fileinfo.go ================================================ //go:build windows // +build windows package winio import ( "os" "runtime" "unsafe" "golang.org/x/sys/windows" ) // FileBasicInfo contains file access time and file attributes information. type FileBasicInfo struct { CreationTime, LastAccessTime, LastWriteTime, ChangeTime windows.Filetime FileAttributes uint32 _ uint32 // padding } // alignedFileBasicInfo is a FileBasicInfo, but aligned to uint64 by containing // uint64 rather than windows.Filetime. Filetime contains two uint32s. uint64 // alignment is necessary to pass this as FILE_BASIC_INFO. type alignedFileBasicInfo struct { CreationTime, LastAccessTime, LastWriteTime, ChangeTime uint64 FileAttributes uint32 _ uint32 // padding } // GetFileBasicInfo retrieves times and attributes for a file. func GetFileBasicInfo(f *os.File) (*FileBasicInfo, error) { bi := &alignedFileBasicInfo{} if err := windows.GetFileInformationByHandleEx( windows.Handle(f.Fd()), windows.FileBasicInfo, (*byte)(unsafe.Pointer(bi)), uint32(unsafe.Sizeof(*bi)), ); err != nil { return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err} } runtime.KeepAlive(f) // Reinterpret the alignedFileBasicInfo as a FileBasicInfo so it matches the // public API of this module. The data may be unnecessarily aligned. return (*FileBasicInfo)(unsafe.Pointer(bi)), nil } // SetFileBasicInfo sets times and attributes for a file. func SetFileBasicInfo(f *os.File, bi *FileBasicInfo) error { // Create an alignedFileBasicInfo based on a FileBasicInfo. The copy is // suitable to pass to GetFileInformationByHandleEx. biAligned := *(*alignedFileBasicInfo)(unsafe.Pointer(bi)) if err := windows.SetFileInformationByHandle( windows.Handle(f.Fd()), windows.FileBasicInfo, (*byte)(unsafe.Pointer(&biAligned)), uint32(unsafe.Sizeof(biAligned)), ); err != nil { return &os.PathError{Op: "SetFileInformationByHandle", Path: f.Name(), Err: err} } runtime.KeepAlive(f) return nil } // FileStandardInfo contains extended information for the file. // FILE_STANDARD_INFO in WinBase.h // https://docs.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_standard_info type FileStandardInfo struct { AllocationSize, EndOfFile int64 NumberOfLinks uint32 DeletePending, Directory bool } // GetFileStandardInfo retrieves ended information for the file. func GetFileStandardInfo(f *os.File) (*FileStandardInfo, error) { si := &FileStandardInfo{} if err := windows.GetFileInformationByHandleEx(windows.Handle(f.Fd()), windows.FileStandardInfo, (*byte)(unsafe.Pointer(si)), uint32(unsafe.Sizeof(*si))); err != nil { return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err} } runtime.KeepAlive(f) return si, nil } // FileIDInfo contains the volume serial number and file ID for a file. This pair should be // unique on a system. type FileIDInfo struct { VolumeSerialNumber uint64 FileID [16]byte } // GetFileID retrieves the unique (volume, file ID) pair for a file. func GetFileID(f *os.File) (*FileIDInfo, error) { fileID := &FileIDInfo{} if err := windows.GetFileInformationByHandleEx( windows.Handle(f.Fd()), windows.FileIdInfo, (*byte)(unsafe.Pointer(fileID)), uint32(unsafe.Sizeof(*fileID)), ); err != nil { return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err} } runtime.KeepAlive(f) return fileID, nil } ================================================ FILE: vendor/github.com/Microsoft/go-winio/hvsock.go ================================================ //go:build windows // +build windows package winio import ( "context" "errors" "fmt" "io" "net" "os" "time" "unsafe" "golang.org/x/sys/windows" "github.com/Microsoft/go-winio/internal/socket" "github.com/Microsoft/go-winio/pkg/guid" ) const afHVSock = 34 // AF_HYPERV // Well known Service and VM IDs // https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/make-integration-service#vmid-wildcards // HvsockGUIDWildcard is the wildcard VmId for accepting connections from all partitions. func HvsockGUIDWildcard() guid.GUID { // 00000000-0000-0000-0000-000000000000 return guid.GUID{} } // HvsockGUIDBroadcast is the wildcard VmId for broadcasting sends to all partitions. func HvsockGUIDBroadcast() guid.GUID { // ffffffff-ffff-ffff-ffff-ffffffffffff return guid.GUID{ Data1: 0xffffffff, Data2: 0xffff, Data3: 0xffff, Data4: [8]uint8{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, } } // HvsockGUIDLoopback is the Loopback VmId for accepting connections to the same partition as the connector. func HvsockGUIDLoopback() guid.GUID { // e0e16197-dd56-4a10-9195-5ee7a155a838 return guid.GUID{ Data1: 0xe0e16197, Data2: 0xdd56, Data3: 0x4a10, Data4: [8]uint8{0x91, 0x95, 0x5e, 0xe7, 0xa1, 0x55, 0xa8, 0x38}, } } // HvsockGUIDSiloHost is the address of a silo's host partition: // - The silo host of a hosted silo is the utility VM. // - The silo host of a silo on a physical host is the physical host. func HvsockGUIDSiloHost() guid.GUID { // 36bd0c5c-7276-4223-88ba-7d03b654c568 return guid.GUID{ Data1: 0x36bd0c5c, Data2: 0x7276, Data3: 0x4223, Data4: [8]byte{0x88, 0xba, 0x7d, 0x03, 0xb6, 0x54, 0xc5, 0x68}, } } // HvsockGUIDChildren is the wildcard VmId for accepting connections from the connector's child partitions. func HvsockGUIDChildren() guid.GUID { // 90db8b89-0d35-4f79-8ce9-49ea0ac8b7cd return guid.GUID{ Data1: 0x90db8b89, Data2: 0xd35, Data3: 0x4f79, Data4: [8]uint8{0x8c, 0xe9, 0x49, 0xea, 0xa, 0xc8, 0xb7, 0xcd}, } } // HvsockGUIDParent is the wildcard VmId for accepting connections from the connector's parent partition. // Listening on this VmId accepts connection from: // - Inside silos: silo host partition. // - Inside hosted silo: host of the VM. // - Inside VM: VM host. // - Physical host: Not supported. func HvsockGUIDParent() guid.GUID { // a42e7cda-d03f-480c-9cc2-a4de20abb878 return guid.GUID{ Data1: 0xa42e7cda, Data2: 0xd03f, Data3: 0x480c, Data4: [8]uint8{0x9c, 0xc2, 0xa4, 0xde, 0x20, 0xab, 0xb8, 0x78}, } } // hvsockVsockServiceTemplate is the Service GUID used for the VSOCK protocol. func hvsockVsockServiceTemplate() guid.GUID { // 00000000-facb-11e6-bd58-64006a7986d3 return guid.GUID{ Data2: 0xfacb, Data3: 0x11e6, Data4: [8]uint8{0xbd, 0x58, 0x64, 0x00, 0x6a, 0x79, 0x86, 0xd3}, } } // An HvsockAddr is an address for a AF_HYPERV socket. type HvsockAddr struct { VMID guid.GUID ServiceID guid.GUID } type rawHvsockAddr struct { Family uint16 _ uint16 VMID guid.GUID ServiceID guid.GUID } var _ socket.RawSockaddr = &rawHvsockAddr{} // Network returns the address's network name, "hvsock". func (*HvsockAddr) Network() string { return "hvsock" } func (addr *HvsockAddr) String() string { return fmt.Sprintf("%s:%s", &addr.VMID, &addr.ServiceID) } // VsockServiceID returns an hvsock service ID corresponding to the specified AF_VSOCK port. func VsockServiceID(port uint32) guid.GUID { g := hvsockVsockServiceTemplate() // make a copy g.Data1 = port return g } func (addr *HvsockAddr) raw() rawHvsockAddr { return rawHvsockAddr{ Family: afHVSock, VMID: addr.VMID, ServiceID: addr.ServiceID, } } func (addr *HvsockAddr) fromRaw(raw *rawHvsockAddr) { addr.VMID = raw.VMID addr.ServiceID = raw.ServiceID } // Sockaddr returns a pointer to and the size of this struct. // // Implements the [socket.RawSockaddr] interface, and allows use in // [socket.Bind] and [socket.ConnectEx]. func (r *rawHvsockAddr) Sockaddr() (unsafe.Pointer, int32, error) { return unsafe.Pointer(r), int32(unsafe.Sizeof(rawHvsockAddr{})), nil } // Sockaddr interface allows use with `sockets.Bind()` and `.ConnectEx()`. func (r *rawHvsockAddr) FromBytes(b []byte) error { n := int(unsafe.Sizeof(rawHvsockAddr{})) if len(b) < n { return fmt.Errorf("got %d, want %d: %w", len(b), n, socket.ErrBufferSize) } copy(unsafe.Slice((*byte)(unsafe.Pointer(r)), n), b[:n]) if r.Family != afHVSock { return fmt.Errorf("got %d, want %d: %w", r.Family, afHVSock, socket.ErrAddrFamily) } return nil } // HvsockListener is a socket listener for the AF_HYPERV address family. type HvsockListener struct { sock *win32File addr HvsockAddr } var _ net.Listener = &HvsockListener{} // HvsockConn is a connected socket of the AF_HYPERV address family. type HvsockConn struct { sock *win32File local, remote HvsockAddr } var _ net.Conn = &HvsockConn{} func newHVSocket() (*win32File, error) { fd, err := windows.Socket(afHVSock, windows.SOCK_STREAM, 1) if err != nil { return nil, os.NewSyscallError("socket", err) } f, err := makeWin32File(fd) if err != nil { windows.Close(fd) return nil, err } f.socket = true return f, nil } // ListenHvsock listens for connections on the specified hvsock address. func ListenHvsock(addr *HvsockAddr) (_ *HvsockListener, err error) { l := &HvsockListener{addr: *addr} var sock *win32File sock, err = newHVSocket() if err != nil { return nil, l.opErr("listen", err) } defer func() { if err != nil { _ = sock.Close() } }() sa := addr.raw() err = socket.Bind(sock.handle, &sa) if err != nil { return nil, l.opErr("listen", os.NewSyscallError("socket", err)) } err = windows.Listen(sock.handle, 16) if err != nil { return nil, l.opErr("listen", os.NewSyscallError("listen", err)) } return &HvsockListener{sock: sock, addr: *addr}, nil } func (l *HvsockListener) opErr(op string, err error) error { return &net.OpError{Op: op, Net: "hvsock", Addr: &l.addr, Err: err} } // Addr returns the listener's network address. func (l *HvsockListener) Addr() net.Addr { return &l.addr } // Accept waits for the next connection and returns it. func (l *HvsockListener) Accept() (_ net.Conn, err error) { sock, err := newHVSocket() if err != nil { return nil, l.opErr("accept", err) } defer func() { if sock != nil { sock.Close() } }() c, err := l.sock.prepareIO() if err != nil { return nil, l.opErr("accept", err) } defer l.sock.wg.Done() // AcceptEx, per documentation, requires an extra 16 bytes per address. // // https://docs.microsoft.com/en-us/windows/win32/api/mswsock/nf-mswsock-acceptex const addrlen = uint32(16 + unsafe.Sizeof(rawHvsockAddr{})) var addrbuf [addrlen * 2]byte var bytes uint32 err = windows.AcceptEx(l.sock.handle, sock.handle, &addrbuf[0], 0 /* rxdatalen */, addrlen, addrlen, &bytes, &c.o) if _, err = l.sock.asyncIO(c, nil, bytes, err); err != nil { return nil, l.opErr("accept", os.NewSyscallError("acceptex", err)) } conn := &HvsockConn{ sock: sock, } // The local address returned in the AcceptEx buffer is the same as the Listener socket's // address. However, the service GUID reported by GetSockName is different from the Listeners // socket, and is sometimes the same as the local address of the socket that dialed the // address, with the service GUID.Data1 incremented, but othertimes is different. // todo: does the local address matter? is the listener's address or the actual address appropriate? conn.local.fromRaw((*rawHvsockAddr)(unsafe.Pointer(&addrbuf[0]))) conn.remote.fromRaw((*rawHvsockAddr)(unsafe.Pointer(&addrbuf[addrlen]))) // initialize the accepted socket and update its properties with those of the listening socket if err = windows.Setsockopt(sock.handle, windows.SOL_SOCKET, windows.SO_UPDATE_ACCEPT_CONTEXT, (*byte)(unsafe.Pointer(&l.sock.handle)), int32(unsafe.Sizeof(l.sock.handle))); err != nil { return nil, conn.opErr("accept", os.NewSyscallError("setsockopt", err)) } sock = nil return conn, nil } // Close closes the listener, causing any pending Accept calls to fail. func (l *HvsockListener) Close() error { return l.sock.Close() } // HvsockDialer configures and dials a Hyper-V Socket (ie, [HvsockConn]). type HvsockDialer struct { // Deadline is the time the Dial operation must connect before erroring. Deadline time.Time // Retries is the number of additional connects to try if the connection times out, is refused, // or the host is unreachable Retries uint // RetryWait is the time to wait after a connection error to retry RetryWait time.Duration rt *time.Timer // redial wait timer } // Dial the Hyper-V socket at addr. // // See [HvsockDialer.Dial] for more information. func Dial(ctx context.Context, addr *HvsockAddr) (conn *HvsockConn, err error) { return (&HvsockDialer{}).Dial(ctx, addr) } // Dial attempts to connect to the Hyper-V socket at addr, and returns a connection if successful. // Will attempt (HvsockDialer).Retries if dialing fails, waiting (HvsockDialer).RetryWait between // retries. // // Dialing can be cancelled either by providing (HvsockDialer).Deadline, or cancelling ctx. func (d *HvsockDialer) Dial(ctx context.Context, addr *HvsockAddr) (conn *HvsockConn, err error) { op := "dial" // create the conn early to use opErr() conn = &HvsockConn{ remote: *addr, } if !d.Deadline.IsZero() { var cancel context.CancelFunc ctx, cancel = context.WithDeadline(ctx, d.Deadline) defer cancel() } // preemptive timeout/cancellation check if err = ctx.Err(); err != nil { return nil, conn.opErr(op, err) } sock, err := newHVSocket() if err != nil { return nil, conn.opErr(op, err) } defer func() { if sock != nil { sock.Close() } }() sa := addr.raw() err = socket.Bind(sock.handle, &sa) if err != nil { return nil, conn.opErr(op, os.NewSyscallError("bind", err)) } c, err := sock.prepareIO() if err != nil { return nil, conn.opErr(op, err) } defer sock.wg.Done() var bytes uint32 for i := uint(0); i <= d.Retries; i++ { err = socket.ConnectEx( sock.handle, &sa, nil, // sendBuf 0, // sendDataLen &bytes, (*windows.Overlapped)(unsafe.Pointer(&c.o))) _, err = sock.asyncIO(c, nil, bytes, err) if i < d.Retries && canRedial(err) { if err = d.redialWait(ctx); err == nil { continue } } break } if err != nil { return nil, conn.opErr(op, os.NewSyscallError("connectex", err)) } // update the connection properties, so shutdown can be used if err = windows.Setsockopt( sock.handle, windows.SOL_SOCKET, windows.SO_UPDATE_CONNECT_CONTEXT, nil, // optvalue 0, // optlen ); err != nil { return nil, conn.opErr(op, os.NewSyscallError("setsockopt", err)) } // get the local name var sal rawHvsockAddr err = socket.GetSockName(sock.handle, &sal) if err != nil { return nil, conn.opErr(op, os.NewSyscallError("getsockname", err)) } conn.local.fromRaw(&sal) // one last check for timeout, since asyncIO doesn't check the context if err = ctx.Err(); err != nil { return nil, conn.opErr(op, err) } conn.sock = sock sock = nil return conn, nil } // redialWait waits before attempting to redial, resetting the timer as appropriate. func (d *HvsockDialer) redialWait(ctx context.Context) (err error) { if d.RetryWait == 0 { return nil } if d.rt == nil { d.rt = time.NewTimer(d.RetryWait) } else { // should already be stopped and drained d.rt.Reset(d.RetryWait) } select { case <-ctx.Done(): case <-d.rt.C: return nil } // stop and drain the timer if !d.rt.Stop() { <-d.rt.C } return ctx.Err() } // assumes error is a plain, unwrapped windows.Errno provided by direct syscall. func canRedial(err error) bool { //nolint:errorlint // guaranteed to be an Errno switch err { case windows.WSAECONNREFUSED, windows.WSAENETUNREACH, windows.WSAETIMEDOUT, windows.ERROR_CONNECTION_REFUSED, windows.ERROR_CONNECTION_UNAVAIL: return true default: return false } } func (conn *HvsockConn) opErr(op string, err error) error { // translate from "file closed" to "socket closed" if errors.Is(err, ErrFileClosed) { err = socket.ErrSocketClosed } return &net.OpError{Op: op, Net: "hvsock", Source: &conn.local, Addr: &conn.remote, Err: err} } func (conn *HvsockConn) Read(b []byte) (int, error) { c, err := conn.sock.prepareIO() if err != nil { return 0, conn.opErr("read", err) } defer conn.sock.wg.Done() buf := windows.WSABuf{Buf: &b[0], Len: uint32(len(b))} var flags, bytes uint32 err = windows.WSARecv(conn.sock.handle, &buf, 1, &bytes, &flags, &c.o, nil) n, err := conn.sock.asyncIO(c, &conn.sock.readDeadline, bytes, err) if err != nil { var eno windows.Errno if errors.As(err, &eno) { err = os.NewSyscallError("wsarecv", eno) } return 0, conn.opErr("read", err) } else if n == 0 { err = io.EOF } return n, err } func (conn *HvsockConn) Write(b []byte) (int, error) { t := 0 for len(b) != 0 { n, err := conn.write(b) if err != nil { return t + n, err } t += n b = b[n:] } return t, nil } func (conn *HvsockConn) write(b []byte) (int, error) { c, err := conn.sock.prepareIO() if err != nil { return 0, conn.opErr("write", err) } defer conn.sock.wg.Done() buf := windows.WSABuf{Buf: &b[0], Len: uint32(len(b))} var bytes uint32 err = windows.WSASend(conn.sock.handle, &buf, 1, &bytes, 0, &c.o, nil) n, err := conn.sock.asyncIO(c, &conn.sock.writeDeadline, bytes, err) if err != nil { var eno windows.Errno if errors.As(err, &eno) { err = os.NewSyscallError("wsasend", eno) } return 0, conn.opErr("write", err) } return n, err } // Close closes the socket connection, failing any pending read or write calls. func (conn *HvsockConn) Close() error { return conn.sock.Close() } func (conn *HvsockConn) IsClosed() bool { return conn.sock.IsClosed() } // shutdown disables sending or receiving on a socket. func (conn *HvsockConn) shutdown(how int) error { if conn.IsClosed() { return socket.ErrSocketClosed } err := windows.Shutdown(conn.sock.handle, how) if err != nil { // If the connection was closed, shutdowns fail with "not connected" if errors.Is(err, windows.WSAENOTCONN) || errors.Is(err, windows.WSAESHUTDOWN) { err = socket.ErrSocketClosed } return os.NewSyscallError("shutdown", err) } return nil } // CloseRead shuts down the read end of the socket, preventing future read operations. func (conn *HvsockConn) CloseRead() error { err := conn.shutdown(windows.SHUT_RD) if err != nil { return conn.opErr("closeread", err) } return nil } // CloseWrite shuts down the write end of the socket, preventing future write operations and // notifying the other endpoint that no more data will be written. func (conn *HvsockConn) CloseWrite() error { err := conn.shutdown(windows.SHUT_WR) if err != nil { return conn.opErr("closewrite", err) } return nil } // LocalAddr returns the local address of the connection. func (conn *HvsockConn) LocalAddr() net.Addr { return &conn.local } // RemoteAddr returns the remote address of the connection. func (conn *HvsockConn) RemoteAddr() net.Addr { return &conn.remote } // SetDeadline implements the net.Conn SetDeadline method. func (conn *HvsockConn) SetDeadline(t time.Time) error { // todo: implement `SetDeadline` for `win32File` if err := conn.SetReadDeadline(t); err != nil { return fmt.Errorf("set read deadline: %w", err) } if err := conn.SetWriteDeadline(t); err != nil { return fmt.Errorf("set write deadline: %w", err) } return nil } // SetReadDeadline implements the net.Conn SetReadDeadline method. func (conn *HvsockConn) SetReadDeadline(t time.Time) error { return conn.sock.SetReadDeadline(t) } // SetWriteDeadline implements the net.Conn SetWriteDeadline method. func (conn *HvsockConn) SetWriteDeadline(t time.Time) error { return conn.sock.SetWriteDeadline(t) } ================================================ FILE: vendor/github.com/Microsoft/go-winio/internal/fs/doc.go ================================================ // This package contains Win32 filesystem functionality. package fs ================================================ FILE: vendor/github.com/Microsoft/go-winio/internal/fs/fs.go ================================================ //go:build windows package fs import ( "golang.org/x/sys/windows" "github.com/Microsoft/go-winio/internal/stringbuffer" ) //go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go fs.go // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew //sys CreateFile(name string, access AccessMask, mode FileShareMode, sa *windows.SecurityAttributes, createmode FileCreationDisposition, attrs FileFlagOrAttribute, templatefile windows.Handle) (handle windows.Handle, err error) [failretval==windows.InvalidHandle] = CreateFileW const NullHandle windows.Handle = 0 // AccessMask defines standard, specific, and generic rights. // // Used with CreateFile and NtCreateFile (and co.). // // Bitmask: // 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 // 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 // +---------------+---------------+-------------------------------+ // |G|G|G|G|Resvd|A| StandardRights| SpecificRights | // |R|W|E|A| |S| | | // +-+-------------+---------------+-------------------------------+ // // GR Generic Read // GW Generic Write // GE Generic Exectue // GA Generic All // Resvd Reserved // AS Access Security System // // https://learn.microsoft.com/en-us/windows/win32/secauthz/access-mask // // https://learn.microsoft.com/en-us/windows/win32/secauthz/generic-access-rights // // https://learn.microsoft.com/en-us/windows/win32/fileio/file-access-rights-constants type AccessMask = windows.ACCESS_MASK //nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. const ( // Not actually any. // // For CreateFile: "query certain metadata such as file, directory, or device attributes without accessing that file or device" // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew#parameters FILE_ANY_ACCESS AccessMask = 0 GENERIC_READ AccessMask = 0x8000_0000 GENERIC_WRITE AccessMask = 0x4000_0000 GENERIC_EXECUTE AccessMask = 0x2000_0000 GENERIC_ALL AccessMask = 0x1000_0000 ACCESS_SYSTEM_SECURITY AccessMask = 0x0100_0000 // Specific Object Access // from ntioapi.h FILE_READ_DATA AccessMask = (0x0001) // file & pipe FILE_LIST_DIRECTORY AccessMask = (0x0001) // directory FILE_WRITE_DATA AccessMask = (0x0002) // file & pipe FILE_ADD_FILE AccessMask = (0x0002) // directory FILE_APPEND_DATA AccessMask = (0x0004) // file FILE_ADD_SUBDIRECTORY AccessMask = (0x0004) // directory FILE_CREATE_PIPE_INSTANCE AccessMask = (0x0004) // named pipe FILE_READ_EA AccessMask = (0x0008) // file & directory FILE_READ_PROPERTIES AccessMask = FILE_READ_EA FILE_WRITE_EA AccessMask = (0x0010) // file & directory FILE_WRITE_PROPERTIES AccessMask = FILE_WRITE_EA FILE_EXECUTE AccessMask = (0x0020) // file FILE_TRAVERSE AccessMask = (0x0020) // directory FILE_DELETE_CHILD AccessMask = (0x0040) // directory FILE_READ_ATTRIBUTES AccessMask = (0x0080) // all FILE_WRITE_ATTRIBUTES AccessMask = (0x0100) // all FILE_ALL_ACCESS AccessMask = (STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0x1FF) FILE_GENERIC_READ AccessMask = (STANDARD_RIGHTS_READ | FILE_READ_DATA | FILE_READ_ATTRIBUTES | FILE_READ_EA | SYNCHRONIZE) FILE_GENERIC_WRITE AccessMask = (STANDARD_RIGHTS_WRITE | FILE_WRITE_DATA | FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA | FILE_APPEND_DATA | SYNCHRONIZE) FILE_GENERIC_EXECUTE AccessMask = (STANDARD_RIGHTS_EXECUTE | FILE_READ_ATTRIBUTES | FILE_EXECUTE | SYNCHRONIZE) SPECIFIC_RIGHTS_ALL AccessMask = 0x0000FFFF // Standard Access // from ntseapi.h DELETE AccessMask = 0x0001_0000 READ_CONTROL AccessMask = 0x0002_0000 WRITE_DAC AccessMask = 0x0004_0000 WRITE_OWNER AccessMask = 0x0008_0000 SYNCHRONIZE AccessMask = 0x0010_0000 STANDARD_RIGHTS_REQUIRED AccessMask = 0x000F_0000 STANDARD_RIGHTS_READ AccessMask = READ_CONTROL STANDARD_RIGHTS_WRITE AccessMask = READ_CONTROL STANDARD_RIGHTS_EXECUTE AccessMask = READ_CONTROL STANDARD_RIGHTS_ALL AccessMask = 0x001F_0000 ) type FileShareMode uint32 //nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. const ( FILE_SHARE_NONE FileShareMode = 0x00 FILE_SHARE_READ FileShareMode = 0x01 FILE_SHARE_WRITE FileShareMode = 0x02 FILE_SHARE_DELETE FileShareMode = 0x04 FILE_SHARE_VALID_FLAGS FileShareMode = 0x07 ) type FileCreationDisposition uint32 //nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. const ( // from winbase.h CREATE_NEW FileCreationDisposition = 0x01 CREATE_ALWAYS FileCreationDisposition = 0x02 OPEN_EXISTING FileCreationDisposition = 0x03 OPEN_ALWAYS FileCreationDisposition = 0x04 TRUNCATE_EXISTING FileCreationDisposition = 0x05 ) // Create disposition values for NtCreate* type NTFileCreationDisposition uint32 //nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. const ( // From ntioapi.h FILE_SUPERSEDE NTFileCreationDisposition = 0x00 FILE_OPEN NTFileCreationDisposition = 0x01 FILE_CREATE NTFileCreationDisposition = 0x02 FILE_OPEN_IF NTFileCreationDisposition = 0x03 FILE_OVERWRITE NTFileCreationDisposition = 0x04 FILE_OVERWRITE_IF NTFileCreationDisposition = 0x05 FILE_MAXIMUM_DISPOSITION NTFileCreationDisposition = 0x05 ) // CreateFile and co. take flags or attributes together as one parameter. // Define alias until we can use generics to allow both // // https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants type FileFlagOrAttribute uint32 //nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. const ( // from winnt.h FILE_FLAG_WRITE_THROUGH FileFlagOrAttribute = 0x8000_0000 FILE_FLAG_OVERLAPPED FileFlagOrAttribute = 0x4000_0000 FILE_FLAG_NO_BUFFERING FileFlagOrAttribute = 0x2000_0000 FILE_FLAG_RANDOM_ACCESS FileFlagOrAttribute = 0x1000_0000 FILE_FLAG_SEQUENTIAL_SCAN FileFlagOrAttribute = 0x0800_0000 FILE_FLAG_DELETE_ON_CLOSE FileFlagOrAttribute = 0x0400_0000 FILE_FLAG_BACKUP_SEMANTICS FileFlagOrAttribute = 0x0200_0000 FILE_FLAG_POSIX_SEMANTICS FileFlagOrAttribute = 0x0100_0000 FILE_FLAG_OPEN_REPARSE_POINT FileFlagOrAttribute = 0x0020_0000 FILE_FLAG_OPEN_NO_RECALL FileFlagOrAttribute = 0x0010_0000 FILE_FLAG_FIRST_PIPE_INSTANCE FileFlagOrAttribute = 0x0008_0000 ) // NtCreate* functions take a dedicated CreateOptions parameter. // // https://learn.microsoft.com/en-us/windows/win32/api/Winternl/nf-winternl-ntcreatefile // // https://learn.microsoft.com/en-us/windows/win32/devnotes/nt-create-named-pipe-file type NTCreateOptions uint32 //nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. const ( // From ntioapi.h FILE_DIRECTORY_FILE NTCreateOptions = 0x0000_0001 FILE_WRITE_THROUGH NTCreateOptions = 0x0000_0002 FILE_SEQUENTIAL_ONLY NTCreateOptions = 0x0000_0004 FILE_NO_INTERMEDIATE_BUFFERING NTCreateOptions = 0x0000_0008 FILE_SYNCHRONOUS_IO_ALERT NTCreateOptions = 0x0000_0010 FILE_SYNCHRONOUS_IO_NONALERT NTCreateOptions = 0x0000_0020 FILE_NON_DIRECTORY_FILE NTCreateOptions = 0x0000_0040 FILE_CREATE_TREE_CONNECTION NTCreateOptions = 0x0000_0080 FILE_COMPLETE_IF_OPLOCKED NTCreateOptions = 0x0000_0100 FILE_NO_EA_KNOWLEDGE NTCreateOptions = 0x0000_0200 FILE_DISABLE_TUNNELING NTCreateOptions = 0x0000_0400 FILE_RANDOM_ACCESS NTCreateOptions = 0x0000_0800 FILE_DELETE_ON_CLOSE NTCreateOptions = 0x0000_1000 FILE_OPEN_BY_FILE_ID NTCreateOptions = 0x0000_2000 FILE_OPEN_FOR_BACKUP_INTENT NTCreateOptions = 0x0000_4000 FILE_NO_COMPRESSION NTCreateOptions = 0x0000_8000 ) type FileSQSFlag = FileFlagOrAttribute //nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. const ( // from winbase.h SECURITY_ANONYMOUS FileSQSFlag = FileSQSFlag(SecurityAnonymous << 16) SECURITY_IDENTIFICATION FileSQSFlag = FileSQSFlag(SecurityIdentification << 16) SECURITY_IMPERSONATION FileSQSFlag = FileSQSFlag(SecurityImpersonation << 16) SECURITY_DELEGATION FileSQSFlag = FileSQSFlag(SecurityDelegation << 16) SECURITY_SQOS_PRESENT FileSQSFlag = 0x0010_0000 SECURITY_VALID_SQOS_FLAGS FileSQSFlag = 0x001F_0000 ) // GetFinalPathNameByHandle flags // // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew#parameters type GetFinalPathFlag uint32 //nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. const ( GetFinalPathDefaultFlag GetFinalPathFlag = 0x0 FILE_NAME_NORMALIZED GetFinalPathFlag = 0x0 FILE_NAME_OPENED GetFinalPathFlag = 0x8 VOLUME_NAME_DOS GetFinalPathFlag = 0x0 VOLUME_NAME_GUID GetFinalPathFlag = 0x1 VOLUME_NAME_NT GetFinalPathFlag = 0x2 VOLUME_NAME_NONE GetFinalPathFlag = 0x4 ) // getFinalPathNameByHandle facilitates calling the Windows API GetFinalPathNameByHandle // with the given handle and flags. It transparently takes care of creating a buffer of the // correct size for the call. // // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew func GetFinalPathNameByHandle(h windows.Handle, flags GetFinalPathFlag) (string, error) { b := stringbuffer.NewWString() //TODO: can loop infinitely if Win32 keeps returning the same (or a larger) n? for { n, err := windows.GetFinalPathNameByHandle(h, b.Pointer(), b.Cap(), uint32(flags)) if err != nil { return "", err } // If the buffer wasn't large enough, n will be the total size needed (including null terminator). // Resize and try again. if n > b.Cap() { b.ResizeTo(n) continue } // If the buffer is large enough, n will be the size not including the null terminator. // Convert to a Go string and return. return b.String(), nil } } ================================================ FILE: vendor/github.com/Microsoft/go-winio/internal/fs/security.go ================================================ package fs // https://learn.microsoft.com/en-us/windows/win32/api/winnt/ne-winnt-security_impersonation_level type SecurityImpersonationLevel int32 // C default enums underlying type is `int`, which is Go `int32` // Impersonation levels const ( SecurityAnonymous SecurityImpersonationLevel = 0 SecurityIdentification SecurityImpersonationLevel = 1 SecurityImpersonation SecurityImpersonationLevel = 2 SecurityDelegation SecurityImpersonationLevel = 3 ) ================================================ FILE: vendor/github.com/Microsoft/go-winio/internal/fs/zsyscall_windows.go ================================================ //go:build windows // Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT. package fs import ( "syscall" "unsafe" "golang.org/x/sys/windows" ) var _ unsafe.Pointer // Do the interface allocations only once for common // Errno values. const ( errnoERROR_IO_PENDING = 997 ) var ( errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) errERROR_EINVAL error = syscall.EINVAL ) // errnoErr returns common boxed Errno values, to prevent // allocations at runtime. func errnoErr(e syscall.Errno) error { switch e { case 0: return errERROR_EINVAL case errnoERROR_IO_PENDING: return errERROR_IO_PENDING } return e } var ( modkernel32 = windows.NewLazySystemDLL("kernel32.dll") procCreateFileW = modkernel32.NewProc("CreateFileW") ) func CreateFile(name string, access AccessMask, mode FileShareMode, sa *windows.SecurityAttributes, createmode FileCreationDisposition, attrs FileFlagOrAttribute, templatefile windows.Handle) (handle windows.Handle, err error) { var _p0 *uint16 _p0, err = syscall.UTF16PtrFromString(name) if err != nil { return } return _CreateFile(_p0, access, mode, sa, createmode, attrs, templatefile) } func _CreateFile(name *uint16, access AccessMask, mode FileShareMode, sa *windows.SecurityAttributes, createmode FileCreationDisposition, attrs FileFlagOrAttribute, templatefile windows.Handle) (handle windows.Handle, err error) { r0, _, e1 := syscall.SyscallN(procCreateFileW.Addr(), uintptr(unsafe.Pointer(name)), uintptr(access), uintptr(mode), uintptr(unsafe.Pointer(sa)), uintptr(createmode), uintptr(attrs), uintptr(templatefile)) handle = windows.Handle(r0) if handle == windows.InvalidHandle { err = errnoErr(e1) } return } ================================================ FILE: vendor/github.com/Microsoft/go-winio/internal/socket/rawaddr.go ================================================ package socket import ( "unsafe" ) // RawSockaddr allows structs to be used with [Bind] and [ConnectEx]. The // struct must meet the Win32 sockaddr requirements specified here: // https://docs.microsoft.com/en-us/windows/win32/winsock/sockaddr-2 // // Specifically, the struct size must be least larger than an int16 (unsigned short) // for the address family. type RawSockaddr interface { // Sockaddr returns a pointer to the RawSockaddr and its struct size, allowing // for the RawSockaddr's data to be overwritten by syscalls (if necessary). // // It is the callers responsibility to validate that the values are valid; invalid // pointers or size can cause a panic. Sockaddr() (unsafe.Pointer, int32, error) } ================================================ FILE: vendor/github.com/Microsoft/go-winio/internal/socket/socket.go ================================================ //go:build windows package socket import ( "errors" "fmt" "net" "sync" "syscall" "unsafe" "github.com/Microsoft/go-winio/pkg/guid" "golang.org/x/sys/windows" ) //go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go socket.go //sys getsockname(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) [failretval==socketError] = ws2_32.getsockname //sys getpeername(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) [failretval==socketError] = ws2_32.getpeername //sys bind(s windows.Handle, name unsafe.Pointer, namelen int32) (err error) [failretval==socketError] = ws2_32.bind const socketError = uintptr(^uint32(0)) var ( // todo(helsaawy): create custom error types to store the desired vs actual size and addr family? ErrBufferSize = errors.New("buffer size") ErrAddrFamily = errors.New("address family") ErrInvalidPointer = errors.New("invalid pointer") ErrSocketClosed = fmt.Errorf("socket closed: %w", net.ErrClosed) ) // todo(helsaawy): replace these with generics, ie: GetSockName[S RawSockaddr](s windows.Handle) (S, error) // GetSockName writes the local address of socket s to the [RawSockaddr] rsa. // If rsa is not large enough, the [windows.WSAEFAULT] is returned. func GetSockName(s windows.Handle, rsa RawSockaddr) error { ptr, l, err := rsa.Sockaddr() if err != nil { return fmt.Errorf("could not retrieve socket pointer and size: %w", err) } // although getsockname returns WSAEFAULT if the buffer is too small, it does not set // &l to the correct size, so--apart from doubling the buffer repeatedly--there is no remedy return getsockname(s, ptr, &l) } // GetPeerName returns the remote address the socket is connected to. // // See [GetSockName] for more information. func GetPeerName(s windows.Handle, rsa RawSockaddr) error { ptr, l, err := rsa.Sockaddr() if err != nil { return fmt.Errorf("could not retrieve socket pointer and size: %w", err) } return getpeername(s, ptr, &l) } func Bind(s windows.Handle, rsa RawSockaddr) (err error) { ptr, l, err := rsa.Sockaddr() if err != nil { return fmt.Errorf("could not retrieve socket pointer and size: %w", err) } return bind(s, ptr, l) } // "golang.org/x/sys/windows".ConnectEx and .Bind only accept internal implementations of the // their sockaddr interface, so they cannot be used with HvsockAddr // Replicate functionality here from // https://cs.opensource.google/go/x/sys/+/master:windows/syscall_windows.go // The function pointers to `AcceptEx`, `ConnectEx` and `GetAcceptExSockaddrs` must be loaded at // runtime via a WSAIoctl call: // https://docs.microsoft.com/en-us/windows/win32/api/Mswsock/nc-mswsock-lpfn_connectex#remarks type runtimeFunc struct { id guid.GUID once sync.Once addr uintptr err error } func (f *runtimeFunc) Load() error { f.once.Do(func() { var s windows.Handle s, f.err = windows.Socket(windows.AF_INET, windows.SOCK_STREAM, windows.IPPROTO_TCP) if f.err != nil { return } defer windows.CloseHandle(s) //nolint:errcheck var n uint32 f.err = windows.WSAIoctl(s, windows.SIO_GET_EXTENSION_FUNCTION_POINTER, (*byte)(unsafe.Pointer(&f.id)), uint32(unsafe.Sizeof(f.id)), (*byte)(unsafe.Pointer(&f.addr)), uint32(unsafe.Sizeof(f.addr)), &n, nil, // overlapped 0, // completionRoutine ) }) return f.err } var ( // todo: add `AcceptEx` and `GetAcceptExSockaddrs` WSAID_CONNECTEX = guid.GUID{ //revive:disable-line:var-naming ALL_CAPS Data1: 0x25a207b9, Data2: 0xddf3, Data3: 0x4660, Data4: [8]byte{0x8e, 0xe9, 0x76, 0xe5, 0x8c, 0x74, 0x06, 0x3e}, } connectExFunc = runtimeFunc{id: WSAID_CONNECTEX} ) func ConnectEx( fd windows.Handle, rsa RawSockaddr, sendBuf *byte, sendDataLen uint32, bytesSent *uint32, overlapped *windows.Overlapped, ) error { if err := connectExFunc.Load(); err != nil { return fmt.Errorf("failed to load ConnectEx function pointer: %w", err) } ptr, n, err := rsa.Sockaddr() if err != nil { return err } return connectEx(fd, ptr, n, sendBuf, sendDataLen, bytesSent, overlapped) } // BOOL LpfnConnectex( // [in] SOCKET s, // [in] const sockaddr *name, // [in] int namelen, // [in, optional] PVOID lpSendBuffer, // [in] DWORD dwSendDataLength, // [out] LPDWORD lpdwBytesSent, // [in] LPOVERLAPPED lpOverlapped // ) func connectEx( s windows.Handle, name unsafe.Pointer, namelen int32, sendBuf *byte, sendDataLen uint32, bytesSent *uint32, overlapped *windows.Overlapped, ) (err error) { r1, _, e1 := syscall.SyscallN(connectExFunc.addr, uintptr(s), uintptr(name), uintptr(namelen), uintptr(unsafe.Pointer(sendBuf)), uintptr(sendDataLen), uintptr(unsafe.Pointer(bytesSent)), uintptr(unsafe.Pointer(overlapped)), ) if r1 == 0 { if e1 != 0 { err = error(e1) } else { err = syscall.EINVAL } } return err } ================================================ FILE: vendor/github.com/Microsoft/go-winio/internal/socket/zsyscall_windows.go ================================================ //go:build windows // Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT. package socket import ( "syscall" "unsafe" "golang.org/x/sys/windows" ) var _ unsafe.Pointer // Do the interface allocations only once for common // Errno values. const ( errnoERROR_IO_PENDING = 997 ) var ( errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) errERROR_EINVAL error = syscall.EINVAL ) // errnoErr returns common boxed Errno values, to prevent // allocations at runtime. func errnoErr(e syscall.Errno) error { switch e { case 0: return errERROR_EINVAL case errnoERROR_IO_PENDING: return errERROR_IO_PENDING } return e } var ( modws2_32 = windows.NewLazySystemDLL("ws2_32.dll") procbind = modws2_32.NewProc("bind") procgetpeername = modws2_32.NewProc("getpeername") procgetsockname = modws2_32.NewProc("getsockname") ) func bind(s windows.Handle, name unsafe.Pointer, namelen int32) (err error) { r1, _, e1 := syscall.SyscallN(procbind.Addr(), uintptr(s), uintptr(name), uintptr(namelen)) if r1 == socketError { err = errnoErr(e1) } return } func getpeername(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) { r1, _, e1 := syscall.SyscallN(procgetpeername.Addr(), uintptr(s), uintptr(name), uintptr(unsafe.Pointer(namelen))) if r1 == socketError { err = errnoErr(e1) } return } func getsockname(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) { r1, _, e1 := syscall.SyscallN(procgetsockname.Addr(), uintptr(s), uintptr(name), uintptr(unsafe.Pointer(namelen))) if r1 == socketError { err = errnoErr(e1) } return } ================================================ FILE: vendor/github.com/Microsoft/go-winio/internal/stringbuffer/wstring.go ================================================ package stringbuffer import ( "sync" "unicode/utf16" ) // TODO: worth exporting and using in mkwinsyscall? // Uint16BufferSize is the buffer size in the pool, chosen somewhat arbitrarily to accommodate // large path strings: // MAX_PATH (260) + size of volume GUID prefix (49) + null terminator = 310. const MinWStringCap = 310 // use *[]uint16 since []uint16 creates an extra allocation where the slice header // is copied to heap and then referenced via pointer in the interface header that sync.Pool // stores. var pathPool = sync.Pool{ // if go1.18+ adds Pool[T], use that to store []uint16 directly New: func() interface{} { b := make([]uint16, MinWStringCap) return &b }, } func newBuffer() []uint16 { return *(pathPool.Get().(*[]uint16)) } // freeBuffer copies the slice header data, and puts a pointer to that in the pool. // This avoids taking a pointer to the slice header in WString, which can be set to nil. func freeBuffer(b []uint16) { pathPool.Put(&b) } // WString is a wide string buffer ([]uint16) meant for storing UTF-16 encoded strings // for interacting with Win32 APIs. // Sizes are specified as uint32 and not int. // // It is not thread safe. type WString struct { // type-def allows casting to []uint16 directly, use struct to prevent that and allow adding fields in the future. // raw buffer b []uint16 } // NewWString returns a [WString] allocated from a shared pool with an // initial capacity of at least [MinWStringCap]. // Since the buffer may have been previously used, its contents are not guaranteed to be empty. // // The buffer should be freed via [WString.Free] func NewWString() *WString { return &WString{ b: newBuffer(), } } func (b *WString) Free() { if b.empty() { return } freeBuffer(b.b) b.b = nil } // ResizeTo grows the buffer to at least c and returns the new capacity, freeing the // previous buffer back into pool. func (b *WString) ResizeTo(c uint32) uint32 { // already sufficient (or n is 0) if c <= b.Cap() { return b.Cap() } if c <= MinWStringCap { c = MinWStringCap } // allocate at-least double buffer size, as is done in [bytes.Buffer] and other places if c <= 2*b.Cap() { c = 2 * b.Cap() } b2 := make([]uint16, c) if !b.empty() { copy(b2, b.b) freeBuffer(b.b) } b.b = b2 return c } // Buffer returns the underlying []uint16 buffer. func (b *WString) Buffer() []uint16 { if b.empty() { return nil } return b.b } // Pointer returns a pointer to the first uint16 in the buffer. // If the [WString.Free] has already been called, the pointer will be nil. func (b *WString) Pointer() *uint16 { if b.empty() { return nil } return &b.b[0] } // String returns the returns the UTF-8 encoding of the UTF-16 string in the buffer. // // It assumes that the data is null-terminated. func (b *WString) String() string { // Using [windows.UTF16ToString] would require importing "golang.org/x/sys/windows" // and would make this code Windows-only, which makes no sense. // So copy UTF16ToString code into here. // If other windows-specific code is added, switch to [windows.UTF16ToString] s := b.b for i, v := range s { if v == 0 { s = s[:i] break } } return string(utf16.Decode(s)) } // Cap returns the underlying buffer capacity. func (b *WString) Cap() uint32 { if b.empty() { return 0 } return b.cap() } func (b *WString) cap() uint32 { return uint32(cap(b.b)) } func (b *WString) empty() bool { return b == nil || b.cap() == 0 } ================================================ FILE: vendor/github.com/Microsoft/go-winio/pipe.go ================================================ //go:build windows // +build windows package winio import ( "context" "errors" "fmt" "io" "net" "os" "runtime" "time" "unsafe" "golang.org/x/sys/windows" "github.com/Microsoft/go-winio/internal/fs" ) //sys connectNamedPipe(pipe windows.Handle, o *windows.Overlapped) (err error) = ConnectNamedPipe //sys createNamedPipe(name string, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *windows.SecurityAttributes) (handle windows.Handle, err error) [failretval==windows.InvalidHandle] = CreateNamedPipeW //sys disconnectNamedPipe(pipe windows.Handle) (err error) = DisconnectNamedPipe //sys getNamedPipeInfo(pipe windows.Handle, flags *uint32, outSize *uint32, inSize *uint32, maxInstances *uint32) (err error) = GetNamedPipeInfo //sys getNamedPipeHandleState(pipe windows.Handle, state *uint32, curInstances *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32, userName *uint16, maxUserNameSize uint32) (err error) = GetNamedPipeHandleStateW //sys ntCreateNamedPipeFile(pipe *windows.Handle, access ntAccessMask, oa *objectAttributes, iosb *ioStatusBlock, share ntFileShareMode, disposition ntFileCreationDisposition, options ntFileOptions, typ uint32, readMode uint32, completionMode uint32, maxInstances uint32, inboundQuota uint32, outputQuota uint32, timeout *int64) (status ntStatus) = ntdll.NtCreateNamedPipeFile //sys rtlNtStatusToDosError(status ntStatus) (winerr error) = ntdll.RtlNtStatusToDosErrorNoTeb //sys rtlDosPathNameToNtPathName(name *uint16, ntName *unicodeString, filePart uintptr, reserved uintptr) (status ntStatus) = ntdll.RtlDosPathNameToNtPathName_U //sys rtlDefaultNpAcl(dacl *uintptr) (status ntStatus) = ntdll.RtlDefaultNpAcl type PipeConn interface { net.Conn Disconnect() error Flush() error } // type aliases for mkwinsyscall code type ( ntAccessMask = fs.AccessMask ntFileShareMode = fs.FileShareMode ntFileCreationDisposition = fs.NTFileCreationDisposition ntFileOptions = fs.NTCreateOptions ) type ioStatusBlock struct { Status, Information uintptr } // typedef struct _OBJECT_ATTRIBUTES { // ULONG Length; // HANDLE RootDirectory; // PUNICODE_STRING ObjectName; // ULONG Attributes; // PVOID SecurityDescriptor; // PVOID SecurityQualityOfService; // } OBJECT_ATTRIBUTES; // // https://learn.microsoft.com/en-us/windows/win32/api/ntdef/ns-ntdef-_object_attributes type objectAttributes struct { Length uintptr RootDirectory uintptr ObjectName *unicodeString Attributes uintptr SecurityDescriptor *securityDescriptor SecurityQoS uintptr } type unicodeString struct { Length uint16 MaximumLength uint16 Buffer uintptr } // typedef struct _SECURITY_DESCRIPTOR { // BYTE Revision; // BYTE Sbz1; // SECURITY_DESCRIPTOR_CONTROL Control; // PSID Owner; // PSID Group; // PACL Sacl; // PACL Dacl; // } SECURITY_DESCRIPTOR, *PISECURITY_DESCRIPTOR; // // https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-security_descriptor type securityDescriptor struct { Revision byte Sbz1 byte Control uint16 Owner uintptr Group uintptr Sacl uintptr //revive:disable-line:var-naming SACL, not Sacl Dacl uintptr //revive:disable-line:var-naming DACL, not Dacl } type ntStatus int32 func (status ntStatus) Err() error { if status >= 0 { return nil } return rtlNtStatusToDosError(status) } var ( // ErrPipeListenerClosed is returned for pipe operations on listeners that have been closed. ErrPipeListenerClosed = net.ErrClosed errPipeWriteClosed = errors.New("pipe has been closed for write") ) type win32Pipe struct { *win32File path string } var _ PipeConn = (*win32Pipe)(nil) type win32MessageBytePipe struct { win32Pipe writeClosed bool readEOF bool } type pipeAddress string func (f *win32Pipe) LocalAddr() net.Addr { return pipeAddress(f.path) } func (f *win32Pipe) RemoteAddr() net.Addr { return pipeAddress(f.path) } func (f *win32Pipe) SetDeadline(t time.Time) error { if err := f.SetReadDeadline(t); err != nil { return err } return f.SetWriteDeadline(t) } func (f *win32Pipe) Disconnect() error { return disconnectNamedPipe(f.win32File.handle) } // CloseWrite closes the write side of a message pipe in byte mode. func (f *win32MessageBytePipe) CloseWrite() error { if f.writeClosed { return errPipeWriteClosed } err := f.win32File.Flush() if err != nil { return err } _, err = f.win32File.Write(nil) if err != nil { return err } f.writeClosed = true return nil } // Write writes bytes to a message pipe in byte mode. Zero-byte writes are ignored, since // they are used to implement CloseWrite(). func (f *win32MessageBytePipe) Write(b []byte) (int, error) { if f.writeClosed { return 0, errPipeWriteClosed } if len(b) == 0 { return 0, nil } return f.win32File.Write(b) } // Read reads bytes from a message pipe in byte mode. A read of a zero-byte message on a message // mode pipe will return io.EOF, as will all subsequent reads. func (f *win32MessageBytePipe) Read(b []byte) (int, error) { if f.readEOF { return 0, io.EOF } n, err := f.win32File.Read(b) if err == io.EOF { //nolint:errorlint // If this was the result of a zero-byte read, then // it is possible that the read was due to a zero-size // message. Since we are simulating CloseWrite with a // zero-byte message, ensure that all future Read() calls // also return EOF. f.readEOF = true } else if err == windows.ERROR_MORE_DATA { //nolint:errorlint // err is Errno // ERROR_MORE_DATA indicates that the pipe's read mode is message mode // and the message still has more bytes. Treat this as a success, since // this package presents all named pipes as byte streams. err = nil } return n, err } func (pipeAddress) Network() string { return "pipe" } func (s pipeAddress) String() string { return string(s) } // tryDialPipe attempts to dial the pipe at `path` until `ctx` cancellation or timeout. func tryDialPipe(ctx context.Context, path *string, access fs.AccessMask, impLevel PipeImpLevel) (windows.Handle, error) { for { select { case <-ctx.Done(): return windows.Handle(0), ctx.Err() default: h, err := fs.CreateFile(*path, access, 0, // mode nil, // security attributes fs.OPEN_EXISTING, fs.FILE_FLAG_OVERLAPPED|fs.SECURITY_SQOS_PRESENT|fs.FileSQSFlag(impLevel), 0, // template file handle ) if err == nil { return h, nil } if err != windows.ERROR_PIPE_BUSY { //nolint:errorlint // err is Errno return h, &os.PathError{Err: err, Op: "open", Path: *path} } // Wait 10 msec and try again. This is a rather simplistic // view, as we always try each 10 milliseconds. time.Sleep(10 * time.Millisecond) } } } // DialPipe connects to a named pipe by path, timing out if the connection // takes longer than the specified duration. If timeout is nil, then we use // a default timeout of 2 seconds. (We do not use WaitNamedPipe.) func DialPipe(path string, timeout *time.Duration) (net.Conn, error) { var absTimeout time.Time if timeout != nil { absTimeout = time.Now().Add(*timeout) } else { absTimeout = time.Now().Add(2 * time.Second) } ctx, cancel := context.WithDeadline(context.Background(), absTimeout) defer cancel() conn, err := DialPipeContext(ctx, path) if errors.Is(err, context.DeadlineExceeded) { return nil, ErrTimeout } return conn, err } // DialPipeContext attempts to connect to a named pipe by `path` until `ctx` // cancellation or timeout. func DialPipeContext(ctx context.Context, path string) (net.Conn, error) { return DialPipeAccess(ctx, path, uint32(fs.GENERIC_READ|fs.GENERIC_WRITE)) } // PipeImpLevel is an enumeration of impersonation levels that may be set // when calling DialPipeAccessImpersonation. type PipeImpLevel uint32 const ( PipeImpLevelAnonymous = PipeImpLevel(fs.SECURITY_ANONYMOUS) PipeImpLevelIdentification = PipeImpLevel(fs.SECURITY_IDENTIFICATION) PipeImpLevelImpersonation = PipeImpLevel(fs.SECURITY_IMPERSONATION) PipeImpLevelDelegation = PipeImpLevel(fs.SECURITY_DELEGATION) ) // DialPipeAccess attempts to connect to a named pipe by `path` with `access` until `ctx` // cancellation or timeout. func DialPipeAccess(ctx context.Context, path string, access uint32) (net.Conn, error) { return DialPipeAccessImpLevel(ctx, path, access, PipeImpLevelAnonymous) } // DialPipeAccessImpLevel attempts to connect to a named pipe by `path` with // `access` at `impLevel` until `ctx` cancellation or timeout. The other // DialPipe* implementations use PipeImpLevelAnonymous. func DialPipeAccessImpLevel(ctx context.Context, path string, access uint32, impLevel PipeImpLevel) (net.Conn, error) { var err error var h windows.Handle h, err = tryDialPipe(ctx, &path, fs.AccessMask(access), impLevel) if err != nil { return nil, err } var flags uint32 err = getNamedPipeInfo(h, &flags, nil, nil, nil) if err != nil { return nil, err } f, err := makeWin32File(h) if err != nil { windows.Close(h) return nil, err } // If the pipe is in message mode, return a message byte pipe, which // supports CloseWrite(). if flags&windows.PIPE_TYPE_MESSAGE != 0 { return &win32MessageBytePipe{ win32Pipe: win32Pipe{win32File: f, path: path}, }, nil } return &win32Pipe{win32File: f, path: path}, nil } type acceptResponse struct { f *win32File err error } type win32PipeListener struct { firstHandle windows.Handle path string config PipeConfig acceptCh chan (chan acceptResponse) closeCh chan int doneCh chan int } func makeServerPipeHandle(path string, sd []byte, c *PipeConfig, first bool) (windows.Handle, error) { path16, err := windows.UTF16FromString(path) if err != nil { return 0, &os.PathError{Op: "open", Path: path, Err: err} } var oa objectAttributes oa.Length = unsafe.Sizeof(oa) var ntPath unicodeString if err := rtlDosPathNameToNtPathName(&path16[0], &ntPath, 0, 0, ).Err(); err != nil { return 0, &os.PathError{Op: "open", Path: path, Err: err} } defer windows.LocalFree(windows.Handle(ntPath.Buffer)) //nolint:errcheck oa.ObjectName = &ntPath oa.Attributes = windows.OBJ_CASE_INSENSITIVE // The security descriptor is only needed for the first pipe. if first { if sd != nil { //todo: does `sdb` need to be allocated on the heap, or can go allocate it? l := uint32(len(sd)) sdb, err := windows.LocalAlloc(0, l) if err != nil { return 0, fmt.Errorf("LocalAlloc for security descriptor with of length %d: %w", l, err) } defer windows.LocalFree(windows.Handle(sdb)) //nolint:errcheck copy((*[0xffff]byte)(unsafe.Pointer(sdb))[:], sd) oa.SecurityDescriptor = (*securityDescriptor)(unsafe.Pointer(sdb)) } else { // Construct the default named pipe security descriptor. var dacl uintptr if err := rtlDefaultNpAcl(&dacl).Err(); err != nil { return 0, fmt.Errorf("getting default named pipe ACL: %w", err) } defer windows.LocalFree(windows.Handle(dacl)) //nolint:errcheck sdb := &securityDescriptor{ Revision: 1, Control: windows.SE_DACL_PRESENT, Dacl: dacl, } oa.SecurityDescriptor = sdb } } typ := uint32(windows.FILE_PIPE_REJECT_REMOTE_CLIENTS) if c.MessageMode { typ |= windows.FILE_PIPE_MESSAGE_TYPE } disposition := fs.FILE_OPEN access := fs.GENERIC_READ | fs.GENERIC_WRITE | fs.SYNCHRONIZE if first { disposition = fs.FILE_CREATE // By not asking for read or write access, the named pipe file system // will put this pipe into an initially disconnected state, blocking // client connections until the next call with first == false. access = fs.SYNCHRONIZE } timeout := int64(-50 * 10000) // 50ms var ( h windows.Handle iosb ioStatusBlock ) err = ntCreateNamedPipeFile(&h, access, &oa, &iosb, fs.FILE_SHARE_READ|fs.FILE_SHARE_WRITE, disposition, 0, typ, 0, 0, 0xffffffff, uint32(c.InputBufferSize), uint32(c.OutputBufferSize), &timeout).Err() if err != nil { return 0, &os.PathError{Op: "open", Path: path, Err: err} } runtime.KeepAlive(ntPath) return h, nil } func (l *win32PipeListener) makeServerPipe() (*win32File, error) { h, err := makeServerPipeHandle(l.path, nil, &l.config, false) if err != nil { return nil, err } f, err := makeWin32File(h) if err != nil { windows.Close(h) return nil, err } return f, nil } func (l *win32PipeListener) makeConnectedServerPipe() (*win32File, error) { p, err := l.makeServerPipe() if err != nil { return nil, err } // Wait for the client to connect. ch := make(chan error) go func(p *win32File) { ch <- connectPipe(p) }(p) select { case err = <-ch: if err != nil { p.Close() p = nil } case <-l.closeCh: // Abort the connect request by closing the handle. p.Close() p = nil err = <-ch if err == nil || err == ErrFileClosed { //nolint:errorlint // err is Errno err = ErrPipeListenerClosed } } return p, err } func (l *win32PipeListener) listenerRoutine() { closed := false for !closed { select { case <-l.closeCh: closed = true case responseCh := <-l.acceptCh: var ( p *win32File err error ) for { p, err = l.makeConnectedServerPipe() // If the connection was immediately closed by the client, try // again. if err != windows.ERROR_NO_DATA { //nolint:errorlint // err is Errno break } } responseCh <- acceptResponse{p, err} closed = err == ErrPipeListenerClosed //nolint:errorlint // err is Errno } } windows.Close(l.firstHandle) l.firstHandle = 0 // Notify Close() and Accept() callers that the handle has been closed. close(l.doneCh) } // PipeConfig contain configuration for the pipe listener. type PipeConfig struct { // SecurityDescriptor contains a Windows security descriptor in SDDL format. SecurityDescriptor string // MessageMode determines whether the pipe is in byte or message mode. In either // case the pipe is read in byte mode by default. The only practical difference in // this implementation is that CloseWrite() is only supported for message mode pipes; // CloseWrite() is implemented as a zero-byte write, but zero-byte writes are only // transferred to the reader (and returned as io.EOF in this implementation) // when the pipe is in message mode. MessageMode bool // InputBufferSize specifies the size of the input buffer, in bytes. InputBufferSize int32 // OutputBufferSize specifies the size of the output buffer, in bytes. OutputBufferSize int32 } // ListenPipe creates a listener on a Windows named pipe path, e.g. \\.\pipe\mypipe. // The pipe must not already exist. func ListenPipe(path string, c *PipeConfig) (net.Listener, error) { var ( sd []byte err error ) if c == nil { c = &PipeConfig{} } if c.SecurityDescriptor != "" { sd, err = SddlToSecurityDescriptor(c.SecurityDescriptor) if err != nil { return nil, err } } h, err := makeServerPipeHandle(path, sd, c, true) if err != nil { return nil, err } l := &win32PipeListener{ firstHandle: h, path: path, config: *c, acceptCh: make(chan (chan acceptResponse)), closeCh: make(chan int), doneCh: make(chan int), } go l.listenerRoutine() return l, nil } func connectPipe(p *win32File) error { c, err := p.prepareIO() if err != nil { return err } defer p.wg.Done() err = connectNamedPipe(p.handle, &c.o) _, err = p.asyncIO(c, nil, 0, err) if err != nil && err != windows.ERROR_PIPE_CONNECTED { //nolint:errorlint // err is Errno return err } return nil } func (l *win32PipeListener) Accept() (net.Conn, error) { ch := make(chan acceptResponse) select { case l.acceptCh <- ch: response := <-ch err := response.err if err != nil { return nil, err } if l.config.MessageMode { return &win32MessageBytePipe{ win32Pipe: win32Pipe{win32File: response.f, path: l.path}, }, nil } return &win32Pipe{win32File: response.f, path: l.path}, nil case <-l.doneCh: return nil, ErrPipeListenerClosed } } func (l *win32PipeListener) Close() error { select { case l.closeCh <- 1: <-l.doneCh case <-l.doneCh: } return nil } func (l *win32PipeListener) Addr() net.Addr { return pipeAddress(l.path) } ================================================ FILE: vendor/github.com/Microsoft/go-winio/pkg/guid/guid.go ================================================ // Package guid provides a GUID type. The backing structure for a GUID is // identical to that used by the golang.org/x/sys/windows GUID type. // There are two main binary encodings used for a GUID, the big-endian encoding, // and the Windows (mixed-endian) encoding. See here for details: // https://en.wikipedia.org/wiki/Universally_unique_identifier#Encoding package guid import ( "crypto/rand" "crypto/sha1" //nolint:gosec // not used for secure application "encoding" "encoding/binary" "fmt" "strconv" ) //go:generate go run golang.org/x/tools/cmd/stringer -type=Variant -trimprefix=Variant -linecomment // Variant specifies which GUID variant (or "type") of the GUID. It determines // how the entirety of the rest of the GUID is interpreted. type Variant uint8 // The variants specified by RFC 4122 section 4.1.1. const ( // VariantUnknown specifies a GUID variant which does not conform to one of // the variant encodings specified in RFC 4122. VariantUnknown Variant = iota VariantNCS VariantRFC4122 // RFC 4122 VariantMicrosoft VariantFuture ) // Version specifies how the bits in the GUID were generated. For instance, a // version 4 GUID is randomly generated, and a version 5 is generated from the // hash of an input string. type Version uint8 func (v Version) String() string { return strconv.FormatUint(uint64(v), 10) } var _ = (encoding.TextMarshaler)(GUID{}) var _ = (encoding.TextUnmarshaler)(&GUID{}) // NewV4 returns a new version 4 (pseudorandom) GUID, as defined by RFC 4122. func NewV4() (GUID, error) { var b [16]byte if _, err := rand.Read(b[:]); err != nil { return GUID{}, err } g := FromArray(b) g.setVersion(4) // Version 4 means randomly generated. g.setVariant(VariantRFC4122) return g, nil } // NewV5 returns a new version 5 (generated from a string via SHA-1 hashing) // GUID, as defined by RFC 4122. The RFC is unclear on the encoding of the name, // and the sample code treats it as a series of bytes, so we do the same here. // // Some implementations, such as those found on Windows, treat the name as a // big-endian UTF16 stream of bytes. If that is desired, the string can be // encoded as such before being passed to this function. func NewV5(namespace GUID, name []byte) (GUID, error) { b := sha1.New() //nolint:gosec // not used for secure application namespaceBytes := namespace.ToArray() b.Write(namespaceBytes[:]) b.Write(name) a := [16]byte{} copy(a[:], b.Sum(nil)) g := FromArray(a) g.setVersion(5) // Version 5 means generated from a string. g.setVariant(VariantRFC4122) return g, nil } func fromArray(b [16]byte, order binary.ByteOrder) GUID { var g GUID g.Data1 = order.Uint32(b[0:4]) g.Data2 = order.Uint16(b[4:6]) g.Data3 = order.Uint16(b[6:8]) copy(g.Data4[:], b[8:16]) return g } func (g GUID) toArray(order binary.ByteOrder) [16]byte { b := [16]byte{} order.PutUint32(b[0:4], g.Data1) order.PutUint16(b[4:6], g.Data2) order.PutUint16(b[6:8], g.Data3) copy(b[8:16], g.Data4[:]) return b } // FromArray constructs a GUID from a big-endian encoding array of 16 bytes. func FromArray(b [16]byte) GUID { return fromArray(b, binary.BigEndian) } // ToArray returns an array of 16 bytes representing the GUID in big-endian // encoding. func (g GUID) ToArray() [16]byte { return g.toArray(binary.BigEndian) } // FromWindowsArray constructs a GUID from a Windows encoding array of bytes. func FromWindowsArray(b [16]byte) GUID { return fromArray(b, binary.LittleEndian) } // ToWindowsArray returns an array of 16 bytes representing the GUID in Windows // encoding. func (g GUID) ToWindowsArray() [16]byte { return g.toArray(binary.LittleEndian) } func (g GUID) String() string { return fmt.Sprintf( "%08x-%04x-%04x-%04x-%012x", g.Data1, g.Data2, g.Data3, g.Data4[:2], g.Data4[2:]) } // FromString parses a string containing a GUID and returns the GUID. The only // format currently supported is the `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` // format. func FromString(s string) (GUID, error) { if len(s) != 36 { return GUID{}, fmt.Errorf("invalid GUID %q", s) } if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' { return GUID{}, fmt.Errorf("invalid GUID %q", s) } var g GUID data1, err := strconv.ParseUint(s[0:8], 16, 32) if err != nil { return GUID{}, fmt.Errorf("invalid GUID %q", s) } g.Data1 = uint32(data1) data2, err := strconv.ParseUint(s[9:13], 16, 16) if err != nil { return GUID{}, fmt.Errorf("invalid GUID %q", s) } g.Data2 = uint16(data2) data3, err := strconv.ParseUint(s[14:18], 16, 16) if err != nil { return GUID{}, fmt.Errorf("invalid GUID %q", s) } g.Data3 = uint16(data3) for i, x := range []int{19, 21, 24, 26, 28, 30, 32, 34} { v, err := strconv.ParseUint(s[x:x+2], 16, 8) if err != nil { return GUID{}, fmt.Errorf("invalid GUID %q", s) } g.Data4[i] = uint8(v) } return g, nil } func (g *GUID) setVariant(v Variant) { d := g.Data4[0] switch v { case VariantNCS: d = (d & 0x7f) case VariantRFC4122: d = (d & 0x3f) | 0x80 case VariantMicrosoft: d = (d & 0x1f) | 0xc0 case VariantFuture: d = (d & 0x0f) | 0xe0 case VariantUnknown: fallthrough default: panic(fmt.Sprintf("invalid variant: %d", v)) } g.Data4[0] = d } // Variant returns the GUID variant, as defined in RFC 4122. func (g GUID) Variant() Variant { b := g.Data4[0] if b&0x80 == 0 { return VariantNCS } else if b&0xc0 == 0x80 { return VariantRFC4122 } else if b&0xe0 == 0xc0 { return VariantMicrosoft } else if b&0xe0 == 0xe0 { return VariantFuture } return VariantUnknown } func (g *GUID) setVersion(v Version) { g.Data3 = (g.Data3 & 0x0fff) | (uint16(v) << 12) } // Version returns the GUID version, as defined in RFC 4122. func (g GUID) Version() Version { return Version((g.Data3 & 0xF000) >> 12) } // MarshalText returns the textual representation of the GUID. func (g GUID) MarshalText() ([]byte, error) { return []byte(g.String()), nil } // UnmarshalText takes the textual representation of a GUID, and unmarhals it // into this GUID. func (g *GUID) UnmarshalText(text []byte) error { g2, err := FromString(string(text)) if err != nil { return err } *g = g2 return nil } ================================================ FILE: vendor/github.com/Microsoft/go-winio/pkg/guid/guid_nonwindows.go ================================================ //go:build !windows // +build !windows package guid // GUID represents a GUID/UUID. It has the same structure as // golang.org/x/sys/windows.GUID so that it can be used with functions expecting // that type. It is defined as its own type as that is only available to builds // targeted at `windows`. The representation matches that used by native Windows // code. type GUID struct { Data1 uint32 Data2 uint16 Data3 uint16 Data4 [8]byte } ================================================ FILE: vendor/github.com/Microsoft/go-winio/pkg/guid/guid_windows.go ================================================ //go:build windows // +build windows package guid import "golang.org/x/sys/windows" // GUID represents a GUID/UUID. It has the same structure as // golang.org/x/sys/windows.GUID so that it can be used with functions expecting // that type. It is defined as its own type so that stringification and // marshaling can be supported. The representation matches that used by native // Windows code. type GUID windows.GUID ================================================ FILE: vendor/github.com/Microsoft/go-winio/pkg/guid/variant_string.go ================================================ // Code generated by "stringer -type=Variant -trimprefix=Variant -linecomment"; DO NOT EDIT. package guid import "strconv" func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} _ = x[VariantUnknown-0] _ = x[VariantNCS-1] _ = x[VariantRFC4122-2] _ = x[VariantMicrosoft-3] _ = x[VariantFuture-4] } const _Variant_name = "UnknownNCSRFC 4122MicrosoftFuture" var _Variant_index = [...]uint8{0, 7, 10, 18, 27, 33} func (i Variant) String() string { if i >= Variant(len(_Variant_index)-1) { return "Variant(" + strconv.FormatInt(int64(i), 10) + ")" } return _Variant_name[_Variant_index[i]:_Variant_index[i+1]] } ================================================ FILE: vendor/github.com/Microsoft/go-winio/privilege.go ================================================ //go:build windows // +build windows package winio import ( "bytes" "encoding/binary" "fmt" "runtime" "sync" "unicode/utf16" "golang.org/x/sys/windows" ) //sys adjustTokenPrivileges(token windows.Token, releaseAll bool, input *byte, outputSize uint32, output *byte, requiredSize *uint32) (success bool, err error) [true] = advapi32.AdjustTokenPrivileges //sys impersonateSelf(level uint32) (err error) = advapi32.ImpersonateSelf //sys revertToSelf() (err error) = advapi32.RevertToSelf //sys openThreadToken(thread windows.Handle, accessMask uint32, openAsSelf bool, token *windows.Token) (err error) = advapi32.OpenThreadToken //sys getCurrentThread() (h windows.Handle) = GetCurrentThread //sys lookupPrivilegeValue(systemName string, name string, luid *uint64) (err error) = advapi32.LookupPrivilegeValueW //sys lookupPrivilegeName(systemName string, luid *uint64, buffer *uint16, size *uint32) (err error) = advapi32.LookupPrivilegeNameW //sys lookupPrivilegeDisplayName(systemName string, name *uint16, buffer *uint16, size *uint32, languageId *uint32) (err error) = advapi32.LookupPrivilegeDisplayNameW const ( //revive:disable-next-line:var-naming ALL_CAPS SE_PRIVILEGE_ENABLED = windows.SE_PRIVILEGE_ENABLED //revive:disable-next-line:var-naming ALL_CAPS ERROR_NOT_ALL_ASSIGNED windows.Errno = windows.ERROR_NOT_ALL_ASSIGNED SeBackupPrivilege = "SeBackupPrivilege" SeRestorePrivilege = "SeRestorePrivilege" SeSecurityPrivilege = "SeSecurityPrivilege" ) var ( privNames = make(map[string]uint64) privNameMutex sync.Mutex ) // PrivilegeError represents an error enabling privileges. type PrivilegeError struct { privileges []uint64 } func (e *PrivilegeError) Error() string { s := "Could not enable privilege " if len(e.privileges) > 1 { s = "Could not enable privileges " } for i, p := range e.privileges { if i != 0 { s += ", " } s += `"` s += getPrivilegeName(p) s += `"` } return s } // RunWithPrivilege enables a single privilege for a function call. func RunWithPrivilege(name string, fn func() error) error { return RunWithPrivileges([]string{name}, fn) } // RunWithPrivileges enables privileges for a function call. func RunWithPrivileges(names []string, fn func() error) error { privileges, err := mapPrivileges(names) if err != nil { return err } runtime.LockOSThread() defer runtime.UnlockOSThread() token, err := newThreadToken() if err != nil { return err } defer releaseThreadToken(token) err = adjustPrivileges(token, privileges, SE_PRIVILEGE_ENABLED) if err != nil { return err } return fn() } func mapPrivileges(names []string) ([]uint64, error) { privileges := make([]uint64, 0, len(names)) privNameMutex.Lock() defer privNameMutex.Unlock() for _, name := range names { p, ok := privNames[name] if !ok { err := lookupPrivilegeValue("", name, &p) if err != nil { return nil, err } privNames[name] = p } privileges = append(privileges, p) } return privileges, nil } // EnableProcessPrivileges enables privileges globally for the process. func EnableProcessPrivileges(names []string) error { return enableDisableProcessPrivilege(names, SE_PRIVILEGE_ENABLED) } // DisableProcessPrivileges disables privileges globally for the process. func DisableProcessPrivileges(names []string) error { return enableDisableProcessPrivilege(names, 0) } func enableDisableProcessPrivilege(names []string, action uint32) error { privileges, err := mapPrivileges(names) if err != nil { return err } p := windows.CurrentProcess() var token windows.Token err = windows.OpenProcessToken(p, windows.TOKEN_ADJUST_PRIVILEGES|windows.TOKEN_QUERY, &token) if err != nil { return err } defer token.Close() return adjustPrivileges(token, privileges, action) } func adjustPrivileges(token windows.Token, privileges []uint64, action uint32) error { var b bytes.Buffer _ = binary.Write(&b, binary.LittleEndian, uint32(len(privileges))) for _, p := range privileges { _ = binary.Write(&b, binary.LittleEndian, p) _ = binary.Write(&b, binary.LittleEndian, action) } prevState := make([]byte, b.Len()) reqSize := uint32(0) success, err := adjustTokenPrivileges(token, false, &b.Bytes()[0], uint32(len(prevState)), &prevState[0], &reqSize) if !success { return err } if err == ERROR_NOT_ALL_ASSIGNED { //nolint:errorlint // err is Errno return &PrivilegeError{privileges} } return nil } func getPrivilegeName(luid uint64) string { var nameBuffer [256]uint16 bufSize := uint32(len(nameBuffer)) err := lookupPrivilegeName("", &luid, &nameBuffer[0], &bufSize) if err != nil { return fmt.Sprintf("", luid) } var displayNameBuffer [256]uint16 displayBufSize := uint32(len(displayNameBuffer)) var langID uint32 err = lookupPrivilegeDisplayName("", &nameBuffer[0], &displayNameBuffer[0], &displayBufSize, &langID) if err != nil { return fmt.Sprintf("", string(utf16.Decode(nameBuffer[:bufSize]))) } return string(utf16.Decode(displayNameBuffer[:displayBufSize])) } func newThreadToken() (windows.Token, error) { err := impersonateSelf(windows.SecurityImpersonation) if err != nil { return 0, err } var token windows.Token err = openThreadToken(getCurrentThread(), windows.TOKEN_ADJUST_PRIVILEGES|windows.TOKEN_QUERY, false, &token) if err != nil { rerr := revertToSelf() if rerr != nil { panic(rerr) } return 0, err } return token, nil } func releaseThreadToken(h windows.Token) { err := revertToSelf() if err != nil { panic(err) } h.Close() } ================================================ FILE: vendor/github.com/Microsoft/go-winio/reparse.go ================================================ //go:build windows // +build windows package winio import ( "bytes" "encoding/binary" "fmt" "strings" "unicode/utf16" "unsafe" ) const ( reparseTagMountPoint = 0xA0000003 reparseTagSymlink = 0xA000000C ) type reparseDataBuffer struct { ReparseTag uint32 ReparseDataLength uint16 Reserved uint16 SubstituteNameOffset uint16 SubstituteNameLength uint16 PrintNameOffset uint16 PrintNameLength uint16 } // ReparsePoint describes a Win32 symlink or mount point. type ReparsePoint struct { Target string IsMountPoint bool } // UnsupportedReparsePointError is returned when trying to decode a non-symlink or // mount point reparse point. type UnsupportedReparsePointError struct { Tag uint32 } func (e *UnsupportedReparsePointError) Error() string { return fmt.Sprintf("unsupported reparse point %x", e.Tag) } // DecodeReparsePoint decodes a Win32 REPARSE_DATA_BUFFER structure containing either a symlink // or a mount point. func DecodeReparsePoint(b []byte) (*ReparsePoint, error) { tag := binary.LittleEndian.Uint32(b[0:4]) return DecodeReparsePointData(tag, b[8:]) } func DecodeReparsePointData(tag uint32, b []byte) (*ReparsePoint, error) { isMountPoint := false switch tag { case reparseTagMountPoint: isMountPoint = true case reparseTagSymlink: default: return nil, &UnsupportedReparsePointError{tag} } nameOffset := 8 + binary.LittleEndian.Uint16(b[4:6]) if !isMountPoint { nameOffset += 4 } nameLength := binary.LittleEndian.Uint16(b[6:8]) name := make([]uint16, nameLength/2) err := binary.Read(bytes.NewReader(b[nameOffset:nameOffset+nameLength]), binary.LittleEndian, &name) if err != nil { return nil, err } return &ReparsePoint{string(utf16.Decode(name)), isMountPoint}, nil } func isDriveLetter(c byte) bool { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') } // EncodeReparsePoint encodes a Win32 REPARSE_DATA_BUFFER structure describing a symlink or // mount point. func EncodeReparsePoint(rp *ReparsePoint) []byte { // Generate an NT path and determine if this is a relative path. var ntTarget string relative := false if strings.HasPrefix(rp.Target, `\\?\`) { ntTarget = `\??\` + rp.Target[4:] } else if strings.HasPrefix(rp.Target, `\\`) { ntTarget = `\??\UNC\` + rp.Target[2:] } else if len(rp.Target) >= 2 && isDriveLetter(rp.Target[0]) && rp.Target[1] == ':' { ntTarget = `\??\` + rp.Target } else { ntTarget = rp.Target relative = true } // The paths must be NUL-terminated even though they are counted strings. target16 := utf16.Encode([]rune(rp.Target + "\x00")) ntTarget16 := utf16.Encode([]rune(ntTarget + "\x00")) size := int(unsafe.Sizeof(reparseDataBuffer{})) - 8 size += len(ntTarget16)*2 + len(target16)*2 tag := uint32(reparseTagMountPoint) if !rp.IsMountPoint { tag = reparseTagSymlink size += 4 // Add room for symlink flags } data := reparseDataBuffer{ ReparseTag: tag, ReparseDataLength: uint16(size), SubstituteNameOffset: 0, SubstituteNameLength: uint16((len(ntTarget16) - 1) * 2), PrintNameOffset: uint16(len(ntTarget16) * 2), PrintNameLength: uint16((len(target16) - 1) * 2), } var b bytes.Buffer _ = binary.Write(&b, binary.LittleEndian, &data) if !rp.IsMountPoint { flags := uint32(0) if relative { flags |= 1 } _ = binary.Write(&b, binary.LittleEndian, flags) } _ = binary.Write(&b, binary.LittleEndian, ntTarget16) _ = binary.Write(&b, binary.LittleEndian, target16) return b.Bytes() } ================================================ FILE: vendor/github.com/Microsoft/go-winio/sd.go ================================================ //go:build windows // +build windows package winio import ( "errors" "fmt" "unsafe" "golang.org/x/sys/windows" ) //sys lookupAccountName(systemName *uint16, accountName string, sid *byte, sidSize *uint32, refDomain *uint16, refDomainSize *uint32, sidNameUse *uint32) (err error) = advapi32.LookupAccountNameW //sys lookupAccountSid(systemName *uint16, sid *byte, name *uint16, nameSize *uint32, refDomain *uint16, refDomainSize *uint32, sidNameUse *uint32) (err error) = advapi32.LookupAccountSidW //sys convertSidToStringSid(sid *byte, str **uint16) (err error) = advapi32.ConvertSidToStringSidW //sys convertStringSidToSid(str *uint16, sid **byte) (err error) = advapi32.ConvertStringSidToSidW type AccountLookupError struct { Name string Err error } func (e *AccountLookupError) Error() string { if e.Name == "" { return "lookup account: empty account name specified" } var s string switch { case errors.Is(e.Err, windows.ERROR_INVALID_SID): s = "the security ID structure is invalid" case errors.Is(e.Err, windows.ERROR_NONE_MAPPED): s = "not found" default: s = e.Err.Error() } return "lookup account " + e.Name + ": " + s } func (e *AccountLookupError) Unwrap() error { return e.Err } type SddlConversionError struct { Sddl string Err error } func (e *SddlConversionError) Error() string { return "convert " + e.Sddl + ": " + e.Err.Error() } func (e *SddlConversionError) Unwrap() error { return e.Err } // LookupSidByName looks up the SID of an account by name // //revive:disable-next-line:var-naming SID, not Sid func LookupSidByName(name string) (sid string, err error) { if name == "" { return "", &AccountLookupError{name, windows.ERROR_NONE_MAPPED} } var sidSize, sidNameUse, refDomainSize uint32 err = lookupAccountName(nil, name, nil, &sidSize, nil, &refDomainSize, &sidNameUse) if err != nil && err != windows.ERROR_INSUFFICIENT_BUFFER { //nolint:errorlint // err is Errno return "", &AccountLookupError{name, err} } sidBuffer := make([]byte, sidSize) refDomainBuffer := make([]uint16, refDomainSize) err = lookupAccountName(nil, name, &sidBuffer[0], &sidSize, &refDomainBuffer[0], &refDomainSize, &sidNameUse) if err != nil { return "", &AccountLookupError{name, err} } var strBuffer *uint16 err = convertSidToStringSid(&sidBuffer[0], &strBuffer) if err != nil { return "", &AccountLookupError{name, err} } sid = windows.UTF16ToString((*[0xffff]uint16)(unsafe.Pointer(strBuffer))[:]) _, _ = windows.LocalFree(windows.Handle(unsafe.Pointer(strBuffer))) return sid, nil } // LookupNameBySid looks up the name of an account by SID // //revive:disable-next-line:var-naming SID, not Sid func LookupNameBySid(sid string) (name string, err error) { if sid == "" { return "", &AccountLookupError{sid, windows.ERROR_NONE_MAPPED} } sidBuffer, err := windows.UTF16PtrFromString(sid) if err != nil { return "", &AccountLookupError{sid, err} } var sidPtr *byte if err = convertStringSidToSid(sidBuffer, &sidPtr); err != nil { return "", &AccountLookupError{sid, err} } defer windows.LocalFree(windows.Handle(unsafe.Pointer(sidPtr))) //nolint:errcheck var nameSize, refDomainSize, sidNameUse uint32 err = lookupAccountSid(nil, sidPtr, nil, &nameSize, nil, &refDomainSize, &sidNameUse) if err != nil && err != windows.ERROR_INSUFFICIENT_BUFFER { //nolint:errorlint // err is Errno return "", &AccountLookupError{sid, err} } nameBuffer := make([]uint16, nameSize) refDomainBuffer := make([]uint16, refDomainSize) err = lookupAccountSid(nil, sidPtr, &nameBuffer[0], &nameSize, &refDomainBuffer[0], &refDomainSize, &sidNameUse) if err != nil { return "", &AccountLookupError{sid, err} } name = windows.UTF16ToString(nameBuffer) return name, nil } func SddlToSecurityDescriptor(sddl string) ([]byte, error) { sd, err := windows.SecurityDescriptorFromString(sddl) if err != nil { return nil, &SddlConversionError{Sddl: sddl, Err: err} } b := unsafe.Slice((*byte)(unsafe.Pointer(sd)), sd.Length()) return b, nil } func SecurityDescriptorToSddl(sd []byte) (string, error) { if l := int(unsafe.Sizeof(windows.SECURITY_DESCRIPTOR{})); len(sd) < l { return "", fmt.Errorf("SecurityDescriptor (%d) smaller than expected (%d): %w", len(sd), l, windows.ERROR_INCORRECT_SIZE) } s := (*windows.SECURITY_DESCRIPTOR)(unsafe.Pointer(&sd[0])) return s.String(), nil } ================================================ FILE: vendor/github.com/Microsoft/go-winio/syscall.go ================================================ //go:build windows package winio //go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go ./*.go ================================================ FILE: vendor/github.com/Microsoft/go-winio/zsyscall_windows.go ================================================ //go:build windows // Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT. package winio import ( "syscall" "unsafe" "golang.org/x/sys/windows" ) var _ unsafe.Pointer // Do the interface allocations only once for common // Errno values. const ( errnoERROR_IO_PENDING = 997 ) var ( errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) errERROR_EINVAL error = syscall.EINVAL ) // errnoErr returns common boxed Errno values, to prevent // allocations at runtime. func errnoErr(e syscall.Errno) error { switch e { case 0: return errERROR_EINVAL case errnoERROR_IO_PENDING: return errERROR_IO_PENDING } return e } var ( modadvapi32 = windows.NewLazySystemDLL("advapi32.dll") modkernel32 = windows.NewLazySystemDLL("kernel32.dll") modntdll = windows.NewLazySystemDLL("ntdll.dll") modws2_32 = windows.NewLazySystemDLL("ws2_32.dll") procAdjustTokenPrivileges = modadvapi32.NewProc("AdjustTokenPrivileges") procConvertSidToStringSidW = modadvapi32.NewProc("ConvertSidToStringSidW") procConvertStringSidToSidW = modadvapi32.NewProc("ConvertStringSidToSidW") procImpersonateSelf = modadvapi32.NewProc("ImpersonateSelf") procLookupAccountNameW = modadvapi32.NewProc("LookupAccountNameW") procLookupAccountSidW = modadvapi32.NewProc("LookupAccountSidW") procLookupPrivilegeDisplayNameW = modadvapi32.NewProc("LookupPrivilegeDisplayNameW") procLookupPrivilegeNameW = modadvapi32.NewProc("LookupPrivilegeNameW") procLookupPrivilegeValueW = modadvapi32.NewProc("LookupPrivilegeValueW") procOpenThreadToken = modadvapi32.NewProc("OpenThreadToken") procRevertToSelf = modadvapi32.NewProc("RevertToSelf") procBackupRead = modkernel32.NewProc("BackupRead") procBackupWrite = modkernel32.NewProc("BackupWrite") procCancelIoEx = modkernel32.NewProc("CancelIoEx") procConnectNamedPipe = modkernel32.NewProc("ConnectNamedPipe") procCreateIoCompletionPort = modkernel32.NewProc("CreateIoCompletionPort") procCreateNamedPipeW = modkernel32.NewProc("CreateNamedPipeW") procDisconnectNamedPipe = modkernel32.NewProc("DisconnectNamedPipe") procGetCurrentThread = modkernel32.NewProc("GetCurrentThread") procGetNamedPipeHandleStateW = modkernel32.NewProc("GetNamedPipeHandleStateW") procGetNamedPipeInfo = modkernel32.NewProc("GetNamedPipeInfo") procGetQueuedCompletionStatus = modkernel32.NewProc("GetQueuedCompletionStatus") procSetFileCompletionNotificationModes = modkernel32.NewProc("SetFileCompletionNotificationModes") procNtCreateNamedPipeFile = modntdll.NewProc("NtCreateNamedPipeFile") procRtlDefaultNpAcl = modntdll.NewProc("RtlDefaultNpAcl") procRtlDosPathNameToNtPathName_U = modntdll.NewProc("RtlDosPathNameToNtPathName_U") procRtlNtStatusToDosErrorNoTeb = modntdll.NewProc("RtlNtStatusToDosErrorNoTeb") procWSAGetOverlappedResult = modws2_32.NewProc("WSAGetOverlappedResult") ) func adjustTokenPrivileges(token windows.Token, releaseAll bool, input *byte, outputSize uint32, output *byte, requiredSize *uint32) (success bool, err error) { var _p0 uint32 if releaseAll { _p0 = 1 } r0, _, e1 := syscall.SyscallN(procAdjustTokenPrivileges.Addr(), uintptr(token), uintptr(_p0), uintptr(unsafe.Pointer(input)), uintptr(outputSize), uintptr(unsafe.Pointer(output)), uintptr(unsafe.Pointer(requiredSize))) success = r0 != 0 if true { err = errnoErr(e1) } return } func convertSidToStringSid(sid *byte, str **uint16) (err error) { r1, _, e1 := syscall.SyscallN(procConvertSidToStringSidW.Addr(), uintptr(unsafe.Pointer(sid)), uintptr(unsafe.Pointer(str))) if r1 == 0 { err = errnoErr(e1) } return } func convertStringSidToSid(str *uint16, sid **byte) (err error) { r1, _, e1 := syscall.SyscallN(procConvertStringSidToSidW.Addr(), uintptr(unsafe.Pointer(str)), uintptr(unsafe.Pointer(sid))) if r1 == 0 { err = errnoErr(e1) } return } func impersonateSelf(level uint32) (err error) { r1, _, e1 := syscall.SyscallN(procImpersonateSelf.Addr(), uintptr(level)) if r1 == 0 { err = errnoErr(e1) } return } func lookupAccountName(systemName *uint16, accountName string, sid *byte, sidSize *uint32, refDomain *uint16, refDomainSize *uint32, sidNameUse *uint32) (err error) { var _p0 *uint16 _p0, err = syscall.UTF16PtrFromString(accountName) if err != nil { return } return _lookupAccountName(systemName, _p0, sid, sidSize, refDomain, refDomainSize, sidNameUse) } func _lookupAccountName(systemName *uint16, accountName *uint16, sid *byte, sidSize *uint32, refDomain *uint16, refDomainSize *uint32, sidNameUse *uint32) (err error) { r1, _, e1 := syscall.SyscallN(procLookupAccountNameW.Addr(), uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(accountName)), uintptr(unsafe.Pointer(sid)), uintptr(unsafe.Pointer(sidSize)), uintptr(unsafe.Pointer(refDomain)), uintptr(unsafe.Pointer(refDomainSize)), uintptr(unsafe.Pointer(sidNameUse))) if r1 == 0 { err = errnoErr(e1) } return } func lookupAccountSid(systemName *uint16, sid *byte, name *uint16, nameSize *uint32, refDomain *uint16, refDomainSize *uint32, sidNameUse *uint32) (err error) { r1, _, e1 := syscall.SyscallN(procLookupAccountSidW.Addr(), uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(sid)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(nameSize)), uintptr(unsafe.Pointer(refDomain)), uintptr(unsafe.Pointer(refDomainSize)), uintptr(unsafe.Pointer(sidNameUse))) if r1 == 0 { err = errnoErr(e1) } return } func lookupPrivilegeDisplayName(systemName string, name *uint16, buffer *uint16, size *uint32, languageId *uint32) (err error) { var _p0 *uint16 _p0, err = syscall.UTF16PtrFromString(systemName) if err != nil { return } return _lookupPrivilegeDisplayName(_p0, name, buffer, size, languageId) } func _lookupPrivilegeDisplayName(systemName *uint16, name *uint16, buffer *uint16, size *uint32, languageId *uint32) (err error) { r1, _, e1 := syscall.SyscallN(procLookupPrivilegeDisplayNameW.Addr(), uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(buffer)), uintptr(unsafe.Pointer(size)), uintptr(unsafe.Pointer(languageId))) if r1 == 0 { err = errnoErr(e1) } return } func lookupPrivilegeName(systemName string, luid *uint64, buffer *uint16, size *uint32) (err error) { var _p0 *uint16 _p0, err = syscall.UTF16PtrFromString(systemName) if err != nil { return } return _lookupPrivilegeName(_p0, luid, buffer, size) } func _lookupPrivilegeName(systemName *uint16, luid *uint64, buffer *uint16, size *uint32) (err error) { r1, _, e1 := syscall.SyscallN(procLookupPrivilegeNameW.Addr(), uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(luid)), uintptr(unsafe.Pointer(buffer)), uintptr(unsafe.Pointer(size))) if r1 == 0 { err = errnoErr(e1) } return } func lookupPrivilegeValue(systemName string, name string, luid *uint64) (err error) { var _p0 *uint16 _p0, err = syscall.UTF16PtrFromString(systemName) if err != nil { return } var _p1 *uint16 _p1, err = syscall.UTF16PtrFromString(name) if err != nil { return } return _lookupPrivilegeValue(_p0, _p1, luid) } func _lookupPrivilegeValue(systemName *uint16, name *uint16, luid *uint64) (err error) { r1, _, e1 := syscall.SyscallN(procLookupPrivilegeValueW.Addr(), uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(luid))) if r1 == 0 { err = errnoErr(e1) } return } func openThreadToken(thread windows.Handle, accessMask uint32, openAsSelf bool, token *windows.Token) (err error) { var _p0 uint32 if openAsSelf { _p0 = 1 } r1, _, e1 := syscall.SyscallN(procOpenThreadToken.Addr(), uintptr(thread), uintptr(accessMask), uintptr(_p0), uintptr(unsafe.Pointer(token))) if r1 == 0 { err = errnoErr(e1) } return } func revertToSelf() (err error) { r1, _, e1 := syscall.SyscallN(procRevertToSelf.Addr()) if r1 == 0 { err = errnoErr(e1) } return } func backupRead(h windows.Handle, b []byte, bytesRead *uint32, abort bool, processSecurity bool, context *uintptr) (err error) { var _p0 *byte if len(b) > 0 { _p0 = &b[0] } var _p1 uint32 if abort { _p1 = 1 } var _p2 uint32 if processSecurity { _p2 = 1 } r1, _, e1 := syscall.SyscallN(procBackupRead.Addr(), uintptr(h), uintptr(unsafe.Pointer(_p0)), uintptr(len(b)), uintptr(unsafe.Pointer(bytesRead)), uintptr(_p1), uintptr(_p2), uintptr(unsafe.Pointer(context))) if r1 == 0 { err = errnoErr(e1) } return } func backupWrite(h windows.Handle, b []byte, bytesWritten *uint32, abort bool, processSecurity bool, context *uintptr) (err error) { var _p0 *byte if len(b) > 0 { _p0 = &b[0] } var _p1 uint32 if abort { _p1 = 1 } var _p2 uint32 if processSecurity { _p2 = 1 } r1, _, e1 := syscall.SyscallN(procBackupWrite.Addr(), uintptr(h), uintptr(unsafe.Pointer(_p0)), uintptr(len(b)), uintptr(unsafe.Pointer(bytesWritten)), uintptr(_p1), uintptr(_p2), uintptr(unsafe.Pointer(context))) if r1 == 0 { err = errnoErr(e1) } return } func cancelIoEx(file windows.Handle, o *windows.Overlapped) (err error) { r1, _, e1 := syscall.SyscallN(procCancelIoEx.Addr(), uintptr(file), uintptr(unsafe.Pointer(o))) if r1 == 0 { err = errnoErr(e1) } return } func connectNamedPipe(pipe windows.Handle, o *windows.Overlapped) (err error) { r1, _, e1 := syscall.SyscallN(procConnectNamedPipe.Addr(), uintptr(pipe), uintptr(unsafe.Pointer(o))) if r1 == 0 { err = errnoErr(e1) } return } func createIoCompletionPort(file windows.Handle, port windows.Handle, key uintptr, threadCount uint32) (newport windows.Handle, err error) { r0, _, e1 := syscall.SyscallN(procCreateIoCompletionPort.Addr(), uintptr(file), uintptr(port), uintptr(key), uintptr(threadCount)) newport = windows.Handle(r0) if newport == 0 { err = errnoErr(e1) } return } func createNamedPipe(name string, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *windows.SecurityAttributes) (handle windows.Handle, err error) { var _p0 *uint16 _p0, err = syscall.UTF16PtrFromString(name) if err != nil { return } return _createNamedPipe(_p0, flags, pipeMode, maxInstances, outSize, inSize, defaultTimeout, sa) } func _createNamedPipe(name *uint16, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *windows.SecurityAttributes) (handle windows.Handle, err error) { r0, _, e1 := syscall.SyscallN(procCreateNamedPipeW.Addr(), uintptr(unsafe.Pointer(name)), uintptr(flags), uintptr(pipeMode), uintptr(maxInstances), uintptr(outSize), uintptr(inSize), uintptr(defaultTimeout), uintptr(unsafe.Pointer(sa))) handle = windows.Handle(r0) if handle == windows.InvalidHandle { err = errnoErr(e1) } return } func disconnectNamedPipe(pipe windows.Handle) (err error) { r1, _, e1 := syscall.SyscallN(procDisconnectNamedPipe.Addr(), uintptr(pipe)) if r1 == 0 { err = errnoErr(e1) } return } func getCurrentThread() (h windows.Handle) { r0, _, _ := syscall.SyscallN(procGetCurrentThread.Addr()) h = windows.Handle(r0) return } func getNamedPipeHandleState(pipe windows.Handle, state *uint32, curInstances *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32, userName *uint16, maxUserNameSize uint32) (err error) { r1, _, e1 := syscall.SyscallN(procGetNamedPipeHandleStateW.Addr(), uintptr(pipe), uintptr(unsafe.Pointer(state)), uintptr(unsafe.Pointer(curInstances)), uintptr(unsafe.Pointer(maxCollectionCount)), uintptr(unsafe.Pointer(collectDataTimeout)), uintptr(unsafe.Pointer(userName)), uintptr(maxUserNameSize)) if r1 == 0 { err = errnoErr(e1) } return } func getNamedPipeInfo(pipe windows.Handle, flags *uint32, outSize *uint32, inSize *uint32, maxInstances *uint32) (err error) { r1, _, e1 := syscall.SyscallN(procGetNamedPipeInfo.Addr(), uintptr(pipe), uintptr(unsafe.Pointer(flags)), uintptr(unsafe.Pointer(outSize)), uintptr(unsafe.Pointer(inSize)), uintptr(unsafe.Pointer(maxInstances))) if r1 == 0 { err = errnoErr(e1) } return } func getQueuedCompletionStatus(port windows.Handle, bytes *uint32, key *uintptr, o **ioOperation, timeout uint32) (err error) { r1, _, e1 := syscall.SyscallN(procGetQueuedCompletionStatus.Addr(), uintptr(port), uintptr(unsafe.Pointer(bytes)), uintptr(unsafe.Pointer(key)), uintptr(unsafe.Pointer(o)), uintptr(timeout)) if r1 == 0 { err = errnoErr(e1) } return } func setFileCompletionNotificationModes(h windows.Handle, flags uint8) (err error) { r1, _, e1 := syscall.SyscallN(procSetFileCompletionNotificationModes.Addr(), uintptr(h), uintptr(flags)) if r1 == 0 { err = errnoErr(e1) } return } func ntCreateNamedPipeFile(pipe *windows.Handle, access ntAccessMask, oa *objectAttributes, iosb *ioStatusBlock, share ntFileShareMode, disposition ntFileCreationDisposition, options ntFileOptions, typ uint32, readMode uint32, completionMode uint32, maxInstances uint32, inboundQuota uint32, outputQuota uint32, timeout *int64) (status ntStatus) { r0, _, _ := syscall.SyscallN(procNtCreateNamedPipeFile.Addr(), uintptr(unsafe.Pointer(pipe)), uintptr(access), uintptr(unsafe.Pointer(oa)), uintptr(unsafe.Pointer(iosb)), uintptr(share), uintptr(disposition), uintptr(options), uintptr(typ), uintptr(readMode), uintptr(completionMode), uintptr(maxInstances), uintptr(inboundQuota), uintptr(outputQuota), uintptr(unsafe.Pointer(timeout))) status = ntStatus(r0) return } func rtlDefaultNpAcl(dacl *uintptr) (status ntStatus) { r0, _, _ := syscall.SyscallN(procRtlDefaultNpAcl.Addr(), uintptr(unsafe.Pointer(dacl))) status = ntStatus(r0) return } func rtlDosPathNameToNtPathName(name *uint16, ntName *unicodeString, filePart uintptr, reserved uintptr) (status ntStatus) { r0, _, _ := syscall.SyscallN(procRtlDosPathNameToNtPathName_U.Addr(), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(ntName)), uintptr(filePart), uintptr(reserved)) status = ntStatus(r0) return } func rtlNtStatusToDosError(status ntStatus) (winerr error) { r0, _, _ := syscall.SyscallN(procRtlNtStatusToDosErrorNoTeb.Addr(), uintptr(status)) if r0 != 0 { winerr = syscall.Errno(r0) } return } func wsaGetOverlappedResult(h windows.Handle, o *windows.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) { var _p0 uint32 if wait { _p0 = 1 } r1, _, e1 := syscall.SyscallN(procWSAGetOverlappedResult.Addr(), uintptr(h), uintptr(unsafe.Pointer(o)), uintptr(unsafe.Pointer(bytes)), uintptr(_p0), uintptr(unsafe.Pointer(flags))) if r1 == 0 { err = errnoErr(e1) } return } ================================================ FILE: vendor/github.com/VividCortex/ewma/.gitignore ================================================ .DS_Store .*.sw? /coverage.txt ================================================ FILE: vendor/github.com/VividCortex/ewma/.whitesource ================================================ { "settingsInheritedFrom": "VividCortex/whitesource-config@master" } ================================================ FILE: vendor/github.com/VividCortex/ewma/LICENSE ================================================ The MIT License Copyright (c) 2013 VividCortex Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: vendor/github.com/VividCortex/ewma/README.md ================================================ # EWMA [![GoDoc](https://godoc.org/github.com/VividCortex/ewma?status.svg)](https://godoc.org/github.com/VividCortex/ewma) ![build](https://github.com/VividCortex/ewma/workflows/build/badge.svg) [![codecov](https://codecov.io/gh/VividCortex/ewma/branch/master/graph/badge.svg)](https://codecov.io/gh/VividCortex/ewma) This repo provides Exponentially Weighted Moving Average algorithms, or EWMAs for short, [based on our Quantifying Abnormal Behavior talk](https://vividcortex.com/blog/2013/07/23/a-fast-go-library-for-exponential-moving-averages/). ### Exponentially Weighted Moving Average An exponentially weighted moving average is a way to continuously compute a type of average for a series of numbers, as the numbers arrive. After a value in the series is added to the average, its weight in the average decreases exponentially over time. This biases the average towards more recent data. EWMAs are useful for several reasons, chiefly their inexpensive computational and memory cost, as well as the fact that they represent the recent central tendency of the series of values. The EWMA algorithm requires a decay factor, alpha. The larger the alpha, the more the average is biased towards recent history. The alpha must be between 0 and 1, and is typically a fairly small number, such as 0.04. We will discuss the choice of alpha later. The algorithm works thus, in pseudocode: 1. Multiply the next number in the series by alpha. 2. Multiply the current value of the average by 1 minus alpha. 3. Add the result of steps 1 and 2, and store it as the new current value of the average. 4. Repeat for each number in the series. There are special-case behaviors for how to initialize the current value, and these vary between implementations. One approach is to start with the first value in the series; another is to average the first 10 or so values in the series using an arithmetic average, and then begin the incremental updating of the average. Each method has pros and cons. It may help to look at it pictorially. Suppose the series has five numbers, and we choose alpha to be 0.50 for simplicity. Here's the series, with numbers in the neighborhood of 300. ![Data Series](https://user-images.githubusercontent.com/279875/28242350-463289a2-6977-11e7-88ca-fd778ccef1f0.png) Now let's take the moving average of those numbers. First we set the average to the value of the first number. ![EWMA Step 1](https://user-images.githubusercontent.com/279875/28242353-464c96bc-6977-11e7-9981-dc4e0789c7ba.png) Next we multiply the next number by alpha, multiply the current value by 1-alpha, and add them to generate a new value. ![EWMA Step 2](https://user-images.githubusercontent.com/279875/28242351-464abefa-6977-11e7-95d0-43900f29bef2.png) This continues until we are done. ![EWMA Step N](https://user-images.githubusercontent.com/279875/28242352-464c58f0-6977-11e7-8cd0-e01e4efaac7f.png) Notice how each of the values in the series decays by half each time a new value is added, and the top of the bars in the lower portion of the image represents the size of the moving average. It is a smoothed, or low-pass, average of the original series. For further reading, see [Exponentially weighted moving average](http://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average) on wikipedia. ### Choosing Alpha Consider a fixed-size sliding-window moving average (not an exponentially weighted moving average) that averages over the previous N samples. What is the average age of each sample? It is N/2. Now suppose that you wish to construct a EWMA whose samples have the same average age. The formula to compute the alpha required for this is: alpha = 2/(N+1). Proof is in the book "Production and Operations Analysis" by Steven Nahmias. So, for example, if you have a time-series with samples once per second, and you want to get the moving average over the previous minute, you should use an alpha of .032786885. This, by the way, is the constant alpha used for this repository's SimpleEWMA. ### Implementations This repository contains two implementations of the EWMA algorithm, with different properties. The implementations all conform to the MovingAverage interface, and the constructor returns that type. Current implementations assume an implicit time interval of 1.0 between every sample added. That is, the passage of time is treated as though it's the same as the arrival of samples. If you need time-based decay when samples are not arriving precisely at set intervals, then this package will not support your needs at present. #### SimpleEWMA A SimpleEWMA is designed for low CPU and memory consumption. It **will** have different behavior than the VariableEWMA for multiple reasons. It has no warm-up period and it uses a constant decay. These properties let it use less memory. It will also behave differently when it's equal to zero, which is assumed to mean uninitialized, so if a value is likely to actually become zero over time, then any non-zero value will cause a sharp jump instead of a small change. #### VariableEWMA Unlike SimpleEWMA, this supports a custom age which must be stored, and thus uses more memory. It also has a "warmup" time when you start adding values to it. It will report a value of 0.0 until you have added the required number of samples to it. It uses some memory to store the number of samples added to it. As a result it uses a little over twice the memory of SimpleEWMA. ## Usage ### API Documentation View the GoDoc generated documentation [here](http://godoc.org/github.com/VividCortex/ewma). ```go package main import "github.com/VividCortex/ewma" func main() { samples := [100]float64{ 4599, 5711, 4746, 4621, 5037, 4218, 4925, 4281, 5207, 5203, 5594, 5149, } e := ewma.NewMovingAverage() //=> Returns a SimpleEWMA if called without params a := ewma.NewMovingAverage(5) //=> returns a VariableEWMA with a decay of 2 / (5 + 1) for _, f := range samples { e.Add(f) a.Add(f) } e.Value() //=> 13.577404704631077 a.Value() //=> 1.5806140565521463e-12 } ``` ## Contributing We only accept pull requests for minor fixes or improvements. This includes: * Small bug fixes * Typos * Documentation or comments Please open issues to discuss new features. Pull requests for new features will be rejected, so we recommend forking the repository and making changes in your fork for your use case. ## License This repository is Copyright (c) 2013 VividCortex, Inc. All rights reserved. It is licensed under the MIT license. Please see the LICENSE file for applicable license terms. ================================================ FILE: vendor/github.com/VividCortex/ewma/codecov.yml ================================================ coverage: status: project: default: threshold: 15% patch: off ================================================ FILE: vendor/github.com/VividCortex/ewma/ewma.go ================================================ // Package ewma implements exponentially weighted moving averages. package ewma // Copyright (c) 2013 VividCortex, Inc. All rights reserved. // Please see the LICENSE file for applicable license terms. const ( // By default, we average over a one-minute period, which means the average // age of the metrics in the period is 30 seconds. AVG_METRIC_AGE float64 = 30.0 // The formula for computing the decay factor from the average age comes // from "Production and Operations Analysis" by Steven Nahmias. DECAY float64 = 2 / (float64(AVG_METRIC_AGE) + 1) // For best results, the moving average should not be initialized to the // samples it sees immediately. The book "Production and Operations // Analysis" by Steven Nahmias suggests initializing the moving average to // the mean of the first 10 samples. Until the VariableEwma has seen this // many samples, it is not "ready" to be queried for the value of the // moving average. This adds some memory cost. WARMUP_SAMPLES uint8 = 10 ) // MovingAverage is the interface that computes a moving average over a time- // series stream of numbers. The average may be over a window or exponentially // decaying. type MovingAverage interface { Add(float64) Value() float64 Set(float64) } // NewMovingAverage constructs a MovingAverage that computes an average with the // desired characteristics in the moving window or exponential decay. If no // age is given, it constructs a default exponentially weighted implementation // that consumes minimal memory. The age is related to the decay factor alpha // by the formula given for the DECAY constant. It signifies the average age // of the samples as time goes to infinity. func NewMovingAverage(age ...float64) MovingAverage { if len(age) == 0 || age[0] == AVG_METRIC_AGE { return new(SimpleEWMA) } return &VariableEWMA{ decay: 2 / (age[0] + 1), } } // A SimpleEWMA represents the exponentially weighted moving average of a // series of numbers. It WILL have different behavior than the VariableEWMA // for multiple reasons. It has no warm-up period and it uses a constant // decay. These properties let it use less memory. It will also behave // differently when it's equal to zero, which is assumed to mean // uninitialized, so if a value is likely to actually become zero over time, // then any non-zero value will cause a sharp jump instead of a small change. // However, note that this takes a long time, and the value may just // decays to a stable value that's close to zero, but which won't be mistaken // for uninitialized. See http://play.golang.org/p/litxBDr_RC for example. type SimpleEWMA struct { // The current value of the average. After adding with Add(), this is // updated to reflect the average of all values seen thus far. value float64 } // Add adds a value to the series and updates the moving average. func (e *SimpleEWMA) Add(value float64) { if e.value == 0 { // this is a proxy for "uninitialized" e.value = value } else { e.value = (value * DECAY) + (e.value * (1 - DECAY)) } } // Value returns the current value of the moving average. func (e *SimpleEWMA) Value() float64 { return e.value } // Set sets the EWMA's value. func (e *SimpleEWMA) Set(value float64) { e.value = value } // VariableEWMA represents the exponentially weighted moving average of a series of // numbers. Unlike SimpleEWMA, it supports a custom age, and thus uses more memory. type VariableEWMA struct { // The multiplier factor by which the previous samples decay. decay float64 // The current value of the average. value float64 // The number of samples added to this instance. count uint8 } // Add adds a value to the series and updates the moving average. func (e *VariableEWMA) Add(value float64) { switch { case e.count < WARMUP_SAMPLES: e.count++ e.value += value case e.count == WARMUP_SAMPLES: e.count++ e.value = e.value / float64(WARMUP_SAMPLES) e.value = (value * e.decay) + (e.value * (1 - e.decay)) default: e.value = (value * e.decay) + (e.value * (1 - e.decay)) } } // Value returns the current value of the average, or 0.0 if the series hasn't // warmed up yet. func (e *VariableEWMA) Value() float64 { if e.count <= WARMUP_SAMPLES { return 0.0 } return e.value } // Set sets the EWMA's value. func (e *VariableEWMA) Set(value float64) { e.value = value if e.count <= WARMUP_SAMPLES { e.count = WARMUP_SAMPLES + 1 } } ================================================ FILE: vendor/github.com/acarl005/stripansi/LICENSE ================================================ MIT License Copyright (c) 2018 Andrew Carlson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: vendor/github.com/acarl005/stripansi/README.md ================================================ Strip ANSI ========== This Go package removes ANSI escape codes from strings. Ideally, we would prevent these from appearing in any text we want to process. However, sometimes this can't be helped, and we need to be able to deal with that noise. This will use a regexp to remove those unwanted escape codes. ## Install ```sh $ go get -u github.com/acarl005/stripansi ``` ## Usage ```go import ( "fmt" "github.com/acarl005/stripansi" ) func main() { msg := "\x1b[38;5;140m foo\x1b[0m bar" cleanMsg := stripansi.Strip(msg) fmt.Println(cleanMsg) // " foo bar" } ``` ================================================ FILE: vendor/github.com/acarl005/stripansi/stripansi.go ================================================ package stripansi import ( "regexp" ) const ansi = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))" var re = regexp.MustCompile(ansi) func Strip(str string) string { return re.ReplaceAllString(str, "") } ================================================ FILE: vendor/github.com/aead/serpent/.gitignore ================================================ # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test .vscode # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof ================================================ FILE: vendor/github.com/aead/serpent/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016 Andreas Auernhammer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: vendor/github.com/aead/serpent/README.md ================================================ [![Godoc Reference](https://godoc.org/github.com/aead/serpent?status.svg)](https://godoc.org/github.com/aead/serpent) ## The Serpent block cipher Serpent is a symmetric key block cipher that was a finalist in the Advanced Encryption Standard (AES) contest, where it was ranked second to Rijndael. Serpent was designed by Ross Anderson, Eli Biham, and Lars Knudsen. ### Installation Install in your GOPATH: `go get -u github.com/aead/serpent` ================================================ FILE: vendor/github.com/aead/serpent/sbox_ref.go ================================================ // Copyright (c) 2016 Andreas Auernhammer. All rights reserved. // Use of this source code is governed by a license that can be // found in the LICENSE file. package serpent // The linear transformation of serpent // This version, tries not to minimize the // number of registers, but maximize parallism. func linear(v0, v1, v2, v3 *uint32) { t0 := ((*v0 << 13) | (*v0 >> (32 - 13))) t2 := ((*v2 << 3) | (*v2 >> (32 - 3))) t1 := *v1 ^ t0 ^ t2 t3 := *v3 ^ t2 ^ (t0 << 3) *v1 = (t1 << 1) | (t1 >> (32 - 1)) *v3 = (t3 << 7) | (t3 >> (32 - 7)) t0 ^= *v1 ^ *v3 t2 ^= *v3 ^ (*v1 << 7) *v0 = (t0 << 5) | (t0 >> (32 - 5)) *v2 = (t2 << 22) | (t2 >> (32 - 22)) } // The inverse linear transformation of serpent // This version, tries not to minimize the // number of registers, but maximize parallism. func linearInv(v0, v1, v2, v3 *uint32) { t2 := (*v2 >> 22) | (*v2 << (32 - 22)) t0 := (*v0 >> 5) | (*v0 << (32 - 5)) t2 ^= *v3 ^ (*v1 << 7) t0 ^= *v1 ^ *v3 t3 := (*v3 >> 7) | (*v3 << (32 - 7)) t1 := (*v1 >> 1) | (*v1 << (32 - 1)) *v3 = t3 ^ t2 ^ (t0 << 3) *v1 = t1 ^ t0 ^ t2 *v2 = (t2 >> 3) | (t2 << (32 - 3)) *v0 = (t0 >> 13) | (t0 << (32 - 13)) } // The following functions sb0,sb1, ..., sb7 represent the 8 Serpent S-Boxes. // sb0Inv til sb7Inv are the inverse functions (e.g. sb0Inv is the Inverse to sb0 // and vice versa). // The S-Boxes differ from the original Serpent definitions. This is for // optimisation. The functions use the Serpent S-Box improvements for (non x86) // from Dr. B. R. Gladman and Sam Simpson. // S-Box 0 func sb0(r0, r1, r2, r3 *uint32) { t0 := *r0 ^ *r3 t1 := *r2 ^ t0 t2 := *r1 ^ t1 *r3 = (*r0 & *r3) ^ t2 t3 := *r0 ^ (*r1 & t0) *r2 = t2 ^ (*r2 | t3) t4 := *r3 & (t1 ^ t3) *r1 = (^t1) ^ t4 *r0 = t4 ^ (^t3) } // Inverse S-Box 0 func sb0Inv(r0, r1, r2, r3 *uint32) { t0 := ^(*r0) t1 := *r0 ^ *r1 t2 := *r3 ^ (t0 | t1) t3 := *r2 ^ t2 *r2 = t1 ^ t3 t4 := t0 ^ (*r3 & t1) *r1 = t2 ^ (*r2 & t4) *r3 = (*r0 & t2) ^ (t3 | *r1) *r0 = *r3 ^ (t3 ^ t4) } // S-Box 1 func sb1(r0, r1, r2, r3 *uint32) { t0 := *r1 ^ (^(*r0)) t1 := *r2 ^ (*r0 | t0) *r2 = *r3 ^ t1 t2 := *r1 ^ (*r3 | t0) t3 := t0 ^ *r2 *r3 = t3 ^ (t1 & t2) t4 := t1 ^ t2 *r1 = *r3 ^ t4 *r0 = t1 ^ (t3 & t4) } // Inverse S-Box 1 func sb1Inv(r0, r1, r2, r3 *uint32) { t0 := *r1 ^ *r3 t1 := *r0 ^ (*r1 & t0) t2 := t0 ^ t1 *r3 = *r2 ^ t2 t3 := *r1 ^ (t0 & t1) t4 := *r3 | t3 *r1 = t1 ^ t4 t5 := ^(*r1) t6 := *r3 ^ t3 *r0 = t5 ^ t6 *r2 = t2 ^ (t5 | t6) } // S-Box 2 func sb2(r0, r1, r2, r3 *uint32) { v0 := *r0 // save r0 v3 := *r3 // save r3 t0 := ^v0 t1 := *r1 ^ v3 t2 := *r2 & t0 *r0 = t1 ^ t2 t3 := *r2 ^ t0 t4 := *r2 ^ *r0 t5 := *r1 & t4 *r3 = t3 ^ t5 *r2 = v0 ^ ((v3 | t5) & (*r0 | t3)) *r1 = (t1 ^ *r3) ^ (*r2 ^ (v3 | t0)) } // Inverse S-Box 2 func sb2Inv(r0, r1, r2, r3 *uint32) { v0 := *r0 // save r0 v3 := *r3 // save r3 t0 := *r1 ^ v3 t1 := ^t0 t2 := v0 ^ *r2 t3 := *r2 ^ t0 t4 := *r1 & t3 *r0 = t2 ^ t4 t5 := v0 | t1 t6 := v3 ^ t5 t7 := t2 | t6 *r3 = t0 ^ t7 t8 := ^t3 t9 := *r0 | *r3 *r1 = t8 ^ t9 *r2 = (v3 & t8) ^ (t2 ^ t9) } // S-Box 3 func sb3(r0, r1, r2, r3 *uint32) { v1 := *r1 // save r1 v3 := *r3 // save r3 t0 := *r0 ^ *r1 t1 := *r0 & *r2 t2 := *r0 | *r3 t3 := *r2 ^ *r3 t4 := t0 & t2 t5 := t1 | t4 *r2 = t3 ^ t5 t6 := *r1 ^ t2 t7 := t5 ^ t6 t8 := t3 & t7 *r0 = t0 ^ t8 t9 := *r2 & *r0 *r1 = t7 ^ t9 *r3 = (v1 | v3) ^ (t3 ^ t9) } // Inverse S-Box 3 func sb3Inv(r0, r1, r2, r3 *uint32) { t0 := *r0 | *r1 t1 := *r1 ^ *r2 t2 := *r1 & t1 t3 := *r0 ^ t2 t4 := *r2 ^ t3 t5 := *r3 | t3 *r0 = t1 ^ t5 t6 := t1 | t5 t7 := *r3 ^ t6 *r2 = t4 ^ t7 t8 := t0 ^ t7 t9 := *r0 & t8 *r3 = t3 ^ t9 *r1 = *r3 ^ (*r0 ^ t8) } // S-Box 4 func sb4(r0, r1, r2, r3 *uint32) { v0 := *r0 // save r0 t0 := v0 ^ *r3 t1 := *r3 & t0 t2 := *r2 ^ t1 t3 := *r1 | t2 *r3 = t0 ^ t3 t4 := ^(*r1) t5 := t0 | t4 *r0 = t2 ^ t5 t6 := v0 & *r0 t7 := t0 ^ t4 t8 := t3 & t7 *r2 = t6 ^ t8 *r1 = (v0 ^ t2) ^ (t7 & *r2) } // Inverse S-Box 4 func sb4Inv(r0, r1, r2, r3 *uint32) { v3 := *r3 // save r3 t0 := *r2 | v3 t1 := *r0 & t0 t2 := *r1 ^ t1 t3 := *r0 & t2 t4 := *r2 ^ t3 *r1 = v3 ^ t4 t5 := ^(*r0) t6 := t4 & *r1 *r3 = t2 ^ t6 t7 := *r1 | t5 t8 := v3 ^ t7 *r0 = *r3 ^ t8 *r2 = (t2 & t8) ^ (*r1 ^ t5) } // S-Box 5 func sb5(r0, r1, r2, r3 *uint32) { v1 := *r1 // save r1 t0 := ^(*r0) t1 := *r0 ^ v1 t2 := *r0 ^ *r3 t3 := *r2 ^ t0 t4 := t1 | t2 *r0 = t3 ^ t4 t5 := *r3 & *r0 t6 := t1 ^ *r0 *r1 = t5 ^ t6 t7 := t0 | *r0 t8 := t1 | t5 t9 := t2 ^ t7 *r2 = t8 ^ t9 *r3 = (v1 ^ t5) ^ (*r1 & t9) } // Inverse S-Box 5 func sb5Inv(r0, r1, r2, r3 *uint32) { v0 := *r0 // save r0 v1 := *r1 // save r1 v3 := *r3 // save r3 t0 := ^(*r2) t1 := v1 & t0 t2 := v3 ^ t1 t3 := v0 & t2 t4 := v1 ^ t0 *r3 = t3 ^ t4 t5 := v1 | *r3 t6 := v0 & t5 *r1 = t2 ^ t6 t7 := v0 | v3 t8 := t0 ^ t5 *r0 = t7 ^ t8 *r2 = (v1 & t7) ^ (t3 | (v0 ^ *r2)) } // S-Box 6 func sb6(r0, r1, r2, r3 *uint32) { t0 := ^(*r0) t1 := *r0 ^ *r3 t2 := *r1 ^ t1 t3 := t0 | t1 t4 := *r2 ^ t3 *r1 = *r1 ^ t4 t5 := t1 | *r1 t6 := *r3 ^ t5 t7 := t4 & t6 *r2 = t2 ^ t7 t8 := t4 ^ t6 *r0 = *r2 ^ t8 *r3 = (^t4) ^ (t2 & t8) } // Inverse S-Box 6 func sb6Inv(r0, r1, r2, r3 *uint32) { v1 := *r1 // save r1 v3 := *r3 // save r3 t0 := ^(*r0) t1 := *r0 ^ v1 t2 := *r2 ^ t1 t3 := *r2 | t0 t4 := v3 ^ t3 *r1 = t2 ^ t4 t5 := t2 & t4 t6 := t1 ^ t5 t7 := v1 | t6 *r3 = t4 ^ t7 t8 := v1 | *r3 *r0 = t6 ^ t8 *r2 = (v3 & t0) ^ (t2 ^ t8) } // S-Box 7 func sb7(r0, r1, r2, r3 *uint32) { t0 := *r1 ^ *r2 t1 := *r2 & t0 t2 := *r3 ^ t1 t3 := *r0 ^ t2 t4 := *r3 | t0 t5 := t3 & t4 *r1 = *r1 ^ t5 t6 := t2 | *r1 t7 := *r0 & t3 *r3 = t0 ^ t7 t8 := t3 ^ t6 t9 := *r3 & t8 *r2 = t2 ^ t9 *r0 = (^t8) ^ (*r3 & *r2) } // Inverse S-Box 7 func sb7Inv(r0, r1, r2, r3 *uint32) { v0 := *r0 // save r0 v3 := *r3 // save r3 t0 := *r2 | (v0 & *r1) t1 := v3 & (v0 | *r1) *r3 = t0 ^ t1 t2 := ^v3 t3 := *r1 ^ t1 t4 := t3 | (*r3 ^ t2) *r1 = v0 ^ t4 *r0 = (*r2 ^ t3) ^ (v3 | *r1) *r2 = (t0 ^ *r1) ^ (*r0 ^ (v0 & *r3)) } ================================================ FILE: vendor/github.com/aead/serpent/serpent.go ================================================ // Copyright (c) 2016 Andreas Auernhammer. All rights reserved. // Use of this source code is governed by a license that can be // found in the LICENSE file. // Package serpent implements the Serpent block cipher // submitted to the AES challenge. Serpent was designed by // Ross Anderson, Eli Biham und Lars Knudsen. // The block cipher takes a 128, 192 or 256 bit key and // has a block size of 128 bit. package serpent // import "github.com/aead/serpent" import ( "crypto/cipher" "errors" ) // BlockSize is the serpent block size in bytes. const BlockSize = 16 const phi = 0x9e3779b9 // The Serpent phi constant (sqrt(5) - 1) * 2**31 var errKeySize = errors.New("invalid key size") // NewCipher returns a new cipher.Block implementing the serpent block cipher. // The key argument must be 128, 192 or 256 bit (16, 24, 32 byte). func NewCipher(key []byte) (cipher.Block, error) { if k := len(key); k != 16 && k != 24 && k != 32 { return nil, errKeySize } s := &subkeys{} s.keySchedule(key) return s, nil } // The 132 32 bit subkeys of serpent type subkeys [132]uint32 func (s *subkeys) BlockSize() int { return BlockSize } func (s *subkeys) Encrypt(dst, src []byte) { if len(src) < BlockSize { panic("src buffer to small") } if len(dst) < BlockSize { panic("dst buffer to small") } encryptBlock(dst, src, s) } func (s *subkeys) Decrypt(dst, src []byte) { if len(src) < BlockSize { panic("src buffer to small") } if len(dst) < BlockSize { panic("dst buffer to small") } decryptBlock(dst, src, s) } // The key schedule of serpent. func (s *subkeys) keySchedule(key []byte) { var k [16]uint32 j := 0 for i := 0; i+4 <= len(key); i += 4 { k[j] = uint32(key[i]) | uint32(key[i+1])<<8 | uint32(key[i+2])<<16 | uint32(key[i+3])<<24 j++ } if j < 8 { k[j] = 1 } for i := 8; i < 16; i++ { x := k[i-8] ^ k[i-5] ^ k[i-3] ^ k[i-1] ^ phi ^ uint32(i-8) k[i] = (x << 11) | (x >> 21) s[i-8] = k[i] } for i := 8; i < 132; i++ { x := s[i-8] ^ s[i-5] ^ s[i-3] ^ s[i-1] ^ phi ^ uint32(i) s[i] = (x << 11) | (x >> 21) } sb3(&s[0], &s[1], &s[2], &s[3]) sb2(&s[4], &s[5], &s[6], &s[7]) sb1(&s[8], &s[9], &s[10], &s[11]) sb0(&s[12], &s[13], &s[14], &s[15]) sb7(&s[16], &s[17], &s[18], &s[19]) sb6(&s[20], &s[21], &s[22], &s[23]) sb5(&s[24], &s[25], &s[26], &s[27]) sb4(&s[28], &s[29], &s[30], &s[31]) sb3(&s[32], &s[33], &s[34], &s[35]) sb2(&s[36], &s[37], &s[38], &s[39]) sb1(&s[40], &s[41], &s[42], &s[43]) sb0(&s[44], &s[45], &s[46], &s[47]) sb7(&s[48], &s[49], &s[50], &s[51]) sb6(&s[52], &s[53], &s[54], &s[55]) sb5(&s[56], &s[57], &s[58], &s[59]) sb4(&s[60], &s[61], &s[62], &s[63]) sb3(&s[64], &s[65], &s[66], &s[67]) sb2(&s[68], &s[69], &s[70], &s[71]) sb1(&s[72], &s[73], &s[74], &s[75]) sb0(&s[76], &s[77], &s[78], &s[79]) sb7(&s[80], &s[81], &s[82], &s[83]) sb6(&s[84], &s[85], &s[86], &s[87]) sb5(&s[88], &s[89], &s[90], &s[91]) sb4(&s[92], &s[93], &s[94], &s[95]) sb3(&s[96], &s[97], &s[98], &s[99]) sb2(&s[100], &s[101], &s[102], &s[103]) sb1(&s[104], &s[105], &s[106], &s[107]) sb0(&s[108], &s[109], &s[110], &s[111]) sb7(&s[112], &s[113], &s[114], &s[115]) sb6(&s[116], &s[117], &s[118], &s[119]) sb5(&s[120], &s[121], &s[122], &s[123]) sb4(&s[124], &s[125], &s[126], &s[127]) sb3(&s[128], &s[129], &s[130], &s[131]) } ================================================ FILE: vendor/github.com/aead/serpent/serpent_ref.go ================================================ // Copyright (c) 2016 Andreas Auernhammer. All rights reserved. // Use of this source code is governed by a license that can be // found in the LICENSE file. package serpent // Encrypts one block with the given 132 sub-keys sk. func encryptBlock(dst, src []byte, sk *subkeys) { // Transform the input block to 4 x 32 bit registers r0 := uint32(src[0]) | uint32(src[1])<<8 | uint32(src[2])<<16 | uint32(src[3])<<24 r1 := uint32(src[4]) | uint32(src[5])<<8 | uint32(src[6])<<16 | uint32(src[7])<<24 r2 := uint32(src[8]) | uint32(src[9])<<8 | uint32(src[10])<<16 | uint32(src[11])<<24 r3 := uint32(src[12]) | uint32(src[13])<<8 | uint32(src[14])<<16 | uint32(src[15])<<24 // Encrypt the block with the 132 sub-keys and 8 S-Boxes r0, r1, r2, r3 = r0^sk[0], r1^sk[1], r2^sk[2], r3^sk[3] sb0(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[4], r1^sk[5], r2^sk[6], r3^sk[7] sb1(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[8], r1^sk[9], r2^sk[10], r3^sk[11] sb2(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[12], r1^sk[13], r2^sk[14], r3^sk[15] sb3(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[16], r1^sk[17], r2^sk[18], r3^sk[19] sb4(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[20], r1^sk[21], r2^sk[22], r3^sk[23] sb5(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[24], r1^sk[25], r2^sk[26], r3^sk[27] sb6(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[28], r1^sk[29], r2^sk[30], r3^sk[31] sb7(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[32], r1^sk[33], r2^sk[34], r3^sk[35] sb0(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[36], r1^sk[37], r2^sk[38], r3^sk[39] sb1(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[40], r1^sk[41], r2^sk[42], r3^sk[43] sb2(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[44], r1^sk[45], r2^sk[46], r3^sk[47] sb3(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[48], r1^sk[49], r2^sk[50], r3^sk[51] sb4(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[52], r1^sk[53], r2^sk[54], r3^sk[55] sb5(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[56], r1^sk[57], r2^sk[58], r3^sk[59] sb6(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[60], r1^sk[61], r2^sk[62], r3^sk[63] sb7(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[64], r1^sk[65], r2^sk[66], r3^sk[67] sb0(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[68], r1^sk[69], r2^sk[70], r3^sk[71] sb1(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[72], r1^sk[73], r2^sk[74], r3^sk[75] sb2(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[76], r1^sk[77], r2^sk[78], r3^sk[79] sb3(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[80], r1^sk[81], r2^sk[82], r3^sk[83] sb4(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[84], r1^sk[85], r2^sk[86], r3^sk[87] sb5(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[88], r1^sk[89], r2^sk[90], r3^sk[91] sb6(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[92], r1^sk[93], r2^sk[94], r3^sk[95] sb7(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[96], r1^sk[97], r2^sk[98], r3^sk[99] sb0(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[100], r1^sk[101], r2^sk[102], r3^sk[103] sb1(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[104], r1^sk[105], r2^sk[106], r3^sk[107] sb2(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[108], r1^sk[109], r2^sk[110], r3^sk[111] sb3(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[112], r1^sk[113], r2^sk[114], r3^sk[115] sb4(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[116], r1^sk[117], r2^sk[118], r3^sk[119] sb5(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[120], r1^sk[121], r2^sk[122], r3^sk[123] sb6(&r0, &r1, &r2, &r3) linear(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[124], r1^sk[125], r2^sk[126], r3^sk[127] sb7(&r0, &r1, &r2, &r3) // whitening r0 ^= sk[128] r1 ^= sk[129] r2 ^= sk[130] r3 ^= sk[131] // write the encrypted block to the output dst[0] = byte(r0) dst[1] = byte(r0 >> 8) dst[2] = byte(r0 >> 16) dst[3] = byte(r0 >> 24) dst[4] = byte(r1) dst[5] = byte(r1 >> 8) dst[6] = byte(r1 >> 16) dst[7] = byte(r1 >> 24) dst[8] = byte(r2) dst[9] = byte(r2 >> 8) dst[10] = byte(r2 >> 16) dst[11] = byte(r2 >> 24) dst[12] = byte(r3) dst[13] = byte(r3 >> 8) dst[14] = byte(r3 >> 16) dst[15] = byte(r3 >> 24) } // Decrypts one block with the given 132 sub-keys sk. func decryptBlock(dst, src []byte, sk *subkeys) { // Transform the input block to 4 x 32 bit registers r0 := uint32(src[0]) | uint32(src[1])<<8 | uint32(src[2])<<16 | uint32(src[3])<<24 r1 := uint32(src[4]) | uint32(src[5])<<8 | uint32(src[6])<<16 | uint32(src[7])<<24 r2 := uint32(src[8]) | uint32(src[9])<<8 | uint32(src[10])<<16 | uint32(src[11])<<24 r3 := uint32(src[12]) | uint32(src[13])<<8 | uint32(src[14])<<16 | uint32(src[15])<<24 // undo whitening r0 ^= sk[128] r1 ^= sk[129] r2 ^= sk[130] r3 ^= sk[131] // Decrypt the block with the 132 sub-keys and 8 S-Boxes sb7Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[124], r1^sk[125], r2^sk[126], r3^sk[127] linearInv(&r0, &r1, &r2, &r3) sb6Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[120], r1^sk[121], r2^sk[122], r3^sk[123] linearInv(&r0, &r1, &r2, &r3) sb5Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[116], r1^sk[117], r2^sk[118], r3^sk[119] linearInv(&r0, &r1, &r2, &r3) sb4Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[112], r1^sk[113], r2^sk[114], r3^sk[115] linearInv(&r0, &r1, &r2, &r3) sb3Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[108], r1^sk[109], r2^sk[110], r3^sk[111] linearInv(&r0, &r1, &r2, &r3) sb2Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[104], r1^sk[105], r2^sk[106], r3^sk[107] linearInv(&r0, &r1, &r2, &r3) sb1Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[100], r1^sk[101], r2^sk[102], r3^sk[103] linearInv(&r0, &r1, &r2, &r3) sb0Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[96], r1^sk[97], r2^sk[98], r3^sk[99] linearInv(&r0, &r1, &r2, &r3) sb7Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[92], r1^sk[93], r2^sk[94], r3^sk[95] linearInv(&r0, &r1, &r2, &r3) sb6Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[88], r1^sk[89], r2^sk[90], r3^sk[91] linearInv(&r0, &r1, &r2, &r3) sb5Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[84], r1^sk[85], r2^sk[86], r3^sk[87] linearInv(&r0, &r1, &r2, &r3) sb4Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[80], r1^sk[81], r2^sk[82], r3^sk[83] linearInv(&r0, &r1, &r2, &r3) sb3Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[76], r1^sk[77], r2^sk[78], r3^sk[79] linearInv(&r0, &r1, &r2, &r3) sb2Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[72], r1^sk[73], r2^sk[74], r3^sk[75] linearInv(&r0, &r1, &r2, &r3) sb1Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[68], r1^sk[69], r2^sk[70], r3^sk[71] linearInv(&r0, &r1, &r2, &r3) sb0Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[64], r1^sk[65], r2^sk[66], r3^sk[67] linearInv(&r0, &r1, &r2, &r3) sb7Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[60], r1^sk[61], r2^sk[62], r3^sk[63] linearInv(&r0, &r1, &r2, &r3) sb6Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[56], r1^sk[57], r2^sk[58], r3^sk[59] linearInv(&r0, &r1, &r2, &r3) sb5Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[52], r1^sk[53], r2^sk[54], r3^sk[55] linearInv(&r0, &r1, &r2, &r3) sb4Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[48], r1^sk[49], r2^sk[50], r3^sk[51] linearInv(&r0, &r1, &r2, &r3) sb3Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[44], r1^sk[45], r2^sk[46], r3^sk[47] linearInv(&r0, &r1, &r2, &r3) sb2Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[40], r1^sk[41], r2^sk[42], r3^sk[43] linearInv(&r0, &r1, &r2, &r3) sb1Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[36], r1^sk[37], r2^sk[38], r3^sk[39] linearInv(&r0, &r1, &r2, &r3) sb0Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[32], r1^sk[33], r2^sk[34], r3^sk[35] linearInv(&r0, &r1, &r2, &r3) sb7Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[28], r1^sk[29], r2^sk[30], r3^sk[31] linearInv(&r0, &r1, &r2, &r3) sb6Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[24], r1^sk[25], r2^sk[26], r3^sk[27] linearInv(&r0, &r1, &r2, &r3) sb5Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[20], r1^sk[21], r2^sk[22], r3^sk[23] linearInv(&r0, &r1, &r2, &r3) sb4Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[16], r1^sk[17], r2^sk[18], r3^sk[19] linearInv(&r0, &r1, &r2, &r3) sb3Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[12], r1^sk[13], r2^sk[14], r3^sk[15] linearInv(&r0, &r1, &r2, &r3) sb2Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[8], r1^sk[9], r2^sk[10], r3^sk[11] linearInv(&r0, &r1, &r2, &r3) sb1Inv(&r0, &r1, &r2, &r3) r0, r1, r2, r3 = r0^sk[4], r1^sk[5], r2^sk[6], r3^sk[7] linearInv(&r0, &r1, &r2, &r3) sb0Inv(&r0, &r1, &r2, &r3) r0 ^= sk[0] r1 ^= sk[1] r2 ^= sk[2] r3 ^= sk[3] // write the decrypted block to the output dst[0] = byte(r0) dst[1] = byte(r0 >> 8) dst[2] = byte(r0 >> 16) dst[3] = byte(r0 >> 24) dst[4] = byte(r1) dst[5] = byte(r1 >> 8) dst[6] = byte(r1 >> 16) dst[7] = byte(r1 >> 24) dst[8] = byte(r2) dst[9] = byte(r2 >> 8) dst[10] = byte(r2 >> 16) dst[11] = byte(r2 >> 24) dst[12] = byte(r3) dst[13] = byte(r3 >> 8) dst[14] = byte(r3 >> 16) dst[15] = byte(r3 >> 24) } ================================================ FILE: vendor/github.com/cespare/xxhash/v2/LICENSE.txt ================================================ Copyright (c) 2016 Caleb Spare MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: vendor/github.com/cespare/xxhash/v2/README.md ================================================ # xxhash [![Go Reference](https://pkg.go.dev/badge/github.com/cespare/xxhash/v2.svg)](https://pkg.go.dev/github.com/cespare/xxhash/v2) [![Test](https://github.com/cespare/xxhash/actions/workflows/test.yml/badge.svg)](https://github.com/cespare/xxhash/actions/workflows/test.yml) xxhash is a Go implementation of the 64-bit [xxHash] algorithm, XXH64. This is a high-quality hashing algorithm that is much faster than anything in the Go standard library. This package provides a straightforward API: ``` func Sum64(b []byte) uint64 func Sum64String(s string) uint64 type Digest struct{ ... } func New() *Digest ``` The `Digest` type implements hash.Hash64. Its key methods are: ``` func (*Digest) Write([]byte) (int, error) func (*Digest) WriteString(string) (int, error) func (*Digest) Sum64() uint64 ``` The package is written with optimized pure Go and also contains even faster assembly implementations for amd64 and arm64. If desired, the `purego` build tag opts into using the Go code even on those architectures. [xxHash]: http://cyan4973.github.io/xxHash/ ## Compatibility This package is in a module and the latest code is in version 2 of the module. You need a version of Go with at least "minimal module compatibility" to use github.com/cespare/xxhash/v2: * 1.9.7+ for Go 1.9 * 1.10.3+ for Go 1.10 * Go 1.11 or later I recommend using the latest release of Go. ## Benchmarks Here are some quick benchmarks comparing the pure-Go and assembly implementations of Sum64. | input size | purego | asm | | ---------- | --------- | --------- | | 4 B | 1.3 GB/s | 1.2 GB/s | | 16 B | 2.9 GB/s | 3.5 GB/s | | 100 B | 6.9 GB/s | 8.1 GB/s | | 4 KB | 11.7 GB/s | 16.7 GB/s | | 10 MB | 12.0 GB/s | 17.3 GB/s | These numbers were generated on Ubuntu 20.04 with an Intel Xeon Platinum 8252C CPU using the following commands under Go 1.19.2: ``` benchstat <(go test -tags purego -benchtime 500ms -count 15 -bench 'Sum64$') benchstat <(go test -benchtime 500ms -count 15 -bench 'Sum64$') ``` ## Projects using this package - [InfluxDB](https://github.com/influxdata/influxdb) - [Prometheus](https://github.com/prometheus/prometheus) - [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics) - [FreeCache](https://github.com/coocood/freecache) - [FastCache](https://github.com/VictoriaMetrics/fastcache) - [Ristretto](https://github.com/dgraph-io/ristretto) - [Badger](https://github.com/dgraph-io/badger) ================================================ FILE: vendor/github.com/cespare/xxhash/v2/testall.sh ================================================ #!/bin/bash set -eu -o pipefail # Small convenience script for running the tests with various combinations of # arch/tags. This assumes we're running on amd64 and have qemu available. go test ./... go test -tags purego ./... GOARCH=arm64 go test GOARCH=arm64 go test -tags purego ================================================ FILE: vendor/github.com/cespare/xxhash/v2/xxhash.go ================================================ // Package xxhash implements the 64-bit variant of xxHash (XXH64) as described // at http://cyan4973.github.io/xxHash/. package xxhash import ( "encoding/binary" "errors" "math/bits" ) const ( prime1 uint64 = 11400714785074694791 prime2 uint64 = 14029467366897019727 prime3 uint64 = 1609587929392839161 prime4 uint64 = 9650029242287828579 prime5 uint64 = 2870177450012600261 ) // Store the primes in an array as well. // // The consts are used when possible in Go code to avoid MOVs but we need a // contiguous array for the assembly code. var primes = [...]uint64{prime1, prime2, prime3, prime4, prime5} // Digest implements hash.Hash64. // // Note that a zero-valued Digest is not ready to receive writes. // Call Reset or create a Digest using New before calling other methods. type Digest struct { v1 uint64 v2 uint64 v3 uint64 v4 uint64 total uint64 mem [32]byte n int // how much of mem is used } // New creates a new Digest with a zero seed. func New() *Digest { return NewWithSeed(0) } // NewWithSeed creates a new Digest with the given seed. func NewWithSeed(seed uint64) *Digest { var d Digest d.ResetWithSeed(seed) return &d } // Reset clears the Digest's state so that it can be reused. // It uses a seed value of zero. func (d *Digest) Reset() { d.ResetWithSeed(0) } // ResetWithSeed clears the Digest's state so that it can be reused. // It uses the given seed to initialize the state. func (d *Digest) ResetWithSeed(seed uint64) { d.v1 = seed + prime1 + prime2 d.v2 = seed + prime2 d.v3 = seed d.v4 = seed - prime1 d.total = 0 d.n = 0 } // Size always returns 8 bytes. func (d *Digest) Size() int { return 8 } // BlockSize always returns 32 bytes. func (d *Digest) BlockSize() int { return 32 } // Write adds more data to d. It always returns len(b), nil. func (d *Digest) Write(b []byte) (n int, err error) { n = len(b) d.total += uint64(n) memleft := d.mem[d.n&(len(d.mem)-1):] if d.n+n < 32 { // This new data doesn't even fill the current block. copy(memleft, b) d.n += n return } if d.n > 0 { // Finish off the partial block. c := copy(memleft, b) d.v1 = round(d.v1, u64(d.mem[0:8])) d.v2 = round(d.v2, u64(d.mem[8:16])) d.v3 = round(d.v3, u64(d.mem[16:24])) d.v4 = round(d.v4, u64(d.mem[24:32])) b = b[c:] d.n = 0 } if len(b) >= 32 { // One or more full blocks left. nw := writeBlocks(d, b) b = b[nw:] } // Store any remaining partial block. copy(d.mem[:], b) d.n = len(b) return } // Sum appends the current hash to b and returns the resulting slice. func (d *Digest) Sum(b []byte) []byte { s := d.Sum64() return append( b, byte(s>>56), byte(s>>48), byte(s>>40), byte(s>>32), byte(s>>24), byte(s>>16), byte(s>>8), byte(s), ) } // Sum64 returns the current hash. func (d *Digest) Sum64() uint64 { var h uint64 if d.total >= 32 { v1, v2, v3, v4 := d.v1, d.v2, d.v3, d.v4 h = rol1(v1) + rol7(v2) + rol12(v3) + rol18(v4) h = mergeRound(h, v1) h = mergeRound(h, v2) h = mergeRound(h, v3) h = mergeRound(h, v4) } else { h = d.v3 + prime5 } h += d.total b := d.mem[:d.n&(len(d.mem)-1)] for ; len(b) >= 8; b = b[8:] { k1 := round(0, u64(b[:8])) h ^= k1 h = rol27(h)*prime1 + prime4 } if len(b) >= 4 { h ^= uint64(u32(b[:4])) * prime1 h = rol23(h)*prime2 + prime3 b = b[4:] } for ; len(b) > 0; b = b[1:] { h ^= uint64(b[0]) * prime5 h = rol11(h) * prime1 } h ^= h >> 33 h *= prime2 h ^= h >> 29 h *= prime3 h ^= h >> 32 return h } const ( magic = "xxh\x06" marshaledSize = len(magic) + 8*5 + 32 ) // MarshalBinary implements the encoding.BinaryMarshaler interface. func (d *Digest) MarshalBinary() ([]byte, error) { b := make([]byte, 0, marshaledSize) b = append(b, magic...) b = appendUint64(b, d.v1) b = appendUint64(b, d.v2) b = appendUint64(b, d.v3) b = appendUint64(b, d.v4) b = appendUint64(b, d.total) b = append(b, d.mem[:d.n]...) b = b[:len(b)+len(d.mem)-d.n] return b, nil } // UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. func (d *Digest) UnmarshalBinary(b []byte) error { if len(b) < len(magic) || string(b[:len(magic)]) != magic { return errors.New("xxhash: invalid hash state identifier") } if len(b) != marshaledSize { return errors.New("xxhash: invalid hash state size") } b = b[len(magic):] b, d.v1 = consumeUint64(b) b, d.v2 = consumeUint64(b) b, d.v3 = consumeUint64(b) b, d.v4 = consumeUint64(b) b, d.total = consumeUint64(b) copy(d.mem[:], b) d.n = int(d.total % uint64(len(d.mem))) return nil } func appendUint64(b []byte, x uint64) []byte { var a [8]byte binary.LittleEndian.PutUint64(a[:], x) return append(b, a[:]...) } func consumeUint64(b []byte) ([]byte, uint64) { x := u64(b) return b[8:], x } func u64(b []byte) uint64 { return binary.LittleEndian.Uint64(b) } func u32(b []byte) uint32 { return binary.LittleEndian.Uint32(b) } func round(acc, input uint64) uint64 { acc += input * prime2 acc = rol31(acc) acc *= prime1 return acc } func mergeRound(acc, val uint64) uint64 { val = round(0, val) acc ^= val acc = acc*prime1 + prime4 return acc } func rol1(x uint64) uint64 { return bits.RotateLeft64(x, 1) } func rol7(x uint64) uint64 { return bits.RotateLeft64(x, 7) } func rol11(x uint64) uint64 { return bits.RotateLeft64(x, 11) } func rol12(x uint64) uint64 { return bits.RotateLeft64(x, 12) } func rol18(x uint64) uint64 { return bits.RotateLeft64(x, 18) } func rol23(x uint64) uint64 { return bits.RotateLeft64(x, 23) } func rol27(x uint64) uint64 { return bits.RotateLeft64(x, 27) } func rol31(x uint64) uint64 { return bits.RotateLeft64(x, 31) } ================================================ FILE: vendor/github.com/cespare/xxhash/v2/xxhash_amd64.s ================================================ //go:build !appengine && gc && !purego // +build !appengine // +build gc // +build !purego #include "textflag.h" // Registers: #define h AX #define d AX #define p SI // pointer to advance through b #define n DX #define end BX // loop end #define v1 R8 #define v2 R9 #define v3 R10 #define v4 R11 #define x R12 #define prime1 R13 #define prime2 R14 #define prime4 DI #define round(acc, x) \ IMULQ prime2, x \ ADDQ x, acc \ ROLQ $31, acc \ IMULQ prime1, acc // round0 performs the operation x = round(0, x). #define round0(x) \ IMULQ prime2, x \ ROLQ $31, x \ IMULQ prime1, x // mergeRound applies a merge round on the two registers acc and x. // It assumes that prime1, prime2, and prime4 have been loaded. #define mergeRound(acc, x) \ round0(x) \ XORQ x, acc \ IMULQ prime1, acc \ ADDQ prime4, acc // blockLoop processes as many 32-byte blocks as possible, // updating v1, v2, v3, and v4. It assumes that there is at least one block // to process. #define blockLoop() \ loop: \ MOVQ +0(p), x \ round(v1, x) \ MOVQ +8(p), x \ round(v2, x) \ MOVQ +16(p), x \ round(v3, x) \ MOVQ +24(p), x \ round(v4, x) \ ADDQ $32, p \ CMPQ p, end \ JLE loop // func Sum64(b []byte) uint64 TEXT ·Sum64(SB), NOSPLIT|NOFRAME, $0-32 // Load fixed primes. MOVQ ·primes+0(SB), prime1 MOVQ ·primes+8(SB), prime2 MOVQ ·primes+24(SB), prime4 // Load slice. MOVQ b_base+0(FP), p MOVQ b_len+8(FP), n LEAQ (p)(n*1), end // The first loop limit will be len(b)-32. SUBQ $32, end // Check whether we have at least one block. CMPQ n, $32 JLT noBlocks // Set up initial state (v1, v2, v3, v4). MOVQ prime1, v1 ADDQ prime2, v1 MOVQ prime2, v2 XORQ v3, v3 XORQ v4, v4 SUBQ prime1, v4 blockLoop() MOVQ v1, h ROLQ $1, h MOVQ v2, x ROLQ $7, x ADDQ x, h MOVQ v3, x ROLQ $12, x ADDQ x, h MOVQ v4, x ROLQ $18, x ADDQ x, h mergeRound(h, v1) mergeRound(h, v2) mergeRound(h, v3) mergeRound(h, v4) JMP afterBlocks noBlocks: MOVQ ·primes+32(SB), h afterBlocks: ADDQ n, h ADDQ $24, end CMPQ p, end JG try4 loop8: MOVQ (p), x ADDQ $8, p round0(x) XORQ x, h ROLQ $27, h IMULQ prime1, h ADDQ prime4, h CMPQ p, end JLE loop8 try4: ADDQ $4, end CMPQ p, end JG try1 MOVL (p), x ADDQ $4, p IMULQ prime1, x XORQ x, h ROLQ $23, h IMULQ prime2, h ADDQ ·primes+16(SB), h try1: ADDQ $4, end CMPQ p, end JGE finalize loop1: MOVBQZX (p), x ADDQ $1, p IMULQ ·primes+32(SB), x XORQ x, h ROLQ $11, h IMULQ prime1, h CMPQ p, end JL loop1 finalize: MOVQ h, x SHRQ $33, x XORQ x, h IMULQ prime2, h MOVQ h, x SHRQ $29, x XORQ x, h IMULQ ·primes+16(SB), h MOVQ h, x SHRQ $32, x XORQ x, h MOVQ h, ret+24(FP) RET // func writeBlocks(d *Digest, b []byte) int TEXT ·writeBlocks(SB), NOSPLIT|NOFRAME, $0-40 // Load fixed primes needed for round. MOVQ ·primes+0(SB), prime1 MOVQ ·primes+8(SB), prime2 // Load slice. MOVQ b_base+8(FP), p MOVQ b_len+16(FP), n LEAQ (p)(n*1), end SUBQ $32, end // Load vN from d. MOVQ s+0(FP), d MOVQ 0(d), v1 MOVQ 8(d), v2 MOVQ 16(d), v3 MOVQ 24(d), v4 // We don't need to check the loop condition here; this function is // always called with at least one block of data to process. blockLoop() // Copy vN back to d. MOVQ v1, 0(d) MOVQ v2, 8(d) MOVQ v3, 16(d) MOVQ v4, 24(d) // The number of bytes written is p minus the old base pointer. SUBQ b_base+8(FP), p MOVQ p, ret+32(FP) RET ================================================ FILE: vendor/github.com/cespare/xxhash/v2/xxhash_arm64.s ================================================ //go:build !appengine && gc && !purego // +build !appengine // +build gc // +build !purego #include "textflag.h" // Registers: #define digest R1 #define h R2 // return value #define p R3 // input pointer #define n R4 // input length #define nblocks R5 // n / 32 #define prime1 R7 #define prime2 R8 #define prime3 R9 #define prime4 R10 #define prime5 R11 #define v1 R12 #define v2 R13 #define v3 R14 #define v4 R15 #define x1 R20 #define x2 R21 #define x3 R22 #define x4 R23 #define round(acc, x) \ MADD prime2, acc, x, acc \ ROR $64-31, acc \ MUL prime1, acc // round0 performs the operation x = round(0, x). #define round0(x) \ MUL prime2, x \ ROR $64-31, x \ MUL prime1, x #define mergeRound(acc, x) \ round0(x) \ EOR x, acc \ MADD acc, prime4, prime1, acc // blockLoop processes as many 32-byte blocks as possible, // updating v1, v2, v3, and v4. It assumes that n >= 32. #define blockLoop() \ LSR $5, n, nblocks \ PCALIGN $16 \ loop: \ LDP.P 16(p), (x1, x2) \ LDP.P 16(p), (x3, x4) \ round(v1, x1) \ round(v2, x2) \ round(v3, x3) \ round(v4, x4) \ SUB $1, nblocks \ CBNZ nblocks, loop // func Sum64(b []byte) uint64 TEXT ·Sum64(SB), NOSPLIT|NOFRAME, $0-32 LDP b_base+0(FP), (p, n) LDP ·primes+0(SB), (prime1, prime2) LDP ·primes+16(SB), (prime3, prime4) MOVD ·primes+32(SB), prime5 CMP $32, n CSEL LT, prime5, ZR, h // if n < 32 { h = prime5 } else { h = 0 } BLT afterLoop ADD prime1, prime2, v1 MOVD prime2, v2 MOVD $0, v3 NEG prime1, v4 blockLoop() ROR $64-1, v1, x1 ROR $64-7, v2, x2 ADD x1, x2 ROR $64-12, v3, x3 ROR $64-18, v4, x4 ADD x3, x4 ADD x2, x4, h mergeRound(h, v1) mergeRound(h, v2) mergeRound(h, v3) mergeRound(h, v4) afterLoop: ADD n, h TBZ $4, n, try8 LDP.P 16(p), (x1, x2) round0(x1) // NOTE: here and below, sequencing the EOR after the ROR (using a // rotated register) is worth a small but measurable speedup for small // inputs. ROR $64-27, h EOR x1 @> 64-27, h, h MADD h, prime4, prime1, h round0(x2) ROR $64-27, h EOR x2 @> 64-27, h, h MADD h, prime4, prime1, h try8: TBZ $3, n, try4 MOVD.P 8(p), x1 round0(x1) ROR $64-27, h EOR x1 @> 64-27, h, h MADD h, prime4, prime1, h try4: TBZ $2, n, try2 MOVWU.P 4(p), x2 MUL prime1, x2 ROR $64-23, h EOR x2 @> 64-23, h, h MADD h, prime3, prime2, h try2: TBZ $1, n, try1 MOVHU.P 2(p), x3 AND $255, x3, x1 LSR $8, x3, x2 MUL prime5, x1 ROR $64-11, h EOR x1 @> 64-11, h, h MUL prime1, h MUL prime5, x2 ROR $64-11, h EOR x2 @> 64-11, h, h MUL prime1, h try1: TBZ $0, n, finalize MOVBU (p), x4 MUL prime5, x4 ROR $64-11, h EOR x4 @> 64-11, h, h MUL prime1, h finalize: EOR h >> 33, h MUL prime2, h EOR h >> 29, h MUL prime3, h EOR h >> 32, h MOVD h, ret+24(FP) RET // func writeBlocks(d *Digest, b []byte) int TEXT ·writeBlocks(SB), NOSPLIT|NOFRAME, $0-40 LDP ·primes+0(SB), (prime1, prime2) // Load state. Assume v[1-4] are stored contiguously. MOVD d+0(FP), digest LDP 0(digest), (v1, v2) LDP 16(digest), (v3, v4) LDP b_base+8(FP), (p, n) blockLoop() // Store updated state. STP (v1, v2), 0(digest) STP (v3, v4), 16(digest) BIC $31, n MOVD n, ret+32(FP) RET ================================================ FILE: vendor/github.com/cespare/xxhash/v2/xxhash_asm.go ================================================ //go:build (amd64 || arm64) && !appengine && gc && !purego // +build amd64 arm64 // +build !appengine // +build gc // +build !purego package xxhash // Sum64 computes the 64-bit xxHash digest of b with a zero seed. // //go:noescape func Sum64(b []byte) uint64 //go:noescape func writeBlocks(d *Digest, b []byte) int ================================================ FILE: vendor/github.com/cespare/xxhash/v2/xxhash_other.go ================================================ //go:build (!amd64 && !arm64) || appengine || !gc || purego // +build !amd64,!arm64 appengine !gc purego package xxhash // Sum64 computes the 64-bit xxHash digest of b with a zero seed. func Sum64(b []byte) uint64 { // A simpler version would be // d := New() // d.Write(b) // return d.Sum64() // but this is faster, particularly for small inputs. n := len(b) var h uint64 if n >= 32 { v1 := primes[0] + prime2 v2 := prime2 v3 := uint64(0) v4 := -primes[0] for len(b) >= 32 { v1 = round(v1, u64(b[0:8:len(b)])) v2 = round(v2, u64(b[8:16:len(b)])) v3 = round(v3, u64(b[16:24:len(b)])) v4 = round(v4, u64(b[24:32:len(b)])) b = b[32:len(b):len(b)] } h = rol1(v1) + rol7(v2) + rol12(v3) + rol18(v4) h = mergeRound(h, v1) h = mergeRound(h, v2) h = mergeRound(h, v3) h = mergeRound(h, v4) } else { h = prime5 } h += uint64(n) for ; len(b) >= 8; b = b[8:] { k1 := round(0, u64(b[:8])) h ^= k1 h = rol27(h)*prime1 + prime4 } if len(b) >= 4 { h ^= uint64(u32(b[:4])) * prime1 h = rol23(h)*prime2 + prime3 b = b[4:] } for ; len(b) > 0; b = b[1:] { h ^= uint64(b[0]) * prime5 h = rol11(h) * prime1 } h ^= h >> 33 h *= prime2 h ^= h >> 29 h *= prime3 h ^= h >> 32 return h } func writeBlocks(d *Digest, b []byte) int { v1, v2, v3, v4 := d.v1, d.v2, d.v3, d.v4 n := len(b) for len(b) >= 32 { v1 = round(v1, u64(b[0:8:len(b)])) v2 = round(v2, u64(b[8:16:len(b)])) v3 = round(v3, u64(b[16:24:len(b)])) v4 = round(v4, u64(b[24:32:len(b)])) b = b[32:len(b):len(b)] } d.v1, d.v2, d.v3, d.v4 = v1, v2, v3, v4 return n - len(b) } ================================================ FILE: vendor/github.com/cespare/xxhash/v2/xxhash_safe.go ================================================ //go:build appengine // +build appengine // This file contains the safe implementations of otherwise unsafe-using code. package xxhash // Sum64String computes the 64-bit xxHash digest of s with a zero seed. func Sum64String(s string) uint64 { return Sum64([]byte(s)) } // WriteString adds more data to d. It always returns len(s), nil. func (d *Digest) WriteString(s string) (n int, err error) { return d.Write([]byte(s)) } ================================================ FILE: vendor/github.com/cespare/xxhash/v2/xxhash_unsafe.go ================================================ //go:build !appengine // +build !appengine // This file encapsulates usage of unsafe. // xxhash_safe.go contains the safe implementations. package xxhash import ( "unsafe" ) // In the future it's possible that compiler optimizations will make these // XxxString functions unnecessary by realizing that calls such as // Sum64([]byte(s)) don't need to copy s. See https://go.dev/issue/2205. // If that happens, even if we keep these functions they can be replaced with // the trivial safe code. // NOTE: The usual way of doing an unsafe string-to-[]byte conversion is: // // var b []byte // bh := (*reflect.SliceHeader)(unsafe.Pointer(&b)) // bh.Data = (*reflect.StringHeader)(unsafe.Pointer(&s)).Data // bh.Len = len(s) // bh.Cap = len(s) // // Unfortunately, as of Go 1.15.3 the inliner's cost model assigns a high enough // weight to this sequence of expressions that any function that uses it will // not be inlined. Instead, the functions below use a different unsafe // conversion designed to minimize the inliner weight and allow both to be // inlined. There is also a test (TestInlining) which verifies that these are // inlined. // // See https://github.com/golang/go/issues/42739 for discussion. // Sum64String computes the 64-bit xxHash digest of s with a zero seed. // It may be faster than Sum64([]byte(s)) by avoiding a copy. func Sum64String(s string) uint64 { b := *(*[]byte)(unsafe.Pointer(&sliceHeader{s, len(s)})) return Sum64(b) } // WriteString adds more data to d. It always returns len(s), nil. // It may be faster than Write([]byte(s)) by avoiding a copy. func (d *Digest) WriteString(s string) (n int, err error) { d.Write(*(*[]byte)(unsafe.Pointer(&sliceHeader{s, len(s)}))) // d.Write always returns len(s), nil. // Ignoring the return output and returning these fixed values buys a // savings of 6 in the inliner's cost model. return len(s), nil } // sliceHeader is similar to reflect.SliceHeader, but it assumes that the layout // of the first two words is the same as the layout of a string. type sliceHeader struct { s string cap int } ================================================ FILE: vendor/github.com/chzyer/readline/.gitignore ================================================ .vscode/* ================================================ FILE: vendor/github.com/chzyer/readline/.travis.yml ================================================ language: go go: - 1.x script: - GOOS=windows go install github.com/chzyer/readline/example/... - GOOS=linux go install github.com/chzyer/readline/example/... - GOOS=darwin go install github.com/chzyer/readline/example/... - go test -race -v ================================================ FILE: vendor/github.com/chzyer/readline/CHANGELOG.md ================================================ # ChangeLog ### 1.4 - 2016-07-25 * [#60][60] Support dynamic autocompletion * Fix ANSI parser on Windows * Fix wrong column width in complete mode on Windows * Remove dependent package "golang.org/x/crypto/ssh/terminal" ### 1.3 - 2016-05-09 * [#38][38] add SetChildren for prefix completer interface * [#42][42] improve multiple lines compatibility * [#43][43] remove sub-package(runes) for gopkg compatibility * [#46][46] Auto complete with space prefixed line * [#48][48] support suspend process (ctrl+Z) * [#49][49] fix bug that check equals with previous command * [#53][53] Fix bug which causes integer divide by zero panicking when input buffer is empty ### 1.2 - 2016-03-05 * Add a demo for checking password strength [example/readline-pass-strength](https://github.com/chzyer/readline/blob/master/example/readline-pass-strength/readline-pass-strength.go), , written by [@sahib](https://github.com/sahib) * [#23][23], support stdin remapping * [#27][27], add a `UniqueEditLine` to `Config`, which will erase the editing line after user submited it, usually use in IM. * Add a demo for multiline [example/readline-multiline](https://github.com/chzyer/readline/blob/master/example/readline-multiline/readline-multiline.go) which can submit one SQL by multiple lines. * Supports performs even stdin/stdout is not a tty. * Add a new simple apis for single instance, check by [here](https://github.com/chzyer/readline/blob/master/std.go). It need to save history manually if using this api. * [#28][28], fixes the history is not working as expected. * [#33][33], vim mode now support `c`, `d`, `x (delete character)`, `r (replace character)` ### 1.1 - 2015-11-20 * [#12][12] Add support for key ``/``/`` * Only enter raw mode as needed (calling `Readline()`), program will receive signal(e.g. Ctrl+C) if not interact with `readline`. * Bugs fixed for `PrefixCompleter` * Press `Ctrl+D` in empty line will cause `io.EOF` in error, Press `Ctrl+C` in anytime will cause `ErrInterrupt` instead of `io.EOF`, this will privodes a shell-like user experience. * Customable Interrupt/EOF prompt in `Config` * [#17][17] Change atomic package to use 32bit function to let it runnable on arm 32bit devices * Provides a new password user experience(`readline.ReadPasswordEx()`). ### 1.0 - 2015-10-14 * Initial public release. [12]: https://github.com/chzyer/readline/pull/12 [17]: https://github.com/chzyer/readline/pull/17 [23]: https://github.com/chzyer/readline/pull/23 [27]: https://github.com/chzyer/readline/pull/27 [28]: https://github.com/chzyer/readline/pull/28 [33]: https://github.com/chzyer/readline/pull/33 [38]: https://github.com/chzyer/readline/pull/38 [42]: https://github.com/chzyer/readline/pull/42 [43]: https://github.com/chzyer/readline/pull/43 [46]: https://github.com/chzyer/readline/pull/46 [48]: https://github.com/chzyer/readline/pull/48 [49]: https://github.com/chzyer/readline/pull/49 [53]: https://github.com/chzyer/readline/pull/53 [60]: https://github.com/chzyer/readline/pull/60 ================================================ FILE: vendor/github.com/chzyer/readline/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 Chzyer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: vendor/github.com/chzyer/readline/README.md ================================================ [![Build Status](https://travis-ci.org/chzyer/readline.svg?branch=master)](https://travis-ci.org/chzyer/readline) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE.md) [![Version](https://img.shields.io/github/tag/chzyer/readline.svg)](https://github.com/chzyer/readline/releases) [![GoDoc](https://godoc.org/github.com/chzyer/readline?status.svg)](https://godoc.org/github.com/chzyer/readline) [![OpenCollective](https://opencollective.com/readline/badge/backers.svg)](#backers) [![OpenCollective](https://opencollective.com/readline/badge/sponsors.svg)](#sponsors)

    A powerful readline library in `Linux` `macOS` `Windows` `Solaris` `AIX` ## Guide * [Demo](example/readline-demo/readline-demo.go) * [Shortcut](doc/shortcut.md) ## Repos using readline [![cockroachdb](https://img.shields.io/github/stars/cockroachdb/cockroach.svg?label=cockroachdb/cockroach)](https://github.com/cockroachdb/cockroach) [![robertkrimen/otto](https://img.shields.io/github/stars/robertkrimen/otto.svg?label=robertkrimen/otto)](https://github.com/robertkrimen/otto) [![empire](https://img.shields.io/github/stars/remind101/empire.svg?label=remind101/empire)](https://github.com/remind101/empire) [![mehrdadrad/mylg](https://img.shields.io/github/stars/mehrdadrad/mylg.svg?label=mehrdadrad/mylg)](https://github.com/mehrdadrad/mylg) [![knq/usql](https://img.shields.io/github/stars/knq/usql.svg?label=knq/usql)](https://github.com/knq/usql) [![youtube/doorman](https://img.shields.io/github/stars/youtube/doorman.svg?label=youtube/doorman)](https://github.com/youtube/doorman) [![bom-d-van/harp](https://img.shields.io/github/stars/bom-d-van/harp.svg?label=bom-d-van/harp)](https://github.com/bom-d-van/harp) [![abiosoft/ishell](https://img.shields.io/github/stars/abiosoft/ishell.svg?label=abiosoft/ishell)](https://github.com/abiosoft/ishell) [![Netflix/hal-9001](https://img.shields.io/github/stars/Netflix/hal-9001.svg?label=Netflix/hal-9001)](https://github.com/Netflix/hal-9001) [![docker/go-p9p](https://img.shields.io/github/stars/docker/go-p9p.svg?label=docker/go-p9p)](https://github.com/docker/go-p9p) ## Feedback If you have any questions, please submit a github issue and any pull requests is welcomed :) * [https://twitter.com/chzyer](https://twitter.com/chzyer) * [http://weibo.com/2145262190](http://weibo.com/2145262190) ## Backers Love Readline? Help me keep it alive by donating funds to cover project expenses!
    [[Become a backer](https://opencollective.com/readline#backer)] ## Sponsors Become a sponsor and get your logo here on our Github page. [[Become a sponsor](https://opencollective.com/readline#sponsor)] ================================================ FILE: vendor/github.com/chzyer/readline/ansi_windows.go ================================================ // +build windows package readline import ( "bufio" "io" "strconv" "strings" "sync" "unicode/utf8" "unsafe" ) const ( _ = uint16(0) COLOR_FBLUE = 0x0001 COLOR_FGREEN = 0x0002 COLOR_FRED = 0x0004 COLOR_FINTENSITY = 0x0008 COLOR_BBLUE = 0x0010 COLOR_BGREEN = 0x0020 COLOR_BRED = 0x0040 COLOR_BINTENSITY = 0x0080 COMMON_LVB_UNDERSCORE = 0x8000 COMMON_LVB_BOLD = 0x0007 ) var ColorTableFg = []word{ 0, // 30: Black COLOR_FRED, // 31: Red COLOR_FGREEN, // 32: Green COLOR_FRED | COLOR_FGREEN, // 33: Yellow COLOR_FBLUE, // 34: Blue COLOR_FRED | COLOR_FBLUE, // 35: Magenta COLOR_FGREEN | COLOR_FBLUE, // 36: Cyan COLOR_FRED | COLOR_FBLUE | COLOR_FGREEN, // 37: White } var ColorTableBg = []word{ 0, // 40: Black COLOR_BRED, // 41: Red COLOR_BGREEN, // 42: Green COLOR_BRED | COLOR_BGREEN, // 43: Yellow COLOR_BBLUE, // 44: Blue COLOR_BRED | COLOR_BBLUE, // 45: Magenta COLOR_BGREEN | COLOR_BBLUE, // 46: Cyan COLOR_BRED | COLOR_BBLUE | COLOR_BGREEN, // 47: White } type ANSIWriter struct { target io.Writer wg sync.WaitGroup ctx *ANSIWriterCtx sync.Mutex } func NewANSIWriter(w io.Writer) *ANSIWriter { a := &ANSIWriter{ target: w, ctx: NewANSIWriterCtx(w), } return a } func (a *ANSIWriter) Close() error { a.wg.Wait() return nil } type ANSIWriterCtx struct { isEsc bool isEscSeq bool arg []string target *bufio.Writer wantFlush bool } func NewANSIWriterCtx(target io.Writer) *ANSIWriterCtx { return &ANSIWriterCtx{ target: bufio.NewWriter(target), } } func (a *ANSIWriterCtx) Flush() { a.target.Flush() } func (a *ANSIWriterCtx) process(r rune) bool { if a.wantFlush { if r == 0 || r == CharEsc { a.wantFlush = false a.target.Flush() } } if a.isEscSeq { a.isEscSeq = a.ioloopEscSeq(a.target, r, &a.arg) return true } switch r { case CharEsc: a.isEsc = true case '[': if a.isEsc { a.arg = nil a.isEscSeq = true a.isEsc = false break } fallthrough default: a.target.WriteRune(r) a.wantFlush = true } return true } func (a *ANSIWriterCtx) ioloopEscSeq(w *bufio.Writer, r rune, argptr *[]string) bool { arg := *argptr var err error if r >= 'A' && r <= 'D' { count := short(GetInt(arg, 1)) info, err := GetConsoleScreenBufferInfo() if err != nil { return false } switch r { case 'A': // up info.dwCursorPosition.y -= count case 'B': // down info.dwCursorPosition.y += count case 'C': // right info.dwCursorPosition.x += count case 'D': // left info.dwCursorPosition.x -= count } SetConsoleCursorPosition(&info.dwCursorPosition) return false } switch r { case 'J': killLines() case 'K': eraseLine() case 'm': color := word(0) for _, item := range arg { var c int c, err = strconv.Atoi(item) if err != nil { w.WriteString("[" + strings.Join(arg, ";") + "m") break } if c >= 30 && c < 40 { color ^= COLOR_FINTENSITY color |= ColorTableFg[c-30] } else if c >= 40 && c < 50 { color ^= COLOR_BINTENSITY color |= ColorTableBg[c-40] } else if c == 4 { color |= COMMON_LVB_UNDERSCORE | ColorTableFg[7] } else if c == 1 { color |= COMMON_LVB_BOLD | COLOR_FINTENSITY } else { // unknown code treat as reset color = ColorTableFg[7] } } if err != nil { break } kernel.SetConsoleTextAttribute(stdout, uintptr(color)) case '\007': // set title case ';': if len(arg) == 0 || arg[len(arg)-1] != "" { arg = append(arg, "") *argptr = arg } return true default: if len(arg) == 0 { arg = append(arg, "") } arg[len(arg)-1] += string(r) *argptr = arg return true } *argptr = nil return false } func (a *ANSIWriter) Write(b []byte) (int, error) { a.Lock() defer a.Unlock() off := 0 for len(b) > off { r, size := utf8.DecodeRune(b[off:]) if size == 0 { return off, io.ErrShortWrite } off += size a.ctx.process(r) } a.ctx.Flush() return off, nil } func killLines() error { sbi, err := GetConsoleScreenBufferInfo() if err != nil { return err } size := (sbi.dwCursorPosition.y - sbi.dwSize.y) * sbi.dwSize.x size += sbi.dwCursorPosition.x var written int kernel.FillConsoleOutputAttribute(stdout, uintptr(ColorTableFg[7]), uintptr(size), sbi.dwCursorPosition.ptr(), uintptr(unsafe.Pointer(&written)), ) return kernel.FillConsoleOutputCharacterW(stdout, uintptr(' '), uintptr(size), sbi.dwCursorPosition.ptr(), uintptr(unsafe.Pointer(&written)), ) } func eraseLine() error { sbi, err := GetConsoleScreenBufferInfo() if err != nil { return err } size := sbi.dwSize.x sbi.dwCursorPosition.x = 0 var written int return kernel.FillConsoleOutputCharacterW(stdout, uintptr(' '), uintptr(size), sbi.dwCursorPosition.ptr(), uintptr(unsafe.Pointer(&written)), ) } ================================================ FILE: vendor/github.com/chzyer/readline/complete.go ================================================ package readline import ( "bufio" "bytes" "fmt" "io" ) type AutoCompleter interface { // Readline will pass the whole line and current offset to it // Completer need to pass all the candidates, and how long they shared the same characters in line // Example: // [go, git, git-shell, grep] // Do("g", 1) => ["o", "it", "it-shell", "rep"], 1 // Do("gi", 2) => ["t", "t-shell"], 2 // Do("git", 3) => ["", "-shell"], 3 Do(line []rune, pos int) (newLine [][]rune, length int) } type TabCompleter struct{} func (t *TabCompleter) Do([]rune, int) ([][]rune, int) { return [][]rune{[]rune("\t")}, 0 } type opCompleter struct { w io.Writer op *Operation width int inCompleteMode bool inSelectMode bool candidate [][]rune candidateSource []rune candidateOff int candidateChoise int candidateColNum int } func newOpCompleter(w io.Writer, op *Operation, width int) *opCompleter { return &opCompleter{ w: w, op: op, width: width, } } func (o *opCompleter) doSelect() { if len(o.candidate) == 1 { o.op.buf.WriteRunes(o.candidate[0]) o.ExitCompleteMode(false) return } o.nextCandidate(1) o.CompleteRefresh() } func (o *opCompleter) nextCandidate(i int) { o.candidateChoise += i o.candidateChoise = o.candidateChoise % len(o.candidate) if o.candidateChoise < 0 { o.candidateChoise = len(o.candidate) + o.candidateChoise } } func (o *opCompleter) OnComplete() bool { if o.width == 0 { return false } if o.IsInCompleteSelectMode() { o.doSelect() return true } buf := o.op.buf rs := buf.Runes() if o.IsInCompleteMode() && o.candidateSource != nil && runes.Equal(rs, o.candidateSource) { o.EnterCompleteSelectMode() o.doSelect() return true } o.ExitCompleteSelectMode() o.candidateSource = rs newLines, offset := o.op.cfg.AutoComplete.Do(rs, buf.idx) if len(newLines) == 0 { o.ExitCompleteMode(false) return true } // only Aggregate candidates in non-complete mode if !o.IsInCompleteMode() { if len(newLines) == 1 { buf.WriteRunes(newLines[0]) o.ExitCompleteMode(false) return true } same, size := runes.Aggregate(newLines) if size > 0 { buf.WriteRunes(same) o.ExitCompleteMode(false) return true } } o.EnterCompleteMode(offset, newLines) return true } func (o *opCompleter) IsInCompleteSelectMode() bool { return o.inSelectMode } func (o *opCompleter) IsInCompleteMode() bool { return o.inCompleteMode } func (o *opCompleter) HandleCompleteSelect(r rune) bool { next := true switch r { case CharEnter, CharCtrlJ: next = false o.op.buf.WriteRunes(o.op.candidate[o.op.candidateChoise]) o.ExitCompleteMode(false) case CharLineStart: num := o.candidateChoise % o.candidateColNum o.nextCandidate(-num) case CharLineEnd: num := o.candidateColNum - o.candidateChoise%o.candidateColNum - 1 o.candidateChoise += num if o.candidateChoise >= len(o.candidate) { o.candidateChoise = len(o.candidate) - 1 } case CharBackspace: o.ExitCompleteSelectMode() next = false case CharTab, CharForward: o.doSelect() case CharBell, CharInterrupt: o.ExitCompleteMode(true) next = false case CharNext: tmpChoise := o.candidateChoise + o.candidateColNum if tmpChoise >= o.getMatrixSize() { tmpChoise -= o.getMatrixSize() } else if tmpChoise >= len(o.candidate) { tmpChoise += o.candidateColNum tmpChoise -= o.getMatrixSize() } o.candidateChoise = tmpChoise case CharBackward: o.nextCandidate(-1) case CharPrev: tmpChoise := o.candidateChoise - o.candidateColNum if tmpChoise < 0 { tmpChoise += o.getMatrixSize() if tmpChoise >= len(o.candidate) { tmpChoise -= o.candidateColNum } } o.candidateChoise = tmpChoise default: next = false o.ExitCompleteSelectMode() } if next { o.CompleteRefresh() return true } return false } func (o *opCompleter) getMatrixSize() int { line := len(o.candidate) / o.candidateColNum if len(o.candidate)%o.candidateColNum != 0 { line++ } return line * o.candidateColNum } func (o *opCompleter) OnWidthChange(newWidth int) { o.width = newWidth } func (o *opCompleter) CompleteRefresh() { if !o.inCompleteMode { return } lineCnt := o.op.buf.CursorLineCount() colWidth := 0 for _, c := range o.candidate { w := runes.WidthAll(c) if w > colWidth { colWidth = w } } colWidth += o.candidateOff + 1 same := o.op.buf.RuneSlice(-o.candidateOff) // -1 to avoid reach the end of line width := o.width - 1 colNum := width / colWidth if colNum != 0 { colWidth += (width - (colWidth * colNum)) / colNum } o.candidateColNum = colNum buf := bufio.NewWriter(o.w) buf.Write(bytes.Repeat([]byte("\n"), lineCnt)) colIdx := 0 lines := 1 buf.WriteString("\033[J") for idx, c := range o.candidate { inSelect := idx == o.candidateChoise && o.IsInCompleteSelectMode() if inSelect { buf.WriteString("\033[30;47m") } buf.WriteString(string(same)) buf.WriteString(string(c)) buf.Write(bytes.Repeat([]byte(" "), colWidth-runes.WidthAll(c)-runes.WidthAll(same))) if inSelect { buf.WriteString("\033[0m") } colIdx++ if colIdx == colNum { buf.WriteString("\n") lines++ colIdx = 0 } } // move back fmt.Fprintf(buf, "\033[%dA\r", lineCnt-1+lines) fmt.Fprintf(buf, "\033[%dC", o.op.buf.idx+o.op.buf.PromptLen()) buf.Flush() } func (o *opCompleter) aggCandidate(candidate [][]rune) int { offset := 0 for i := 0; i < len(candidate[0]); i++ { for j := 0; j < len(candidate)-1; j++ { if i > len(candidate[j]) { goto aggregate } if candidate[j][i] != candidate[j+1][i] { goto aggregate } } offset = i } aggregate: return offset } func (o *opCompleter) EnterCompleteSelectMode() { o.inSelectMode = true o.candidateChoise = -1 o.CompleteRefresh() } func (o *opCompleter) EnterCompleteMode(offset int, candidate [][]rune) { o.inCompleteMode = true o.candidate = candidate o.candidateOff = offset o.CompleteRefresh() } func (o *opCompleter) ExitCompleteSelectMode() { o.inSelectMode = false o.candidate = nil o.candidateChoise = -1 o.candidateOff = -1 o.candidateSource = nil } func (o *opCompleter) ExitCompleteMode(revent bool) { o.inCompleteMode = false o.ExitCompleteSelectMode() } ================================================ FILE: vendor/github.com/chzyer/readline/complete_helper.go ================================================ package readline import ( "bytes" "strings" ) // Caller type for dynamic completion type DynamicCompleteFunc func(string) []string type PrefixCompleterInterface interface { Print(prefix string, level int, buf *bytes.Buffer) Do(line []rune, pos int) (newLine [][]rune, length int) GetName() []rune GetChildren() []PrefixCompleterInterface SetChildren(children []PrefixCompleterInterface) } type DynamicPrefixCompleterInterface interface { PrefixCompleterInterface IsDynamic() bool GetDynamicNames(line []rune) [][]rune } type PrefixCompleter struct { Name []rune Dynamic bool Callback DynamicCompleteFunc Children []PrefixCompleterInterface } func (p *PrefixCompleter) Tree(prefix string) string { buf := bytes.NewBuffer(nil) p.Print(prefix, 0, buf) return buf.String() } func Print(p PrefixCompleterInterface, prefix string, level int, buf *bytes.Buffer) { if strings.TrimSpace(string(p.GetName())) != "" { buf.WriteString(prefix) if level > 0 { buf.WriteString("├") buf.WriteString(strings.Repeat("─", (level*4)-2)) buf.WriteString(" ") } buf.WriteString(string(p.GetName()) + "\n") level++ } for _, ch := range p.GetChildren() { ch.Print(prefix, level, buf) } } func (p *PrefixCompleter) Print(prefix string, level int, buf *bytes.Buffer) { Print(p, prefix, level, buf) } func (p *PrefixCompleter) IsDynamic() bool { return p.Dynamic } func (p *PrefixCompleter) GetName() []rune { return p.Name } func (p *PrefixCompleter) GetDynamicNames(line []rune) [][]rune { var names = [][]rune{} for _, name := range p.Callback(string(line)) { names = append(names, []rune(name+" ")) } return names } func (p *PrefixCompleter) GetChildren() []PrefixCompleterInterface { return p.Children } func (p *PrefixCompleter) SetChildren(children []PrefixCompleterInterface) { p.Children = children } func NewPrefixCompleter(pc ...PrefixCompleterInterface) *PrefixCompleter { return PcItem("", pc...) } func PcItem(name string, pc ...PrefixCompleterInterface) *PrefixCompleter { name += " " return &PrefixCompleter{ Name: []rune(name), Dynamic: false, Children: pc, } } func PcItemDynamic(callback DynamicCompleteFunc, pc ...PrefixCompleterInterface) *PrefixCompleter { return &PrefixCompleter{ Callback: callback, Dynamic: true, Children: pc, } } func (p *PrefixCompleter) Do(line []rune, pos int) (newLine [][]rune, offset int) { return doInternal(p, line, pos, line) } func Do(p PrefixCompleterInterface, line []rune, pos int) (newLine [][]rune, offset int) { return doInternal(p, line, pos, line) } func doInternal(p PrefixCompleterInterface, line []rune, pos int, origLine []rune) (newLine [][]rune, offset int) { line = runes.TrimSpaceLeft(line[:pos]) goNext := false var lineCompleter PrefixCompleterInterface for _, child := range p.GetChildren() { childNames := make([][]rune, 1) childDynamic, ok := child.(DynamicPrefixCompleterInterface) if ok && childDynamic.IsDynamic() { childNames = childDynamic.GetDynamicNames(origLine) } else { childNames[0] = child.GetName() } for _, childName := range childNames { if len(line) >= len(childName) { if runes.HasPrefix(line, childName) { if len(line) == len(childName) { newLine = append(newLine, []rune{' '}) } else { newLine = append(newLine, childName) } offset = len(childName) lineCompleter = child goNext = true } } else { if runes.HasPrefix(childName, line) { newLine = append(newLine, childName[len(line):]) offset = len(line) lineCompleter = child } } } } if len(newLine) != 1 { return } tmpLine := make([]rune, 0, len(line)) for i := offset; i < len(line); i++ { if line[i] == ' ' { continue } tmpLine = append(tmpLine, line[i:]...) return doInternal(lineCompleter, tmpLine, len(tmpLine), origLine) } if goNext { return doInternal(lineCompleter, nil, 0, origLine) } return } ================================================ FILE: vendor/github.com/chzyer/readline/complete_segment.go ================================================ package readline type SegmentCompleter interface { // a // |- a1 // |--- a11 // |- a2 // b // input: // DoTree([], 0) [a, b] // DoTree([a], 1) [a] // DoTree([a, ], 0) [a1, a2] // DoTree([a, a], 1) [a1, a2] // DoTree([a, a1], 2) [a1] // DoTree([a, a1, ], 0) [a11] // DoTree([a, a1, a], 1) [a11] DoSegment([][]rune, int) [][]rune } type dumpSegmentCompleter struct { f func([][]rune, int) [][]rune } func (d *dumpSegmentCompleter) DoSegment(segment [][]rune, n int) [][]rune { return d.f(segment, n) } func SegmentFunc(f func([][]rune, int) [][]rune) AutoCompleter { return &SegmentComplete{&dumpSegmentCompleter{f}} } func SegmentAutoComplete(completer SegmentCompleter) *SegmentComplete { return &SegmentComplete{ SegmentCompleter: completer, } } type SegmentComplete struct { SegmentCompleter } func RetSegment(segments [][]rune, cands [][]rune, idx int) ([][]rune, int) { ret := make([][]rune, 0, len(cands)) lastSegment := segments[len(segments)-1] for _, cand := range cands { if !runes.HasPrefix(cand, lastSegment) { continue } ret = append(ret, cand[len(lastSegment):]) } return ret, idx } func SplitSegment(line []rune, pos int) ([][]rune, int) { segs := [][]rune{} lastIdx := -1 line = line[:pos] pos = 0 for idx, l := range line { if l == ' ' { pos = 0 segs = append(segs, line[lastIdx+1:idx]) lastIdx = idx } else { pos++ } } segs = append(segs, line[lastIdx+1:]) return segs, pos } func (c *SegmentComplete) Do(line []rune, pos int) (newLine [][]rune, offset int) { segment, idx := SplitSegment(line, pos) cands := c.DoSegment(segment, idx) newLine, offset = RetSegment(segment, cands, idx) for idx := range newLine { newLine[idx] = append(newLine[idx], ' ') } return newLine, offset } ================================================ FILE: vendor/github.com/chzyer/readline/history.go ================================================ package readline import ( "bufio" "container/list" "fmt" "os" "strings" "sync" ) type hisItem struct { Source []rune Version int64 Tmp []rune } func (h *hisItem) Clean() { h.Source = nil h.Tmp = nil } type opHistory struct { cfg *Config history *list.List historyVer int64 current *list.Element fd *os.File fdLock sync.Mutex enable bool } func newOpHistory(cfg *Config) (o *opHistory) { o = &opHistory{ cfg: cfg, history: list.New(), enable: true, } return o } func (o *opHistory) Reset() { o.history = list.New() o.current = nil } func (o *opHistory) IsHistoryClosed() bool { o.fdLock.Lock() defer o.fdLock.Unlock() return o.fd.Fd() == ^(uintptr(0)) } func (o *opHistory) Init() { if o.IsHistoryClosed() { o.initHistory() } } func (o *opHistory) initHistory() { if o.cfg.HistoryFile != "" { o.historyUpdatePath(o.cfg.HistoryFile) } } // only called by newOpHistory func (o *opHistory) historyUpdatePath(path string) { o.fdLock.Lock() defer o.fdLock.Unlock() f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666) if err != nil { return } o.fd = f r := bufio.NewReader(o.fd) total := 0 for ; ; total++ { line, err := r.ReadString('\n') if err != nil { break } // ignore the empty line line = strings.TrimSpace(line) if len(line) == 0 { continue } o.Push([]rune(line)) o.Compact() } if total > o.cfg.HistoryLimit { o.rewriteLocked() } o.historyVer++ o.Push(nil) return } func (o *opHistory) Compact() { for o.history.Len() > o.cfg.HistoryLimit && o.history.Len() > 0 { o.history.Remove(o.history.Front()) } } func (o *opHistory) Rewrite() { o.fdLock.Lock() defer o.fdLock.Unlock() o.rewriteLocked() } func (o *opHistory) rewriteLocked() { if o.cfg.HistoryFile == "" { return } tmpFile := o.cfg.HistoryFile + ".tmp" fd, err := os.OpenFile(tmpFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC|os.O_APPEND, 0666) if err != nil { return } buf := bufio.NewWriter(fd) for elem := o.history.Front(); elem != nil; elem = elem.Next() { buf.WriteString(string(elem.Value.(*hisItem).Source) + "\n") } buf.Flush() // replace history file if err = os.Rename(tmpFile, o.cfg.HistoryFile); err != nil { fd.Close() return } if o.fd != nil { o.fd.Close() } // fd is write only, just satisfy what we need. o.fd = fd } func (o *opHistory) Close() { o.fdLock.Lock() defer o.fdLock.Unlock() if o.fd != nil { o.fd.Close() } } func (o *opHistory) FindBck(isNewSearch bool, rs []rune, start int) (int, *list.Element) { for elem := o.current; elem != nil; elem = elem.Prev() { item := o.showItem(elem.Value) if isNewSearch { start += len(rs) } if elem == o.current { if len(item) >= start { item = item[:start] } } idx := runes.IndexAllBckEx(item, rs, o.cfg.HistorySearchFold) if idx < 0 { continue } return idx, elem } return -1, nil } func (o *opHistory) FindFwd(isNewSearch bool, rs []rune, start int) (int, *list.Element) { for elem := o.current; elem != nil; elem = elem.Next() { item := o.showItem(elem.Value) if isNewSearch { start -= len(rs) if start < 0 { start = 0 } } if elem == o.current { if len(item)-1 >= start { item = item[start:] } else { continue } } idx := runes.IndexAllEx(item, rs, o.cfg.HistorySearchFold) if idx < 0 { continue } if elem == o.current { idx += start } return idx, elem } return -1, nil } func (o *opHistory) showItem(obj interface{}) []rune { item := obj.(*hisItem) if item.Version == o.historyVer { return item.Tmp } return item.Source } func (o *opHistory) Prev() []rune { if o.current == nil { return nil } current := o.current.Prev() if current == nil { return nil } o.current = current return runes.Copy(o.showItem(current.Value)) } func (o *opHistory) Next() ([]rune, bool) { if o.current == nil { return nil, false } current := o.current.Next() if current == nil { return nil, false } o.current = current return runes.Copy(o.showItem(current.Value)), true } // Disable the current history func (o *opHistory) Disable() { o.enable = false } // Enable the current history func (o *opHistory) Enable() { o.enable = true } func (o *opHistory) debug() { Debug("-------") for item := o.history.Front(); item != nil; item = item.Next() { Debug(fmt.Sprintf("%+v", item.Value)) } } // save history func (o *opHistory) New(current []rune) (err error) { // history deactivated if !o.enable { return nil } current = runes.Copy(current) // if just use last command without modify // just clean lastest history if back := o.history.Back(); back != nil { prev := back.Prev() if prev != nil { if runes.Equal(current, prev.Value.(*hisItem).Source) { o.current = o.history.Back() o.current.Value.(*hisItem).Clean() o.historyVer++ return nil } } } if len(current) == 0 { o.current = o.history.Back() if o.current != nil { o.current.Value.(*hisItem).Clean() o.historyVer++ return nil } } if o.current != o.history.Back() { // move history item to current command currentItem := o.current.Value.(*hisItem) // set current to last item o.current = o.history.Back() current = runes.Copy(currentItem.Tmp) } // err only can be a IO error, just report err = o.Update(current, true) // push a new one to commit current command o.historyVer++ o.Push(nil) return } func (o *opHistory) Revert() { o.historyVer++ o.current = o.history.Back() } func (o *opHistory) Update(s []rune, commit bool) (err error) { o.fdLock.Lock() defer o.fdLock.Unlock() s = runes.Copy(s) if o.current == nil { o.Push(s) o.Compact() return } r := o.current.Value.(*hisItem) r.Version = o.historyVer if commit { r.Source = s if o.fd != nil { // just report the error _, err = o.fd.Write([]byte(string(r.Source) + "\n")) } } else { r.Tmp = append(r.Tmp[:0], s...) } o.current.Value = r o.Compact() return } func (o *opHistory) Push(s []rune) { s = runes.Copy(s) elem := o.history.PushBack(&hisItem{Source: s}) o.current = elem } ================================================ FILE: vendor/github.com/chzyer/readline/operation.go ================================================ package readline import ( "errors" "io" "sync" ) var ( ErrInterrupt = errors.New("Interrupt") ) type InterruptError struct { Line []rune } func (*InterruptError) Error() string { return "Interrupted" } type Operation struct { m sync.Mutex cfg *Config t *Terminal buf *RuneBuffer outchan chan []rune errchan chan error w io.Writer history *opHistory *opSearch *opCompleter *opPassword *opVim } func (o *Operation) SetBuffer(what string) { o.buf.Set([]rune(what)) } type wrapWriter struct { r *Operation t *Terminal target io.Writer } func (w *wrapWriter) Write(b []byte) (int, error) { if !w.t.IsReading() { return w.target.Write(b) } var ( n int err error ) w.r.buf.Refresh(func() { n, err = w.target.Write(b) }) if w.r.IsSearchMode() { w.r.SearchRefresh(-1) } if w.r.IsInCompleteMode() { w.r.CompleteRefresh() } return n, err } func NewOperation(t *Terminal, cfg *Config) *Operation { width := cfg.FuncGetWidth() op := &Operation{ t: t, buf: NewRuneBuffer(t, cfg.Prompt, cfg, width), outchan: make(chan []rune), errchan: make(chan error, 1), } op.w = op.buf.w op.SetConfig(cfg) op.opVim = newVimMode(op) op.opCompleter = newOpCompleter(op.buf.w, op, width) op.opPassword = newOpPassword(op) op.cfg.FuncOnWidthChanged(func() { newWidth := cfg.FuncGetWidth() op.opCompleter.OnWidthChange(newWidth) op.opSearch.OnWidthChange(newWidth) op.buf.OnWidthChange(newWidth) }) go op.ioloop() return op } func (o *Operation) SetPrompt(s string) { o.buf.SetPrompt(s) } func (o *Operation) SetMaskRune(r rune) { o.buf.SetMask(r) } func (o *Operation) GetConfig() *Config { o.m.Lock() cfg := *o.cfg o.m.Unlock() return &cfg } func (o *Operation) ioloop() { for { keepInSearchMode := false keepInCompleteMode := false r := o.t.ReadRune() if o.GetConfig().FuncFilterInputRune != nil { var process bool r, process = o.GetConfig().FuncFilterInputRune(r) if !process { o.t.KickRead() o.buf.Refresh(nil) // to refresh the line continue // ignore this rune } } if r == 0 { // io.EOF if o.buf.Len() == 0 { o.buf.Clean() select { case o.errchan <- io.EOF: } break } else { // if stdin got io.EOF and there is something left in buffer, // let's flush them by sending CharEnter. // And we will got io.EOF int next loop. r = CharEnter } } isUpdateHistory := true if o.IsInCompleteSelectMode() { keepInCompleteMode = o.HandleCompleteSelect(r) if keepInCompleteMode { continue } o.buf.Refresh(nil) switch r { case CharEnter, CharCtrlJ: o.history.Update(o.buf.Runes(), false) fallthrough case CharInterrupt: o.t.KickRead() fallthrough case CharBell: continue } } if o.IsEnableVimMode() { r = o.HandleVim(r, o.t.ReadRune) if r == 0 { continue } } switch r { case CharBell: if o.IsSearchMode() { o.ExitSearchMode(true) o.buf.Refresh(nil) } if o.IsInCompleteMode() { o.ExitCompleteMode(true) o.buf.Refresh(nil) } case CharTab: if o.GetConfig().AutoComplete == nil { o.t.Bell() break } if o.OnComplete() { keepInCompleteMode = true } else { o.t.Bell() break } case CharBckSearch: if !o.SearchMode(S_DIR_BCK) { o.t.Bell() break } keepInSearchMode = true case CharCtrlU: o.buf.KillFront() case CharFwdSearch: if !o.SearchMode(S_DIR_FWD) { o.t.Bell() break } keepInSearchMode = true case CharKill: o.buf.Kill() keepInCompleteMode = true case MetaForward: o.buf.MoveToNextWord() case CharTranspose: o.buf.Transpose() case MetaBackward: o.buf.MoveToPrevWord() case MetaDelete: o.buf.DeleteWord() case CharLineStart: o.buf.MoveToLineStart() case CharLineEnd: o.buf.MoveToLineEnd() case CharBackspace, CharCtrlH: if o.IsSearchMode() { o.SearchBackspace() keepInSearchMode = true break } if o.buf.Len() == 0 { o.t.Bell() break } o.buf.Backspace() if o.IsInCompleteMode() { o.OnComplete() } case CharCtrlZ: o.buf.Clean() o.t.SleepToResume() o.Refresh() case CharCtrlL: ClearScreen(o.w) o.Refresh() case MetaBackspace, CharCtrlW: o.buf.BackEscapeWord() case CharCtrlY: o.buf.Yank() case CharEnter, CharCtrlJ: if o.IsSearchMode() { o.ExitSearchMode(false) } o.buf.MoveToLineEnd() var data []rune if !o.GetConfig().UniqueEditLine { o.buf.WriteRune('\n') data = o.buf.Reset() data = data[:len(data)-1] // trim \n } else { o.buf.Clean() data = o.buf.Reset() } o.outchan <- data if !o.GetConfig().DisableAutoSaveHistory { // ignore IO error _ = o.history.New(data) } else { isUpdateHistory = false } case CharBackward: o.buf.MoveBackward() case CharForward: o.buf.MoveForward() case CharPrev: buf := o.history.Prev() if buf != nil { o.buf.Set(buf) } else { o.t.Bell() } case CharNext: buf, ok := o.history.Next() if ok { o.buf.Set(buf) } else { o.t.Bell() } case CharDelete: if o.buf.Len() > 0 || !o.IsNormalMode() { o.t.KickRead() if !o.buf.Delete() { o.t.Bell() } break } // treat as EOF if !o.GetConfig().UniqueEditLine { o.buf.WriteString(o.GetConfig().EOFPrompt + "\n") } o.buf.Reset() isUpdateHistory = false o.history.Revert() o.errchan <- io.EOF if o.GetConfig().UniqueEditLine { o.buf.Clean() } case CharInterrupt: if o.IsSearchMode() { o.t.KickRead() o.ExitSearchMode(true) break } if o.IsInCompleteMode() { o.t.KickRead() o.ExitCompleteMode(true) o.buf.Refresh(nil) break } o.buf.MoveToLineEnd() o.buf.Refresh(nil) hint := o.GetConfig().InterruptPrompt + "\n" if !o.GetConfig().UniqueEditLine { o.buf.WriteString(hint) } remain := o.buf.Reset() if !o.GetConfig().UniqueEditLine { remain = remain[:len(remain)-len([]rune(hint))] } isUpdateHistory = false o.history.Revert() o.errchan <- &InterruptError{remain} default: if o.IsSearchMode() { o.SearchChar(r) keepInSearchMode = true break } o.buf.WriteRune(r) if o.IsInCompleteMode() { o.OnComplete() keepInCompleteMode = true } } listener := o.GetConfig().Listener if listener != nil { newLine, newPos, ok := listener.OnChange(o.buf.Runes(), o.buf.Pos(), r) if ok { o.buf.SetWithIdx(newPos, newLine) } } o.m.Lock() if !keepInSearchMode && o.IsSearchMode() { o.ExitSearchMode(false) o.buf.Refresh(nil) } else if o.IsInCompleteMode() { if !keepInCompleteMode { o.ExitCompleteMode(false) o.Refresh() } else { o.buf.Refresh(nil) o.CompleteRefresh() } } if isUpdateHistory && !o.IsSearchMode() { // it will cause null history o.history.Update(o.buf.Runes(), false) } o.m.Unlock() } } func (o *Operation) Stderr() io.Writer { return &wrapWriter{target: o.GetConfig().Stderr, r: o, t: o.t} } func (o *Operation) Stdout() io.Writer { return &wrapWriter{target: o.GetConfig().Stdout, r: o, t: o.t} } func (o *Operation) String() (string, error) { r, err := o.Runes() return string(r), err } func (o *Operation) Runes() ([]rune, error) { o.t.EnterRawMode() defer o.t.ExitRawMode() listener := o.GetConfig().Listener if listener != nil { listener.OnChange(nil, 0, 0) } o.buf.Refresh(nil) // print prompt o.t.KickRead() select { case r := <-o.outchan: return r, nil case err := <-o.errchan: if e, ok := err.(*InterruptError); ok { return e.Line, ErrInterrupt } return nil, err } } func (o *Operation) PasswordEx(prompt string, l Listener) ([]byte, error) { cfg := o.GenPasswordConfig() cfg.Prompt = prompt cfg.Listener = l return o.PasswordWithConfig(cfg) } func (o *Operation) GenPasswordConfig() *Config { return o.opPassword.PasswordConfig() } func (o *Operation) PasswordWithConfig(cfg *Config) ([]byte, error) { if err := o.opPassword.EnterPasswordMode(cfg); err != nil { return nil, err } defer o.opPassword.ExitPasswordMode() return o.Slice() } func (o *Operation) Password(prompt string) ([]byte, error) { return o.PasswordEx(prompt, nil) } func (o *Operation) SetTitle(t string) { o.w.Write([]byte("\033[2;" + t + "\007")) } func (o *Operation) Slice() ([]byte, error) { r, err := o.Runes() if err != nil { return nil, err } return []byte(string(r)), nil } func (o *Operation) Close() { select { case o.errchan <- io.EOF: default: } o.history.Close() } func (o *Operation) SetHistoryPath(path string) { if o.history != nil { o.history.Close() } o.cfg.HistoryFile = path o.history = newOpHistory(o.cfg) } func (o *Operation) IsNormalMode() bool { return !o.IsInCompleteMode() && !o.IsSearchMode() } func (op *Operation) SetConfig(cfg *Config) (*Config, error) { op.m.Lock() defer op.m.Unlock() if op.cfg == cfg { return op.cfg, nil } if err := cfg.Init(); err != nil { return op.cfg, err } old := op.cfg op.cfg = cfg op.SetPrompt(cfg.Prompt) op.SetMaskRune(cfg.MaskRune) op.buf.SetConfig(cfg) width := op.cfg.FuncGetWidth() if cfg.opHistory == nil { op.SetHistoryPath(cfg.HistoryFile) cfg.opHistory = op.history cfg.opSearch = newOpSearch(op.buf.w, op.buf, op.history, cfg, width) } op.history = cfg.opHistory // SetHistoryPath will close opHistory which already exists // so if we use it next time, we need to reopen it by `InitHistory()` op.history.Init() if op.cfg.AutoComplete != nil { op.opCompleter = newOpCompleter(op.buf.w, op, width) } op.opSearch = cfg.opSearch return old, nil } func (o *Operation) ResetHistory() { o.history.Reset() } // if err is not nil, it just mean it fail to write to file // other things goes fine. func (o *Operation) SaveHistory(content string) error { return o.history.New([]rune(content)) } func (o *Operation) Refresh() { if o.t.IsReading() { o.buf.Refresh(nil) } } func (o *Operation) Clean() { o.buf.Clean() } func FuncListener(f func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool)) Listener { return &DumpListener{f: f} } type DumpListener struct { f func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) } func (d *DumpListener) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) { return d.f(line, pos, key) } type Listener interface { OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) } type Painter interface { Paint(line []rune, pos int) []rune } type defaultPainter struct{} func (p *defaultPainter) Paint(line []rune, _ int) []rune { return line } ================================================ FILE: vendor/github.com/chzyer/readline/password.go ================================================ package readline type opPassword struct { o *Operation backupCfg *Config } func newOpPassword(o *Operation) *opPassword { return &opPassword{o: o} } func (o *opPassword) ExitPasswordMode() { o.o.SetConfig(o.backupCfg) o.backupCfg = nil } func (o *opPassword) EnterPasswordMode(cfg *Config) (err error) { o.backupCfg, err = o.o.SetConfig(cfg) return } func (o *opPassword) PasswordConfig() *Config { return &Config{ EnableMask: true, InterruptPrompt: "\n", EOFPrompt: "\n", HistoryLimit: -1, Painter: &defaultPainter{}, Stdout: o.o.cfg.Stdout, Stderr: o.o.cfg.Stderr, } } ================================================ FILE: vendor/github.com/chzyer/readline/rawreader_windows.go ================================================ // +build windows package readline import "unsafe" const ( VK_CANCEL = 0x03 VK_BACK = 0x08 VK_TAB = 0x09 VK_RETURN = 0x0D VK_SHIFT = 0x10 VK_CONTROL = 0x11 VK_MENU = 0x12 VK_ESCAPE = 0x1B VK_LEFT = 0x25 VK_UP = 0x26 VK_RIGHT = 0x27 VK_DOWN = 0x28 VK_DELETE = 0x2E VK_LSHIFT = 0xA0 VK_RSHIFT = 0xA1 VK_LCONTROL = 0xA2 VK_RCONTROL = 0xA3 ) // RawReader translate input record to ANSI escape sequence. // To provides same behavior as unix terminal. type RawReader struct { ctrlKey bool altKey bool } func NewRawReader() *RawReader { r := new(RawReader) return r } // only process one action in one read func (r *RawReader) Read(buf []byte) (int, error) { ir := new(_INPUT_RECORD) var read int var err error next: err = kernel.ReadConsoleInputW(stdin, uintptr(unsafe.Pointer(ir)), 1, uintptr(unsafe.Pointer(&read)), ) if err != nil { return 0, err } if ir.EventType != EVENT_KEY { goto next } ker := (*_KEY_EVENT_RECORD)(unsafe.Pointer(&ir.Event[0])) if ker.bKeyDown == 0 { // keyup if r.ctrlKey || r.altKey { switch ker.wVirtualKeyCode { case VK_RCONTROL, VK_LCONTROL: r.ctrlKey = false case VK_MENU: //alt r.altKey = false } } goto next } if ker.unicodeChar == 0 { var target rune switch ker.wVirtualKeyCode { case VK_RCONTROL, VK_LCONTROL: r.ctrlKey = true case VK_MENU: //alt r.altKey = true case VK_LEFT: target = CharBackward case VK_RIGHT: target = CharForward case VK_UP: target = CharPrev case VK_DOWN: target = CharNext } if target != 0 { return r.write(buf, target) } goto next } char := rune(ker.unicodeChar) if r.ctrlKey { switch char { case 'A': char = CharLineStart case 'E': char = CharLineEnd case 'R': char = CharBckSearch case 'S': char = CharFwdSearch } } else if r.altKey { switch char { case VK_BACK: char = CharBackspace } return r.writeEsc(buf, char) } return r.write(buf, char) } func (r *RawReader) writeEsc(b []byte, char rune) (int, error) { b[0] = '\033' n := copy(b[1:], []byte(string(char))) return n + 1, nil } func (r *RawReader) write(b []byte, char rune) (int, error) { n := copy(b, []byte(string(char))) return n, nil } func (r *RawReader) Close() error { return nil } ================================================ FILE: vendor/github.com/chzyer/readline/readline.go ================================================ // Readline is a pure go implementation for GNU-Readline kind library. // // example: // rl, err := readline.New("> ") // if err != nil { // panic(err) // } // defer rl.Close() // // for { // line, err := rl.Readline() // if err != nil { // io.EOF // break // } // println(line) // } // package readline import ( "io" ) type Instance struct { Config *Config Terminal *Terminal Operation *Operation } type Config struct { // prompt supports ANSI escape sequence, so we can color some characters even in windows Prompt string // readline will persist historys to file where HistoryFile specified HistoryFile string // specify the max length of historys, it's 500 by default, set it to -1 to disable history HistoryLimit int DisableAutoSaveHistory bool // enable case-insensitive history searching HistorySearchFold bool // AutoCompleter will called once user press TAB AutoComplete AutoCompleter // Any key press will pass to Listener // NOTE: Listener will be triggered by (nil, 0, 0) immediately Listener Listener Painter Painter // If VimMode is true, readline will in vim.insert mode by default VimMode bool InterruptPrompt string EOFPrompt string FuncGetWidth func() int Stdin io.ReadCloser StdinWriter io.Writer Stdout io.Writer Stderr io.Writer EnableMask bool MaskRune rune // erase the editing line after user submited it // it use in IM usually. UniqueEditLine bool // filter input runes (may be used to disable CtrlZ or for translating some keys to different actions) // -> output = new (translated) rune and true/false if continue with processing this one FuncFilterInputRune func(rune) (rune, bool) // force use interactive even stdout is not a tty FuncIsTerminal func() bool FuncMakeRaw func() error FuncExitRaw func() error FuncOnWidthChanged func(func()) ForceUseInteractive bool // private fields inited bool opHistory *opHistory opSearch *opSearch } func (c *Config) useInteractive() bool { if c.ForceUseInteractive { return true } return c.FuncIsTerminal() } func (c *Config) Init() error { if c.inited { return nil } c.inited = true if c.Stdin == nil { c.Stdin = NewCancelableStdin(Stdin) } c.Stdin, c.StdinWriter = NewFillableStdin(c.Stdin) if c.Stdout == nil { c.Stdout = Stdout } if c.Stderr == nil { c.Stderr = Stderr } if c.HistoryLimit == 0 { c.HistoryLimit = 500 } if c.InterruptPrompt == "" { c.InterruptPrompt = "^C" } else if c.InterruptPrompt == "\n" { c.InterruptPrompt = "" } if c.EOFPrompt == "" { c.EOFPrompt = "^D" } else if c.EOFPrompt == "\n" { c.EOFPrompt = "" } if c.AutoComplete == nil { c.AutoComplete = &TabCompleter{} } if c.FuncGetWidth == nil { c.FuncGetWidth = GetScreenWidth } if c.FuncIsTerminal == nil { c.FuncIsTerminal = DefaultIsTerminal } rm := new(RawMode) if c.FuncMakeRaw == nil { c.FuncMakeRaw = rm.Enter } if c.FuncExitRaw == nil { c.FuncExitRaw = rm.Exit } if c.FuncOnWidthChanged == nil { c.FuncOnWidthChanged = DefaultOnWidthChanged } return nil } func (c Config) Clone() *Config { c.opHistory = nil c.opSearch = nil return &c } func (c *Config) SetListener(f func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool)) { c.Listener = FuncListener(f) } func (c *Config) SetPainter(p Painter) { c.Painter = p } func NewEx(cfg *Config) (*Instance, error) { t, err := NewTerminal(cfg) if err != nil { return nil, err } rl := t.Readline() if cfg.Painter == nil { cfg.Painter = &defaultPainter{} } return &Instance{ Config: cfg, Terminal: t, Operation: rl, }, nil } func New(prompt string) (*Instance, error) { return NewEx(&Config{Prompt: prompt}) } func (i *Instance) ResetHistory() { i.Operation.ResetHistory() } func (i *Instance) SetPrompt(s string) { i.Operation.SetPrompt(s) } func (i *Instance) SetMaskRune(r rune) { i.Operation.SetMaskRune(r) } // change history persistence in runtime func (i *Instance) SetHistoryPath(p string) { i.Operation.SetHistoryPath(p) } // readline will refresh automatic when write through Stdout() func (i *Instance) Stdout() io.Writer { return i.Operation.Stdout() } // readline will refresh automatic when write through Stdout() func (i *Instance) Stderr() io.Writer { return i.Operation.Stderr() } // switch VimMode in runtime func (i *Instance) SetVimMode(on bool) { i.Operation.SetVimMode(on) } func (i *Instance) IsVimMode() bool { return i.Operation.IsEnableVimMode() } func (i *Instance) GenPasswordConfig() *Config { return i.Operation.GenPasswordConfig() } // we can generate a config by `i.GenPasswordConfig()` func (i *Instance) ReadPasswordWithConfig(cfg *Config) ([]byte, error) { return i.Operation.PasswordWithConfig(cfg) } func (i *Instance) ReadPasswordEx(prompt string, l Listener) ([]byte, error) { return i.Operation.PasswordEx(prompt, l) } func (i *Instance) ReadPassword(prompt string) ([]byte, error) { return i.Operation.Password(prompt) } type Result struct { Line string Error error } func (l *Result) CanContinue() bool { return len(l.Line) != 0 && l.Error == ErrInterrupt } func (l *Result) CanBreak() bool { return !l.CanContinue() && l.Error != nil } func (i *Instance) Line() *Result { ret, err := i.Readline() return &Result{ret, err} } // err is one of (nil, io.EOF, readline.ErrInterrupt) func (i *Instance) Readline() (string, error) { return i.Operation.String() } func (i *Instance) ReadlineWithDefault(what string) (string, error) { i.Operation.SetBuffer(what) return i.Operation.String() } func (i *Instance) SaveHistory(content string) error { return i.Operation.SaveHistory(content) } // same as readline func (i *Instance) ReadSlice() ([]byte, error) { return i.Operation.Slice() } // we must make sure that call Close() before process exit. // if there has a pending reading operation, that reading will be interrupted. // so you can capture the signal and call Instance.Close(), it's thread-safe. func (i *Instance) Close() error { i.Config.Stdin.Close() i.Operation.Close() if err := i.Terminal.Close(); err != nil { return err } return nil } // call CaptureExitSignal when you want readline exit gracefully. func (i *Instance) CaptureExitSignal() { CaptureExitSignal(func() { i.Close() }) } func (i *Instance) Clean() { i.Operation.Clean() } func (i *Instance) Write(b []byte) (int, error) { return i.Stdout().Write(b) } // WriteStdin prefill the next Stdin fetch // Next time you call ReadLine() this value will be writen before the user input // ie : // i := readline.New() // i.WriteStdin([]byte("test")) // _, _= i.Readline() // // gives // // > test[cursor] func (i *Instance) WriteStdin(val []byte) (int, error) { return i.Terminal.WriteStdin(val) } func (i *Instance) SetConfig(cfg *Config) *Config { if i.Config == cfg { return cfg } old := i.Config i.Config = cfg i.Operation.SetConfig(cfg) i.Terminal.SetConfig(cfg) return old } func (i *Instance) Refresh() { i.Operation.Refresh() } // HistoryDisable the save of the commands into the history func (i *Instance) HistoryDisable() { i.Operation.history.Disable() } // HistoryEnable the save of the commands into the history (default on) func (i *Instance) HistoryEnable() { i.Operation.history.Enable() } ================================================ FILE: vendor/github.com/chzyer/readline/remote.go ================================================ package readline import ( "bufio" "bytes" "encoding/binary" "fmt" "io" "net" "os" "sync" "sync/atomic" ) type MsgType int16 const ( T_DATA = MsgType(iota) T_WIDTH T_WIDTH_REPORT T_ISTTY_REPORT T_RAW T_ERAW // exit raw T_EOF ) type RemoteSvr struct { eof int32 closed int32 width int32 reciveChan chan struct{} writeChan chan *writeCtx conn net.Conn isTerminal bool funcWidthChan func() stopChan chan struct{} dataBufM sync.Mutex dataBuf bytes.Buffer } type writeReply struct { n int err error } type writeCtx struct { msg *Message reply chan *writeReply } func newWriteCtx(msg *Message) *writeCtx { return &writeCtx{ msg: msg, reply: make(chan *writeReply), } } func NewRemoteSvr(conn net.Conn) (*RemoteSvr, error) { rs := &RemoteSvr{ width: -1, conn: conn, writeChan: make(chan *writeCtx), reciveChan: make(chan struct{}), stopChan: make(chan struct{}), } buf := bufio.NewReader(rs.conn) if err := rs.init(buf); err != nil { return nil, err } go rs.readLoop(buf) go rs.writeLoop() return rs, nil } func (r *RemoteSvr) init(buf *bufio.Reader) error { m, err := ReadMessage(buf) if err != nil { return err } // receive isTerminal if m.Type != T_ISTTY_REPORT { return fmt.Errorf("unexpected init message") } r.GotIsTerminal(m.Data) // receive width m, err = ReadMessage(buf) if err != nil { return err } if m.Type != T_WIDTH_REPORT { return fmt.Errorf("unexpected init message") } r.GotReportWidth(m.Data) return nil } func (r *RemoteSvr) HandleConfig(cfg *Config) { cfg.Stderr = r cfg.Stdout = r cfg.Stdin = r cfg.FuncExitRaw = r.ExitRawMode cfg.FuncIsTerminal = r.IsTerminal cfg.FuncMakeRaw = r.EnterRawMode cfg.FuncExitRaw = r.ExitRawMode cfg.FuncGetWidth = r.GetWidth cfg.FuncOnWidthChanged = func(f func()) { r.funcWidthChan = f } } func (r *RemoteSvr) IsTerminal() bool { return r.isTerminal } func (r *RemoteSvr) checkEOF() error { if atomic.LoadInt32(&r.eof) == 1 { return io.EOF } return nil } func (r *RemoteSvr) Read(b []byte) (int, error) { r.dataBufM.Lock() n, err := r.dataBuf.Read(b) r.dataBufM.Unlock() if n == 0 { if err := r.checkEOF(); err != nil { return 0, err } } if n == 0 && err == io.EOF { <-r.reciveChan r.dataBufM.Lock() n, err = r.dataBuf.Read(b) r.dataBufM.Unlock() } if n == 0 { if err := r.checkEOF(); err != nil { return 0, err } } return n, err } func (r *RemoteSvr) writeMsg(m *Message) error { ctx := newWriteCtx(m) r.writeChan <- ctx reply := <-ctx.reply return reply.err } func (r *RemoteSvr) Write(b []byte) (int, error) { ctx := newWriteCtx(NewMessage(T_DATA, b)) r.writeChan <- ctx reply := <-ctx.reply return reply.n, reply.err } func (r *RemoteSvr) EnterRawMode() error { return r.writeMsg(NewMessage(T_RAW, nil)) } func (r *RemoteSvr) ExitRawMode() error { return r.writeMsg(NewMessage(T_ERAW, nil)) } func (r *RemoteSvr) writeLoop() { defer r.Close() loop: for { select { case ctx, ok := <-r.writeChan: if !ok { break } n, err := ctx.msg.WriteTo(r.conn) ctx.reply <- &writeReply{n, err} case <-r.stopChan: break loop } } } func (r *RemoteSvr) Close() error { if atomic.CompareAndSwapInt32(&r.closed, 0, 1) { close(r.stopChan) r.conn.Close() } return nil } func (r *RemoteSvr) readLoop(buf *bufio.Reader) { defer r.Close() for { m, err := ReadMessage(buf) if err != nil { break } switch m.Type { case T_EOF: atomic.StoreInt32(&r.eof, 1) select { case r.reciveChan <- struct{}{}: default: } case T_DATA: r.dataBufM.Lock() r.dataBuf.Write(m.Data) r.dataBufM.Unlock() select { case r.reciveChan <- struct{}{}: default: } case T_WIDTH_REPORT: r.GotReportWidth(m.Data) case T_ISTTY_REPORT: r.GotIsTerminal(m.Data) } } } func (r *RemoteSvr) GotIsTerminal(data []byte) { if binary.BigEndian.Uint16(data) == 0 { r.isTerminal = false } else { r.isTerminal = true } } func (r *RemoteSvr) GotReportWidth(data []byte) { atomic.StoreInt32(&r.width, int32(binary.BigEndian.Uint16(data))) if r.funcWidthChan != nil { r.funcWidthChan() } } func (r *RemoteSvr) GetWidth() int { return int(atomic.LoadInt32(&r.width)) } // ----------------------------------------------------------------------------- type Message struct { Type MsgType Data []byte } func ReadMessage(r io.Reader) (*Message, error) { m := new(Message) var length int32 if err := binary.Read(r, binary.BigEndian, &length); err != nil { return nil, err } if err := binary.Read(r, binary.BigEndian, &m.Type); err != nil { return nil, err } m.Data = make([]byte, int(length)-2) if _, err := io.ReadFull(r, m.Data); err != nil { return nil, err } return m, nil } func NewMessage(t MsgType, data []byte) *Message { return &Message{t, data} } func (m *Message) WriteTo(w io.Writer) (int, error) { buf := bytes.NewBuffer(make([]byte, 0, len(m.Data)+2+4)) binary.Write(buf, binary.BigEndian, int32(len(m.Data)+2)) binary.Write(buf, binary.BigEndian, m.Type) buf.Write(m.Data) n, err := buf.WriteTo(w) return int(n), err } // ----------------------------------------------------------------------------- type RemoteCli struct { conn net.Conn raw RawMode receiveChan chan struct{} inited int32 isTerminal *bool data bytes.Buffer dataM sync.Mutex } func NewRemoteCli(conn net.Conn) (*RemoteCli, error) { r := &RemoteCli{ conn: conn, receiveChan: make(chan struct{}), } return r, nil } func (r *RemoteCli) MarkIsTerminal(is bool) { r.isTerminal = &is } func (r *RemoteCli) init() error { if !atomic.CompareAndSwapInt32(&r.inited, 0, 1) { return nil } if err := r.reportIsTerminal(); err != nil { return err } if err := r.reportWidth(); err != nil { return err } // register sig for width changed DefaultOnWidthChanged(func() { r.reportWidth() }) return nil } func (r *RemoteCli) writeMsg(m *Message) error { r.dataM.Lock() _, err := m.WriteTo(r.conn) r.dataM.Unlock() return err } func (r *RemoteCli) Write(b []byte) (int, error) { m := NewMessage(T_DATA, b) r.dataM.Lock() _, err := m.WriteTo(r.conn) r.dataM.Unlock() return len(b), err } func (r *RemoteCli) reportWidth() error { screenWidth := GetScreenWidth() data := make([]byte, 2) binary.BigEndian.PutUint16(data, uint16(screenWidth)) msg := NewMessage(T_WIDTH_REPORT, data) if err := r.writeMsg(msg); err != nil { return err } return nil } func (r *RemoteCli) reportIsTerminal() error { var isTerminal bool if r.isTerminal != nil { isTerminal = *r.isTerminal } else { isTerminal = DefaultIsTerminal() } data := make([]byte, 2) if isTerminal { binary.BigEndian.PutUint16(data, 1) } else { binary.BigEndian.PutUint16(data, 0) } msg := NewMessage(T_ISTTY_REPORT, data) if err := r.writeMsg(msg); err != nil { return err } return nil } func (r *RemoteCli) readLoop() { buf := bufio.NewReader(r.conn) for { msg, err := ReadMessage(buf) if err != nil { break } switch msg.Type { case T_ERAW: r.raw.Exit() case T_RAW: r.raw.Enter() case T_DATA: os.Stdout.Write(msg.Data) } } } func (r *RemoteCli) ServeBy(source io.Reader) error { if err := r.init(); err != nil { return err } go func() { defer r.Close() for { n, _ := io.Copy(r, source) if n == 0 { break } } }() defer r.raw.Exit() r.readLoop() return nil } func (r *RemoteCli) Close() { r.writeMsg(NewMessage(T_EOF, nil)) } func (r *RemoteCli) Serve() error { return r.ServeBy(os.Stdin) } func ListenRemote(n, addr string, cfg *Config, h func(*Instance), onListen ...func(net.Listener) error) error { ln, err := net.Listen(n, addr) if err != nil { return err } if len(onListen) > 0 { if err := onListen[0](ln); err != nil { return err } } for { conn, err := ln.Accept() if err != nil { break } go func() { defer conn.Close() rl, err := HandleConn(*cfg, conn) if err != nil { return } h(rl) }() } return nil } func HandleConn(cfg Config, conn net.Conn) (*Instance, error) { r, err := NewRemoteSvr(conn) if err != nil { return nil, err } r.HandleConfig(&cfg) rl, err := NewEx(&cfg) if err != nil { return nil, err } return rl, nil } func DialRemote(n, addr string) error { conn, err := net.Dial(n, addr) if err != nil { return err } defer conn.Close() cli, err := NewRemoteCli(conn) if err != nil { return err } return cli.Serve() } ================================================ FILE: vendor/github.com/chzyer/readline/runebuf.go ================================================ package readline import ( "bufio" "bytes" "io" "strconv" "strings" "sync" ) type runeBufferBck struct { buf []rune idx int } type RuneBuffer struct { buf []rune idx int prompt []rune w io.Writer hadClean bool interactive bool cfg *Config width int bck *runeBufferBck offset string lastKill []rune sync.Mutex } func (r *RuneBuffer) pushKill(text []rune) { r.lastKill = append([]rune{}, text...) } func (r *RuneBuffer) OnWidthChange(newWidth int) { r.Lock() r.width = newWidth r.Unlock() } func (r *RuneBuffer) Backup() { r.Lock() r.bck = &runeBufferBck{r.buf, r.idx} r.Unlock() } func (r *RuneBuffer) Restore() { r.Refresh(func() { if r.bck == nil { return } r.buf = r.bck.buf r.idx = r.bck.idx }) } func NewRuneBuffer(w io.Writer, prompt string, cfg *Config, width int) *RuneBuffer { rb := &RuneBuffer{ w: w, interactive: cfg.useInteractive(), cfg: cfg, width: width, } rb.SetPrompt(prompt) return rb } func (r *RuneBuffer) SetConfig(cfg *Config) { r.Lock() r.cfg = cfg r.interactive = cfg.useInteractive() r.Unlock() } func (r *RuneBuffer) SetMask(m rune) { r.Lock() r.cfg.MaskRune = m r.Unlock() } func (r *RuneBuffer) CurrentWidth(x int) int { r.Lock() defer r.Unlock() return runes.WidthAll(r.buf[:x]) } func (r *RuneBuffer) PromptLen() int { r.Lock() width := r.promptLen() r.Unlock() return width } func (r *RuneBuffer) promptLen() int { return runes.WidthAll(runes.ColorFilter(r.prompt)) } func (r *RuneBuffer) RuneSlice(i int) []rune { r.Lock() defer r.Unlock() if i > 0 { rs := make([]rune, i) copy(rs, r.buf[r.idx:r.idx+i]) return rs } rs := make([]rune, -i) copy(rs, r.buf[r.idx+i:r.idx]) return rs } func (r *RuneBuffer) Runes() []rune { r.Lock() newr := make([]rune, len(r.buf)) copy(newr, r.buf) r.Unlock() return newr } func (r *RuneBuffer) Pos() int { r.Lock() defer r.Unlock() return r.idx } func (r *RuneBuffer) Len() int { r.Lock() defer r.Unlock() return len(r.buf) } func (r *RuneBuffer) MoveToLineStart() { r.Refresh(func() { if r.idx == 0 { return } r.idx = 0 }) } func (r *RuneBuffer) MoveBackward() { r.Refresh(func() { if r.idx == 0 { return } r.idx-- }) } func (r *RuneBuffer) WriteString(s string) { r.WriteRunes([]rune(s)) } func (r *RuneBuffer) WriteRune(s rune) { r.WriteRunes([]rune{s}) } func (r *RuneBuffer) WriteRunes(s []rune) { r.Refresh(func() { tail := append(s, r.buf[r.idx:]...) r.buf = append(r.buf[:r.idx], tail...) r.idx += len(s) }) } func (r *RuneBuffer) MoveForward() { r.Refresh(func() { if r.idx == len(r.buf) { return } r.idx++ }) } func (r *RuneBuffer) IsCursorInEnd() bool { r.Lock() defer r.Unlock() return r.idx == len(r.buf) } func (r *RuneBuffer) Replace(ch rune) { r.Refresh(func() { r.buf[r.idx] = ch }) } func (r *RuneBuffer) Erase() { r.Refresh(func() { r.idx = 0 r.pushKill(r.buf[:]) r.buf = r.buf[:0] }) } func (r *RuneBuffer) Delete() (success bool) { r.Refresh(func() { if r.idx == len(r.buf) { return } r.pushKill(r.buf[r.idx : r.idx+1]) r.buf = append(r.buf[:r.idx], r.buf[r.idx+1:]...) success = true }) return } func (r *RuneBuffer) DeleteWord() { if r.idx == len(r.buf) { return } init := r.idx for init < len(r.buf) && IsWordBreak(r.buf[init]) { init++ } for i := init + 1; i < len(r.buf); i++ { if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) { r.pushKill(r.buf[r.idx : i-1]) r.Refresh(func() { r.buf = append(r.buf[:r.idx], r.buf[i-1:]...) }) return } } r.Kill() } func (r *RuneBuffer) MoveToPrevWord() (success bool) { r.Refresh(func() { if r.idx == 0 { return } for i := r.idx - 1; i > 0; i-- { if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) { r.idx = i success = true return } } r.idx = 0 success = true }) return } func (r *RuneBuffer) KillFront() { r.Refresh(func() { if r.idx == 0 { return } length := len(r.buf) - r.idx r.pushKill(r.buf[:r.idx]) copy(r.buf[:length], r.buf[r.idx:]) r.idx = 0 r.buf = r.buf[:length] }) } func (r *RuneBuffer) Kill() { r.Refresh(func() { r.pushKill(r.buf[r.idx:]) r.buf = r.buf[:r.idx] }) } func (r *RuneBuffer) Transpose() { r.Refresh(func() { if len(r.buf) == 1 { r.idx++ } if len(r.buf) < 2 { return } if r.idx == 0 { r.idx = 1 } else if r.idx >= len(r.buf) { r.idx = len(r.buf) - 1 } r.buf[r.idx], r.buf[r.idx-1] = r.buf[r.idx-1], r.buf[r.idx] r.idx++ }) } func (r *RuneBuffer) MoveToNextWord() { r.Refresh(func() { for i := r.idx + 1; i < len(r.buf); i++ { if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) { r.idx = i return } } r.idx = len(r.buf) }) } func (r *RuneBuffer) MoveToEndWord() { r.Refresh(func() { // already at the end, so do nothing if r.idx == len(r.buf) { return } // if we are at the end of a word already, go to next if !IsWordBreak(r.buf[r.idx]) && IsWordBreak(r.buf[r.idx+1]) { r.idx++ } // keep going until at the end of a word for i := r.idx + 1; i < len(r.buf); i++ { if IsWordBreak(r.buf[i]) && !IsWordBreak(r.buf[i-1]) { r.idx = i - 1 return } } r.idx = len(r.buf) }) } func (r *RuneBuffer) BackEscapeWord() { r.Refresh(func() { if r.idx == 0 { return } for i := r.idx - 1; i > 0; i-- { if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) { r.pushKill(r.buf[i:r.idx]) r.buf = append(r.buf[:i], r.buf[r.idx:]...) r.idx = i return } } r.buf = r.buf[:0] r.idx = 0 }) } func (r *RuneBuffer) Yank() { if len(r.lastKill) == 0 { return } r.Refresh(func() { buf := make([]rune, 0, len(r.buf)+len(r.lastKill)) buf = append(buf, r.buf[:r.idx]...) buf = append(buf, r.lastKill...) buf = append(buf, r.buf[r.idx:]...) r.buf = buf r.idx += len(r.lastKill) }) } func (r *RuneBuffer) Backspace() { r.Refresh(func() { if r.idx == 0 { return } r.idx-- r.buf = append(r.buf[:r.idx], r.buf[r.idx+1:]...) }) } func (r *RuneBuffer) MoveToLineEnd() { r.Refresh(func() { if r.idx == len(r.buf) { return } r.idx = len(r.buf) }) } func (r *RuneBuffer) LineCount(width int) int { if width == -1 { width = r.width } return LineCount(width, runes.WidthAll(r.buf)+r.PromptLen()) } func (r *RuneBuffer) MoveTo(ch rune, prevChar, reverse bool) (success bool) { r.Refresh(func() { if reverse { for i := r.idx - 1; i >= 0; i-- { if r.buf[i] == ch { r.idx = i if prevChar { r.idx++ } success = true return } } return } for i := r.idx + 1; i < len(r.buf); i++ { if r.buf[i] == ch { r.idx = i if prevChar { r.idx-- } success = true return } } }) return } func (r *RuneBuffer) isInLineEdge() bool { if isWindows { return false } sp := r.getSplitByLine(r.buf) return len(sp[len(sp)-1]) == 0 } func (r *RuneBuffer) getSplitByLine(rs []rune) []string { return SplitByLine(r.promptLen(), r.width, rs) } func (r *RuneBuffer) IdxLine(width int) int { r.Lock() defer r.Unlock() return r.idxLine(width) } func (r *RuneBuffer) idxLine(width int) int { if width == 0 { return 0 } sp := r.getSplitByLine(r.buf[:r.idx]) return len(sp) - 1 } func (r *RuneBuffer) CursorLineCount() int { return r.LineCount(r.width) - r.IdxLine(r.width) } func (r *RuneBuffer) Refresh(f func()) { r.Lock() defer r.Unlock() if !r.interactive { if f != nil { f() } return } r.clean() if f != nil { f() } r.print() } func (r *RuneBuffer) SetOffset(offset string) { r.Lock() r.offset = offset r.Unlock() } func (r *RuneBuffer) print() { r.w.Write(r.output()) r.hadClean = false } func (r *RuneBuffer) output() []byte { buf := bytes.NewBuffer(nil) buf.WriteString(string(r.prompt)) if r.cfg.EnableMask && len(r.buf) > 0 { buf.Write([]byte(strings.Repeat(string(r.cfg.MaskRune), len(r.buf)-1))) if r.buf[len(r.buf)-1] == '\n' { buf.Write([]byte{'\n'}) } else { buf.Write([]byte(string(r.cfg.MaskRune))) } if len(r.buf) > r.idx { buf.Write(r.getBackspaceSequence()) } } else { for _, e := range r.cfg.Painter.Paint(r.buf, r.idx) { if e == '\t' { buf.WriteString(strings.Repeat(" ", TabWidth)) } else { buf.WriteRune(e) } } if r.isInLineEdge() { buf.Write([]byte(" \b")) } } // cursor position if len(r.buf) > r.idx { buf.Write(r.getBackspaceSequence()) } return buf.Bytes() } func (r *RuneBuffer) getBackspaceSequence() []byte { var sep = map[int]bool{} var i int for { if i >= runes.WidthAll(r.buf) { break } if i == 0 { i -= r.promptLen() } i += r.width sep[i] = true } var buf []byte for i := len(r.buf); i > r.idx; i-- { // move input to the left of one buf = append(buf, '\b') if sep[i] { // up one line, go to the start of the line and move cursor right to the end (r.width) buf = append(buf, "\033[A\r"+"\033["+strconv.Itoa(r.width)+"C"...) } } return buf } func (r *RuneBuffer) Reset() []rune { ret := runes.Copy(r.buf) r.buf = r.buf[:0] r.idx = 0 return ret } func (r *RuneBuffer) calWidth(m int) int { if m > 0 { return runes.WidthAll(r.buf[r.idx : r.idx+m]) } return runes.WidthAll(r.buf[r.idx+m : r.idx]) } func (r *RuneBuffer) SetStyle(start, end int, style string) { if end < start { panic("end < start") } // goto start move := start - r.idx if move > 0 { r.w.Write([]byte(string(r.buf[r.idx : r.idx+move]))) } else { r.w.Write(bytes.Repeat([]byte("\b"), r.calWidth(move))) } r.w.Write([]byte("\033[" + style + "m")) r.w.Write([]byte(string(r.buf[start:end]))) r.w.Write([]byte("\033[0m")) // TODO: move back } func (r *RuneBuffer) SetWithIdx(idx int, buf []rune) { r.Refresh(func() { r.buf = buf r.idx = idx }) } func (r *RuneBuffer) Set(buf []rune) { r.SetWithIdx(len(buf), buf) } func (r *RuneBuffer) SetPrompt(prompt string) { r.Lock() r.prompt = []rune(prompt) r.Unlock() } func (r *RuneBuffer) cleanOutput(w io.Writer, idxLine int) { buf := bufio.NewWriter(w) if r.width == 0 { buf.WriteString(strings.Repeat("\r\b", len(r.buf)+r.promptLen())) buf.Write([]byte("\033[J")) } else { buf.Write([]byte("\033[J")) // just like ^k :) if idxLine == 0 { buf.WriteString("\033[2K") buf.WriteString("\r") } else { for i := 0; i < idxLine; i++ { io.WriteString(buf, "\033[2K\r\033[A") } io.WriteString(buf, "\033[2K\r") } } buf.Flush() return } func (r *RuneBuffer) Clean() { r.Lock() r.clean() r.Unlock() } func (r *RuneBuffer) clean() { r.cleanWithIdxLine(r.idxLine(r.width)) } func (r *RuneBuffer) cleanWithIdxLine(idxLine int) { if r.hadClean || !r.interactive { return } r.hadClean = true r.cleanOutput(r.w, idxLine) } ================================================ FILE: vendor/github.com/chzyer/readline/runes.go ================================================ package readline import ( "bytes" "unicode" "unicode/utf8" ) var runes = Runes{} var TabWidth = 4 type Runes struct{} func (Runes) EqualRune(a, b rune, fold bool) bool { if a == b { return true } if !fold { return false } if a > b { a, b = b, a } if b < utf8.RuneSelf && 'A' <= a && a <= 'Z' { if b == a+'a'-'A' { return true } } return false } func (r Runes) EqualRuneFold(a, b rune) bool { return r.EqualRune(a, b, true) } func (r Runes) EqualFold(a, b []rune) bool { if len(a) != len(b) { return false } for i := 0; i < len(a); i++ { if r.EqualRuneFold(a[i], b[i]) { continue } return false } return true } func (Runes) Equal(a, b []rune) bool { if len(a) != len(b) { return false } for i := 0; i < len(a); i++ { if a[i] != b[i] { return false } } return true } func (rs Runes) IndexAllBckEx(r, sub []rune, fold bool) int { for i := len(r) - len(sub); i >= 0; i-- { found := true for j := 0; j < len(sub); j++ { if !rs.EqualRune(r[i+j], sub[j], fold) { found = false break } } if found { return i } } return -1 } // Search in runes from end to front func (rs Runes) IndexAllBck(r, sub []rune) int { return rs.IndexAllBckEx(r, sub, false) } // Search in runes from front to end func (rs Runes) IndexAll(r, sub []rune) int { return rs.IndexAllEx(r, sub, false) } func (rs Runes) IndexAllEx(r, sub []rune, fold bool) int { for i := 0; i < len(r); i++ { found := true if len(r[i:]) < len(sub) { return -1 } for j := 0; j < len(sub); j++ { if !rs.EqualRune(r[i+j], sub[j], fold) { found = false break } } if found { return i } } return -1 } func (Runes) Index(r rune, rs []rune) int { for i := 0; i < len(rs); i++ { if rs[i] == r { return i } } return -1 } func (Runes) ColorFilter(r []rune) []rune { newr := make([]rune, 0, len(r)) for pos := 0; pos < len(r); pos++ { if r[pos] == '\033' && r[pos+1] == '[' { idx := runes.Index('m', r[pos+2:]) if idx == -1 { continue } pos += idx + 2 continue } newr = append(newr, r[pos]) } return newr } var zeroWidth = []*unicode.RangeTable{ unicode.Mn, unicode.Me, unicode.Cc, unicode.Cf, } var doubleWidth = []*unicode.RangeTable{ unicode.Han, unicode.Hangul, unicode.Hiragana, unicode.Katakana, } func (Runes) Width(r rune) int { if r == '\t' { return TabWidth } if unicode.IsOneOf(zeroWidth, r) { return 0 } if unicode.IsOneOf(doubleWidth, r) { return 2 } return 1 } func (Runes) WidthAll(r []rune) (length int) { for i := 0; i < len(r); i++ { length += runes.Width(r[i]) } return } func (Runes) Backspace(r []rune) []byte { return bytes.Repeat([]byte{'\b'}, runes.WidthAll(r)) } func (Runes) Copy(r []rune) []rune { n := make([]rune, len(r)) copy(n, r) return n } func (Runes) HasPrefixFold(r, prefix []rune) bool { if len(r) < len(prefix) { return false } return runes.EqualFold(r[:len(prefix)], prefix) } func (Runes) HasPrefix(r, prefix []rune) bool { if len(r) < len(prefix) { return false } return runes.Equal(r[:len(prefix)], prefix) } func (Runes) Aggregate(candicate [][]rune) (same []rune, size int) { for i := 0; i < len(candicate[0]); i++ { for j := 0; j < len(candicate)-1; j++ { if i >= len(candicate[j]) || i >= len(candicate[j+1]) { goto aggregate } if candicate[j][i] != candicate[j+1][i] { goto aggregate } } size = i + 1 } aggregate: if size > 0 { same = runes.Copy(candicate[0][:size]) for i := 0; i < len(candicate); i++ { n := runes.Copy(candicate[i]) copy(n, n[size:]) candicate[i] = n[:len(n)-size] } } return } func (Runes) TrimSpaceLeft(in []rune) []rune { firstIndex := len(in) for i, r := range in { if unicode.IsSpace(r) == false { firstIndex = i break } } return in[firstIndex:] } ================================================ FILE: vendor/github.com/chzyer/readline/search.go ================================================ package readline import ( "bytes" "container/list" "fmt" "io" ) const ( S_STATE_FOUND = iota S_STATE_FAILING ) const ( S_DIR_BCK = iota S_DIR_FWD ) type opSearch struct { inMode bool state int dir int source *list.Element w io.Writer buf *RuneBuffer data []rune history *opHistory cfg *Config markStart int markEnd int width int } func newOpSearch(w io.Writer, buf *RuneBuffer, history *opHistory, cfg *Config, width int) *opSearch { return &opSearch{ w: w, buf: buf, cfg: cfg, history: history, width: width, } } func (o *opSearch) OnWidthChange(newWidth int) { o.width = newWidth } func (o *opSearch) IsSearchMode() bool { return o.inMode } func (o *opSearch) SearchBackspace() { if len(o.data) > 0 { o.data = o.data[:len(o.data)-1] o.search(true) } } func (o *opSearch) findHistoryBy(isNewSearch bool) (int, *list.Element) { if o.dir == S_DIR_BCK { return o.history.FindBck(isNewSearch, o.data, o.buf.idx) } return o.history.FindFwd(isNewSearch, o.data, o.buf.idx) } func (o *opSearch) search(isChange bool) bool { if len(o.data) == 0 { o.state = S_STATE_FOUND o.SearchRefresh(-1) return true } idx, elem := o.findHistoryBy(isChange) if elem == nil { o.SearchRefresh(-2) return false } o.history.current = elem item := o.history.showItem(o.history.current.Value) start, end := 0, 0 if o.dir == S_DIR_BCK { start, end = idx, idx+len(o.data) } else { start, end = idx, idx+len(o.data) idx += len(o.data) } o.buf.SetWithIdx(idx, item) o.markStart, o.markEnd = start, end o.SearchRefresh(idx) return true } func (o *opSearch) SearchChar(r rune) { o.data = append(o.data, r) o.search(true) } func (o *opSearch) SearchMode(dir int) bool { if o.width == 0 { return false } alreadyInMode := o.inMode o.inMode = true o.dir = dir o.source = o.history.current if alreadyInMode { o.search(false) } else { o.SearchRefresh(-1) } return true } func (o *opSearch) ExitSearchMode(revert bool) { if revert { o.history.current = o.source o.buf.Set(o.history.showItem(o.history.current.Value)) } o.markStart, o.markEnd = 0, 0 o.state = S_STATE_FOUND o.inMode = false o.source = nil o.data = nil } func (o *opSearch) SearchRefresh(x int) { if x == -2 { o.state = S_STATE_FAILING } else if x >= 0 { o.state = S_STATE_FOUND } if x < 0 { x = o.buf.idx } x = o.buf.CurrentWidth(x) x += o.buf.PromptLen() x = x % o.width if o.markStart > 0 { o.buf.SetStyle(o.markStart, o.markEnd, "4") } lineCnt := o.buf.CursorLineCount() buf := bytes.NewBuffer(nil) buf.Write(bytes.Repeat([]byte("\n"), lineCnt)) buf.WriteString("\033[J") if o.state == S_STATE_FAILING { buf.WriteString("failing ") } if o.dir == S_DIR_BCK { buf.WriteString("bck") } else if o.dir == S_DIR_FWD { buf.WriteString("fwd") } buf.WriteString("-i-search: ") buf.WriteString(string(o.data)) // keyword buf.WriteString("\033[4m \033[0m") // _ fmt.Fprintf(buf, "\r\033[%dA", lineCnt) // move prev if x > 0 { fmt.Fprintf(buf, "\033[%dC", x) // move forward } o.w.Write(buf.Bytes()) } ================================================ FILE: vendor/github.com/chzyer/readline/std.go ================================================ package readline import ( "io" "os" "sync" "sync/atomic" ) var ( Stdin io.ReadCloser = os.Stdin Stdout io.WriteCloser = os.Stdout Stderr io.WriteCloser = os.Stderr ) var ( std *Instance stdOnce sync.Once ) // global instance will not submit history automatic func getInstance() *Instance { stdOnce.Do(func() { std, _ = NewEx(&Config{ DisableAutoSaveHistory: true, }) }) return std } // let readline load history from filepath // and try to persist history into disk // set fp to "" to prevent readline persisting history to disk // so the `AddHistory` will return nil error forever. func SetHistoryPath(fp string) { ins := getInstance() cfg := ins.Config.Clone() cfg.HistoryFile = fp ins.SetConfig(cfg) } // set auto completer to global instance func SetAutoComplete(completer AutoCompleter) { ins := getInstance() cfg := ins.Config.Clone() cfg.AutoComplete = completer ins.SetConfig(cfg) } // add history to global instance manually // raise error only if `SetHistoryPath` is set with a non-empty path func AddHistory(content string) error { ins := getInstance() return ins.SaveHistory(content) } func Password(prompt string) ([]byte, error) { ins := getInstance() return ins.ReadPassword(prompt) } // readline with global configs func Line(prompt string) (string, error) { ins := getInstance() ins.SetPrompt(prompt) return ins.Readline() } type CancelableStdin struct { r io.Reader mutex sync.Mutex stop chan struct{} closed int32 notify chan struct{} data []byte read int err error } func NewCancelableStdin(r io.Reader) *CancelableStdin { c := &CancelableStdin{ r: r, notify: make(chan struct{}), stop: make(chan struct{}), } go c.ioloop() return c } func (c *CancelableStdin) ioloop() { loop: for { select { case <-c.notify: c.read, c.err = c.r.Read(c.data) select { case c.notify <- struct{}{}: case <-c.stop: break loop } case <-c.stop: break loop } } } func (c *CancelableStdin) Read(b []byte) (n int, err error) { c.mutex.Lock() defer c.mutex.Unlock() if atomic.LoadInt32(&c.closed) == 1 { return 0, io.EOF } c.data = b select { case c.notify <- struct{}{}: case <-c.stop: return 0, io.EOF } select { case <-c.notify: return c.read, c.err case <-c.stop: return 0, io.EOF } } func (c *CancelableStdin) Close() error { if atomic.CompareAndSwapInt32(&c.closed, 0, 1) { close(c.stop) } return nil } // FillableStdin is a stdin reader which can prepend some data before // reading into the real stdin type FillableStdin struct { sync.Mutex stdin io.Reader stdinBuffer io.ReadCloser buf []byte bufErr error } // NewFillableStdin gives you FillableStdin func NewFillableStdin(stdin io.Reader) (io.ReadCloser, io.Writer) { r, w := io.Pipe() s := &FillableStdin{ stdinBuffer: r, stdin: stdin, } s.ioloop() return s, w } func (s *FillableStdin) ioloop() { go func() { for { bufR := make([]byte, 100) var n int n, s.bufErr = s.stdinBuffer.Read(bufR) if s.bufErr != nil { if s.bufErr == io.ErrClosedPipe { break } } s.Lock() s.buf = append(s.buf, bufR[:n]...) s.Unlock() } }() } // Read will read from the local buffer and if no data, read from stdin func (s *FillableStdin) Read(p []byte) (n int, err error) { s.Lock() i := len(s.buf) if len(p) < i { i = len(p) } if i > 0 { n := copy(p, s.buf) s.buf = s.buf[:0] cerr := s.bufErr s.bufErr = nil s.Unlock() return n, cerr } s.Unlock() n, err = s.stdin.Read(p) return n, err } func (s *FillableStdin) Close() error { s.stdinBuffer.Close() return nil } ================================================ FILE: vendor/github.com/chzyer/readline/std_windows.go ================================================ // +build windows package readline func init() { Stdin = NewRawReader() Stdout = NewANSIWriter(Stdout) Stderr = NewANSIWriter(Stderr) } ================================================ FILE: vendor/github.com/chzyer/readline/term.go ================================================ // Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build aix darwin dragonfly freebsd linux,!appengine netbsd openbsd os400 solaris // Package terminal provides support functions for dealing with terminals, as // commonly found on UNIX systems. // // Putting a terminal into raw mode is the most common requirement: // // oldState, err := terminal.MakeRaw(0) // if err != nil { // panic(err) // } // defer terminal.Restore(0, oldState) package readline import ( "io" "syscall" ) // State contains the state of a terminal. type State struct { termios Termios } // IsTerminal returns true if the given file descriptor is a terminal. func IsTerminal(fd int) bool { _, err := getTermios(fd) return err == nil } // MakeRaw put the terminal connected to the given file descriptor into raw // mode and returns the previous state of the terminal so that it can be // restored. func MakeRaw(fd int) (*State, error) { var oldState State if termios, err := getTermios(fd); err != nil { return nil, err } else { oldState.termios = *termios } newState := oldState.termios // This attempts to replicate the behaviour documented for cfmakeraw in // the termios(3) manpage. newState.Iflag &^= syscall.IGNBRK | syscall.BRKINT | syscall.PARMRK | syscall.ISTRIP | syscall.INLCR | syscall.IGNCR | syscall.ICRNL | syscall.IXON // newState.Oflag &^= syscall.OPOST newState.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN newState.Cflag &^= syscall.CSIZE | syscall.PARENB newState.Cflag |= syscall.CS8 newState.Cc[syscall.VMIN] = 1 newState.Cc[syscall.VTIME] = 0 return &oldState, setTermios(fd, &newState) } // GetState returns the current state of a terminal which may be useful to // restore the terminal after a signal. func GetState(fd int) (*State, error) { termios, err := getTermios(fd) if err != nil { return nil, err } return &State{termios: *termios}, nil } // Restore restores the terminal connected to the given file descriptor to a // previous state. func restoreTerm(fd int, state *State) error { return setTermios(fd, &state.termios) } // ReadPassword reads a line of input from a terminal without local echo. This // is commonly used for inputting passwords and other sensitive data. The slice // returned does not include the \n. func ReadPassword(fd int) ([]byte, error) { oldState, err := getTermios(fd) if err != nil { return nil, err } newState := oldState newState.Lflag &^= syscall.ECHO newState.Lflag |= syscall.ICANON | syscall.ISIG newState.Iflag |= syscall.ICRNL if err := setTermios(fd, newState); err != nil { return nil, err } defer func() { setTermios(fd, oldState) }() var buf [16]byte var ret []byte for { n, err := syscall.Read(fd, buf[:]) if err != nil { return nil, err } if n == 0 { if len(ret) == 0 { return nil, io.EOF } break } if buf[n-1] == '\n' { n-- } ret = append(ret, buf[:n]...) if n < len(buf) { break } } return ret, nil } ================================================ FILE: vendor/github.com/chzyer/readline/term_bsd.go ================================================ // Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build darwin dragonfly freebsd netbsd openbsd package readline import ( "syscall" "unsafe" ) func getTermios(fd int) (*Termios, error) { termios := new(Termios) _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), syscall.TIOCGETA, uintptr(unsafe.Pointer(termios)), 0, 0, 0) if err != 0 { return nil, err } return termios, nil } func setTermios(fd int, termios *Termios) error { _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), syscall.TIOCSETA, uintptr(unsafe.Pointer(termios)), 0, 0, 0) if err != 0 { return err } return nil } ================================================ FILE: vendor/github.com/chzyer/readline/term_linux.go ================================================ // Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package readline import ( "syscall" "unsafe" ) // These constants are declared here, rather than importing // them from the syscall package as some syscall packages, even // on linux, for example gccgo, do not declare them. const ioctlReadTermios = 0x5401 // syscall.TCGETS const ioctlWriteTermios = 0x5402 // syscall.TCSETS func getTermios(fd int) (*Termios, error) { termios := new(Termios) _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(termios)), 0, 0, 0) if err != 0 { return nil, err } return termios, nil } func setTermios(fd int, termios *Termios) error { _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(termios)), 0, 0, 0) if err != 0 { return err } return nil } ================================================ FILE: vendor/github.com/chzyer/readline/term_nosyscall6.go ================================================ // Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build aix os400 solaris package readline import "golang.org/x/sys/unix" // GetSize returns the dimensions of the given terminal. func GetSize(fd int) (int, int, error) { ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) if err != nil { return 0, 0, err } return int(ws.Col), int(ws.Row), nil } type Termios unix.Termios func getTermios(fd int) (*Termios, error) { termios, err := unix.IoctlGetTermios(fd, unix.TCGETS) if err != nil { return nil, err } return (*Termios)(termios), nil } func setTermios(fd int, termios *Termios) error { return unix.IoctlSetTermios(fd, unix.TCSETSF, (*unix.Termios)(termios)) } ================================================ FILE: vendor/github.com/chzyer/readline/term_unix.go ================================================ // Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build darwin dragonfly freebsd linux,!appengine netbsd openbsd package readline import ( "syscall" "unsafe" ) type Termios syscall.Termios // GetSize returns the dimensions of the given terminal. func GetSize(fd int) (int, int, error) { var dimensions [4]uint16 _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&dimensions)), 0, 0, 0) if err != 0 { return 0, 0, err } return int(dimensions[1]), int(dimensions[0]), nil } ================================================ FILE: vendor/github.com/chzyer/readline/term_windows.go ================================================ // Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build windows // Package terminal provides support functions for dealing with terminals, as // commonly found on UNIX systems. // // Putting a terminal into raw mode is the most common requirement: // // oldState, err := terminal.MakeRaw(0) // if err != nil { // panic(err) // } // defer terminal.Restore(0, oldState) package readline import ( "io" "syscall" "unsafe" ) const ( enableLineInput = 2 enableEchoInput = 4 enableProcessedInput = 1 enableWindowInput = 8 enableMouseInput = 16 enableInsertMode = 32 enableQuickEditMode = 64 enableExtendedFlags = 128 enableAutoPosition = 256 enableProcessedOutput = 1 enableWrapAtEolOutput = 2 ) var kernel32 = syscall.NewLazyDLL("kernel32.dll") var ( procGetConsoleMode = kernel32.NewProc("GetConsoleMode") procSetConsoleMode = kernel32.NewProc("SetConsoleMode") procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") ) type ( coord struct { x short y short } smallRect struct { left short top short right short bottom short } consoleScreenBufferInfo struct { size coord cursorPosition coord attributes word window smallRect maximumWindowSize coord } ) type State struct { mode uint32 } // IsTerminal returns true if the given file descriptor is a terminal. func IsTerminal(fd int) bool { var st uint32 r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) return r != 0 && e == 0 } // MakeRaw put the terminal connected to the given file descriptor into raw // mode and returns the previous state of the terminal so that it can be // restored. func MakeRaw(fd int) (*State, error) { var st uint32 _, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) if e != 0 { return nil, error(e) } raw := st &^ (enableEchoInput | enableProcessedInput | enableLineInput | enableProcessedOutput) _, _, e = syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(raw), 0) if e != 0 { return nil, error(e) } return &State{st}, nil } // GetState returns the current state of a terminal which may be useful to // restore the terminal after a signal. func GetState(fd int) (*State, error) { var st uint32 _, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) if e != 0 { return nil, error(e) } return &State{st}, nil } // Restore restores the terminal connected to the given file descriptor to a // previous state. func restoreTerm(fd int, state *State) error { _, _, err := syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(state.mode), 0) return err } // GetSize returns the dimensions of the given terminal. func GetSize(fd int) (width, height int, err error) { var info consoleScreenBufferInfo _, _, e := syscall.Syscall(procGetConsoleScreenBufferInfo.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&info)), 0) if e != 0 { return 0, 0, error(e) } return int(info.size.x), int(info.size.y), nil } // ReadPassword reads a line of input from a terminal without local echo. This // is commonly used for inputting passwords and other sensitive data. The slice // returned does not include the \n. func ReadPassword(fd int) ([]byte, error) { var st uint32 _, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) if e != 0 { return nil, error(e) } old := st st &^= (enableEchoInput) st |= (enableProcessedInput | enableLineInput | enableProcessedOutput) _, _, e = syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(st), 0) if e != 0 { return nil, error(e) } defer func() { syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(old), 0) }() var buf [16]byte var ret []byte for { n, err := syscall.Read(syscall.Handle(fd), buf[:]) if err != nil { return nil, err } if n == 0 { if len(ret) == 0 { return nil, io.EOF } break } if buf[n-1] == '\n' { n-- } if n > 0 && buf[n-1] == '\r' { n-- } ret = append(ret, buf[:n]...) if n < len(buf) { break } } return ret, nil } ================================================ FILE: vendor/github.com/chzyer/readline/terminal.go ================================================ package readline import ( "bufio" "fmt" "io" "strings" "sync" "sync/atomic" ) type Terminal struct { m sync.Mutex cfg *Config outchan chan rune closed int32 stopChan chan struct{} kickChan chan struct{} wg sync.WaitGroup isReading int32 sleeping int32 sizeChan chan string } func NewTerminal(cfg *Config) (*Terminal, error) { if err := cfg.Init(); err != nil { return nil, err } t := &Terminal{ cfg: cfg, kickChan: make(chan struct{}, 1), outchan: make(chan rune), stopChan: make(chan struct{}, 1), sizeChan: make(chan string, 1), } go t.ioloop() return t, nil } // SleepToResume will sleep myself, and return only if I'm resumed. func (t *Terminal) SleepToResume() { if !atomic.CompareAndSwapInt32(&t.sleeping, 0, 1) { return } defer atomic.StoreInt32(&t.sleeping, 0) t.ExitRawMode() ch := WaitForResume() SuspendMe() <-ch t.EnterRawMode() } func (t *Terminal) EnterRawMode() (err error) { return t.cfg.FuncMakeRaw() } func (t *Terminal) ExitRawMode() (err error) { return t.cfg.FuncExitRaw() } func (t *Terminal) Write(b []byte) (int, error) { return t.cfg.Stdout.Write(b) } // WriteStdin prefill the next Stdin fetch // Next time you call ReadLine() this value will be writen before the user input func (t *Terminal) WriteStdin(b []byte) (int, error) { return t.cfg.StdinWriter.Write(b) } type termSize struct { left int top int } func (t *Terminal) GetOffset(f func(offset string)) { go func() { f(<-t.sizeChan) }() t.Write([]byte("\033[6n")) } func (t *Terminal) Print(s string) { fmt.Fprintf(t.cfg.Stdout, "%s", s) } func (t *Terminal) PrintRune(r rune) { fmt.Fprintf(t.cfg.Stdout, "%c", r) } func (t *Terminal) Readline() *Operation { return NewOperation(t, t.cfg) } // return rune(0) if meet EOF func (t *Terminal) ReadRune() rune { ch, ok := <-t.outchan if !ok { return rune(0) } return ch } func (t *Terminal) IsReading() bool { return atomic.LoadInt32(&t.isReading) == 1 } func (t *Terminal) KickRead() { select { case t.kickChan <- struct{}{}: default: } } func (t *Terminal) ioloop() { t.wg.Add(1) defer func() { t.wg.Done() close(t.outchan) }() var ( isEscape bool isEscapeEx bool isEscapeSS3 bool expectNextChar bool ) buf := bufio.NewReader(t.getStdin()) for { if !expectNextChar { atomic.StoreInt32(&t.isReading, 0) select { case <-t.kickChan: atomic.StoreInt32(&t.isReading, 1) case <-t.stopChan: return } } expectNextChar = false r, _, err := buf.ReadRune() if err != nil { if strings.Contains(err.Error(), "interrupted system call") { expectNextChar = true continue } break } if isEscape { isEscape = false if r == CharEscapeEx { // ^][ expectNextChar = true isEscapeEx = true continue } else if r == CharO { // ^]O expectNextChar = true isEscapeSS3 = true continue } r = escapeKey(r, buf) } else if isEscapeEx { isEscapeEx = false if key := readEscKey(r, buf); key != nil { r = escapeExKey(key) // offset if key.typ == 'R' { if _, _, ok := key.Get2(); ok { select { case t.sizeChan <- key.attr: default: } } expectNextChar = true continue } } if r == 0 { expectNextChar = true continue } } else if isEscapeSS3 { isEscapeSS3 = false if key := readEscKey(r, buf); key != nil { r = escapeSS3Key(key) } if r == 0 { expectNextChar = true continue } } expectNextChar = true switch r { case CharEsc: if t.cfg.VimMode { t.outchan <- r break } isEscape = true case CharInterrupt, CharEnter, CharCtrlJ, CharDelete: expectNextChar = false fallthrough default: t.outchan <- r } } } func (t *Terminal) Bell() { fmt.Fprintf(t, "%c", CharBell) } func (t *Terminal) Close() error { if atomic.SwapInt32(&t.closed, 1) != 0 { return nil } if closer, ok := t.cfg.Stdin.(io.Closer); ok { closer.Close() } close(t.stopChan) t.wg.Wait() return t.ExitRawMode() } func (t *Terminal) GetConfig() *Config { t.m.Lock() cfg := *t.cfg t.m.Unlock() return &cfg } func (t *Terminal) getStdin() io.Reader { t.m.Lock() r := t.cfg.Stdin t.m.Unlock() return r } func (t *Terminal) SetConfig(c *Config) error { if err := c.Init(); err != nil { return err } t.m.Lock() t.cfg = c t.m.Unlock() return nil } ================================================ FILE: vendor/github.com/chzyer/readline/utils.go ================================================ package readline import ( "bufio" "bytes" "container/list" "fmt" "os" "os/signal" "strconv" "strings" "sync" "syscall" "time" "unicode" ) var ( isWindows = false ) const ( CharLineStart = 1 CharBackward = 2 CharInterrupt = 3 CharDelete = 4 CharLineEnd = 5 CharForward = 6 CharBell = 7 CharCtrlH = 8 CharTab = 9 CharCtrlJ = 10 CharKill = 11 CharCtrlL = 12 CharEnter = 13 CharNext = 14 CharPrev = 16 CharBckSearch = 18 CharFwdSearch = 19 CharTranspose = 20 CharCtrlU = 21 CharCtrlW = 23 CharCtrlY = 25 CharCtrlZ = 26 CharEsc = 27 CharO = 79 CharEscapeEx = 91 CharBackspace = 127 ) const ( MetaBackward rune = -iota - 1 MetaForward MetaDelete MetaBackspace MetaTranspose ) // WaitForResume need to call before current process got suspend. // It will run a ticker until a long duration is occurs, // which means this process is resumed. func WaitForResume() chan struct{} { ch := make(chan struct{}) var wg sync.WaitGroup wg.Add(1) go func() { ticker := time.NewTicker(10 * time.Millisecond) t := time.Now() wg.Done() for { now := <-ticker.C if now.Sub(t) > 100*time.Millisecond { break } t = now } ticker.Stop() ch <- struct{}{} }() wg.Wait() return ch } func Restore(fd int, state *State) error { err := restoreTerm(fd, state) if err != nil { // errno 0 means everything is ok :) if err.Error() == "errno 0" { return nil } else { return err } } return nil } func IsPrintable(key rune) bool { isInSurrogateArea := key >= 0xd800 && key <= 0xdbff return key >= 32 && !isInSurrogateArea } // translate Esc[X func escapeExKey(key *escapeKeyPair) rune { var r rune switch key.typ { case 'D': r = CharBackward case 'C': r = CharForward case 'A': r = CharPrev case 'B': r = CharNext case 'H': r = CharLineStart case 'F': r = CharLineEnd case '~': if key.attr == "3" { r = CharDelete } default: } return r } // translate EscOX SS3 codes for up/down/etc. func escapeSS3Key(key *escapeKeyPair) rune { var r rune switch key.typ { case 'D': r = CharBackward case 'C': r = CharForward case 'A': r = CharPrev case 'B': r = CharNext case 'H': r = CharLineStart case 'F': r = CharLineEnd default: } return r } type escapeKeyPair struct { attr string typ rune } func (e *escapeKeyPair) Get2() (int, int, bool) { sp := strings.Split(e.attr, ";") if len(sp) < 2 { return -1, -1, false } s1, err := strconv.Atoi(sp[0]) if err != nil { return -1, -1, false } s2, err := strconv.Atoi(sp[1]) if err != nil { return -1, -1, false } return s1, s2, true } func readEscKey(r rune, reader *bufio.Reader) *escapeKeyPair { p := escapeKeyPair{} buf := bytes.NewBuffer(nil) for { if r == ';' { } else if unicode.IsNumber(r) { } else { p.typ = r break } buf.WriteRune(r) r, _, _ = reader.ReadRune() } p.attr = buf.String() return &p } // translate EscX to Meta+X func escapeKey(r rune, reader *bufio.Reader) rune { switch r { case 'b': r = MetaBackward case 'f': r = MetaForward case 'd': r = MetaDelete case CharTranspose: r = MetaTranspose case CharBackspace: r = MetaBackspace case 'O': d, _, _ := reader.ReadRune() switch d { case 'H': r = CharLineStart case 'F': r = CharLineEnd default: reader.UnreadRune() } case CharEsc: } return r } func SplitByLine(start, screenWidth int, rs []rune) []string { var ret []string buf := bytes.NewBuffer(nil) currentWidth := start for _, r := range rs { w := runes.Width(r) currentWidth += w buf.WriteRune(r) if currentWidth >= screenWidth { ret = append(ret, buf.String()) buf.Reset() currentWidth = 0 } } ret = append(ret, buf.String()) return ret } // calculate how many lines for N character func LineCount(screenWidth, w int) int { r := w / screenWidth if w%screenWidth != 0 { r++ } return r } func IsWordBreak(i rune) bool { switch { case i >= 'a' && i <= 'z': case i >= 'A' && i <= 'Z': case i >= '0' && i <= '9': default: return true } return false } func GetInt(s []string, def int) int { if len(s) == 0 { return def } c, err := strconv.Atoi(s[0]) if err != nil { return def } return c } type RawMode struct { state *State } func (r *RawMode) Enter() (err error) { r.state, err = MakeRaw(GetStdin()) return err } func (r *RawMode) Exit() error { if r.state == nil { return nil } return Restore(GetStdin(), r.state) } // ----------------------------------------------------------------------------- func sleep(n int) { Debug(n) time.Sleep(2000 * time.Millisecond) } // print a linked list to Debug() func debugList(l *list.List) { idx := 0 for e := l.Front(); e != nil; e = e.Next() { Debug(idx, fmt.Sprintf("%+v", e.Value)) idx++ } } // append log info to another file func Debug(o ...interface{}) { f, _ := os.OpenFile("debug.tmp", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) fmt.Fprintln(f, o...) f.Close() } func CaptureExitSignal(f func()) { cSignal := make(chan os.Signal, 1) signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM) go func() { for range cSignal { f() } }() } ================================================ FILE: vendor/github.com/chzyer/readline/utils_unix.go ================================================ // +build aix darwin dragonfly freebsd linux,!appengine netbsd openbsd os400 solaris package readline import ( "io" "os" "os/signal" "sync" "syscall" ) type winsize struct { Row uint16 Col uint16 Xpixel uint16 Ypixel uint16 } // SuspendMe use to send suspend signal to myself, when we in the raw mode. // For OSX it need to send to parent's pid // For Linux it need to send to myself func SuspendMe() { p, _ := os.FindProcess(os.Getppid()) p.Signal(syscall.SIGTSTP) p, _ = os.FindProcess(os.Getpid()) p.Signal(syscall.SIGTSTP) } // get width of the terminal func getWidth(stdoutFd int) int { cols, _, err := GetSize(stdoutFd) if err != nil { return -1 } return cols } func GetScreenWidth() int { w := getWidth(syscall.Stdout) if w < 0 { w = getWidth(syscall.Stderr) } return w } // ClearScreen clears the console screen func ClearScreen(w io.Writer) (int, error) { return w.Write([]byte("\033[H")) } func DefaultIsTerminal() bool { return IsTerminal(syscall.Stdin) && (IsTerminal(syscall.Stdout) || IsTerminal(syscall.Stderr)) } func GetStdin() int { return syscall.Stdin } // ----------------------------------------------------------------------------- var ( widthChange sync.Once widthChangeCallback func() ) func DefaultOnWidthChanged(f func()) { widthChangeCallback = f widthChange.Do(func() { ch := make(chan os.Signal, 1) signal.Notify(ch, syscall.SIGWINCH) go func() { for { _, ok := <-ch if !ok { break } widthChangeCallback() } }() }) } ================================================ FILE: vendor/github.com/chzyer/readline/utils_windows.go ================================================ // +build windows package readline import ( "io" "syscall" ) func SuspendMe() { } func GetStdin() int { return int(syscall.Stdin) } func init() { isWindows = true } // get width of the terminal func GetScreenWidth() int { info, _ := GetConsoleScreenBufferInfo() if info == nil { return -1 } return int(info.dwSize.x) } // ClearScreen clears the console screen func ClearScreen(_ io.Writer) error { return SetConsoleCursorPosition(&_COORD{0, 0}) } func DefaultIsTerminal() bool { return true } func DefaultOnWidthChanged(func()) { } ================================================ FILE: vendor/github.com/chzyer/readline/vim.go ================================================ package readline const ( VIM_NORMAL = iota VIM_INSERT VIM_VISUAL ) type opVim struct { cfg *Config op *Operation vimMode int } func newVimMode(op *Operation) *opVim { ov := &opVim{ cfg: op.cfg, op: op, } ov.SetVimMode(ov.cfg.VimMode) return ov } func (o *opVim) SetVimMode(on bool) { if o.cfg.VimMode && !on { // turn off o.ExitVimMode() } o.cfg.VimMode = on o.vimMode = VIM_INSERT } func (o *opVim) ExitVimMode() { o.vimMode = VIM_INSERT } func (o *opVim) IsEnableVimMode() bool { return o.cfg.VimMode } func (o *opVim) handleVimNormalMovement(r rune, readNext func() rune) (t rune, handled bool) { rb := o.op.buf handled = true switch r { case 'h': t = CharBackward case 'j': t = CharNext case 'k': t = CharPrev case 'l': t = CharForward case '0', '^': rb.MoveToLineStart() case '$': rb.MoveToLineEnd() case 'x': rb.Delete() if rb.IsCursorInEnd() { rb.MoveBackward() } case 'r': rb.Replace(readNext()) case 'd': next := readNext() switch next { case 'd': rb.Erase() case 'w': rb.DeleteWord() case 'h': rb.Backspace() case 'l': rb.Delete() } case 'p': rb.Yank() case 'b', 'B': rb.MoveToPrevWord() case 'w', 'W': rb.MoveToNextWord() case 'e', 'E': rb.MoveToEndWord() case 'f', 'F', 't', 'T': next := readNext() prevChar := r == 't' || r == 'T' reverse := r == 'F' || r == 'T' switch next { case CharEsc: default: rb.MoveTo(next, prevChar, reverse) } default: return r, false } return t, true } func (o *opVim) handleVimNormalEnterInsert(r rune, readNext func() rune) (t rune, handled bool) { rb := o.op.buf handled = true switch r { case 'i': case 'I': rb.MoveToLineStart() case 'a': rb.MoveForward() case 'A': rb.MoveToLineEnd() case 's': rb.Delete() case 'S': rb.Erase() case 'c': next := readNext() switch next { case 'c': rb.Erase() case 'w': rb.DeleteWord() case 'h': rb.Backspace() case 'l': rb.Delete() } default: return r, false } o.EnterVimInsertMode() return } func (o *opVim) HandleVimNormal(r rune, readNext func() rune) (t rune) { switch r { case CharEnter, CharInterrupt: o.ExitVimMode() return r } if r, handled := o.handleVimNormalMovement(r, readNext); handled { return r } if r, handled := o.handleVimNormalEnterInsert(r, readNext); handled { return r } // invalid operation o.op.t.Bell() return 0 } func (o *opVim) EnterVimInsertMode() { o.vimMode = VIM_INSERT } func (o *opVim) ExitVimInsertMode() { o.vimMode = VIM_NORMAL } func (o *opVim) HandleVim(r rune, readNext func() rune) rune { if o.vimMode == VIM_NORMAL { return o.HandleVimNormal(r, readNext) } if r == CharEsc { o.ExitVimInsertMode() return 0 } switch o.vimMode { case VIM_INSERT: return r case VIM_VISUAL: } return r } ================================================ FILE: vendor/github.com/chzyer/readline/windows_api.go ================================================ // +build windows package readline import ( "reflect" "syscall" "unsafe" ) var ( kernel = NewKernel() stdout = uintptr(syscall.Stdout) stdin = uintptr(syscall.Stdin) ) type Kernel struct { SetConsoleCursorPosition, SetConsoleTextAttribute, FillConsoleOutputCharacterW, FillConsoleOutputAttribute, ReadConsoleInputW, GetConsoleScreenBufferInfo, GetConsoleCursorInfo, GetStdHandle CallFunc } type short int16 type word uint16 type dword uint32 type wchar uint16 type _COORD struct { x short y short } func (c *_COORD) ptr() uintptr { return uintptr(*(*int32)(unsafe.Pointer(c))) } const ( EVENT_KEY = 0x0001 EVENT_MOUSE = 0x0002 EVENT_WINDOW_BUFFER_SIZE = 0x0004 EVENT_MENU = 0x0008 EVENT_FOCUS = 0x0010 ) type _KEY_EVENT_RECORD struct { bKeyDown int32 wRepeatCount word wVirtualKeyCode word wVirtualScanCode word unicodeChar wchar dwControlKeyState dword } // KEY_EVENT_RECORD KeyEvent; // MOUSE_EVENT_RECORD MouseEvent; // WINDOW_BUFFER_SIZE_RECORD WindowBufferSizeEvent; // MENU_EVENT_RECORD MenuEvent; // FOCUS_EVENT_RECORD FocusEvent; type _INPUT_RECORD struct { EventType word Padding uint16 Event [16]byte } type _CONSOLE_SCREEN_BUFFER_INFO struct { dwSize _COORD dwCursorPosition _COORD wAttributes word srWindow _SMALL_RECT dwMaximumWindowSize _COORD } type _SMALL_RECT struct { left short top short right short bottom short } type _CONSOLE_CURSOR_INFO struct { dwSize dword bVisible bool } type CallFunc func(u ...uintptr) error func NewKernel() *Kernel { k := &Kernel{} kernel32 := syscall.NewLazyDLL("kernel32.dll") v := reflect.ValueOf(k).Elem() t := v.Type() for i := 0; i < t.NumField(); i++ { name := t.Field(i).Name f := kernel32.NewProc(name) v.Field(i).Set(reflect.ValueOf(k.Wrap(f))) } return k } func (k *Kernel) Wrap(p *syscall.LazyProc) CallFunc { return func(args ...uintptr) error { var r0 uintptr var e1 syscall.Errno size := uintptr(len(args)) if len(args) <= 3 { buf := make([]uintptr, 3) copy(buf, args) r0, _, e1 = syscall.Syscall(p.Addr(), size, buf[0], buf[1], buf[2]) } else { buf := make([]uintptr, 6) copy(buf, args) r0, _, e1 = syscall.Syscall6(p.Addr(), size, buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], ) } if int(r0) == 0 { if e1 != 0 { return error(e1) } else { return syscall.EINVAL } } return nil } } func GetConsoleScreenBufferInfo() (*_CONSOLE_SCREEN_BUFFER_INFO, error) { t := new(_CONSOLE_SCREEN_BUFFER_INFO) err := kernel.GetConsoleScreenBufferInfo( stdout, uintptr(unsafe.Pointer(t)), ) return t, err } func GetConsoleCursorInfo() (*_CONSOLE_CURSOR_INFO, error) { t := new(_CONSOLE_CURSOR_INFO) err := kernel.GetConsoleCursorInfo(stdout, uintptr(unsafe.Pointer(t))) return t, err } func SetConsoleCursorPosition(c *_COORD) error { return kernel.SetConsoleCursorPosition(stdout, c.ptr()) } ================================================ FILE: vendor/github.com/clipperhouse/uax29/v2/LICENSE ================================================ MIT License Copyright (c) 2020 Matt Sherman Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: vendor/github.com/clipperhouse/uax29/v2/graphemes/README.md ================================================ An implementation of grapheme cluster boundaries from [Unicode text segmentation](https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries) (UAX 29), for Unicode 17. [![Documentation](https://pkg.go.dev/badge/github.com/clipperhouse/uax29/v2/graphemes.svg)](https://pkg.go.dev/github.com/clipperhouse/uax29/v2/graphemes) ![Tests](https://github.com/clipperhouse/uax29/actions/workflows/gotest.yml/badge.svg) ![Fuzz](https://github.com/clipperhouse/uax29/actions/workflows/gofuzz.yml/badge.svg) ## Quick start ``` go get github.com/clipperhouse/uax29/v2/graphemes ``` ```go import "github.com/clipperhouse/uax29/v2/graphemes" text := "Hello, 世界. Nice dog! 👍🐶" g := graphemes.FromString(text) for g.Next() { // Next() returns true until end of data fmt.Println(g.Value()) // Do something with the current grapheme } ``` _A grapheme is a “single visible character”, which might be a simple as a single letter, or a complex emoji that consists of several Unicode code points._ ## Conformance We use the Unicode [test suite](https://unicode.org/reports/tr41/tr41-36.html#Tests29). ![Tests](https://github.com/clipperhouse/uax29/actions/workflows/gotest.yml/badge.svg) ![Fuzz](https://github.com/clipperhouse/uax29/actions/workflows/gofuzz.yml/badge.svg) ## APIs ### If you have a `string` ```go text := "Hello, 世界. Nice dog! 👍🐶" g := graphemes.FromString(text) for g.Next() { // Next() returns true until end of data fmt.Println(g.Value()) // Do something with the current grapheme } ``` ### If you have an `io.Reader` `FromReader` embeds a [`bufio.Scanner`](https://pkg.go.dev/bufio#Scanner), so just use those methods. ```go r := getYourReader() // from a file or network maybe g := graphemes.FromReader(r) for g.Scan() { // Scan() returns true until error or EOF fmt.Println(g.Text()) // Do something with the current grapheme } if g.Err() != nil { // Check the error log.Fatal(g.Err()) } ``` ### If you have a `[]byte` ```go b := []byte("Hello, 世界. Nice dog! 👍🐶") g := graphemes.FromBytes(b) for g.Next() { // Next() returns true until end of data fmt.Println(g.Value()) // Do something with the current grapheme } ``` ### ANSI escape sequences By the UAX 29 specification, ANSI escape sequences are not grapheme clusters. To treat 7-bit ANSI escape sequences as a single cluster, set `AnsiEscapeSequences` to true. ```go text := "Hello, \x1b[31mworld\x1b[0m!" g := graphemes.FromString(text) g.AnsiEscapeSequences = true for g.Next() { fmt.Println(g.Value()) } ``` To also parse 8-bit C1 controls (non-UTF-8 bytes), set `AnsiEscapeSequences8Bit` to true. ```go g.AnsiEscapeSequences = true // 7-bit forms (ESC ...) g.AnsiEscapeSequences8Bit = true // 8-bit C1 forms (0x80-0x9F), not valid UTF-8 ``` For ESC-initiated (7-bit) control strings, only 7-bit terminators are recognized. For C1-initiated (8-bit) control strings, only C1 ST (`0x9C`) is recognized as ST. We implement [ECMA-48](https://ecma-international.org/publications-and-standards/standards/ecma-48/) control codes in both 7-bit and 8-bit representations. 8-bit control codes are not UTF-8 encoded and are not valid UTF-8, caveat emptor. ### Benchmarks ``` goos: darwin goarch: arm64 pkg: github.com/clipperhouse/uax29/graphemes/comparative cpu: Apple M2 BenchmarkGraphemesMixed/clipperhouse/uax29-8 142635 ns/op 245.12 MB/s 0 B/op 0 allocs/op BenchmarkGraphemesMixed/rivo/uniseg-8 2018284 ns/op 17.32 MB/s 0 B/op 0 allocs/op BenchmarkGraphemesASCII/clipperhouse/uax29-8 8846 ns/op 508.73 MB/s 0 B/op 0 allocs/op BenchmarkGraphemesASCII/rivo/uniseg-8 366760 ns/op 12.27 MB/s 0 B/op 0 allocs/op ``` ### Invalid inputs Invalid UTF-8 input is considered undefined behavior. We test to ensure that bad inputs will not cause pathological outcomes, such as a panic or infinite loop. Callers should expect “garbage-in, garbage-out”. Your pipeline should probably include a call to [`utf8.Valid()`](https://pkg.go.dev/unicode/utf8#Valid). ================================================ FILE: vendor/github.com/clipperhouse/uax29/v2/graphemes/ansi.go ================================================ package graphemes // ansiEscapeLength returns the byte length of a valid 7-bit ANSI escape // sequence at the start of data, or 0 if none. // // Recognized forms (ECMA-48 / ISO 6429): // - CSI: ESC [ then parameter bytes (0x30-0x3F), intermediate (0x20-0x2F), final (0x40-0x7E) // - OSC: ESC ] then payload until BEL (0x07), 7-bit ST (ESC \), CAN (0x18), or SUB (0x1A) // - DCS, SOS, PM, APC: ESC P/X/^/_ then payload until 7-bit ST (ESC \), CAN, or SUB // - Two-byte: ESC + Fe/Fs (0x40-0x7E excluding above), or Fp (0x30-0x3F), or nF (0x20-0x2F then final) func ansiEscapeLength[T ~string | ~[]byte](data T) int { n := len(data) if n < 2 || data[0] != esc { return 0 } b1 := data[1] switch b1 { case '[': // CSI body := csiBodyLength(data[2:]) if body == 0 { return 0 } return 2 + body case ']': // OSC - allows BEL or 7-bit ST terminator body := oscLength(data[2:]) if body < 0 { return 0 } return 2 + body case 'P', 'X', '^', '_': // DCS, SOS, PM, APC body := stSequenceLength(data[2:]) if body < 0 { return 0 } return 2 + body } if b1 >= 0x40 && b1 <= 0x7E { // Fe/Fs two-byte; [ ] P X ^ _ handled above return 2 } if b1 >= 0x30 && b1 <= 0x3F { // Fp (private) two-byte return 2 } if b1 >= 0x20 && b1 <= 0x2F { // nF: intermediates then one final (0x30-0x7E) i := 2 for i < n && data[i] >= 0x20 && data[i] <= 0x2F { i++ } if i < n && data[i] >= 0x30 && data[i] <= 0x7E { return i + 1 } return 0 } return 0 } // csiBodyLength returns the length of the CSI body (param/intermediate/final bytes). // data is the slice after "ESC [". // Per ECMA-48, the CSI body has the form: // // parameters (0x30–0x3F)*, intermediates (0x20–0x2F)*, final (0x40–0x7E) // // Once an intermediate byte is seen, subsequent parameter bytes are invalid. func csiBodyLength[T ~string | ~[]byte](data T) int { seenIntermediate := false for i := 0; i < len(data); i++ { b := data[i] if b >= 0x30 && b <= 0x3F { if seenIntermediate { return 0 } continue } if b >= 0x20 && b <= 0x2F { seenIntermediate = true continue } if b >= 0x40 && b <= 0x7E { return i + 1 } return 0 } return 0 } // oscLength returns the length of the OSC body. // data is the slice after "ESC ]". // // Returns: // - n >= 0: consumed body length (includes BEL/ST terminator when present) // - -1: not terminated in the provided data // // OSC accepts BEL (0x07) or 7-bit ST (ESC \) as terminators by widespread convention. // Per ECMA-48, CAN (0x18) and SUB (0x1A) cancel the control string; in that // case they are not part of the OSC sequence length. func oscLength[T ~string | ~[]byte](data T) int { for i := 0; i < len(data); i++ { b := data[i] if b == bel { return i + 1 } if b == can || b == sub { return i } if b == esc && i+1 < len(data) && data[i+1] == '\\' { return i + 2 } } return -1 } // stSequenceLength returns the length of a control-string body. // data is the slice after "ESC x". // // Returns: // - n >= 0: consumed body length (includes ST terminator when present) // - -1: not terminated in the provided data // // Used for DCS, SOS, PM, and APC, which per ECMA-48 terminate with ST. // ST here is the 7-bit form (ESC \). // CAN (0x18) and SUB (0x1A) cancel the control string; in that case they are // not part of the sequence length. func stSequenceLength[T ~string | ~[]byte](data T) int { for i := 0; i < len(data); i++ { if data[i] == can || data[i] == sub { return i } if data[i] == esc && i+1 < len(data) && data[i+1] == '\\' { return i + 2 } } return -1 } ================================================ FILE: vendor/github.com/clipperhouse/uax29/v2/graphemes/ansi8.go ================================================ package graphemes // ansiEscapeLength8Bit returns the byte length of a valid 8-bit C1 ANSI // sequence at the start of data, or 0 if none. // // Recognized forms (ECMA-48 / ISO 6429): // - C1 CSI (0x9B) body as parameter/intermediate/final bytes // - C1 OSC (0x9D) body terminated by BEL, C1 ST, CAN, or SUB // - C1 DCS/SOS/PM/APC (0x90/0x98/0x9E/0x9F) body terminated by C1 ST, CAN, or SUB // - Standalone C1 controls (0x80..0x9F not listed above): single byte func ansiEscapeLength8Bit[T ~string | ~[]byte](data T) int { if len(data) == 0 { return 0 } switch data[0] { case 0x9B: // C1 CSI body := csiBodyLength(data[1:]) if body == 0 { return 0 } return 1 + body case 0x9D: // C1 OSC body := oscLengthC1(data[1:]) if body < 0 { return 0 } return 1 + body case 0x90, 0x98, 0x9E, 0x9F: // C1 DCS, SOS, PM, APC body := stSequenceLengthC1(data[1:]) if body < 0 { return 0 } return 1 + body default: if data[0] >= 0x80 && data[0] <= 0x9F { return 1 } } return 0 } // oscLengthC1 returns the length of a C1 OSC body. // data is the slice after the C1 OSC initiator (0x9D). // // Returns: // - n >= 0: consumed body length (includes BEL/ST terminator when present) // - -1: not terminated in the provided data // // Terminators: BEL (0x07) or C1 ST (0x9C). // CAN (0x18) and SUB (0x1A) cancel the control string. func oscLengthC1[T ~string | ~[]byte](data T) int { for i := 0; i < len(data); i++ { b := data[i] if b == bel || b == st { return i + 1 } if b == can || b == sub { return i } } return -1 } // stSequenceLengthC1 parses DCS/SOS/PM/APC bodies that terminate with C1 ST // (0x9C), or are canceled by CAN/SUB. func stSequenceLengthC1[T ~string | ~[]byte](data T) int { for i := 0; i < len(data); i++ { b := data[i] if b == can || b == sub { return i } if b == st { return i + 1 } } return -1 } ================================================ FILE: vendor/github.com/clipperhouse/uax29/v2/graphemes/iterator.go ================================================ package graphemes import "unicode/utf8" // FromString returns an iterator for the grapheme clusters in the input string. // Iterate while Next() is true, and access the grapheme via Value(). func FromString(s string) *Iterator[string] { return &Iterator[string]{ split: splitFuncString, data: s, } } // FromBytes returns an iterator for the grapheme clusters in the input bytes. // Iterate while Next() is true, and access the grapheme via Value(). func FromBytes(b []byte) *Iterator[[]byte] { return &Iterator[[]byte]{ split: splitFuncBytes, data: b, } } // Iterator is a generic iterator for grapheme clusters in strings or byte slices, // with an ASCII hot path optimization. type Iterator[T ~string | ~[]byte] struct { split func(T, bool) (int, T, error) data T pos int start int // AnsiEscapeSequences treats 7-bit ANSI escape sequences (ECMA-48) as // single grapheme clusters when true. The default is false. // // 8-bit controls are not enabled by this option. See [AnsiEscapeSequences8Bit]. AnsiEscapeSequences bool // AnsiEscapeSequences8Bit treats 8-bit C1 ANSI escape sequences (ECMA-48) as single // grapheme clusters when true. The default is false. // // 8-bit control bytes are not UTF-8 encoded, i.e. not valid UTF-8. If you // choose this option, you are choosing to interpret non-UTF-8 data, caveat // emptor. AnsiEscapeSequences8Bit bool } var ( splitFuncString = splitFunc[string] splitFuncBytes = splitFunc[[]byte] ) const ( esc = 0x1B cr = 0x0D bel = 0x07 can = 0x18 sub = 0x1A st = 0x9C ) // Next advances the iterator to the next grapheme cluster. // Returns false when there are no more grapheme clusters. func (iter *Iterator[T]) Next() bool { if iter.pos >= len(iter.data) { return false } iter.start = iter.pos b := iter.data[iter.pos] if iter.AnsiEscapeSequences && b == esc { if a := ansiEscapeLength(iter.data[iter.pos:]); a > 0 { iter.pos += a return true } } if iter.AnsiEscapeSequences8Bit && b >= 0x80 && b <= 0x9F { if a := ansiEscapeLength8Bit(iter.data[iter.pos:]); a > 0 { iter.pos += a return true } } // ASCII hot path: any ASCII is one grapheme when next byte is ASCII or end. if b < utf8.RuneSelf && b != cr { if iter.pos+1 >= len(iter.data) || iter.data[iter.pos+1] < utf8.RuneSelf { iter.pos++ return true } } // Fall back to UAX29 grapheme parsing remaining := iter.data[iter.pos:] advance, _, err := iter.split(remaining, true) if err != nil { panic(err) } if advance <= 0 { panic("splitFunc returned a zero or negative advance") } iter.pos += advance if iter.pos > len(iter.data) { panic("splitFunc advanced beyond end of data") } return true } // Value returns the current grapheme cluster. func (iter *Iterator[T]) Value() T { return iter.data[iter.start:iter.pos] } // Start returns the byte position of the current grapheme in the original data. func (iter *Iterator[T]) Start() int { return iter.start } // End returns the byte position after the current grapheme in the original data. func (iter *Iterator[T]) End() int { return iter.pos } // Reset resets the iterator to the beginning of the data. func (iter *Iterator[T]) Reset() { iter.start = 0 iter.pos = 0 } // SetText sets the data for the iterator to operate on, and resets all state. func (iter *Iterator[T]) SetText(data T) { iter.data = data iter.start = 0 iter.pos = 0 } // First returns the first grapheme cluster without advancing the iterator. func (iter *Iterator[T]) First() T { if len(iter.data) == 0 { return iter.data } // Use a copy to leverage Next()'s ASCII optimization cp := *iter cp.pos = 0 cp.start = 0 cp.Next() return cp.Value() } ================================================ FILE: vendor/github.com/clipperhouse/uax29/v2/graphemes/reader.go ================================================ // Package graphemes implements Unicode grapheme cluster boundaries: https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries package graphemes import ( "bufio" "io" ) type Scanner struct { *bufio.Scanner } // FromReader returns a Scanner, to split graphemes per // https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries. // // It embeds a [bufio.Scanner], so you can use its methods. // // Iterate through graphemes by calling Scan() until false, then check Err(). func FromReader(r io.Reader) *Scanner { sc := bufio.NewScanner(r) sc.Split(SplitFunc) return &Scanner{ Scanner: sc, } } ================================================ FILE: vendor/github.com/clipperhouse/uax29/v2/graphemes/splitfunc.go ================================================ package graphemes import ( "bufio" ) // is determines if lookup intersects propert(ies) func (lookup property) is(properties property) bool { return (lookup & properties) != 0 } const _Ignore = _Extend // incbState tracks state for GB9c rule (Indic conjunct clusters) // Pattern: Consonant (Extend|Linker)* Linker (Extend|Linker)* × Consonant type incbState int const ( incbNone incbState = iota // initial/reset incbConsonant // seen Consonant, awaiting Linker incbLinker // seen Consonant and Linker (conjunct ready) ) // SplitFunc is a bufio.SplitFunc implementation of Unicode grapheme cluster segmentation, for use with bufio.Scanner. // // See https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries. var SplitFunc bufio.SplitFunc = splitFunc[[]byte] func splitFunc[T ~string | ~[]byte](data T, atEOF bool) (advance int, token T, err error) { var empty T if len(data) == 0 { return 0, empty, nil } // These vars are stateful across loop iterations var pos int var lastExIgnore property = 0 // "last excluding ignored categories" var lastLastExIgnore property = 0 // "last one before that" var regionalIndicatorCount int // GB9c state: tracking Indic conjunct clusters var incb incbState // Rules are usually of the form Cat1 × Cat2; "current" refers to the first property // to the right of the ×, from which we look back or forward current, w := lookup(data[pos:]) if w == 0 { if !atEOF { // Rune extends past current data, request more return 0, empty, nil } pos = len(data) return pos, data[:pos], nil } // https://unicode.org/reports/tr29/#GB1 // Start of text always advances pos += w for { eot := pos == len(data) // "end of text" if eot { if !atEOF { // Token extends past current data, request more return 0, empty, nil } // https://unicode.org/reports/tr29/#GB2 break } /* We've switched the evaluation order of GB1↓ and GB2↑. It's ok: because we've checked for len(data) at the top of this function, sot and eot are mutually exclusive, order doesn't matter. */ // Rules are usually of the form Cat1 × Cat2; "current" refers to the first property // to the right of the ×, from which we look back or forward // Remember previous properties to avoid lookups/lookbacks last := current if !last.is(_Ignore) { lastLastExIgnore = lastExIgnore lastExIgnore = last } // Update GB9c state based on what we just advanced past if last.is(_InCBConsonant | _InCBLinker | _InCBExtend) { switch { case last.is(_InCBConsonant): if incb != incbLinker { incb = incbConsonant } case last.is(_InCBLinker): if incb >= incbConsonant { incb = incbLinker } // case last.is(_InCBExtend): stay in current state } } else { incb = incbNone } current, w = lookup(data[pos:]) if w == 0 { if atEOF { // Just return the bytes, we can't do anything with them pos = len(data) break } // Rune extends past current data, request more return 0, empty, nil } // Optimization: no rule can possibly apply if current|last == 0 { // i.e. both are zero break } // https://unicode.org/reports/tr29/#GB3 if current.is(_LF) && last.is(_CR) { pos += w continue } // https://unicode.org/reports/tr29/#GB4 // https://unicode.org/reports/tr29/#GB5 if (current | last).is(_Control | _CR | _LF) { break } // https://unicode.org/reports/tr29/#GB6 if current.is(_L|_V|_LV|_LVT) && last.is(_L) { pos += w continue } // https://unicode.org/reports/tr29/#GB7 if current.is(_V|_T) && last.is(_LV|_V) { pos += w continue } // https://unicode.org/reports/tr29/#GB8 if current.is(_T) && last.is(_LVT|_T) { pos += w continue } // https://unicode.org/reports/tr29/#GB9 if current.is(_Extend | _ZWJ) { pos += w continue } // https://unicode.org/reports/tr29/#GB9a if current.is(_SpacingMark) { pos += w continue } // https://unicode.org/reports/tr29/#GB9b if last.is(_Prepend) { pos += w continue } // https://unicode.org/reports/tr29/#GB9c // Do not break within certain combinations with Indic_Conjunct_Break (InCB)=Linker. if incb == incbLinker && current.is(_InCBConsonant) { // After matching the pattern, reset state to start tracking a new pattern // The current Consonant becomes the start of the new pattern incb = incbConsonant pos += w continue } // https://unicode.org/reports/tr29/#GB11 if current.is(_ExtendedPictographic) && last.is(_ZWJ) && lastLastExIgnore.is(_ExtendedPictographic) { pos += w continue } // https://unicode.org/reports/tr29/#GB12 // https://unicode.org/reports/tr29/#GB13 if (current & last).is(_RegionalIndicator) { regionalIndicatorCount++ odd := regionalIndicatorCount%2 == 1 if odd { pos += w continue } } // If we fall through all the above rules, it's a grapheme cluster break break } // Return token return pos, data[:pos], nil } ================================================ FILE: vendor/github.com/clipperhouse/uax29/v2/graphemes/trie.go ================================================ package graphemes // generated by github.com/clipperhouse/uax29/v2 // from https://www.unicode.org/Public/17.0.0/ucd/auxiliary/GraphemeBreakProperty.txt type property uint32 const ( _CR property = 1 << iota _Control _Extend _ExtendedPictographic _InCBConsonant _InCBExtend _InCBLinker _L _LF _LV _LVT _Prepend _RegionalIndicator _SpacingMark _T _V _ZWJ ) // lookup returns the trie value for the first UTF-8 encoding in s and // the width in bytes of this encoding. The size will be 0 if s does not // hold enough bytes to complete the encoding. len(s) must be greater than 0. func lookup[T ~string | ~[]byte](s T) (v property, sz int) { c0 := s[0] switch { case c0 < 0x80: // is ASCII return graphemesValues[c0], 1 case c0 < 0xC2: return 0, 1 // Illegal UTF-8: not a starter, not ASCII. case c0 < 0xE0: // 2-byte UTF-8 if len(s) < 2 { return 0, 0 } i := graphemesIndex[c0] c1 := s[1] if c1 < 0x80 || 0xC0 <= c1 { return 0, 1 // Illegal UTF-8: not a continuation byte. } return lookupValue(uint32(i), c1), 2 case c0 < 0xF0: // 3-byte UTF-8 if len(s) < 3 { return 0, 0 } i := graphemesIndex[c0] c1 := s[1] if c1 < 0x80 || 0xC0 <= c1 { return 0, 1 // Illegal UTF-8: not a continuation byte. } o := uint32(i)<<6 + uint32(c1) i = graphemesIndex[o] c2 := s[2] if c2 < 0x80 || 0xC0 <= c2 { return 0, 2 // Illegal UTF-8: not a continuation byte. } return lookupValue(uint32(i), c2), 3 case c0 < 0xF8: // 4-byte UTF-8 if len(s) < 4 { return 0, 0 } i := graphemesIndex[c0] c1 := s[1] if c1 < 0x80 || 0xC0 <= c1 { return 0, 1 // Illegal UTF-8: not a continuation byte. } o := uint32(i)<<6 + uint32(c1) i = graphemesIndex[o] c2 := s[2] if c2 < 0x80 || 0xC0 <= c2 { return 0, 2 // Illegal UTF-8: not a continuation byte. } o = uint32(i)<<6 + uint32(c2) i = graphemesIndex[o] c3 := s[3] if c3 < 0x80 || 0xC0 <= c3 { return 0, 3 // Illegal UTF-8: not a continuation byte. } return lookupValue(uint32(i), c3), 4 } // Illegal rune return 0, 1 } // graphemesTrie. Total size: 61760 bytes (60.31 KiB). Checksum: af733ba94cd94ba6. // type graphemesTrie struct { } // func newGraphemesTrie(i int) *graphemesTrie { // return &graphemesTrie{} // } // lookupValue determines the type of block n and looks up the value for b. func lookupValue(n uint32, b byte) property { switch { default: return property(graphemesValues[n<<6+uint32(b)]) } } // graphemesValues: 235 blocks, 15040 entries, 60160 bytes // The third block is the zero block. var graphemesValues = [15040]property{ // Block 0x0, offset 0x0 0x00: 0x0002, 0x01: 0x0002, 0x02: 0x0002, 0x03: 0x0002, 0x04: 0x0002, 0x05: 0x0002, 0x06: 0x0002, 0x07: 0x0002, 0x08: 0x0002, 0x09: 0x0002, 0x0a: 0x0100, 0x0b: 0x0002, 0x0c: 0x0002, 0x0d: 0x0001, 0x0e: 0x0002, 0x0f: 0x0002, 0x10: 0x0002, 0x11: 0x0002, 0x12: 0x0002, 0x13: 0x0002, 0x14: 0x0002, 0x15: 0x0002, 0x16: 0x0002, 0x17: 0x0002, 0x18: 0x0002, 0x19: 0x0002, 0x1a: 0x0002, 0x1b: 0x0002, 0x1c: 0x0002, 0x1d: 0x0002, 0x1e: 0x0002, 0x1f: 0x0002, // Block 0x1, offset 0x40 0x7f: 0x0002, // Block 0x2, offset 0x80 // Block 0x3, offset 0xc0 0xc0: 0x0002, 0xc1: 0x0002, 0xc2: 0x0002, 0xc3: 0x0002, 0xc4: 0x0002, 0xc5: 0x0002, 0xc6: 0x0002, 0xc7: 0x0002, 0xc8: 0x0002, 0xc9: 0x0002, 0xca: 0x0002, 0xcb: 0x0002, 0xcc: 0x0002, 0xcd: 0x0002, 0xce: 0x0002, 0xcf: 0x0002, 0xd0: 0x0002, 0xd1: 0x0002, 0xd2: 0x0002, 0xd3: 0x0002, 0xd4: 0x0002, 0xd5: 0x0002, 0xd6: 0x0002, 0xd7: 0x0002, 0xd8: 0x0002, 0xd9: 0x0002, 0xda: 0x0002, 0xdb: 0x0002, 0xdc: 0x0002, 0xdd: 0x0002, 0xde: 0x0002, 0xdf: 0x0002, 0xe9: 0x0008, 0xed: 0x0002, 0xee: 0x0008, // Block 0x4, offset 0x100 0x100: 0x0024, 0x101: 0x0024, 0x102: 0x0024, 0x103: 0x0024, 0x104: 0x0024, 0x105: 0x0024, 0x106: 0x0024, 0x107: 0x0024, 0x108: 0x0024, 0x109: 0x0024, 0x10a: 0x0024, 0x10b: 0x0024, 0x10c: 0x0024, 0x10d: 0x0024, 0x10e: 0x0024, 0x10f: 0x0024, 0x110: 0x0024, 0x111: 0x0024, 0x112: 0x0024, 0x113: 0x0024, 0x114: 0x0024, 0x115: 0x0024, 0x116: 0x0024, 0x117: 0x0024, 0x118: 0x0024, 0x119: 0x0024, 0x11a: 0x0024, 0x11b: 0x0024, 0x11c: 0x0024, 0x11d: 0x0024, 0x11e: 0x0024, 0x11f: 0x0024, 0x120: 0x0024, 0x121: 0x0024, 0x122: 0x0024, 0x123: 0x0024, 0x124: 0x0024, 0x125: 0x0024, 0x126: 0x0024, 0x127: 0x0024, 0x128: 0x0024, 0x129: 0x0024, 0x12a: 0x0024, 0x12b: 0x0024, 0x12c: 0x0024, 0x12d: 0x0024, 0x12e: 0x0024, 0x12f: 0x0024, 0x130: 0x0024, 0x131: 0x0024, 0x132: 0x0024, 0x133: 0x0024, 0x134: 0x0024, 0x135: 0x0024, 0x136: 0x0024, 0x137: 0x0024, 0x138: 0x0024, 0x139: 0x0024, 0x13a: 0x0024, 0x13b: 0x0024, 0x13c: 0x0024, 0x13d: 0x0024, 0x13e: 0x0024, 0x13f: 0x0024, // Block 0x5, offset 0x140 0x140: 0x0024, 0x141: 0x0024, 0x142: 0x0024, 0x143: 0x0024, 0x144: 0x0024, 0x145: 0x0024, 0x146: 0x0024, 0x147: 0x0024, 0x148: 0x0024, 0x149: 0x0024, 0x14a: 0x0024, 0x14b: 0x0024, 0x14c: 0x0024, 0x14d: 0x0024, 0x14e: 0x0024, 0x14f: 0x0024, 0x150: 0x0024, 0x151: 0x0024, 0x152: 0x0024, 0x153: 0x0024, 0x154: 0x0024, 0x155: 0x0024, 0x156: 0x0024, 0x157: 0x0024, 0x158: 0x0024, 0x159: 0x0024, 0x15a: 0x0024, 0x15b: 0x0024, 0x15c: 0x0024, 0x15d: 0x0024, 0x15e: 0x0024, 0x15f: 0x0024, 0x160: 0x0024, 0x161: 0x0024, 0x162: 0x0024, 0x163: 0x0024, 0x164: 0x0024, 0x165: 0x0024, 0x166: 0x0024, 0x167: 0x0024, 0x168: 0x0024, 0x169: 0x0024, 0x16a: 0x0024, 0x16b: 0x0024, 0x16c: 0x0024, 0x16d: 0x0024, 0x16e: 0x0024, 0x16f: 0x0024, // Block 0x6, offset 0x180 0x183: 0x0024, 0x184: 0x0024, 0x185: 0x0024, 0x186: 0x0024, 0x187: 0x0024, 0x188: 0x0024, 0x189: 0x0024, // Block 0x7, offset 0x1c0 0x1d1: 0x0024, 0x1d2: 0x0024, 0x1d3: 0x0024, 0x1d4: 0x0024, 0x1d5: 0x0024, 0x1d6: 0x0024, 0x1d7: 0x0024, 0x1d8: 0x0024, 0x1d9: 0x0024, 0x1da: 0x0024, 0x1db: 0x0024, 0x1dc: 0x0024, 0x1dd: 0x0024, 0x1de: 0x0024, 0x1df: 0x0024, 0x1e0: 0x0024, 0x1e1: 0x0024, 0x1e2: 0x0024, 0x1e3: 0x0024, 0x1e4: 0x0024, 0x1e5: 0x0024, 0x1e6: 0x0024, 0x1e7: 0x0024, 0x1e8: 0x0024, 0x1e9: 0x0024, 0x1ea: 0x0024, 0x1eb: 0x0024, 0x1ec: 0x0024, 0x1ed: 0x0024, 0x1ee: 0x0024, 0x1ef: 0x0024, 0x1f0: 0x0024, 0x1f1: 0x0024, 0x1f2: 0x0024, 0x1f3: 0x0024, 0x1f4: 0x0024, 0x1f5: 0x0024, 0x1f6: 0x0024, 0x1f7: 0x0024, 0x1f8: 0x0024, 0x1f9: 0x0024, 0x1fa: 0x0024, 0x1fb: 0x0024, 0x1fc: 0x0024, 0x1fd: 0x0024, 0x1ff: 0x0024, // Block 0x8, offset 0x200 0x201: 0x0024, 0x202: 0x0024, 0x204: 0x0024, 0x205: 0x0024, 0x207: 0x0024, // Block 0x9, offset 0x240 0x240: 0x0800, 0x241: 0x0800, 0x242: 0x0800, 0x243: 0x0800, 0x244: 0x0800, 0x245: 0x0800, 0x250: 0x0024, 0x251: 0x0024, 0x252: 0x0024, 0x253: 0x0024, 0x254: 0x0024, 0x255: 0x0024, 0x256: 0x0024, 0x257: 0x0024, 0x258: 0x0024, 0x259: 0x0024, 0x25a: 0x0024, 0x25c: 0x0002, // Block 0xa, offset 0x280 0x28b: 0x0024, 0x28c: 0x0024, 0x28d: 0x0024, 0x28e: 0x0024, 0x28f: 0x0024, 0x290: 0x0024, 0x291: 0x0024, 0x292: 0x0024, 0x293: 0x0024, 0x294: 0x0024, 0x295: 0x0024, 0x296: 0x0024, 0x297: 0x0024, 0x298: 0x0024, 0x299: 0x0024, 0x29a: 0x0024, 0x29b: 0x0024, 0x29c: 0x0024, 0x29d: 0x0024, 0x29e: 0x0024, 0x29f: 0x0024, 0x2b0: 0x0024, // Block 0xb, offset 0x2c0 0x2d6: 0x0024, 0x2d7: 0x0024, 0x2d8: 0x0024, 0x2d9: 0x0024, 0x2da: 0x0024, 0x2db: 0x0024, 0x2dc: 0x0024, 0x2dd: 0x0800, 0x2df: 0x0024, 0x2e0: 0x0024, 0x2e1: 0x0024, 0x2e2: 0x0024, 0x2e3: 0x0024, 0x2e4: 0x0024, 0x2e7: 0x0024, 0x2e8: 0x0024, 0x2ea: 0x0024, 0x2eb: 0x0024, 0x2ec: 0x0024, 0x2ed: 0x0024, // Block 0xc, offset 0x300 0x30f: 0x0800, 0x311: 0x0024, 0x330: 0x0024, 0x331: 0x0024, 0x332: 0x0024, 0x333: 0x0024, 0x334: 0x0024, 0x335: 0x0024, 0x336: 0x0024, 0x337: 0x0024, 0x338: 0x0024, 0x339: 0x0024, 0x33a: 0x0024, 0x33b: 0x0024, 0x33c: 0x0024, 0x33d: 0x0024, 0x33e: 0x0024, 0x33f: 0x0024, // Block 0xd, offset 0x340 0x340: 0x0024, 0x341: 0x0024, 0x342: 0x0024, 0x343: 0x0024, 0x344: 0x0024, 0x345: 0x0024, 0x346: 0x0024, 0x347: 0x0024, 0x348: 0x0024, 0x349: 0x0024, 0x34a: 0x0024, // Block 0xe, offset 0x380 0x3a6: 0x0024, 0x3a7: 0x0024, 0x3a8: 0x0024, 0x3a9: 0x0024, 0x3aa: 0x0024, 0x3ab: 0x0024, 0x3ac: 0x0024, 0x3ad: 0x0024, 0x3ae: 0x0024, 0x3af: 0x0024, 0x3b0: 0x0024, // Block 0xf, offset 0x3c0 0x3eb: 0x0024, 0x3ec: 0x0024, 0x3ed: 0x0024, 0x3ee: 0x0024, 0x3ef: 0x0024, 0x3f0: 0x0024, 0x3f1: 0x0024, 0x3f2: 0x0024, 0x3f3: 0x0024, 0x3fd: 0x0024, // Block 0x10, offset 0x400 0x416: 0x0024, 0x417: 0x0024, 0x418: 0x0024, 0x419: 0x0024, 0x41b: 0x0024, 0x41c: 0x0024, 0x41d: 0x0024, 0x41e: 0x0024, 0x41f: 0x0024, 0x420: 0x0024, 0x421: 0x0024, 0x422: 0x0024, 0x423: 0x0024, 0x425: 0x0024, 0x426: 0x0024, 0x427: 0x0024, 0x429: 0x0024, 0x42a: 0x0024, 0x42b: 0x0024, 0x42c: 0x0024, 0x42d: 0x0024, // Block 0x11, offset 0x440 0x459: 0x0024, 0x45a: 0x0024, 0x45b: 0x0024, // Block 0x12, offset 0x480 0x490: 0x0800, 0x491: 0x0800, 0x497: 0x0024, 0x498: 0x0024, 0x499: 0x0024, 0x49a: 0x0024, 0x49b: 0x0024, 0x49c: 0x0024, 0x49d: 0x0024, 0x49e: 0x0024, 0x49f: 0x0024, // Block 0x13, offset 0x4c0 0x4ca: 0x0024, 0x4cb: 0x0024, 0x4cc: 0x0024, 0x4cd: 0x0024, 0x4ce: 0x0024, 0x4cf: 0x0024, 0x4d0: 0x0024, 0x4d1: 0x0024, 0x4d2: 0x0024, 0x4d3: 0x0024, 0x4d4: 0x0024, 0x4d5: 0x0024, 0x4d6: 0x0024, 0x4d7: 0x0024, 0x4d8: 0x0024, 0x4d9: 0x0024, 0x4da: 0x0024, 0x4db: 0x0024, 0x4dc: 0x0024, 0x4dd: 0x0024, 0x4de: 0x0024, 0x4df: 0x0024, 0x4e0: 0x0024, 0x4e1: 0x0024, 0x4e2: 0x0800, 0x4e3: 0x0024, 0x4e4: 0x0024, 0x4e5: 0x0024, 0x4e6: 0x0024, 0x4e7: 0x0024, 0x4e8: 0x0024, 0x4e9: 0x0024, 0x4ea: 0x0024, 0x4eb: 0x0024, 0x4ec: 0x0024, 0x4ed: 0x0024, 0x4ee: 0x0024, 0x4ef: 0x0024, 0x4f0: 0x0024, 0x4f1: 0x0024, 0x4f2: 0x0024, 0x4f3: 0x0024, 0x4f4: 0x0024, 0x4f5: 0x0024, 0x4f6: 0x0024, 0x4f7: 0x0024, 0x4f8: 0x0024, 0x4f9: 0x0024, 0x4fa: 0x0024, 0x4fb: 0x0024, 0x4fc: 0x0024, 0x4fd: 0x0024, 0x4fe: 0x0024, 0x4ff: 0x0024, // Block 0x14, offset 0x500 0x500: 0x0024, 0x501: 0x0024, 0x502: 0x0024, 0x503: 0x2000, 0x515: 0x0010, 0x516: 0x0010, 0x517: 0x0010, 0x518: 0x0010, 0x519: 0x0010, 0x51a: 0x0010, 0x51b: 0x0010, 0x51c: 0x0010, 0x51d: 0x0010, 0x51e: 0x0010, 0x51f: 0x0010, 0x520: 0x0010, 0x521: 0x0010, 0x522: 0x0010, 0x523: 0x0010, 0x524: 0x0010, 0x525: 0x0010, 0x526: 0x0010, 0x527: 0x0010, 0x528: 0x0010, 0x529: 0x0010, 0x52a: 0x0010, 0x52b: 0x0010, 0x52c: 0x0010, 0x52d: 0x0010, 0x52e: 0x0010, 0x52f: 0x0010, 0x530: 0x0010, 0x531: 0x0010, 0x532: 0x0010, 0x533: 0x0010, 0x534: 0x0010, 0x535: 0x0010, 0x536: 0x0010, 0x537: 0x0010, 0x538: 0x0010, 0x539: 0x0010, 0x53a: 0x0024, 0x53b: 0x2000, 0x53c: 0x0024, 0x53e: 0x2000, 0x53f: 0x2000, // Block 0x15, offset 0x540 0x540: 0x2000, 0x541: 0x0024, 0x542: 0x0024, 0x543: 0x0024, 0x544: 0x0024, 0x545: 0x0024, 0x546: 0x0024, 0x547: 0x0024, 0x548: 0x0024, 0x549: 0x2000, 0x54a: 0x2000, 0x54b: 0x2000, 0x54c: 0x2000, 0x54d: 0x0044, 0x54e: 0x2000, 0x54f: 0x2000, 0x551: 0x0024, 0x552: 0x0024, 0x553: 0x0024, 0x554: 0x0024, 0x555: 0x0024, 0x556: 0x0024, 0x557: 0x0024, 0x558: 0x0010, 0x559: 0x0010, 0x55a: 0x0010, 0x55b: 0x0010, 0x55c: 0x0010, 0x55d: 0x0010, 0x55e: 0x0010, 0x55f: 0x0010, 0x562: 0x0024, 0x563: 0x0024, 0x578: 0x0010, 0x579: 0x0010, 0x57a: 0x0010, 0x57b: 0x0010, 0x57c: 0x0010, 0x57d: 0x0010, 0x57e: 0x0010, 0x57f: 0x0010, // Block 0x16, offset 0x580 0x581: 0x0024, 0x582: 0x2000, 0x583: 0x2000, 0x595: 0x0010, 0x596: 0x0010, 0x597: 0x0010, 0x598: 0x0010, 0x599: 0x0010, 0x59a: 0x0010, 0x59b: 0x0010, 0x59c: 0x0010, 0x59d: 0x0010, 0x59e: 0x0010, 0x59f: 0x0010, 0x5a0: 0x0010, 0x5a1: 0x0010, 0x5a2: 0x0010, 0x5a3: 0x0010, 0x5a4: 0x0010, 0x5a5: 0x0010, 0x5a6: 0x0010, 0x5a7: 0x0010, 0x5a8: 0x0010, 0x5aa: 0x0010, 0x5ab: 0x0010, 0x5ac: 0x0010, 0x5ad: 0x0010, 0x5ae: 0x0010, 0x5af: 0x0010, 0x5b0: 0x0010, 0x5b2: 0x0010, 0x5b6: 0x0010, 0x5b7: 0x0010, 0x5b8: 0x0010, 0x5b9: 0x0010, 0x5bc: 0x0024, 0x5be: 0x0024, 0x5bf: 0x2000, // Block 0x17, offset 0x5c0 0x5c0: 0x2000, 0x5c1: 0x0024, 0x5c2: 0x0024, 0x5c3: 0x0024, 0x5c4: 0x0024, 0x5c7: 0x2000, 0x5c8: 0x2000, 0x5cb: 0x2000, 0x5cc: 0x2000, 0x5cd: 0x0044, 0x5d7: 0x0024, 0x5dc: 0x0010, 0x5dd: 0x0010, 0x5df: 0x0010, 0x5e2: 0x0024, 0x5e3: 0x0024, 0x5f0: 0x0010, 0x5f1: 0x0010, 0x5fe: 0x0024, // Block 0x18, offset 0x600 0x601: 0x0024, 0x602: 0x0024, 0x603: 0x2000, 0x63c: 0x0024, 0x63e: 0x2000, 0x63f: 0x2000, // Block 0x19, offset 0x640 0x640: 0x2000, 0x641: 0x0024, 0x642: 0x0024, 0x647: 0x0024, 0x648: 0x0024, 0x64b: 0x0024, 0x64c: 0x0024, 0x64d: 0x0024, 0x651: 0x0024, 0x670: 0x0024, 0x671: 0x0024, 0x675: 0x0024, // Block 0x1a, offset 0x680 0x681: 0x0024, 0x682: 0x0024, 0x683: 0x2000, 0x695: 0x0010, 0x696: 0x0010, 0x697: 0x0010, 0x698: 0x0010, 0x699: 0x0010, 0x69a: 0x0010, 0x69b: 0x0010, 0x69c: 0x0010, 0x69d: 0x0010, 0x69e: 0x0010, 0x69f: 0x0010, 0x6a0: 0x0010, 0x6a1: 0x0010, 0x6a2: 0x0010, 0x6a3: 0x0010, 0x6a4: 0x0010, 0x6a5: 0x0010, 0x6a6: 0x0010, 0x6a7: 0x0010, 0x6a8: 0x0010, 0x6aa: 0x0010, 0x6ab: 0x0010, 0x6ac: 0x0010, 0x6ad: 0x0010, 0x6ae: 0x0010, 0x6af: 0x0010, 0x6b0: 0x0010, 0x6b2: 0x0010, 0x6b3: 0x0010, 0x6b5: 0x0010, 0x6b6: 0x0010, 0x6b7: 0x0010, 0x6b8: 0x0010, 0x6b9: 0x0010, 0x6bc: 0x0024, 0x6be: 0x2000, 0x6bf: 0x2000, // Block 0x1b, offset 0x6c0 0x6c0: 0x2000, 0x6c1: 0x0024, 0x6c2: 0x0024, 0x6c3: 0x0024, 0x6c4: 0x0024, 0x6c5: 0x0024, 0x6c7: 0x0024, 0x6c8: 0x0024, 0x6c9: 0x2000, 0x6cb: 0x2000, 0x6cc: 0x2000, 0x6cd: 0x0044, 0x6e2: 0x0024, 0x6e3: 0x0024, 0x6f9: 0x0010, 0x6fa: 0x0024, 0x6fb: 0x0024, 0x6fc: 0x0024, 0x6fd: 0x0024, 0x6fe: 0x0024, 0x6ff: 0x0024, // Block 0x1c, offset 0x700 0x701: 0x0024, 0x702: 0x2000, 0x703: 0x2000, 0x715: 0x0010, 0x716: 0x0010, 0x717: 0x0010, 0x718: 0x0010, 0x719: 0x0010, 0x71a: 0x0010, 0x71b: 0x0010, 0x71c: 0x0010, 0x71d: 0x0010, 0x71e: 0x0010, 0x71f: 0x0010, 0x720: 0x0010, 0x721: 0x0010, 0x722: 0x0010, 0x723: 0x0010, 0x724: 0x0010, 0x725: 0x0010, 0x726: 0x0010, 0x727: 0x0010, 0x728: 0x0010, 0x72a: 0x0010, 0x72b: 0x0010, 0x72c: 0x0010, 0x72d: 0x0010, 0x72e: 0x0010, 0x72f: 0x0010, 0x730: 0x0010, 0x732: 0x0010, 0x733: 0x0010, 0x735: 0x0010, 0x736: 0x0010, 0x737: 0x0010, 0x738: 0x0010, 0x739: 0x0010, 0x73c: 0x0024, 0x73e: 0x0024, 0x73f: 0x0024, // Block 0x1d, offset 0x740 0x740: 0x2000, 0x741: 0x0024, 0x742: 0x0024, 0x743: 0x0024, 0x744: 0x0024, 0x747: 0x2000, 0x748: 0x2000, 0x74b: 0x2000, 0x74c: 0x2000, 0x74d: 0x0044, 0x755: 0x0024, 0x756: 0x0024, 0x757: 0x0024, 0x75c: 0x0010, 0x75d: 0x0010, 0x75f: 0x0010, 0x762: 0x0024, 0x763: 0x0024, 0x771: 0x0010, // Block 0x1e, offset 0x780 0x782: 0x0024, 0x7be: 0x0024, 0x7bf: 0x2000, // Block 0x1f, offset 0x7c0 0x7c0: 0x0024, 0x7c1: 0x2000, 0x7c2: 0x2000, 0x7c6: 0x2000, 0x7c7: 0x2000, 0x7c8: 0x2000, 0x7ca: 0x2000, 0x7cb: 0x2000, 0x7cc: 0x2000, 0x7cd: 0x0024, 0x7d7: 0x0024, // Block 0x20, offset 0x800 0x800: 0x0024, 0x801: 0x2000, 0x802: 0x2000, 0x803: 0x2000, 0x804: 0x0024, 0x815: 0x0010, 0x816: 0x0010, 0x817: 0x0010, 0x818: 0x0010, 0x819: 0x0010, 0x81a: 0x0010, 0x81b: 0x0010, 0x81c: 0x0010, 0x81d: 0x0010, 0x81e: 0x0010, 0x81f: 0x0010, 0x820: 0x0010, 0x821: 0x0010, 0x822: 0x0010, 0x823: 0x0010, 0x824: 0x0010, 0x825: 0x0010, 0x826: 0x0010, 0x827: 0x0010, 0x828: 0x0010, 0x82a: 0x0010, 0x82b: 0x0010, 0x82c: 0x0010, 0x82d: 0x0010, 0x82e: 0x0010, 0x82f: 0x0010, 0x830: 0x0010, 0x831: 0x0010, 0x832: 0x0010, 0x833: 0x0010, 0x834: 0x0010, 0x835: 0x0010, 0x836: 0x0010, 0x837: 0x0010, 0x838: 0x0010, 0x839: 0x0010, 0x83c: 0x0024, 0x83e: 0x0024, 0x83f: 0x0024, // Block 0x21, offset 0x840 0x840: 0x0024, 0x841: 0x2000, 0x842: 0x2000, 0x843: 0x2000, 0x844: 0x2000, 0x846: 0x0024, 0x847: 0x0024, 0x848: 0x0024, 0x84a: 0x0024, 0x84b: 0x0024, 0x84c: 0x0024, 0x84d: 0x0044, 0x855: 0x0024, 0x856: 0x0024, 0x858: 0x0010, 0x859: 0x0010, 0x85a: 0x0010, 0x862: 0x0024, 0x863: 0x0024, // Block 0x22, offset 0x880 0x881: 0x0024, 0x882: 0x2000, 0x883: 0x2000, 0x8bc: 0x0024, 0x8be: 0x2000, 0x8bf: 0x0024, // Block 0x23, offset 0x8c0 0x8c0: 0x0024, 0x8c1: 0x2000, 0x8c2: 0x0024, 0x8c3: 0x2000, 0x8c4: 0x2000, 0x8c6: 0x0024, 0x8c7: 0x0024, 0x8c8: 0x0024, 0x8ca: 0x0024, 0x8cb: 0x0024, 0x8cc: 0x0024, 0x8cd: 0x0024, 0x8d5: 0x0024, 0x8d6: 0x0024, 0x8e2: 0x0024, 0x8e3: 0x0024, 0x8f3: 0x2000, // Block 0x24, offset 0x900 0x900: 0x0024, 0x901: 0x0024, 0x902: 0x2000, 0x903: 0x2000, 0x915: 0x0010, 0x916: 0x0010, 0x917: 0x0010, 0x918: 0x0010, 0x919: 0x0010, 0x91a: 0x0010, 0x91b: 0x0010, 0x91c: 0x0010, 0x91d: 0x0010, 0x91e: 0x0010, 0x91f: 0x0010, 0x920: 0x0010, 0x921: 0x0010, 0x922: 0x0010, 0x923: 0x0010, 0x924: 0x0010, 0x925: 0x0010, 0x926: 0x0010, 0x927: 0x0010, 0x928: 0x0010, 0x929: 0x0010, 0x92a: 0x0010, 0x92b: 0x0010, 0x92c: 0x0010, 0x92d: 0x0010, 0x92e: 0x0010, 0x92f: 0x0010, 0x930: 0x0010, 0x931: 0x0010, 0x932: 0x0010, 0x933: 0x0010, 0x934: 0x0010, 0x935: 0x0010, 0x936: 0x0010, 0x937: 0x0010, 0x938: 0x0010, 0x939: 0x0010, 0x93a: 0x0010, 0x93b: 0x0024, 0x93c: 0x0024, 0x93e: 0x0024, 0x93f: 0x2000, // Block 0x25, offset 0x940 0x940: 0x2000, 0x941: 0x0024, 0x942: 0x0024, 0x943: 0x0024, 0x944: 0x0024, 0x946: 0x2000, 0x947: 0x2000, 0x948: 0x2000, 0x94a: 0x2000, 0x94b: 0x2000, 0x94c: 0x2000, 0x94d: 0x0044, 0x94e: 0x0800, 0x957: 0x0024, 0x962: 0x0024, 0x963: 0x0024, // Block 0x26, offset 0x980 0x981: 0x0024, 0x982: 0x2000, 0x983: 0x2000, // Block 0x27, offset 0x9c0 0x9ca: 0x0024, 0x9cf: 0x0024, 0x9d0: 0x2000, 0x9d1: 0x2000, 0x9d2: 0x0024, 0x9d3: 0x0024, 0x9d4: 0x0024, 0x9d6: 0x0024, 0x9d8: 0x2000, 0x9d9: 0x2000, 0x9da: 0x2000, 0x9db: 0x2000, 0x9dc: 0x2000, 0x9dd: 0x2000, 0x9de: 0x2000, 0x9df: 0x0024, 0x9f2: 0x2000, 0x9f3: 0x2000, // Block 0x28, offset 0xa00 0xa31: 0x0024, 0xa33: 0x2000, 0xa34: 0x0024, 0xa35: 0x0024, 0xa36: 0x0024, 0xa37: 0x0024, 0xa38: 0x0024, 0xa39: 0x0024, 0xa3a: 0x0024, // Block 0x29, offset 0xa40 0xa47: 0x0024, 0xa48: 0x0024, 0xa49: 0x0024, 0xa4a: 0x0024, 0xa4b: 0x0024, 0xa4c: 0x0024, 0xa4d: 0x0024, 0xa4e: 0x0024, // Block 0x2a, offset 0xa80 0xab1: 0x0024, 0xab3: 0x2000, 0xab4: 0x0024, 0xab5: 0x0024, 0xab6: 0x0024, 0xab7: 0x0024, 0xab8: 0x0024, 0xab9: 0x0024, 0xaba: 0x0024, 0xabb: 0x0024, 0xabc: 0x0024, // Block 0x2b, offset 0xac0 0xac8: 0x0024, 0xac9: 0x0024, 0xaca: 0x0024, 0xacb: 0x0024, 0xacc: 0x0024, 0xacd: 0x0024, 0xace: 0x0024, // Block 0x2c, offset 0xb00 0xb18: 0x0024, 0xb19: 0x0024, 0xb35: 0x0024, 0xb37: 0x0024, 0xb39: 0x0024, 0xb3e: 0x2000, 0xb3f: 0x2000, // Block 0x2d, offset 0xb40 0xb71: 0x0024, 0xb72: 0x0024, 0xb73: 0x0024, 0xb74: 0x0024, 0xb75: 0x0024, 0xb76: 0x0024, 0xb77: 0x0024, 0xb78: 0x0024, 0xb79: 0x0024, 0xb7a: 0x0024, 0xb7b: 0x0024, 0xb7c: 0x0024, 0xb7d: 0x0024, 0xb7e: 0x0024, 0xb7f: 0x2000, // Block 0x2e, offset 0xb80 0xb80: 0x0024, 0xb81: 0x0024, 0xb82: 0x0024, 0xb83: 0x0024, 0xb84: 0x0024, 0xb86: 0x0024, 0xb87: 0x0024, 0xb8d: 0x0024, 0xb8e: 0x0024, 0xb8f: 0x0024, 0xb90: 0x0024, 0xb91: 0x0024, 0xb92: 0x0024, 0xb93: 0x0024, 0xb94: 0x0024, 0xb95: 0x0024, 0xb96: 0x0024, 0xb97: 0x0024, 0xb99: 0x0024, 0xb9a: 0x0024, 0xb9b: 0x0024, 0xb9c: 0x0024, 0xb9d: 0x0024, 0xb9e: 0x0024, 0xb9f: 0x0024, 0xba0: 0x0024, 0xba1: 0x0024, 0xba2: 0x0024, 0xba3: 0x0024, 0xba4: 0x0024, 0xba5: 0x0024, 0xba6: 0x0024, 0xba7: 0x0024, 0xba8: 0x0024, 0xba9: 0x0024, 0xbaa: 0x0024, 0xbab: 0x0024, 0xbac: 0x0024, 0xbad: 0x0024, 0xbae: 0x0024, 0xbaf: 0x0024, 0xbb0: 0x0024, 0xbb1: 0x0024, 0xbb2: 0x0024, 0xbb3: 0x0024, 0xbb4: 0x0024, 0xbb5: 0x0024, 0xbb6: 0x0024, 0xbb7: 0x0024, 0xbb8: 0x0024, 0xbb9: 0x0024, 0xbba: 0x0024, 0xbbb: 0x0024, 0xbbc: 0x0024, // Block 0x2f, offset 0xbc0 0xbc6: 0x0024, // Block 0x30, offset 0xc00 0xc00: 0x0010, 0xc01: 0x0010, 0xc02: 0x0010, 0xc03: 0x0010, 0xc04: 0x0010, 0xc05: 0x0010, 0xc06: 0x0010, 0xc07: 0x0010, 0xc08: 0x0010, 0xc09: 0x0010, 0xc0a: 0x0010, 0xc0b: 0x0010, 0xc0c: 0x0010, 0xc0d: 0x0010, 0xc0e: 0x0010, 0xc0f: 0x0010, 0xc10: 0x0010, 0xc11: 0x0010, 0xc12: 0x0010, 0xc13: 0x0010, 0xc14: 0x0010, 0xc15: 0x0010, 0xc16: 0x0010, 0xc17: 0x0010, 0xc18: 0x0010, 0xc19: 0x0010, 0xc1a: 0x0010, 0xc1b: 0x0010, 0xc1c: 0x0010, 0xc1d: 0x0010, 0xc1e: 0x0010, 0xc1f: 0x0010, 0xc20: 0x0010, 0xc21: 0x0010, 0xc22: 0x0010, 0xc23: 0x0010, 0xc24: 0x0010, 0xc25: 0x0010, 0xc26: 0x0010, 0xc27: 0x0010, 0xc28: 0x0010, 0xc29: 0x0010, 0xc2a: 0x0010, 0xc2d: 0x0024, 0xc2e: 0x0024, 0xc2f: 0x0024, 0xc30: 0x0024, 0xc31: 0x2000, 0xc32: 0x0024, 0xc33: 0x0024, 0xc34: 0x0024, 0xc35: 0x0024, 0xc36: 0x0024, 0xc37: 0x0024, 0xc39: 0x0044, 0xc3a: 0x0024, 0xc3b: 0x2000, 0xc3c: 0x2000, 0xc3d: 0x0024, 0xc3e: 0x0024, 0xc3f: 0x0010, // Block 0x31, offset 0xc40 0xc50: 0x0010, 0xc51: 0x0010, 0xc52: 0x0010, 0xc53: 0x0010, 0xc54: 0x0010, 0xc55: 0x0010, 0xc56: 0x2000, 0xc57: 0x2000, 0xc58: 0x0024, 0xc59: 0x0024, 0xc5a: 0x0010, 0xc5b: 0x0010, 0xc5c: 0x0010, 0xc5d: 0x0010, 0xc5e: 0x0024, 0xc5f: 0x0024, 0xc60: 0x0024, 0xc61: 0x0010, 0xc65: 0x0010, 0xc66: 0x0010, 0xc6e: 0x0010, 0xc6f: 0x0010, 0xc70: 0x0010, 0xc71: 0x0024, 0xc72: 0x0024, 0xc73: 0x0024, 0xc74: 0x0024, 0xc75: 0x0010, 0xc76: 0x0010, 0xc77: 0x0010, 0xc78: 0x0010, 0xc79: 0x0010, 0xc7a: 0x0010, 0xc7b: 0x0010, 0xc7c: 0x0010, 0xc7d: 0x0010, 0xc7e: 0x0010, 0xc7f: 0x0010, // Block 0x32, offset 0xc80 0xc80: 0x0010, 0xc81: 0x0010, 0xc82: 0x0024, 0xc84: 0x2000, 0xc85: 0x0024, 0xc86: 0x0024, 0xc8d: 0x0024, 0xc8e: 0x0010, 0xc9d: 0x0024, // Block 0x33, offset 0xcc0 0xcc0: 0x0080, 0xcc1: 0x0080, 0xcc2: 0x0080, 0xcc3: 0x0080, 0xcc4: 0x0080, 0xcc5: 0x0080, 0xcc6: 0x0080, 0xcc7: 0x0080, 0xcc8: 0x0080, 0xcc9: 0x0080, 0xcca: 0x0080, 0xccb: 0x0080, 0xccc: 0x0080, 0xccd: 0x0080, 0xcce: 0x0080, 0xccf: 0x0080, 0xcd0: 0x0080, 0xcd1: 0x0080, 0xcd2: 0x0080, 0xcd3: 0x0080, 0xcd4: 0x0080, 0xcd5: 0x0080, 0xcd6: 0x0080, 0xcd7: 0x0080, 0xcd8: 0x0080, 0xcd9: 0x0080, 0xcda: 0x0080, 0xcdb: 0x0080, 0xcdc: 0x0080, 0xcdd: 0x0080, 0xcde: 0x0080, 0xcdf: 0x0080, 0xce0: 0x0080, 0xce1: 0x0080, 0xce2: 0x0080, 0xce3: 0x0080, 0xce4: 0x0080, 0xce5: 0x0080, 0xce6: 0x0080, 0xce7: 0x0080, 0xce8: 0x0080, 0xce9: 0x0080, 0xcea: 0x0080, 0xceb: 0x0080, 0xcec: 0x0080, 0xced: 0x0080, 0xcee: 0x0080, 0xcef: 0x0080, 0xcf0: 0x0080, 0xcf1: 0x0080, 0xcf2: 0x0080, 0xcf3: 0x0080, 0xcf4: 0x0080, 0xcf5: 0x0080, 0xcf6: 0x0080, 0xcf7: 0x0080, 0xcf8: 0x0080, 0xcf9: 0x0080, 0xcfa: 0x0080, 0xcfb: 0x0080, 0xcfc: 0x0080, 0xcfd: 0x0080, 0xcfe: 0x0080, 0xcff: 0x0080, // Block 0x34, offset 0xd00 0xd00: 0x0080, 0xd01: 0x0080, 0xd02: 0x0080, 0xd03: 0x0080, 0xd04: 0x0080, 0xd05: 0x0080, 0xd06: 0x0080, 0xd07: 0x0080, 0xd08: 0x0080, 0xd09: 0x0080, 0xd0a: 0x0080, 0xd0b: 0x0080, 0xd0c: 0x0080, 0xd0d: 0x0080, 0xd0e: 0x0080, 0xd0f: 0x0080, 0xd10: 0x0080, 0xd11: 0x0080, 0xd12: 0x0080, 0xd13: 0x0080, 0xd14: 0x0080, 0xd15: 0x0080, 0xd16: 0x0080, 0xd17: 0x0080, 0xd18: 0x0080, 0xd19: 0x0080, 0xd1a: 0x0080, 0xd1b: 0x0080, 0xd1c: 0x0080, 0xd1d: 0x0080, 0xd1e: 0x0080, 0xd1f: 0x0080, 0xd20: 0x8000, 0xd21: 0x8000, 0xd22: 0x8000, 0xd23: 0x8000, 0xd24: 0x8000, 0xd25: 0x8000, 0xd26: 0x8000, 0xd27: 0x8000, 0xd28: 0x8000, 0xd29: 0x8000, 0xd2a: 0x8000, 0xd2b: 0x8000, 0xd2c: 0x8000, 0xd2d: 0x8000, 0xd2e: 0x8000, 0xd2f: 0x8000, 0xd30: 0x8000, 0xd31: 0x8000, 0xd32: 0x8000, 0xd33: 0x8000, 0xd34: 0x8000, 0xd35: 0x8000, 0xd36: 0x8000, 0xd37: 0x8000, 0xd38: 0x8000, 0xd39: 0x8000, 0xd3a: 0x8000, 0xd3b: 0x8000, 0xd3c: 0x8000, 0xd3d: 0x8000, 0xd3e: 0x8000, 0xd3f: 0x8000, // Block 0x35, offset 0xd40 0xd40: 0x8000, 0xd41: 0x8000, 0xd42: 0x8000, 0xd43: 0x8000, 0xd44: 0x8000, 0xd45: 0x8000, 0xd46: 0x8000, 0xd47: 0x8000, 0xd48: 0x8000, 0xd49: 0x8000, 0xd4a: 0x8000, 0xd4b: 0x8000, 0xd4c: 0x8000, 0xd4d: 0x8000, 0xd4e: 0x8000, 0xd4f: 0x8000, 0xd50: 0x8000, 0xd51: 0x8000, 0xd52: 0x8000, 0xd53: 0x8000, 0xd54: 0x8000, 0xd55: 0x8000, 0xd56: 0x8000, 0xd57: 0x8000, 0xd58: 0x8000, 0xd59: 0x8000, 0xd5a: 0x8000, 0xd5b: 0x8000, 0xd5c: 0x8000, 0xd5d: 0x8000, 0xd5e: 0x8000, 0xd5f: 0x8000, 0xd60: 0x8000, 0xd61: 0x8000, 0xd62: 0x8000, 0xd63: 0x8000, 0xd64: 0x8000, 0xd65: 0x8000, 0xd66: 0x8000, 0xd67: 0x8000, 0xd68: 0x4000, 0xd69: 0x4000, 0xd6a: 0x4000, 0xd6b: 0x4000, 0xd6c: 0x4000, 0xd6d: 0x4000, 0xd6e: 0x4000, 0xd6f: 0x4000, 0xd70: 0x4000, 0xd71: 0x4000, 0xd72: 0x4000, 0xd73: 0x4000, 0xd74: 0x4000, 0xd75: 0x4000, 0xd76: 0x4000, 0xd77: 0x4000, 0xd78: 0x4000, 0xd79: 0x4000, 0xd7a: 0x4000, 0xd7b: 0x4000, 0xd7c: 0x4000, 0xd7d: 0x4000, 0xd7e: 0x4000, 0xd7f: 0x4000, // Block 0x36, offset 0xd80 0xd80: 0x4000, 0xd81: 0x4000, 0xd82: 0x4000, 0xd83: 0x4000, 0xd84: 0x4000, 0xd85: 0x4000, 0xd86: 0x4000, 0xd87: 0x4000, 0xd88: 0x4000, 0xd89: 0x4000, 0xd8a: 0x4000, 0xd8b: 0x4000, 0xd8c: 0x4000, 0xd8d: 0x4000, 0xd8e: 0x4000, 0xd8f: 0x4000, 0xd90: 0x4000, 0xd91: 0x4000, 0xd92: 0x4000, 0xd93: 0x4000, 0xd94: 0x4000, 0xd95: 0x4000, 0xd96: 0x4000, 0xd97: 0x4000, 0xd98: 0x4000, 0xd99: 0x4000, 0xd9a: 0x4000, 0xd9b: 0x4000, 0xd9c: 0x4000, 0xd9d: 0x4000, 0xd9e: 0x4000, 0xd9f: 0x4000, 0xda0: 0x4000, 0xda1: 0x4000, 0xda2: 0x4000, 0xda3: 0x4000, 0xda4: 0x4000, 0xda5: 0x4000, 0xda6: 0x4000, 0xda7: 0x4000, 0xda8: 0x4000, 0xda9: 0x4000, 0xdaa: 0x4000, 0xdab: 0x4000, 0xdac: 0x4000, 0xdad: 0x4000, 0xdae: 0x4000, 0xdaf: 0x4000, 0xdb0: 0x4000, 0xdb1: 0x4000, 0xdb2: 0x4000, 0xdb3: 0x4000, 0xdb4: 0x4000, 0xdb5: 0x4000, 0xdb6: 0x4000, 0xdb7: 0x4000, 0xdb8: 0x4000, 0xdb9: 0x4000, 0xdba: 0x4000, 0xdbb: 0x4000, 0xdbc: 0x4000, 0xdbd: 0x4000, 0xdbe: 0x4000, 0xdbf: 0x4000, // Block 0x37, offset 0xdc0 0xddd: 0x0024, 0xdde: 0x0024, 0xddf: 0x0024, // Block 0x38, offset 0xe00 0xe12: 0x0024, 0xe13: 0x0024, 0xe14: 0x0024, 0xe15: 0x0024, 0xe32: 0x0024, 0xe33: 0x0024, 0xe34: 0x0024, // Block 0x39, offset 0xe40 0xe52: 0x0024, 0xe53: 0x0024, 0xe72: 0x0024, 0xe73: 0x0024, // Block 0x3a, offset 0xe80 0xe80: 0x0010, 0xe81: 0x0010, 0xe82: 0x0010, 0xe83: 0x0010, 0xe84: 0x0010, 0xe85: 0x0010, 0xe86: 0x0010, 0xe87: 0x0010, 0xe88: 0x0010, 0xe89: 0x0010, 0xe8a: 0x0010, 0xe8b: 0x0010, 0xe8c: 0x0010, 0xe8d: 0x0010, 0xe8e: 0x0010, 0xe8f: 0x0010, 0xe90: 0x0010, 0xe91: 0x0010, 0xe92: 0x0010, 0xe93: 0x0010, 0xe94: 0x0010, 0xe95: 0x0010, 0xe96: 0x0010, 0xe97: 0x0010, 0xe98: 0x0010, 0xe99: 0x0010, 0xe9a: 0x0010, 0xe9b: 0x0010, 0xe9c: 0x0010, 0xe9d: 0x0010, 0xe9e: 0x0010, 0xe9f: 0x0010, 0xea0: 0x0010, 0xea1: 0x0010, 0xea2: 0x0010, 0xea3: 0x0010, 0xea4: 0x0010, 0xea5: 0x0010, 0xea6: 0x0010, 0xea7: 0x0010, 0xea8: 0x0010, 0xea9: 0x0010, 0xeaa: 0x0010, 0xeab: 0x0010, 0xeac: 0x0010, 0xead: 0x0010, 0xeae: 0x0010, 0xeaf: 0x0010, 0xeb0: 0x0010, 0xeb1: 0x0010, 0xeb2: 0x0010, 0xeb3: 0x0010, 0xeb4: 0x0024, 0xeb5: 0x0024, 0xeb6: 0x2000, 0xeb7: 0x0024, 0xeb8: 0x0024, 0xeb9: 0x0024, 0xeba: 0x0024, 0xebb: 0x0024, 0xebc: 0x0024, 0xebd: 0x0024, 0xebe: 0x2000, 0xebf: 0x2000, // Block 0x3b, offset 0xec0 0xec0: 0x2000, 0xec1: 0x2000, 0xec2: 0x2000, 0xec3: 0x2000, 0xec4: 0x2000, 0xec5: 0x2000, 0xec6: 0x0024, 0xec7: 0x2000, 0xec8: 0x2000, 0xec9: 0x0024, 0xeca: 0x0024, 0xecb: 0x0024, 0xecc: 0x0024, 0xecd: 0x0024, 0xece: 0x0024, 0xecf: 0x0024, 0xed0: 0x0024, 0xed1: 0x0024, 0xed2: 0x0044, 0xed3: 0x0024, 0xedd: 0x0024, // Block 0x3c, offset 0xf00 0xf0b: 0x0024, 0xf0c: 0x0024, 0xf0d: 0x0024, 0xf0e: 0x0002, 0xf0f: 0x0024, // Block 0x3d, offset 0xf40 0xf45: 0x0024, 0xf46: 0x0024, 0xf69: 0x0024, // Block 0x3e, offset 0xf80 0xfa0: 0x0024, 0xfa1: 0x0024, 0xfa2: 0x0024, 0xfa3: 0x2000, 0xfa4: 0x2000, 0xfa5: 0x2000, 0xfa6: 0x2000, 0xfa7: 0x0024, 0xfa8: 0x0024, 0xfa9: 0x2000, 0xfaa: 0x2000, 0xfab: 0x2000, 0xfb0: 0x2000, 0xfb1: 0x2000, 0xfb2: 0x0024, 0xfb3: 0x2000, 0xfb4: 0x2000, 0xfb5: 0x2000, 0xfb6: 0x2000, 0xfb7: 0x2000, 0xfb8: 0x2000, 0xfb9: 0x0024, 0xfba: 0x0024, 0xfbb: 0x0024, // Block 0x3f, offset 0xfc0 0xfd7: 0x0024, 0xfd8: 0x0024, 0xfd9: 0x2000, 0xfda: 0x2000, 0xfdb: 0x0024, 0xfe0: 0x0010, 0xfe1: 0x0010, 0xfe2: 0x0010, 0xfe3: 0x0010, 0xfe4: 0x0010, 0xfe5: 0x0010, 0xfe6: 0x0010, 0xfe7: 0x0010, 0xfe8: 0x0010, 0xfe9: 0x0010, 0xfea: 0x0010, 0xfeb: 0x0010, 0xfec: 0x0010, 0xfed: 0x0010, 0xfee: 0x0010, 0xfef: 0x0010, 0xff0: 0x0010, 0xff1: 0x0010, 0xff2: 0x0010, 0xff3: 0x0010, 0xff4: 0x0010, 0xff5: 0x0010, 0xff6: 0x0010, 0xff7: 0x0010, 0xff8: 0x0010, 0xff9: 0x0010, 0xffa: 0x0010, 0xffb: 0x0010, 0xffc: 0x0010, 0xffd: 0x0010, 0xffe: 0x0010, 0xfff: 0x0010, // Block 0x40, offset 0x1000 0x1000: 0x0010, 0x1001: 0x0010, 0x1002: 0x0010, 0x1003: 0x0010, 0x1004: 0x0010, 0x1005: 0x0010, 0x1006: 0x0010, 0x1007: 0x0010, 0x1008: 0x0010, 0x1009: 0x0010, 0x100a: 0x0010, 0x100b: 0x0010, 0x100c: 0x0010, 0x100d: 0x0010, 0x100e: 0x0010, 0x100f: 0x0010, 0x1010: 0x0010, 0x1011: 0x0010, 0x1012: 0x0010, 0x1013: 0x0010, 0x1014: 0x0010, 0x1015: 0x2000, 0x1016: 0x0024, 0x1017: 0x2000, 0x1018: 0x0024, 0x1019: 0x0024, 0x101a: 0x0024, 0x101b: 0x0024, 0x101c: 0x0024, 0x101d: 0x0024, 0x101e: 0x0024, 0x1020: 0x0044, 0x1022: 0x0024, 0x1025: 0x0024, 0x1026: 0x0024, 0x1027: 0x0024, 0x1028: 0x0024, 0x1029: 0x0024, 0x102a: 0x0024, 0x102b: 0x0024, 0x102c: 0x0024, 0x102d: 0x2000, 0x102e: 0x2000, 0x102f: 0x2000, 0x1030: 0x2000, 0x1031: 0x2000, 0x1032: 0x2000, 0x1033: 0x0024, 0x1034: 0x0024, 0x1035: 0x0024, 0x1036: 0x0024, 0x1037: 0x0024, 0x1038: 0x0024, 0x1039: 0x0024, 0x103a: 0x0024, 0x103b: 0x0024, 0x103c: 0x0024, 0x103f: 0x0024, // Block 0x41, offset 0x1040 0x1070: 0x0024, 0x1071: 0x0024, 0x1072: 0x0024, 0x1073: 0x0024, 0x1074: 0x0024, 0x1075: 0x0024, 0x1076: 0x0024, 0x1077: 0x0024, 0x1078: 0x0024, 0x1079: 0x0024, 0x107a: 0x0024, 0x107b: 0x0024, 0x107c: 0x0024, 0x107d: 0x0024, 0x107e: 0x0024, 0x107f: 0x0024, // Block 0x42, offset 0x1080 0x1080: 0x0024, 0x1081: 0x0024, 0x1082: 0x0024, 0x1083: 0x0024, 0x1084: 0x0024, 0x1085: 0x0024, 0x1086: 0x0024, 0x1087: 0x0024, 0x1088: 0x0024, 0x1089: 0x0024, 0x108a: 0x0024, 0x108b: 0x0024, 0x108c: 0x0024, 0x108d: 0x0024, 0x108e: 0x0024, 0x108f: 0x0024, 0x1090: 0x0024, 0x1091: 0x0024, 0x1092: 0x0024, 0x1093: 0x0024, 0x1094: 0x0024, 0x1095: 0x0024, 0x1096: 0x0024, 0x1097: 0x0024, 0x1098: 0x0024, 0x1099: 0x0024, 0x109a: 0x0024, 0x109b: 0x0024, 0x109c: 0x0024, 0x109d: 0x0024, 0x10a0: 0x0024, 0x10a1: 0x0024, 0x10a2: 0x0024, 0x10a3: 0x0024, 0x10a4: 0x0024, 0x10a5: 0x0024, 0x10a6: 0x0024, 0x10a7: 0x0024, 0x10a8: 0x0024, 0x10a9: 0x0024, 0x10aa: 0x0024, 0x10ab: 0x0024, // Block 0x43, offset 0x10c0 0x10c0: 0x0024, 0x10c1: 0x0024, 0x10c2: 0x0024, 0x10c3: 0x0024, 0x10c4: 0x2000, 0x10cb: 0x0010, 0x10cc: 0x0010, 0x10d3: 0x0010, 0x10d4: 0x0010, 0x10d5: 0x0010, 0x10d6: 0x0010, 0x10d7: 0x0010, 0x10d8: 0x0010, 0x10d9: 0x0010, 0x10da: 0x0010, 0x10db: 0x0010, 0x10dc: 0x0010, 0x10dd: 0x0010, 0x10de: 0x0010, 0x10df: 0x0010, 0x10e0: 0x0010, 0x10e1: 0x0010, 0x10e2: 0x0010, 0x10e3: 0x0010, 0x10e4: 0x0010, 0x10e5: 0x0010, 0x10e6: 0x0010, 0x10e7: 0x0010, 0x10e8: 0x0010, 0x10e9: 0x0010, 0x10ea: 0x0010, 0x10eb: 0x0010, 0x10ec: 0x0010, 0x10ed: 0x0010, 0x10ee: 0x0010, 0x10ef: 0x0010, 0x10f0: 0x0010, 0x10f1: 0x0010, 0x10f2: 0x0010, 0x10f3: 0x0010, 0x10f4: 0x0024, 0x10f5: 0x0024, 0x10f6: 0x0024, 0x10f7: 0x0024, 0x10f8: 0x0024, 0x10f9: 0x0024, 0x10fa: 0x0024, 0x10fb: 0x0024, 0x10fc: 0x0024, 0x10fd: 0x0024, 0x10fe: 0x2000, 0x10ff: 0x2000, // Block 0x44, offset 0x1100 0x1100: 0x2000, 0x1101: 0x2000, 0x1102: 0x0024, 0x1103: 0x0024, 0x1104: 0x0044, 0x1105: 0x0010, 0x1106: 0x0010, 0x1107: 0x0010, 0x1108: 0x0010, 0x1109: 0x0010, 0x110a: 0x0010, 0x110b: 0x0010, 0x110c: 0x0010, 0x112b: 0x0024, 0x112c: 0x0024, 0x112d: 0x0024, 0x112e: 0x0024, 0x112f: 0x0024, 0x1130: 0x0024, 0x1131: 0x0024, 0x1132: 0x0024, 0x1133: 0x0024, // Block 0x45, offset 0x1140 0x1140: 0x0024, 0x1141: 0x0024, 0x1142: 0x2000, 0x1143: 0x0010, 0x1144: 0x0010, 0x1145: 0x0010, 0x1146: 0x0010, 0x1147: 0x0010, 0x1148: 0x0010, 0x1149: 0x0010, 0x114a: 0x0010, 0x114b: 0x0010, 0x114c: 0x0010, 0x114d: 0x0010, 0x114e: 0x0010, 0x114f: 0x0010, 0x1150: 0x0010, 0x1151: 0x0010, 0x1152: 0x0010, 0x1153: 0x0010, 0x1154: 0x0010, 0x1155: 0x0010, 0x1156: 0x0010, 0x1157: 0x0010, 0x1158: 0x0010, 0x1159: 0x0010, 0x115a: 0x0010, 0x115b: 0x0010, 0x115c: 0x0010, 0x115d: 0x0010, 0x115e: 0x0010, 0x115f: 0x0010, 0x1160: 0x0010, 0x1161: 0x2000, 0x1162: 0x0024, 0x1163: 0x0024, 0x1164: 0x0024, 0x1165: 0x0024, 0x1166: 0x2000, 0x1167: 0x2000, 0x1168: 0x0024, 0x1169: 0x0024, 0x116a: 0x0024, 0x116b: 0x0044, 0x116c: 0x0024, 0x116d: 0x0024, 0x116e: 0x0010, 0x116f: 0x0010, 0x117b: 0x0010, 0x117c: 0x0010, 0x117d: 0x0010, // Block 0x46, offset 0x1180 0x11a6: 0x0024, 0x11a7: 0x2000, 0x11a8: 0x0024, 0x11a9: 0x0024, 0x11aa: 0x2000, 0x11ab: 0x2000, 0x11ac: 0x2000, 0x11ad: 0x0024, 0x11ae: 0x2000, 0x11af: 0x0024, 0x11b0: 0x0024, 0x11b1: 0x0024, 0x11b2: 0x0024, 0x11b3: 0x0024, // Block 0x47, offset 0x11c0 0x11e4: 0x2000, 0x11e5: 0x2000, 0x11e6: 0x2000, 0x11e7: 0x2000, 0x11e8: 0x2000, 0x11e9: 0x2000, 0x11ea: 0x2000, 0x11eb: 0x2000, 0x11ec: 0x0024, 0x11ed: 0x0024, 0x11ee: 0x0024, 0x11ef: 0x0024, 0x11f0: 0x0024, 0x11f1: 0x0024, 0x11f2: 0x0024, 0x11f3: 0x0024, 0x11f4: 0x2000, 0x11f5: 0x2000, 0x11f6: 0x0024, 0x11f7: 0x0024, // Block 0x48, offset 0x1200 0x1210: 0x0024, 0x1211: 0x0024, 0x1212: 0x0024, 0x1214: 0x0024, 0x1215: 0x0024, 0x1216: 0x0024, 0x1217: 0x0024, 0x1218: 0x0024, 0x1219: 0x0024, 0x121a: 0x0024, 0x121b: 0x0024, 0x121c: 0x0024, 0x121d: 0x0024, 0x121e: 0x0024, 0x121f: 0x0024, 0x1220: 0x0024, 0x1221: 0x2000, 0x1222: 0x0024, 0x1223: 0x0024, 0x1224: 0x0024, 0x1225: 0x0024, 0x1226: 0x0024, 0x1227: 0x0024, 0x1228: 0x0024, 0x122d: 0x0024, 0x1234: 0x0024, 0x1237: 0x2000, 0x1238: 0x0024, 0x1239: 0x0024, // Block 0x49, offset 0x1240 0x124b: 0x0002, 0x124c: 0x0004, 0x124d: 0x10020, 0x124e: 0x0002, 0x124f: 0x0002, 0x1268: 0x0002, 0x1269: 0x0002, 0x126a: 0x0002, 0x126b: 0x0002, 0x126c: 0x0002, 0x126d: 0x0002, 0x126e: 0x0002, 0x127c: 0x0008, // Block 0x4a, offset 0x1280 0x1289: 0x0008, 0x12a0: 0x0002, 0x12a1: 0x0002, 0x12a2: 0x0002, 0x12a3: 0x0002, 0x12a4: 0x0002, 0x12a5: 0x0002, 0x12a6: 0x0002, 0x12a7: 0x0002, 0x12a8: 0x0002, 0x12a9: 0x0002, 0x12aa: 0x0002, 0x12ab: 0x0002, 0x12ac: 0x0002, 0x12ad: 0x0002, 0x12ae: 0x0002, 0x12af: 0x0002, // Block 0x4b, offset 0x12c0 0x12d0: 0x0024, 0x12d1: 0x0024, 0x12d2: 0x0024, 0x12d3: 0x0024, 0x12d4: 0x0024, 0x12d5: 0x0024, 0x12d6: 0x0024, 0x12d7: 0x0024, 0x12d8: 0x0024, 0x12d9: 0x0024, 0x12da: 0x0024, 0x12db: 0x0024, 0x12dc: 0x0024, 0x12dd: 0x0024, 0x12de: 0x0024, 0x12df: 0x0024, 0x12e0: 0x0024, 0x12e1: 0x0024, 0x12e2: 0x0024, 0x12e3: 0x0024, 0x12e4: 0x0024, 0x12e5: 0x0024, 0x12e6: 0x0024, 0x12e7: 0x0024, 0x12e8: 0x0024, 0x12e9: 0x0024, 0x12ea: 0x0024, 0x12eb: 0x0024, 0x12ec: 0x0024, 0x12ed: 0x0024, 0x12ee: 0x0024, 0x12ef: 0x0024, 0x12f0: 0x0024, // Block 0x4c, offset 0x1300 0x1322: 0x0008, 0x1339: 0x0008, // Block 0x4d, offset 0x1340 0x1354: 0x0008, 0x1355: 0x0008, 0x1356: 0x0008, 0x1357: 0x0008, 0x1358: 0x0008, 0x1359: 0x0008, 0x1369: 0x0008, 0x136a: 0x0008, // Block 0x4e, offset 0x1380 0x139a: 0x0008, 0x139b: 0x0008, 0x13a8: 0x0008, // Block 0x4f, offset 0x13c0 0x13cf: 0x0008, 0x13e9: 0x0008, 0x13ea: 0x0008, 0x13eb: 0x0008, 0x13ec: 0x0008, 0x13ed: 0x0008, 0x13ee: 0x0008, 0x13ef: 0x0008, 0x13f0: 0x0008, 0x13f1: 0x0008, 0x13f2: 0x0008, 0x13f3: 0x0008, 0x13f8: 0x0008, 0x13f9: 0x0008, 0x13fa: 0x0008, // Block 0x50, offset 0x1400 0x1402: 0x0008, // Block 0x51, offset 0x1440 0x146a: 0x0008, 0x146b: 0x0008, 0x1476: 0x0008, // Block 0x52, offset 0x1480 0x1480: 0x0008, 0x14bb: 0x0008, 0x14bc: 0x0008, 0x14bd: 0x0008, 0x14be: 0x0008, // Block 0x53, offset 0x14c0 0x14c0: 0x0008, 0x14c1: 0x0008, 0x14c2: 0x0008, 0x14c3: 0x0008, 0x14c4: 0x0008, 0x14ce: 0x0008, 0x14d1: 0x0008, 0x14d4: 0x0008, 0x14d5: 0x0008, 0x14d8: 0x0008, 0x14dd: 0x0008, 0x14e0: 0x0008, 0x14e2: 0x0008, 0x14e3: 0x0008, 0x14e6: 0x0008, 0x14ea: 0x0008, 0x14ee: 0x0008, 0x14ef: 0x0008, 0x14f8: 0x0008, 0x14f9: 0x0008, 0x14fa: 0x0008, // Block 0x54, offset 0x1500 0x1500: 0x0008, 0x1502: 0x0008, 0x1508: 0x0008, 0x1509: 0x0008, 0x150a: 0x0008, 0x150b: 0x0008, 0x150c: 0x0008, 0x150d: 0x0008, 0x150e: 0x0008, 0x150f: 0x0008, 0x1510: 0x0008, 0x1511: 0x0008, 0x1512: 0x0008, 0x1513: 0x0008, 0x151f: 0x0008, 0x1520: 0x0008, 0x1523: 0x0008, 0x1525: 0x0008, 0x1526: 0x0008, 0x1528: 0x0008, 0x153b: 0x0008, 0x153e: 0x0008, 0x153f: 0x0008, // Block 0x55, offset 0x1540 0x1552: 0x0008, 0x1553: 0x0008, 0x1554: 0x0008, 0x1555: 0x0008, 0x1556: 0x0008, 0x1557: 0x0008, 0x1559: 0x0008, 0x155b: 0x0008, 0x155c: 0x0008, 0x1560: 0x0008, 0x1561: 0x0008, 0x1567: 0x0008, 0x156a: 0x0008, 0x156b: 0x0008, 0x1570: 0x0008, 0x1571: 0x0008, 0x157d: 0x0008, 0x157e: 0x0008, // Block 0x56, offset 0x1580 0x1584: 0x0008, 0x1585: 0x0008, 0x1588: 0x0008, 0x158e: 0x0008, 0x158f: 0x0008, 0x1591: 0x0008, 0x1593: 0x0008, 0x1594: 0x0008, 0x15a9: 0x0008, 0x15aa: 0x0008, 0x15b0: 0x0008, 0x15b1: 0x0008, 0x15b2: 0x0008, 0x15b3: 0x0008, 0x15b4: 0x0008, 0x15b5: 0x0008, 0x15b7: 0x0008, 0x15b8: 0x0008, 0x15b9: 0x0008, 0x15ba: 0x0008, 0x15bd: 0x0008, // Block 0x57, offset 0x15c0 0x15c2: 0x0008, 0x15c5: 0x0008, 0x15c8: 0x0008, 0x15c9: 0x0008, 0x15ca: 0x0008, 0x15cb: 0x0008, 0x15cc: 0x0008, 0x15cd: 0x0008, 0x15cf: 0x0008, 0x15d2: 0x0008, 0x15d4: 0x0008, 0x15d6: 0x0008, 0x15dd: 0x0008, 0x15e1: 0x0008, 0x15e8: 0x0008, 0x15f3: 0x0008, 0x15f4: 0x0008, // Block 0x58, offset 0x1600 0x1604: 0x0008, 0x1607: 0x0008, 0x160c: 0x0008, 0x160e: 0x0008, 0x1613: 0x0008, 0x1614: 0x0008, 0x1615: 0x0008, 0x1617: 0x0008, 0x1623: 0x0008, 0x1624: 0x0008, // Block 0x59, offset 0x1640 0x1655: 0x0008, 0x1656: 0x0008, 0x1657: 0x0008, 0x1661: 0x0008, 0x1670: 0x0008, 0x167f: 0x0008, // Block 0x5a, offset 0x1680 0x16b4: 0x0008, 0x16b5: 0x0008, // Block 0x5b, offset 0x16c0 0x16c5: 0x0008, 0x16c6: 0x0008, 0x16c7: 0x0008, 0x16db: 0x0008, 0x16dc: 0x0008, // Block 0x5c, offset 0x1700 0x1710: 0x0008, 0x1715: 0x0008, // Block 0x5d, offset 0x1740 0x176f: 0x0024, 0x1770: 0x0024, 0x1771: 0x0024, // Block 0x5e, offset 0x1780 0x17bf: 0x0024, // Block 0x5f, offset 0x17c0 0x17e0: 0x0024, 0x17e1: 0x0024, 0x17e2: 0x0024, 0x17e3: 0x0024, 0x17e4: 0x0024, 0x17e5: 0x0024, 0x17e6: 0x0024, 0x17e7: 0x0024, 0x17e8: 0x0024, 0x17e9: 0x0024, 0x17ea: 0x0024, 0x17eb: 0x0024, 0x17ec: 0x0024, 0x17ed: 0x0024, 0x17ee: 0x0024, 0x17ef: 0x0024, 0x17f0: 0x0024, 0x17f1: 0x0024, 0x17f2: 0x0024, 0x17f3: 0x0024, 0x17f4: 0x0024, 0x17f5: 0x0024, 0x17f6: 0x0024, 0x17f7: 0x0024, 0x17f8: 0x0024, 0x17f9: 0x0024, 0x17fa: 0x0024, 0x17fb: 0x0024, 0x17fc: 0x0024, 0x17fd: 0x0024, 0x17fe: 0x0024, 0x17ff: 0x0024, // Block 0x60, offset 0x1800 0x182a: 0x0024, 0x182b: 0x0024, 0x182c: 0x0024, 0x182d: 0x0024, 0x182e: 0x0024, 0x182f: 0x0024, 0x1830: 0x0008, 0x183d: 0x0008, // Block 0x61, offset 0x1840 0x1859: 0x0024, 0x185a: 0x0024, // Block 0x62, offset 0x1880 0x1897: 0x0008, 0x1899: 0x0008, // Block 0x63, offset 0x18c0 0x18ef: 0x0024, 0x18f0: 0x0024, 0x18f1: 0x0024, 0x18f2: 0x0024, 0x18f4: 0x0024, 0x18f5: 0x0024, 0x18f6: 0x0024, 0x18f7: 0x0024, 0x18f8: 0x0024, 0x18f9: 0x0024, 0x18fa: 0x0024, 0x18fb: 0x0024, 0x18fc: 0x0024, 0x18fd: 0x0024, // Block 0x64, offset 0x1900 0x191e: 0x0024, 0x191f: 0x0024, // Block 0x65, offset 0x1940 0x1970: 0x0024, 0x1971: 0x0024, // Block 0x66, offset 0x1980 0x1982: 0x0024, 0x1986: 0x0024, 0x198b: 0x0024, 0x19a3: 0x2000, 0x19a4: 0x2000, 0x19a5: 0x0024, 0x19a6: 0x0024, 0x19a7: 0x2000, 0x19ac: 0x0024, // Block 0x67, offset 0x19c0 0x19c0: 0x2000, 0x19c1: 0x2000, 0x19f4: 0x2000, 0x19f5: 0x2000, 0x19f6: 0x2000, 0x19f7: 0x2000, 0x19f8: 0x2000, 0x19f9: 0x2000, 0x19fa: 0x2000, 0x19fb: 0x2000, 0x19fc: 0x2000, 0x19fd: 0x2000, 0x19fe: 0x2000, 0x19ff: 0x2000, // Block 0x68, offset 0x1a00 0x1a00: 0x2000, 0x1a01: 0x2000, 0x1a02: 0x2000, 0x1a03: 0x2000, 0x1a04: 0x0024, 0x1a05: 0x0024, 0x1a20: 0x0024, 0x1a21: 0x0024, 0x1a22: 0x0024, 0x1a23: 0x0024, 0x1a24: 0x0024, 0x1a25: 0x0024, 0x1a26: 0x0024, 0x1a27: 0x0024, 0x1a28: 0x0024, 0x1a29: 0x0024, 0x1a2a: 0x0024, 0x1a2b: 0x0024, 0x1a2c: 0x0024, 0x1a2d: 0x0024, 0x1a2e: 0x0024, 0x1a2f: 0x0024, 0x1a30: 0x0024, 0x1a31: 0x0024, 0x1a3f: 0x0024, // Block 0x69, offset 0x1a40 0x1a66: 0x0024, 0x1a67: 0x0024, 0x1a68: 0x0024, 0x1a69: 0x0024, 0x1a6a: 0x0024, 0x1a6b: 0x0024, 0x1a6c: 0x0024, 0x1a6d: 0x0024, // Block 0x6a, offset 0x1a80 0x1a87: 0x0024, 0x1a88: 0x0024, 0x1a89: 0x0024, 0x1a8a: 0x0024, 0x1a8b: 0x0024, 0x1a8c: 0x0024, 0x1a8d: 0x0024, 0x1a8e: 0x0024, 0x1a8f: 0x0024, 0x1a90: 0x0024, 0x1a91: 0x0024, 0x1a92: 0x2000, 0x1a93: 0x0024, 0x1aa0: 0x0080, 0x1aa1: 0x0080, 0x1aa2: 0x0080, 0x1aa3: 0x0080, 0x1aa4: 0x0080, 0x1aa5: 0x0080, 0x1aa6: 0x0080, 0x1aa7: 0x0080, 0x1aa8: 0x0080, 0x1aa9: 0x0080, 0x1aaa: 0x0080, 0x1aab: 0x0080, 0x1aac: 0x0080, 0x1aad: 0x0080, 0x1aae: 0x0080, 0x1aaf: 0x0080, 0x1ab0: 0x0080, 0x1ab1: 0x0080, 0x1ab2: 0x0080, 0x1ab3: 0x0080, 0x1ab4: 0x0080, 0x1ab5: 0x0080, 0x1ab6: 0x0080, 0x1ab7: 0x0080, 0x1ab8: 0x0080, 0x1ab9: 0x0080, 0x1aba: 0x0080, 0x1abb: 0x0080, 0x1abc: 0x0080, // Block 0x6b, offset 0x1ac0 0x1ac0: 0x0024, 0x1ac1: 0x0024, 0x1ac2: 0x0024, 0x1ac3: 0x2000, 0x1ac9: 0x0010, 0x1aca: 0x0010, 0x1acb: 0x0010, 0x1acf: 0x0010, 0x1ad0: 0x0010, 0x1ad1: 0x0010, 0x1ad2: 0x0010, 0x1ad3: 0x0010, 0x1ad4: 0x0010, 0x1ad5: 0x0010, 0x1ad6: 0x0010, 0x1ad7: 0x0010, 0x1ad8: 0x0010, 0x1ad9: 0x0010, 0x1ada: 0x0010, 0x1adb: 0x0010, 0x1adc: 0x0010, 0x1add: 0x0010, 0x1ade: 0x0010, 0x1adf: 0x0010, 0x1ae0: 0x0010, 0x1ae1: 0x0010, 0x1ae2: 0x0010, 0x1ae3: 0x0010, 0x1ae4: 0x0010, 0x1ae5: 0x0010, 0x1ae6: 0x0010, 0x1ae7: 0x0010, 0x1ae8: 0x0010, 0x1ae9: 0x0010, 0x1aea: 0x0010, 0x1aeb: 0x0010, 0x1aec: 0x0010, 0x1aed: 0x0010, 0x1aee: 0x0010, 0x1aef: 0x0010, 0x1af0: 0x0010, 0x1af1: 0x0010, 0x1af2: 0x0010, 0x1af3: 0x0024, 0x1af4: 0x2000, 0x1af5: 0x2000, 0x1af6: 0x0024, 0x1af7: 0x0024, 0x1af8: 0x0024, 0x1af9: 0x0024, 0x1afa: 0x2000, 0x1afb: 0x2000, 0x1afc: 0x0024, 0x1afd: 0x0024, 0x1afe: 0x2000, 0x1aff: 0x2000, // Block 0x6c, offset 0x1b00 0x1b00: 0x0044, 0x1b20: 0x0010, 0x1b21: 0x0010, 0x1b22: 0x0010, 0x1b23: 0x0010, 0x1b24: 0x0010, 0x1b25: 0x0024, 0x1b27: 0x0010, 0x1b28: 0x0010, 0x1b29: 0x0010, 0x1b2a: 0x0010, 0x1b2b: 0x0010, 0x1b2c: 0x0010, 0x1b2d: 0x0010, 0x1b2e: 0x0010, 0x1b2f: 0x0010, 0x1b3a: 0x0010, 0x1b3b: 0x0010, 0x1b3c: 0x0010, 0x1b3d: 0x0010, 0x1b3e: 0x0010, // Block 0x6d, offset 0x1b40 0x1b69: 0x0024, 0x1b6a: 0x0024, 0x1b6b: 0x0024, 0x1b6c: 0x0024, 0x1b6d: 0x0024, 0x1b6e: 0x0024, 0x1b6f: 0x2000, 0x1b70: 0x2000, 0x1b71: 0x0024, 0x1b72: 0x0024, 0x1b73: 0x2000, 0x1b74: 0x2000, 0x1b75: 0x0024, 0x1b76: 0x0024, // Block 0x6e, offset 0x1b80 0x1b83: 0x0024, 0x1b8c: 0x0024, 0x1b8d: 0x2000, 0x1ba0: 0x0010, 0x1ba1: 0x0010, 0x1ba2: 0x0010, 0x1ba3: 0x0010, 0x1ba4: 0x0010, 0x1ba5: 0x0010, 0x1ba6: 0x0010, 0x1ba7: 0x0010, 0x1ba8: 0x0010, 0x1ba9: 0x0010, 0x1baa: 0x0010, 0x1bab: 0x0010, 0x1bac: 0x0010, 0x1bad: 0x0010, 0x1bae: 0x0010, 0x1baf: 0x0010, 0x1bb1: 0x0010, 0x1bb2: 0x0010, 0x1bb3: 0x0010, 0x1bba: 0x0010, 0x1bbc: 0x0024, 0x1bbe: 0x0010, 0x1bbf: 0x0010, // Block 0x6f, offset 0x1bc0 0x1bf0: 0x0024, 0x1bf2: 0x0024, 0x1bf3: 0x0024, 0x1bf4: 0x0024, 0x1bf7: 0x0024, 0x1bf8: 0x0024, 0x1bfe: 0x0024, 0x1bff: 0x0024, // Block 0x70, offset 0x1c00 0x1c01: 0x0024, 0x1c20: 0x0010, 0x1c21: 0x0010, 0x1c22: 0x0010, 0x1c23: 0x0010, 0x1c24: 0x0010, 0x1c25: 0x0010, 0x1c26: 0x0010, 0x1c27: 0x0010, 0x1c28: 0x0010, 0x1c29: 0x0010, 0x1c2a: 0x0010, 0x1c2b: 0x2000, 0x1c2c: 0x0024, 0x1c2d: 0x0024, 0x1c2e: 0x2000, 0x1c2f: 0x2000, 0x1c35: 0x2000, 0x1c36: 0x0044, // Block 0x71, offset 0x1c40 0x1c40: 0x0010, 0x1c41: 0x0010, 0x1c42: 0x0010, 0x1c43: 0x0010, 0x1c44: 0x0010, 0x1c45: 0x0010, 0x1c46: 0x0010, 0x1c47: 0x0010, 0x1c48: 0x0010, 0x1c49: 0x0010, 0x1c4a: 0x0010, 0x1c4b: 0x0010, 0x1c4c: 0x0010, 0x1c4d: 0x0010, 0x1c4e: 0x0010, 0x1c4f: 0x0010, 0x1c50: 0x0010, 0x1c51: 0x0010, 0x1c52: 0x0010, 0x1c53: 0x0010, 0x1c54: 0x0010, 0x1c55: 0x0010, 0x1c56: 0x0010, 0x1c57: 0x0010, 0x1c58: 0x0010, 0x1c59: 0x0010, 0x1c5a: 0x0010, 0x1c63: 0x2000, 0x1c64: 0x2000, 0x1c65: 0x0024, 0x1c66: 0x2000, 0x1c67: 0x2000, 0x1c68: 0x0024, 0x1c69: 0x2000, 0x1c6a: 0x2000, 0x1c6c: 0x2000, 0x1c6d: 0x0024, // Block 0x72, offset 0x1c80 0x1c80: 0x0200, 0x1c81: 0x0400, 0x1c82: 0x0400, 0x1c83: 0x0400, 0x1c84: 0x0400, 0x1c85: 0x0400, 0x1c86: 0x0400, 0x1c87: 0x0400, 0x1c88: 0x0400, 0x1c89: 0x0400, 0x1c8a: 0x0400, 0x1c8b: 0x0400, 0x1c8c: 0x0400, 0x1c8d: 0x0400, 0x1c8e: 0x0400, 0x1c8f: 0x0400, 0x1c90: 0x0400, 0x1c91: 0x0400, 0x1c92: 0x0400, 0x1c93: 0x0400, 0x1c94: 0x0400, 0x1c95: 0x0400, 0x1c96: 0x0400, 0x1c97: 0x0400, 0x1c98: 0x0400, 0x1c99: 0x0400, 0x1c9a: 0x0400, 0x1c9b: 0x0400, 0x1c9c: 0x0200, 0x1c9d: 0x0400, 0x1c9e: 0x0400, 0x1c9f: 0x0400, 0x1ca0: 0x0400, 0x1ca1: 0x0400, 0x1ca2: 0x0400, 0x1ca3: 0x0400, 0x1ca4: 0x0400, 0x1ca5: 0x0400, 0x1ca6: 0x0400, 0x1ca7: 0x0400, 0x1ca8: 0x0400, 0x1ca9: 0x0400, 0x1caa: 0x0400, 0x1cab: 0x0400, 0x1cac: 0x0400, 0x1cad: 0x0400, 0x1cae: 0x0400, 0x1caf: 0x0400, 0x1cb0: 0x0400, 0x1cb1: 0x0400, 0x1cb2: 0x0400, 0x1cb3: 0x0400, 0x1cb4: 0x0400, 0x1cb5: 0x0400, 0x1cb6: 0x0400, 0x1cb7: 0x0400, 0x1cb8: 0x0200, 0x1cb9: 0x0400, 0x1cba: 0x0400, 0x1cbb: 0x0400, 0x1cbc: 0x0400, 0x1cbd: 0x0400, 0x1cbe: 0x0400, 0x1cbf: 0x0400, // Block 0x73, offset 0x1cc0 0x1cc0: 0x0400, 0x1cc1: 0x0400, 0x1cc2: 0x0400, 0x1cc3: 0x0400, 0x1cc4: 0x0400, 0x1cc5: 0x0400, 0x1cc6: 0x0400, 0x1cc7: 0x0400, 0x1cc8: 0x0400, 0x1cc9: 0x0400, 0x1cca: 0x0400, 0x1ccb: 0x0400, 0x1ccc: 0x0400, 0x1ccd: 0x0400, 0x1cce: 0x0400, 0x1ccf: 0x0400, 0x1cd0: 0x0400, 0x1cd1: 0x0400, 0x1cd2: 0x0400, 0x1cd3: 0x0400, 0x1cd4: 0x0200, 0x1cd5: 0x0400, 0x1cd6: 0x0400, 0x1cd7: 0x0400, 0x1cd8: 0x0400, 0x1cd9: 0x0400, 0x1cda: 0x0400, 0x1cdb: 0x0400, 0x1cdc: 0x0400, 0x1cdd: 0x0400, 0x1cde: 0x0400, 0x1cdf: 0x0400, 0x1ce0: 0x0400, 0x1ce1: 0x0400, 0x1ce2: 0x0400, 0x1ce3: 0x0400, 0x1ce4: 0x0400, 0x1ce5: 0x0400, 0x1ce6: 0x0400, 0x1ce7: 0x0400, 0x1ce8: 0x0400, 0x1ce9: 0x0400, 0x1cea: 0x0400, 0x1ceb: 0x0400, 0x1cec: 0x0400, 0x1ced: 0x0400, 0x1cee: 0x0400, 0x1cef: 0x0400, 0x1cf0: 0x0200, 0x1cf1: 0x0400, 0x1cf2: 0x0400, 0x1cf3: 0x0400, 0x1cf4: 0x0400, 0x1cf5: 0x0400, 0x1cf6: 0x0400, 0x1cf7: 0x0400, 0x1cf8: 0x0400, 0x1cf9: 0x0400, 0x1cfa: 0x0400, 0x1cfb: 0x0400, 0x1cfc: 0x0400, 0x1cfd: 0x0400, 0x1cfe: 0x0400, 0x1cff: 0x0400, // Block 0x74, offset 0x1d00 0x1d00: 0x0400, 0x1d01: 0x0400, 0x1d02: 0x0400, 0x1d03: 0x0400, 0x1d04: 0x0400, 0x1d05: 0x0400, 0x1d06: 0x0400, 0x1d07: 0x0400, 0x1d08: 0x0400, 0x1d09: 0x0400, 0x1d0a: 0x0400, 0x1d0b: 0x0400, 0x1d0c: 0x0200, 0x1d0d: 0x0400, 0x1d0e: 0x0400, 0x1d0f: 0x0400, 0x1d10: 0x0400, 0x1d11: 0x0400, 0x1d12: 0x0400, 0x1d13: 0x0400, 0x1d14: 0x0400, 0x1d15: 0x0400, 0x1d16: 0x0400, 0x1d17: 0x0400, 0x1d18: 0x0400, 0x1d19: 0x0400, 0x1d1a: 0x0400, 0x1d1b: 0x0400, 0x1d1c: 0x0400, 0x1d1d: 0x0400, 0x1d1e: 0x0400, 0x1d1f: 0x0400, 0x1d20: 0x0400, 0x1d21: 0x0400, 0x1d22: 0x0400, 0x1d23: 0x0400, 0x1d24: 0x0400, 0x1d25: 0x0400, 0x1d26: 0x0400, 0x1d27: 0x0400, 0x1d28: 0x0200, 0x1d29: 0x0400, 0x1d2a: 0x0400, 0x1d2b: 0x0400, 0x1d2c: 0x0400, 0x1d2d: 0x0400, 0x1d2e: 0x0400, 0x1d2f: 0x0400, 0x1d30: 0x0400, 0x1d31: 0x0400, 0x1d32: 0x0400, 0x1d33: 0x0400, 0x1d34: 0x0400, 0x1d35: 0x0400, 0x1d36: 0x0400, 0x1d37: 0x0400, 0x1d38: 0x0400, 0x1d39: 0x0400, 0x1d3a: 0x0400, 0x1d3b: 0x0400, 0x1d3c: 0x0400, 0x1d3d: 0x0400, 0x1d3e: 0x0400, 0x1d3f: 0x0400, // Block 0x75, offset 0x1d40 0x1d40: 0x0400, 0x1d41: 0x0400, 0x1d42: 0x0400, 0x1d43: 0x0400, 0x1d44: 0x0200, 0x1d45: 0x0400, 0x1d46: 0x0400, 0x1d47: 0x0400, 0x1d48: 0x0400, 0x1d49: 0x0400, 0x1d4a: 0x0400, 0x1d4b: 0x0400, 0x1d4c: 0x0400, 0x1d4d: 0x0400, 0x1d4e: 0x0400, 0x1d4f: 0x0400, 0x1d50: 0x0400, 0x1d51: 0x0400, 0x1d52: 0x0400, 0x1d53: 0x0400, 0x1d54: 0x0400, 0x1d55: 0x0400, 0x1d56: 0x0400, 0x1d57: 0x0400, 0x1d58: 0x0400, 0x1d59: 0x0400, 0x1d5a: 0x0400, 0x1d5b: 0x0400, 0x1d5c: 0x0400, 0x1d5d: 0x0400, 0x1d5e: 0x0400, 0x1d5f: 0x0400, 0x1d60: 0x0200, 0x1d61: 0x0400, 0x1d62: 0x0400, 0x1d63: 0x0400, 0x1d64: 0x0400, 0x1d65: 0x0400, 0x1d66: 0x0400, 0x1d67: 0x0400, 0x1d68: 0x0400, 0x1d69: 0x0400, 0x1d6a: 0x0400, 0x1d6b: 0x0400, 0x1d6c: 0x0400, 0x1d6d: 0x0400, 0x1d6e: 0x0400, 0x1d6f: 0x0400, 0x1d70: 0x0400, 0x1d71: 0x0400, 0x1d72: 0x0400, 0x1d73: 0x0400, 0x1d74: 0x0400, 0x1d75: 0x0400, 0x1d76: 0x0400, 0x1d77: 0x0400, 0x1d78: 0x0400, 0x1d79: 0x0400, 0x1d7a: 0x0400, 0x1d7b: 0x0400, 0x1d7c: 0x0200, 0x1d7d: 0x0400, 0x1d7e: 0x0400, 0x1d7f: 0x0400, // Block 0x76, offset 0x1d80 0x1d80: 0x0400, 0x1d81: 0x0400, 0x1d82: 0x0400, 0x1d83: 0x0400, 0x1d84: 0x0400, 0x1d85: 0x0400, 0x1d86: 0x0400, 0x1d87: 0x0400, 0x1d88: 0x0400, 0x1d89: 0x0400, 0x1d8a: 0x0400, 0x1d8b: 0x0400, 0x1d8c: 0x0400, 0x1d8d: 0x0400, 0x1d8e: 0x0400, 0x1d8f: 0x0400, 0x1d90: 0x0400, 0x1d91: 0x0400, 0x1d92: 0x0400, 0x1d93: 0x0400, 0x1d94: 0x0400, 0x1d95: 0x0400, 0x1d96: 0x0400, 0x1d97: 0x0400, 0x1d98: 0x0200, 0x1d99: 0x0400, 0x1d9a: 0x0400, 0x1d9b: 0x0400, 0x1d9c: 0x0400, 0x1d9d: 0x0400, 0x1d9e: 0x0400, 0x1d9f: 0x0400, 0x1da0: 0x0400, 0x1da1: 0x0400, 0x1da2: 0x0400, 0x1da3: 0x0400, 0x1da4: 0x0400, 0x1da5: 0x0400, 0x1da6: 0x0400, 0x1da7: 0x0400, 0x1da8: 0x0400, 0x1da9: 0x0400, 0x1daa: 0x0400, 0x1dab: 0x0400, 0x1dac: 0x0400, 0x1dad: 0x0400, 0x1dae: 0x0400, 0x1daf: 0x0400, 0x1db0: 0x0400, 0x1db1: 0x0400, 0x1db2: 0x0400, 0x1db3: 0x0400, 0x1db4: 0x0200, 0x1db5: 0x0400, 0x1db6: 0x0400, 0x1db7: 0x0400, 0x1db8: 0x0400, 0x1db9: 0x0400, 0x1dba: 0x0400, 0x1dbb: 0x0400, 0x1dbc: 0x0400, 0x1dbd: 0x0400, 0x1dbe: 0x0400, 0x1dbf: 0x0400, // Block 0x77, offset 0x1dc0 0x1dc0: 0x0400, 0x1dc1: 0x0400, 0x1dc2: 0x0400, 0x1dc3: 0x0400, 0x1dc4: 0x0400, 0x1dc5: 0x0400, 0x1dc6: 0x0400, 0x1dc7: 0x0400, 0x1dc8: 0x0400, 0x1dc9: 0x0400, 0x1dca: 0x0400, 0x1dcb: 0x0400, 0x1dcc: 0x0400, 0x1dcd: 0x0400, 0x1dce: 0x0400, 0x1dcf: 0x0400, 0x1dd0: 0x0200, 0x1dd1: 0x0400, 0x1dd2: 0x0400, 0x1dd3: 0x0400, 0x1dd4: 0x0400, 0x1dd5: 0x0400, 0x1dd6: 0x0400, 0x1dd7: 0x0400, 0x1dd8: 0x0400, 0x1dd9: 0x0400, 0x1dda: 0x0400, 0x1ddb: 0x0400, 0x1ddc: 0x0400, 0x1ddd: 0x0400, 0x1dde: 0x0400, 0x1ddf: 0x0400, 0x1de0: 0x0400, 0x1de1: 0x0400, 0x1de2: 0x0400, 0x1de3: 0x0400, 0x1de4: 0x0400, 0x1de5: 0x0400, 0x1de6: 0x0400, 0x1de7: 0x0400, 0x1de8: 0x0400, 0x1de9: 0x0400, 0x1dea: 0x0400, 0x1deb: 0x0400, 0x1dec: 0x0200, 0x1ded: 0x0400, 0x1dee: 0x0400, 0x1def: 0x0400, 0x1df0: 0x0400, 0x1df1: 0x0400, 0x1df2: 0x0400, 0x1df3: 0x0400, 0x1df4: 0x0400, 0x1df5: 0x0400, 0x1df6: 0x0400, 0x1df7: 0x0400, 0x1df8: 0x0400, 0x1df9: 0x0400, 0x1dfa: 0x0400, 0x1dfb: 0x0400, 0x1dfc: 0x0400, 0x1dfd: 0x0400, 0x1dfe: 0x0400, 0x1dff: 0x0400, // Block 0x78, offset 0x1e00 0x1e00: 0x0400, 0x1e01: 0x0400, 0x1e02: 0x0400, 0x1e03: 0x0400, 0x1e04: 0x0400, 0x1e05: 0x0400, 0x1e06: 0x0400, 0x1e07: 0x0400, 0x1e08: 0x0200, 0x1e09: 0x0400, 0x1e0a: 0x0400, 0x1e0b: 0x0400, 0x1e0c: 0x0400, 0x1e0d: 0x0400, 0x1e0e: 0x0400, 0x1e0f: 0x0400, 0x1e10: 0x0400, 0x1e11: 0x0400, 0x1e12: 0x0400, 0x1e13: 0x0400, 0x1e14: 0x0400, 0x1e15: 0x0400, 0x1e16: 0x0400, 0x1e17: 0x0400, 0x1e18: 0x0400, 0x1e19: 0x0400, 0x1e1a: 0x0400, 0x1e1b: 0x0400, 0x1e1c: 0x0400, 0x1e1d: 0x0400, 0x1e1e: 0x0400, 0x1e1f: 0x0400, 0x1e20: 0x0400, 0x1e21: 0x0400, 0x1e22: 0x0400, 0x1e23: 0x0400, 0x1e24: 0x0200, 0x1e25: 0x0400, 0x1e26: 0x0400, 0x1e27: 0x0400, 0x1e28: 0x0400, 0x1e29: 0x0400, 0x1e2a: 0x0400, 0x1e2b: 0x0400, 0x1e2c: 0x0400, 0x1e2d: 0x0400, 0x1e2e: 0x0400, 0x1e2f: 0x0400, 0x1e30: 0x0400, 0x1e31: 0x0400, 0x1e32: 0x0400, 0x1e33: 0x0400, 0x1e34: 0x0400, 0x1e35: 0x0400, 0x1e36: 0x0400, 0x1e37: 0x0400, 0x1e38: 0x0400, 0x1e39: 0x0400, 0x1e3a: 0x0400, 0x1e3b: 0x0400, 0x1e3c: 0x0400, 0x1e3d: 0x0400, 0x1e3e: 0x0400, 0x1e3f: 0x0400, // Block 0x79, offset 0x1e40 0x1e40: 0x0400, 0x1e41: 0x0400, 0x1e42: 0x0400, 0x1e43: 0x0400, 0x1e44: 0x0400, 0x1e45: 0x0400, 0x1e46: 0x0400, 0x1e47: 0x0400, 0x1e48: 0x0200, 0x1e49: 0x0400, 0x1e4a: 0x0400, 0x1e4b: 0x0400, 0x1e4c: 0x0400, 0x1e4d: 0x0400, 0x1e4e: 0x0400, 0x1e4f: 0x0400, 0x1e50: 0x0400, 0x1e51: 0x0400, 0x1e52: 0x0400, 0x1e53: 0x0400, 0x1e54: 0x0400, 0x1e55: 0x0400, 0x1e56: 0x0400, 0x1e57: 0x0400, 0x1e58: 0x0400, 0x1e59: 0x0400, 0x1e5a: 0x0400, 0x1e5b: 0x0400, 0x1e5c: 0x0400, 0x1e5d: 0x0400, 0x1e5e: 0x0400, 0x1e5f: 0x0400, 0x1e60: 0x0400, 0x1e61: 0x0400, 0x1e62: 0x0400, 0x1e63: 0x0400, 0x1e70: 0x8000, 0x1e71: 0x8000, 0x1e72: 0x8000, 0x1e73: 0x8000, 0x1e74: 0x8000, 0x1e75: 0x8000, 0x1e76: 0x8000, 0x1e77: 0x8000, 0x1e78: 0x8000, 0x1e79: 0x8000, 0x1e7a: 0x8000, 0x1e7b: 0x8000, 0x1e7c: 0x8000, 0x1e7d: 0x8000, 0x1e7e: 0x8000, 0x1e7f: 0x8000, // Block 0x7a, offset 0x1e80 0x1e80: 0x8000, 0x1e81: 0x8000, 0x1e82: 0x8000, 0x1e83: 0x8000, 0x1e84: 0x8000, 0x1e85: 0x8000, 0x1e86: 0x8000, 0x1e8b: 0x4000, 0x1e8c: 0x4000, 0x1e8d: 0x4000, 0x1e8e: 0x4000, 0x1e8f: 0x4000, 0x1e90: 0x4000, 0x1e91: 0x4000, 0x1e92: 0x4000, 0x1e93: 0x4000, 0x1e94: 0x4000, 0x1e95: 0x4000, 0x1e96: 0x4000, 0x1e97: 0x4000, 0x1e98: 0x4000, 0x1e99: 0x4000, 0x1e9a: 0x4000, 0x1e9b: 0x4000, 0x1e9c: 0x4000, 0x1e9d: 0x4000, 0x1e9e: 0x4000, 0x1e9f: 0x4000, 0x1ea0: 0x4000, 0x1ea1: 0x4000, 0x1ea2: 0x4000, 0x1ea3: 0x4000, 0x1ea4: 0x4000, 0x1ea5: 0x4000, 0x1ea6: 0x4000, 0x1ea7: 0x4000, 0x1ea8: 0x4000, 0x1ea9: 0x4000, 0x1eaa: 0x4000, 0x1eab: 0x4000, 0x1eac: 0x4000, 0x1ead: 0x4000, 0x1eae: 0x4000, 0x1eaf: 0x4000, 0x1eb0: 0x4000, 0x1eb1: 0x4000, 0x1eb2: 0x4000, 0x1eb3: 0x4000, 0x1eb4: 0x4000, 0x1eb5: 0x4000, 0x1eb6: 0x4000, 0x1eb7: 0x4000, 0x1eb8: 0x4000, 0x1eb9: 0x4000, 0x1eba: 0x4000, 0x1ebb: 0x4000, // Block 0x7b, offset 0x1ec0 0x1ede: 0x0024, // Block 0x7c, offset 0x1f00 0x1f00: 0x0024, 0x1f01: 0x0024, 0x1f02: 0x0024, 0x1f03: 0x0024, 0x1f04: 0x0024, 0x1f05: 0x0024, 0x1f06: 0x0024, 0x1f07: 0x0024, 0x1f08: 0x0024, 0x1f09: 0x0024, 0x1f0a: 0x0024, 0x1f0b: 0x0024, 0x1f0c: 0x0024, 0x1f0d: 0x0024, 0x1f0e: 0x0024, 0x1f0f: 0x0024, 0x1f20: 0x0024, 0x1f21: 0x0024, 0x1f22: 0x0024, 0x1f23: 0x0024, 0x1f24: 0x0024, 0x1f25: 0x0024, 0x1f26: 0x0024, 0x1f27: 0x0024, 0x1f28: 0x0024, 0x1f29: 0x0024, 0x1f2a: 0x0024, 0x1f2b: 0x0024, 0x1f2c: 0x0024, 0x1f2d: 0x0024, 0x1f2e: 0x0024, 0x1f2f: 0x0024, // Block 0x7d, offset 0x1f40 0x1f7f: 0x0002, // Block 0x7e, offset 0x1f80 0x1fb0: 0x0002, 0x1fb1: 0x0002, 0x1fb2: 0x0002, 0x1fb3: 0x0002, 0x1fb4: 0x0002, 0x1fb5: 0x0002, 0x1fb6: 0x0002, 0x1fb7: 0x0002, 0x1fb8: 0x0002, 0x1fb9: 0x0002, 0x1fba: 0x0002, 0x1fbb: 0x0002, // Block 0x7f, offset 0x1fc0 0x1ffd: 0x0024, // Block 0x80, offset 0x2000 0x2020: 0x0024, // Block 0x81, offset 0x2040 0x2076: 0x0024, 0x2077: 0x0024, 0x2078: 0x0024, 0x2079: 0x0024, 0x207a: 0x0024, // Block 0x82, offset 0x2080 0x2080: 0x0010, 0x2081: 0x0024, 0x2082: 0x0024, 0x2083: 0x0024, 0x2085: 0x0024, 0x2086: 0x0024, 0x208c: 0x0024, 0x208d: 0x0024, 0x208e: 0x0024, 0x208f: 0x0024, 0x2090: 0x0010, 0x2091: 0x0010, 0x2092: 0x0010, 0x2093: 0x0010, 0x2095: 0x0010, 0x2096: 0x0010, 0x2097: 0x0010, 0x2099: 0x0010, 0x209a: 0x0010, 0x209b: 0x0010, 0x209c: 0x0010, 0x209d: 0x0010, 0x209e: 0x0010, 0x209f: 0x0010, 0x20a0: 0x0010, 0x20a1: 0x0010, 0x20a2: 0x0010, 0x20a3: 0x0010, 0x20a4: 0x0010, 0x20a5: 0x0010, 0x20a6: 0x0010, 0x20a7: 0x0010, 0x20a8: 0x0010, 0x20a9: 0x0010, 0x20aa: 0x0010, 0x20ab: 0x0010, 0x20ac: 0x0010, 0x20ad: 0x0010, 0x20ae: 0x0010, 0x20af: 0x0010, 0x20b0: 0x0010, 0x20b1: 0x0010, 0x20b2: 0x0010, 0x20b3: 0x0010, 0x20b4: 0x0010, 0x20b5: 0x0010, 0x20b8: 0x0024, 0x20b9: 0x0024, 0x20ba: 0x0024, 0x20bf: 0x0044, // Block 0x83, offset 0x20c0 0x20e5: 0x0024, 0x20e6: 0x0024, // Block 0x84, offset 0x2100 0x2124: 0x0024, 0x2125: 0x0024, 0x2126: 0x0024, 0x2127: 0x0024, // Block 0x85, offset 0x2140 0x2169: 0x0024, 0x216a: 0x0024, 0x216b: 0x0024, 0x216c: 0x0024, 0x216d: 0x0024, // Block 0x86, offset 0x2180 0x21ab: 0x0024, 0x21ac: 0x0024, // Block 0x87, offset 0x21c0 0x21fa: 0x0024, 0x21fb: 0x0024, 0x21fc: 0x0024, 0x21fd: 0x0024, 0x21fe: 0x0024, 0x21ff: 0x0024, // Block 0x88, offset 0x2200 0x2206: 0x0024, 0x2207: 0x0024, 0x2208: 0x0024, 0x2209: 0x0024, 0x220a: 0x0024, 0x220b: 0x0024, 0x220c: 0x0024, 0x220d: 0x0024, 0x220e: 0x0024, 0x220f: 0x0024, 0x2210: 0x0024, // Block 0x89, offset 0x2240 0x2242: 0x0024, 0x2243: 0x0024, 0x2244: 0x0024, 0x2245: 0x0024, // Block 0x8a, offset 0x2280 0x2280: 0x2000, 0x2281: 0x0024, 0x2282: 0x2000, 0x22b8: 0x0024, 0x22b9: 0x0024, 0x22ba: 0x0024, 0x22bb: 0x0024, 0x22bc: 0x0024, 0x22bd: 0x0024, 0x22be: 0x0024, 0x22bf: 0x0024, // Block 0x8b, offset 0x22c0 0x22c0: 0x0024, 0x22c1: 0x0024, 0x22c2: 0x0024, 0x22c3: 0x0024, 0x22c4: 0x0024, 0x22c5: 0x0024, 0x22c6: 0x0024, 0x22f0: 0x0024, 0x22f3: 0x0024, 0x22f4: 0x0024, 0x22ff: 0x0024, // Block 0x8c, offset 0x2300 0x2300: 0x0024, 0x2301: 0x0024, 0x2302: 0x2000, 0x2330: 0x2000, 0x2331: 0x2000, 0x2332: 0x2000, 0x2333: 0x0024, 0x2334: 0x0024, 0x2335: 0x0024, 0x2336: 0x0024, 0x2337: 0x2000, 0x2338: 0x2000, 0x2339: 0x0024, 0x233a: 0x0024, 0x233d: 0x0800, // Block 0x8d, offset 0x2340 0x2342: 0x0024, 0x234d: 0x0800, // Block 0x8e, offset 0x2380 0x2380: 0x0024, 0x2381: 0x0024, 0x2382: 0x0024, 0x2383: 0x0010, 0x2384: 0x0010, 0x2385: 0x0010, 0x2386: 0x0010, 0x2387: 0x0010, 0x2388: 0x0010, 0x2389: 0x0010, 0x238a: 0x0010, 0x238b: 0x0010, 0x238c: 0x0010, 0x238d: 0x0010, 0x238e: 0x0010, 0x238f: 0x0010, 0x2390: 0x0010, 0x2391: 0x0010, 0x2392: 0x0010, 0x2393: 0x0010, 0x2394: 0x0010, 0x2395: 0x0010, 0x2396: 0x0010, 0x2397: 0x0010, 0x2398: 0x0010, 0x2399: 0x0010, 0x239a: 0x0010, 0x239b: 0x0010, 0x239c: 0x0010, 0x239d: 0x0010, 0x239e: 0x0010, 0x239f: 0x0010, 0x23a0: 0x0010, 0x23a1: 0x0010, 0x23a2: 0x0010, 0x23a3: 0x0010, 0x23a4: 0x0010, 0x23a5: 0x0010, 0x23a6: 0x0010, 0x23a7: 0x0024, 0x23a8: 0x0024, 0x23a9: 0x0024, 0x23aa: 0x0024, 0x23ab: 0x0024, 0x23ac: 0x2000, 0x23ad: 0x0024, 0x23ae: 0x0024, 0x23af: 0x0024, 0x23b0: 0x0024, 0x23b1: 0x0024, 0x23b2: 0x0024, 0x23b3: 0x0044, 0x23b4: 0x0024, // Block 0x8f, offset 0x23c0 0x23c4: 0x0010, 0x23c5: 0x2000, 0x23c6: 0x2000, 0x23c7: 0x0010, 0x23f3: 0x0024, // Block 0x90, offset 0x2400 0x2400: 0x0024, 0x2401: 0x0024, 0x2402: 0x2000, 0x2433: 0x2000, 0x2434: 0x2000, 0x2435: 0x2000, 0x2436: 0x0024, 0x2437: 0x0024, 0x2438: 0x0024, 0x2439: 0x0024, 0x243a: 0x0024, 0x243b: 0x0024, 0x243c: 0x0024, 0x243d: 0x0024, 0x243e: 0x0024, 0x243f: 0x2000, // Block 0x91, offset 0x2440 0x2440: 0x0024, 0x2442: 0x0800, 0x2443: 0x0800, 0x2449: 0x0024, 0x244a: 0x0024, 0x244b: 0x0024, 0x244c: 0x0024, 0x244e: 0x2000, 0x244f: 0x0024, // Block 0x92, offset 0x2480 0x24ac: 0x2000, 0x24ad: 0x2000, 0x24ae: 0x2000, 0x24af: 0x0024, 0x24b0: 0x0024, 0x24b1: 0x0024, 0x24b2: 0x2000, 0x24b3: 0x2000, 0x24b4: 0x0024, 0x24b5: 0x0024, 0x24b6: 0x0024, 0x24b7: 0x0024, 0x24be: 0x0024, // Block 0x93, offset 0x24c0 0x24c1: 0x0024, // Block 0x94, offset 0x2500 0x251f: 0x0024, 0x2520: 0x2000, 0x2521: 0x2000, 0x2522: 0x2000, 0x2523: 0x0024, 0x2524: 0x0024, 0x2525: 0x0024, 0x2526: 0x0024, 0x2527: 0x0024, 0x2528: 0x0024, 0x2529: 0x0024, 0x252a: 0x0024, // Block 0x95, offset 0x2540 0x2540: 0x0024, 0x2541: 0x0024, 0x2542: 0x2000, 0x2543: 0x2000, 0x257b: 0x0024, 0x257c: 0x0024, 0x257e: 0x0024, 0x257f: 0x2000, // Block 0x96, offset 0x2580 0x2580: 0x0024, 0x2581: 0x2000, 0x2582: 0x2000, 0x2583: 0x2000, 0x2584: 0x2000, 0x2587: 0x2000, 0x2588: 0x2000, 0x258b: 0x2000, 0x258c: 0x2000, 0x258d: 0x0024, 0x2597: 0x0024, 0x25a2: 0x2000, 0x25a3: 0x2000, 0x25a6: 0x0024, 0x25a7: 0x0024, 0x25a8: 0x0024, 0x25a9: 0x0024, 0x25aa: 0x0024, 0x25ab: 0x0024, 0x25ac: 0x0024, 0x25b0: 0x0024, 0x25b1: 0x0024, 0x25b2: 0x0024, 0x25b3: 0x0024, 0x25b4: 0x0024, // Block 0x97, offset 0x25c0 0x25c0: 0x0010, 0x25c1: 0x0010, 0x25c2: 0x0010, 0x25c3: 0x0010, 0x25c4: 0x0010, 0x25c5: 0x0010, 0x25c6: 0x0010, 0x25c7: 0x0010, 0x25c8: 0x0010, 0x25c9: 0x0010, 0x25cb: 0x0010, 0x25ce: 0x0010, 0x25d0: 0x0010, 0x25d1: 0x0010, 0x25d2: 0x0010, 0x25d3: 0x0010, 0x25d4: 0x0010, 0x25d5: 0x0010, 0x25d6: 0x0010, 0x25d7: 0x0010, 0x25d8: 0x0010, 0x25d9: 0x0010, 0x25da: 0x0010, 0x25db: 0x0010, 0x25dc: 0x0010, 0x25dd: 0x0010, 0x25de: 0x0010, 0x25df: 0x0010, 0x25e0: 0x0010, 0x25e1: 0x0010, 0x25e2: 0x0010, 0x25e3: 0x0010, 0x25e4: 0x0010, 0x25e5: 0x0010, 0x25e6: 0x0010, 0x25e7: 0x0010, 0x25e8: 0x0010, 0x25e9: 0x0010, 0x25ea: 0x0010, 0x25eb: 0x0010, 0x25ec: 0x0010, 0x25ed: 0x0010, 0x25ee: 0x0010, 0x25ef: 0x0010, 0x25f0: 0x0010, 0x25f1: 0x0010, 0x25f2: 0x0010, 0x25f3: 0x0010, 0x25f4: 0x0010, 0x25f5: 0x0010, 0x25f8: 0x0024, 0x25f9: 0x2000, 0x25fa: 0x2000, 0x25fb: 0x0024, 0x25fc: 0x0024, 0x25fd: 0x0024, 0x25fe: 0x0024, 0x25ff: 0x0024, // Block 0x98, offset 0x2600 0x2600: 0x0024, 0x2602: 0x0024, 0x2605: 0x0024, 0x2607: 0x0024, 0x2608: 0x0024, 0x2609: 0x0024, 0x260a: 0x2000, 0x260c: 0x2000, 0x260d: 0x2000, 0x260e: 0x0024, 0x260f: 0x0024, 0x2610: 0x0044, 0x2611: 0x0800, 0x2612: 0x0024, 0x2621: 0x0024, 0x2622: 0x0024, // Block 0x99, offset 0x2640 0x2675: 0x2000, 0x2676: 0x2000, 0x2677: 0x2000, 0x2678: 0x0024, 0x2679: 0x0024, 0x267a: 0x0024, 0x267b: 0x0024, 0x267c: 0x0024, 0x267d: 0x0024, 0x267e: 0x0024, 0x267f: 0x0024, // Block 0x9a, offset 0x2680 0x2680: 0x2000, 0x2681: 0x2000, 0x2682: 0x0024, 0x2683: 0x0024, 0x2684: 0x0024, 0x2685: 0x2000, 0x2686: 0x0024, 0x269e: 0x0024, // Block 0x9b, offset 0x26c0 0x26f0: 0x0024, 0x26f1: 0x2000, 0x26f2: 0x2000, 0x26f3: 0x0024, 0x26f4: 0x0024, 0x26f5: 0x0024, 0x26f6: 0x0024, 0x26f7: 0x0024, 0x26f8: 0x0024, 0x26f9: 0x2000, 0x26fa: 0x0024, 0x26fb: 0x2000, 0x26fc: 0x2000, 0x26fd: 0x0024, 0x26fe: 0x2000, 0x26ff: 0x0024, // Block 0x9c, offset 0x2700 0x2700: 0x0024, 0x2701: 0x2000, 0x2702: 0x0024, 0x2703: 0x0024, // Block 0x9d, offset 0x2740 0x276f: 0x0024, 0x2770: 0x2000, 0x2771: 0x2000, 0x2772: 0x0024, 0x2773: 0x0024, 0x2774: 0x0024, 0x2775: 0x0024, 0x2778: 0x2000, 0x2779: 0x2000, 0x277a: 0x2000, 0x277b: 0x2000, 0x277c: 0x0024, 0x277d: 0x0024, 0x277e: 0x2000, 0x277f: 0x0024, // Block 0x9e, offset 0x2780 0x2780: 0x0024, 0x279c: 0x0024, 0x279d: 0x0024, // Block 0x9f, offset 0x27c0 0x27f0: 0x2000, 0x27f1: 0x2000, 0x27f2: 0x2000, 0x27f3: 0x0024, 0x27f4: 0x0024, 0x27f5: 0x0024, 0x27f6: 0x0024, 0x27f7: 0x0024, 0x27f8: 0x0024, 0x27f9: 0x0024, 0x27fa: 0x0024, 0x27fb: 0x2000, 0x27fc: 0x2000, 0x27fd: 0x0024, 0x27fe: 0x2000, 0x27ff: 0x0024, // Block 0xa0, offset 0x2800 0x2800: 0x0024, // Block 0xa1, offset 0x2840 0x286b: 0x0024, 0x286c: 0x2000, 0x286d: 0x0024, 0x286e: 0x2000, 0x286f: 0x2000, 0x2870: 0x0024, 0x2871: 0x0024, 0x2872: 0x0024, 0x2873: 0x0024, 0x2874: 0x0024, 0x2875: 0x0024, 0x2876: 0x0024, 0x2877: 0x0024, // Block 0xa2, offset 0x2880 0x289d: 0x0024, 0x289e: 0x2000, 0x289f: 0x0024, 0x28a2: 0x0024, 0x28a3: 0x0024, 0x28a4: 0x0024, 0x28a5: 0x0024, 0x28a6: 0x2000, 0x28a7: 0x0024, 0x28a8: 0x0024, 0x28a9: 0x0024, 0x28aa: 0x0024, 0x28ab: 0x0024, // Block 0xa3, offset 0x28c0 0x28ec: 0x2000, 0x28ed: 0x2000, 0x28ee: 0x2000, 0x28ef: 0x0024, 0x28f0: 0x0024, 0x28f1: 0x0024, 0x28f2: 0x0024, 0x28f3: 0x0024, 0x28f4: 0x0024, 0x28f5: 0x0024, 0x28f6: 0x0024, 0x28f7: 0x0024, 0x28f8: 0x2000, 0x28f9: 0x0024, 0x28fa: 0x0024, // Block 0xa4, offset 0x2900 0x2900: 0x0010, 0x2901: 0x0010, 0x2902: 0x0010, 0x2903: 0x0010, 0x2904: 0x0010, 0x2905: 0x0010, 0x2906: 0x0010, 0x2909: 0x0010, 0x290c: 0x0010, 0x290d: 0x0010, 0x290e: 0x0010, 0x290f: 0x0010, 0x2910: 0x0010, 0x2911: 0x0010, 0x2912: 0x0010, 0x2913: 0x0010, 0x2915: 0x0010, 0x2916: 0x0010, 0x2918: 0x0010, 0x2919: 0x0010, 0x291a: 0x0010, 0x291b: 0x0010, 0x291c: 0x0010, 0x291d: 0x0010, 0x291e: 0x0010, 0x291f: 0x0010, 0x2920: 0x0010, 0x2921: 0x0010, 0x2922: 0x0010, 0x2923: 0x0010, 0x2924: 0x0010, 0x2925: 0x0010, 0x2926: 0x0010, 0x2927: 0x0010, 0x2928: 0x0010, 0x2929: 0x0010, 0x292a: 0x0010, 0x292b: 0x0010, 0x292c: 0x0010, 0x292d: 0x0010, 0x292e: 0x0010, 0x292f: 0x0010, 0x2930: 0x0024, 0x2931: 0x2000, 0x2932: 0x2000, 0x2933: 0x2000, 0x2934: 0x2000, 0x2935: 0x2000, 0x2937: 0x2000, 0x2938: 0x2000, 0x293b: 0x0024, 0x293c: 0x0024, 0x293d: 0x0024, 0x293e: 0x0044, 0x293f: 0x0800, // Block 0xa5, offset 0x2940 0x2940: 0x2000, 0x2941: 0x0800, 0x2942: 0x2000, 0x2943: 0x0024, // Block 0xa6, offset 0x2980 0x2991: 0x2000, 0x2992: 0x2000, 0x2993: 0x2000, 0x2994: 0x0024, 0x2995: 0x0024, 0x2996: 0x0024, 0x2997: 0x0024, 0x299a: 0x0024, 0x299b: 0x0024, 0x299c: 0x2000, 0x299d: 0x2000, 0x299e: 0x2000, 0x299f: 0x2000, 0x29a0: 0x0024, 0x29a4: 0x2000, // Block 0xa7, offset 0x29c0 0x29c0: 0x0010, 0x29c1: 0x0024, 0x29c2: 0x0024, 0x29c3: 0x0024, 0x29c4: 0x0024, 0x29c5: 0x0024, 0x29c6: 0x0024, 0x29c7: 0x0024, 0x29c8: 0x0024, 0x29c9: 0x0024, 0x29ca: 0x0024, 0x29cb: 0x0010, 0x29cc: 0x0010, 0x29cd: 0x0010, 0x29ce: 0x0010, 0x29cf: 0x0010, 0x29d0: 0x0010, 0x29d1: 0x0010, 0x29d2: 0x0010, 0x29d3: 0x0010, 0x29d4: 0x0010, 0x29d5: 0x0010, 0x29d6: 0x0010, 0x29d7: 0x0010, 0x29d8: 0x0010, 0x29d9: 0x0010, 0x29da: 0x0010, 0x29db: 0x0010, 0x29dc: 0x0010, 0x29dd: 0x0010, 0x29de: 0x0010, 0x29df: 0x0010, 0x29e0: 0x0010, 0x29e1: 0x0010, 0x29e2: 0x0010, 0x29e3: 0x0010, 0x29e4: 0x0010, 0x29e5: 0x0010, 0x29e6: 0x0010, 0x29e7: 0x0010, 0x29e8: 0x0010, 0x29e9: 0x0010, 0x29ea: 0x0010, 0x29eb: 0x0010, 0x29ec: 0x0010, 0x29ed: 0x0010, 0x29ee: 0x0010, 0x29ef: 0x0010, 0x29f0: 0x0010, 0x29f1: 0x0010, 0x29f2: 0x0010, 0x29f3: 0x0024, 0x29f4: 0x0024, 0x29f5: 0x0024, 0x29f6: 0x0024, 0x29f7: 0x0024, 0x29f8: 0x0024, 0x29f9: 0x2000, 0x29fb: 0x0024, 0x29fc: 0x0024, 0x29fd: 0x0024, 0x29fe: 0x0024, // Block 0xa8, offset 0x2a00 0x2a07: 0x0044, 0x2a10: 0x0010, 0x2a11: 0x0024, 0x2a12: 0x0024, 0x2a13: 0x0024, 0x2a14: 0x0024, 0x2a15: 0x0024, 0x2a16: 0x0024, 0x2a17: 0x2000, 0x2a18: 0x2000, 0x2a19: 0x0024, 0x2a1a: 0x0024, 0x2a1b: 0x0024, 0x2a1c: 0x0010, 0x2a1d: 0x0010, 0x2a1e: 0x0010, 0x2a1f: 0x0010, 0x2a20: 0x0010, 0x2a21: 0x0010, 0x2a22: 0x0010, 0x2a23: 0x0010, 0x2a24: 0x0010, 0x2a25: 0x0010, 0x2a26: 0x0010, 0x2a27: 0x0010, 0x2a28: 0x0010, 0x2a29: 0x0010, 0x2a2a: 0x0010, 0x2a2b: 0x0010, 0x2a2c: 0x0010, 0x2a2d: 0x0010, 0x2a2e: 0x0010, 0x2a2f: 0x0010, 0x2a30: 0x0010, 0x2a31: 0x0010, 0x2a32: 0x0010, 0x2a33: 0x0010, 0x2a34: 0x0010, 0x2a35: 0x0010, 0x2a36: 0x0010, 0x2a37: 0x0010, 0x2a38: 0x0010, 0x2a39: 0x0010, 0x2a3a: 0x0010, 0x2a3b: 0x0010, 0x2a3c: 0x0010, 0x2a3d: 0x0010, 0x2a3e: 0x0010, 0x2a3f: 0x0010, // Block 0xa9, offset 0x2a40 0x2a40: 0x0010, 0x2a41: 0x0010, 0x2a42: 0x0010, 0x2a43: 0x0010, 0x2a44: 0x0800, 0x2a45: 0x0800, 0x2a46: 0x0800, 0x2a47: 0x0800, 0x2a48: 0x0800, 0x2a49: 0x0800, 0x2a4a: 0x0024, 0x2a4b: 0x0024, 0x2a4c: 0x0024, 0x2a4d: 0x0024, 0x2a4e: 0x0024, 0x2a4f: 0x0024, 0x2a50: 0x0024, 0x2a51: 0x0024, 0x2a52: 0x0024, 0x2a53: 0x0024, 0x2a54: 0x0024, 0x2a55: 0x0024, 0x2a56: 0x0024, 0x2a57: 0x2000, 0x2a58: 0x0024, 0x2a59: 0x0044, // Block 0xaa, offset 0x2a80 0x2aa0: 0x0024, 0x2aa1: 0x2000, 0x2aa2: 0x0024, 0x2aa3: 0x0024, 0x2aa4: 0x0024, 0x2aa5: 0x2000, 0x2aa6: 0x0024, 0x2aa7: 0x2000, // Block 0xab, offset 0x2ac0 0x2aef: 0x2000, 0x2af0: 0x0024, 0x2af1: 0x0024, 0x2af2: 0x0024, 0x2af3: 0x0024, 0x2af4: 0x0024, 0x2af5: 0x0024, 0x2af6: 0x0024, 0x2af8: 0x0024, 0x2af9: 0x0024, 0x2afa: 0x0024, 0x2afb: 0x0024, 0x2afc: 0x0024, 0x2afd: 0x0024, 0x2afe: 0x2000, 0x2aff: 0x0024, // Block 0xac, offset 0x2b00 0x2b12: 0x0024, 0x2b13: 0x0024, 0x2b14: 0x0024, 0x2b15: 0x0024, 0x2b16: 0x0024, 0x2b17: 0x0024, 0x2b18: 0x0024, 0x2b19: 0x0024, 0x2b1a: 0x0024, 0x2b1b: 0x0024, 0x2b1c: 0x0024, 0x2b1d: 0x0024, 0x2b1e: 0x0024, 0x2b1f: 0x0024, 0x2b20: 0x0024, 0x2b21: 0x0024, 0x2b22: 0x0024, 0x2b23: 0x0024, 0x2b24: 0x0024, 0x2b25: 0x0024, 0x2b26: 0x0024, 0x2b27: 0x0024, 0x2b29: 0x2000, 0x2b2a: 0x0024, 0x2b2b: 0x0024, 0x2b2c: 0x0024, 0x2b2d: 0x0024, 0x2b2e: 0x0024, 0x2b2f: 0x0024, 0x2b30: 0x0024, 0x2b31: 0x2000, 0x2b32: 0x0024, 0x2b33: 0x0024, 0x2b34: 0x2000, 0x2b35: 0x0024, 0x2b36: 0x0024, // Block 0xad, offset 0x2b40 0x2b71: 0x0024, 0x2b72: 0x0024, 0x2b73: 0x0024, 0x2b74: 0x0024, 0x2b75: 0x0024, 0x2b76: 0x0024, 0x2b7a: 0x0024, 0x2b7c: 0x0024, 0x2b7d: 0x0024, 0x2b7f: 0x0024, // Block 0xae, offset 0x2b80 0x2b80: 0x0024, 0x2b81: 0x0024, 0x2b82: 0x0024, 0x2b83: 0x0024, 0x2b84: 0x0024, 0x2b85: 0x0024, 0x2b86: 0x0800, 0x2b87: 0x0024, // Block 0xaf, offset 0x2bc0 0x2bca: 0x2000, 0x2bcb: 0x2000, 0x2bcc: 0x2000, 0x2bcd: 0x2000, 0x2bce: 0x2000, 0x2bd0: 0x0024, 0x2bd1: 0x0024, 0x2bd3: 0x2000, 0x2bd4: 0x2000, 0x2bd5: 0x0024, 0x2bd6: 0x2000, 0x2bd7: 0x0024, // Block 0xb0, offset 0x2c00 0x2c33: 0x0024, 0x2c34: 0x0024, 0x2c35: 0x2000, 0x2c36: 0x2000, // Block 0xb1, offset 0x2c40 0x2c40: 0x0024, 0x2c41: 0x0024, 0x2c42: 0x0800, 0x2c43: 0x2000, 0x2c44: 0x0010, 0x2c45: 0x0010, 0x2c46: 0x0010, 0x2c47: 0x0010, 0x2c48: 0x0010, 0x2c49: 0x0010, 0x2c4a: 0x0010, 0x2c4b: 0x0010, 0x2c4c: 0x0010, 0x2c4d: 0x0010, 0x2c4e: 0x0010, 0x2c4f: 0x0010, 0x2c50: 0x0010, 0x2c52: 0x0010, 0x2c53: 0x0010, 0x2c54: 0x0010, 0x2c55: 0x0010, 0x2c56: 0x0010, 0x2c57: 0x0010, 0x2c58: 0x0010, 0x2c59: 0x0010, 0x2c5a: 0x0010, 0x2c5b: 0x0010, 0x2c5c: 0x0010, 0x2c5d: 0x0010, 0x2c5e: 0x0010, 0x2c5f: 0x0010, 0x2c60: 0x0010, 0x2c61: 0x0010, 0x2c62: 0x0010, 0x2c63: 0x0010, 0x2c64: 0x0010, 0x2c65: 0x0010, 0x2c66: 0x0010, 0x2c67: 0x0010, 0x2c68: 0x0010, 0x2c69: 0x0010, 0x2c6a: 0x0010, 0x2c6b: 0x0010, 0x2c6c: 0x0010, 0x2c6d: 0x0010, 0x2c6e: 0x0010, 0x2c6f: 0x0010, 0x2c70: 0x0010, 0x2c71: 0x0010, 0x2c72: 0x0010, 0x2c73: 0x0010, 0x2c74: 0x2000, 0x2c75: 0x2000, 0x2c76: 0x0024, 0x2c77: 0x0024, 0x2c78: 0x0024, 0x2c79: 0x0024, 0x2c7a: 0x0024, 0x2c7e: 0x2000, 0x2c7f: 0x2000, // Block 0xb2, offset 0x2c80 0x2c80: 0x0024, 0x2c81: 0x0024, 0x2c82: 0x0044, 0x2c9a: 0x0024, // Block 0xb3, offset 0x2cc0 0x2cf0: 0x0002, 0x2cf1: 0x0002, 0x2cf2: 0x0002, 0x2cf3: 0x0002, 0x2cf4: 0x0002, 0x2cf5: 0x0002, 0x2cf6: 0x0002, 0x2cf7: 0x0002, 0x2cf8: 0x0002, 0x2cf9: 0x0002, 0x2cfa: 0x0002, 0x2cfb: 0x0002, 0x2cfc: 0x0002, 0x2cfd: 0x0002, 0x2cfe: 0x0002, 0x2cff: 0x0002, // Block 0xb4, offset 0x2d00 0x2d00: 0x0024, 0x2d07: 0x0024, 0x2d08: 0x0024, 0x2d09: 0x0024, 0x2d0a: 0x0024, 0x2d0b: 0x0024, 0x2d0c: 0x0024, 0x2d0d: 0x0024, 0x2d0e: 0x0024, 0x2d0f: 0x0024, 0x2d10: 0x0024, 0x2d11: 0x0024, 0x2d12: 0x0024, 0x2d13: 0x0024, 0x2d14: 0x0024, 0x2d15: 0x0024, // Block 0xb5, offset 0x2d40 0x2d5e: 0x0024, 0x2d5f: 0x0024, 0x2d60: 0x0024, 0x2d61: 0x0024, 0x2d62: 0x0024, 0x2d63: 0x0024, 0x2d64: 0x0024, 0x2d65: 0x0024, 0x2d66: 0x0024, 0x2d67: 0x0024, 0x2d68: 0x0024, 0x2d69: 0x0024, 0x2d6a: 0x2000, 0x2d6b: 0x2000, 0x2d6c: 0x2000, 0x2d6d: 0x0024, 0x2d6e: 0x0024, 0x2d6f: 0x0024, // Block 0xb6, offset 0x2d80 0x2db0: 0x0024, 0x2db1: 0x0024, 0x2db2: 0x0024, 0x2db3: 0x0024, 0x2db4: 0x0024, // Block 0xb7, offset 0x2dc0 0x2df0: 0x0024, 0x2df1: 0x0024, 0x2df2: 0x0024, 0x2df3: 0x0024, 0x2df4: 0x0024, 0x2df5: 0x0024, 0x2df6: 0x0024, // Block 0xb8, offset 0x2e00 0x2e23: 0x8000, 0x2e27: 0x8000, 0x2e28: 0x8000, 0x2e29: 0x8000, 0x2e2a: 0x8000, // Block 0xb9, offset 0x2e40 0x2e4f: 0x0024, 0x2e51: 0x2000, 0x2e52: 0x2000, 0x2e53: 0x2000, 0x2e54: 0x2000, 0x2e55: 0x2000, 0x2e56: 0x2000, 0x2e57: 0x2000, 0x2e58: 0x2000, 0x2e59: 0x2000, 0x2e5a: 0x2000, 0x2e5b: 0x2000, 0x2e5c: 0x2000, 0x2e5d: 0x2000, 0x2e5e: 0x2000, 0x2e5f: 0x2000, 0x2e60: 0x2000, 0x2e61: 0x2000, 0x2e62: 0x2000, 0x2e63: 0x2000, 0x2e64: 0x2000, 0x2e65: 0x2000, 0x2e66: 0x2000, 0x2e67: 0x2000, 0x2e68: 0x2000, 0x2e69: 0x2000, 0x2e6a: 0x2000, 0x2e6b: 0x2000, 0x2e6c: 0x2000, 0x2e6d: 0x2000, 0x2e6e: 0x2000, 0x2e6f: 0x2000, 0x2e70: 0x2000, 0x2e71: 0x2000, 0x2e72: 0x2000, 0x2e73: 0x2000, 0x2e74: 0x2000, 0x2e75: 0x2000, 0x2e76: 0x2000, 0x2e77: 0x2000, 0x2e78: 0x2000, 0x2e79: 0x2000, 0x2e7a: 0x2000, 0x2e7b: 0x2000, 0x2e7c: 0x2000, 0x2e7d: 0x2000, 0x2e7e: 0x2000, 0x2e7f: 0x2000, // Block 0xba, offset 0x2e80 0x2e80: 0x2000, 0x2e81: 0x2000, 0x2e82: 0x2000, 0x2e83: 0x2000, 0x2e84: 0x2000, 0x2e85: 0x2000, 0x2e86: 0x2000, 0x2e87: 0x2000, 0x2e8f: 0x0024, 0x2e90: 0x0024, 0x2e91: 0x0024, 0x2e92: 0x0024, // Block 0xbb, offset 0x2ec0 0x2ee4: 0x0024, 0x2ef0: 0x0024, 0x2ef1: 0x0024, // Block 0xbc, offset 0x2f00 0x2f1d: 0x0024, 0x2f1e: 0x0024, 0x2f20: 0x0002, 0x2f21: 0x0002, 0x2f22: 0x0002, 0x2f23: 0x0002, // Block 0xbd, offset 0x2f40 0x2f40: 0x0024, 0x2f41: 0x0024, 0x2f42: 0x0024, 0x2f43: 0x0024, 0x2f44: 0x0024, 0x2f45: 0x0024, 0x2f46: 0x0024, 0x2f47: 0x0024, 0x2f48: 0x0024, 0x2f49: 0x0024, 0x2f4a: 0x0024, 0x2f4b: 0x0024, 0x2f4c: 0x0024, 0x2f4d: 0x0024, 0x2f4e: 0x0024, 0x2f4f: 0x0024, 0x2f50: 0x0024, 0x2f51: 0x0024, 0x2f52: 0x0024, 0x2f53: 0x0024, 0x2f54: 0x0024, 0x2f55: 0x0024, 0x2f56: 0x0024, 0x2f57: 0x0024, 0x2f58: 0x0024, 0x2f59: 0x0024, 0x2f5a: 0x0024, 0x2f5b: 0x0024, 0x2f5c: 0x0024, 0x2f5d: 0x0024, 0x2f5e: 0x0024, 0x2f5f: 0x0024, 0x2f60: 0x0024, 0x2f61: 0x0024, 0x2f62: 0x0024, 0x2f63: 0x0024, 0x2f64: 0x0024, 0x2f65: 0x0024, 0x2f66: 0x0024, 0x2f67: 0x0024, 0x2f68: 0x0024, 0x2f69: 0x0024, 0x2f6a: 0x0024, 0x2f6b: 0x0024, 0x2f6c: 0x0024, 0x2f6d: 0x0024, 0x2f70: 0x0024, 0x2f71: 0x0024, 0x2f72: 0x0024, 0x2f73: 0x0024, 0x2f74: 0x0024, 0x2f75: 0x0024, 0x2f76: 0x0024, 0x2f77: 0x0024, 0x2f78: 0x0024, 0x2f79: 0x0024, 0x2f7a: 0x0024, 0x2f7b: 0x0024, 0x2f7c: 0x0024, 0x2f7d: 0x0024, 0x2f7e: 0x0024, 0x2f7f: 0x0024, // Block 0xbe, offset 0x2f80 0x2f80: 0x0024, 0x2f81: 0x0024, 0x2f82: 0x0024, 0x2f83: 0x0024, 0x2f84: 0x0024, 0x2f85: 0x0024, 0x2f86: 0x0024, // Block 0xbf, offset 0x2fc0 0x2fe5: 0x0024, 0x2fe6: 0x0024, 0x2fe7: 0x0024, 0x2fe8: 0x0024, 0x2fe9: 0x0024, 0x2fed: 0x0024, 0x2fee: 0x0024, 0x2fef: 0x0024, 0x2ff0: 0x0024, 0x2ff1: 0x0024, 0x2ff2: 0x0024, 0x2ff3: 0x0002, 0x2ff4: 0x0002, 0x2ff5: 0x0002, 0x2ff6: 0x0002, 0x2ff7: 0x0002, 0x2ff8: 0x0002, 0x2ff9: 0x0002, 0x2ffa: 0x0002, 0x2ffb: 0x0024, 0x2ffc: 0x0024, 0x2ffd: 0x0024, 0x2ffe: 0x0024, 0x2fff: 0x0024, // Block 0xc0, offset 0x3000 0x3000: 0x0024, 0x3001: 0x0024, 0x3002: 0x0024, 0x3005: 0x0024, 0x3006: 0x0024, 0x3007: 0x0024, 0x3008: 0x0024, 0x3009: 0x0024, 0x300a: 0x0024, 0x300b: 0x0024, 0x302a: 0x0024, 0x302b: 0x0024, 0x302c: 0x0024, 0x302d: 0x0024, // Block 0xc1, offset 0x3040 0x3042: 0x0024, 0x3043: 0x0024, 0x3044: 0x0024, // Block 0xc2, offset 0x3080 0x3080: 0x0024, 0x3081: 0x0024, 0x3082: 0x0024, 0x3083: 0x0024, 0x3084: 0x0024, 0x3085: 0x0024, 0x3086: 0x0024, 0x3087: 0x0024, 0x3088: 0x0024, 0x3089: 0x0024, 0x308a: 0x0024, 0x308b: 0x0024, 0x308c: 0x0024, 0x308d: 0x0024, 0x308e: 0x0024, 0x308f: 0x0024, 0x3090: 0x0024, 0x3091: 0x0024, 0x3092: 0x0024, 0x3093: 0x0024, 0x3094: 0x0024, 0x3095: 0x0024, 0x3096: 0x0024, 0x3097: 0x0024, 0x3098: 0x0024, 0x3099: 0x0024, 0x309a: 0x0024, 0x309b: 0x0024, 0x309c: 0x0024, 0x309d: 0x0024, 0x309e: 0x0024, 0x309f: 0x0024, 0x30a0: 0x0024, 0x30a1: 0x0024, 0x30a2: 0x0024, 0x30a3: 0x0024, 0x30a4: 0x0024, 0x30a5: 0x0024, 0x30a6: 0x0024, 0x30a7: 0x0024, 0x30a8: 0x0024, 0x30a9: 0x0024, 0x30aa: 0x0024, 0x30ab: 0x0024, 0x30ac: 0x0024, 0x30ad: 0x0024, 0x30ae: 0x0024, 0x30af: 0x0024, 0x30b0: 0x0024, 0x30b1: 0x0024, 0x30b2: 0x0024, 0x30b3: 0x0024, 0x30b4: 0x0024, 0x30b5: 0x0024, 0x30b6: 0x0024, 0x30bb: 0x0024, 0x30bc: 0x0024, 0x30bd: 0x0024, 0x30be: 0x0024, 0x30bf: 0x0024, // Block 0xc3, offset 0x30c0 0x30c0: 0x0024, 0x30c1: 0x0024, 0x30c2: 0x0024, 0x30c3: 0x0024, 0x30c4: 0x0024, 0x30c5: 0x0024, 0x30c6: 0x0024, 0x30c7: 0x0024, 0x30c8: 0x0024, 0x30c9: 0x0024, 0x30ca: 0x0024, 0x30cb: 0x0024, 0x30cc: 0x0024, 0x30cd: 0x0024, 0x30ce: 0x0024, 0x30cf: 0x0024, 0x30d0: 0x0024, 0x30d1: 0x0024, 0x30d2: 0x0024, 0x30d3: 0x0024, 0x30d4: 0x0024, 0x30d5: 0x0024, 0x30d6: 0x0024, 0x30d7: 0x0024, 0x30d8: 0x0024, 0x30d9: 0x0024, 0x30da: 0x0024, 0x30db: 0x0024, 0x30dc: 0x0024, 0x30dd: 0x0024, 0x30de: 0x0024, 0x30df: 0x0024, 0x30e0: 0x0024, 0x30e1: 0x0024, 0x30e2: 0x0024, 0x30e3: 0x0024, 0x30e4: 0x0024, 0x30e5: 0x0024, 0x30e6: 0x0024, 0x30e7: 0x0024, 0x30e8: 0x0024, 0x30e9: 0x0024, 0x30ea: 0x0024, 0x30eb: 0x0024, 0x30ec: 0x0024, 0x30f5: 0x0024, // Block 0xc4, offset 0x3100 0x3104: 0x0024, 0x311b: 0x0024, 0x311c: 0x0024, 0x311d: 0x0024, 0x311e: 0x0024, 0x311f: 0x0024, 0x3121: 0x0024, 0x3122: 0x0024, 0x3123: 0x0024, 0x3124: 0x0024, 0x3125: 0x0024, 0x3126: 0x0024, 0x3127: 0x0024, 0x3128: 0x0024, 0x3129: 0x0024, 0x312a: 0x0024, 0x312b: 0x0024, 0x312c: 0x0024, 0x312d: 0x0024, 0x312e: 0x0024, 0x312f: 0x0024, // Block 0xc5, offset 0x3140 0x3140: 0x0024, 0x3141: 0x0024, 0x3142: 0x0024, 0x3143: 0x0024, 0x3144: 0x0024, 0x3145: 0x0024, 0x3146: 0x0024, 0x3148: 0x0024, 0x3149: 0x0024, 0x314a: 0x0024, 0x314b: 0x0024, 0x314c: 0x0024, 0x314d: 0x0024, 0x314e: 0x0024, 0x314f: 0x0024, 0x3150: 0x0024, 0x3151: 0x0024, 0x3152: 0x0024, 0x3153: 0x0024, 0x3154: 0x0024, 0x3155: 0x0024, 0x3156: 0x0024, 0x3157: 0x0024, 0x3158: 0x0024, 0x315b: 0x0024, 0x315c: 0x0024, 0x315d: 0x0024, 0x315e: 0x0024, 0x315f: 0x0024, 0x3160: 0x0024, 0x3161: 0x0024, 0x3163: 0x0024, 0x3164: 0x0024, 0x3166: 0x0024, 0x3167: 0x0024, 0x3168: 0x0024, 0x3169: 0x0024, 0x316a: 0x0024, // Block 0xc6, offset 0x3180 0x318f: 0x0024, // Block 0xc7, offset 0x31c0 0x31ee: 0x0024, // Block 0xc8, offset 0x3200 0x322c: 0x0024, 0x322d: 0x0024, 0x322e: 0x0024, 0x322f: 0x0024, // Block 0xc9, offset 0x3240 0x326e: 0x0024, 0x326f: 0x0024, // Block 0xca, offset 0x3280 0x32a3: 0x0024, 0x32a6: 0x0024, 0x32ae: 0x0024, 0x32af: 0x0024, 0x32b5: 0x0024, // Block 0xcb, offset 0x32c0 0x32d0: 0x0024, 0x32d1: 0x0024, 0x32d2: 0x0024, 0x32d3: 0x0024, 0x32d4: 0x0024, 0x32d5: 0x0024, 0x32d6: 0x0024, // Block 0xcc, offset 0x3300 0x3304: 0x0024, 0x3305: 0x0024, 0x3306: 0x0024, 0x3307: 0x0024, 0x3308: 0x0024, 0x3309: 0x0024, 0x330a: 0x0024, // Block 0xcd, offset 0x3340 0x3344: 0x0008, 0x336c: 0x0008, 0x336d: 0x0008, 0x336e: 0x0008, 0x336f: 0x0008, // Block 0xce, offset 0x3380 0x3394: 0x0008, 0x3395: 0x0008, 0x3396: 0x0008, 0x3397: 0x0008, 0x3398: 0x0008, 0x3399: 0x0008, 0x339a: 0x0008, 0x339b: 0x0008, 0x339c: 0x0008, 0x339d: 0x0008, 0x339e: 0x0008, 0x339f: 0x0008, 0x33af: 0x0008, 0x33b0: 0x0008, // Block 0xcf, offset 0x33c0 0x33c0: 0x0008, 0x33cf: 0x0008, 0x33d0: 0x0008, 0x33f6: 0x0008, 0x33f7: 0x0008, 0x33f8: 0x0008, 0x33f9: 0x0008, 0x33fa: 0x0008, 0x33fb: 0x0008, 0x33fc: 0x0008, 0x33fd: 0x0008, 0x33fe: 0x0008, 0x33ff: 0x0008, // Block 0xd0, offset 0x3400 0x3430: 0x0008, 0x3431: 0x0008, 0x343e: 0x0008, 0x343f: 0x0008, // Block 0xd1, offset 0x3440 0x344e: 0x0008, 0x3451: 0x0008, 0x3452: 0x0008, 0x3453: 0x0008, 0x3454: 0x0008, 0x3455: 0x0008, 0x3456: 0x0008, 0x3457: 0x0008, 0x3458: 0x0008, 0x3459: 0x0008, 0x345a: 0x0008, 0x346e: 0x0008, 0x346f: 0x0008, 0x3470: 0x0008, 0x3471: 0x0008, 0x3472: 0x0008, 0x3473: 0x0008, 0x3474: 0x0008, 0x3475: 0x0008, 0x3476: 0x0008, 0x3477: 0x0008, 0x3478: 0x0008, 0x3479: 0x0008, 0x347a: 0x0008, 0x347b: 0x0008, 0x347c: 0x0008, 0x347d: 0x0008, 0x347e: 0x0008, 0x347f: 0x0008, // Block 0xd2, offset 0x3480 0x3480: 0x0008, 0x3481: 0x0008, 0x3482: 0x0008, 0x3483: 0x0008, 0x3484: 0x0008, 0x3485: 0x0008, 0x3486: 0x0008, 0x3487: 0x0008, 0x3488: 0x0008, 0x3489: 0x0008, 0x348a: 0x0008, 0x348b: 0x0008, 0x348c: 0x0008, 0x348d: 0x0008, 0x348e: 0x0008, 0x348f: 0x0008, 0x3490: 0x0008, 0x3491: 0x0008, 0x3492: 0x0008, 0x3493: 0x0008, 0x3494: 0x0008, 0x3495: 0x0008, 0x3496: 0x0008, 0x3497: 0x0008, 0x3498: 0x0008, 0x3499: 0x0008, 0x349a: 0x0008, 0x349b: 0x0008, 0x349c: 0x0008, 0x349d: 0x0008, 0x349e: 0x0008, 0x349f: 0x0008, 0x34a0: 0x0008, 0x34a1: 0x0008, 0x34a2: 0x0008, 0x34a3: 0x0008, 0x34a4: 0x0008, 0x34a5: 0x0008, 0x34a6: 0x1000, 0x34a7: 0x1000, 0x34a8: 0x1000, 0x34a9: 0x1000, 0x34aa: 0x1000, 0x34ab: 0x1000, 0x34ac: 0x1000, 0x34ad: 0x1000, 0x34ae: 0x1000, 0x34af: 0x1000, 0x34b0: 0x1000, 0x34b1: 0x1000, 0x34b2: 0x1000, 0x34b3: 0x1000, 0x34b4: 0x1000, 0x34b5: 0x1000, 0x34b6: 0x1000, 0x34b7: 0x1000, 0x34b8: 0x1000, 0x34b9: 0x1000, 0x34ba: 0x1000, 0x34bb: 0x1000, 0x34bc: 0x1000, 0x34bd: 0x1000, 0x34be: 0x1000, 0x34bf: 0x1000, // Block 0xd3, offset 0x34c0 0x34c1: 0x0008, 0x34c2: 0x0008, 0x34c3: 0x0008, 0x34c4: 0x0008, 0x34c5: 0x0008, 0x34c6: 0x0008, 0x34c7: 0x0008, 0x34c8: 0x0008, 0x34c9: 0x0008, 0x34ca: 0x0008, 0x34cb: 0x0008, 0x34cc: 0x0008, 0x34cd: 0x0008, 0x34ce: 0x0008, 0x34cf: 0x0008, 0x34da: 0x0008, 0x34ef: 0x0008, 0x34f2: 0x0008, 0x34f3: 0x0008, 0x34f4: 0x0008, 0x34f5: 0x0008, 0x34f6: 0x0008, 0x34f7: 0x0008, 0x34f8: 0x0008, 0x34f9: 0x0008, 0x34fa: 0x0008, 0x34fc: 0x0008, 0x34fd: 0x0008, 0x34fe: 0x0008, 0x34ff: 0x0008, // Block 0xd4, offset 0x3500 0x3509: 0x0008, 0x350a: 0x0008, 0x350b: 0x0008, 0x350c: 0x0008, 0x350d: 0x0008, 0x350e: 0x0008, 0x350f: 0x0008, 0x3510: 0x0008, 0x3511: 0x0008, 0x3512: 0x0008, 0x3513: 0x0008, 0x3514: 0x0008, 0x3515: 0x0008, 0x3516: 0x0008, 0x3517: 0x0008, 0x3518: 0x0008, 0x3519: 0x0008, 0x351a: 0x0008, 0x351b: 0x0008, 0x351c: 0x0008, 0x351d: 0x0008, 0x351e: 0x0008, 0x351f: 0x0008, 0x3526: 0x0008, 0x3527: 0x0008, 0x3528: 0x0008, 0x3529: 0x0008, 0x352a: 0x0008, 0x352b: 0x0008, 0x352c: 0x0008, 0x352d: 0x0008, 0x352e: 0x0008, 0x352f: 0x0008, 0x3530: 0x0008, 0x3531: 0x0008, 0x3532: 0x0008, 0x3533: 0x0008, 0x3534: 0x0008, 0x3535: 0x0008, 0x3536: 0x0008, 0x3537: 0x0008, 0x3538: 0x0008, 0x3539: 0x0008, 0x353a: 0x0008, 0x353b: 0x0008, 0x353c: 0x0008, 0x353d: 0x0008, 0x353e: 0x0008, 0x353f: 0x0008, // Block 0xd5, offset 0x3540 0x3540: 0x0008, 0x3541: 0x0008, 0x3542: 0x0008, 0x3543: 0x0008, 0x3544: 0x0008, 0x3545: 0x0008, 0x3546: 0x0008, 0x3547: 0x0008, 0x3548: 0x0008, 0x3549: 0x0008, 0x354a: 0x0008, 0x354b: 0x0008, 0x354c: 0x0008, 0x354d: 0x0008, 0x354e: 0x0008, 0x354f: 0x0008, 0x3550: 0x0008, 0x3551: 0x0008, 0x3552: 0x0008, 0x3553: 0x0008, 0x3554: 0x0008, 0x3555: 0x0008, 0x3556: 0x0008, 0x3557: 0x0008, 0x3558: 0x0008, 0x3559: 0x0008, 0x355a: 0x0008, 0x355b: 0x0008, 0x355c: 0x0008, 0x355d: 0x0008, 0x355e: 0x0008, 0x355f: 0x0008, 0x3560: 0x0008, 0x3561: 0x0008, 0x3562: 0x0008, 0x3563: 0x0008, 0x3564: 0x0008, 0x3565: 0x0008, 0x3566: 0x0008, 0x3567: 0x0008, 0x3568: 0x0008, 0x3569: 0x0008, 0x356a: 0x0008, 0x356b: 0x0008, 0x356c: 0x0008, 0x356d: 0x0008, 0x356e: 0x0008, 0x356f: 0x0008, 0x3570: 0x0008, 0x3571: 0x0008, 0x3572: 0x0008, 0x3573: 0x0008, 0x3574: 0x0008, 0x3575: 0x0008, 0x3576: 0x0008, 0x3577: 0x0008, 0x3578: 0x0008, 0x3579: 0x0008, 0x357a: 0x0008, 0x357b: 0x0008, 0x357c: 0x0008, 0x357d: 0x0008, 0x357e: 0x0008, 0x357f: 0x0008, // Block 0xd6, offset 0x3580 0x3580: 0x0008, 0x3581: 0x0008, 0x3582: 0x0008, 0x3583: 0x0008, 0x3584: 0x0008, 0x3585: 0x0008, 0x3586: 0x0008, 0x3587: 0x0008, 0x3588: 0x0008, 0x3589: 0x0008, 0x358a: 0x0008, 0x358b: 0x0008, 0x358c: 0x0008, 0x358d: 0x0008, 0x358e: 0x0008, 0x358f: 0x0008, 0x3590: 0x0008, 0x3591: 0x0008, 0x3592: 0x0008, 0x3593: 0x0008, 0x3594: 0x0008, 0x3595: 0x0008, 0x3596: 0x0008, 0x3597: 0x0008, 0x3598: 0x0008, 0x3599: 0x0008, 0x359a: 0x0008, 0x359b: 0x0008, 0x359c: 0x0008, 0x359d: 0x0008, 0x359e: 0x0008, 0x359f: 0x0008, 0x35a0: 0x0008, 0x35a1: 0x0008, 0x35a4: 0x0008, 0x35a5: 0x0008, 0x35a6: 0x0008, 0x35a7: 0x0008, 0x35a8: 0x0008, 0x35a9: 0x0008, 0x35aa: 0x0008, 0x35ab: 0x0008, 0x35ac: 0x0008, 0x35ad: 0x0008, 0x35ae: 0x0008, 0x35af: 0x0008, 0x35b0: 0x0008, 0x35b1: 0x0008, 0x35b2: 0x0008, 0x35b3: 0x0008, 0x35b4: 0x0008, 0x35b5: 0x0008, 0x35b6: 0x0008, 0x35b7: 0x0008, 0x35b8: 0x0008, 0x35b9: 0x0008, 0x35ba: 0x0008, 0x35bb: 0x0008, 0x35bc: 0x0008, 0x35bd: 0x0008, 0x35be: 0x0008, 0x35bf: 0x0008, // Block 0xd7, offset 0x35c0 0x35c0: 0x0008, 0x35c1: 0x0008, 0x35c2: 0x0008, 0x35c3: 0x0008, 0x35c4: 0x0008, 0x35c5: 0x0008, 0x35c6: 0x0008, 0x35c7: 0x0008, 0x35c8: 0x0008, 0x35c9: 0x0008, 0x35ca: 0x0008, 0x35cb: 0x0008, 0x35cc: 0x0008, 0x35cd: 0x0008, 0x35ce: 0x0008, 0x35cf: 0x0008, 0x35d0: 0x0008, 0x35d1: 0x0008, 0x35d2: 0x0008, 0x35d3: 0x0008, 0x35d6: 0x0008, 0x35d7: 0x0008, 0x35d9: 0x0008, 0x35da: 0x0008, 0x35db: 0x0008, 0x35de: 0x0008, 0x35df: 0x0008, 0x35e0: 0x0008, 0x35e1: 0x0008, 0x35e2: 0x0008, 0x35e3: 0x0008, 0x35e4: 0x0008, 0x35e5: 0x0008, 0x35e6: 0x0008, 0x35e7: 0x0008, 0x35e8: 0x0008, 0x35e9: 0x0008, 0x35ea: 0x0008, 0x35eb: 0x0008, 0x35ec: 0x0008, 0x35ed: 0x0008, 0x35ee: 0x0008, 0x35ef: 0x0008, 0x35f0: 0x0008, 0x35f1: 0x0008, 0x35f2: 0x0008, 0x35f3: 0x0008, 0x35f4: 0x0008, 0x35f5: 0x0008, 0x35f6: 0x0008, 0x35f7: 0x0008, 0x35f8: 0x0008, 0x35f9: 0x0008, 0x35fa: 0x0008, 0x35fb: 0x0008, 0x35fc: 0x0008, 0x35fd: 0x0008, 0x35fe: 0x0008, 0x35ff: 0x0008, // Block 0xd8, offset 0x3600 0x3600: 0x0008, 0x3601: 0x0008, 0x3602: 0x0008, 0x3603: 0x0008, 0x3604: 0x0008, 0x3605: 0x0008, 0x3606: 0x0008, 0x3607: 0x0008, 0x3608: 0x0008, 0x3609: 0x0008, 0x360a: 0x0008, 0x360b: 0x0008, 0x360c: 0x0008, 0x360d: 0x0008, 0x360e: 0x0008, 0x360f: 0x0008, 0x3610: 0x0008, 0x3611: 0x0008, 0x3612: 0x0008, 0x3613: 0x0008, 0x3614: 0x0008, 0x3615: 0x0008, 0x3616: 0x0008, 0x3617: 0x0008, 0x3618: 0x0008, 0x3619: 0x0008, 0x361a: 0x0008, 0x361b: 0x0008, 0x361c: 0x0008, 0x361d: 0x0008, 0x361e: 0x0008, 0x361f: 0x0008, 0x3620: 0x0008, 0x3621: 0x0008, 0x3622: 0x0008, 0x3623: 0x0008, 0x3624: 0x0008, 0x3625: 0x0008, 0x3626: 0x0008, 0x3627: 0x0008, 0x3628: 0x0008, 0x3629: 0x0008, 0x362a: 0x0008, 0x362b: 0x0008, 0x362c: 0x0008, 0x362d: 0x0008, 0x362e: 0x0008, 0x362f: 0x0008, 0x3630: 0x0008, 0x3633: 0x0008, 0x3634: 0x0008, 0x3635: 0x0008, 0x3637: 0x0008, 0x3638: 0x0008, 0x3639: 0x0008, 0x363a: 0x0008, 0x363b: 0x0024, 0x363c: 0x0024, 0x363d: 0x0024, 0x363e: 0x0024, 0x363f: 0x0024, // Block 0xd9, offset 0x3640 0x3640: 0x0008, 0x3641: 0x0008, 0x3642: 0x0008, 0x3643: 0x0008, 0x3644: 0x0008, 0x3645: 0x0008, 0x3646: 0x0008, 0x3647: 0x0008, 0x3648: 0x0008, 0x3649: 0x0008, 0x364a: 0x0008, 0x364b: 0x0008, 0x364c: 0x0008, 0x364d: 0x0008, 0x364e: 0x0008, 0x364f: 0x0008, 0x3650: 0x0008, 0x3651: 0x0008, 0x3652: 0x0008, 0x3653: 0x0008, 0x3654: 0x0008, 0x3655: 0x0008, 0x3656: 0x0008, 0x3657: 0x0008, 0x3658: 0x0008, 0x3659: 0x0008, 0x365a: 0x0008, 0x365b: 0x0008, 0x365c: 0x0008, 0x365d: 0x0008, 0x365e: 0x0008, 0x365f: 0x0008, 0x3660: 0x0008, 0x3661: 0x0008, 0x3662: 0x0008, 0x3663: 0x0008, 0x3664: 0x0008, 0x3665: 0x0008, 0x3666: 0x0008, 0x3667: 0x0008, 0x3668: 0x0008, 0x3669: 0x0008, 0x366a: 0x0008, 0x366b: 0x0008, 0x366c: 0x0008, 0x366d: 0x0008, 0x366e: 0x0008, 0x366f: 0x0008, 0x3670: 0x0008, 0x3671: 0x0008, 0x3672: 0x0008, 0x3673: 0x0008, 0x3674: 0x0008, 0x3675: 0x0008, 0x3676: 0x0008, 0x3677: 0x0008, 0x3678: 0x0008, 0x3679: 0x0008, 0x367a: 0x0008, 0x367b: 0x0008, 0x367c: 0x0008, 0x367d: 0x0008, 0x367f: 0x0008, // Block 0xda, offset 0x3680 0x3680: 0x0008, 0x3681: 0x0008, 0x3682: 0x0008, 0x3683: 0x0008, 0x3684: 0x0008, 0x3685: 0x0008, 0x3686: 0x0008, 0x3687: 0x0008, 0x3688: 0x0008, 0x3689: 0x0008, 0x368a: 0x0008, 0x368b: 0x0008, 0x368c: 0x0008, 0x368d: 0x0008, 0x368e: 0x0008, 0x368f: 0x0008, 0x3690: 0x0008, 0x3691: 0x0008, 0x3692: 0x0008, 0x3693: 0x0008, 0x3694: 0x0008, 0x3695: 0x0008, 0x3696: 0x0008, 0x3697: 0x0008, 0x3698: 0x0008, 0x3699: 0x0008, 0x369a: 0x0008, 0x369b: 0x0008, 0x369c: 0x0008, 0x369d: 0x0008, 0x369e: 0x0008, 0x369f: 0x0008, 0x36a0: 0x0008, 0x36a1: 0x0008, 0x36a2: 0x0008, 0x36a3: 0x0008, 0x36a4: 0x0008, 0x36a5: 0x0008, 0x36a6: 0x0008, 0x36a7: 0x0008, 0x36a8: 0x0008, 0x36a9: 0x0008, 0x36aa: 0x0008, 0x36ab: 0x0008, 0x36ac: 0x0008, 0x36ad: 0x0008, 0x36ae: 0x0008, 0x36af: 0x0008, 0x36b0: 0x0008, 0x36b1: 0x0008, 0x36b2: 0x0008, 0x36b3: 0x0008, 0x36b4: 0x0008, 0x36b5: 0x0008, 0x36b6: 0x0008, 0x36b7: 0x0008, 0x36b8: 0x0008, 0x36b9: 0x0008, 0x36ba: 0x0008, 0x36bb: 0x0008, 0x36bc: 0x0008, 0x36bd: 0x0008, // Block 0xdb, offset 0x36c0 0x36c9: 0x0008, 0x36ca: 0x0008, 0x36cb: 0x0008, 0x36cc: 0x0008, 0x36cd: 0x0008, 0x36ce: 0x0008, 0x36d0: 0x0008, 0x36d1: 0x0008, 0x36d2: 0x0008, 0x36d3: 0x0008, 0x36d4: 0x0008, 0x36d5: 0x0008, 0x36d6: 0x0008, 0x36d7: 0x0008, 0x36d8: 0x0008, 0x36d9: 0x0008, 0x36da: 0x0008, 0x36db: 0x0008, 0x36dc: 0x0008, 0x36dd: 0x0008, 0x36de: 0x0008, 0x36df: 0x0008, 0x36e0: 0x0008, 0x36e1: 0x0008, 0x36e2: 0x0008, 0x36e3: 0x0008, 0x36e4: 0x0008, 0x36e5: 0x0008, 0x36e6: 0x0008, 0x36e7: 0x0008, 0x36ef: 0x0008, 0x36f0: 0x0008, 0x36f3: 0x0008, 0x36f4: 0x0008, 0x36f5: 0x0008, 0x36f6: 0x0008, 0x36f7: 0x0008, 0x36f8: 0x0008, 0x36f9: 0x0008, 0x36fa: 0x0008, // Block 0xdc, offset 0x3700 0x3707: 0x0008, 0x370a: 0x0008, 0x370b: 0x0008, 0x370c: 0x0008, 0x370d: 0x0008, 0x3710: 0x0008, 0x3715: 0x0008, 0x3716: 0x0008, 0x3724: 0x0008, 0x3725: 0x0008, 0x3728: 0x0008, 0x3731: 0x0008, 0x3732: 0x0008, 0x373c: 0x0008, // Block 0xdd, offset 0x3740 0x3742: 0x0008, 0x3743: 0x0008, 0x3744: 0x0008, 0x3751: 0x0008, 0x3752: 0x0008, 0x3753: 0x0008, 0x375c: 0x0008, 0x375d: 0x0008, 0x375e: 0x0008, 0x3761: 0x0008, 0x3763: 0x0008, 0x3768: 0x0008, 0x376f: 0x0008, 0x3773: 0x0008, 0x377a: 0x0008, 0x377b: 0x0008, 0x377c: 0x0008, 0x377d: 0x0008, 0x377e: 0x0008, 0x377f: 0x0008, // Block 0xde, offset 0x3780 0x3780: 0x0008, 0x3781: 0x0008, 0x3782: 0x0008, 0x3783: 0x0008, 0x3784: 0x0008, 0x3785: 0x0008, 0x3786: 0x0008, 0x3787: 0x0008, 0x3788: 0x0008, 0x3789: 0x0008, 0x378a: 0x0008, 0x378b: 0x0008, 0x378c: 0x0008, 0x378d: 0x0008, 0x378e: 0x0008, 0x378f: 0x0008, // Block 0xdf, offset 0x37c0 0x37c0: 0x0008, 0x37c1: 0x0008, 0x37c2: 0x0008, 0x37c3: 0x0008, 0x37c4: 0x0008, 0x37c5: 0x0008, 0x37cb: 0x0008, 0x37cc: 0x0008, 0x37cd: 0x0008, 0x37ce: 0x0008, 0x37cf: 0x0008, 0x37d0: 0x0008, 0x37d1: 0x0008, 0x37d2: 0x0008, 0x37d5: 0x0008, 0x37d6: 0x0008, 0x37d7: 0x0008, 0x37d8: 0x0008, 0x37d9: 0x0008, 0x37da: 0x0008, 0x37db: 0x0008, 0x37dc: 0x0008, 0x37dd: 0x0008, 0x37de: 0x0008, 0x37df: 0x0008, 0x37e0: 0x0008, 0x37e1: 0x0008, 0x37e2: 0x0008, 0x37e3: 0x0008, 0x37e4: 0x0008, 0x37e5: 0x0008, 0x37e9: 0x0008, 0x37eb: 0x0008, 0x37ec: 0x0008, 0x37ed: 0x0008, 0x37ee: 0x0008, 0x37ef: 0x0008, 0x37f0: 0x0008, 0x37f3: 0x0008, 0x37f4: 0x0008, 0x37f5: 0x0008, 0x37f6: 0x0008, 0x37f7: 0x0008, 0x37f8: 0x0008, 0x37f9: 0x0008, 0x37fa: 0x0008, 0x37fb: 0x0008, 0x37fc: 0x0008, 0x37fd: 0x0008, 0x37fe: 0x0008, 0x37ff: 0x0008, // Block 0xe0, offset 0x3800 0x381a: 0x0008, 0x381b: 0x0008, 0x381c: 0x0008, 0x381d: 0x0008, 0x381e: 0x0008, 0x381f: 0x0008, 0x3820: 0x0008, 0x3821: 0x0008, 0x3822: 0x0008, 0x3823: 0x0008, 0x3824: 0x0008, 0x3825: 0x0008, 0x3826: 0x0008, 0x3827: 0x0008, 0x3828: 0x0008, 0x3829: 0x0008, 0x382a: 0x0008, 0x382b: 0x0008, 0x382c: 0x0008, 0x382d: 0x0008, 0x382e: 0x0008, 0x382f: 0x0008, 0x3830: 0x0008, 0x3831: 0x0008, 0x3832: 0x0008, 0x3833: 0x0008, 0x3834: 0x0008, 0x3835: 0x0008, 0x3836: 0x0008, 0x3837: 0x0008, 0x3838: 0x0008, 0x3839: 0x0008, 0x383a: 0x0008, 0x383b: 0x0008, 0x383c: 0x0008, 0x383d: 0x0008, 0x383e: 0x0008, 0x383f: 0x0008, // Block 0xe1, offset 0x3840 0x384c: 0x0008, 0x384d: 0x0008, 0x384e: 0x0008, 0x384f: 0x0008, // Block 0xe2, offset 0x3880 0x3888: 0x0008, 0x3889: 0x0008, 0x388a: 0x0008, 0x388b: 0x0008, 0x388c: 0x0008, 0x388d: 0x0008, 0x388e: 0x0008, 0x388f: 0x0008, 0x389a: 0x0008, 0x389b: 0x0008, 0x389c: 0x0008, 0x389d: 0x0008, 0x389e: 0x0008, 0x389f: 0x0008, // Block 0xe3, offset 0x38c0 0x38c8: 0x0008, 0x38c9: 0x0008, 0x38ca: 0x0008, 0x38cb: 0x0008, 0x38cc: 0x0008, 0x38cd: 0x0008, 0x38ce: 0x0008, 0x38cf: 0x0008, 0x38ee: 0x0008, 0x38ef: 0x0008, 0x38fc: 0x0008, 0x38fd: 0x0008, 0x38fe: 0x0008, 0x38ff: 0x0008, // Block 0xe4, offset 0x3900 0x3902: 0x0008, 0x3903: 0x0008, 0x3904: 0x0008, 0x3905: 0x0008, 0x3906: 0x0008, 0x3907: 0x0008, 0x3908: 0x0008, 0x3909: 0x0008, 0x390a: 0x0008, 0x390b: 0x0008, 0x390c: 0x0008, 0x390d: 0x0008, 0x390e: 0x0008, 0x390f: 0x0008, 0x3919: 0x0008, 0x391a: 0x0008, 0x391b: 0x0008, 0x391c: 0x0008, 0x391d: 0x0008, 0x391e: 0x0008, 0x391f: 0x0008, 0x3920: 0x0008, 0x3921: 0x0008, 0x3922: 0x0008, 0x3923: 0x0008, 0x3924: 0x0008, 0x3925: 0x0008, 0x3926: 0x0008, 0x3927: 0x0008, 0x3928: 0x0008, 0x3929: 0x0008, 0x392a: 0x0008, 0x392b: 0x0008, 0x392c: 0x0008, 0x392d: 0x0008, 0x392e: 0x0008, 0x392f: 0x0008, 0x3930: 0x0008, 0x3931: 0x0008, 0x3932: 0x0008, 0x3933: 0x0008, 0x3934: 0x0008, 0x3935: 0x0008, 0x3936: 0x0008, 0x3937: 0x0008, 0x3938: 0x0008, 0x3939: 0x0008, 0x393a: 0x0008, 0x393b: 0x0008, 0x393c: 0x0008, 0x393d: 0x0008, 0x393e: 0x0008, 0x393f: 0x0008, // Block 0xe5, offset 0x3940 0x394c: 0x0008, 0x394d: 0x0008, 0x394e: 0x0008, 0x394f: 0x0008, 0x3950: 0x0008, 0x3951: 0x0008, 0x3952: 0x0008, 0x3953: 0x0008, 0x3954: 0x0008, 0x3955: 0x0008, 0x3956: 0x0008, 0x3957: 0x0008, 0x3958: 0x0008, 0x3959: 0x0008, 0x395a: 0x0008, 0x395b: 0x0008, 0x395c: 0x0008, 0x395d: 0x0008, 0x395e: 0x0008, 0x395f: 0x0008, 0x3960: 0x0008, 0x3961: 0x0008, 0x3962: 0x0008, 0x3963: 0x0008, 0x3964: 0x0008, 0x3965: 0x0008, 0x3966: 0x0008, 0x3967: 0x0008, 0x3968: 0x0008, 0x3969: 0x0008, 0x396a: 0x0008, 0x396b: 0x0008, 0x396c: 0x0008, 0x396d: 0x0008, 0x396e: 0x0008, 0x396f: 0x0008, 0x3970: 0x0008, 0x3971: 0x0008, 0x3972: 0x0008, 0x3973: 0x0008, 0x3974: 0x0008, 0x3975: 0x0008, 0x3976: 0x0008, 0x3977: 0x0008, 0x3978: 0x0008, 0x3979: 0x0008, 0x397a: 0x0008, 0x397c: 0x0008, 0x397d: 0x0008, 0x397e: 0x0008, 0x397f: 0x0008, // Block 0xe6, offset 0x3980 0x3980: 0x0008, 0x3981: 0x0008, 0x3982: 0x0008, 0x3983: 0x0008, 0x3984: 0x0008, 0x3985: 0x0008, 0x3987: 0x0008, 0x3988: 0x0008, 0x3989: 0x0008, 0x398a: 0x0008, 0x398b: 0x0008, 0x398c: 0x0008, 0x398d: 0x0008, 0x398e: 0x0008, 0x398f: 0x0008, 0x3990: 0x0008, 0x3991: 0x0008, 0x3992: 0x0008, 0x3993: 0x0008, 0x3994: 0x0008, 0x3995: 0x0008, 0x3996: 0x0008, 0x3997: 0x0008, 0x3998: 0x0008, 0x3999: 0x0008, 0x399a: 0x0008, 0x399b: 0x0008, 0x399c: 0x0008, 0x399d: 0x0008, 0x399e: 0x0008, 0x399f: 0x0008, 0x39a0: 0x0008, 0x39a1: 0x0008, 0x39a2: 0x0008, 0x39a3: 0x0008, 0x39a4: 0x0008, 0x39a5: 0x0008, 0x39a6: 0x0008, 0x39a7: 0x0008, 0x39a8: 0x0008, 0x39a9: 0x0008, 0x39aa: 0x0008, 0x39ab: 0x0008, 0x39ac: 0x0008, 0x39ad: 0x0008, 0x39ae: 0x0008, 0x39af: 0x0008, 0x39b0: 0x0008, 0x39b1: 0x0008, 0x39b2: 0x0008, 0x39b3: 0x0008, 0x39b4: 0x0008, 0x39b5: 0x0008, 0x39b6: 0x0008, 0x39b7: 0x0008, 0x39b8: 0x0008, 0x39b9: 0x0008, 0x39ba: 0x0008, 0x39bb: 0x0008, 0x39bc: 0x0008, 0x39bd: 0x0008, 0x39be: 0x0008, 0x39bf: 0x0008, // Block 0xe7, offset 0x39c0 0x39d8: 0x0008, 0x39d9: 0x0008, 0x39da: 0x0008, 0x39db: 0x0008, 0x39dc: 0x0008, 0x39dd: 0x0008, 0x39de: 0x0008, 0x39df: 0x0008, 0x39ee: 0x0008, 0x39ef: 0x0008, 0x39f0: 0x0008, 0x39f1: 0x0008, 0x39f2: 0x0008, 0x39f3: 0x0008, 0x39f4: 0x0008, 0x39f5: 0x0008, 0x39f6: 0x0008, 0x39f7: 0x0008, 0x39f8: 0x0008, 0x39f9: 0x0008, 0x39fa: 0x0008, 0x39fb: 0x0008, 0x39fc: 0x0008, 0x39fd: 0x0008, 0x39fe: 0x0008, 0x39ff: 0x0008, // Block 0xe8, offset 0x3a00 0x3a00: 0x0002, 0x3a01: 0x0002, 0x3a02: 0x0002, 0x3a03: 0x0002, 0x3a04: 0x0002, 0x3a05: 0x0002, 0x3a06: 0x0002, 0x3a07: 0x0002, 0x3a08: 0x0002, 0x3a09: 0x0002, 0x3a0a: 0x0002, 0x3a0b: 0x0002, 0x3a0c: 0x0002, 0x3a0d: 0x0002, 0x3a0e: 0x0002, 0x3a0f: 0x0002, 0x3a10: 0x0002, 0x3a11: 0x0002, 0x3a12: 0x0002, 0x3a13: 0x0002, 0x3a14: 0x0002, 0x3a15: 0x0002, 0x3a16: 0x0002, 0x3a17: 0x0002, 0x3a18: 0x0002, 0x3a19: 0x0002, 0x3a1a: 0x0002, 0x3a1b: 0x0002, 0x3a1c: 0x0002, 0x3a1d: 0x0002, 0x3a1e: 0x0002, 0x3a1f: 0x0002, 0x3a20: 0x0024, 0x3a21: 0x0024, 0x3a22: 0x0024, 0x3a23: 0x0024, 0x3a24: 0x0024, 0x3a25: 0x0024, 0x3a26: 0x0024, 0x3a27: 0x0024, 0x3a28: 0x0024, 0x3a29: 0x0024, 0x3a2a: 0x0024, 0x3a2b: 0x0024, 0x3a2c: 0x0024, 0x3a2d: 0x0024, 0x3a2e: 0x0024, 0x3a2f: 0x0024, 0x3a30: 0x0024, 0x3a31: 0x0024, 0x3a32: 0x0024, 0x3a33: 0x0024, 0x3a34: 0x0024, 0x3a35: 0x0024, 0x3a36: 0x0024, 0x3a37: 0x0024, 0x3a38: 0x0024, 0x3a39: 0x0024, 0x3a3a: 0x0024, 0x3a3b: 0x0024, 0x3a3c: 0x0024, 0x3a3d: 0x0024, 0x3a3e: 0x0024, 0x3a3f: 0x0024, // Block 0xe9, offset 0x3a40 0x3a40: 0x0002, 0x3a41: 0x0002, 0x3a42: 0x0002, 0x3a43: 0x0002, 0x3a44: 0x0002, 0x3a45: 0x0002, 0x3a46: 0x0002, 0x3a47: 0x0002, 0x3a48: 0x0002, 0x3a49: 0x0002, 0x3a4a: 0x0002, 0x3a4b: 0x0002, 0x3a4c: 0x0002, 0x3a4d: 0x0002, 0x3a4e: 0x0002, 0x3a4f: 0x0002, 0x3a50: 0x0002, 0x3a51: 0x0002, 0x3a52: 0x0002, 0x3a53: 0x0002, 0x3a54: 0x0002, 0x3a55: 0x0002, 0x3a56: 0x0002, 0x3a57: 0x0002, 0x3a58: 0x0002, 0x3a59: 0x0002, 0x3a5a: 0x0002, 0x3a5b: 0x0002, 0x3a5c: 0x0002, 0x3a5d: 0x0002, 0x3a5e: 0x0002, 0x3a5f: 0x0002, 0x3a60: 0x0002, 0x3a61: 0x0002, 0x3a62: 0x0002, 0x3a63: 0x0002, 0x3a64: 0x0002, 0x3a65: 0x0002, 0x3a66: 0x0002, 0x3a67: 0x0002, 0x3a68: 0x0002, 0x3a69: 0x0002, 0x3a6a: 0x0002, 0x3a6b: 0x0002, 0x3a6c: 0x0002, 0x3a6d: 0x0002, 0x3a6e: 0x0002, 0x3a6f: 0x0002, 0x3a70: 0x0002, 0x3a71: 0x0002, 0x3a72: 0x0002, 0x3a73: 0x0002, 0x3a74: 0x0002, 0x3a75: 0x0002, 0x3a76: 0x0002, 0x3a77: 0x0002, 0x3a78: 0x0002, 0x3a79: 0x0002, 0x3a7a: 0x0002, 0x3a7b: 0x0002, 0x3a7c: 0x0002, 0x3a7d: 0x0002, 0x3a7e: 0x0002, 0x3a7f: 0x0002, // Block 0xea, offset 0x3a80 0x3a80: 0x0024, 0x3a81: 0x0024, 0x3a82: 0x0024, 0x3a83: 0x0024, 0x3a84: 0x0024, 0x3a85: 0x0024, 0x3a86: 0x0024, 0x3a87: 0x0024, 0x3a88: 0x0024, 0x3a89: 0x0024, 0x3a8a: 0x0024, 0x3a8b: 0x0024, 0x3a8c: 0x0024, 0x3a8d: 0x0024, 0x3a8e: 0x0024, 0x3a8f: 0x0024, 0x3a90: 0x0024, 0x3a91: 0x0024, 0x3a92: 0x0024, 0x3a93: 0x0024, 0x3a94: 0x0024, 0x3a95: 0x0024, 0x3a96: 0x0024, 0x3a97: 0x0024, 0x3a98: 0x0024, 0x3a99: 0x0024, 0x3a9a: 0x0024, 0x3a9b: 0x0024, 0x3a9c: 0x0024, 0x3a9d: 0x0024, 0x3a9e: 0x0024, 0x3a9f: 0x0024, 0x3aa0: 0x0024, 0x3aa1: 0x0024, 0x3aa2: 0x0024, 0x3aa3: 0x0024, 0x3aa4: 0x0024, 0x3aa5: 0x0024, 0x3aa6: 0x0024, 0x3aa7: 0x0024, 0x3aa8: 0x0024, 0x3aa9: 0x0024, 0x3aaa: 0x0024, 0x3aab: 0x0024, 0x3aac: 0x0024, 0x3aad: 0x0024, 0x3aae: 0x0024, 0x3aaf: 0x0024, 0x3ab0: 0x0002, 0x3ab1: 0x0002, 0x3ab2: 0x0002, 0x3ab3: 0x0002, 0x3ab4: 0x0002, 0x3ab5: 0x0002, 0x3ab6: 0x0002, 0x3ab7: 0x0002, 0x3ab8: 0x0002, 0x3ab9: 0x0002, 0x3aba: 0x0002, 0x3abb: 0x0002, 0x3abc: 0x0002, 0x3abd: 0x0002, 0x3abe: 0x0002, 0x3abf: 0x0002, } // graphemesIndex: 25 blocks, 1600 entries, 1600 bytes // Block 0 is the zero block. var graphemesIndex = [1600]property{ // Block 0x0, offset 0x0 // Block 0x1, offset 0x40 // Block 0x2, offset 0x80 // Block 0x3, offset 0xc0 0xc2: 0x01, 0xcc: 0x02, 0xcd: 0x03, 0xd2: 0x04, 0xd6: 0x05, 0xd7: 0x06, 0xd8: 0x07, 0xd9: 0x08, 0xdb: 0x09, 0xdc: 0x0a, 0xdd: 0x0b, 0xde: 0x0c, 0xdf: 0x0d, 0xe0: 0x02, 0xe1: 0x03, 0xe2: 0x04, 0xe3: 0x05, 0xea: 0x06, 0xeb: 0x07, 0xec: 0x08, 0xed: 0x09, 0xef: 0x0a, 0xf0: 0x14, 0xf3: 0x16, // Block 0x4, offset 0x100 0x120: 0x0e, 0x121: 0x0f, 0x122: 0x10, 0x123: 0x11, 0x124: 0x12, 0x125: 0x13, 0x126: 0x14, 0x127: 0x15, 0x128: 0x16, 0x129: 0x17, 0x12a: 0x18, 0x12b: 0x19, 0x12c: 0x1a, 0x12d: 0x1b, 0x12e: 0x1c, 0x12f: 0x1d, 0x130: 0x1e, 0x131: 0x1f, 0x132: 0x20, 0x133: 0x21, 0x134: 0x22, 0x135: 0x23, 0x136: 0x24, 0x137: 0x25, 0x138: 0x26, 0x139: 0x27, 0x13a: 0x28, 0x13b: 0x29, 0x13c: 0x2a, 0x13d: 0x2b, 0x13e: 0x2c, 0x13f: 0x2d, // Block 0x5, offset 0x140 0x140: 0x2e, 0x141: 0x2f, 0x142: 0x30, 0x144: 0x31, 0x145: 0x32, 0x146: 0x33, 0x147: 0x34, 0x14d: 0x35, 0x15c: 0x36, 0x15d: 0x37, 0x15e: 0x38, 0x15f: 0x39, 0x160: 0x3a, 0x162: 0x3b, 0x164: 0x3c, 0x168: 0x3d, 0x169: 0x3e, 0x16a: 0x3f, 0x16b: 0x40, 0x16c: 0x41, 0x16d: 0x42, 0x16e: 0x43, 0x16f: 0x44, 0x170: 0x45, 0x173: 0x46, 0x177: 0x02, // Block 0x6, offset 0x180 0x180: 0x47, 0x181: 0x48, 0x183: 0x49, 0x184: 0x4a, 0x186: 0x4b, 0x18c: 0x4c, 0x18f: 0x4d, 0x193: 0x4e, 0x196: 0x4f, 0x197: 0x50, 0x198: 0x51, 0x199: 0x52, 0x19a: 0x53, 0x19b: 0x54, 0x19c: 0x55, 0x19d: 0x56, 0x19e: 0x57, 0x1a4: 0x58, 0x1ac: 0x59, 0x1ad: 0x5a, 0x1b3: 0x5b, 0x1b5: 0x5c, 0x1b7: 0x5d, // Block 0x7, offset 0x1c0 0x1c0: 0x5e, 0x1c2: 0x5f, 0x1ca: 0x60, // Block 0x8, offset 0x200 0x219: 0x61, 0x21a: 0x62, 0x21b: 0x63, 0x220: 0x64, 0x222: 0x65, 0x223: 0x66, 0x224: 0x67, 0x225: 0x68, 0x226: 0x69, 0x227: 0x6a, 0x228: 0x6b, 0x229: 0x6c, 0x22a: 0x6d, 0x22b: 0x6e, 0x22f: 0x6f, 0x230: 0x70, 0x231: 0x71, 0x232: 0x72, 0x233: 0x73, 0x234: 0x74, 0x235: 0x75, 0x236: 0x76, 0x237: 0x70, 0x238: 0x71, 0x239: 0x72, 0x23a: 0x73, 0x23b: 0x74, 0x23c: 0x75, 0x23d: 0x76, 0x23e: 0x70, 0x23f: 0x71, // Block 0x9, offset 0x240 0x240: 0x72, 0x241: 0x73, 0x242: 0x74, 0x243: 0x75, 0x244: 0x76, 0x245: 0x70, 0x246: 0x71, 0x247: 0x72, 0x248: 0x73, 0x249: 0x74, 0x24a: 0x75, 0x24b: 0x76, 0x24c: 0x70, 0x24d: 0x71, 0x24e: 0x72, 0x24f: 0x73, 0x250: 0x74, 0x251: 0x75, 0x252: 0x76, 0x253: 0x70, 0x254: 0x71, 0x255: 0x72, 0x256: 0x73, 0x257: 0x74, 0x258: 0x75, 0x259: 0x76, 0x25a: 0x70, 0x25b: 0x71, 0x25c: 0x72, 0x25d: 0x73, 0x25e: 0x74, 0x25f: 0x75, 0x260: 0x76, 0x261: 0x70, 0x262: 0x71, 0x263: 0x72, 0x264: 0x73, 0x265: 0x74, 0x266: 0x75, 0x267: 0x76, 0x268: 0x70, 0x269: 0x71, 0x26a: 0x72, 0x26b: 0x73, 0x26c: 0x74, 0x26d: 0x75, 0x26e: 0x76, 0x26f: 0x70, 0x270: 0x71, 0x271: 0x72, 0x272: 0x73, 0x273: 0x74, 0x274: 0x75, 0x275: 0x76, 0x276: 0x70, 0x277: 0x71, 0x278: 0x72, 0x279: 0x73, 0x27a: 0x74, 0x27b: 0x75, 0x27c: 0x76, 0x27d: 0x70, 0x27e: 0x71, 0x27f: 0x72, // Block 0xa, offset 0x280 0x280: 0x73, 0x281: 0x74, 0x282: 0x75, 0x283: 0x76, 0x284: 0x70, 0x285: 0x71, 0x286: 0x72, 0x287: 0x73, 0x288: 0x74, 0x289: 0x75, 0x28a: 0x76, 0x28b: 0x70, 0x28c: 0x71, 0x28d: 0x72, 0x28e: 0x73, 0x28f: 0x74, 0x290: 0x75, 0x291: 0x76, 0x292: 0x70, 0x293: 0x71, 0x294: 0x72, 0x295: 0x73, 0x296: 0x74, 0x297: 0x75, 0x298: 0x76, 0x299: 0x70, 0x29a: 0x71, 0x29b: 0x72, 0x29c: 0x73, 0x29d: 0x74, 0x29e: 0x75, 0x29f: 0x76, 0x2a0: 0x70, 0x2a1: 0x71, 0x2a2: 0x72, 0x2a3: 0x73, 0x2a4: 0x74, 0x2a5: 0x75, 0x2a6: 0x76, 0x2a7: 0x70, 0x2a8: 0x71, 0x2a9: 0x72, 0x2aa: 0x73, 0x2ab: 0x74, 0x2ac: 0x75, 0x2ad: 0x76, 0x2ae: 0x70, 0x2af: 0x71, 0x2b0: 0x72, 0x2b1: 0x73, 0x2b2: 0x74, 0x2b3: 0x75, 0x2b4: 0x76, 0x2b5: 0x70, 0x2b6: 0x71, 0x2b7: 0x72, 0x2b8: 0x73, 0x2b9: 0x74, 0x2ba: 0x75, 0x2bb: 0x76, 0x2bc: 0x70, 0x2bd: 0x71, 0x2be: 0x72, 0x2bf: 0x73, // Block 0xb, offset 0x2c0 0x2c0: 0x74, 0x2c1: 0x75, 0x2c2: 0x76, 0x2c3: 0x70, 0x2c4: 0x71, 0x2c5: 0x72, 0x2c6: 0x73, 0x2c7: 0x74, 0x2c8: 0x75, 0x2c9: 0x76, 0x2ca: 0x70, 0x2cb: 0x71, 0x2cc: 0x72, 0x2cd: 0x73, 0x2ce: 0x74, 0x2cf: 0x75, 0x2d0: 0x76, 0x2d1: 0x70, 0x2d2: 0x71, 0x2d3: 0x72, 0x2d4: 0x73, 0x2d5: 0x74, 0x2d6: 0x75, 0x2d7: 0x76, 0x2d8: 0x70, 0x2d9: 0x71, 0x2da: 0x72, 0x2db: 0x73, 0x2dc: 0x74, 0x2dd: 0x75, 0x2de: 0x77, 0x2df: 0x78, // Block 0xc, offset 0x300 0x32c: 0x79, 0x338: 0x7a, 0x33b: 0x7b, 0x33e: 0x62, 0x33f: 0x7c, // Block 0xd, offset 0x340 0x347: 0x7d, 0x34b: 0x7e, 0x34d: 0x7f, 0x368: 0x80, 0x36b: 0x81, 0x374: 0x82, 0x375: 0x83, 0x37a: 0x84, 0x37b: 0x85, 0x37d: 0x86, 0x37e: 0x87, // Block 0xe, offset 0x380 0x380: 0x88, 0x381: 0x89, 0x382: 0x8a, 0x383: 0x8b, 0x384: 0x8c, 0x385: 0x8d, 0x386: 0x8e, 0x387: 0x8f, 0x388: 0x90, 0x389: 0x91, 0x38b: 0x92, 0x38c: 0x93, 0x38d: 0x94, 0x38e: 0x95, 0x38f: 0x96, 0x390: 0x97, 0x391: 0x98, 0x392: 0x99, 0x393: 0x9a, 0x396: 0x9b, 0x397: 0x9c, 0x398: 0x9d, 0x399: 0x9e, 0x39a: 0x9f, 0x39c: 0xa0, 0x3a0: 0xa1, 0x3a4: 0xa2, 0x3a5: 0xa3, 0x3a7: 0xa4, 0x3a8: 0xa5, 0x3a9: 0xa6, 0x3aa: 0xa7, 0x3ad: 0xa8, 0x3b0: 0xa9, 0x3b2: 0xaa, 0x3b4: 0xab, 0x3b5: 0xac, 0x3b6: 0xad, 0x3bb: 0xae, 0x3bc: 0xaf, 0x3bd: 0xb0, // Block 0xf, offset 0x3c0 0x3d0: 0xb1, 0x3d1: 0xb2, // Block 0x10, offset 0x400 0x404: 0xb3, 0x42b: 0xb4, 0x42c: 0xb5, 0x435: 0xb6, 0x43d: 0xb7, 0x43e: 0xb8, 0x43f: 0xb9, // Block 0x11, offset 0x440 0x472: 0xba, // Block 0x12, offset 0x480 0x4bc: 0xbb, 0x4bd: 0xbc, // Block 0x13, offset 0x4c0 0x4c5: 0xbd, 0x4c6: 0xbe, 0x4c9: 0xbf, 0x4e8: 0xc0, 0x4e9: 0xc1, 0x4ea: 0xc2, // Block 0x14, offset 0x500 0x500: 0xc3, 0x502: 0xc4, 0x504: 0xb5, 0x50a: 0xc5, 0x50b: 0xc6, 0x513: 0xc6, 0x517: 0xc7, 0x51b: 0xc8, 0x523: 0xc9, 0x525: 0xca, // Block 0x15, offset 0x540 0x540: 0xcb, 0x542: 0xcc, 0x543: 0xcd, 0x545: 0xce, 0x546: 0xcf, 0x547: 0xd0, 0x548: 0xd1, 0x549: 0xd2, 0x54a: 0xd3, 0x54b: 0xd3, 0x54c: 0xd4, 0x54d: 0xd3, 0x54e: 0xd5, 0x54f: 0xd6, 0x550: 0xd3, 0x551: 0xd3, 0x552: 0xd3, 0x553: 0xd7, 0x554: 0xd8, 0x555: 0xd9, 0x556: 0xda, 0x557: 0xdb, 0x558: 0xd3, 0x559: 0xdc, 0x55a: 0xd3, 0x55b: 0xdd, 0x55f: 0xde, 0x560: 0xdf, 0x561: 0xe0, 0x562: 0xe1, 0x563: 0xe2, 0x564: 0xe3, 0x565: 0xe4, 0x566: 0xd3, 0x567: 0xd3, 0x569: 0xe5, 0x56a: 0xd3, 0x56b: 0xd3, 0x570: 0xd3, 0x571: 0xd3, 0x572: 0xd3, 0x573: 0xd3, 0x574: 0xd3, 0x575: 0xd3, 0x576: 0xd3, 0x577: 0xd3, 0x578: 0xd3, 0x579: 0xd3, 0x57a: 0xd3, 0x57b: 0xd3, 0x57c: 0xd3, 0x57d: 0xd3, 0x57e: 0xd3, 0x57f: 0xd8, // Block 0x16, offset 0x580 0x590: 0x0b, 0x591: 0x0c, 0x593: 0x0d, 0x596: 0x0e, 0x59b: 0x0f, 0x59c: 0x10, 0x59d: 0x11, 0x59e: 0x12, 0x59f: 0x13, // Block 0x17, offset 0x5c0 0x5c0: 0xe6, 0x5c1: 0x02, 0x5c2: 0xe7, 0x5c3: 0xe7, 0x5c4: 0x02, 0x5c5: 0x02, 0x5c6: 0x02, 0x5c7: 0xe8, 0x5c8: 0xe7, 0x5c9: 0xe7, 0x5ca: 0xe7, 0x5cb: 0xe7, 0x5cc: 0xe7, 0x5cd: 0xe7, 0x5ce: 0xe7, 0x5cf: 0xe7, 0x5d0: 0xe7, 0x5d1: 0xe7, 0x5d2: 0xe7, 0x5d3: 0xe7, 0x5d4: 0xe7, 0x5d5: 0xe7, 0x5d6: 0xe7, 0x5d7: 0xe7, 0x5d8: 0xe7, 0x5d9: 0xe7, 0x5da: 0xe7, 0x5db: 0xe7, 0x5dc: 0xe7, 0x5dd: 0xe7, 0x5de: 0xe7, 0x5df: 0xe7, 0x5e0: 0xe7, 0x5e1: 0xe7, 0x5e2: 0xe7, 0x5e3: 0xe7, 0x5e4: 0xe7, 0x5e5: 0xe7, 0x5e6: 0xe7, 0x5e7: 0xe7, 0x5e8: 0xe7, 0x5e9: 0xe7, 0x5ea: 0xe7, 0x5eb: 0xe7, 0x5ec: 0xe7, 0x5ed: 0xe7, 0x5ee: 0xe7, 0x5ef: 0xe7, 0x5f0: 0xe7, 0x5f1: 0xe7, 0x5f2: 0xe7, 0x5f3: 0xe7, 0x5f4: 0xe7, 0x5f5: 0xe7, 0x5f6: 0xe7, 0x5f7: 0xe7, 0x5f8: 0xe7, 0x5f9: 0xe7, 0x5fa: 0xe7, 0x5fb: 0xe7, 0x5fc: 0xe7, 0x5fd: 0xe7, 0x5fe: 0xe7, 0x5ff: 0xe7, // Block 0x18, offset 0x600 0x620: 0x15, } ================================================ FILE: vendor/github.com/containerd/errdefs/LICENSE ================================================ Apache License Version 2.0, January 2004 https://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 Copyright The containerd Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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: vendor/github.com/containerd/errdefs/README.md ================================================ # errdefs A Go package for defining and checking common containerd errors. ## Project details **errdefs** is a containerd sub-project, licensed under the [Apache 2.0 license](./LICENSE). As a containerd sub-project, you will find the: * [Project governance](https://github.com/containerd/project/blob/main/GOVERNANCE.md), * [Maintainers](https://github.com/containerd/project/blob/main/MAINTAINERS), * and [Contributing guidelines](https://github.com/containerd/project/blob/main/CONTRIBUTING.md) information in our [`containerd/project`](https://github.com/containerd/project) repository. ================================================ FILE: vendor/github.com/containerd/errdefs/errors.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package errdefs defines the common errors used throughout containerd // packages. // // Use with fmt.Errorf to add context to an error. // // To detect an error class, use the IsXXX functions to tell whether an error // is of a certain type. package errdefs import ( "context" "errors" ) // Definitions of common error types used throughout containerd. All containerd // errors returned by most packages will map into one of these errors classes. // Packages should return errors of these types when they want to instruct a // client to take a particular action. // // These errors map closely to grpc errors. var ( ErrUnknown = errUnknown{} ErrInvalidArgument = errInvalidArgument{} ErrNotFound = errNotFound{} ErrAlreadyExists = errAlreadyExists{} ErrPermissionDenied = errPermissionDenied{} ErrResourceExhausted = errResourceExhausted{} ErrFailedPrecondition = errFailedPrecondition{} ErrConflict = errConflict{} ErrNotModified = errNotModified{} ErrAborted = errAborted{} ErrOutOfRange = errOutOfRange{} ErrNotImplemented = errNotImplemented{} ErrInternal = errInternal{} ErrUnavailable = errUnavailable{} ErrDataLoss = errDataLoss{} ErrUnauthenticated = errUnauthorized{} ) // cancelled maps to Moby's "ErrCancelled" type cancelled interface { Cancelled() } // IsCanceled returns true if the error is due to `context.Canceled`. func IsCanceled(err error) bool { return errors.Is(err, context.Canceled) || isInterface[cancelled](err) } type errUnknown struct{} func (errUnknown) Error() string { return "unknown" } func (errUnknown) Unknown() {} func (e errUnknown) WithMessage(msg string) error { return customMessage{e, msg} } // unknown maps to Moby's "ErrUnknown" type unknown interface { Unknown() } // IsUnknown returns true if the error is due to an unknown error, // unhandled condition or unexpected response. func IsUnknown(err error) bool { return errors.Is(err, errUnknown{}) || isInterface[unknown](err) } type errInvalidArgument struct{} func (errInvalidArgument) Error() string { return "invalid argument" } func (errInvalidArgument) InvalidParameter() {} func (e errInvalidArgument) WithMessage(msg string) error { return customMessage{e, msg} } // invalidParameter maps to Moby's "ErrInvalidParameter" type invalidParameter interface { InvalidParameter() } // IsInvalidArgument returns true if the error is due to an invalid argument func IsInvalidArgument(err error) bool { return errors.Is(err, ErrInvalidArgument) || isInterface[invalidParameter](err) } // deadlineExceed maps to Moby's "ErrDeadline" type deadlineExceeded interface { DeadlineExceeded() } // IsDeadlineExceeded returns true if the error is due to // `context.DeadlineExceeded`. func IsDeadlineExceeded(err error) bool { return errors.Is(err, context.DeadlineExceeded) || isInterface[deadlineExceeded](err) } type errNotFound struct{} func (errNotFound) Error() string { return "not found" } func (errNotFound) NotFound() {} func (e errNotFound) WithMessage(msg string) error { return customMessage{e, msg} } // notFound maps to Moby's "ErrNotFound" type notFound interface { NotFound() } // IsNotFound returns true if the error is due to a missing object func IsNotFound(err error) bool { return errors.Is(err, ErrNotFound) || isInterface[notFound](err) } type errAlreadyExists struct{} func (errAlreadyExists) Error() string { return "already exists" } func (errAlreadyExists) AlreadyExists() {} func (e errAlreadyExists) WithMessage(msg string) error { return customMessage{e, msg} } type alreadyExists interface { AlreadyExists() } // IsAlreadyExists returns true if the error is due to an already existing // metadata item func IsAlreadyExists(err error) bool { return errors.Is(err, ErrAlreadyExists) || isInterface[alreadyExists](err) } type errPermissionDenied struct{} func (errPermissionDenied) Error() string { return "permission denied" } func (errPermissionDenied) Forbidden() {} func (e errPermissionDenied) WithMessage(msg string) error { return customMessage{e, msg} } // forbidden maps to Moby's "ErrForbidden" type forbidden interface { Forbidden() } // IsPermissionDenied returns true if the error is due to permission denied // or forbidden (403) response func IsPermissionDenied(err error) bool { return errors.Is(err, ErrPermissionDenied) || isInterface[forbidden](err) } type errResourceExhausted struct{} func (errResourceExhausted) Error() string { return "resource exhausted" } func (errResourceExhausted) ResourceExhausted() {} func (e errResourceExhausted) WithMessage(msg string) error { return customMessage{e, msg} } type resourceExhausted interface { ResourceExhausted() } // IsResourceExhausted returns true if the error is due to // a lack of resources or too many attempts. func IsResourceExhausted(err error) bool { return errors.Is(err, errResourceExhausted{}) || isInterface[resourceExhausted](err) } type errFailedPrecondition struct{} func (e errFailedPrecondition) Error() string { return "failed precondition" } func (errFailedPrecondition) FailedPrecondition() {} func (e errFailedPrecondition) WithMessage(msg string) error { return customMessage{e, msg} } type failedPrecondition interface { FailedPrecondition() } // IsFailedPrecondition returns true if an operation could not proceed due to // the lack of a particular condition func IsFailedPrecondition(err error) bool { return errors.Is(err, errFailedPrecondition{}) || isInterface[failedPrecondition](err) } type errConflict struct{} func (errConflict) Error() string { return "conflict" } func (errConflict) Conflict() {} func (e errConflict) WithMessage(msg string) error { return customMessage{e, msg} } // conflict maps to Moby's "ErrConflict" type conflict interface { Conflict() } // IsConflict returns true if an operation could not proceed due to // a conflict. func IsConflict(err error) bool { return errors.Is(err, errConflict{}) || isInterface[conflict](err) } type errNotModified struct{} func (errNotModified) Error() string { return "not modified" } func (errNotModified) NotModified() {} func (e errNotModified) WithMessage(msg string) error { return customMessage{e, msg} } // notModified maps to Moby's "ErrNotModified" type notModified interface { NotModified() } // IsNotModified returns true if an operation could not proceed due // to an object not modified from a previous state. func IsNotModified(err error) bool { return errors.Is(err, errNotModified{}) || isInterface[notModified](err) } type errAborted struct{} func (errAborted) Error() string { return "aborted" } func (errAborted) Aborted() {} func (e errAborted) WithMessage(msg string) error { return customMessage{e, msg} } type aborted interface { Aborted() } // IsAborted returns true if an operation was aborted. func IsAborted(err error) bool { return errors.Is(err, errAborted{}) || isInterface[aborted](err) } type errOutOfRange struct{} func (errOutOfRange) Error() string { return "out of range" } func (errOutOfRange) OutOfRange() {} func (e errOutOfRange) WithMessage(msg string) error { return customMessage{e, msg} } type outOfRange interface { OutOfRange() } // IsOutOfRange returns true if an operation could not proceed due // to data being out of the expected range. func IsOutOfRange(err error) bool { return errors.Is(err, errOutOfRange{}) || isInterface[outOfRange](err) } type errNotImplemented struct{} func (errNotImplemented) Error() string { return "not implemented" } func (errNotImplemented) NotImplemented() {} func (e errNotImplemented) WithMessage(msg string) error { return customMessage{e, msg} } // notImplemented maps to Moby's "ErrNotImplemented" type notImplemented interface { NotImplemented() } // IsNotImplemented returns true if the error is due to not being implemented func IsNotImplemented(err error) bool { return errors.Is(err, errNotImplemented{}) || isInterface[notImplemented](err) } type errInternal struct{} func (errInternal) Error() string { return "internal" } func (errInternal) System() {} func (e errInternal) WithMessage(msg string) error { return customMessage{e, msg} } // system maps to Moby's "ErrSystem" type system interface { System() } // IsInternal returns true if the error returns to an internal or system error func IsInternal(err error) bool { return errors.Is(err, errInternal{}) || isInterface[system](err) } type errUnavailable struct{} func (errUnavailable) Error() string { return "unavailable" } func (errUnavailable) Unavailable() {} func (e errUnavailable) WithMessage(msg string) error { return customMessage{e, msg} } // unavailable maps to Moby's "ErrUnavailable" type unavailable interface { Unavailable() } // IsUnavailable returns true if the error is due to a resource being unavailable func IsUnavailable(err error) bool { return errors.Is(err, errUnavailable{}) || isInterface[unavailable](err) } type errDataLoss struct{} func (errDataLoss) Error() string { return "data loss" } func (errDataLoss) DataLoss() {} func (e errDataLoss) WithMessage(msg string) error { return customMessage{e, msg} } // dataLoss maps to Moby's "ErrDataLoss" type dataLoss interface { DataLoss() } // IsDataLoss returns true if data during an operation was lost or corrupted func IsDataLoss(err error) bool { return errors.Is(err, errDataLoss{}) || isInterface[dataLoss](err) } type errUnauthorized struct{} func (errUnauthorized) Error() string { return "unauthorized" } func (errUnauthorized) Unauthorized() {} func (e errUnauthorized) WithMessage(msg string) error { return customMessage{e, msg} } // unauthorized maps to Moby's "ErrUnauthorized" type unauthorized interface { Unauthorized() } // IsUnauthorized returns true if the error indicates that the user was // unauthenticated or unauthorized. func IsUnauthorized(err error) bool { return errors.Is(err, errUnauthorized{}) || isInterface[unauthorized](err) } func isInterface[T any](err error) bool { for { switch x := err.(type) { case T: return true case customMessage: err = x.err case interface{ Unwrap() error }: err = x.Unwrap() if err == nil { return false } case interface{ Unwrap() []error }: for _, err := range x.Unwrap() { if isInterface[T](err) { return true } } return false default: return false } } } // customMessage is used to provide a defined error with a custom message. // The message is not wrapped but can be compared by the `Is(error) bool` interface. type customMessage struct { err error msg string } func (c customMessage) Is(err error) bool { return c.err == err } func (c customMessage) As(target any) bool { return errors.As(c.err, target) } func (c customMessage) Error() string { return c.msg } ================================================ FILE: vendor/github.com/containerd/errdefs/pkg/LICENSE ================================================ Apache License Version 2.0, January 2004 https://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 Copyright The containerd Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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: vendor/github.com/containerd/errdefs/pkg/errhttp/http.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package errhttp provides utility functions for translating errors to // and from a HTTP context. // // The functions ToHTTP and ToNative can be used to map server-side and // client-side errors to the correct types. package errhttp import ( "errors" "net/http" "github.com/containerd/errdefs" "github.com/containerd/errdefs/pkg/internal/cause" ) // ToHTTP returns the best status code for the given error func ToHTTP(err error) int { switch { case errdefs.IsNotFound(err): return http.StatusNotFound case errdefs.IsInvalidArgument(err): return http.StatusBadRequest case errdefs.IsConflict(err): return http.StatusConflict case errdefs.IsNotModified(err): return http.StatusNotModified case errdefs.IsFailedPrecondition(err): return http.StatusPreconditionFailed case errdefs.IsUnauthorized(err): return http.StatusUnauthorized case errdefs.IsPermissionDenied(err): return http.StatusForbidden case errdefs.IsResourceExhausted(err): return http.StatusTooManyRequests case errdefs.IsInternal(err): return http.StatusInternalServerError case errdefs.IsNotImplemented(err): return http.StatusNotImplemented case errdefs.IsUnavailable(err): return http.StatusServiceUnavailable case errdefs.IsUnknown(err): var unexpected cause.ErrUnexpectedStatus if errors.As(err, &unexpected) && unexpected.Status >= 200 && unexpected.Status < 600 { return unexpected.Status } return http.StatusInternalServerError default: return http.StatusInternalServerError } } // ToNative returns the error best matching the HTTP status code func ToNative(statusCode int) error { switch statusCode { case http.StatusNotFound: return errdefs.ErrNotFound case http.StatusBadRequest: return errdefs.ErrInvalidArgument case http.StatusConflict: return errdefs.ErrConflict case http.StatusPreconditionFailed: return errdefs.ErrFailedPrecondition case http.StatusUnauthorized: return errdefs.ErrUnauthenticated case http.StatusForbidden: return errdefs.ErrPermissionDenied case http.StatusNotModified: return errdefs.ErrNotModified case http.StatusTooManyRequests: return errdefs.ErrResourceExhausted case http.StatusInternalServerError: return errdefs.ErrInternal case http.StatusNotImplemented: return errdefs.ErrNotImplemented case http.StatusServiceUnavailable: return errdefs.ErrUnavailable default: return cause.ErrUnexpectedStatus{Status: statusCode} } } ================================================ FILE: vendor/github.com/containerd/errdefs/pkg/internal/cause/cause.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package cause is used to define root causes for errors // common to errors packages like grpc and http. package cause import "fmt" type ErrUnexpectedStatus struct { Status int } const UnexpectedStatusPrefix = "unexpected status " func (e ErrUnexpectedStatus) Error() string { return fmt.Sprintf("%s%d", UnexpectedStatusPrefix, e.Status) } func (ErrUnexpectedStatus) Unknown() {} ================================================ FILE: vendor/github.com/containerd/errdefs/resolve.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package errdefs import "context" // Resolve returns the first error found in the error chain which matches an // error defined in this package or context error. A raw, unwrapped error is // returned or ErrUnknown if no matching error is found. // // This is useful for determining a response code based on the outermost wrapped // error rather than the original cause. For example, a not found error deep // in the code may be wrapped as an invalid argument. When determining status // code from Is* functions, the depth or ordering of the error is not // considered. // // The search order is depth first, a wrapped error returned from any part of // the chain from `Unwrap() error` will be returned before any joined errors // as returned by `Unwrap() []error`. func Resolve(err error) error { if err == nil { return nil } err = firstError(err) if err == nil { err = ErrUnknown } return err } func firstError(err error) error { for { switch err { case ErrUnknown, ErrInvalidArgument, ErrNotFound, ErrAlreadyExists, ErrPermissionDenied, ErrResourceExhausted, ErrFailedPrecondition, ErrConflict, ErrNotModified, ErrAborted, ErrOutOfRange, ErrNotImplemented, ErrInternal, ErrUnavailable, ErrDataLoss, ErrUnauthenticated, context.DeadlineExceeded, context.Canceled: return err } switch e := err.(type) { case customMessage: err = e.err case unknown: return ErrUnknown case invalidParameter: return ErrInvalidArgument case notFound: return ErrNotFound case alreadyExists: return ErrAlreadyExists case forbidden: return ErrPermissionDenied case resourceExhausted: return ErrResourceExhausted case failedPrecondition: return ErrFailedPrecondition case conflict: return ErrConflict case notModified: return ErrNotModified case aborted: return ErrAborted case errOutOfRange: return ErrOutOfRange case notImplemented: return ErrNotImplemented case system: return ErrInternal case unavailable: return ErrUnavailable case dataLoss: return ErrDataLoss case unauthorized: return ErrUnauthenticated case deadlineExceeded: return context.DeadlineExceeded case cancelled: return context.Canceled case interface{ Unwrap() error }: err = e.Unwrap() if err == nil { return nil } case interface{ Unwrap() []error }: for _, ue := range e.Unwrap() { if fe := firstError(ue); fe != nil { return fe } } return nil case interface{ Is(error) bool }: for _, target := range []error{ErrUnknown, ErrInvalidArgument, ErrNotFound, ErrAlreadyExists, ErrPermissionDenied, ErrResourceExhausted, ErrFailedPrecondition, ErrConflict, ErrNotModified, ErrAborted, ErrOutOfRange, ErrNotImplemented, ErrInternal, ErrUnavailable, ErrDataLoss, ErrUnauthenticated, context.DeadlineExceeded, context.Canceled} { if e.Is(target) { return target } } return nil default: return nil } } } ================================================ FILE: vendor/github.com/containerd/log/.golangci.yml ================================================ linters: enable: - exportloopref # Checks for pointers to enclosing loop variables - gofmt - goimports - gosec - ineffassign - misspell - nolintlint - revive - staticcheck - tenv # Detects using os.Setenv instead of t.Setenv since Go 1.17 - unconvert - unused - vet - dupword # Checks for duplicate words in the source code disable: - errcheck run: timeout: 5m skip-dirs: - api - cluster - design - docs - docs/man - releases - reports - test # e2e scripts ================================================ FILE: vendor/github.com/containerd/log/LICENSE ================================================ Apache License Version 2.0, January 2004 https://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 Copyright The containerd Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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: vendor/github.com/containerd/log/README.md ================================================ # log A Go package providing a common logging interface across containerd repositories and a way for clients to use and configure logging in containerd packages. This package is not intended to be used as a standalone logging package outside of the containerd ecosystem and is intended as an interface wrapper around a logging implementation. In the future this package may be replaced with a common go logging interface. ## Project details **log** is a containerd sub-project, licensed under the [Apache 2.0 license](./LICENSE). As a containerd sub-project, you will find the: * [Project governance](https://github.com/containerd/project/blob/main/GOVERNANCE.md), * [Maintainers](https://github.com/containerd/project/blob/main/MAINTAINERS), * and [Contributing guidelines](https://github.com/containerd/project/blob/main/CONTRIBUTING.md) information in our [`containerd/project`](https://github.com/containerd/project) repository. ================================================ FILE: vendor/github.com/containerd/log/context.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package log provides types and functions related to logging, passing // loggers through a context, and attaching context to the logger. // // # Transitional types // // This package contains various types that are aliases for types in [logrus]. // These aliases are intended for transitioning away from hard-coding logrus // as logging implementation. Consumers of this package are encouraged to use // the type-aliases from this package instead of directly using their logrus // equivalent. // // The intent is to replace these aliases with locally defined types and // interfaces once all consumers are no longer directly importing logrus // types. // // IMPORTANT: due to the transitional purpose of this package, it is not // guaranteed for the full logrus API to be provided in the future. As // outlined, these aliases are provided as a step to transition away from // a specific implementation which, as a result, exposes the full logrus API. // While no decisions have been made on the ultimate design and interface // provided by this package, we do not expect carrying "less common" features. package log import ( "context" "fmt" "github.com/sirupsen/logrus" ) // G is a shorthand for [GetLogger]. // // We may want to define this locally to a package to get package tagged log // messages. var G = GetLogger // L is an alias for the standard logger. var L = &Entry{ Logger: logrus.StandardLogger(), // Default is three fields plus a little extra room. Data: make(Fields, 6), } type loggerKey struct{} // Fields type to pass to "WithFields". type Fields = map[string]any // Entry is a logging entry. It contains all the fields passed with // [Entry.WithFields]. It's finally logged when Trace, Debug, Info, Warn, // Error, Fatal or Panic is called on it. These objects can be reused and // passed around as much as you wish to avoid field duplication. // // Entry is a transitional type, and currently an alias for [logrus.Entry]. type Entry = logrus.Entry // RFC3339NanoFixed is [time.RFC3339Nano] with nanoseconds padded using // zeros to ensure the formatted time is always the same number of // characters. const RFC3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00" // Level is a logging level. type Level = logrus.Level // Supported log levels. const ( // TraceLevel level. Designates finer-grained informational events // than [DebugLevel]. TraceLevel Level = logrus.TraceLevel // DebugLevel level. Usually only enabled when debugging. Very verbose // logging. DebugLevel Level = logrus.DebugLevel // InfoLevel level. General operational entries about what's going on // inside the application. InfoLevel Level = logrus.InfoLevel // WarnLevel level. Non-critical entries that deserve eyes. WarnLevel Level = logrus.WarnLevel // ErrorLevel level. Logs errors that should definitely be noted. // Commonly used for hooks to send errors to an error tracking service. ErrorLevel Level = logrus.ErrorLevel // FatalLevel level. Logs and then calls "logger.Exit(1)". It exits // even if the logging level is set to Panic. FatalLevel Level = logrus.FatalLevel // PanicLevel level. This is the highest level of severity. Logs and // then calls panic with the message passed to Debug, Info, ... PanicLevel Level = logrus.PanicLevel ) // SetLevel sets log level globally. It returns an error if the given // level is not supported. // // level can be one of: // // - "trace" ([TraceLevel]) // - "debug" ([DebugLevel]) // - "info" ([InfoLevel]) // - "warn" ([WarnLevel]) // - "error" ([ErrorLevel]) // - "fatal" ([FatalLevel]) // - "panic" ([PanicLevel]) func SetLevel(level string) error { lvl, err := logrus.ParseLevel(level) if err != nil { return err } L.Logger.SetLevel(lvl) return nil } // GetLevel returns the current log level. func GetLevel() Level { return L.Logger.GetLevel() } // OutputFormat specifies a log output format. type OutputFormat string // Supported log output formats. const ( // TextFormat represents the text logging format. TextFormat OutputFormat = "text" // JSONFormat represents the JSON logging format. JSONFormat OutputFormat = "json" ) // SetFormat sets the log output format ([TextFormat] or [JSONFormat]). func SetFormat(format OutputFormat) error { switch format { case TextFormat: L.Logger.SetFormatter(&logrus.TextFormatter{ TimestampFormat: RFC3339NanoFixed, FullTimestamp: true, }) return nil case JSONFormat: L.Logger.SetFormatter(&logrus.JSONFormatter{ TimestampFormat: RFC3339NanoFixed, }) return nil default: return fmt.Errorf("unknown log format: %s", format) } } // WithLogger returns a new context with the provided logger. Use in // combination with logger.WithField(s) for great effect. func WithLogger(ctx context.Context, logger *Entry) context.Context { return context.WithValue(ctx, loggerKey{}, logger.WithContext(ctx)) } // GetLogger retrieves the current logger from the context. If no logger is // available, the default logger is returned. func GetLogger(ctx context.Context) *Entry { if logger := ctx.Value(loggerKey{}); logger != nil { return logger.(*Entry) } return L.WithContext(ctx) } ================================================ FILE: vendor/github.com/containerd/platforms/.gitattributes ================================================ *.go text eol=lf ================================================ FILE: vendor/github.com/containerd/platforms/.golangci.yml ================================================ linters: enable: - copyloopvar - gofmt - goimports - gosec - ineffassign - misspell - nolintlint - revive - staticcheck - tenv # Detects using os.Setenv instead of t.Setenv since Go 1.17 - unconvert - unused - govet - dupword # Checks for duplicate words in the source code disable: - errcheck run: timeout: 5m issues: exclude-dirs: - api - cluster - design - docs - docs/man - releases - reports - test # e2e scripts ================================================ FILE: vendor/github.com/containerd/platforms/LICENSE ================================================ Apache License Version 2.0, January 2004 https://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 Copyright The containerd Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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: vendor/github.com/containerd/platforms/README.md ================================================ # platforms A Go package for formatting, normalizing and matching container platforms. This package is based on the Open Containers Image Spec definition of a [platform](https://github.com/opencontainers/image-spec/blob/main/specs-go/v1/descriptor.go#L52). ## Platform Specifier While the OCI platform specifications provide a tool for components to specify structured information, user input typically doesn't need the full context and much can be inferred. To solve this problem, this package introduces "specifiers". A specifier has the format `||/[/]`. The user can provide either the operating system or the architecture or both. An example of a common specifier is `linux/amd64`. If the host has a default runtime that matches this, the user can simply provide the component that matters. For example, if an image provides `amd64` and `arm64` support, the operating system, `linux` can be inferred, so they only have to provide `arm64` or `amd64`. Similar behavior is implemented for operating systems, where the architecture may be known but a runtime may support images from different operating systems. ## Project details **platforms** is a containerd sub-project, licensed under the [Apache 2.0 license](./LICENSE). As a containerd sub-project, you will find the: * [Project governance](https://github.com/containerd/project/blob/main/GOVERNANCE.md), * [Maintainers](https://github.com/containerd/project/blob/main/MAINTAINERS), * and [Contributing guidelines](https://github.com/containerd/project/blob/main/CONTRIBUTING.md) information in our [`containerd/project`](https://github.com/containerd/project) repository. ================================================ FILE: vendor/github.com/containerd/platforms/compare.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package platforms import ( "strconv" "strings" specs "github.com/opencontainers/image-spec/specs-go/v1" ) // MatchComparer is able to match and compare platforms to // filter and sort platforms. type MatchComparer interface { Matcher Less(specs.Platform, specs.Platform) bool } type platformVersions struct { major []int minor []int } var arm64variantToVersion = map[string]platformVersions{ "v8": {[]int{8}, []int{0}}, "v8.0": {[]int{8}, []int{0}}, "v8.1": {[]int{8}, []int{1}}, "v8.2": {[]int{8}, []int{2}}, "v8.3": {[]int{8}, []int{3}}, "v8.4": {[]int{8}, []int{4}}, "v8.5": {[]int{8}, []int{5}}, "v8.6": {[]int{8}, []int{6}}, "v8.7": {[]int{8}, []int{7}}, "v8.8": {[]int{8}, []int{8}}, "v8.9": {[]int{8}, []int{9}}, "v9": {[]int{9, 8}, []int{0, 5}}, "v9.0": {[]int{9, 8}, []int{0, 5}}, "v9.1": {[]int{9, 8}, []int{1, 6}}, "v9.2": {[]int{9, 8}, []int{2, 7}}, "v9.3": {[]int{9, 8}, []int{3, 8}}, "v9.4": {[]int{9, 8}, []int{4, 9}}, "v9.5": {[]int{9, 8}, []int{5, 9}}, "v9.6": {[]int{9, 8}, []int{6, 9}}, "v9.7": {[]int{9, 8}, []int{7, 9}}, } // platformVector returns an (ordered) vector of appropriate specs.Platform // objects to try matching for the given platform object (see platforms.Only). func platformVector(platform specs.Platform) []specs.Platform { vector := []specs.Platform{platform} switch platform.Architecture { case "amd64": if amd64Version, err := strconv.Atoi(strings.TrimPrefix(platform.Variant, "v")); err == nil && amd64Version > 1 { for amd64Version--; amd64Version >= 1; amd64Version-- { vector = append(vector, specs.Platform{ Architecture: platform.Architecture, OS: platform.OS, OSVersion: platform.OSVersion, OSFeatures: platform.OSFeatures, Variant: "v" + strconv.Itoa(amd64Version), }) } } vector = append(vector, specs.Platform{ Architecture: "386", OS: platform.OS, OSVersion: platform.OSVersion, OSFeatures: platform.OSFeatures, }) case "arm": if armVersion, err := strconv.Atoi(strings.TrimPrefix(platform.Variant, "v")); err == nil && armVersion > 5 { for armVersion--; armVersion >= 5; armVersion-- { vector = append(vector, specs.Platform{ Architecture: platform.Architecture, OS: platform.OS, OSVersion: platform.OSVersion, OSFeatures: platform.OSFeatures, Variant: "v" + strconv.Itoa(armVersion), }) } } case "arm64": variant := platform.Variant if variant == "" { variant = "v8" } vector = []specs.Platform{} // Reset vector, the first variant will be added in loop. arm64Versions, ok := arm64variantToVersion[variant] if !ok { break } for i, major := range arm64Versions.major { for minor := arm64Versions.minor[i]; minor >= 0; minor-- { arm64Variant := "v" + strconv.Itoa(major) + "." + strconv.Itoa(minor) if minor == 0 { arm64Variant = "v" + strconv.Itoa(major) } vector = append(vector, specs.Platform{ Architecture: "arm64", OS: platform.OS, OSVersion: platform.OSVersion, OSFeatures: platform.OSFeatures, Variant: arm64Variant, }) } } // All arm64/v8.x and arm64/v9.x are compatible with arm/v8 (32-bits) and below. // There's no arm64 v9 variant, so it's normalized to v8. if strings.HasPrefix(variant, "v8") || strings.HasPrefix(variant, "v9") { variant = "v8" } vector = append(vector, platformVector(specs.Platform{ Architecture: "arm", OS: platform.OS, OSVersion: platform.OSVersion, OSFeatures: platform.OSFeatures, Variant: variant, })...) } return vector } // Only returns a match comparer for a single platform // using default resolution logic for the platform. // // For arm64/v9.x, will also match arm64/v9.{0..x-1} and arm64/v8.{0..x+5} // For arm64/v8.x, will also match arm64/v8.{0..x-1} // For arm/v8, will also match arm/v7, arm/v6 and arm/v5 // For arm/v7, will also match arm/v6 and arm/v5 // For arm/v6, will also match arm/v5 // For amd64, will also match 386 func Only(platform specs.Platform) MatchComparer { return Ordered(platformVector(Normalize(platform))...) } // OnlyStrict returns a match comparer for a single platform. // // Unlike Only, OnlyStrict does not match sub platforms. // So, "arm/vN" will not match "arm/vM" where M < N, // and "amd64" will not also match "386". // // OnlyStrict matches non-canonical forms. // So, "arm64" matches "arm/64/v8". func OnlyStrict(platform specs.Platform) MatchComparer { return Ordered(Normalize(platform)) } // Ordered returns a platform MatchComparer which matches any of the platforms // but orders them in order they are provided. func Ordered(platforms ...specs.Platform) MatchComparer { matchers := make([]Matcher, len(platforms)) for i := range platforms { matchers[i] = NewMatcher(platforms[i]) } return orderedPlatformComparer{ matchers: matchers, } } // Any returns a platform MatchComparer which matches any of the platforms // with no preference for ordering. func Any(platforms ...specs.Platform) MatchComparer { matchers := make([]Matcher, len(platforms)) for i := range platforms { matchers[i] = NewMatcher(platforms[i]) } return anyPlatformComparer{ matchers: matchers, } } // All is a platform MatchComparer which matches all platforms // with preference for ordering. var All MatchComparer = allPlatformComparer{} type orderedPlatformComparer struct { matchers []Matcher } func (c orderedPlatformComparer) Match(platform specs.Platform) bool { for _, m := range c.matchers { if m.Match(platform) { return true } } return false } func (c orderedPlatformComparer) Less(p1 specs.Platform, p2 specs.Platform) bool { for _, m := range c.matchers { p1m := m.Match(p1) p2m := m.Match(p2) if p1m && !p2m { return true } if p1m || p2m { return false } } return false } type anyPlatformComparer struct { matchers []Matcher } func (c anyPlatformComparer) Match(platform specs.Platform) bool { for _, m := range c.matchers { if m.Match(platform) { return true } } return false } func (c anyPlatformComparer) Less(p1, p2 specs.Platform) bool { var p1m, p2m bool for _, m := range c.matchers { if !p1m && m.Match(p1) { p1m = true } if !p2m && m.Match(p2) { p2m = true } if p1m && p2m { return false } } // If one matches, and the other does, sort match first return p1m && !p2m } type allPlatformComparer struct{} func (allPlatformComparer) Match(specs.Platform) bool { return true } func (allPlatformComparer) Less(specs.Platform, specs.Platform) bool { return false } ================================================ FILE: vendor/github.com/containerd/platforms/cpuinfo.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package platforms import ( "runtime" "sync" "github.com/containerd/log" ) // Present the ARM instruction set architecture, eg: v7, v8 // Don't use this value directly; call cpuVariant() instead. var cpuVariantValue string var cpuVariantOnce sync.Once func cpuVariant() string { cpuVariantOnce.Do(func() { if isArmArch(runtime.GOARCH) { var err error cpuVariantValue, err = getCPUVariant() if err != nil { log.L.Errorf("Error getCPUVariant for OS %s: %v", runtime.GOOS, err) } } }) return cpuVariantValue } ================================================ FILE: vendor/github.com/containerd/platforms/cpuinfo_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 platforms import ( "bufio" "bytes" "errors" "fmt" "os" "runtime" "strings" "golang.org/x/sys/unix" ) // getMachineArch retrieves the machine architecture through system call func getMachineArch() (string, error) { var uname unix.Utsname err := unix.Uname(&uname) if err != nil { return "", err } arch := string(uname.Machine[:bytes.IndexByte(uname.Machine[:], 0)]) return arch, nil } // For Linux, the kernel has already detected the ABI, ISA and Features. // So we don't need to access the ARM registers to detect platform information // by ourselves. We can just parse these information from /proc/cpuinfo func getCPUInfo(pattern string) (info string, err error) { cpuinfo, err := os.Open("/proc/cpuinfo") if err != nil { return "", err } defer cpuinfo.Close() // Start to Parse the Cpuinfo line by line. For SMP SoC, we parse // the first core is enough. scanner := bufio.NewScanner(cpuinfo) for scanner.Scan() { newline := scanner.Text() list := strings.Split(newline, ":") if len(list) > 1 && strings.EqualFold(strings.TrimSpace(list[0]), pattern) { return strings.TrimSpace(list[1]), nil } } // Check whether the scanner encountered errors err = scanner.Err() if err != nil { return "", err } return "", fmt.Errorf("getCPUInfo for pattern %s: %w", pattern, errNotFound) } // getCPUVariantFromArch get CPU variant from arch through a system call func getCPUVariantFromArch(arch string) (string, error) { var variant string arch = strings.ToLower(arch) if arch == "aarch64" { variant = "8" } else if arch[0:4] == "armv" && len(arch) >= 5 { // Valid arch format is in form of armvXx switch arch[3:5] { case "v8": variant = "8" case "v7": variant = "7" case "v6": variant = "6" case "v5": variant = "5" case "v4": variant = "4" case "v3": variant = "3" default: variant = "unknown" } } else { return "", fmt.Errorf("getCPUVariantFromArch invalid arch: %s, %w", arch, errInvalidArgument) } return variant, nil } // getCPUVariant returns cpu variant for ARM // We first try reading "Cpu architecture" field from /proc/cpuinfo // If we can't find it, then fall back using a system call // This is to cover running ARM in emulated environment on x86 host as this field in /proc/cpuinfo // was not present. func getCPUVariant() (string, error) { variant, err := getCPUInfo("Cpu architecture") if err != nil { if errors.Is(err, errNotFound) { // Let's try getting CPU variant from machine architecture arch, err := getMachineArch() if err != nil { return "", fmt.Errorf("failure getting machine architecture: %v", err) } variant, err = getCPUVariantFromArch(arch) if err != nil { return "", fmt.Errorf("failure getting CPU variant from machine architecture: %v", err) } } else { return "", fmt.Errorf("failure getting CPU variant: %v", err) } } // handle edge case for Raspberry Pi ARMv6 devices (which due to a kernel quirk, report "CPU architecture: 7") // https://www.raspberrypi.org/forums/viewtopic.php?t=12614 if runtime.GOARCH == "arm" && variant == "7" { model, err := getCPUInfo("model name") if err == nil && strings.HasPrefix(strings.ToLower(model), "armv6-compatible") { variant = "6" } } switch strings.ToLower(variant) { case "8", "aarch64": variant = "v8" case "7", "7m", "?(12)", "?(13)", "?(14)", "?(15)", "?(16)", "?(17)": variant = "v7" case "6", "6tej": variant = "v6" case "5", "5t", "5te", "5tej": variant = "v5" case "4", "4t": variant = "v4" case "3": variant = "v3" default: variant = "unknown" } return variant, nil } ================================================ FILE: vendor/github.com/containerd/platforms/cpuinfo_other.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 platforms import ( "fmt" "runtime" ) func getCPUVariant() (string, error) { var variant string if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { // Windows/Darwin only supports v7 for ARM32 and v8 for ARM64 and so we can use // runtime.GOARCH to determine the variants switch runtime.GOARCH { case "arm64": variant = "v8" case "arm": variant = "v7" default: variant = "unknown" } } else if runtime.GOOS == "freebsd" { // FreeBSD supports ARMv6 and ARMv7 as well as ARMv4 and ARMv5 (though deprecated) // detecting those variants is currently unimplemented switch runtime.GOARCH { case "arm64": variant = "v8" default: variant = "unknown" } } else { return "", fmt.Errorf("getCPUVariant for OS %s: %v", runtime.GOOS, errNotImplemented) } return variant, nil } ================================================ FILE: vendor/github.com/containerd/platforms/database.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package platforms import ( "runtime" "strings" ) // These function are generated from https://golang.org/src/go/build/syslist.go. // // We use switch statements because they are slightly faster than map lookups // and use a little less memory. // isKnownOS returns true if we know about the operating system. // // The OS value should be normalized before calling this function. func isKnownOS(os string) bool { switch os { case "aix", "android", "darwin", "dragonfly", "freebsd", "hurd", "illumos", "ios", "js", "linux", "nacl", "netbsd", "openbsd", "plan9", "solaris", "windows", "zos": return true } return false } // isArmArch returns true if the architecture is ARM. // // The arch value should be normalized before being passed to this function. func isArmArch(arch string) bool { switch arch { case "arm", "arm64": return true } return false } // isKnownArch returns true if we know about the architecture. // // The arch value should be normalized before being passed to this function. func isKnownArch(arch string) bool { switch arch { case "386", "amd64", "amd64p32", "arm", "armbe", "arm64", "arm64be", "ppc64", "ppc64le", "loong64", "mips", "mipsle", "mips64", "mips64le", "mips64p32", "mips64p32le", "ppc", "riscv", "riscv64", "s390", "s390x", "sparc", "sparc64", "wasm": return true } return false } func normalizeOS(os string) string { if os == "" { return runtime.GOOS } os = strings.ToLower(os) switch os { case "macos": os = "darwin" } return os } // normalizeArch normalizes the architecture. func normalizeArch(arch, variant string) (string, string) { arch, variant = strings.ToLower(arch), strings.ToLower(variant) switch arch { case "i386": arch = "386" variant = "" case "x86_64", "x86-64", "amd64": arch = "amd64" if variant == "v1" { variant = "" } case "aarch64", "arm64": arch = "arm64" switch variant { case "8", "v8", "v8.0": variant = "" case "9", "9.0", "v9.0": variant = "v9" } case "armhf": arch = "arm" variant = "v7" case "armel": arch = "arm" variant = "v6" case "arm": switch variant { case "", "7": variant = "v7" case "5", "6", "8": variant = "v" + variant } } return arch, variant } ================================================ FILE: vendor/github.com/containerd/platforms/defaults.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package platforms // DefaultString returns the default string specifier for the platform, // with [PR#6](https://github.com/containerd/platforms/pull/6) the result // may now also include the OSVersion from the provided platform specification. func DefaultString() string { return FormatAll(DefaultSpec()) } // DefaultStrict returns strict form of Default. func DefaultStrict() MatchComparer { return OnlyStrict(DefaultSpec()) } ================================================ FILE: vendor/github.com/containerd/platforms/defaults_darwin.go ================================================ //go:build darwin /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package platforms import ( "runtime" specs "github.com/opencontainers/image-spec/specs-go/v1" ) // DefaultSpec returns the current platform's default platform specification. func DefaultSpec() specs.Platform { return specs.Platform{ OS: runtime.GOOS, Architecture: runtime.GOARCH, // The Variant field will be empty if arch != ARM. Variant: cpuVariant(), } } // Default returns the default matcher for the platform. func Default() MatchComparer { return Ordered(DefaultSpec(), specs.Platform{ // darwin runtime also supports Linux binary via runu/LKL OS: "linux", Architecture: runtime.GOARCH, }) } ================================================ FILE: vendor/github.com/containerd/platforms/defaults_freebsd.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package platforms import ( "runtime" specs "github.com/opencontainers/image-spec/specs-go/v1" ) // DefaultSpec returns the current platform's default platform specification. func DefaultSpec() specs.Platform { return specs.Platform{ OS: runtime.GOOS, Architecture: runtime.GOARCH, // The Variant field will be empty if arch != ARM. Variant: cpuVariant(), } } // Default returns the default matcher for the platform. func Default() MatchComparer { return Ordered(DefaultSpec(), specs.Platform{ OS: "linux", Architecture: runtime.GOARCH, // The Variant field will be empty if arch != ARM. Variant: cpuVariant(), }) } ================================================ FILE: vendor/github.com/containerd/platforms/defaults_unix.go ================================================ //go:build !windows && !darwin && !freebsd /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package platforms import ( "runtime" specs "github.com/opencontainers/image-spec/specs-go/v1" ) // DefaultSpec returns the current platform's default platform specification. func DefaultSpec() specs.Platform { return specs.Platform{ OS: runtime.GOOS, Architecture: runtime.GOARCH, // The Variant field will be empty if arch != ARM. Variant: cpuVariant(), } } // Default returns the default matcher for the platform. func Default() MatchComparer { return Only(DefaultSpec()) } ================================================ FILE: vendor/github.com/containerd/platforms/defaults_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 platforms import ( "fmt" "runtime" specs "github.com/opencontainers/image-spec/specs-go/v1" "golang.org/x/sys/windows" ) // DefaultSpec returns the current platform's default platform specification. func DefaultSpec() specs.Platform { major, minor, build := windows.RtlGetNtVersionNumbers() return specs.Platform{ OS: runtime.GOOS, Architecture: runtime.GOARCH, OSVersion: fmt.Sprintf("%d.%d.%d", major, minor, build), // The Variant field will be empty if arch != ARM. Variant: cpuVariant(), } } // Default returns the current platform's default platform specification. func Default() MatchComparer { return &windowsMatchComparer{Matcher: NewMatcher(DefaultSpec())} } ================================================ FILE: vendor/github.com/containerd/platforms/errors.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package platforms import "errors" // These errors mirror the errors defined in [github.com/containerd/containerd/errdefs], // however, they are not exported as they are not expected to be used as sentinel // errors by consumers of this package. // //nolint:unused // not all errors are used on all platforms. var ( errNotFound = errors.New("not found") errInvalidArgument = errors.New("invalid argument") errNotImplemented = errors.New("not implemented") ) ================================================ FILE: vendor/github.com/containerd/platforms/platform_windows_compat.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package platforms import ( "strconv" "strings" specs "github.com/opencontainers/image-spec/specs-go/v1" ) // windowsOSVersion is a wrapper for Windows version information // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724439(v=vs.85).aspx type windowsOSVersion struct { Version uint32 MajorVersion uint8 MinorVersion uint8 Build uint16 } // Windows Client and Server build numbers. // // See: // https://learn.microsoft.com/en-us/windows/release-health/release-information // https://learn.microsoft.com/en-us/windows/release-health/windows-server-release-info // https://learn.microsoft.com/en-us/windows/release-health/windows11-release-information const ( // rs5 (version 1809, codename "Redstone 5") corresponds to Windows Server // 2019 (ltsc2019), and Windows 10 (October 2018 Update). rs5 = 17763 // ltsc2019 (Windows Server 2019) is an alias for [RS5]. ltsc2019 = rs5 // v21H2Server corresponds to Windows Server 2022 (ltsc2022). v21H2Server = 20348 // ltsc2022 (Windows Server 2022) is an alias for [v21H2Server] ltsc2022 = v21H2Server // v22H2Win11 corresponds to Windows 11 (2022 Update). v22H2Win11 = 22621 // v23H2 is the 23H2 release in the Windows Server annual channel. v23H2 = 25398 // Windows Server 2025 build 26100 v25H1Server = 26100 ltsc2025 = v25H1Server ) // List of stable ABI compliant ltsc releases // Note: List must be sorted in ascending order var compatLTSCReleases = []uint16{ ltsc2022, ltsc2025, } // CheckHostAndContainerCompat checks if given host and container // OS versions are compatible. // It includes support for stable ABI compliant versions as well. // Every release after WS 2022 will support the previous ltsc // container image. Stable ABI is in preview mode for windows 11 client. // Refer: https://learn.microsoft.com/en-us/virtualization/windowscontainers/deploy-containers/version-compatibility?tabs=windows-server-2022%2Cwindows-10#windows-server-host-os-compatibility func checkWindowsHostAndContainerCompat(host, ctr windowsOSVersion) bool { // check major minor versions of host and guest if host.MajorVersion != ctr.MajorVersion || host.MinorVersion != ctr.MinorVersion { return false } // If host is < WS 2022, exact version match is required if host.Build < ltsc2022 { return host.Build == ctr.Build } // Find the latest LTSC version that is earlier than the host version. // This is the earliest version of container that the host can run. // // If the host version is an LTSC, then it supports compatibility with // everything from the previous LTSC up to itself, so we want supportedLTSCRelease // to be the previous entry. // // If no match is found, then we know that the host is LTSC2022 exactly, // since we already checked that it's not less than LTSC2022. var supportedLTSCRelease uint16 = ltsc2022 for i := len(compatLTSCReleases) - 1; i >= 0; i-- { if host.Build > compatLTSCReleases[i] { supportedLTSCRelease = compatLTSCReleases[i] break } } return supportedLTSCRelease <= ctr.Build && ctr.Build <= host.Build } func getWindowsOSVersion(osVersionPrefix string) windowsOSVersion { if strings.Count(osVersionPrefix, ".") < 2 { return windowsOSVersion{} } major, extra, _ := strings.Cut(osVersionPrefix, ".") minor, extra, _ := strings.Cut(extra, ".") build, _, _ := strings.Cut(extra, ".") majorVersion, err := strconv.ParseUint(major, 10, 8) if err != nil { return windowsOSVersion{} } minorVersion, err := strconv.ParseUint(minor, 10, 8) if err != nil { return windowsOSVersion{} } buildNumber, err := strconv.ParseUint(build, 10, 16) if err != nil { return windowsOSVersion{} } return windowsOSVersion{ MajorVersion: uint8(majorVersion), MinorVersion: uint8(minorVersion), Build: uint16(buildNumber), } } type windowsVersionMatcher struct { windowsOSVersion } func (m windowsVersionMatcher) Match(v string) bool { if m.isEmpty() || v == "" { return true } osv := getWindowsOSVersion(v) return checkWindowsHostAndContainerCompat(m.windowsOSVersion, osv) } func (m windowsVersionMatcher) isEmpty() bool { return m.MajorVersion == 0 && m.MinorVersion == 0 && m.Build == 0 } type windowsMatchComparer struct { Matcher } func (c *windowsMatchComparer) Less(p1, p2 specs.Platform) bool { m1, m2 := c.Match(p1), c.Match(p2) if m1 && m2 { return p1.OSVersion > p2.OSVersion } return m1 && !m2 } ================================================ FILE: vendor/github.com/containerd/platforms/platforms.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package platforms provides a toolkit for normalizing, matching and // specifying container platforms. // // Centered around OCI platform specifications, we define a string-based // specifier syntax that can be used for user input. With a specifier, users // only need to specify the parts of the platform that are relevant to their // context, providing an operating system or architecture or both. // // How do I use this package? // // The vast majority of use cases should simply use the match function with // user input. The first step is to parse a specifier into a matcher: // // m, err := Parse("linux") // if err != nil { ... } // // Once you have a matcher, use it to match against the platform declared by a // component, typically from an image or runtime. Since extracting an images // platform is a little more involved, we'll use an example against the // platform default: // // if ok := m.Match(Default()); !ok { /* doesn't match */ } // // This can be composed in loops for resolving runtimes or used as a filter for // fetch and select images. // // More details of the specifier syntax and platform spec follow. // // # Declaring Platform Support // // Components that have strict platform requirements should use the OCI // platform specification to declare their support. Typically, this will be // images and runtimes that should make these declaring which platform they // support specifically. This looks roughly as follows: // // type Platform struct { // Architecture string // OS string // Variant string // } // // Most images and runtimes should at least set Architecture and OS, according // to their GOARCH and GOOS values, respectively (follow the OCI image // specification when in doubt). ARM should set variant under certain // discussions, which are outlined below. // // # Platform Specifiers // // While the OCI platform specifications provide a tool for components to // specify structured information, user input typically doesn't need the full // context and much can be inferred. To solve this problem, we introduced // "specifiers". A specifier has the format // `||/[/]`. The user can provide either the // operating system or the architecture or both. // // An example of a common specifier is `linux/amd64`. If the host has a default // of runtime that matches this, the user can simply provide the component that // matters. For example, if a image provides amd64 and arm64 support, the // operating system, `linux` can be inferred, so they only have to provide // `arm64` or `amd64`. Similar behavior is implemented for operating systems, // where the architecture may be known but a runtime may support images from // different operating systems. // // # Normalization // // Because not all users are familiar with the way the Go runtime represents // platforms, several normalizations have been provided to make this package // easier to user. // // The following are performed for architectures: // // Value Normalized // aarch64 arm64 // armhf arm // armel arm/v6 // i386 386 // x86_64 amd64 // x86-64 amd64 // // We also normalize the operating system `macos` to `darwin`. // // # ARM Support // // To qualify ARM architecture, the Variant field is used to qualify the arm // version. The most common arm version, v7, is represented without the variant // unless it is explicitly provided. This is treated as equivalent to armhf. A // previous architecture, armel, will be normalized to arm/v6. // // Similarly, the most common arm64 version v8, and most common amd64 version v1 // are represented without the variant. // // While these normalizations are provided, their support on arm platforms has // not yet been fully implemented and tested. package platforms import ( "fmt" "path" "regexp" "runtime" "strconv" "strings" specs "github.com/opencontainers/image-spec/specs-go/v1" ) var ( specifierRe = regexp.MustCompile(`^[A-Za-z0-9_.-]+$`) osAndVersionRe = regexp.MustCompile(`^([A-Za-z0-9_-]+)(?:\(([A-Za-z0-9_.-]*)\))?$`) ) const osAndVersionFormat = "%s(%s)" // Platform is a type alias for convenience, so there is no need to import image-spec package everywhere. type Platform = specs.Platform // Matcher matches platforms specifications, provided by an image or runtime. type Matcher interface { Match(platform specs.Platform) bool } // NewMatcher returns a simple matcher based on the provided platform // specification. The returned matcher only looks for equality based on os, // architecture and variant. // // One may implement their own matcher if this doesn't provide the required // functionality. // // Applications should opt to use `Match` over directly parsing specifiers. func NewMatcher(platform specs.Platform) Matcher { m := &matcher{ Platform: Normalize(platform), } if platform.OS == "windows" { m.osvM = &windowsVersionMatcher{ windowsOSVersion: getWindowsOSVersion(platform.OSVersion), } // In prior versions, on windows, the returned matcher implements a // MatchComprarer interface. // This preserves that behavior for backwards compatibility. // // TODO: This isn't actually used in this package, except for a test case, // which may have been an unintended side of some refactor. // It was likely intended to be used in `Ordered` but it is not since // `Less` that is implemented here ends up getting masked due to wrapping. if runtime.GOOS == "windows" { return &windowsMatchComparer{m} } } return m } type osVerMatcher interface { Match(string) bool } type matcher struct { specs.Platform osvM osVerMatcher } func (m *matcher) Match(platform specs.Platform) bool { normalized := Normalize(platform) return m.OS == normalized.OS && m.Architecture == normalized.Architecture && m.Variant == normalized.Variant && m.matchOSVersion(platform) } func (m *matcher) matchOSVersion(platform specs.Platform) bool { if m.osvM != nil { return m.osvM.Match(platform.OSVersion) } return true } func (m *matcher) String() string { return FormatAll(m.Platform) } // ParseAll parses a list of platform specifiers into a list of platform. func ParseAll(specifiers []string) ([]specs.Platform, error) { platforms := make([]specs.Platform, len(specifiers)) for i, s := range specifiers { p, err := Parse(s) if err != nil { return nil, fmt.Errorf("invalid platform %s: %w", s, err) } platforms[i] = p } return platforms, nil } // Parse parses the platform specifier syntax into a platform declaration. // // Platform specifiers are in the format `[()]||[()]/[/]`. // The minimum required information for a platform specifier is the operating // system or architecture. The OSVersion can be part of the OS like `windows(10.0.17763)` // When an OSVersion is specified, then specs.Platform.OSVersion is populated with that value, // and an empty string otherwise. // If there is only a single string (no slashes), the // value will be matched against the known set of operating systems, then fall // back to the known set of architectures. The missing component will be // inferred based on the local environment. func Parse(specifier string) (specs.Platform, error) { if strings.Contains(specifier, "*") { // TODO(stevvooe): need to work out exact wildcard handling return specs.Platform{}, fmt.Errorf("%q: wildcards not yet supported: %w", specifier, errInvalidArgument) } // Limit to 4 elements to prevent unbounded split parts := strings.SplitN(specifier, "/", 4) var p specs.Platform for i, part := range parts { if i == 0 { // First element is [()] osVer := osAndVersionRe.FindStringSubmatch(part) if osVer == nil { return specs.Platform{}, fmt.Errorf("%q is an invalid OS component of %q: OSAndVersion specifier component must match %q: %w", part, specifier, osAndVersionRe.String(), errInvalidArgument) } p.OS = normalizeOS(osVer[1]) p.OSVersion = osVer[2] } else { if !specifierRe.MatchString(part) { return specs.Platform{}, fmt.Errorf("%q is an invalid component of %q: platform specifier component must match %q: %w", part, specifier, specifierRe.String(), errInvalidArgument) } } } switch len(parts) { case 1: // in this case, we will test that the value might be an OS (with or // without the optional OSVersion specified) and look it up. // If it is not known, we'll treat it as an architecture. Since // we have very little information about the platform here, we are // going to be a little more strict if we don't know about the argument // value. if isKnownOS(p.OS) { // picks a default architecture p.Architecture = runtime.GOARCH if p.Architecture == "arm" && cpuVariant() != "v7" { p.Variant = cpuVariant() } return p, nil } p.Architecture, p.Variant = normalizeArch(parts[0], "") if p.Architecture == "arm" && p.Variant == "v7" { p.Variant = "" } if isKnownArch(p.Architecture) { p.OS = runtime.GOOS return p, nil } return specs.Platform{}, fmt.Errorf("%q: unknown operating system or architecture: %w", specifier, errInvalidArgument) case 2: // In this case, we treat as a regular OS[(OSVersion)]/arch pair. We don't care // about whether or not we know of the platform. p.Architecture, p.Variant = normalizeArch(parts[1], "") if p.Architecture == "arm" && p.Variant == "v7" { p.Variant = "" } return p, nil case 3: // we have a fully specified variant, this is rare p.Architecture, p.Variant = normalizeArch(parts[1], parts[2]) if p.Architecture == "arm64" && p.Variant == "" { p.Variant = "v8" } return p, nil } return specs.Platform{}, fmt.Errorf("%q: cannot parse platform specifier: %w", specifier, errInvalidArgument) } // MustParse is like Parses but panics if the specifier cannot be parsed. // Simplifies initialization of global variables. func MustParse(specifier string) specs.Platform { p, err := Parse(specifier) if err != nil { panic("platform: Parse(" + strconv.Quote(specifier) + "): " + err.Error()) } return p } // Format returns a string specifier from the provided platform specification. func Format(platform specs.Platform) string { if platform.OS == "" { return "unknown" } return path.Join(platform.OS, platform.Architecture, platform.Variant) } // FormatAll returns a string specifier that also includes the OSVersion from the // provided platform specification. func FormatAll(platform specs.Platform) string { if platform.OS == "" { return "unknown" } if platform.OSVersion != "" { OSAndVersion := fmt.Sprintf(osAndVersionFormat, platform.OS, platform.OSVersion) return path.Join(OSAndVersion, platform.Architecture, platform.Variant) } return path.Join(platform.OS, platform.Architecture, platform.Variant) } // Normalize validates and translate the platform to the canonical value. // // For example, if "Aarch64" is encountered, we change it to "arm64" or if // "x86_64" is encountered, it becomes "amd64". func Normalize(platform specs.Platform) specs.Platform { platform.OS = normalizeOS(platform.OS) platform.Architecture, platform.Variant = normalizeArch(platform.Architecture, platform.Variant) return platform } ================================================ FILE: vendor/github.com/containerd/stargz-snapshotter/estargz/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: vendor/github.com/containerd/stargz-snapshotter/estargz/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. */ /* Copyright 2019 The Go Authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. */ package estargz import ( "archive/tar" "bytes" "compress/gzip" "context" "errors" "fmt" "io" "os" "path" "runtime" "strings" "sync" "sync/atomic" "github.com/containerd/stargz-snapshotter/estargz/errorutil" "github.com/klauspost/compress/zstd" digest "github.com/opencontainers/go-digest" "golang.org/x/sync/errgroup" ) type GzipHelperFunc func(io.Reader) (io.ReadCloser, error) type options struct { chunkSize int compressionLevel int prioritizedFiles []string missedPrioritizedFiles *[]string compression Compression ctx context.Context minChunkSize int gzipHelperFunc GzipHelperFunc } type Option func(o *options) error // WithChunkSize option specifies the chunk size of eStargz blob to build. func WithChunkSize(chunkSize int) Option { return func(o *options) error { o.chunkSize = chunkSize return nil } } // WithCompressionLevel option specifies the gzip compression level. // The default is gzip.BestCompression. // This option will be ignored if WithCompression option is used. // See also: https://godoc.org/compress/gzip#pkg-constants func WithCompressionLevel(level int) Option { return func(o *options) error { o.compressionLevel = level return nil } } // WithPrioritizedFiles option specifies the list of prioritized files. // These files must be complete paths that are absolute or relative to "/" // For example, all of "foo/bar", "/foo/bar", "./foo/bar" and "../foo/bar" // are treated as "/foo/bar". func WithPrioritizedFiles(files []string) Option { return func(o *options) error { o.prioritizedFiles = files return nil } } // WithAllowPrioritizeNotFound makes Build continue the execution even if some // of prioritized files specified by WithPrioritizedFiles option aren't found // in the input tar. Instead, this records all missed file names to the passed // slice. func WithAllowPrioritizeNotFound(missedFiles *[]string) Option { return func(o *options) error { if missedFiles == nil { return fmt.Errorf("WithAllowPrioritizeNotFound: slice must be passed") } o.missedPrioritizedFiles = missedFiles return nil } } // WithCompression specifies compression algorithm to be used. // Default is gzip. func WithCompression(compression Compression) Option { return func(o *options) error { o.compression = compression return nil } } // WithContext specifies a context that can be used for clean canceleration. func WithContext(ctx context.Context) Option { return func(o *options) error { o.ctx = ctx return nil } } // WithMinChunkSize option specifies the minimal number of bytes of data // must be written in one gzip stream. // By increasing this number, one gzip stream can contain multiple files // and it hopefully leads to smaller result blob. // NOTE: This adds a TOC property that old reader doesn't understand. func WithMinChunkSize(minChunkSize int) Option { return func(o *options) error { o.minChunkSize = minChunkSize return nil } } // WithGzipHelperFunc option specifies a custom function to decompress gzip-compressed layers. // When a gzip-compressed layer is detected, this function will be used instead of the // Go standard library gzip decompression for better performance. // The function should take an io.Reader as input and return an io.ReadCloser. // If nil, the Go standard library gzip.NewReader will be used. func WithGzipHelperFunc(gzipHelperFunc GzipHelperFunc) Option { return func(o *options) error { o.gzipHelperFunc = gzipHelperFunc return nil } } // Blob is an eStargz blob. type Blob struct { io.ReadCloser diffID digest.Digester tocDigest digest.Digest readCompleted *atomic.Bool uncompressedSize *atomic.Int64 } // DiffID returns the digest of uncompressed blob. // It is only valid to call DiffID after Close. func (b *Blob) DiffID() digest.Digest { return b.diffID.Digest() } // TOCDigest returns the digest of uncompressed TOC JSON. func (b *Blob) TOCDigest() digest.Digest { return b.tocDigest } // UncompressedSize returns the size of uncompressed blob. // UncompressedSize should only be called after the blob has been fully read. func (b *Blob) UncompressedSize() (int64, error) { switch { case b.uncompressedSize == nil || b.readCompleted == nil: return -1, fmt.Errorf("readCompleted or uncompressedSize is not initialized") case !b.readCompleted.Load(): return -1, fmt.Errorf("called UncompressedSize before the blob has been fully read") default: return b.uncompressedSize.Load(), nil } } // Build builds an eStargz blob which is an extended version of stargz, from a blob (gzip, zstd // or plain tar) passed through the argument. If there are some prioritized files are listed in // the option, these files are grouped as "prioritized" and can be used for runtime optimization // (e.g. prefetch). This function builds a blob in parallel, with dividing that blob into several // (at least the number of runtime.GOMAXPROCS(0)) sub-blobs. func Build(tarBlob *io.SectionReader, opt ...Option) (_ *Blob, rErr error) { var opts options opts.compressionLevel = gzip.BestCompression // BestCompression by default for _, o := range opt { if err := o(&opts); err != nil { return nil, err } } if opts.compression == nil { opts.compression = newGzipCompressionWithLevel(opts.compressionLevel) } layerFiles := newTempFiles() ctx := opts.ctx if ctx == nil { ctx = context.Background() } done := make(chan struct{}) defer close(done) go func() { select { case <-done: // nop case <-ctx.Done(): layerFiles.CleanupAll() } }() defer func() { if rErr != nil { if err := layerFiles.CleanupAll(); err != nil { rErr = fmt.Errorf("failed to cleanup tmp files: %v: %w", err, rErr) } } if cErr := ctx.Err(); cErr != nil { rErr = fmt.Errorf("error from context %q: %w", cErr, rErr) } }() tarBlob, err := decompressBlob(tarBlob, layerFiles, opts.gzipHelperFunc) if err != nil { return nil, err } entries, err := sortEntries(tarBlob, opts.prioritizedFiles, opts.missedPrioritizedFiles) if err != nil { return nil, err } var tarParts [][]*entry if opts.minChunkSize > 0 { // Each entry needs to know the size of the current gzip stream so they // cannot be processed in parallel. tarParts = [][]*entry{entries} } else { tarParts = divideEntries(entries, runtime.GOMAXPROCS(0)) } writers := make([]*Writer, len(tarParts)) payloads := make([]*os.File, len(tarParts)) var mu sync.Mutex var eg errgroup.Group for i, parts := range tarParts { i, parts := i, parts // builds verifiable stargz sub-blobs eg.Go(func() error { esgzFile, err := layerFiles.TempFile("", "esgzdata") if err != nil { return err } sw := NewWriterWithCompressor(esgzFile, opts.compression) sw.ChunkSize = opts.chunkSize sw.MinChunkSize = opts.minChunkSize if sw.needsOpenGzEntries == nil { sw.needsOpenGzEntries = make(map[string]struct{}) } for _, f := range []string{PrefetchLandmark, NoPrefetchLandmark} { sw.needsOpenGzEntries[f] = struct{}{} } if err := sw.AppendTar(readerFromEntries(parts...)); err != nil { return err } mu.Lock() writers[i] = sw payloads[i] = esgzFile mu.Unlock() return nil }) } if err := eg.Wait(); err != nil { rErr = err return nil, err } tocAndFooter, tocDgst, err := closeWithCombine(writers...) if err != nil { rErr = err return nil, err } var rs []io.Reader for _, p := range payloads { fs, err := fileSectionReader(p) if err != nil { return nil, err } rs = append(rs, fs) } diffID := digest.Canonical.Digester() pr, pw := io.Pipe() readCompleted := new(atomic.Bool) uncompressedSize := new(atomic.Int64) go func() { var size int64 var decompressFunc func(io.Reader) (io.ReadCloser, error) if _, ok := opts.compression.(*gzipCompression); ok && opts.gzipHelperFunc != nil { decompressFunc = opts.gzipHelperFunc } else { decompressFunc = opts.compression.Reader } decompressR, err := decompressFunc(io.TeeReader(io.MultiReader(append(rs, tocAndFooter)...), pw)) if err != nil { pw.CloseWithError(err) return } defer decompressR.Close() if size, err = io.Copy(diffID.Hash(), decompressR); err != nil { pw.CloseWithError(err) return } uncompressedSize.Store(size) readCompleted.Store(true) pw.Close() }() return &Blob{ ReadCloser: readCloser{ Reader: pr, closeFunc: layerFiles.CleanupAll, }, tocDigest: tocDgst, diffID: diffID, readCompleted: readCompleted, uncompressedSize: uncompressedSize, }, nil } // closeWithCombine takes unclosed Writers and close them. This also returns the // toc that combined all Writers into. // Writers doesn't write TOC and footer to the underlying writers so they can be // combined into a single eStargz and tocAndFooter returned by this function can // be appended at the tail of that combined blob. func closeWithCombine(ws ...*Writer) (tocAndFooterR io.Reader, tocDgst digest.Digest, err error) { if len(ws) == 0 { return nil, "", fmt.Errorf("at least one writer must be passed") } for _, w := range ws { if w.closed { return nil, "", fmt.Errorf("writer must be unclosed") } defer func(w *Writer) { w.closed = true }(w) if err := w.closeGz(); err != nil { return nil, "", err } if err := w.bw.Flush(); err != nil { return nil, "", err } } var ( mtoc = new(JTOC) currentOffset int64 ) mtoc.Version = ws[0].toc.Version for _, w := range ws { for _, e := range w.toc.Entries { // Recalculate Offset of non-empty files/chunks if (e.Type == "reg" && e.Size > 0) || e.Type == "chunk" { e.Offset += currentOffset } mtoc.Entries = append(mtoc.Entries, e) } if w.toc.Version > mtoc.Version { mtoc.Version = w.toc.Version } currentOffset += w.cw.n } return tocAndFooter(ws[0].compressor, mtoc, currentOffset) } func tocAndFooter(compressor Compressor, toc *JTOC, offset int64) (io.Reader, digest.Digest, error) { buf := new(bytes.Buffer) tocDigest, err := compressor.WriteTOCAndFooter(buf, offset, toc, nil) if err != nil { return nil, "", err } return buf, tocDigest, nil } // divideEntries divides passed entries to the parts at least the number specified by the // argument. func divideEntries(entries []*entry, minPartsNum int) (set [][]*entry) { var estimatedSize int64 for _, e := range entries { estimatedSize += e.header.Size } unitSize := estimatedSize / int64(minPartsNum) var ( nextEnd = unitSize offset int64 ) set = append(set, []*entry{}) for _, e := range entries { set[len(set)-1] = append(set[len(set)-1], e) offset += e.header.Size if offset > nextEnd { set = append(set, []*entry{}) nextEnd += unitSize } } return } var errNotFound = errors.New("not found") // sortEntries reads the specified tar blob and returns a list of tar entries. // If some of prioritized files are specified, the list starts from these // files with keeping the order specified by the argument. func sortEntries(in io.ReaderAt, prioritized []string, missedPrioritized *[]string) ([]*entry, error) { // Import tar file. intar, err := importTar(in) if err != nil { return nil, fmt.Errorf("failed to sort: %w", err) } // Sort the tar file respecting to the prioritized files list. sorted := &tarFile{} picked := make(map[string]struct{}) for _, l := range prioritized { if err := moveRec(l, intar, sorted, picked); err != nil { if errors.Is(err, errNotFound) && missedPrioritized != nil { *missedPrioritized = append(*missedPrioritized, l) continue // allow not found } return nil, fmt.Errorf("failed to sort tar entries: %w", err) } } if len(prioritized) == 0 { sorted.add(&entry{ header: &tar.Header{ Name: NoPrefetchLandmark, Typeflag: tar.TypeReg, Size: int64(len([]byte{landmarkContents})), }, payload: bytes.NewReader([]byte{landmarkContents}), }) } else { sorted.add(&entry{ header: &tar.Header{ Name: PrefetchLandmark, Typeflag: tar.TypeReg, Size: int64(len([]byte{landmarkContents})), }, payload: bytes.NewReader([]byte{landmarkContents}), }) } // Dump prioritized entries followed by the rest entries while skipping picked ones. return append(sorted.dump(nil), intar.dump(picked)...), nil } // readerFromEntries returns a reader of tar archive that contains entries passed // through the arguments. func readerFromEntries(entries ...*entry) io.Reader { pr, pw := io.Pipe() go func() { tw := tar.NewWriter(pw) defer tw.Close() for _, entry := range entries { if err := tw.WriteHeader(entry.header); err != nil { pw.CloseWithError(fmt.Errorf("failed to write tar header: %v", err)) return } if _, err := io.Copy(tw, entry.payload); err != nil { pw.CloseWithError(fmt.Errorf("failed to write tar payload: %v", err)) return } } pw.Close() }() return pr } func importTar(in io.ReaderAt) (*tarFile, error) { tf := &tarFile{} pw, err := newCountReadSeeker(in) if err != nil { return nil, fmt.Errorf("failed to make position watcher: %w", err) } tr := tar.NewReader(pw) // Walk through all nodes. for { // Fetch and parse next header. h, err := tr.Next() if err != nil { if err == io.EOF { break } return nil, fmt.Errorf("failed to parse tar file, %w", err) } switch cleanEntryName(h.Name) { case PrefetchLandmark, NoPrefetchLandmark: // Ignore existing landmark continue } // Add entry. If it already exists, replace it. if _, ok := tf.get(h.Name); ok { tf.remove(h.Name) } tf.add(&entry{ header: h, payload: io.NewSectionReader(in, pw.currentPos(), h.Size), }) } return tf, nil } func moveRec(name string, in *tarFile, out *tarFile, picked map[string]struct{}) error { name = cleanEntryName(name) if name == "" { // root directory. stop recursion. if e, ok := in.get(name); ok { // entry of the root directory exists. we should move it as well. // this case will occur if tar entries are prefixed with "./", "/", etc. if _, done := picked[name]; !done { out.add(e) picked[name] = struct{}{} } } return nil } _, okIn := in.get(name) _, okOut := out.get(name) _, okPicked := picked[name] if !okIn && !okOut && !okPicked { return fmt.Errorf("file: %q: %w", name, errNotFound) } parent, _ := path.Split(strings.TrimSuffix(name, "/")) if err := moveRec(parent, in, out, picked); err != nil { return err } if e, ok := in.get(name); ok && e.header.Typeflag == tar.TypeLink { if err := moveRec(e.header.Linkname, in, out, picked); err != nil { return err } } if _, done := picked[name]; done { return nil } if e, ok := in.get(name); ok { out.add(e) picked[name] = struct{}{} } return nil } type entry struct { header *tar.Header payload io.ReadSeeker } type tarFile struct { index map[string]*entry stream []*entry } func (f *tarFile) add(e *entry) { if f.index == nil { f.index = make(map[string]*entry) } f.index[cleanEntryName(e.header.Name)] = e f.stream = append(f.stream, e) } func (f *tarFile) remove(name string) { name = cleanEntryName(name) if f.index != nil { delete(f.index, name) } var filtered []*entry for _, e := range f.stream { if cleanEntryName(e.header.Name) == name { continue } filtered = append(filtered, e) } f.stream = filtered } func (f *tarFile) get(name string) (e *entry, ok bool) { if f.index == nil { return nil, false } e, ok = f.index[cleanEntryName(name)] return } func (f *tarFile) dump(skip map[string]struct{}) []*entry { if len(skip) == 0 { return f.stream } var out []*entry for _, e := range f.stream { if _, ok := skip[cleanEntryName(e.header.Name)]; ok { continue } out = append(out, e) } return out } type readCloser struct { io.Reader closeFunc func() error } func (rc readCloser) Close() error { return rc.closeFunc() } func fileSectionReader(file *os.File) (*io.SectionReader, error) { info, err := file.Stat() if err != nil { return nil, err } return io.NewSectionReader(file, 0, info.Size()), nil } func newTempFiles() *tempFiles { return &tempFiles{} } type tempFiles struct { files []*os.File filesMu sync.Mutex cleanupOnce sync.Once } func (tf *tempFiles) TempFile(dir, pattern string) (*os.File, error) { f, err := os.CreateTemp(dir, pattern) if err != nil { return nil, err } tf.filesMu.Lock() tf.files = append(tf.files, f) tf.filesMu.Unlock() return f, nil } func (tf *tempFiles) CleanupAll() (err error) { tf.cleanupOnce.Do(func() { err = tf.cleanupAll() }) return } func (tf *tempFiles) cleanupAll() error { tf.filesMu.Lock() defer tf.filesMu.Unlock() var allErr []error for _, f := range tf.files { if err := f.Close(); err != nil { allErr = append(allErr, err) } if err := os.Remove(f.Name()); err != nil { allErr = append(allErr, err) } } tf.files = nil return errorutil.Aggregate(allErr) } func newCountReadSeeker(r io.ReaderAt) (*countReadSeeker, error) { pos := int64(0) return &countReadSeeker{r: r, cPos: &pos}, nil } type countReadSeeker struct { r io.ReaderAt cPos *int64 mu sync.Mutex } func (cr *countReadSeeker) Read(p []byte) (int, error) { cr.mu.Lock() defer cr.mu.Unlock() n, err := cr.r.ReadAt(p, *cr.cPos) if err == nil { *cr.cPos += int64(n) } return n, err } func (cr *countReadSeeker) Seek(offset int64, whence int) (int64, error) { cr.mu.Lock() defer cr.mu.Unlock() switch whence { default: return 0, fmt.Errorf("unknown whence: %v", whence) case io.SeekStart: case io.SeekCurrent: offset += *cr.cPos case io.SeekEnd: return 0, fmt.Errorf("unsupported whence: %v", whence) } if offset < 0 { return 0, fmt.Errorf("invalid offset") } *cr.cPos = offset return offset, nil } func (cr *countReadSeeker) currentPos() int64 { cr.mu.Lock() defer cr.mu.Unlock() return *cr.cPos } func decompressBlob(org *io.SectionReader, tmp *tempFiles, gzipHelperFunc GzipHelperFunc) (*io.SectionReader, error) { if org.Size() < 4 { return org, nil } src := make([]byte, 4) if _, err := org.Read(src); err != nil && err != io.EOF { return nil, err } var dR io.Reader if bytes.Equal([]byte{0x1F, 0x8B, 0x08}, src[:3]) { // gzip var dgR io.ReadCloser var err error if gzipHelperFunc != nil { dgR, err = gzipHelperFunc(io.NewSectionReader(org, 0, org.Size())) } else { dgR, err = gzip.NewReader(io.NewSectionReader(org, 0, org.Size())) } if err != nil { return nil, err } defer dgR.Close() dR = io.Reader(dgR) } else if bytes.Equal([]byte{0x28, 0xb5, 0x2f, 0xfd}, src[:4]) { // zstd dzR, err := zstd.NewReader(io.NewSectionReader(org, 0, org.Size())) if err != nil { return nil, err } defer dzR.Close() dR = io.Reader(dzR) } else { // uncompressed return io.NewSectionReader(org, 0, org.Size()), nil } b, err := tmp.TempFile("", "uncompresseddata") if err != nil { return nil, err } if _, err := io.Copy(b, dR); err != nil { return nil, err } return fileSectionReader(b) } ================================================ FILE: vendor/github.com/containerd/stargz-snapshotter/estargz/errorutil/errors.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package errorutil import ( "errors" "fmt" "strings" ) // Aggregate combines a list of errors into a single new error. func Aggregate(errs []error) error { switch len(errs) { case 0: return nil case 1: return errs[0] default: points := make([]string, len(errs)+1) points[0] = fmt.Sprintf("%d error(s) occurred:", len(errs)) for i, err := range errs { points[i+1] = fmt.Sprintf("* %s", err) } return errors.New(strings.Join(points, "\n\t")) } } ================================================ FILE: vendor/github.com/containerd/stargz-snapshotter/estargz/estargz.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ /* Copyright 2019 The Go Authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. */ package estargz import ( "bufio" "bytes" "compress/gzip" "crypto/sha256" "errors" "fmt" "hash" "io" "os" "path" "sort" "strings" "sync" "time" "github.com/containerd/stargz-snapshotter/estargz/errorutil" digest "github.com/opencontainers/go-digest" "github.com/vbatts/tar-split/archive/tar" ) // A Reader permits random access reads from a stargz file. type Reader struct { sr *io.SectionReader toc *JTOC tocDigest digest.Digest // m stores all non-chunk entries, keyed by name. m map[string]*TOCEntry // chunks stores all TOCEntry values for regular files that // are split up. For a file with a single chunk, it's only // stored in m. chunks map[string][]*TOCEntry decompressor Decompressor } type openOpts struct { tocOffset int64 decompressors []Decompressor telemetry *Telemetry } // OpenOption is an option used during opening the layer type OpenOption func(o *openOpts) error // WithTOCOffset option specifies the offset of TOC func WithTOCOffset(tocOffset int64) OpenOption { return func(o *openOpts) error { o.tocOffset = tocOffset return nil } } // WithDecompressors option specifies decompressors to use. // Default is gzip-based decompressor. func WithDecompressors(decompressors ...Decompressor) OpenOption { return func(o *openOpts) error { o.decompressors = decompressors return nil } } // WithTelemetry option specifies the telemetry hooks func WithTelemetry(telemetry *Telemetry) OpenOption { return func(o *openOpts) error { o.telemetry = telemetry return nil } } // MeasureLatencyHook is a func which takes start time and records the diff type MeasureLatencyHook func(time.Time) // Telemetry is a struct which defines telemetry hooks. By implementing these hooks you should be able to record // the latency metrics of the respective steps of estargz open operation. To be used with estargz.OpenWithTelemetry(...) type Telemetry struct { GetFooterLatency MeasureLatencyHook // measure time to get stargz footer (in milliseconds) GetTocLatency MeasureLatencyHook // measure time to GET TOC JSON (in milliseconds) DeserializeTocLatency MeasureLatencyHook // measure time to deserialize TOC JSON (in milliseconds) } // Open opens a stargz file for reading. // The behavior is configurable using options. // // Note that each entry name is normalized as the path that is relative to root. func Open(sr *io.SectionReader, opt ...OpenOption) (*Reader, error) { var opts openOpts for _, o := range opt { if err := o(&opts); err != nil { return nil, err } } gzipCompressors := []Decompressor{new(GzipDecompressor), new(LegacyGzipDecompressor)} decompressors := append(gzipCompressors, opts.decompressors...) // Determine the size to fetch. Try to fetch as many bytes as possible. fetchSize := maxFooterSize(sr.Size(), decompressors...) if maybeTocOffset := opts.tocOffset; maybeTocOffset > fetchSize { if maybeTocOffset > sr.Size() { return nil, fmt.Errorf("blob size %d is smaller than the toc offset", sr.Size()) } fetchSize = sr.Size() - maybeTocOffset } start := time.Now() // before getting layer footer footer := make([]byte, fetchSize) if _, err := sr.ReadAt(footer, sr.Size()-fetchSize); err != nil { return nil, fmt.Errorf("error reading footer: %v", err) } if opts.telemetry != nil && opts.telemetry.GetFooterLatency != nil { opts.telemetry.GetFooterLatency(start) } var allErr []error var found bool var r *Reader for _, d := range decompressors { fSize := d.FooterSize() fOffset := positive(int64(len(footer)) - fSize) maybeTocBytes := footer[:fOffset] _, tocOffset, tocSize, err := d.ParseFooter(footer[fOffset:]) if err != nil { allErr = append(allErr, err) continue } if tocOffset >= 0 && tocSize <= 0 { tocSize = sr.Size() - tocOffset - fSize } if tocOffset >= 0 && tocSize < int64(len(maybeTocBytes)) { maybeTocBytes = maybeTocBytes[:tocSize] } r, err = parseTOC(d, sr, tocOffset, tocSize, maybeTocBytes, opts) if err == nil { found = true break } allErr = append(allErr, err) } if !found { return nil, errorutil.Aggregate(allErr) } if err := r.initFields(); err != nil { return nil, fmt.Errorf("failed to initialize fields of entries: %v", err) } return r, nil } // OpenFooter extracts and parses footer from the given blob. // only supports gzip-based eStargz. func OpenFooter(sr *io.SectionReader) (tocOffset int64, footerSize int64, rErr error) { if sr.Size() < FooterSize && sr.Size() < legacyFooterSize { return 0, 0, fmt.Errorf("blob size %d is smaller than the footer size", sr.Size()) } var footer [FooterSize]byte if _, err := sr.ReadAt(footer[:], sr.Size()-FooterSize); err != nil { return 0, 0, fmt.Errorf("error reading footer: %v", err) } var allErr []error for _, d := range []Decompressor{new(GzipDecompressor), new(LegacyGzipDecompressor)} { fSize := d.FooterSize() fOffset := positive(int64(len(footer)) - fSize) _, tocOffset, _, err := d.ParseFooter(footer[fOffset:]) if err == nil { return tocOffset, fSize, err } allErr = append(allErr, err) } return 0, 0, errorutil.Aggregate(allErr) } // initFields populates the Reader from r.toc after decoding it from // JSON. // // Unexported fields are populated and TOCEntry fields that were // implicit in the JSON are populated. func (r *Reader) initFields() error { r.m = make(map[string]*TOCEntry, len(r.toc.Entries)) r.chunks = make(map[string][]*TOCEntry) var lastPath string uname := map[int]string{} gname := map[int]string{} var lastRegEnt *TOCEntry var chunkTopIndex int for i, ent := range r.toc.Entries { ent.Name = cleanEntryName(ent.Name) switch ent.Type { case "reg", "chunk": if ent.Offset != r.toc.Entries[chunkTopIndex].Offset { chunkTopIndex = i } ent.chunkTopIndex = chunkTopIndex } if ent.Type == "reg" { lastRegEnt = ent } if ent.Type == "chunk" { ent.Name = lastPath r.chunks[ent.Name] = append(r.chunks[ent.Name], ent) if ent.ChunkSize == 0 && lastRegEnt != nil { ent.ChunkSize = lastRegEnt.Size - ent.ChunkOffset } } else { lastPath = ent.Name if ent.Uname != "" { uname[ent.UID] = ent.Uname } else { ent.Uname = uname[ent.UID] } if ent.Gname != "" { gname[ent.GID] = ent.Gname } else { ent.Gname = gname[ent.GID] } ent.modTime, _ = time.Parse(time.RFC3339, ent.ModTime3339) if ent.Type == "dir" { ent.NumLink++ // Parent dir links to this directory } r.m[ent.Name] = ent } if ent.Type == "reg" && ent.ChunkSize > 0 && ent.ChunkSize < ent.Size { r.chunks[ent.Name] = make([]*TOCEntry, 0, ent.Size/ent.ChunkSize+1) r.chunks[ent.Name] = append(r.chunks[ent.Name], ent) } if ent.ChunkSize == 0 && ent.Size != 0 { ent.ChunkSize = ent.Size } } // Populate children, add implicit directories: for _, ent := range r.toc.Entries { if ent.Type == "chunk" { continue } // add "foo/": // add "foo" child to "" (creating "" if necessary) // // add "foo/bar/": // add "bar" child to "foo" (creating "foo" if necessary) // // add "foo/bar.txt": // add "bar.txt" child to "foo" (creating "foo" if necessary) // // add "a/b/c/d/e/f.txt": // create "a/b/c/d/e" node // add "f.txt" child to "e" name := ent.Name pdirName := parentDir(name) if name == pdirName { // This entry and its parent are the same. // Ignore this for avoiding infinite loop of the reference. // The example case where this can occur is when tar contains the root // directory itself (e.g. "./", "/"). continue } pdir := r.getOrCreateDir(pdirName) ent.NumLink++ // at least one name(ent.Name) references this entry. if ent.Type == "hardlink" { org, err := r.getSource(ent) if err != nil { return err } org.NumLink++ // original entry is referenced by this ent.Name. ent = org } pdir.addChild(path.Base(name), ent) } lastOffset := r.sr.Size() for i := len(r.toc.Entries) - 1; i >= 0; i-- { e := r.toc.Entries[i] if e.isDataType() { e.nextOffset = lastOffset } if e.Offset != 0 && e.InnerOffset == 0 { lastOffset = e.Offset } } if len(r.m) == 0 { r.m[""] = &TOCEntry{ Name: "", Type: "dir", Mode: 0755, NumLink: 1, } } return nil } func (r *Reader) getSource(ent *TOCEntry) (_ *TOCEntry, err error) { if ent.Type == "hardlink" { org, ok := r.m[cleanEntryName(ent.LinkName)] if !ok { return nil, fmt.Errorf("%q is a hardlink but the linkname %q isn't found", ent.Name, ent.LinkName) } ent, err = r.getSource(org) if err != nil { return nil, err } } return ent, nil } func parentDir(p string) string { dir, _ := path.Split(p) return strings.TrimSuffix(dir, "/") } func (r *Reader) getOrCreateDir(d string) *TOCEntry { e, ok := r.m[d] if !ok { e = &TOCEntry{ Name: d, Type: "dir", Mode: 0755, NumLink: 2, // The directory itself(.) and the parent link to this directory. } r.m[d] = e if d != "" { pdir := r.getOrCreateDir(parentDir(d)) pdir.addChild(path.Base(d), e) } } return e } func (r *Reader) TOCDigest() digest.Digest { return r.tocDigest } // VerifyTOC checks that the TOC JSON in the passed blob matches the // passed digests and that the TOC JSON contains digests for all chunks // contained in the blob. If the verification succceeds, this function // returns TOCEntryVerifier which holds all chunk digests in the stargz blob. func (r *Reader) VerifyTOC(tocDigest digest.Digest) (TOCEntryVerifier, error) { // Verify the digest of TOC JSON if r.tocDigest != tocDigest { return nil, fmt.Errorf("invalid TOC JSON %q; want %q", r.tocDigest, tocDigest) } return r.Verifiers() } // Verifiers returns TOCEntryVerifier of this chunk. Use VerifyTOC instead in most cases // because this doesn't verify TOC. func (r *Reader) Verifiers() (TOCEntryVerifier, error) { chunkDigestMap := make(map[int64]digest.Digest) // map from chunk offset to the chunk digest regDigestMap := make(map[int64]digest.Digest) // map from chunk offset to the reg file digest var chunkDigestMapIncomplete bool var regDigestMapIncomplete bool var containsChunk bool for _, e := range r.toc.Entries { if e.Type != "reg" && e.Type != "chunk" { continue } // offset must be unique in stargz blob _, dOK := chunkDigestMap[e.Offset] _, rOK := regDigestMap[e.Offset] if dOK || rOK { return nil, fmt.Errorf("offset %d found twice", e.Offset) } if e.Type == "reg" { if e.Size == 0 { continue // ignores empty file } // record the digest of regular file payload if e.Digest != "" { d, err := digest.Parse(e.Digest) if err != nil { return nil, fmt.Errorf("failed to parse regular file digest %q: %w", e.Digest, err) } regDigestMap[e.Offset] = d } else { regDigestMapIncomplete = true } } else { containsChunk = true // this layer contains "chunk" entries. } // "reg" also can contain ChunkDigest (e.g. when "reg" is the first entry of // chunked file) if e.ChunkDigest != "" { d, err := digest.Parse(e.ChunkDigest) if err != nil { return nil, fmt.Errorf("failed to parse chunk digest %q: %w", e.ChunkDigest, err) } chunkDigestMap[e.Offset] = d } else { chunkDigestMapIncomplete = true } } if chunkDigestMapIncomplete { // Though some chunk digests are not found, if this layer doesn't contain // "chunk"s and all digest of "reg" files are recorded, we can use them instead. if !containsChunk && !regDigestMapIncomplete { return &verifier{digestMap: regDigestMap}, nil } return nil, fmt.Errorf("some ChunkDigest not found in TOC JSON") } return &verifier{digestMap: chunkDigestMap}, nil } // verifier is an implementation of TOCEntryVerifier which holds verifiers keyed by // offset of the chunk. type verifier struct { digestMap map[int64]digest.Digest digestMapMu sync.Mutex } // Verifier returns a content verifier specified by TOCEntry. func (v *verifier) Verifier(ce *TOCEntry) (digest.Verifier, error) { v.digestMapMu.Lock() defer v.digestMapMu.Unlock() d, ok := v.digestMap[ce.Offset] if !ok { return nil, fmt.Errorf("verifier for offset=%d,size=%d hasn't been registered", ce.Offset, ce.ChunkSize) } return d.Verifier(), nil } // ChunkEntryForOffset returns the TOCEntry containing the byte of the // named file at the given offset within the file. // Name must be absolute path or one that is relative to root. func (r *Reader) ChunkEntryForOffset(name string, offset int64) (e *TOCEntry, ok bool) { name = cleanEntryName(name) e, ok = r.Lookup(name) if !ok || !e.isDataType() { return nil, false } ents := r.chunks[name] if len(ents) < 2 { if offset >= e.ChunkSize { return nil, false } return e, true } i := sort.Search(len(ents), func(i int) bool { e := ents[i] return e.ChunkOffset >= offset || (offset > e.ChunkOffset && offset < e.ChunkOffset+e.ChunkSize) }) if i == len(ents) { return nil, false } return ents[i], true } // Lookup returns the Table of Contents entry for the given path. // // To get the root directory, use the empty string. // Path must be absolute path or one that is relative to root. func (r *Reader) Lookup(path string) (e *TOCEntry, ok bool) { path = cleanEntryName(path) if r == nil { return } e, ok = r.m[path] if ok && e.Type == "hardlink" { var err error e, err = r.getSource(e) if err != nil { return nil, false } } return } // OpenFile returns the reader of the specified file payload. // // Name must be absolute path or one that is relative to root. func (r *Reader) OpenFile(name string) (*io.SectionReader, error) { fr, err := r.newFileReader(name) if err != nil { return nil, err } return io.NewSectionReader(fr, 0, fr.size), nil } func (r *Reader) newFileReader(name string) (*fileReader, error) { name = cleanEntryName(name) ent, ok := r.Lookup(name) if !ok { // TODO: come up with some error plan. This is lazy: return nil, &os.PathError{ Path: name, Op: "OpenFile", Err: os.ErrNotExist, } } if ent.Type != "reg" { return nil, &os.PathError{ Path: name, Op: "OpenFile", Err: errors.New("not a regular file"), } } return &fileReader{ r: r, size: ent.Size, ents: r.getChunks(ent), }, nil } func (r *Reader) OpenFileWithPreReader(name string, preRead func(*TOCEntry, io.Reader) error) (*io.SectionReader, error) { fr, err := r.newFileReader(name) if err != nil { return nil, err } fr.preRead = preRead return io.NewSectionReader(fr, 0, fr.size), nil } func (r *Reader) getChunks(ent *TOCEntry) []*TOCEntry { if ents, ok := r.chunks[ent.Name]; ok { return ents } return []*TOCEntry{ent} } type fileReader struct { r *Reader size int64 ents []*TOCEntry // 1 or more reg/chunk entries preRead func(*TOCEntry, io.Reader) error } func (fr *fileReader) ReadAt(p []byte, off int64) (n int, err error) { if off >= fr.size { return 0, io.EOF } if off < 0 { return 0, errors.New("invalid offset") } var i int if len(fr.ents) > 1 { i = sort.Search(len(fr.ents), func(i int) bool { return fr.ents[i].ChunkOffset >= off }) if i == len(fr.ents) { i = len(fr.ents) - 1 } } ent := fr.ents[i] if ent.ChunkOffset > off { if i == 0 { return 0, errors.New("internal error; first chunk offset is non-zero") } ent = fr.ents[i-1] } // If ent is a chunk of a large file, adjust the ReadAt // offset by the chunk's offset. off -= ent.ChunkOffset finalEnt := fr.ents[len(fr.ents)-1] compressedOff := ent.Offset // compressedBytesRemain is the number of compressed bytes in this // file remaining, over 1+ chunks. compressedBytesRemain := finalEnt.NextOffset() - compressedOff sr := io.NewSectionReader(fr.r.sr, compressedOff, compressedBytesRemain) const maxRead = 2 << 20 var bufSize = maxRead if compressedBytesRemain < maxRead { bufSize = int(compressedBytesRemain) } br := bufio.NewReaderSize(sr, bufSize) if _, err := br.Peek(bufSize); err != nil { return 0, fmt.Errorf("fileReader.ReadAt.peek: %v", err) } dr, err := fr.r.decompressor.Reader(br) if err != nil { return 0, fmt.Errorf("fileReader.ReadAt.decompressor.Reader: %v", err) } defer dr.Close() if fr.preRead == nil { if n, err := io.CopyN(io.Discard, dr, ent.InnerOffset+off); n != ent.InnerOffset+off || err != nil { return 0, fmt.Errorf("discard of %d bytes != %v, %v", ent.InnerOffset+off, n, err) } return io.ReadFull(dr, p) } var retN int var retErr error var found bool var nr int64 for _, e := range fr.r.toc.Entries[ent.chunkTopIndex:] { if !e.isDataType() { continue } if e.Offset != fr.r.toc.Entries[ent.chunkTopIndex].Offset { break } if in, err := io.CopyN(io.Discard, dr, e.InnerOffset-nr); err != nil || in != e.InnerOffset-nr { return 0, fmt.Errorf("discard of remaining %d bytes != %v, %v", e.InnerOffset-nr, in, err) } nr = e.InnerOffset if e == ent { found = true if n, err := io.CopyN(io.Discard, dr, off); n != off || err != nil { return 0, fmt.Errorf("discard of offset %d bytes != %v, %v", off, n, err) } retN, retErr = io.ReadFull(dr, p) nr += off + int64(retN) continue } cr := &countReader{r: io.LimitReader(dr, e.ChunkSize)} if err := fr.preRead(e, cr); err != nil { return 0, fmt.Errorf("failed to pre read: %w", err) } nr += cr.n } if !found { return 0, fmt.Errorf("fileReader.ReadAt: target entry not found") } return retN, retErr } // A Writer writes stargz files. // // Use NewWriter to create a new Writer. type Writer struct { bw *bufio.Writer cw *countWriter toc *JTOC diffHash hash.Hash // SHA-256 of uncompressed tar closed bool gz io.WriteCloser lastUsername map[int]string lastGroupname map[int]string compressor Compressor uncompressedCounter *countWriteFlusher // ChunkSize optionally controls the maximum number of bytes // of data of a regular file that can be written in one gzip // stream before a new gzip stream is started. // Zero means to use a default, currently 4 MiB. ChunkSize int // MinChunkSize optionally controls the minimum number of bytes // of data must be written in one gzip stream before a new gzip // NOTE: This adds a TOC property that stargz snapshotter < v0.13.0 doesn't understand. MinChunkSize int needsOpenGzEntries map[string]struct{} } // currentCompressionWriter writes to the current w.gz field, which can // change throughout writing a tar entry. // // Additionally, it updates w's SHA-256 of the uncompressed bytes // of the tar file. type currentCompressionWriter struct{ w *Writer } func (ccw currentCompressionWriter) Write(p []byte) (int, error) { ccw.w.diffHash.Write(p) if ccw.w.gz == nil { if err := ccw.w.condOpenGz(); err != nil { return 0, err } } return ccw.w.gz.Write(p) } func (w *Writer) chunkSize() int { if w.ChunkSize <= 0 { return 4 << 20 } return w.ChunkSize } // Unpack decompresses the given estargz blob and returns a ReadCloser of the tar blob. // TOC JSON and footer are removed. func Unpack(sr *io.SectionReader, c Decompressor) (io.ReadCloser, error) { footerSize := c.FooterSize() if sr.Size() < footerSize { return nil, fmt.Errorf("blob is too small; %d < %d", sr.Size(), footerSize) } footerOffset := sr.Size() - footerSize footer := make([]byte, footerSize) if _, err := sr.ReadAt(footer, footerOffset); err != nil { return nil, err } blobPayloadSize, _, _, err := c.ParseFooter(footer) if err != nil { return nil, fmt.Errorf("failed to parse footer: %w", err) } if blobPayloadSize < 0 { blobPayloadSize = sr.Size() } return c.Reader(io.LimitReader(sr, blobPayloadSize)) } // NewWriter returns a new stargz writer (gzip-based) writing to w. // // The writer must be closed to write its trailing table of contents. func NewWriter(w io.Writer) *Writer { return NewWriterLevel(w, gzip.BestCompression) } // NewWriterLevel returns a new stargz writer (gzip-based) writing to w. // The compression level is configurable. // // The writer must be closed to write its trailing table of contents. func NewWriterLevel(w io.Writer, compressionLevel int) *Writer { return NewWriterWithCompressor(w, NewGzipCompressorWithLevel(compressionLevel)) } // NewWriterWithCompressor returns a new stargz writer writing to w. // The compression method is configurable. // // The writer must be closed to write its trailing table of contents. func NewWriterWithCompressor(w io.Writer, c Compressor) *Writer { bw := bufio.NewWriter(w) cw := &countWriter{w: bw} return &Writer{ bw: bw, cw: cw, toc: &JTOC{Version: 1}, diffHash: sha256.New(), compressor: c, uncompressedCounter: &countWriteFlusher{}, } } // Close writes the stargz's table of contents and flushes all the // buffers, returning any error. func (w *Writer) Close() (digest.Digest, error) { if w.closed { return "", nil } defer func() { w.closed = true }() if err := w.closeGz(); err != nil { return "", err } // Write the TOC index and footer. tocDigest, err := w.compressor.WriteTOCAndFooter(w.cw, w.cw.n, w.toc, w.diffHash) if err != nil { return "", err } if err := w.bw.Flush(); err != nil { return "", err } return tocDigest, nil } func (w *Writer) closeGz() error { if w.closed { return errors.New("write on closed Writer") } if w.gz != nil { if err := w.gz.Close(); err != nil { return err } w.gz = nil } return nil } func (w *Writer) flushGz() error { if w.closed { return errors.New("flush on closed Writer") } if w.gz != nil { if f, ok := w.gz.(interface { Flush() error }); ok { return f.Flush() } } return nil } // nameIfChanged returns name, unless it was the already the value of (*mp)[id], // in which case it returns the empty string. func (w *Writer) nameIfChanged(mp *map[int]string, id int, name string) string { if name == "" { return "" } if *mp == nil { *mp = make(map[int]string) } if (*mp)[id] == name { return "" } (*mp)[id] = name return name } func (w *Writer) condOpenGz() (err error) { if w.gz == nil { w.gz, err = w.compressor.Writer(w.cw) if w.gz != nil { w.gz = w.uncompressedCounter.register(w.gz) } } return } // AppendTar reads the tar or tar.gz file from r and appends // each of its contents to w. // // The input r can optionally be gzip compressed but the output will // always be compressed by the specified compressor. func (w *Writer) AppendTar(r io.Reader) error { return w.appendTar(r, false) } // AppendTarLossLess reads the tar or tar.gz file from r and appends // each of its contents to w. // // The input r can optionally be gzip compressed but the output will // always be compressed by the specified compressor. // // The difference of this func with AppendTar is that this writes // the input tar stream into w without any modification (e.g. to header bytes). // // Note that if the input tar stream already contains TOC JSON, this returns // error because w cannot overwrite the TOC JSON to the one generated by w without // lossy modification. To avoid this error, if the input stream is known to be stargz/estargz, // you shoud decompress it and remove TOC JSON in advance. func (w *Writer) AppendTarLossLess(r io.Reader) error { return w.appendTar(r, true) } func (w *Writer) appendTar(r io.Reader, lossless bool) error { var src io.Reader br := bufio.NewReader(r) if isGzip(br) { zr, _ := gzip.NewReader(br) src = zr } else { src = io.Reader(br) } dst := currentCompressionWriter{w} var tw *tar.Writer if !lossless { tw = tar.NewWriter(dst) // use tar writer only when this isn't lossless mode. } tr := tar.NewReader(src) if lossless { tr.RawAccounting = true } prevOffset := w.cw.n var prevOffsetUncompressed int64 for { h, err := tr.Next() if err == io.EOF { if lossless { if remain := tr.RawBytes(); len(remain) > 0 { // Collect the remaining null bytes. // https://github.com/vbatts/tar-split/blob/80a436fd6164c557b131f7c59ed69bd81af69761/concept/main.go#L49-L53 if _, err := dst.Write(remain); err != nil { return err } } } break } if err != nil { return fmt.Errorf("error reading from source tar: tar.Reader.Next: %v", err) } if cleanEntryName(h.Name) == TOCTarName { // It is possible for a layer to be "stargzified" twice during the // distribution lifecycle. So we reserve "TOCTarName" here to avoid // duplicated entries in the resulting layer. if lossless { // We cannot handle this in lossless way. return fmt.Errorf("existing TOC JSON is not allowed; decompress layer before append") } continue } xattrs := make(map[string][]byte) const xattrPAXRecordsPrefix = "SCHILY.xattr." if h.PAXRecords != nil { for k, v := range h.PAXRecords { if strings.HasPrefix(k, xattrPAXRecordsPrefix) { xattrs[k[len(xattrPAXRecordsPrefix):]] = []byte(v) } } } ent := &TOCEntry{ Name: h.Name, Mode: h.Mode, UID: h.Uid, GID: h.Gid, Uname: w.nameIfChanged(&w.lastUsername, h.Uid, h.Uname), Gname: w.nameIfChanged(&w.lastGroupname, h.Gid, h.Gname), ModTime3339: formatModtime(h.ModTime), Xattrs: xattrs, } if err := w.condOpenGz(); err != nil { return err } if tw != nil { if err := tw.WriteHeader(h); err != nil { return err } } else { if _, err := dst.Write(tr.RawBytes()); err != nil { return err } } switch h.Typeflag { case tar.TypeLink: ent.Type = "hardlink" ent.LinkName = h.Linkname case tar.TypeSymlink: ent.Type = "symlink" ent.LinkName = h.Linkname case tar.TypeDir: ent.Type = "dir" case tar.TypeReg: ent.Type = "reg" ent.Size = h.Size case tar.TypeChar: ent.Type = "char" ent.DevMajor = int(h.Devmajor) ent.DevMinor = int(h.Devminor) case tar.TypeBlock: ent.Type = "block" ent.DevMajor = int(h.Devmajor) ent.DevMinor = int(h.Devminor) case tar.TypeFifo: ent.Type = "fifo" default: return fmt.Errorf("unsupported input tar entry %q", h.Typeflag) } // We need to keep a reference to the TOC entry for regular files, so that we // can fill the digest later. var regFileEntry *TOCEntry var payloadDigest digest.Digester if h.Typeflag == tar.TypeReg { regFileEntry = ent payloadDigest = digest.Canonical.Digester() } if h.Typeflag == tar.TypeReg && ent.Size > 0 { var written int64 totalSize := ent.Size // save it before we destroy ent tee := io.TeeReader(tr, payloadDigest.Hash()) for written < totalSize { chunkSize := int64(w.chunkSize()) remain := totalSize - written if remain < chunkSize { chunkSize = remain } else { ent.ChunkSize = chunkSize } // We flush the underlying compression writer here to correctly calculate "w.cw.n". if err := w.flushGz(); err != nil { return err } if w.needsOpenGz(ent) || w.cw.n-prevOffset >= int64(w.MinChunkSize) { if err := w.closeGz(); err != nil { return err } ent.Offset = w.cw.n prevOffset = ent.Offset prevOffsetUncompressed = w.uncompressedCounter.n } else { ent.Offset = prevOffset ent.InnerOffset = w.uncompressedCounter.n - prevOffsetUncompressed } ent.ChunkOffset = written chunkDigest := digest.Canonical.Digester() if err := w.condOpenGz(); err != nil { return err } teeChunk := io.TeeReader(tee, chunkDigest.Hash()) var out io.Writer if tw != nil { out = tw } else { out = dst } if _, err := io.CopyN(out, teeChunk, chunkSize); err != nil { return fmt.Errorf("error copying %q: %v", h.Name, err) } ent.ChunkDigest = chunkDigest.Digest().String() w.toc.Entries = append(w.toc.Entries, ent) written += chunkSize ent = &TOCEntry{ Name: h.Name, Type: "chunk", } } } else { w.toc.Entries = append(w.toc.Entries, ent) } if payloadDigest != nil { regFileEntry.Digest = payloadDigest.Digest().String() } if tw != nil { if err := tw.Flush(); err != nil { return err } } } remainDest := io.Discard if lossless { remainDest = dst // Preserve the remaining bytes in lossless mode } _, err := io.Copy(remainDest, src) return err } func (w *Writer) needsOpenGz(ent *TOCEntry) bool { if ent.Type != "reg" { return false } if w.needsOpenGzEntries == nil { return false } _, ok := w.needsOpenGzEntries[ent.Name] return ok } // DiffID returns the SHA-256 of the uncompressed tar bytes. // It is only valid to call DiffID after Close. func (w *Writer) DiffID() string { return fmt.Sprintf("sha256:%x", w.diffHash.Sum(nil)) } func maxFooterSize(blobSize int64, decompressors ...Decompressor) (res int64) { for _, d := range decompressors { if s := d.FooterSize(); res < s && s <= blobSize { res = s } } return } func parseTOC(d Decompressor, sr *io.SectionReader, tocOff, tocSize int64, tocBytes []byte, opts openOpts) (*Reader, error) { if tocOff < 0 { // This means that TOC isn't contained in the blob. // We pass nil reader to ParseTOC and expect that ParseTOC acquire TOC from // the external location. start := time.Now() toc, tocDgst, err := d.ParseTOC(nil) if err != nil { return nil, err } if opts.telemetry != nil && opts.telemetry.GetTocLatency != nil { opts.telemetry.GetTocLatency(start) } if opts.telemetry != nil && opts.telemetry.DeserializeTocLatency != nil { opts.telemetry.DeserializeTocLatency(start) } return &Reader{ sr: sr, toc: toc, tocDigest: tocDgst, decompressor: d, }, nil } if len(tocBytes) > 0 { start := time.Now() toc, tocDgst, err := d.ParseTOC(bytes.NewReader(tocBytes)) if err == nil { if opts.telemetry != nil && opts.telemetry.DeserializeTocLatency != nil { opts.telemetry.DeserializeTocLatency(start) } return &Reader{ sr: sr, toc: toc, tocDigest: tocDgst, decompressor: d, }, nil } } start := time.Now() tocBytes = make([]byte, tocSize) if _, err := sr.ReadAt(tocBytes, tocOff); err != nil { return nil, fmt.Errorf("error reading %d byte TOC targz: %v", len(tocBytes), err) } if opts.telemetry != nil && opts.telemetry.GetTocLatency != nil { opts.telemetry.GetTocLatency(start) } start = time.Now() toc, tocDgst, err := d.ParseTOC(bytes.NewReader(tocBytes)) if err != nil { return nil, err } if opts.telemetry != nil && opts.telemetry.DeserializeTocLatency != nil { opts.telemetry.DeserializeTocLatency(start) } return &Reader{ sr: sr, toc: toc, tocDigest: tocDgst, decompressor: d, }, nil } func formatModtime(t time.Time) string { if t.IsZero() || t.Unix() == 0 { return "" } return t.UTC().Round(time.Second).Format(time.RFC3339) } func cleanEntryName(name string) string { // Use path.Clean to consistently deal with path separators across platforms. return strings.TrimPrefix(path.Clean("/"+name), "/") } // countWriter counts how many bytes have been written to its wrapped // io.Writer. type countWriter struct { w io.Writer n int64 } func (cw *countWriter) Write(p []byte) (n int, err error) { n, err = cw.w.Write(p) cw.n += int64(n) return } type countWriteFlusher struct { io.WriteCloser n int64 } func (wc *countWriteFlusher) register(w io.WriteCloser) io.WriteCloser { wc.WriteCloser = w return wc } func (wc *countWriteFlusher) Write(p []byte) (n int, err error) { n, err = wc.WriteCloser.Write(p) wc.n += int64(n) return } func (wc *countWriteFlusher) Flush() error { if f, ok := wc.WriteCloser.(interface { Flush() error }); ok { return f.Flush() } return nil } func (wc *countWriteFlusher) Close() error { err := wc.WriteCloser.Close() wc.WriteCloser = nil return err } // isGzip reports whether br is positioned right before an upcoming gzip stream. // It does not consume any bytes from br. func isGzip(br *bufio.Reader) bool { const ( gzipID1 = 0x1f gzipID2 = 0x8b gzipDeflate = 8 ) peek, _ := br.Peek(3) return len(peek) >= 3 && peek[0] == gzipID1 && peek[1] == gzipID2 && peek[2] == gzipDeflate } func positive(n int64) int64 { if n < 0 { return 0 } return n } type countReader struct { r io.Reader n int64 } func (cr *countReader) Read(p []byte) (n int, err error) { n, err = cr.r.Read(p) cr.n += int64(n) return } ================================================ FILE: vendor/github.com/containerd/stargz-snapshotter/estargz/gzip.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ /* Copyright 2019 The Go Authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. */ package estargz import ( "archive/tar" "bytes" "compress/gzip" "encoding/binary" "encoding/json" "fmt" "hash" "io" "strconv" digest "github.com/opencontainers/go-digest" ) type gzipCompression struct { *GzipCompressor *GzipDecompressor } func newGzipCompressionWithLevel(level int) Compression { return &gzipCompression{ &GzipCompressor{level}, &GzipDecompressor{}, } } func NewGzipCompressor() *GzipCompressor { return &GzipCompressor{gzip.BestCompression} } func NewGzipCompressorWithLevel(level int) *GzipCompressor { return &GzipCompressor{level} } type GzipCompressor struct { compressionLevel int } func (gc *GzipCompressor) Writer(w io.Writer) (WriteFlushCloser, error) { return gzip.NewWriterLevel(w, gc.compressionLevel) } func (gc *GzipCompressor) WriteTOCAndFooter(w io.Writer, off int64, toc *JTOC, diffHash hash.Hash) (digest.Digest, error) { tocJSON, err := json.MarshalIndent(toc, "", "\t") if err != nil { return "", err } gz, _ := gzip.NewWriterLevel(w, gc.compressionLevel) gw := io.Writer(gz) if diffHash != nil { gw = io.MultiWriter(gz, diffHash) } tw := tar.NewWriter(gw) if err := tw.WriteHeader(&tar.Header{ Typeflag: tar.TypeReg, Name: TOCTarName, Size: int64(len(tocJSON)), }); err != nil { return "", err } if _, err := tw.Write(tocJSON); err != nil { return "", err } if err := tw.Close(); err != nil { return "", err } if err := gz.Close(); err != nil { return "", err } if _, err := w.Write(gzipFooterBytes(off)); err != nil { return "", err } return digest.FromBytes(tocJSON), nil } // gzipFooterBytes returns the 51 bytes footer. func gzipFooterBytes(tocOff int64) []byte { buf := bytes.NewBuffer(make([]byte, 0, FooterSize)) gz, _ := gzip.NewWriterLevel(buf, gzip.NoCompression) // MUST be NoCompression to keep 51 bytes // Extra header indicating the offset of TOCJSON // https://tools.ietf.org/html/rfc1952#section-2.3.1.1 header := make([]byte, 4) header[0], header[1] = 'S', 'G' subfield := fmt.Sprintf("%016xSTARGZ", tocOff) binary.LittleEndian.PutUint16(header[2:4], uint16(len(subfield))) // little-endian per RFC1952 gz.Extra = append(header, []byte(subfield)...) gz.Close() if buf.Len() != FooterSize { panic(fmt.Sprintf("footer buffer = %d, not %d", buf.Len(), FooterSize)) } return buf.Bytes() } type GzipDecompressor struct{} func (gz *GzipDecompressor) Reader(r io.Reader) (io.ReadCloser, error) { return gzip.NewReader(r) } func (gz *GzipDecompressor) ParseTOC(r io.Reader) (toc *JTOC, tocDgst digest.Digest, err error) { return parseTOCEStargz(r) } func (gz *GzipDecompressor) ParseFooter(p []byte) (blobPayloadSize, tocOffset, tocSize int64, err error) { if len(p) != FooterSize { return 0, 0, 0, fmt.Errorf("invalid length %d cannot be parsed", len(p)) } zr, err := gzip.NewReader(bytes.NewReader(p)) if err != nil { return 0, 0, 0, err } defer zr.Close() extra := zr.Extra si1, si2, subfieldlen, subfield := extra[0], extra[1], extra[2:4], extra[4:] if si1 != 'S' || si2 != 'G' { return 0, 0, 0, fmt.Errorf("invalid subfield IDs: %q, %q; want E, S", si1, si2) } if slen := binary.LittleEndian.Uint16(subfieldlen); slen != uint16(16+len("STARGZ")) { return 0, 0, 0, fmt.Errorf("invalid length of subfield %d; want %d", slen, 16+len("STARGZ")) } if string(subfield[16:]) != "STARGZ" { return 0, 0, 0, fmt.Errorf("STARGZ magic string must be included in the footer subfield") } tocOffset, err = strconv.ParseInt(string(subfield[:16]), 16, 64) if err != nil { return 0, 0, 0, fmt.Errorf("legacy: failed to parse toc offset: %w", err) } return tocOffset, tocOffset, 0, nil } func (gz *GzipDecompressor) FooterSize() int64 { return FooterSize } func (gz *GzipDecompressor) DecompressTOC(r io.Reader) (tocJSON io.ReadCloser, err error) { return decompressTOCEStargz(r) } type LegacyGzipDecompressor struct{} func (gz *LegacyGzipDecompressor) Reader(r io.Reader) (io.ReadCloser, error) { return gzip.NewReader(r) } func (gz *LegacyGzipDecompressor) ParseTOC(r io.Reader) (toc *JTOC, tocDgst digest.Digest, err error) { return parseTOCEStargz(r) } func (gz *LegacyGzipDecompressor) ParseFooter(p []byte) (blobPayloadSize, tocOffset, tocSize int64, err error) { if len(p) != legacyFooterSize { return 0, 0, 0, fmt.Errorf("legacy: invalid length %d cannot be parsed", len(p)) } zr, err := gzip.NewReader(bytes.NewReader(p)) if err != nil { return 0, 0, 0, fmt.Errorf("legacy: failed to get footer gzip reader: %w", err) } defer zr.Close() extra := zr.Extra if len(extra) != 16+len("STARGZ") { return 0, 0, 0, fmt.Errorf("legacy: invalid stargz's extra field size") } if string(extra[16:]) != "STARGZ" { return 0, 0, 0, fmt.Errorf("legacy: magic string STARGZ not found") } tocOffset, err = strconv.ParseInt(string(extra[:16]), 16, 64) if err != nil { return 0, 0, 0, fmt.Errorf("legacy: failed to parse toc offset: %w", err) } return tocOffset, tocOffset, 0, nil } func (gz *LegacyGzipDecompressor) FooterSize() int64 { return legacyFooterSize } func (gz *LegacyGzipDecompressor) DecompressTOC(r io.Reader) (tocJSON io.ReadCloser, err error) { return decompressTOCEStargz(r) } func parseTOCEStargz(r io.Reader) (toc *JTOC, tocDgst digest.Digest, err error) { tr, err := decompressTOCEStargz(r) if err != nil { return nil, "", err } dgstr := digest.Canonical.Digester() toc = new(JTOC) if err := json.NewDecoder(io.TeeReader(tr, dgstr.Hash())).Decode(&toc); err != nil { return nil, "", fmt.Errorf("error decoding TOC JSON: %v", err) } if err := tr.Close(); err != nil { return nil, "", err } return toc, dgstr.Digest(), nil } func decompressTOCEStargz(r io.Reader) (tocJSON io.ReadCloser, err error) { zr, err := gzip.NewReader(r) if err != nil { return nil, fmt.Errorf("malformed TOC gzip header: %v", err) } zr.Multistream(false) tr := tar.NewReader(zr) h, err := tr.Next() if err != nil { return nil, fmt.Errorf("failed to find tar header in TOC gzip stream: %v", err) } if h.Name != TOCTarName { return nil, fmt.Errorf("TOC tar entry had name %q; expected %q", h.Name, TOCTarName) } return readCloser{tr, zr.Close}, nil } ================================================ FILE: vendor/github.com/containerd/stargz-snapshotter/estargz/testutil.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ /* Copyright 2019 The Go Authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. */ package estargz import ( "archive/tar" "bytes" "compress/gzip" "crypto/rand" "crypto/sha256" "encoding/json" "errors" "fmt" "io" "math/big" "os" "path/filepath" "reflect" "sort" "strings" "time" "github.com/containerd/stargz-snapshotter/estargz/errorutil" "github.com/klauspost/compress/zstd" digest "github.com/opencontainers/go-digest" ) // TestingController is Compression with some helper methods necessary for testing. type TestingController interface { Compression TestStreams(t TestingT, b []byte, streams []int64) DiffIDOf(TestingT, []byte) string String() string } // TestingT is the minimal set of testing.T required to run the // tests defined in CompressionTestSuite. This interface exists to prevent // leaking the testing package from being exposed outside tests. type TestingT interface { Errorf(format string, args ...any) FailNow() Failed() bool Fatal(args ...any) Fatalf(format string, args ...any) Logf(format string, args ...any) Parallel() } // Runner allows running subtests of TestingT. This exists instead of adding // a Run method to TestingT interface because the Run implementation of // testing.T would not satisfy the interface. type Runner func(t TestingT, name string, fn func(t TestingT)) type TestRunner struct { TestingT Runner Runner } func (r *TestRunner) Run(name string, run func(*TestRunner)) { r.Runner(r.TestingT, name, func(t TestingT) { run(&TestRunner{TestingT: t, Runner: r.Runner}) }) } // CompressionTestSuite tests this pkg with controllers can build valid eStargz blobs and parse them. func CompressionTestSuite(t *TestRunner, controllers ...TestingControllerFactory) { t.Run("testBuild", func(t *TestRunner) { t.Parallel(); testBuild(t, controllers...) }) t.Run("testDigestAndVerify", func(t *TestRunner) { t.Parallel() testDigestAndVerify(t, controllers...) }) t.Run("testWriteAndOpen", func(t *TestRunner) { t.Parallel(); testWriteAndOpen(t, controllers...) }) } type TestingControllerFactory func() TestingController const ( uncompressedType int = iota gzipType zstdType ) var srcCompressions = []int{ uncompressedType, gzipType, zstdType, } var allowedPrefix = [4]string{"", "./", "/", "../"} // testBuild tests the resulting stargz blob built by this pkg has the same // contents as the normal stargz blob. func testBuild(t *TestRunner, controllers ...TestingControllerFactory) { tests := []struct { name string chunkSize int minChunkSize []int in []tarEntry }{ { name: "regfiles and directories", chunkSize: 4, in: tarOf( file("foo", "test1"), dir("foo2/"), file("foo2/bar", "test2", xAttr(map[string]string{"test": "sample"})), ), }, { name: "empty files", chunkSize: 4, in: tarOf( file("foo", "tttttt"), file("foo_empty", ""), file("foo2", "tttttt"), file("foo_empty2", ""), file("foo3", "tttttt"), file("foo_empty3", ""), file("foo4", "tttttt"), file("foo_empty4", ""), file("foo5", "tttttt"), file("foo_empty5", ""), file("foo6", "tttttt"), ), }, { name: "various files", chunkSize: 4, minChunkSize: []int{0, 64000}, in: tarOf( file("baz.txt", "bazbazbazbazbazbazbaz"), file("foo1.txt", "a"), file("bar/foo2.txt", "b"), file("foo3.txt", "c"), symlink("barlink", "test/bar.txt"), dir("test/"), dir("dev/"), blockdev("dev/testblock", 3, 4), fifo("dev/testfifo"), chardev("dev/testchar1", 5, 6), file("test/bar.txt", "testbartestbar", xAttr(map[string]string{"test2": "sample2"})), dir("test2/"), link("test2/bazlink", "baz.txt"), chardev("dev/testchar2", 1, 2), ), }, { name: "no contents", chunkSize: 4, in: tarOf( file("baz.txt", ""), symlink("barlink", "test/bar.txt"), dir("test/"), dir("dev/"), blockdev("dev/testblock", 3, 4), fifo("dev/testfifo"), chardev("dev/testchar1", 5, 6), file("test/bar.txt", "", xAttr(map[string]string{"test2": "sample2"})), dir("test2/"), link("test2/bazlink", "baz.txt"), chardev("dev/testchar2", 1, 2), ), }, } for _, tt := range tests { if len(tt.minChunkSize) == 0 { tt.minChunkSize = []int{0} } for _, srcCompression := range srcCompressions { srcCompression := srcCompression for _, newCL := range controllers { newCL := newCL for _, srcTarFormat := range []tar.Format{tar.FormatUSTAR, tar.FormatPAX, tar.FormatGNU} { srcTarFormat := srcTarFormat for _, prefix := range allowedPrefix { prefix := prefix for _, minChunkSize := range tt.minChunkSize { minChunkSize := minChunkSize t.Run(tt.name+"-"+fmt.Sprintf("compression=%v,prefix=%q,src=%d,format=%s,minChunkSize=%d", newCL(), prefix, srcCompression, srcTarFormat, minChunkSize), func(t *TestRunner) { tarBlob := buildTar(t, tt.in, prefix, srcTarFormat) // Test divideEntries() entries, err := sortEntries(tarBlob, nil, nil) // identical order if err != nil { t.Fatalf("failed to parse tar: %v", err) } var merged []*entry for _, part := range divideEntries(entries, 4) { merged = append(merged, part...) } if !reflect.DeepEqual(entries, merged) { for _, e := range entries { t.Logf("Original: %v", e.header) } for _, e := range merged { t.Logf("Merged: %v", e.header) } t.Errorf("divided entries couldn't be merged") return } // Prepare sample data cl1 := newCL() wantBuf := new(bytes.Buffer) sw := NewWriterWithCompressor(wantBuf, cl1) sw.MinChunkSize = minChunkSize sw.ChunkSize = tt.chunkSize if err := sw.AppendTar(tarBlob); err != nil { t.Fatalf("failed to append tar to want stargz: %v", err) } if _, err := sw.Close(); err != nil { t.Fatalf("failed to prepare want stargz: %v", err) } wantData := wantBuf.Bytes() want, err := Open(io.NewSectionReader( bytes.NewReader(wantData), 0, int64(len(wantData))), WithDecompressors(cl1), ) if err != nil { t.Fatalf("failed to parse the want stargz: %v", err) } // Prepare testing data var opts []Option if minChunkSize > 0 { opts = append(opts, WithMinChunkSize(minChunkSize)) } cl2 := newCL() rc, err := Build(compressBlob(t, tarBlob, srcCompression), append(opts, WithChunkSize(tt.chunkSize), WithCompression(cl2))...) if err != nil { t.Fatalf("failed to build stargz: %v", err) } defer rc.Close() gotBuf := new(bytes.Buffer) if _, err := io.Copy(gotBuf, rc); err != nil { t.Fatalf("failed to copy built stargz blob: %v", err) } gotData := gotBuf.Bytes() got, err := Open(io.NewSectionReader( bytes.NewReader(gotBuf.Bytes()), 0, int64(len(gotData))), WithDecompressors(cl2), ) if err != nil { t.Fatalf("failed to parse the got stargz: %v", err) } // Check DiffID is properly calculated rc.Close() diffID := rc.DiffID() wantDiffID := cl2.DiffIDOf(t, gotData) if diffID.String() != wantDiffID { t.Errorf("DiffID = %q; want %q", diffID, wantDiffID) } // Compare as stargz if !isSameVersion(t, cl1, wantData, cl2, gotData) { t.Errorf("built stargz hasn't same json") return } if !isSameEntries(t, want, got) { t.Errorf("built stargz isn't same as the original") return } // Compare as tar.gz if !isSameTarGz(t, cl1, wantData, cl2, gotData) { t.Errorf("built stargz isn't same tar.gz") return } }) } } } } } } } func isSameTarGz(t TestingT, cla TestingController, a []byte, clb TestingController, b []byte) bool { aGz, err := cla.Reader(bytes.NewReader(a)) if err != nil { t.Fatalf("failed to read A") } defer aGz.Close() bGz, err := clb.Reader(bytes.NewReader(b)) if err != nil { t.Fatalf("failed to read B") } defer bGz.Close() // Same as tar's Next() method but ignores landmarks and TOCJSON file next := func(r *tar.Reader) (h *tar.Header, err error) { for { if h, err = r.Next(); err != nil { return } if h.Name != PrefetchLandmark && h.Name != NoPrefetchLandmark && h.Name != TOCTarName { return } } } aTar := tar.NewReader(aGz) bTar := tar.NewReader(bGz) for { // Fetch and parse next header. aH, aErr := next(aTar) bH, bErr := next(bTar) if aErr != nil || bErr != nil { if aErr == io.EOF && bErr == io.EOF { break } t.Fatalf("Failed to parse tar file: A: %v, B: %v", aErr, bErr) } if !reflect.DeepEqual(aH, bH) { t.Logf("different header (A = %v; B = %v)", aH, bH) return false } aFile, err := io.ReadAll(aTar) if err != nil { t.Fatal("failed to read tar payload of A") } bFile, err := io.ReadAll(bTar) if err != nil { t.Fatal("failed to read tar payload of B") } if !bytes.Equal(aFile, bFile) { t.Logf("different tar payload (A = %q; B = %q)", string(a), string(b)) return false } } return true } func isSameVersion(t TestingT, cla TestingController, a []byte, clb TestingController, b []byte) bool { aJTOC, _, err := parseStargz(io.NewSectionReader(bytes.NewReader(a), 0, int64(len(a))), cla) if err != nil { t.Fatalf("failed to parse A: %v", err) } bJTOC, _, err := parseStargz(io.NewSectionReader(bytes.NewReader(b), 0, int64(len(b))), clb) if err != nil { t.Fatalf("failed to parse B: %v", err) } t.Logf("A: TOCJSON: %v", dumpTOCJSON(t, aJTOC)) t.Logf("B: TOCJSON: %v", dumpTOCJSON(t, bJTOC)) return aJTOC.Version == bJTOC.Version } func isSameEntries(t TestingT, a, b *Reader) bool { aroot, ok := a.Lookup("") if !ok { t.Fatalf("failed to get root of A") } broot, ok := b.Lookup("") if !ok { t.Fatalf("failed to get root of B") } aEntry := stargzEntry{aroot, a} bEntry := stargzEntry{broot, b} return contains(t, aEntry, bEntry) && contains(t, bEntry, aEntry) } func compressBlob(t TestingT, src *io.SectionReader, srcCompression int) *io.SectionReader { buf := new(bytes.Buffer) var w io.WriteCloser var err error switch srcCompression { case gzipType: w = gzip.NewWriter(buf) case zstdType: w, err = zstd.NewWriter(buf) if err != nil { t.Fatalf("failed to init zstd writer: %v", err) } default: return src } src.Seek(0, io.SeekStart) if _, err := io.Copy(w, src); err != nil { t.Fatalf("failed to compress source") } if err := w.Close(); err != nil { t.Fatalf("failed to finalize compress source") } data := buf.Bytes() return io.NewSectionReader(bytes.NewReader(data), 0, int64(len(data))) } type stargzEntry struct { e *TOCEntry r *Reader } // contains checks if all child entries in "b" are also contained in "a". // This function also checks if the files/chunks contain the same contents among "a" and "b". func contains(t TestingT, a, b stargzEntry) bool { ae, ar := a.e, a.r be, br := b.e, b.r t.Logf("Comparing: %q vs %q", ae.Name, be.Name) if !equalEntry(ae, be) { t.Logf("%q != %q: entry: a: %v, b: %v", ae.Name, be.Name, ae, be) return false } if ae.Type == "dir" { t.Logf("Directory: %q vs %q: %v vs %v", ae.Name, be.Name, allChildrenName(ae), allChildrenName(be)) iscontain := true ae.ForeachChild(func(aBaseName string, aChild *TOCEntry) bool { // Walk through all files on this stargz file. if aChild.Name == PrefetchLandmark || aChild.Name == NoPrefetchLandmark { return true // Ignore landmarks } // Ignore a TOCEntry of "./" (formated as "" by stargz lib) on root directory // because this points to the root directory itself. if aChild.Name == "" && ae.Name == "" { return true } bChild, ok := be.LookupChild(aBaseName) if !ok { t.Logf("%q (base: %q): not found in b: %v", ae.Name, aBaseName, allChildrenName(be)) iscontain = false return false } childcontain := contains(t, stargzEntry{aChild, a.r}, stargzEntry{bChild, b.r}) if !childcontain { t.Logf("%q != %q: non-equal dir", ae.Name, be.Name) iscontain = false return false } return true }) return iscontain } else if ae.Type == "reg" { af, err := ar.OpenFile(ae.Name) if err != nil { t.Fatalf("failed to open file %q on A: %v", ae.Name, err) } bf, err := br.OpenFile(be.Name) if err != nil { t.Fatalf("failed to open file %q on B: %v", be.Name, err) } var nr int64 for nr < ae.Size { abytes, anext, aok := readOffset(t, af, nr, a) bbytes, bnext, bok := readOffset(t, bf, nr, b) if !aok && !bok { break } else if !aok || !bok || anext != bnext { t.Logf("%q != %q (offset=%d): chunk existence a=%v vs b=%v, anext=%v vs bnext=%v", ae.Name, be.Name, nr, aok, bok, anext, bnext) return false } nr = anext if !bytes.Equal(abytes, bbytes) { t.Logf("%q != %q: different contents %v vs %v", ae.Name, be.Name, string(abytes), string(bbytes)) return false } } return true } return true } func allChildrenName(e *TOCEntry) (children []string) { e.ForeachChild(func(baseName string, _ *TOCEntry) bool { children = append(children, baseName) return true }) return } func equalEntry(a, b *TOCEntry) bool { // Here, we selectively compare fileds that we are interested in. return a.Name == b.Name && a.Type == b.Type && a.Size == b.Size && a.ModTime3339 == b.ModTime3339 && a.Stat().ModTime().Equal(b.Stat().ModTime()) && // modTime time.Time a.LinkName == b.LinkName && a.Mode == b.Mode && a.UID == b.UID && a.GID == b.GID && a.Uname == b.Uname && a.Gname == b.Gname && (a.Offset >= 0) == (b.Offset >= 0) && (a.NextOffset() > 0) == (b.NextOffset() > 0) && a.DevMajor == b.DevMajor && a.DevMinor == b.DevMinor && a.NumLink == b.NumLink && reflect.DeepEqual(a.Xattrs, b.Xattrs) && // chunk-related infomations aren't compared in this function. // ChunkOffset int64 `json:"chunkOffset,omitempty"` // ChunkSize int64 `json:"chunkSize,omitempty"` // children map[string]*TOCEntry a.Digest == b.Digest } func readOffset(t TestingT, r *io.SectionReader, offset int64, e stargzEntry) ([]byte, int64, bool) { ce, ok := e.r.ChunkEntryForOffset(e.e.Name, offset) if !ok { return nil, 0, false } data := make([]byte, ce.ChunkSize) t.Logf("Offset: %v, NextOffset: %v", ce.Offset, ce.NextOffset()) n, err := r.ReadAt(data, ce.ChunkOffset) if err != nil { t.Fatalf("failed to read file payload of %q (offset:%d,size:%d): %v", e.e.Name, ce.ChunkOffset, ce.ChunkSize, err) } if int64(n) != ce.ChunkSize { t.Fatalf("unexpected copied data size %d; want %d", n, ce.ChunkSize) } return data[:n], offset + ce.ChunkSize, true } func dumpTOCJSON(t TestingT, tocJSON *JTOC) string { jtocData, err := json.Marshal(*tocJSON) if err != nil { t.Fatalf("failed to marshal TOC JSON: %v", err) } buf := new(bytes.Buffer) if _, err := io.Copy(buf, bytes.NewReader(jtocData)); err != nil { t.Fatalf("failed to read toc json blob: %v", err) } return buf.String() } const chunkSize = 3 type check func(t *TestRunner, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) // testDigestAndVerify runs specified checks against sample stargz blobs. func testDigestAndVerify(t *TestRunner, controllers ...TestingControllerFactory) { tests := []struct { name string tarInit func(t TestingT, dgstMap map[string]digest.Digest) (blob []tarEntry) checks []check minChunkSize []int }{ { name: "no-regfile", tarInit: func(t TestingT, dgstMap map[string]digest.Digest) (blob []tarEntry) { return tarOf( dir("test/"), ) }, checks: []check{ checkStargzTOC, checkVerifyTOC, checkVerifyInvalidStargzFail(buildTar(t, tarOf( dir("test2/"), // modified ), allowedPrefix[0])), }, }, { name: "small-files", tarInit: func(t TestingT, dgstMap map[string]digest.Digest) (blob []tarEntry) { return tarOf( regDigest(t, "baz.txt", "", dgstMap), regDigest(t, "foo.txt", "a", dgstMap), dir("test/"), regDigest(t, "test/bar.txt", "bbb", dgstMap), ) }, minChunkSize: []int{0, 64000}, checks: []check{ checkStargzTOC, checkVerifyTOC, checkVerifyInvalidStargzFail(buildTar(t, tarOf( file("baz.txt", ""), file("foo.txt", "M"), // modified dir("test/"), file("test/bar.txt", "bbb"), ), allowedPrefix[0])), // checkVerifyInvalidTOCEntryFail("foo.txt"), // TODO checkVerifyBrokenContentFail("foo.txt"), }, }, { name: "big-files", tarInit: func(t TestingT, dgstMap map[string]digest.Digest) (blob []tarEntry) { return tarOf( regDigest(t, "baz.txt", "bazbazbazbazbazbazbaz", dgstMap), regDigest(t, "foo.txt", "a", dgstMap), dir("test/"), regDigest(t, "test/bar.txt", "testbartestbar", dgstMap), ) }, checks: []check{ checkStargzTOC, checkVerifyTOC, checkVerifyInvalidStargzFail(buildTar(t, tarOf( file("baz.txt", "bazbazbazMMMbazbazbaz"), // modified file("foo.txt", "a"), dir("test/"), file("test/bar.txt", "testbartestbar"), ), allowedPrefix[0])), checkVerifyInvalidTOCEntryFail("test/bar.txt"), checkVerifyBrokenContentFail("test/bar.txt"), }, }, { name: "with-non-regfiles", minChunkSize: []int{0, 64000}, tarInit: func(t TestingT, dgstMap map[string]digest.Digest) (blob []tarEntry) { return tarOf( regDigest(t, "baz.txt", "bazbazbazbazbazbazbaz", dgstMap), regDigest(t, "foo.txt", "a", dgstMap), regDigest(t, "bar/foo2.txt", "b", dgstMap), regDigest(t, "foo3.txt", "c", dgstMap), symlink("barlink", "test/bar.txt"), dir("test/"), regDigest(t, "test/bar.txt", "testbartestbar", dgstMap), dir("test2/"), link("test2/bazlink", "baz.txt"), ) }, checks: []check{ checkStargzTOC, checkVerifyTOC, checkVerifyInvalidStargzFail(buildTar(t, tarOf( file("baz.txt", "bazbazbazbazbazbazbaz"), file("foo.txt", "a"), file("bar/foo2.txt", "b"), file("foo3.txt", "c"), symlink("barlink", "test/bar.txt"), dir("test/"), file("test/bar.txt", "testbartestbar"), dir("test2/"), link("test2/bazlink", "foo.txt"), // modified ), allowedPrefix[0])), checkVerifyInvalidTOCEntryFail("test/bar.txt"), checkVerifyBrokenContentFail("test/bar.txt"), }, }, } for _, tt := range tests { if len(tt.minChunkSize) == 0 { tt.minChunkSize = []int{0} } for _, srcCompression := range srcCompressions { srcCompression := srcCompression for _, newCL := range controllers { newCL := newCL for _, prefix := range allowedPrefix { prefix := prefix for _, srcTarFormat := range []tar.Format{tar.FormatUSTAR, tar.FormatPAX, tar.FormatGNU} { srcTarFormat := srcTarFormat for _, minChunkSize := range tt.minChunkSize { minChunkSize := minChunkSize t.Run(tt.name+"-"+fmt.Sprintf("compression=%v,prefix=%q,format=%s,minChunkSize=%d", newCL(), prefix, srcTarFormat, minChunkSize), func(t *TestRunner) { // Get original tar file and chunk digests dgstMap := make(map[string]digest.Digest) tarBlob := buildTar(t, tt.tarInit(t, dgstMap), prefix, srcTarFormat) cl := newCL() rc, err := Build(compressBlob(t, tarBlob, srcCompression), WithChunkSize(chunkSize), WithCompression(cl)) if err != nil { t.Fatalf("failed to convert stargz: %v", err) } tocDigest := rc.TOCDigest() defer rc.Close() buf := new(bytes.Buffer) if _, err := io.Copy(buf, rc); err != nil { t.Fatalf("failed to copy built stargz blob: %v", err) } newStargz := buf.Bytes() // NoPrefetchLandmark is added during `Bulid`, which is expected behaviour. dgstMap[chunkID(NoPrefetchLandmark, 0, int64(len([]byte{landmarkContents})))] = digest.FromBytes([]byte{landmarkContents}) for _, check := range tt.checks { check(t, newStargz, tocDigest, dgstMap, cl, newCL) } }) } } } } } } } // checkStargzTOC checks the TOC JSON of the passed stargz has the expected // digest and contains valid chunks. It walks all entries in the stargz and // checks all chunk digests stored to the TOC JSON match the actual contents. func checkStargzTOC(t *TestRunner, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) { sgz, err := Open( io.NewSectionReader(bytes.NewReader(sgzData), 0, int64(len(sgzData))), WithDecompressors(controller), ) if err != nil { t.Errorf("failed to parse converted stargz: %v", err) return } digestMapTOC, err := listDigests(io.NewSectionReader( bytes.NewReader(sgzData), 0, int64(len(sgzData))), controller, ) if err != nil { t.Fatalf("failed to list digest: %v", err) } found := make(map[string]bool) for id := range dgstMap { found[id] = false } zr, err := controller.Reader(bytes.NewReader(sgzData)) if err != nil { t.Fatalf("failed to decompress converted stargz: %v", err) } defer zr.Close() tr := tar.NewReader(zr) for { h, err := tr.Next() if err != nil { if err != io.EOF { t.Errorf("failed to read tar entry: %v", err) return } break } if h.Name == TOCTarName { // Check the digest of TOC JSON based on the actual contents // It's sure that TOC JSON exists in this archive because // Open succeeded. dgstr := digest.Canonical.Digester() if _, err := io.Copy(dgstr.Hash(), tr); err != nil { t.Fatalf("failed to calculate digest of TOC JSON: %v", err) } if dgstr.Digest() != tocDigest { t.Errorf("invalid TOC JSON %q; want %q", tocDigest, dgstr.Digest()) } continue } if _, ok := sgz.Lookup(h.Name); !ok { t.Errorf("lost stargz entry %q in the converted TOC", h.Name) return } var n int64 for n < h.Size { ce, ok := sgz.ChunkEntryForOffset(h.Name, n) if !ok { t.Errorf("lost chunk %q(offset=%d) in the converted TOC", h.Name, n) return } // Get the original digest to make sure the file contents are kept unchanged // from the original tar, during the whole conversion steps. id := chunkID(h.Name, n, ce.ChunkSize) want, ok := dgstMap[id] if !ok { t.Errorf("Unexpected chunk %q(offset=%d,size=%d): %v", h.Name, n, ce.ChunkSize, dgstMap) return } found[id] = true // Check the file contents dgstr := digest.Canonical.Digester() if _, err := io.CopyN(dgstr.Hash(), tr, ce.ChunkSize); err != nil { t.Fatalf("failed to calculate digest of %q (offset=%d,size=%d)", h.Name, n, ce.ChunkSize) } if want != dgstr.Digest() { t.Errorf("Invalid contents in converted stargz %q: %q; want %q", h.Name, dgstr.Digest(), want) return } // Check the digest stored in TOC JSON dgstTOC, ok := digestMapTOC[ce.Offset] if !ok { t.Errorf("digest of %q(offset=%d,size=%d,chunkOffset=%d) isn't registered", h.Name, ce.Offset, ce.ChunkSize, ce.ChunkOffset) } if want != dgstTOC { t.Errorf("Invalid digest in TOCEntry %q: %q; want %q", h.Name, dgstTOC, want) return } n += ce.ChunkSize } } for id, ok := range found { if !ok { t.Errorf("required chunk %q not found in the converted stargz: %v", id, found) } } } // checkVerifyTOC checks the verification works for the TOC JSON of the passed // stargz. It walks all entries in the stargz and checks the verifications for // all chunks work. func checkVerifyTOC(t *TestRunner, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) { sgz, err := Open( io.NewSectionReader(bytes.NewReader(sgzData), 0, int64(len(sgzData))), WithDecompressors(controller), ) if err != nil { t.Errorf("failed to parse converted stargz: %v", err) return } ev, err := sgz.VerifyTOC(tocDigest) if err != nil { t.Errorf("failed to verify stargz: %v", err) return } found := make(map[string]bool) for id := range dgstMap { found[id] = false } zr, err := controller.Reader(bytes.NewReader(sgzData)) if err != nil { t.Fatalf("failed to decompress converted stargz: %v", err) } defer zr.Close() tr := tar.NewReader(zr) for { h, err := tr.Next() if err != nil { if err != io.EOF { t.Errorf("failed to read tar entry: %v", err) return } break } if h.Name == TOCTarName { continue } if _, ok := sgz.Lookup(h.Name); !ok { t.Errorf("lost stargz entry %q in the converted TOC", h.Name) return } var n int64 for n < h.Size { ce, ok := sgz.ChunkEntryForOffset(h.Name, n) if !ok { t.Errorf("lost chunk %q(offset=%d) in the converted TOC", h.Name, n) return } v, err := ev.Verifier(ce) if err != nil { t.Errorf("failed to get verifier for %q(offset=%d)", h.Name, n) } found[chunkID(h.Name, n, ce.ChunkSize)] = true // Check the file contents if _, err := io.CopyN(v, tr, ce.ChunkSize); err != nil { t.Fatalf("failed to get chunk of %q (offset=%d,size=%d)", h.Name, n, ce.ChunkSize) } if !v.Verified() { t.Errorf("Invalid contents in converted stargz %q (should be succeeded)", h.Name) return } n += ce.ChunkSize } } for id, ok := range found { if !ok { t.Errorf("required chunk %q not found in the converted stargz: %v", id, found) } } } // checkVerifyInvalidTOCEntryFail checks if misconfigured TOC JSON can be // detected during the verification and the verification returns an error. func checkVerifyInvalidTOCEntryFail(filename string) check { return func(t *TestRunner, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) { funcs := map[string]rewriteFunc{ "lost digest in a entry": func(t TestingT, toc *JTOC, sgz *io.SectionReader) { var found bool for _, e := range toc.Entries { if cleanEntryName(e.Name) == filename { if e.Type != "reg" && e.Type != "chunk" { t.Fatalf("entry %q to break must be regfile or chunk", filename) } if e.ChunkDigest == "" { t.Fatalf("entry %q is already invalid", filename) } e.ChunkDigest = "" found = true } } if !found { t.Fatalf("rewrite target not found") } }, "duplicated entry offset": func(t TestingT, toc *JTOC, sgz *io.SectionReader) { var ( sampleEntry *TOCEntry targetEntry *TOCEntry ) for _, e := range toc.Entries { if e.Type == "reg" || e.Type == "chunk" { if cleanEntryName(e.Name) == filename { targetEntry = e } else { sampleEntry = e } } } if sampleEntry == nil { t.Fatalf("TOC must contain at least one regfile or chunk entry other than the rewrite target") return } if targetEntry == nil { t.Fatalf("rewrite target not found") return } targetEntry.Offset = sampleEntry.Offset }, } for name, rFunc := range funcs { t.Run(name, func(t *TestRunner) { newSgz, newTocDigest := rewriteTOCJSON(t, io.NewSectionReader(bytes.NewReader(sgzData), 0, int64(len(sgzData))), rFunc, controller) buf := new(bytes.Buffer) if _, err := io.Copy(buf, newSgz); err != nil { t.Fatalf("failed to get converted stargz") } isgz := buf.Bytes() sgz, err := Open( io.NewSectionReader(bytes.NewReader(isgz), 0, int64(len(isgz))), WithDecompressors(controller), ) if err != nil { t.Fatalf("failed to parse converted stargz: %v", err) return } _, err = sgz.VerifyTOC(newTocDigest) if err == nil { t.Errorf("must fail for invalid TOC") return } }) } } } // checkVerifyInvalidStargzFail checks if the verification detects that the // given stargz file doesn't match to the expected digest and returns error. func checkVerifyInvalidStargzFail(invalid *io.SectionReader) check { return func(t *TestRunner, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) { cl := newController() rc, err := Build(invalid, WithChunkSize(chunkSize), WithCompression(cl)) if err != nil { t.Fatalf("failed to convert stargz: %v", err) } defer rc.Close() buf := new(bytes.Buffer) if _, err := io.Copy(buf, rc); err != nil { t.Fatalf("failed to copy built stargz blob: %v", err) } mStargz := buf.Bytes() sgz, err := Open( io.NewSectionReader(bytes.NewReader(mStargz), 0, int64(len(mStargz))), WithDecompressors(cl), ) if err != nil { t.Fatalf("failed to parse converted stargz: %v", err) return } _, err = sgz.VerifyTOC(tocDigest) if err == nil { t.Errorf("must fail for invalid TOC") return } } } // checkVerifyBrokenContentFail checks if the verifier detects broken contents // that doesn't match to the expected digest and returns error. func checkVerifyBrokenContentFail(filename string) check { return func(t *TestRunner, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) { // Parse stargz file sgz, err := Open( io.NewSectionReader(bytes.NewReader(sgzData), 0, int64(len(sgzData))), WithDecompressors(controller), ) if err != nil { t.Fatalf("failed to parse converted stargz: %v", err) return } ev, err := sgz.VerifyTOC(tocDigest) if err != nil { t.Fatalf("failed to verify stargz: %v", err) return } // Open the target file sr, err := sgz.OpenFile(filename) if err != nil { t.Fatalf("failed to open file %q", filename) } ce, ok := sgz.ChunkEntryForOffset(filename, 0) if !ok { t.Fatalf("lost chunk %q(offset=%d) in the converted TOC", filename, 0) return } if ce.ChunkSize == 0 { t.Fatalf("file mustn't be empty") return } data := make([]byte, ce.ChunkSize) if _, err := sr.ReadAt(data, ce.ChunkOffset); err != nil { t.Errorf("failed to get data of a chunk of %q(offset=%q)", filename, ce.ChunkOffset) } // Check the broken chunk (must fail) v, err := ev.Verifier(ce) if err != nil { t.Fatalf("failed to get verifier for %q", filename) } broken := append([]byte{^data[0]}, data[1:]...) if _, err := io.CopyN(v, bytes.NewReader(broken), ce.ChunkSize); err != nil { t.Fatalf("failed to get chunk of %q (offset=%d,size=%d)", filename, ce.ChunkOffset, ce.ChunkSize) } if v.Verified() { t.Errorf("verification must fail for broken file chunk %q(org:%q,broken:%q)", filename, data, broken) } } } func chunkID(name string, offset, size int64) string { return fmt.Sprintf("%s-%d-%d", cleanEntryName(name), offset, size) } type rewriteFunc func(t TestingT, toc *JTOC, sgz *io.SectionReader) func rewriteTOCJSON(t TestingT, sgz *io.SectionReader, rewrite rewriteFunc, controller TestingController) (newSgz io.Reader, tocDigest digest.Digest) { decodedJTOC, jtocOffset, err := parseStargz(sgz, controller) if err != nil { t.Fatalf("failed to extract TOC JSON: %v", err) } rewrite(t, decodedJTOC, sgz) tocFooter, tocDigest, err := tocAndFooter(controller, decodedJTOC, jtocOffset) if err != nil { t.Fatalf("failed to create toc and footer: %v", err) } // Reconstruct stargz file with the modified TOC JSON if _, err := sgz.Seek(0, io.SeekStart); err != nil { t.Fatalf("failed to reset the seek position of stargz: %v", err) } return io.MultiReader( io.LimitReader(sgz, jtocOffset), // Original stargz (before TOC JSON) tocFooter, // Rewritten TOC and footer ), tocDigest } func listDigests(sgz *io.SectionReader, controller TestingController) (map[int64]digest.Digest, error) { decodedJTOC, _, err := parseStargz(sgz, controller) if err != nil { return nil, err } digestMap := make(map[int64]digest.Digest) for _, e := range decodedJTOC.Entries { if e.Type == "reg" || e.Type == "chunk" { if e.Type == "reg" && e.Size == 0 { continue // ignores empty file } if e.ChunkDigest == "" { return nil, fmt.Errorf("ChunkDigest of %q(off=%d) not found in TOC JSON", e.Name, e.Offset) } d, err := digest.Parse(e.ChunkDigest) if err != nil { return nil, err } digestMap[e.Offset] = d } } return digestMap, nil } func parseStargz(sgz *io.SectionReader, controller TestingController) (decodedJTOC *JTOC, jtocOffset int64, err error) { fSize := controller.FooterSize() footer := make([]byte, fSize) if _, err := sgz.ReadAt(footer, sgz.Size()-fSize); err != nil { return nil, 0, fmt.Errorf("error reading footer: %w", err) } _, tocOffset, _, err := controller.ParseFooter(footer[positive(int64(len(footer))-fSize):]) if err != nil { return nil, 0, fmt.Errorf("failed to parse footer: %w", err) } // Decode the TOC JSON var tocReader io.Reader if tocOffset >= 0 { tocReader = io.NewSectionReader(sgz, tocOffset, sgz.Size()-tocOffset-fSize) } decodedJTOC, _, err = controller.ParseTOC(tocReader) if err != nil { return nil, 0, fmt.Errorf("failed to parse TOC: %w", err) } return decodedJTOC, tocOffset, nil } func testWriteAndOpen(t *TestRunner, controllers ...TestingControllerFactory) { const content = "Some contents" invalidUtf8 := "\xff\xfe\xfd" xAttrFile := xAttr{"foo": "bar", "invalid-utf8": invalidUtf8} sampleOwner := owner{uid: 50, gid: 100} data64KB := randomContents(64000) tests := []struct { name string chunkSize int minChunkSize int in []tarEntry want []stargzCheck wantNumGz int // expected number of streams wantNumGzLossLess int // expected number of streams (> 0) in lossless mode if it's different from wantNumGz wantFailOnLossLess bool wantTOCVersion int // default = 1 }{ { name: "empty", in: tarOf(), wantNumGz: 2, // (empty tar) + TOC + footer want: checks( numTOCEntries(0), ), }, { name: "1dir_1empty_file", in: tarOf( dir("foo/"), file("foo/bar.txt", ""), ), wantNumGz: 3, // dir, TOC, footer want: checks( numTOCEntries(2), hasDir("foo/"), hasFileLen("foo/bar.txt", 0), entryHasChildren("foo", "bar.txt"), hasFileDigest("foo/bar.txt", digestFor("")), ), }, { name: "1dir_1file", in: tarOf( dir("foo/"), file("foo/bar.txt", content, xAttrFile), ), wantNumGz: 4, // var dir, foo.txt alone, TOC, footer want: checks( numTOCEntries(2), hasDir("foo/"), hasFileLen("foo/bar.txt", len(content)), hasFileDigest("foo/bar.txt", digestFor(content)), hasFileContentsRange("foo/bar.txt", 0, content), hasFileContentsRange("foo/bar.txt", 1, content[1:]), entryHasChildren("", "foo"), entryHasChildren("foo", "bar.txt"), hasFileXattrs("foo/bar.txt", "foo", "bar"), hasFileXattrs("foo/bar.txt", "invalid-utf8", invalidUtf8), ), }, { name: "2meta_2file", in: tarOf( dir("bar/", sampleOwner), dir("foo/", sampleOwner), file("foo/bar.txt", content, sampleOwner), ), wantNumGz: 4, // both dirs, foo.txt alone, TOC, footer want: checks( numTOCEntries(3), hasDir("bar/"), hasDir("foo/"), hasFileLen("foo/bar.txt", len(content)), entryHasChildren("", "bar", "foo"), entryHasChildren("foo", "bar.txt"), hasChunkEntries("foo/bar.txt", 1), hasEntryOwner("bar/", sampleOwner), hasEntryOwner("foo/", sampleOwner), hasEntryOwner("foo/bar.txt", sampleOwner), ), }, { name: "3dir", in: tarOf( dir("bar/"), dir("foo/"), dir("foo/bar/"), ), wantNumGz: 3, // 3 dirs, TOC, footer want: checks( hasDirLinkCount("bar/", 2), hasDirLinkCount("foo/", 3), hasDirLinkCount("foo/bar/", 2), ), }, { name: "symlink", in: tarOf( dir("foo/"), symlink("foo/bar", "../../x"), ), wantNumGz: 3, // metas + TOC + footer want: checks( numTOCEntries(2), hasSymlink("foo/bar", "../../x"), entryHasChildren("", "foo"), entryHasChildren("foo", "bar"), ), }, { name: "chunked_file", chunkSize: 4, in: tarOf( dir("foo/"), file("foo/big.txt", "This "+"is s"+"uch "+"a bi"+"g fi"+"le"), ), wantNumGz: 9, // dir + big.txt(6 chunks) + TOC + footer want: checks( numTOCEntries(7), // 1 for foo dir, 6 for the foo/big.txt file hasDir("foo/"), hasFileLen("foo/big.txt", len("This is such a big file")), hasFileDigest("foo/big.txt", digestFor("This is such a big file")), hasFileContentsRange("foo/big.txt", 0, "This is such a big file"), hasFileContentsRange("foo/big.txt", 1, "his is such a big file"), hasFileContentsRange("foo/big.txt", 2, "is is such a big file"), hasFileContentsRange("foo/big.txt", 3, "s is such a big file"), hasFileContentsRange("foo/big.txt", 4, " is such a big file"), hasFileContentsRange("foo/big.txt", 5, "is such a big file"), hasFileContentsRange("foo/big.txt", 6, "s such a big file"), hasFileContentsRange("foo/big.txt", 7, " such a big file"), hasFileContentsRange("foo/big.txt", 8, "such a big file"), hasFileContentsRange("foo/big.txt", 9, "uch a big file"), hasFileContentsRange("foo/big.txt", 10, "ch a big file"), hasFileContentsRange("foo/big.txt", 11, "h a big file"), hasFileContentsRange("foo/big.txt", 12, " a big file"), hasFileContentsRange("foo/big.txt", len("This is such a big file")-1, ""), hasChunkEntries("foo/big.txt", 6), ), }, { name: "recursive", in: tarOf( dir("/", sampleOwner), dir("bar/", sampleOwner), dir("foo/", sampleOwner), file("foo/bar.txt", content, sampleOwner), ), wantNumGz: 4, // dirs, bar.txt alone, TOC, footer want: checks( maxDepth(2), // 0: root directory, 1: "foo/", 2: "bar.txt" ), }, { name: "block_char_fifo", in: tarOf( tarEntryFunc(func(w *tar.Writer, prefix string, format tar.Format) error { return w.WriteHeader(&tar.Header{ Name: prefix + "b", Typeflag: tar.TypeBlock, Devmajor: 123, Devminor: 456, Format: format, }) }), tarEntryFunc(func(w *tar.Writer, prefix string, format tar.Format) error { return w.WriteHeader(&tar.Header{ Name: prefix + "c", Typeflag: tar.TypeChar, Devmajor: 111, Devminor: 222, Format: format, }) }), tarEntryFunc(func(w *tar.Writer, prefix string, format tar.Format) error { return w.WriteHeader(&tar.Header{ Name: prefix + "f", Typeflag: tar.TypeFifo, Format: format, }) }), ), wantNumGz: 3, want: checks( lookupMatch("b", &TOCEntry{Name: "b", Type: "block", DevMajor: 123, DevMinor: 456, NumLink: 1}), lookupMatch("c", &TOCEntry{Name: "c", Type: "char", DevMajor: 111, DevMinor: 222, NumLink: 1}), lookupMatch("f", &TOCEntry{Name: "f", Type: "fifo", NumLink: 1}), ), }, { name: "modes", in: tarOf( dir("foo1/", 0755|os.ModeDir|os.ModeSetgid), file("foo1/bar1", content, 0700|os.ModeSetuid), file("foo1/bar2", content, 0755|os.ModeSetgid), dir("foo2/", 0755|os.ModeDir|os.ModeSticky), file("foo2/bar3", content, 0755|os.ModeSticky), dir("foo3/", 0755|os.ModeDir), file("foo3/bar4", content, os.FileMode(0700)), file("foo3/bar5", content, os.FileMode(0755)), ), wantNumGz: 8, // dir, bar1 alone, bar2 alone + dir, bar3 alone + dir, bar4 alone, bar5 alone, TOC, footer want: checks( hasMode("foo1/", 0755|os.ModeDir|os.ModeSetgid), hasMode("foo1/bar1", 0700|os.ModeSetuid), hasMode("foo1/bar2", 0755|os.ModeSetgid), hasMode("foo2/", 0755|os.ModeDir|os.ModeSticky), hasMode("foo2/bar3", 0755|os.ModeSticky), hasMode("foo3/", 0755|os.ModeDir), hasMode("foo3/bar4", os.FileMode(0700)), hasMode("foo3/bar5", os.FileMode(0755)), ), }, { name: "lossy", in: tarOf( dir("bar/", sampleOwner), dir("foo/", sampleOwner), file("foo/bar.txt", content, sampleOwner), file(TOCTarName, "dummy"), // ignored by the writer. (lossless write returns error) ), wantNumGz: 4, // both dirs, foo.txt alone, TOC, footer want: checks( numTOCEntries(3), hasDir("bar/"), hasDir("foo/"), hasFileLen("foo/bar.txt", len(content)), entryHasChildren("", "bar", "foo"), entryHasChildren("foo", "bar.txt"), hasChunkEntries("foo/bar.txt", 1), hasEntryOwner("bar/", sampleOwner), hasEntryOwner("foo/", sampleOwner), hasEntryOwner("foo/bar.txt", sampleOwner), ), wantFailOnLossLess: true, }, { name: "hardlink should be replaced to the destination entry", in: tarOf( dir("foo/"), file("foo/foo1", "test"), link("foolink", "foo/foo1"), ), wantNumGz: 4, // dir, foo1 + link, TOC, footer want: checks( mustSameEntry("foo/foo1", "foolink"), ), }, { name: "several_files_in_chunk", minChunkSize: 8000, in: tarOf( dir("foo/"), file("foo/foo1", data64KB), file("foo2", "bb"), file("foo22", "ccc"), dir("bar/"), file("bar/bar.txt", "aaa"), file("foo3", data64KB), ), // NOTE: we assume that the compressed "data64KB" is still larger than 8KB wantNumGz: 4, // dir+foo1, foo2+foo22+dir+bar.txt+foo3, TOC, footer want: checks( numTOCEntries(7), // dir, foo1, foo2, foo22, dir, bar.txt, foo3 hasDir("foo/"), hasDir("bar/"), hasFileLen("foo/foo1", len(data64KB)), hasFileLen("foo2", len("bb")), hasFileLen("foo22", len("ccc")), hasFileLen("bar/bar.txt", len("aaa")), hasFileLen("foo3", len(data64KB)), hasFileDigest("foo/foo1", digestFor(data64KB)), hasFileDigest("foo2", digestFor("bb")), hasFileDigest("foo22", digestFor("ccc")), hasFileDigest("bar/bar.txt", digestFor("aaa")), hasFileDigest("foo3", digestFor(data64KB)), hasFileContentsWithPreRead("foo22", 0, "ccc", chunkInfo{"foo2", "bb"}, chunkInfo{"bar/bar.txt", "aaa"}, chunkInfo{"foo3", data64KB}), hasFileContentsRange("foo/foo1", 0, data64KB), hasFileContentsRange("foo2", 0, "bb"), hasFileContentsRange("foo2", 1, "b"), hasFileContentsRange("foo22", 0, "ccc"), hasFileContentsRange("foo22", 1, "cc"), hasFileContentsRange("foo22", 2, "c"), hasFileContentsRange("bar/bar.txt", 0, "aaa"), hasFileContentsRange("bar/bar.txt", 1, "aa"), hasFileContentsRange("bar/bar.txt", 2, "a"), hasFileContentsRange("foo3", 0, data64KB), hasFileContentsRange("foo3", 1, data64KB[1:]), hasFileContentsRange("foo3", 2, data64KB[2:]), hasFileContentsRange("foo3", len(data64KB)/2, data64KB[len(data64KB)/2:]), hasFileContentsRange("foo3", len(data64KB)-1, data64KB[len(data64KB)-1:]), ), }, { name: "several_files_in_chunk_chunked", minChunkSize: 8000, chunkSize: 32000, in: tarOf( dir("foo/"), file("foo/foo1", data64KB), file("foo2", "bb"), dir("bar/"), file("foo3", data64KB), ), // NOTE: we assume that the compressed chunk of "data64KB" is still larger than 8KB wantNumGz: 6, // dir+foo1(1), foo1(2), foo2+dir+foo3(1), foo3(2), TOC, footer want: checks( numTOCEntries(7), // dir, foo1(2 chunks), foo2, dir, foo3(2 chunks) hasDir("foo/"), hasDir("bar/"), hasFileLen("foo/foo1", len(data64KB)), hasFileLen("foo2", len("bb")), hasFileLen("foo3", len(data64KB)), hasFileDigest("foo/foo1", digestFor(data64KB)), hasFileDigest("foo2", digestFor("bb")), hasFileDigest("foo3", digestFor(data64KB)), hasFileContentsWithPreRead("foo2", 0, "bb", chunkInfo{"foo3", data64KB[:32000]}), hasFileContentsRange("foo/foo1", 0, data64KB), hasFileContentsRange("foo/foo1", 1, data64KB[1:]), hasFileContentsRange("foo/foo1", 2, data64KB[2:]), hasFileContentsRange("foo/foo1", len(data64KB)/2, data64KB[len(data64KB)/2:]), hasFileContentsRange("foo/foo1", len(data64KB)-1, data64KB[len(data64KB)-1:]), hasFileContentsRange("foo2", 0, "bb"), hasFileContentsRange("foo2", 1, "b"), hasFileContentsRange("foo3", 0, data64KB), hasFileContentsRange("foo3", 1, data64KB[1:]), hasFileContentsRange("foo3", 2, data64KB[2:]), hasFileContentsRange("foo3", len(data64KB)/2, data64KB[len(data64KB)/2:]), hasFileContentsRange("foo3", len(data64KB)-1, data64KB[len(data64KB)-1:]), ), }, } for _, tt := range tests { for _, newCL := range controllers { newCL := newCL for _, prefix := range allowedPrefix { prefix := prefix for _, srcTarFormat := range []tar.Format{tar.FormatUSTAR, tar.FormatPAX, tar.FormatGNU} { srcTarFormat := srcTarFormat for _, lossless := range []bool{true, false} { t.Run(tt.name+"-"+fmt.Sprintf("compression=%v,prefix=%q,lossless=%v,format=%s", newCL(), prefix, lossless, srcTarFormat), func(t *TestRunner) { var tr io.Reader = buildTar(t, tt.in, prefix, srcTarFormat) origTarDgstr := digest.Canonical.Digester() tr = io.TeeReader(tr, origTarDgstr.Hash()) var stargzBuf bytes.Buffer cl1 := newCL() w := NewWriterWithCompressor(&stargzBuf, cl1) w.ChunkSize = tt.chunkSize w.MinChunkSize = tt.minChunkSize if lossless { err := w.AppendTarLossLess(tr) if tt.wantFailOnLossLess { if err != nil { return // expected to fail } t.Fatalf("Append wanted to fail on lossless") } if err != nil { t.Fatalf("Append(lossless): %v", err) } } else { if err := w.AppendTar(tr); err != nil { t.Fatalf("Append: %v", err) } } if _, err := w.Close(); err != nil { t.Fatalf("Writer.Close: %v", err) } b := stargzBuf.Bytes() if lossless { // Check if the result blob reserves original tar metadata rc, err := Unpack(io.NewSectionReader(bytes.NewReader(b), 0, int64(len(b))), cl1) if err != nil { t.Errorf("failed to decompress blob: %v", err) return } defer rc.Close() resultDgstr := digest.Canonical.Digester() if _, err := io.Copy(resultDgstr.Hash(), rc); err != nil { t.Errorf("failed to read result decompressed blob: %v", err) return } if resultDgstr.Digest() != origTarDgstr.Digest() { t.Errorf("lossy compression occurred: digest=%v; want %v", resultDgstr.Digest(), origTarDgstr.Digest()) return } } diffID := w.DiffID() wantDiffID := cl1.DiffIDOf(t, b) if diffID != wantDiffID { t.Errorf("DiffID = %q; want %q", diffID, wantDiffID) } telemetry, checkCalled := newCalledTelemetry() sr := io.NewSectionReader(bytes.NewReader(b), 0, int64(len(b))) r, err := Open( sr, WithDecompressors(cl1), WithTelemetry(telemetry), ) if err != nil { t.Fatalf("stargz.Open: %v", err) } if _, ok := r.Lookup(""); !ok { t.Fatalf("failed to lookup rootdir: %v", err) } wantTOCVersion := 1 if tt.wantTOCVersion > 0 { wantTOCVersion = tt.wantTOCVersion } if r.toc.Version != wantTOCVersion { t.Fatalf("invalid TOC Version %d; wanted %d", r.toc.Version, wantTOCVersion) } footerSize := cl1.FooterSize() footerOffset := sr.Size() - footerSize footer := make([]byte, footerSize) if _, err := sr.ReadAt(footer, footerOffset); err != nil { t.Errorf("failed to read footer: %v", err) } _, tocOffset, _, err := cl1.ParseFooter(footer) if err != nil { t.Errorf("failed to parse footer: %v", err) } if err := checkCalled(tocOffset >= 0); err != nil { t.Errorf("telemetry failure: %v", err) } wantNumGz := tt.wantNumGz if lossless && tt.wantNumGzLossLess > 0 { wantNumGz = tt.wantNumGzLossLess } streamOffsets := []int64{0} prevOffset := int64(-1) streams := 0 for _, e := range r.toc.Entries { if e.Offset > prevOffset { streamOffsets = append(streamOffsets, e.Offset) prevOffset = e.Offset streams++ } } streams++ // TOC if tocOffset >= 0 { // toc is in the blob streamOffsets = append(streamOffsets, tocOffset) } streams++ // footer streamOffsets = append(streamOffsets, footerOffset) if streams != wantNumGz { t.Errorf("number of streams in TOC = %d; want %d", streams, wantNumGz) } t.Logf("testing streams: %+v", streamOffsets) cl1.TestStreams(t, b, streamOffsets) for _, want := range tt.want { want.check(t, r) } }) } } } } } } type chunkInfo struct { name string data string } func newCalledTelemetry() (telemetry *Telemetry, check func(needsGetTOC bool) error) { var getFooterLatencyCalled bool var getTocLatencyCalled bool var deserializeTocLatencyCalled bool return &Telemetry{ func(time.Time) { getFooterLatencyCalled = true }, func(time.Time) { getTocLatencyCalled = true }, func(time.Time) { deserializeTocLatencyCalled = true }, }, func(needsGetTOC bool) error { var allErr []error if !getFooterLatencyCalled { allErr = append(allErr, fmt.Errorf("metrics GetFooterLatency isn't called")) } if needsGetTOC { if !getTocLatencyCalled { allErr = append(allErr, fmt.Errorf("metrics GetTocLatency isn't called")) } } if !deserializeTocLatencyCalled { allErr = append(allErr, fmt.Errorf("metrics DeserializeTocLatency isn't called")) } return errorutil.Aggregate(allErr) } } func digestFor(content string) string { sum := sha256.Sum256([]byte(content)) return fmt.Sprintf("sha256:%x", sum) } type numTOCEntries int func (n numTOCEntries) check(t TestingT, r *Reader) { if r.toc == nil { t.Fatal("nil TOC") } if got, want := len(r.toc.Entries), int(n); got != want { t.Errorf("got %d TOC entries; want %d", got, want) } t.Logf("got TOC entries:") for i, ent := range r.toc.Entries { entj, _ := json.Marshal(ent) t.Logf(" [%d]: %s\n", i, entj) } if t.Failed() { t.FailNow() } } func checks(s ...stargzCheck) []stargzCheck { return s } type stargzCheck interface { check(t TestingT, r *Reader) } type stargzCheckFn func(TestingT, *Reader) func (f stargzCheckFn) check(t TestingT, r *Reader) { f(t, r) } func maxDepth(max int) stargzCheck { return stargzCheckFn(func(t TestingT, r *Reader) { e, ok := r.Lookup("") if !ok { t.Fatal("root directory not found") } d, err := getMaxDepth(t, e, 0, 10*max) if err != nil { t.Errorf("failed to get max depth (wanted %d): %v", max, err) return } if d != max { t.Errorf("invalid depth %d; want %d", d, max) return } }) } func getMaxDepth(t TestingT, e *TOCEntry, current, limit int) (max int, rErr error) { if current > limit { return -1, fmt.Errorf("walkMaxDepth: exceeds limit: current:%d > limit:%d", current, limit) } max = current e.ForeachChild(func(baseName string, ent *TOCEntry) bool { t.Logf("%q(basename:%q) is child of %q\n", ent.Name, baseName, e.Name) d, err := getMaxDepth(t, ent, current+1, limit) if err != nil { rErr = err return false } if d > max { max = d } return true }) return } func hasFileLen(file string, wantLen int) stargzCheck { return stargzCheckFn(func(t TestingT, r *Reader) { for _, ent := range r.toc.Entries { if ent.Name == file { if ent.Type != "reg" { t.Errorf("file type of %q is %q; want \"reg\"", file, ent.Type) } else if ent.Size != int64(wantLen) { t.Errorf("file size of %q = %d; want %d", file, ent.Size, wantLen) } return } } t.Errorf("file %q not found", file) }) } func hasFileXattrs(file, name, value string) stargzCheck { return stargzCheckFn(func(t TestingT, r *Reader) { for _, ent := range r.toc.Entries { if ent.Name == file { if ent.Type != "reg" { t.Errorf("file type of %q is %q; want \"reg\"", file, ent.Type) } if ent.Xattrs == nil { t.Errorf("file %q has no xattrs", file) return } valueFound, found := ent.Xattrs[name] if !found { t.Errorf("file %q has no xattr %q", file, name) return } if string(valueFound) != value { t.Errorf("file %q has xattr %q with value %q instead of %q", file, name, valueFound, value) } return } } t.Errorf("file %q not found", file) }) } func hasFileDigest(file string, digest string) stargzCheck { return stargzCheckFn(func(t TestingT, r *Reader) { ent, ok := r.Lookup(file) if !ok { t.Fatalf("didn't find TOCEntry for file %q", file) } if ent.Digest != digest { t.Fatalf("Digest(%q) = %q, want %q", file, ent.Digest, digest) } }) } func hasFileContentsWithPreRead(file string, offset int, want string, extra ...chunkInfo) stargzCheck { return stargzCheckFn(func(t TestingT, r *Reader) { extraMap := make(map[string]chunkInfo) for _, e := range extra { extraMap[e.name] = e } var extraNames []string for n := range extraMap { extraNames = append(extraNames, n) } f, err := r.OpenFileWithPreReader(file, func(e *TOCEntry, cr io.Reader) error { t.Logf("On %q: got preread of %q", file, e.Name) ex, ok := extraMap[e.Name] if !ok { t.Fatalf("fail on %q: unexpected entry %q: %+v, %+v", file, e.Name, e, extraNames) } got, err := io.ReadAll(cr) if err != nil { t.Fatalf("fail on %q: failed to read %q: %v", file, e.Name, err) } if ex.data != string(got) { t.Fatalf("fail on %q: unexpected contents of %q: len=%d; want=%d", file, e.Name, len(got), len(ex.data)) } delete(extraMap, e.Name) return nil }) if err != nil { t.Fatal(err) } got := make([]byte, len(want)) n, err := f.ReadAt(got, int64(offset)) if err != nil { t.Fatalf("ReadAt(len %d, offset %d, size %d) = %v, %v", len(got), offset, f.Size(), n, err) } if string(got) != want { t.Fatalf("ReadAt(len %d, offset %d) = %q, want %q", len(got), offset, viewContent(got), viewContent([]byte(want))) } if len(extraMap) != 0 { var exNames []string for _, ex := range extraMap { exNames = append(exNames, ex.name) } t.Fatalf("fail on %q: some entries aren't read: %+v", file, exNames) } }) } func hasFileContentsRange(file string, offset int, want string) stargzCheck { return stargzCheckFn(func(t TestingT, r *Reader) { f, err := r.OpenFile(file) if err != nil { t.Fatal(err) } got := make([]byte, len(want)) n, err := f.ReadAt(got, int64(offset)) if err != nil { t.Fatalf("ReadAt(len %d, offset %d) = %v, %v", len(got), offset, n, err) } if string(got) != want { t.Fatalf("ReadAt(len %d, offset %d) = %q, want %q", len(got), offset, viewContent(got), viewContent([]byte(want))) } }) } func hasChunkEntries(file string, wantChunks int) stargzCheck { return stargzCheckFn(func(t TestingT, r *Reader) { ent, ok := r.Lookup(file) if !ok { t.Fatalf("no file for %q", file) } if ent.Type != "reg" { t.Fatalf("file %q has unexpected type %q; want reg", file, ent.Type) } chunks := r.getChunks(ent) if len(chunks) != wantChunks { t.Errorf("len(r.getChunks(%q)) = %d; want %d", file, len(chunks), wantChunks) return } f := chunks[0] var gotChunks []*TOCEntry var last *TOCEntry for off := int64(0); off < f.Size; off++ { e, ok := r.ChunkEntryForOffset(file, off) if !ok { t.Errorf("no ChunkEntryForOffset at %d", off) return } if last != e { gotChunks = append(gotChunks, e) last = e } } if !reflect.DeepEqual(chunks, gotChunks) { t.Errorf("gotChunks=%d, want=%d; contents mismatch", len(gotChunks), wantChunks) } // And verify the NextOffset for i := 0; i < len(gotChunks)-1; i++ { ci := gotChunks[i] cnext := gotChunks[i+1] if ci.NextOffset() != cnext.Offset { t.Errorf("chunk %d NextOffset %d != next chunk's Offset of %d", i, ci.NextOffset(), cnext.Offset) } } }) } func entryHasChildren(dir string, want ...string) stargzCheck { return stargzCheckFn(func(t TestingT, r *Reader) { want := append([]string(nil), want...) var got []string ent, ok := r.Lookup(dir) if !ok { t.Fatalf("didn't find TOCEntry for dir node %q", dir) } for baseName := range ent.children { got = append(got, baseName) } sort.Strings(got) sort.Strings(want) if !reflect.DeepEqual(got, want) { t.Errorf("children of %q = %q; want %q", dir, got, want) } }) } func hasDir(file string) stargzCheck { return stargzCheckFn(func(t TestingT, r *Reader) { for _, ent := range r.toc.Entries { if ent.Name == cleanEntryName(file) { if ent.Type != "dir" { t.Errorf("file type of %q is %q; want \"dir\"", file, ent.Type) } return } } t.Errorf("directory %q not found", file) }) } func hasDirLinkCount(file string, count int) stargzCheck { return stargzCheckFn(func(t TestingT, r *Reader) { for _, ent := range r.toc.Entries { if ent.Name == cleanEntryName(file) { if ent.Type != "dir" { t.Errorf("file type of %q is %q; want \"dir\"", file, ent.Type) return } if ent.NumLink != count { t.Errorf("link count of %q = %d; want %d", file, ent.NumLink, count) } return } } t.Errorf("directory %q not found", file) }) } func hasMode(file string, mode os.FileMode) stargzCheck { return stargzCheckFn(func(t TestingT, r *Reader) { for _, ent := range r.toc.Entries { if ent.Name == cleanEntryName(file) { if ent.Stat().Mode() != mode { t.Errorf("invalid mode: got %v; want %v", ent.Stat().Mode(), mode) return } return } } t.Errorf("file %q not found", file) }) } func hasSymlink(file, target string) stargzCheck { return stargzCheckFn(func(t TestingT, r *Reader) { for _, ent := range r.toc.Entries { if ent.Name == file { if ent.Type != "symlink" { t.Errorf("file type of %q is %q; want \"symlink\"", file, ent.Type) } else if ent.LinkName != target { t.Errorf("link target of symlink %q is %q; want %q", file, ent.LinkName, target) } return } } t.Errorf("symlink %q not found", file) }) } func lookupMatch(name string, want *TOCEntry) stargzCheck { return stargzCheckFn(func(t TestingT, r *Reader) { e, ok := r.Lookup(name) if !ok { t.Fatalf("failed to Lookup entry %q", name) } if !reflect.DeepEqual(e, want) { t.Errorf("entry %q mismatch.\n got: %+v\nwant: %+v\n", name, e, want) } }) } func hasEntryOwner(entry string, owner owner) stargzCheck { return stargzCheckFn(func(t TestingT, r *Reader) { ent, ok := r.Lookup(strings.TrimSuffix(entry, "/")) if !ok { t.Errorf("entry %q not found", entry) return } if ent.UID != owner.uid || ent.GID != owner.gid { t.Errorf("entry %q has invalid owner (uid:%d, gid:%d) instead of (uid:%d, gid:%d)", entry, ent.UID, ent.GID, owner.uid, owner.gid) return } }) } func mustSameEntry(files ...string) stargzCheck { return stargzCheckFn(func(t TestingT, r *Reader) { var first *TOCEntry for _, f := range files { if first == nil { var ok bool first, ok = r.Lookup(f) if !ok { t.Errorf("unknown first file on Lookup: %q", f) return } } // Test Lookup e, ok := r.Lookup(f) if !ok { t.Errorf("unknown file on Lookup: %q", f) return } if e != first { t.Errorf("Lookup: %+v(%p) != %+v(%p)", e, e, first, first) return } // Test LookupChild pe, ok := r.Lookup(filepath.Dir(filepath.Clean(f))) if !ok { t.Errorf("failed to get parent of %q", f) return } e, ok = pe.LookupChild(filepath.Base(filepath.Clean(f))) if !ok { t.Errorf("failed to get %q as the child of %+v", f, pe) return } if e != first { t.Errorf("LookupChild: %+v(%p) != %+v(%p)", e, e, first, first) return } // Test ForeachChild pe.ForeachChild(func(baseName string, e *TOCEntry) bool { if baseName == filepath.Base(filepath.Clean(f)) { if e != first { t.Errorf("ForeachChild: %+v(%p) != %+v(%p)", e, e, first, first) return false } } return true }) } }) } func viewContent(c []byte) string { if len(c) < 100 { return string(c) } return string(c[:50]) + "...(omit)..." + string(c[50:100]) } func tarOf(s ...tarEntry) []tarEntry { return s } type tarEntry interface { appendTar(tw *tar.Writer, prefix string, format tar.Format) error } type tarEntryFunc func(*tar.Writer, string, tar.Format) error func (f tarEntryFunc) appendTar(tw *tar.Writer, prefix string, format tar.Format) error { return f(tw, prefix, format) } func buildTar(t TestingT, ents []tarEntry, prefix string, opts ...interface{}) *io.SectionReader { format := tar.FormatUnknown for _, opt := range opts { switch v := opt.(type) { case tar.Format: format = v default: panic(fmt.Errorf("unsupported opt for buildTar: %v", opt)) } } buf := new(bytes.Buffer) tw := tar.NewWriter(buf) for _, ent := range ents { if err := ent.appendTar(tw, prefix, format); err != nil { t.Fatalf("building input tar: %v", err) } } if err := tw.Close(); err != nil { t.Errorf("closing write of input tar: %v", err) } data := append(buf.Bytes(), make([]byte, 100)...) // append empty bytes at the tail to see lossless works return io.NewSectionReader(bytes.NewReader(data), 0, int64(len(data))) } func dir(name string, opts ...interface{}) tarEntry { return tarEntryFunc(func(tw *tar.Writer, prefix string, format tar.Format) error { var o owner mode := os.FileMode(0755) for _, opt := range opts { switch v := opt.(type) { case owner: o = v case os.FileMode: mode = v default: return errors.New("unsupported opt") } } if !strings.HasSuffix(name, "/") { panic(fmt.Sprintf("missing trailing slash in dir %q ", name)) } tm, err := fileModeToTarMode(mode) if err != nil { return err } return tw.WriteHeader(&tar.Header{ Typeflag: tar.TypeDir, Name: prefix + name, Mode: tm, Uid: o.uid, Gid: o.gid, Format: format, }) }) } // xAttr are extended attributes to set on test files created with the file func. type xAttr map[string]string // owner is owner ot set on test files and directories with the file and dir functions. type owner struct { uid int gid int } func file(name, contents string, opts ...interface{}) tarEntry { return tarEntryFunc(func(tw *tar.Writer, prefix string, format tar.Format) error { var xattrs xAttr var o owner mode := os.FileMode(0644) for _, opt := range opts { switch v := opt.(type) { case xAttr: xattrs = v case owner: o = v case os.FileMode: mode = v default: return errors.New("unsupported opt") } } if strings.HasSuffix(name, "/") { return fmt.Errorf("bogus trailing slash in file %q", name) } tm, err := fileModeToTarMode(mode) if err != nil { return err } if len(xattrs) > 0 { format = tar.FormatPAX // only PAX supports xattrs } if err := tw.WriteHeader(&tar.Header{ Typeflag: tar.TypeReg, Name: prefix + name, Mode: tm, Xattrs: xattrs, Size: int64(len(contents)), Uid: o.uid, Gid: o.gid, Format: format, }); err != nil { return err } _, err = io.WriteString(tw, contents) return err }) } func symlink(name, target string) tarEntry { return tarEntryFunc(func(tw *tar.Writer, prefix string, format tar.Format) error { return tw.WriteHeader(&tar.Header{ Typeflag: tar.TypeSymlink, Name: prefix + name, Linkname: target, Mode: 0644, Format: format, }) }) } func link(name string, linkname string) tarEntry { now := time.Now() return tarEntryFunc(func(w *tar.Writer, prefix string, format tar.Format) error { return w.WriteHeader(&tar.Header{ Typeflag: tar.TypeLink, Name: prefix + name, Linkname: linkname, ModTime: now, Format: format, }) }) } func chardev(name string, major, minor int64) tarEntry { now := time.Now() return tarEntryFunc(func(w *tar.Writer, prefix string, format tar.Format) error { return w.WriteHeader(&tar.Header{ Typeflag: tar.TypeChar, Name: prefix + name, Devmajor: major, Devminor: minor, ModTime: now, Format: format, }) }) } func blockdev(name string, major, minor int64) tarEntry { now := time.Now() return tarEntryFunc(func(w *tar.Writer, prefix string, format tar.Format) error { return w.WriteHeader(&tar.Header{ Typeflag: tar.TypeBlock, Name: prefix + name, Devmajor: major, Devminor: minor, ModTime: now, Format: format, }) }) } func fifo(name string) tarEntry { now := time.Now() return tarEntryFunc(func(w *tar.Writer, prefix string, format tar.Format) error { return w.WriteHeader(&tar.Header{ Typeflag: tar.TypeFifo, Name: prefix + name, ModTime: now, Format: format, }) }) } func prefetchLandmark() tarEntry { return tarEntryFunc(func(w *tar.Writer, prefix string, format tar.Format) error { if err := w.WriteHeader(&tar.Header{ Name: PrefetchLandmark, Typeflag: tar.TypeReg, Size: int64(len([]byte{landmarkContents})), Format: format, }); err != nil { return err } contents := []byte{landmarkContents} if _, err := io.CopyN(w, bytes.NewReader(contents), int64(len(contents))); err != nil { return err } return nil }) } func noPrefetchLandmark() tarEntry { return tarEntryFunc(func(w *tar.Writer, prefix string, format tar.Format) error { if err := w.WriteHeader(&tar.Header{ Name: NoPrefetchLandmark, Typeflag: tar.TypeReg, Size: int64(len([]byte{landmarkContents})), Format: format, }); err != nil { return err } contents := []byte{landmarkContents} if _, err := io.CopyN(w, bytes.NewReader(contents), int64(len(contents))); err != nil { return err } return nil }) } func regDigest(t TestingT, name string, contentStr string, digestMap map[string]digest.Digest) tarEntry { if digestMap == nil { t.Fatalf("digest map mustn't be nil") } content := []byte(contentStr) var n int64 for n < int64(len(content)) { size := int64(chunkSize) remain := int64(len(content)) - n if remain < size { size = remain } dgstr := digest.Canonical.Digester() if _, err := io.CopyN(dgstr.Hash(), bytes.NewReader(content[n:n+size]), size); err != nil { t.Fatalf("failed to calculate digest of %q (name=%q,offset=%d,size=%d)", string(content[n:n+size]), name, n, size) } digestMap[chunkID(name, n, size)] = dgstr.Digest() n += size } return tarEntryFunc(func(w *tar.Writer, prefix string, format tar.Format) error { if err := w.WriteHeader(&tar.Header{ Typeflag: tar.TypeReg, Name: prefix + name, Size: int64(len(content)), Format: format, }); err != nil { return err } if _, err := io.CopyN(w, bytes.NewReader(content), int64(len(content))); err != nil { return err } return nil }) } var runes = []rune("1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") func randomContents(n int) string { b := make([]rune, n) for i := range b { bi, err := rand.Int(rand.Reader, big.NewInt(int64(len(runes)))) if err != nil { panic(err) } b[i] = runes[int(bi.Int64())] } return string(b) } func fileModeToTarMode(mode os.FileMode) (int64, error) { h, err := tar.FileInfoHeader(fileInfoOnlyMode(mode), "") if err != nil { return 0, err } return h.Mode, nil } // fileInfoOnlyMode is os.FileMode that populates only file mode. type fileInfoOnlyMode os.FileMode func (f fileInfoOnlyMode) Name() string { return "" } func (f fileInfoOnlyMode) Size() int64 { return 0 } func (f fileInfoOnlyMode) Mode() os.FileMode { return os.FileMode(f) } func (f fileInfoOnlyMode) ModTime() time.Time { return time.Now() } func (f fileInfoOnlyMode) IsDir() bool { return os.FileMode(f).IsDir() } func (f fileInfoOnlyMode) Sys() interface{} { return nil } func CheckGzipHasStreams(t TestingT, b []byte, streams []int64) { if len(streams) == 0 { return // nop } wants := map[int64]struct{}{} for _, s := range streams { wants[s] = struct{}{} } len0 := len(b) br := bytes.NewReader(b) zr := new(gzip.Reader) t.Logf("got gzip streams:") numStreams := 0 for { zoff := len0 - br.Len() if err := zr.Reset(br); err != nil { if err == io.EOF { return } t.Fatalf("countStreams(gzip), Reset: %v", err) } zr.Multistream(false) n, err := io.Copy(io.Discard, zr) if err != nil { t.Fatalf("countStreams(gzip), Copy: %v", err) } var extra string if len(zr.Extra) > 0 { extra = fmt.Sprintf("; extra=%q", zr.Extra) } t.Logf(" [%d] at %d in stargz, uncompressed length %d%s", numStreams, zoff, n, extra) delete(wants, int64(zoff)) numStreams++ } } func GzipDiffIDOf(t TestingT, b []byte) string { h := sha256.New() zr, err := gzip.NewReader(bytes.NewReader(b)) if err != nil { t.Fatalf("diffIDOf(gzip): %v", err) } defer zr.Close() if _, err := io.Copy(h, zr); err != nil { t.Fatalf("diffIDOf(gzip).Copy: %v", err) } return fmt.Sprintf("sha256:%x", h.Sum(nil)) } ================================================ FILE: vendor/github.com/containerd/stargz-snapshotter/estargz/types.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ /* Copyright 2019 The Go Authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. */ package estargz import ( "archive/tar" "hash" "io" "os" "path" "time" digest "github.com/opencontainers/go-digest" ) const ( // TOCTarName is the name of the JSON file in the tar archive in the // table of contents gzip stream. TOCTarName = "stargz.index.json" // FooterSize is the number of bytes in the footer // // The footer is an empty gzip stream with no compression and an Extra // header of the form "%016xSTARGZ", where the 64 bit hex-encoded // number is the offset to the gzip stream of JSON TOC. // // 51 comes from: // // 10 bytes gzip header // 2 bytes XLEN (length of Extra field) = 26 (4 bytes header + 16 hex digits + len("STARGZ")) // 2 bytes Extra: SI1 = 'S', SI2 = 'G' // 2 bytes Extra: LEN = 22 (16 hex digits + len("STARGZ")) // 22 bytes Extra: subfield = fmt.Sprintf("%016xSTARGZ", offsetOfTOC) // 5 bytes flate header // 8 bytes gzip footer // (End of the eStargz blob) // // NOTE: For Extra fields, subfield IDs SI1='S' SI2='G' is used for eStargz. FooterSize = 51 // legacyFooterSize is the number of bytes in the legacy stargz footer. // // 47 comes from: // // 10 byte gzip header + // 2 byte (LE16) length of extra, encoding 22 (16 hex digits + len("STARGZ")) == "\x16\x00" + // 22 bytes of extra (fmt.Sprintf("%016xSTARGZ", tocGzipOffset)) // 5 byte flate header // 8 byte gzip footer (two little endian uint32s: digest, size) legacyFooterSize = 47 // TOCJSONDigestAnnotation is an annotation for an image layer. This stores the // digest of the TOC JSON. // This annotation is valid only when it is specified in `.[]layers.annotations` // of an image manifest. TOCJSONDigestAnnotation = "containerd.io/snapshot/stargz/toc.digest" // StoreUncompressedSizeAnnotation is an additional annotation key for eStargz to enable lazy // pulling on containers/storage. Stargz Store is required to expose the layer's uncompressed size // to the runtime but current OCI image doesn't ship this information by default. So we store this // to the special annotation. StoreUncompressedSizeAnnotation = "io.containers.estargz.uncompressed-size" // PrefetchLandmark is a file entry which indicates the end position of // prefetch in the stargz file. PrefetchLandmark = ".prefetch.landmark" // NoPrefetchLandmark is a file entry which indicates that no prefetch should // occur in the stargz file. NoPrefetchLandmark = ".no.prefetch.landmark" landmarkContents = 0xf ) // JTOC is the JSON-serialized table of contents index of the files in the stargz file. type JTOC struct { Version int `json:"version"` Entries []*TOCEntry `json:"entries"` } // TOCEntry is an entry in the stargz file's TOC (Table of Contents). type TOCEntry struct { // Name is the tar entry's name. It is the complete path // stored in the tar file, not just the base name. Name string `json:"name"` // Type is one of "dir", "reg", "symlink", "hardlink", "char", // "block", "fifo", or "chunk". // The "chunk" type is used for regular file data chunks past the first // TOCEntry; the 2nd chunk and on have only Type ("chunk"), Offset, // ChunkOffset, and ChunkSize populated. Type string `json:"type"` // Size, for regular files, is the logical size of the file. Size int64 `json:"size,omitempty"` // ModTime3339 is the modification time of the tar entry. Empty // means zero or unknown. Otherwise it's in UTC RFC3339 // format. Use the ModTime method to access the time.Time value. ModTime3339 string `json:"modtime,omitempty"` modTime time.Time // LinkName, for symlinks and hardlinks, is the link target. LinkName string `json:"linkName,omitempty"` // Mode is the permission and mode bits. Mode int64 `json:"mode,omitempty"` // UID is the user ID of the owner. UID int `json:"uid,omitempty"` // GID is the group ID of the owner. GID int `json:"gid,omitempty"` // Uname is the username of the owner. // // In the serialized JSON, this field may only be present for // the first entry with the same UID. Uname string `json:"userName,omitempty"` // Gname is the group name of the owner. // // In the serialized JSON, this field may only be present for // the first entry with the same GID. Gname string `json:"groupName,omitempty"` // Offset, for regular files, provides the offset in the // stargz file to the file's data bytes. See ChunkOffset and // ChunkSize. Offset int64 `json:"offset,omitempty"` // InnerOffset is an optional field indicates uncompressed offset // of this "reg" or "chunk" payload in a stream starts from Offset. // This field enables to put multiple "reg" or "chunk" payloads // in one chunk with having the same Offset but different InnerOffset. InnerOffset int64 `json:"innerOffset,omitempty"` nextOffset int64 // the Offset of the next entry with a non-zero Offset // DevMajor is the major device number for "char" and "block" types. DevMajor int `json:"devMajor,omitempty"` // DevMinor is the major device number for "char" and "block" types. DevMinor int `json:"devMinor,omitempty"` // NumLink is the number of entry names pointing to this entry. // Zero means one name references this entry. // This field is calculated during runtime and not recorded in TOC JSON. NumLink int `json:"-"` // Xattrs are the extended attribute for the entry. Xattrs map[string][]byte `json:"xattrs,omitempty"` // Digest stores the OCI checksum for regular files payload. // It has the form "sha256:abcdef01234....". Digest string `json:"digest,omitempty"` // ChunkOffset is non-zero if this is a chunk of a large, // regular file. If so, the Offset is where the gzip header of // ChunkSize bytes at ChunkOffset in Name begin. // // In serialized form, a "chunkSize" JSON field of zero means // that the chunk goes to the end of the file. After reading // from the stargz TOC, though, the ChunkSize is initialized // to a non-zero file for when Type is either "reg" or // "chunk". ChunkOffset int64 `json:"chunkOffset,omitempty"` ChunkSize int64 `json:"chunkSize,omitempty"` // ChunkDigest stores an OCI digest of the chunk. This must be formed // as "sha256:0123abcd...". ChunkDigest string `json:"chunkDigest,omitempty"` children map[string]*TOCEntry // chunkTopIndex is index of the entry where Offset starts in the blob. chunkTopIndex int } // ModTime returns the entry's modification time. func (e *TOCEntry) ModTime() time.Time { return e.modTime } // NextOffset returns the position (relative to the start of the // stargz file) of the next gzip boundary after e.Offset. func (e *TOCEntry) NextOffset() int64 { return e.nextOffset } func (e *TOCEntry) addChild(baseName string, child *TOCEntry) { if e.children == nil { e.children = make(map[string]*TOCEntry) } if child.Type == "dir" { e.NumLink++ // Entry ".." in the subdirectory links to this directory } e.children[baseName] = child } // isDataType reports whether TOCEntry is a regular file or chunk (something that // contains regular file data). func (e *TOCEntry) isDataType() bool { return e.Type == "reg" || e.Type == "chunk" } // Stat returns a FileInfo value representing e. func (e *TOCEntry) Stat() os.FileInfo { return fileInfo{e} } // ForeachChild calls f for each child item. If f returns false, iteration ends. // If e is not a directory, f is not called. func (e *TOCEntry) ForeachChild(f func(baseName string, ent *TOCEntry) bool) { for name, ent := range e.children { if !f(name, ent) { return } } } // LookupChild returns the directory e's child by its base name. func (e *TOCEntry) LookupChild(baseName string) (child *TOCEntry, ok bool) { child, ok = e.children[baseName] return } // fileInfo implements os.FileInfo using the wrapped *TOCEntry. type fileInfo struct{ e *TOCEntry } var _ os.FileInfo = fileInfo{} func (fi fileInfo) Name() string { return path.Base(fi.e.Name) } func (fi fileInfo) IsDir() bool { return fi.e.Type == "dir" } func (fi fileInfo) Size() int64 { return fi.e.Size } func (fi fileInfo) ModTime() time.Time { return fi.e.ModTime() } func (fi fileInfo) Sys() interface{} { return fi.e } func (fi fileInfo) Mode() (m os.FileMode) { // TOCEntry.Mode is tar.Header.Mode so we can understand the these bits using `tar` pkg. m = (&tar.Header{Mode: fi.e.Mode}).FileInfo().Mode() & (os.ModePerm | os.ModeSetuid | os.ModeSetgid | os.ModeSticky) switch fi.e.Type { case "dir": m |= os.ModeDir case "symlink": m |= os.ModeSymlink case "char": m |= os.ModeDevice | os.ModeCharDevice case "block": m |= os.ModeDevice case "fifo": m |= os.ModeNamedPipe } return m } // TOCEntryVerifier holds verifiers that are usable for verifying chunks contained // in a eStargz blob. type TOCEntryVerifier interface { // Verifier provides a content verifier that can be used for verifying the // contents of the specified TOCEntry. Verifier(ce *TOCEntry) (digest.Verifier, error) } // Compression provides the compression helper to be used creating and parsing eStargz. // This package provides gzip-based Compression by default, but any compression // algorithm (e.g. zstd) can be used as long as it implements Compression. type Compression interface { Compressor Decompressor } // Compressor represents the helper mothods to be used for creating eStargz. type Compressor interface { // Writer returns WriteCloser to be used for writing a chunk to eStargz. // Everytime a chunk is written, the WriteCloser is closed and Writer is // called again for writing the next chunk. // // The returned writer should implement "Flush() error" function that flushes // any pending compressed data to the underlying writer. Writer(w io.Writer) (WriteFlushCloser, error) // WriteTOCAndFooter is called to write JTOC to the passed Writer. // diffHash calculates the DiffID (uncompressed sha256 hash) of the blob // WriteTOCAndFooter can optionally write anything that affects DiffID calculation // (e.g. uncompressed TOC JSON). // // This function returns tocDgst that represents the digest of TOC that will be used // to verify this blob when it's parsed. WriteTOCAndFooter(w io.Writer, off int64, toc *JTOC, diffHash hash.Hash) (tocDgst digest.Digest, err error) } // Decompressor represents the helper mothods to be used for parsing eStargz. type Decompressor interface { // Reader returns ReadCloser to be used for decompressing file payload. Reader(r io.Reader) (io.ReadCloser, error) // FooterSize returns the size of the footer of this blob. FooterSize() int64 // ParseFooter parses the footer and returns the offset and (compressed) size of TOC. // payloadBlobSize is the (compressed) size of the blob payload (i.e. the size between // the top until the TOC JSON). // // If tocOffset < 0, we assume that TOC isn't contained in the blob and pass nil reader // to ParseTOC. We expect that ParseTOC acquire TOC from the external location and return it. // // tocSize is optional. If tocSize <= 0, it's by default the size of the range from tocOffset until the beginning of the // footer (blob size - tocOff - FooterSize). // If blobPayloadSize < 0, blobPayloadSize become the blob size. ParseFooter(p []byte) (blobPayloadSize, tocOffset, tocSize int64, err error) // ParseTOC parses TOC from the passed reader. The reader provides the partial contents // of the underlying blob that has the range specified by ParseFooter method. // // This function returns tocDgst that represents the digest of TOC that will be used // to verify this blob. This must match to the value returned from // Compressor.WriteTOCAndFooter that is used when creating this blob. // // If tocOffset returned by ParseFooter is < 0, we assume that TOC isn't contained in the blob. // Pass nil reader to ParseTOC then we expect that ParseTOC acquire TOC from the external location // and return it. ParseTOC(r io.Reader) (toc *JTOC, tocDgst digest.Digest, err error) } type WriteFlushCloser interface { io.WriteCloser Flush() error } ================================================ FILE: vendor/github.com/containerd/typeurl/v2/.gitignore ================================================ *.test coverage.txt ================================================ FILE: vendor/github.com/containerd/typeurl/v2/LICENSE ================================================ Apache License Version 2.0, January 2004 https://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 Copyright The containerd Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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: vendor/github.com/containerd/typeurl/v2/README.md ================================================ # typeurl [![PkgGoDev](https://pkg.go.dev/badge/github.com/containerd/typeurl)](https://pkg.go.dev/github.com/containerd/typeurl) [![Build Status](https://github.com/containerd/typeurl/workflows/CI/badge.svg)](https://github.com/containerd/typeurl/actions?query=workflow%3ACI) [![codecov](https://codecov.io/gh/containerd/typeurl/branch/main/graph/badge.svg)](https://codecov.io/gh/containerd/typeurl) [![Go Report Card](https://goreportcard.com/badge/github.com/containerd/typeurl)](https://goreportcard.com/report/github.com/containerd/typeurl) A Go package for managing the registration, marshaling, and unmarshaling of encoded types. This package helps when types are sent over a ttrpc/GRPC API and marshaled as a protobuf [Any](https://pkg.go.dev/google.golang.org/protobuf@v1.27.1/types/known/anypb#Any) ## Project details **typeurl** is a containerd sub-project, licensed under the [Apache 2.0 license](./LICENSE). As a containerd sub-project, you will find the: * [Project governance](https://github.com/containerd/project/blob/main/GOVERNANCE.md), * [Maintainers](https://github.com/containerd/project/blob/main/MAINTAINERS), * and [Contributing guidelines](https://github.com/containerd/project/blob/main/CONTRIBUTING.md) information in our [`containerd/project`](https://github.com/containerd/project) repository. ## Optional By default, support for gogoproto is available along side the standard Google protobuf types. You can choose to leave gogo support out by using the `!no_gogo` build tag. ================================================ FILE: vendor/github.com/containerd/typeurl/v2/doc.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package typeurl // Package typeurl assists with managing the registration, marshaling, and // unmarshaling of types encoded as protobuf.Any. // // A protobuf.Any is a proto message that can contain any arbitrary data. It // consists of two components, a TypeUrl and a Value, and its proto definition // looks like this: // // message Any { // string type_url = 1; // bytes value = 2; // } // // The TypeUrl is used to distinguish the contents from other proto.Any // messages. This typeurl library manages these URLs to enable automagic // marshaling and unmarshaling of the contents. // // For example, consider this go struct: // // type Foo struct { // Field1 string // Field2 string // } // // To use typeurl, types must first be registered. This is typically done in // the init function // // func init() { // typeurl.Register(&Foo{}, "Foo") // } // // This will register the type Foo with the url path "Foo". The arguments to // Register are variadic, and are used to construct a url path. Consider this // example, from the github.com/containerd/containerd/client package: // // func init() { // const prefix = "types.containerd.io" // // register TypeUrls for commonly marshaled external types // major := strconv.Itoa(specs.VersionMajor) // typeurl.Register(&specs.Spec{}, prefix, "opencontainers/runtime-spec", major, "Spec") // // this function has more Register calls, which are elided. // } // // This registers several types under a more complex url, which ends up mapping // to `types.containerd.io/opencontainers/runtime-spec/1/Spec` (or some other // value for major). // // Once a type is registered, it can be marshaled to a proto.Any message simply // by calling `MarshalAny`, like this: // // foo := &Foo{Field1: "value1", Field2: "value2"} // anyFoo, err := typeurl.MarshalAny(foo) // // MarshalAny will resolve the correct URL for the type. If the type in // question implements the proto.Message interface, then it will be marshaled // as a proto message. Otherwise, it will be marshaled as json. This means that // typeurl will work on any arbitrary data, whether or not it has a proto // definition, as long as it can be serialized to json. // // To unmarshal, the process is simply inverse: // // iface, err := typeurl.UnmarshalAny(anyFoo) // foo := iface.(*Foo) // // The correct type is automatically chosen from the type registry, and the // returned interface can be cast straight to that type. ================================================ FILE: vendor/github.com/containerd/typeurl/v2/types.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package typeurl import ( "encoding/json" "errors" "fmt" "path" "reflect" "sync" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoregistry" "google.golang.org/protobuf/types/known/anypb" ) var ( mu sync.RWMutex registry = make(map[reflect.Type]string) handlers []handler ) type handler interface { Marshaller(interface{}) func() ([]byte, error) Unmarshaller(interface{}) func([]byte) error TypeURL(interface{}) string GetType(url string) (reflect.Type, bool) } // Definitions of common error types used throughout typeurl. // // These error types are used with errors.Wrap and errors.Wrapf to add context // to an error. // // To detect an error class, use errors.Is() functions to tell whether an // error is of this type. var ( ErrNotFound = errors.New("not found") ) // Any contains an arbitrary protcol buffer message along with its type. // // While there is google.golang.org/protobuf/types/known/anypb.Any, // we'd like to have our own to hide the underlying protocol buffer // implementations from containerd clients. // // https://developers.google.com/protocol-buffers/docs/proto3#any type Any interface { // GetTypeUrl returns a URL/resource name that uniquely identifies // the type of the serialized protocol buffer message. GetTypeUrl() string // GetValue returns a valid serialized protocol buffer of the type that // GetTypeUrl() indicates. GetValue() []byte } type anyType struct { typeURL string value []byte } func (a *anyType) GetTypeUrl() string { if a == nil { return "" } return a.typeURL } func (a *anyType) GetValue() []byte { if a == nil { return nil } return a.value } // Register a type with a base URL for JSON marshaling. When the MarshalAny and // UnmarshalAny functions are called they will treat the Any type value as JSON. // To use protocol buffers for handling the Any value the proto.Register // function should be used instead of this function. func Register(v interface{}, args ...string) { var ( t = tryDereference(v) p = path.Join(args...) ) mu.Lock() defer mu.Unlock() if et, ok := registry[t]; ok { if et != p { panic(fmt.Errorf("type registered with alternate path %q != %q", et, p)) } return } registry[t] = p } // TypeURL returns the type url for a registered type. func TypeURL(v interface{}) (string, error) { mu.RLock() u, ok := registry[tryDereference(v)] mu.RUnlock() if !ok { switch t := v.(type) { case proto.Message: return string(t.ProtoReflect().Descriptor().FullName()), nil default: for _, h := range handlers { if u := h.TypeURL(v); u != "" { return u, nil } } return "", fmt.Errorf("type %s: %w", reflect.TypeOf(v), ErrNotFound) } } return u, nil } // Is returns true if the type of the Any is the same as v. func Is(any Any, v interface{}) bool { if any == nil { return false } // call to check that v is a pointer tryDereference(v) url, err := TypeURL(v) if err != nil { return false } return any.GetTypeUrl() == url } // MarshalAny marshals the value v into an any with the correct TypeUrl. // If the provided object is already a proto.Any message, then it will be // returned verbatim. If it is of type proto.Message, it will be marshaled as a // protocol buffer. Otherwise, the object will be marshaled to json. func MarshalAny(v interface{}) (Any, error) { var marshal func(v interface{}) ([]byte, error) switch t := v.(type) { case Any: // avoid reserializing the type if we have an any. return t, nil case proto.Message: marshal = func(v interface{}) ([]byte, error) { return proto.Marshal(t) } default: for _, h := range handlers { if m := h.Marshaller(v); m != nil { marshal = func(v interface{}) ([]byte, error) { return m() } break } } if marshal == nil { marshal = json.Marshal } } url, err := TypeURL(v) if err != nil { return nil, err } data, err := marshal(v) if err != nil { return nil, err } return &anyType{ typeURL: url, value: data, }, nil } // UnmarshalAny unmarshals the any type into a concrete type. func UnmarshalAny(any Any) (interface{}, error) { return UnmarshalByTypeURL(any.GetTypeUrl(), any.GetValue()) } // UnmarshalByTypeURL unmarshals the given type and value to into a concrete type. func UnmarshalByTypeURL(typeURL string, value []byte) (interface{}, error) { return unmarshal(typeURL, value, nil) } // UnmarshalTo unmarshals the any type into a concrete type passed in the out // argument. It is identical to UnmarshalAny, but lets clients provide a // destination type through the out argument. func UnmarshalTo(any Any, out interface{}) error { return UnmarshalToByTypeURL(any.GetTypeUrl(), any.GetValue(), out) } // UnmarshalToByTypeURL unmarshals the given type and value into a concrete type passed // in the out argument. It is identical to UnmarshalByTypeURL, but lets clients // provide a destination type through the out argument. func UnmarshalToByTypeURL(typeURL string, value []byte, out interface{}) error { _, err := unmarshal(typeURL, value, out) return err } // MarshalProto converts typeurl.Any to google.golang.org/protobuf/types/known/anypb.Any. func MarshalProto(from Any) *anypb.Any { if from == nil { return nil } if pbany, ok := from.(*anypb.Any); ok { return pbany } return &anypb.Any{ TypeUrl: from.GetTypeUrl(), Value: from.GetValue(), } } // MarshalAnyToProto converts an arbitrary interface to google.golang.org/protobuf/types/known/anypb.Any. func MarshalAnyToProto(from interface{}) (*anypb.Any, error) { anyType, err := MarshalAny(from) if err != nil { return nil, err } return MarshalProto(anyType), nil } func unmarshal(typeURL string, value []byte, v interface{}) (interface{}, error) { t, isProto, err := getTypeByUrl(typeURL) if err != nil { return nil, err } if v == nil { v = reflect.New(t).Interface() } else { // Validate interface type provided by client vURL, err := TypeURL(v) if err != nil { return nil, err } if typeURL != vURL { return nil, fmt.Errorf("can't unmarshal type %q to output %q", typeURL, vURL) } } if isProto { pm, ok := v.(proto.Message) if ok { return v, proto.Unmarshal(value, pm) } for _, h := range handlers { if unmarshal := h.Unmarshaller(v); unmarshal != nil { return v, unmarshal(value) } } } // fallback to json unmarshaller return v, json.Unmarshal(value, v) } func getTypeByUrl(url string) (_ reflect.Type, isProto bool, _ error) { mu.RLock() for t, u := range registry { if u == url { mu.RUnlock() return t, false, nil } } mu.RUnlock() mt, err := protoregistry.GlobalTypes.FindMessageByURL(url) if err != nil { if errors.Is(err, protoregistry.NotFound) { for _, h := range handlers { if t, isProto := h.GetType(url); t != nil { return t, isProto, nil } } } return nil, false, fmt.Errorf("type with url %s: %w", url, ErrNotFound) } empty := mt.New().Interface() return reflect.TypeOf(empty).Elem(), true, nil } func tryDereference(v interface{}) reflect.Type { t := reflect.TypeOf(v) if t.Kind() == reflect.Ptr { // require check of pointer but dereference to register return t.Elem() } panic("v is not a pointer to a type") } ================================================ FILE: vendor/github.com/containerd/typeurl/v2/types_gogo.go ================================================ //go:build !no_gogo /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package typeurl import ( "reflect" gogoproto "github.com/gogo/protobuf/proto" ) func init() { handlers = append(handlers, gogoHandler{}) } type gogoHandler struct{} func (gogoHandler) Marshaller(v interface{}) func() ([]byte, error) { pm, ok := v.(gogoproto.Message) if !ok { return nil } return func() ([]byte, error) { return gogoproto.Marshal(pm) } } func (gogoHandler) Unmarshaller(v interface{}) func([]byte) error { pm, ok := v.(gogoproto.Message) if !ok { return nil } return func(dt []byte) error { return gogoproto.Unmarshal(dt, pm) } } func (gogoHandler) TypeURL(v interface{}) string { pm, ok := v.(gogoproto.Message) if !ok { return "" } return gogoproto.MessageName(pm) } func (gogoHandler) GetType(url string) (reflect.Type, bool) { t := gogoproto.MessageType(url) if t == nil { return nil, false } return t.Elem(), true } ================================================ FILE: vendor/github.com/containers/libtrust/CODE-OF-CONDUCT.md ================================================ ## The libtrust Project Community Code of Conduct The libtrust project follows the [Containers Community Code of Conduct](https://github.com/containers/common/blob/main/CODE-OF-CONDUCT.md). ================================================ FILE: vendor/github.com/containers/libtrust/CONTRIBUTING.md ================================================ # Contributing to libtrust Want to hack on libtrust? Awesome! Here are instructions to get you started. libtrust is a part of the [Docker](https://www.docker.com) project, and follows the same rules and principles. If you're already familiar with the way Docker does things, you'll feel right at home. Otherwise, go read [Docker's contributions guidelines](https://github.com/docker/docker/blob/master/CONTRIBUTING.md). Happy hacking! ================================================ FILE: vendor/github.com/containers/libtrust/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 Copyright 2014 Docker, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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: vendor/github.com/containers/libtrust/MAINTAINERS ================================================ Solomon Hykes Josh Hawn (github: jlhawn) Derek McGowan (github: dmcgowan) ================================================ FILE: vendor/github.com/containers/libtrust/README.md ================================================ # libtrust > **WARNING** this library is no longer actively developed, and will be integrated > in the [docker/distribution][https://www.github.com/docker/distribution] > repository in future. Libtrust is library for managing authentication and authorization using public key cryptography. Authentication is handled using the identity attached to the public key. Libtrust provides multiple methods to prove possession of the private key associated with an identity. - TLS x509 certificates - Signature verification - Key Challenge Authorization and access control is managed through a distributed trust graph. Trust servers are used as the authorities of the trust graph and allow caching portions of the graph for faster access. ## Copyright and license Code and documentation copyright 2014 Docker, inc. Code released under the Apache 2.0 license. Docs released under Creative commons. ================================================ FILE: vendor/github.com/containers/libtrust/SECURITY.md ================================================ ## Security and Disclosure Information Policy for the libtrust Project The libtrust Project follows the [Security and Disclosure Information Policy](https://github.com/containers/common/blob/main/SECURITY.md) for the Containers Projects. ================================================ FILE: vendor/github.com/containers/libtrust/certificates.go ================================================ package libtrust import ( "crypto/rand" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "fmt" "io/ioutil" "math/big" "net" "time" ) type certTemplateInfo struct { commonName string domains []string ipAddresses []net.IP isCA bool clientAuth bool serverAuth bool } func generateCertTemplate(info *certTemplateInfo) *x509.Certificate { // Generate a certificate template which is valid from the past week to // 10 years from now. The usage of the certificate depends on the // specified fields in the given certTempInfo object. var ( keyUsage x509.KeyUsage extKeyUsage []x509.ExtKeyUsage ) if info.isCA { keyUsage = x509.KeyUsageCertSign } if info.clientAuth { extKeyUsage = append(extKeyUsage, x509.ExtKeyUsageClientAuth) } if info.serverAuth { extKeyUsage = append(extKeyUsage, x509.ExtKeyUsageServerAuth) } return &x509.Certificate{ SerialNumber: big.NewInt(0), Subject: pkix.Name{ CommonName: info.commonName, }, NotBefore: time.Now().Add(-time.Hour * 24 * 7), NotAfter: time.Now().Add(time.Hour * 24 * 365 * 10), DNSNames: info.domains, IPAddresses: info.ipAddresses, IsCA: info.isCA, KeyUsage: keyUsage, ExtKeyUsage: extKeyUsage, BasicConstraintsValid: info.isCA, } } func generateCert(pub PublicKey, priv PrivateKey, subInfo, issInfo *certTemplateInfo) (cert *x509.Certificate, err error) { pubCertTemplate := generateCertTemplate(subInfo) privCertTemplate := generateCertTemplate(issInfo) certDER, err := x509.CreateCertificate( rand.Reader, pubCertTemplate, privCertTemplate, pub.CryptoPublicKey(), priv.CryptoPrivateKey(), ) if err != nil { return nil, fmt.Errorf("failed to create certificate: %s", err) } cert, err = x509.ParseCertificate(certDER) if err != nil { return nil, fmt.Errorf("failed to parse certificate: %s", err) } return } // GenerateSelfSignedServerCert creates a self-signed certificate for the // given key which is to be used for TLS servers with the given domains and // IP addresses. func GenerateSelfSignedServerCert(key PrivateKey, domains []string, ipAddresses []net.IP) (*x509.Certificate, error) { info := &certTemplateInfo{ commonName: key.KeyID(), domains: domains, ipAddresses: ipAddresses, serverAuth: true, } return generateCert(key.PublicKey(), key, info, info) } // GenerateSelfSignedClientCert creates a self-signed certificate for the // given key which is to be used for TLS clients. func GenerateSelfSignedClientCert(key PrivateKey) (*x509.Certificate, error) { info := &certTemplateInfo{ commonName: key.KeyID(), clientAuth: true, } return generateCert(key.PublicKey(), key, info, info) } // GenerateCACert creates a certificate which can be used as a trusted // certificate authority. func GenerateCACert(signer PrivateKey, trustedKey PublicKey) (*x509.Certificate, error) { subjectInfo := &certTemplateInfo{ commonName: trustedKey.KeyID(), isCA: true, } issuerInfo := &certTemplateInfo{ commonName: signer.KeyID(), } return generateCert(trustedKey, signer, subjectInfo, issuerInfo) } // GenerateCACertPool creates a certificate authority pool to be used for a // TLS configuration. Any self-signed certificates issued by the specified // trusted keys will be verified during a TLS handshake func GenerateCACertPool(signer PrivateKey, trustedKeys []PublicKey) (*x509.CertPool, error) { certPool := x509.NewCertPool() for _, trustedKey := range trustedKeys { cert, err := GenerateCACert(signer, trustedKey) if err != nil { return nil, fmt.Errorf("failed to generate CA certificate: %s", err) } certPool.AddCert(cert) } return certPool, nil } // LoadCertificateBundle loads certificates from the given file. The file should be pem encoded // containing one or more certificates. The expected pem type is "CERTIFICATE". func LoadCertificateBundle(filename string) ([]*x509.Certificate, error) { b, err := ioutil.ReadFile(filename) if err != nil { return nil, err } certificates := []*x509.Certificate{} var block *pem.Block block, b = pem.Decode(b) for ; block != nil; block, b = pem.Decode(b) { if block.Type == "CERTIFICATE" { cert, err := x509.ParseCertificate(block.Bytes) if err != nil { return nil, err } certificates = append(certificates, cert) } else { return nil, fmt.Errorf("invalid pem block type: %s", block.Type) } } return certificates, nil } // LoadCertificatePool loads a CA pool from the given file. The file should be pem encoded // containing one or more certificates. The expected pem type is "CERTIFICATE". func LoadCertificatePool(filename string) (*x509.CertPool, error) { certs, err := LoadCertificateBundle(filename) if err != nil { return nil, err } pool := x509.NewCertPool() for _, cert := range certs { pool.AddCert(cert) } return pool, nil } ================================================ FILE: vendor/github.com/containers/libtrust/doc.go ================================================ /* Package libtrust provides an interface for managing authentication and authorization using public key cryptography. Authentication is handled using the identity attached to the public key and verified through TLS x509 certificates, a key challenge, or signature. Authorization and access control is managed through a trust graph distributed between both remote trust servers and locally cached and managed data. */ package libtrust ================================================ FILE: vendor/github.com/containers/libtrust/ec_key.go ================================================ package libtrust import ( "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "encoding/json" "encoding/pem" "errors" "fmt" "io" "math/big" ) /* * EC DSA PUBLIC KEY */ // ecPublicKey implements a libtrust.PublicKey using elliptic curve digital // signature algorithms. type ecPublicKey struct { *ecdsa.PublicKey curveName string signatureAlgorithm *signatureAlgorithm extended map[string]interface{} } func fromECPublicKey(cryptoPublicKey *ecdsa.PublicKey) (*ecPublicKey, error) { curve := cryptoPublicKey.Curve switch { case curve == elliptic.P256(): return &ecPublicKey{cryptoPublicKey, "P-256", es256, map[string]interface{}{}}, nil case curve == elliptic.P384(): return &ecPublicKey{cryptoPublicKey, "P-384", es384, map[string]interface{}{}}, nil case curve == elliptic.P521(): return &ecPublicKey{cryptoPublicKey, "P-521", es512, map[string]interface{}{}}, nil default: return nil, errors.New("unsupported elliptic curve") } } // KeyType returns the key type for elliptic curve keys, i.e., "EC". func (k *ecPublicKey) KeyType() string { return "EC" } // CurveName returns the elliptic curve identifier. // Possible values are "P-256", "P-384", and "P-521". func (k *ecPublicKey) CurveName() string { return k.curveName } // KeyID returns a distinct identifier which is unique to this Public Key. func (k *ecPublicKey) KeyID() string { return keyIDFromCryptoKey(k) } func (k *ecPublicKey) String() string { return fmt.Sprintf("EC Public Key <%s>", k.KeyID()) } // Verify verifyies the signature of the data in the io.Reader using this // PublicKey. The alg parameter should identify the digital signature // algorithm which was used to produce the signature and should be supported // by this public key. Returns a nil error if the signature is valid. func (k *ecPublicKey) Verify(data io.Reader, alg string, signature []byte) error { // For EC keys there is only one supported signature algorithm depending // on the curve parameters. if k.signatureAlgorithm.HeaderParam() != alg { return fmt.Errorf("unable to verify signature: EC Public Key with curve %q does not support signature algorithm %q", k.curveName, alg) } // signature is the concatenation of (r, s), base64Url encoded. sigLength := len(signature) expectedOctetLength := 2 * ((k.Params().BitSize + 7) >> 3) if sigLength != expectedOctetLength { return fmt.Errorf("signature length is %d octets long, should be %d", sigLength, expectedOctetLength) } rBytes, sBytes := signature[:sigLength/2], signature[sigLength/2:] r := new(big.Int).SetBytes(rBytes) s := new(big.Int).SetBytes(sBytes) hasher := k.signatureAlgorithm.HashID().New() _, err := io.Copy(hasher, data) if err != nil { return fmt.Errorf("error reading data to sign: %s", err) } hash := hasher.Sum(nil) if !ecdsa.Verify(k.PublicKey, hash, r, s) { return errors.New("invalid signature") } return nil } // CryptoPublicKey returns the internal object which can be used as a // crypto.PublicKey for use with other standard library operations. The type // is either *rsa.PublicKey or *ecdsa.PublicKey func (k *ecPublicKey) CryptoPublicKey() crypto.PublicKey { return k.PublicKey } func (k *ecPublicKey) toMap() map[string]interface{} { jwk := make(map[string]interface{}) for k, v := range k.extended { jwk[k] = v } jwk["kty"] = k.KeyType() jwk["kid"] = k.KeyID() jwk["crv"] = k.CurveName() xBytes := k.X.Bytes() yBytes := k.Y.Bytes() octetLength := (k.Params().BitSize + 7) >> 3 // MUST include leading zeros in the output so that x, y are each // *octetLength* bytes long. xBuf := make([]byte, octetLength-len(xBytes), octetLength) yBuf := make([]byte, octetLength-len(yBytes), octetLength) xBuf = append(xBuf, xBytes...) yBuf = append(yBuf, yBytes...) jwk["x"] = joseBase64UrlEncode(xBuf) jwk["y"] = joseBase64UrlEncode(yBuf) return jwk } // MarshalJSON serializes this Public Key using the JWK JSON serialization format for // elliptic curve keys. func (k *ecPublicKey) MarshalJSON() (data []byte, err error) { return json.Marshal(k.toMap()) } // PEMBlock serializes this Public Key to DER-encoded PKIX format. func (k *ecPublicKey) PEMBlock() (*pem.Block, error) { derBytes, err := x509.MarshalPKIXPublicKey(k.PublicKey) if err != nil { return nil, fmt.Errorf("unable to serialize EC PublicKey to DER-encoded PKIX format: %s", err) } k.extended["kid"] = k.KeyID() // For display purposes. return createPemBlock("PUBLIC KEY", derBytes, k.extended) } func (k *ecPublicKey) AddExtendedField(field string, value interface{}) { k.extended[field] = value } func (k *ecPublicKey) GetExtendedField(field string) interface{} { v, ok := k.extended[field] if !ok { return nil } return v } func ecPublicKeyFromMap(jwk map[string]interface{}) (*ecPublicKey, error) { // JWK key type (kty) has already been determined to be "EC". // Need to extract 'crv', 'x', 'y', and 'kid' and check for // consistency. // Get the curve identifier value. crv, err := stringFromMap(jwk, "crv") if err != nil { return nil, fmt.Errorf("JWK EC Public Key curve identifier: %s", err) } var ( curve elliptic.Curve sigAlg *signatureAlgorithm ) switch { case crv == "P-256": curve = elliptic.P256() sigAlg = es256 case crv == "P-384": curve = elliptic.P384() sigAlg = es384 case crv == "P-521": curve = elliptic.P521() sigAlg = es512 default: return nil, fmt.Errorf("JWK EC Public Key curve identifier not supported: %q\n", crv) } // Get the X and Y coordinates for the public key point. xB64Url, err := stringFromMap(jwk, "x") if err != nil { return nil, fmt.Errorf("JWK EC Public Key x-coordinate: %s", err) } x, err := parseECCoordinate(xB64Url, curve) if err != nil { return nil, fmt.Errorf("JWK EC Public Key x-coordinate: %s", err) } yB64Url, err := stringFromMap(jwk, "y") if err != nil { return nil, fmt.Errorf("JWK EC Public Key y-coordinate: %s", err) } y, err := parseECCoordinate(yB64Url, curve) if err != nil { return nil, fmt.Errorf("JWK EC Public Key y-coordinate: %s", err) } key := &ecPublicKey{ PublicKey: &ecdsa.PublicKey{Curve: curve, X: x, Y: y}, curveName: crv, signatureAlgorithm: sigAlg, } // Key ID is optional too, but if it exists, it should match the key. _, ok := jwk["kid"] if ok { kid, err := stringFromMap(jwk, "kid") if err != nil { return nil, fmt.Errorf("JWK EC Public Key ID: %s", err) } if kid != key.KeyID() { return nil, fmt.Errorf("JWK EC Public Key ID does not match: %s", kid) } } key.extended = jwk return key, nil } /* * EC DSA PRIVATE KEY */ // ecPrivateKey implements a JWK Private Key using elliptic curve digital signature // algorithms. type ecPrivateKey struct { ecPublicKey *ecdsa.PrivateKey } func fromECPrivateKey(cryptoPrivateKey *ecdsa.PrivateKey) (*ecPrivateKey, error) { publicKey, err := fromECPublicKey(&cryptoPrivateKey.PublicKey) if err != nil { return nil, err } return &ecPrivateKey{*publicKey, cryptoPrivateKey}, nil } // PublicKey returns the Public Key data associated with this Private Key. func (k *ecPrivateKey) PublicKey() PublicKey { return &k.ecPublicKey } func (k *ecPrivateKey) String() string { return fmt.Sprintf("EC Private Key <%s>", k.KeyID()) } // Sign signs the data read from the io.Reader using a signature algorithm supported // by the elliptic curve private key. If the specified hashing algorithm is // supported by this key, that hash function is used to generate the signature // otherwise the the default hashing algorithm for this key is used. Returns // the signature and the name of the JWK signature algorithm used, e.g., // "ES256", "ES384", "ES512". func (k *ecPrivateKey) Sign(data io.Reader, hashID crypto.Hash) (signature []byte, alg string, err error) { // Generate a signature of the data using the internal alg. // The given hashId is only a suggestion, and since EC keys only support // on signature/hash algorithm given the curve name, we disregard it for // the elliptic curve JWK signature implementation. r, s, err := k.sign(data, hashID) if err != nil { return nil, "", fmt.Errorf("error producing signature: %s", err) } rBytes, sBytes := r.Bytes(), s.Bytes() octetLength := (k.ecPublicKey.Params().BitSize + 7) >> 3 // MUST include leading zeros in the output rBuf := make([]byte, octetLength-len(rBytes), octetLength) sBuf := make([]byte, octetLength-len(sBytes), octetLength) rBuf = append(rBuf, rBytes...) sBuf = append(sBuf, sBytes...) signature = append(rBuf, sBuf...) alg = k.signatureAlgorithm.HeaderParam() return } // CryptoPrivateKey returns the internal object which can be used as a // crypto.PublicKey for use with other standard library operations. The type // is either *rsa.PublicKey or *ecdsa.PublicKey func (k *ecPrivateKey) CryptoPrivateKey() crypto.PrivateKey { return k.PrivateKey } func (k *ecPrivateKey) toMap() map[string]interface{} { jwk := k.ecPublicKey.toMap() dBytes := k.D.Bytes() // The length of this octet string MUST be ceiling(log-base-2(n)/8) // octets (where n is the order of the curve). This is because the private // key d must be in the interval [1, n-1] so the bitlength of d should be // no larger than the bitlength of n-1. The easiest way to find the octet // length is to take bitlength(n-1), add 7 to force a carry, and shift this // bit sequence right by 3, which is essentially dividing by 8 and adding // 1 if there is any remainder. Thus, the private key value d should be // output to (bitlength(n-1)+7)>>3 octets. n := k.ecPublicKey.Params().N octetLength := (new(big.Int).Sub(n, big.NewInt(1)).BitLen() + 7) >> 3 // Create a buffer with the necessary zero-padding. dBuf := make([]byte, octetLength-len(dBytes), octetLength) dBuf = append(dBuf, dBytes...) jwk["d"] = joseBase64UrlEncode(dBuf) return jwk } // MarshalJSON serializes this Private Key using the JWK JSON serialization format for // elliptic curve keys. func (k *ecPrivateKey) MarshalJSON() (data []byte, err error) { return json.Marshal(k.toMap()) } // PEMBlock serializes this Private Key to DER-encoded PKIX format. func (k *ecPrivateKey) PEMBlock() (*pem.Block, error) { derBytes, err := x509.MarshalECPrivateKey(k.PrivateKey) if err != nil { return nil, fmt.Errorf("unable to serialize EC PrivateKey to DER-encoded PKIX format: %s", err) } k.extended["keyID"] = k.KeyID() // For display purposes. return createPemBlock("EC PRIVATE KEY", derBytes, k.extended) } func ecPrivateKeyFromMap(jwk map[string]interface{}) (*ecPrivateKey, error) { dB64Url, err := stringFromMap(jwk, "d") if err != nil { return nil, fmt.Errorf("JWK EC Private Key: %s", err) } // JWK key type (kty) has already been determined to be "EC". // Need to extract the public key information, then extract the private // key value 'd'. publicKey, err := ecPublicKeyFromMap(jwk) if err != nil { return nil, err } d, err := parseECPrivateParam(dB64Url, publicKey.Curve) if err != nil { return nil, fmt.Errorf("JWK EC Private Key d-param: %s", err) } key := &ecPrivateKey{ ecPublicKey: *publicKey, PrivateKey: &ecdsa.PrivateKey{ PublicKey: *publicKey.PublicKey, D: d, }, } return key, nil } /* * Key Generation Functions. */ func generateECPrivateKey(curve elliptic.Curve) (k *ecPrivateKey, err error) { k = new(ecPrivateKey) k.PrivateKey, err = ecdsa.GenerateKey(curve, rand.Reader) if err != nil { return nil, err } k.ecPublicKey.PublicKey = &k.PrivateKey.PublicKey k.extended = make(map[string]interface{}) return } // GenerateECP256PrivateKey generates a key pair using elliptic curve P-256. func GenerateECP256PrivateKey() (PrivateKey, error) { k, err := generateECPrivateKey(elliptic.P256()) if err != nil { return nil, fmt.Errorf("error generating EC P-256 key: %s", err) } k.curveName = "P-256" k.signatureAlgorithm = es256 return k, nil } // GenerateECP384PrivateKey generates a key pair using elliptic curve P-384. func GenerateECP384PrivateKey() (PrivateKey, error) { k, err := generateECPrivateKey(elliptic.P384()) if err != nil { return nil, fmt.Errorf("error generating EC P-384 key: %s", err) } k.curveName = "P-384" k.signatureAlgorithm = es384 return k, nil } // GenerateECP521PrivateKey generates aß key pair using elliptic curve P-521. func GenerateECP521PrivateKey() (PrivateKey, error) { k, err := generateECPrivateKey(elliptic.P521()) if err != nil { return nil, fmt.Errorf("error generating EC P-521 key: %s", err) } k.curveName = "P-521" k.signatureAlgorithm = es512 return k, nil } ================================================ FILE: vendor/github.com/containers/libtrust/ec_key_no_openssl.go ================================================ // +build !libtrust_openssl package libtrust import ( "crypto" "crypto/ecdsa" "crypto/rand" "fmt" "io" "math/big" ) func (k *ecPrivateKey) sign(data io.Reader, hashID crypto.Hash) (r, s *big.Int, err error) { hasher := k.signatureAlgorithm.HashID().New() _, err = io.Copy(hasher, data) if err != nil { return nil, nil, fmt.Errorf("error reading data to sign: %s", err) } hash := hasher.Sum(nil) return ecdsa.Sign(rand.Reader, k.PrivateKey, hash) } ================================================ FILE: vendor/github.com/containers/libtrust/ec_key_openssl.go ================================================ // +build libtrust_openssl package libtrust import ( "bytes" "crypto" "crypto/ecdsa" "crypto/rand" "fmt" "io" "math/big" ) func (k *ecPrivateKey) sign(data io.Reader, hashID crypto.Hash) (r, s *big.Int, err error) { hId := k.signatureAlgorithm.HashID() buf := new(bytes.Buffer) _, err = buf.ReadFrom(data) if err != nil { return nil, nil, fmt.Errorf("error reading data: %s", err) } return ecdsa.HashSign(rand.Reader, k.PrivateKey, buf.Bytes(), hId) } ================================================ FILE: vendor/github.com/containers/libtrust/filter.go ================================================ package libtrust import ( "path/filepath" ) // FilterByHosts filters the list of PublicKeys to only those which contain a // 'hosts' pattern which matches the given host. If *includeEmpty* is true, // then keys which do not specify any hosts are also returned. func FilterByHosts(keys []PublicKey, host string, includeEmpty bool) ([]PublicKey, error) { filtered := make([]PublicKey, 0, len(keys)) for _, pubKey := range keys { var hosts []string switch v := pubKey.GetExtendedField("hosts").(type) { case []string: hosts = v case []interface{}: for _, value := range v { h, ok := value.(string) if !ok { continue } hosts = append(hosts, h) } } if len(hosts) == 0 { if includeEmpty { filtered = append(filtered, pubKey) } continue } // Check if any hosts match pattern for _, hostPattern := range hosts { match, err := filepath.Match(hostPattern, host) if err != nil { return nil, err } if match { filtered = append(filtered, pubKey) continue } } } return filtered, nil } ================================================ FILE: vendor/github.com/containers/libtrust/hash.go ================================================ package libtrust import ( "crypto" _ "crypto/sha256" // Registrer SHA224 and SHA256 _ "crypto/sha512" // Registrer SHA384 and SHA512 "fmt" ) type signatureAlgorithm struct { algHeaderParam string hashID crypto.Hash } func (h *signatureAlgorithm) HeaderParam() string { return h.algHeaderParam } func (h *signatureAlgorithm) HashID() crypto.Hash { return h.hashID } var ( rs256 = &signatureAlgorithm{"RS256", crypto.SHA256} rs384 = &signatureAlgorithm{"RS384", crypto.SHA384} rs512 = &signatureAlgorithm{"RS512", crypto.SHA512} es256 = &signatureAlgorithm{"ES256", crypto.SHA256} es384 = &signatureAlgorithm{"ES384", crypto.SHA384} es512 = &signatureAlgorithm{"ES512", crypto.SHA512} ) func rsaSignatureAlgorithmByName(alg string) (*signatureAlgorithm, error) { switch { case alg == "RS256": return rs256, nil case alg == "RS384": return rs384, nil case alg == "RS512": return rs512, nil default: return nil, fmt.Errorf("RSA Digital Signature Algorithm %q not supported", alg) } } func rsaPKCS1v15SignatureAlgorithmForHashID(hashID crypto.Hash) *signatureAlgorithm { switch { case hashID == crypto.SHA512: return rs512 case hashID == crypto.SHA384: return rs384 case hashID == crypto.SHA256: fallthrough default: return rs256 } } ================================================ FILE: vendor/github.com/containers/libtrust/jsonsign.go ================================================ package libtrust import ( "bytes" "crypto" "crypto/x509" "encoding/base64" "encoding/json" "errors" "fmt" "sort" "time" "unicode" ) var ( // ErrInvalidSignContent is used when the content to be signed is invalid. ErrInvalidSignContent = errors.New("invalid sign content") // ErrInvalidJSONContent is used when invalid json is encountered. ErrInvalidJSONContent = errors.New("invalid json content") // ErrMissingSignatureKey is used when the specified signature key // does not exist in the JSON content. ErrMissingSignatureKey = errors.New("missing signature key") ) type jsHeader struct { JWK PublicKey `json:"jwk,omitempty"` Algorithm string `json:"alg"` Chain []string `json:"x5c,omitempty"` } type jsSignature struct { Header jsHeader `json:"header"` Signature string `json:"signature"` Protected string `json:"protected,omitempty"` } type jsSignaturesSorted []jsSignature func (jsbkid jsSignaturesSorted) Swap(i, j int) { jsbkid[i], jsbkid[j] = jsbkid[j], jsbkid[i] } func (jsbkid jsSignaturesSorted) Len() int { return len(jsbkid) } func (jsbkid jsSignaturesSorted) Less(i, j int) bool { ki, kj := jsbkid[i].Header.JWK.KeyID(), jsbkid[j].Header.JWK.KeyID() si, sj := jsbkid[i].Signature, jsbkid[j].Signature if ki == kj { return si < sj } return ki < kj } type signKey struct { PrivateKey Chain []*x509.Certificate } // JSONSignature represents a signature of a json object. type JSONSignature struct { payload string signatures []jsSignature indent string formatLength int formatTail []byte } func newJSONSignature() *JSONSignature { return &JSONSignature{ signatures: make([]jsSignature, 0, 1), } } // Payload returns the encoded payload of the signature. This // payload should not be signed directly func (js *JSONSignature) Payload() ([]byte, error) { return joseBase64UrlDecode(js.payload) } func (js *JSONSignature) protectedHeader() (string, error) { protected := map[string]interface{}{ "formatLength": js.formatLength, "formatTail": joseBase64UrlEncode(js.formatTail), "time": time.Now().UTC().Format(time.RFC3339), } protectedBytes, err := json.Marshal(protected) if err != nil { return "", err } return joseBase64UrlEncode(protectedBytes), nil } func (js *JSONSignature) signBytes(protectedHeader string) ([]byte, error) { buf := make([]byte, len(js.payload)+len(protectedHeader)+1) copy(buf, protectedHeader) buf[len(protectedHeader)] = '.' copy(buf[len(protectedHeader)+1:], js.payload) return buf, nil } // Sign adds a signature using the given private key. func (js *JSONSignature) Sign(key PrivateKey) error { protected, err := js.protectedHeader() if err != nil { return err } signBytes, err := js.signBytes(protected) if err != nil { return err } sigBytes, algorithm, err := key.Sign(bytes.NewReader(signBytes), crypto.SHA256) if err != nil { return err } js.signatures = append(js.signatures, jsSignature{ Header: jsHeader{ JWK: key.PublicKey(), Algorithm: algorithm, }, Signature: joseBase64UrlEncode(sigBytes), Protected: protected, }) return nil } // SignWithChain adds a signature using the given private key // and setting the x509 chain. The public key of the first element // in the chain must be the public key corresponding with the sign key. func (js *JSONSignature) SignWithChain(key PrivateKey, chain []*x509.Certificate) error { // Ensure key.Chain[0] is public key for key //key.Chain.PublicKey //key.PublicKey().CryptoPublicKey() // Verify chain protected, err := js.protectedHeader() if err != nil { return err } signBytes, err := js.signBytes(protected) if err != nil { return err } sigBytes, algorithm, err := key.Sign(bytes.NewReader(signBytes), crypto.SHA256) if err != nil { return err } header := jsHeader{ Chain: make([]string, len(chain)), Algorithm: algorithm, } for i, cert := range chain { header.Chain[i] = base64.StdEncoding.EncodeToString(cert.Raw) } js.signatures = append(js.signatures, jsSignature{ Header: header, Signature: joseBase64UrlEncode(sigBytes), Protected: protected, }) return nil } // Verify verifies all the signatures and returns the list of // public keys used to sign. Any x509 chains are not checked. func (js *JSONSignature) Verify() ([]PublicKey, error) { keys := make([]PublicKey, len(js.signatures)) for i, signature := range js.signatures { signBytes, err := js.signBytes(signature.Protected) if err != nil { return nil, err } var publicKey PublicKey if len(signature.Header.Chain) > 0 { certBytes, err := base64.StdEncoding.DecodeString(signature.Header.Chain[0]) if err != nil { return nil, err } cert, err := x509.ParseCertificate(certBytes) if err != nil { return nil, err } publicKey, err = FromCryptoPublicKey(cert.PublicKey) if err != nil { return nil, err } } else if signature.Header.JWK != nil { publicKey = signature.Header.JWK } else { return nil, errors.New("missing public key") } sigBytes, err := joseBase64UrlDecode(signature.Signature) if err != nil { return nil, err } err = publicKey.Verify(bytes.NewReader(signBytes), signature.Header.Algorithm, sigBytes) if err != nil { return nil, err } keys[i] = publicKey } return keys, nil } // VerifyChains verifies all the signatures and the chains associated // with each signature and returns the list of verified chains. // Signatures without an x509 chain are not checked. func (js *JSONSignature) VerifyChains(ca *x509.CertPool) ([][]*x509.Certificate, error) { chains := make([][]*x509.Certificate, 0, len(js.signatures)) for _, signature := range js.signatures { signBytes, err := js.signBytes(signature.Protected) if err != nil { return nil, err } var publicKey PublicKey if len(signature.Header.Chain) > 0 { certBytes, err := base64.StdEncoding.DecodeString(signature.Header.Chain[0]) if err != nil { return nil, err } cert, err := x509.ParseCertificate(certBytes) if err != nil { return nil, err } publicKey, err = FromCryptoPublicKey(cert.PublicKey) if err != nil { return nil, err } intermediates := x509.NewCertPool() if len(signature.Header.Chain) > 1 { intermediateChain := signature.Header.Chain[1:] for i := range intermediateChain { certBytes, err := base64.StdEncoding.DecodeString(intermediateChain[i]) if err != nil { return nil, err } intermediate, err := x509.ParseCertificate(certBytes) if err != nil { return nil, err } intermediates.AddCert(intermediate) } } verifyOptions := x509.VerifyOptions{ Intermediates: intermediates, Roots: ca, } verifiedChains, err := cert.Verify(verifyOptions) if err != nil { return nil, err } chains = append(chains, verifiedChains...) sigBytes, err := joseBase64UrlDecode(signature.Signature) if err != nil { return nil, err } err = publicKey.Verify(bytes.NewReader(signBytes), signature.Header.Algorithm, sigBytes) if err != nil { return nil, err } } } return chains, nil } // JWS returns JSON serialized JWS according to // http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31#section-7.2 func (js *JSONSignature) JWS() ([]byte, error) { if len(js.signatures) == 0 { return nil, errors.New("missing signature") } sort.Sort(jsSignaturesSorted(js.signatures)) jsonMap := map[string]interface{}{ "payload": js.payload, "signatures": js.signatures, } return json.MarshalIndent(jsonMap, "", " ") } func notSpace(r rune) bool { return !unicode.IsSpace(r) } func detectJSONIndent(jsonContent []byte) (indent string) { if len(jsonContent) > 2 && jsonContent[0] == '{' && jsonContent[1] == '\n' { quoteIndex := bytes.IndexRune(jsonContent[1:], '"') if quoteIndex > 0 { indent = string(jsonContent[2 : quoteIndex+1]) } } return } type jsParsedHeader struct { JWK json.RawMessage `json:"jwk"` Algorithm string `json:"alg"` Chain []string `json:"x5c"` } type jsParsedSignature struct { Header jsParsedHeader `json:"header"` Signature string `json:"signature"` Protected string `json:"protected"` } // ParseJWS parses a JWS serialized JSON object into a Json Signature. func ParseJWS(content []byte) (*JSONSignature, error) { type jsParsed struct { Payload string `json:"payload"` Signatures []jsParsedSignature `json:"signatures"` } parsed := &jsParsed{} err := json.Unmarshal(content, parsed) if err != nil { return nil, err } if len(parsed.Signatures) == 0 { return nil, errors.New("missing signatures") } payload, err := joseBase64UrlDecode(parsed.Payload) if err != nil { return nil, err } js, err := NewJSONSignature(payload) if err != nil { return nil, err } js.signatures = make([]jsSignature, len(parsed.Signatures)) for i, signature := range parsed.Signatures { header := jsHeader{ Algorithm: signature.Header.Algorithm, } if signature.Header.Chain != nil { header.Chain = signature.Header.Chain } if signature.Header.JWK != nil { publicKey, err := UnmarshalPublicKeyJWK([]byte(signature.Header.JWK)) if err != nil { return nil, err } header.JWK = publicKey } js.signatures[i] = jsSignature{ Header: header, Signature: signature.Signature, Protected: signature.Protected, } } return js, nil } // NewJSONSignature returns a new unsigned JWS from a json byte array. // JSONSignature will need to be signed before serializing or storing. // Optionally, one or more signatures can be provided as byte buffers, // containing serialized JWS signatures, to assemble a fully signed JWS // package. It is the callers responsibility to ensure uniqueness of the // provided signatures. func NewJSONSignature(content []byte, signatures ...[]byte) (*JSONSignature, error) { var dataMap map[string]interface{} err := json.Unmarshal(content, &dataMap) if err != nil { return nil, err } js := newJSONSignature() js.indent = detectJSONIndent(content) js.payload = joseBase64UrlEncode(content) // Find trailing } and whitespace, put in protected header closeIndex := bytes.LastIndexFunc(content, notSpace) if content[closeIndex] != '}' { return nil, ErrInvalidJSONContent } lastRuneIndex := bytes.LastIndexFunc(content[:closeIndex], notSpace) if content[lastRuneIndex] == ',' { return nil, ErrInvalidJSONContent } js.formatLength = lastRuneIndex + 1 js.formatTail = content[js.formatLength:] if len(signatures) > 0 { for _, signature := range signatures { var parsedJSig jsParsedSignature if err := json.Unmarshal(signature, &parsedJSig); err != nil { return nil, err } // TODO(stevvooe): A lot of the code below is repeated in // ParseJWS. It will require more refactoring to fix that. jsig := jsSignature{ Header: jsHeader{ Algorithm: parsedJSig.Header.Algorithm, }, Signature: parsedJSig.Signature, Protected: parsedJSig.Protected, } if parsedJSig.Header.Chain != nil { jsig.Header.Chain = parsedJSig.Header.Chain } if parsedJSig.Header.JWK != nil { publicKey, err := UnmarshalPublicKeyJWK([]byte(parsedJSig.Header.JWK)) if err != nil { return nil, err } jsig.Header.JWK = publicKey } js.signatures = append(js.signatures, jsig) } } return js, nil } // NewJSONSignatureFromMap returns a new unsigned JSONSignature from a map or // struct. JWS will need to be signed before serializing or storing. func NewJSONSignatureFromMap(content interface{}) (*JSONSignature, error) { switch content.(type) { case map[string]interface{}: case struct{}: default: return nil, errors.New("invalid data type") } js := newJSONSignature() js.indent = " " payload, err := json.MarshalIndent(content, "", js.indent) if err != nil { return nil, err } js.payload = joseBase64UrlEncode(payload) // Remove '\n}' from formatted section, put in protected header js.formatLength = len(payload) - 2 js.formatTail = payload[js.formatLength:] return js, nil } func readIntFromMap(key string, m map[string]interface{}) (int, bool) { value, ok := m[key] if !ok { return 0, false } switch v := value.(type) { case int: return v, true case float64: return int(v), true default: return 0, false } } func readStringFromMap(key string, m map[string]interface{}) (v string, ok bool) { value, ok := m[key] if !ok { return "", false } v, ok = value.(string) return } // ParsePrettySignature parses a formatted signature into a // JSON signature. If the signatures are missing the format information // an error is thrown. The formatted signature must be created by // the same method as format signature. func ParsePrettySignature(content []byte, signatureKey string) (*JSONSignature, error) { var contentMap map[string]json.RawMessage err := json.Unmarshal(content, &contentMap) if err != nil { return nil, fmt.Errorf("error unmarshalling content: %s", err) } sigMessage, ok := contentMap[signatureKey] if !ok { return nil, ErrMissingSignatureKey } var signatureBlocks []jsParsedSignature err = json.Unmarshal([]byte(sigMessage), &signatureBlocks) if err != nil { return nil, fmt.Errorf("error unmarshalling signatures: %s", err) } js := newJSONSignature() js.signatures = make([]jsSignature, len(signatureBlocks)) for i, signatureBlock := range signatureBlocks { protectedBytes, err := joseBase64UrlDecode(signatureBlock.Protected) if err != nil { return nil, fmt.Errorf("base64 decode error: %s", err) } var protectedHeader map[string]interface{} err = json.Unmarshal(protectedBytes, &protectedHeader) if err != nil { return nil, fmt.Errorf("error unmarshalling protected header: %s", err) } formatLength, ok := readIntFromMap("formatLength", protectedHeader) if !ok { return nil, errors.New("missing formatted length") } encodedTail, ok := readStringFromMap("formatTail", protectedHeader) if !ok { return nil, errors.New("missing formatted tail") } formatTail, err := joseBase64UrlDecode(encodedTail) if err != nil { return nil, fmt.Errorf("base64 decode error on tail: %s", err) } if js.formatLength == 0 { js.formatLength = formatLength } else if js.formatLength != formatLength { return nil, errors.New("conflicting format length") } if len(js.formatTail) == 0 { js.formatTail = formatTail } else if bytes.Compare(js.formatTail, formatTail) != 0 { return nil, errors.New("conflicting format tail") } header := jsHeader{ Algorithm: signatureBlock.Header.Algorithm, Chain: signatureBlock.Header.Chain, } if signatureBlock.Header.JWK != nil { publicKey, err := UnmarshalPublicKeyJWK([]byte(signatureBlock.Header.JWK)) if err != nil { return nil, fmt.Errorf("error unmarshalling public key: %s", err) } header.JWK = publicKey } js.signatures[i] = jsSignature{ Header: header, Signature: signatureBlock.Signature, Protected: signatureBlock.Protected, } } if js.formatLength > len(content) { return nil, errors.New("invalid format length") } formatted := make([]byte, js.formatLength+len(js.formatTail)) copy(formatted, content[:js.formatLength]) copy(formatted[js.formatLength:], js.formatTail) js.indent = detectJSONIndent(formatted) js.payload = joseBase64UrlEncode(formatted) return js, nil } // PrettySignature formats a json signature into an easy to read // single json serialized object. func (js *JSONSignature) PrettySignature(signatureKey string) ([]byte, error) { if len(js.signatures) == 0 { return nil, errors.New("no signatures") } payload, err := joseBase64UrlDecode(js.payload) if err != nil { return nil, err } payload = payload[:js.formatLength] sort.Sort(jsSignaturesSorted(js.signatures)) var marshalled []byte var marshallErr error if js.indent != "" { marshalled, marshallErr = json.MarshalIndent(js.signatures, js.indent, js.indent) } else { marshalled, marshallErr = json.Marshal(js.signatures) } if marshallErr != nil { return nil, marshallErr } buf := bytes.NewBuffer(make([]byte, 0, len(payload)+len(marshalled)+34)) buf.Write(payload) buf.WriteByte(',') if js.indent != "" { buf.WriteByte('\n') buf.WriteString(js.indent) buf.WriteByte('"') buf.WriteString(signatureKey) buf.WriteString("\": ") buf.Write(marshalled) buf.WriteByte('\n') } else { buf.WriteByte('"') buf.WriteString(signatureKey) buf.WriteString("\":") buf.Write(marshalled) } buf.WriteByte('}') return buf.Bytes(), nil } // Signatures provides the signatures on this JWS as opaque blobs, sorted by // keyID. These blobs can be stored and reassembled with payloads. Internally, // they are simply marshaled json web signatures but implementations should // not rely on this. func (js *JSONSignature) Signatures() ([][]byte, error) { sort.Sort(jsSignaturesSorted(js.signatures)) var sb [][]byte for _, jsig := range js.signatures { p, err := json.Marshal(jsig) if err != nil { return nil, err } sb = append(sb, p) } return sb, nil } // Merge combines the signatures from one or more other signatures into the // method receiver. If the payloads differ for any argument, an error will be // returned and the receiver will not be modified. func (js *JSONSignature) Merge(others ...*JSONSignature) error { merged := js.signatures for _, other := range others { if js.payload != other.payload { return fmt.Errorf("payloads differ from merge target") } merged = append(merged, other.signatures...) } js.signatures = merged return nil } ================================================ FILE: vendor/github.com/containers/libtrust/key.go ================================================ package libtrust import ( "crypto" "crypto/ecdsa" "crypto/rsa" "crypto/x509" "encoding/json" "encoding/pem" "errors" "fmt" "io" ) // PublicKey is a generic interface for a Public Key. type PublicKey interface { // KeyType returns the key type for this key. For elliptic curve keys, // this value should be "EC". For RSA keys, this value should be "RSA". KeyType() string // KeyID returns a distinct identifier which is unique to this Public Key. // The format generated by this library is a base32 encoding of a 240 bit // hash of the public key data divided into 12 groups like so: // ABCD:EFGH:IJKL:MNOP:QRST:UVWX:YZ23:4567:ABCD:EFGH:IJKL:MNOP KeyID() string // Verify verifyies the signature of the data in the io.Reader using this // Public Key. The alg parameter should identify the digital signature // algorithm which was used to produce the signature and should be // supported by this public key. Returns a nil error if the signature // is valid. Verify(data io.Reader, alg string, signature []byte) error // CryptoPublicKey returns the internal object which can be used as a // crypto.PublicKey for use with other standard library operations. The type // is either *rsa.PublicKey or *ecdsa.PublicKey CryptoPublicKey() crypto.PublicKey // These public keys can be serialized to the standard JSON encoding for // JSON Web Keys. See section 6 of the IETF draft RFC for JOSE JSON Web // Algorithms. MarshalJSON() ([]byte, error) // These keys can also be serialized to the standard PEM encoding. PEMBlock() (*pem.Block, error) // The string representation of a key is its key type and ID. String() string AddExtendedField(string, interface{}) GetExtendedField(string) interface{} } // PrivateKey is a generic interface for a Private Key. type PrivateKey interface { // A PrivateKey contains all fields and methods of a PublicKey of the // same type. The MarshalJSON method also outputs the private key as a // JSON Web Key, and the PEMBlock method outputs the private key as a // PEM block. PublicKey // PublicKey returns the PublicKey associated with this PrivateKey. PublicKey() PublicKey // Sign signs the data read from the io.Reader using a signature algorithm // supported by the private key. If the specified hashing algorithm is // supported by this key, that hash function is used to generate the // signature otherwise the the default hashing algorithm for this key is // used. Returns the signature and identifier of the algorithm used. Sign(data io.Reader, hashID crypto.Hash) (signature []byte, alg string, err error) // CryptoPrivateKey returns the internal object which can be used as a // crypto.PublicKey for use with other standard library operations. The // type is either *rsa.PublicKey or *ecdsa.PublicKey CryptoPrivateKey() crypto.PrivateKey } // FromCryptoPublicKey returns a libtrust PublicKey representation of the given // *ecdsa.PublicKey or *rsa.PublicKey. Returns a non-nil error when the given // key is of an unsupported type. func FromCryptoPublicKey(cryptoPublicKey crypto.PublicKey) (PublicKey, error) { switch cryptoPublicKey := cryptoPublicKey.(type) { case *ecdsa.PublicKey: return fromECPublicKey(cryptoPublicKey) case *rsa.PublicKey: return fromRSAPublicKey(cryptoPublicKey), nil default: return nil, fmt.Errorf("public key type %T is not supported", cryptoPublicKey) } } // FromCryptoPrivateKey returns a libtrust PrivateKey representation of the given // *ecdsa.PrivateKey or *rsa.PrivateKey. Returns a non-nil error when the given // key is of an unsupported type. func FromCryptoPrivateKey(cryptoPrivateKey crypto.PrivateKey) (PrivateKey, error) { switch cryptoPrivateKey := cryptoPrivateKey.(type) { case *ecdsa.PrivateKey: return fromECPrivateKey(cryptoPrivateKey) case *rsa.PrivateKey: return fromRSAPrivateKey(cryptoPrivateKey), nil default: return nil, fmt.Errorf("private key type %T is not supported", cryptoPrivateKey) } } // UnmarshalPublicKeyPEM parses the PEM encoded data and returns a libtrust // PublicKey or an error if there is a problem with the encoding. func UnmarshalPublicKeyPEM(data []byte) (PublicKey, error) { pemBlock, _ := pem.Decode(data) if pemBlock == nil { return nil, errors.New("unable to find PEM encoded data") } else if pemBlock.Type != "PUBLIC KEY" { return nil, fmt.Errorf("unable to get PublicKey from PEM type: %s", pemBlock.Type) } return pubKeyFromPEMBlock(pemBlock) } // UnmarshalPublicKeyPEMBundle parses the PEM encoded data as a bundle of // PEM blocks appended one after the other and returns a slice of PublicKey // objects that it finds. func UnmarshalPublicKeyPEMBundle(data []byte) ([]PublicKey, error) { pubKeys := []PublicKey{} for { var pemBlock *pem.Block pemBlock, data = pem.Decode(data) if pemBlock == nil { break } else if pemBlock.Type != "PUBLIC KEY" { return nil, fmt.Errorf("unable to get PublicKey from PEM type: %s", pemBlock.Type) } pubKey, err := pubKeyFromPEMBlock(pemBlock) if err != nil { return nil, err } pubKeys = append(pubKeys, pubKey) } return pubKeys, nil } // UnmarshalPrivateKeyPEM parses the PEM encoded data and returns a libtrust // PrivateKey or an error if there is a problem with the encoding. func UnmarshalPrivateKeyPEM(data []byte) (PrivateKey, error) { pemBlock, _ := pem.Decode(data) if pemBlock == nil { return nil, errors.New("unable to find PEM encoded data") } var key PrivateKey switch { case pemBlock.Type == "RSA PRIVATE KEY": rsaPrivateKey, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes) if err != nil { return nil, fmt.Errorf("unable to decode RSA Private Key PEM data: %s", err) } key = fromRSAPrivateKey(rsaPrivateKey) case pemBlock.Type == "EC PRIVATE KEY": ecPrivateKey, err := x509.ParseECPrivateKey(pemBlock.Bytes) if err != nil { return nil, fmt.Errorf("unable to decode EC Private Key PEM data: %s", err) } key, err = fromECPrivateKey(ecPrivateKey) if err != nil { return nil, err } default: return nil, fmt.Errorf("unable to get PrivateKey from PEM type: %s", pemBlock.Type) } addPEMHeadersToKey(pemBlock, key.PublicKey()) return key, nil } // UnmarshalPublicKeyJWK unmarshals the given JSON Web Key into a generic // Public Key to be used with libtrust. func UnmarshalPublicKeyJWK(data []byte) (PublicKey, error) { jwk := make(map[string]interface{}) err := json.Unmarshal(data, &jwk) if err != nil { return nil, fmt.Errorf( "decoding JWK Public Key JSON data: %s\n", err, ) } // Get the Key Type value. kty, err := stringFromMap(jwk, "kty") if err != nil { return nil, fmt.Errorf("JWK Public Key type: %s", err) } switch { case kty == "EC": // Call out to unmarshal EC public key. return ecPublicKeyFromMap(jwk) case kty == "RSA": // Call out to unmarshal RSA public key. return rsaPublicKeyFromMap(jwk) default: return nil, fmt.Errorf( "JWK Public Key type not supported: %q\n", kty, ) } } // UnmarshalPublicKeyJWKSet parses the JSON encoded data as a JSON Web Key Set // and returns a slice of Public Key objects. func UnmarshalPublicKeyJWKSet(data []byte) ([]PublicKey, error) { rawKeys, err := loadJSONKeySetRaw(data) if err != nil { return nil, err } pubKeys := make([]PublicKey, 0, len(rawKeys)) for _, rawKey := range rawKeys { pubKey, err := UnmarshalPublicKeyJWK(rawKey) if err != nil { return nil, err } pubKeys = append(pubKeys, pubKey) } return pubKeys, nil } // UnmarshalPrivateKeyJWK unmarshals the given JSON Web Key into a generic // Private Key to be used with libtrust. func UnmarshalPrivateKeyJWK(data []byte) (PrivateKey, error) { jwk := make(map[string]interface{}) err := json.Unmarshal(data, &jwk) if err != nil { return nil, fmt.Errorf( "decoding JWK Private Key JSON data: %s\n", err, ) } // Get the Key Type value. kty, err := stringFromMap(jwk, "kty") if err != nil { return nil, fmt.Errorf("JWK Private Key type: %s", err) } switch { case kty == "EC": // Call out to unmarshal EC private key. return ecPrivateKeyFromMap(jwk) case kty == "RSA": // Call out to unmarshal RSA private key. return rsaPrivateKeyFromMap(jwk) default: return nil, fmt.Errorf( "JWK Private Key type not supported: %q\n", kty, ) } } ================================================ FILE: vendor/github.com/containers/libtrust/key_files.go ================================================ package libtrust import ( "encoding/json" "encoding/pem" "errors" "fmt" "io/ioutil" "os" "strings" ) var ( // ErrKeyFileDoesNotExist indicates that the private key file does not exist. ErrKeyFileDoesNotExist = errors.New("key file does not exist") ) func readKeyFileBytes(filename string) ([]byte, error) { data, err := ioutil.ReadFile(filename) if err != nil { if os.IsNotExist(err) { err = ErrKeyFileDoesNotExist } else { err = fmt.Errorf("unable to read key file %s: %s", filename, err) } return nil, err } return data, nil } /* Loading and Saving of Public and Private Keys in either PEM or JWK format. */ // LoadKeyFile opens the given filename and attempts to read a Private Key // encoded in either PEM or JWK format (if .json or .jwk file extension). func LoadKeyFile(filename string) (PrivateKey, error) { contents, err := readKeyFileBytes(filename) if err != nil { return nil, err } var key PrivateKey if strings.HasSuffix(filename, ".json") || strings.HasSuffix(filename, ".jwk") { key, err = UnmarshalPrivateKeyJWK(contents) if err != nil { return nil, fmt.Errorf("unable to decode private key JWK: %s", err) } } else { key, err = UnmarshalPrivateKeyPEM(contents) if err != nil { return nil, fmt.Errorf("unable to decode private key PEM: %s", err) } } return key, nil } // LoadPublicKeyFile opens the given filename and attempts to read a Public Key // encoded in either PEM or JWK format (if .json or .jwk file extension). func LoadPublicKeyFile(filename string) (PublicKey, error) { contents, err := readKeyFileBytes(filename) if err != nil { return nil, err } var key PublicKey if strings.HasSuffix(filename, ".json") || strings.HasSuffix(filename, ".jwk") { key, err = UnmarshalPublicKeyJWK(contents) if err != nil { return nil, fmt.Errorf("unable to decode public key JWK: %s", err) } } else { key, err = UnmarshalPublicKeyPEM(contents) if err != nil { return nil, fmt.Errorf("unable to decode public key PEM: %s", err) } } return key, nil } // SaveKey saves the given key to a file using the provided filename. // This process will overwrite any existing file at the provided location. func SaveKey(filename string, key PrivateKey) error { var encodedKey []byte var err error if strings.HasSuffix(filename, ".json") || strings.HasSuffix(filename, ".jwk") { // Encode in JSON Web Key format. encodedKey, err = json.MarshalIndent(key, "", " ") if err != nil { return fmt.Errorf("unable to encode private key JWK: %s", err) } } else { // Encode in PEM format. pemBlock, err := key.PEMBlock() if err != nil { return fmt.Errorf("unable to encode private key PEM: %s", err) } encodedKey = pem.EncodeToMemory(pemBlock) } err = ioutil.WriteFile(filename, encodedKey, os.FileMode(0600)) if err != nil { return fmt.Errorf("unable to write private key file %s: %s", filename, err) } return nil } // SavePublicKey saves the given public key to the file. func SavePublicKey(filename string, key PublicKey) error { var encodedKey []byte var err error if strings.HasSuffix(filename, ".json") || strings.HasSuffix(filename, ".jwk") { // Encode in JSON Web Key format. encodedKey, err = json.MarshalIndent(key, "", " ") if err != nil { return fmt.Errorf("unable to encode public key JWK: %s", err) } } else { // Encode in PEM format. pemBlock, err := key.PEMBlock() if err != nil { return fmt.Errorf("unable to encode public key PEM: %s", err) } encodedKey = pem.EncodeToMemory(pemBlock) } err = ioutil.WriteFile(filename, encodedKey, os.FileMode(0644)) if err != nil { return fmt.Errorf("unable to write public key file %s: %s", filename, err) } return nil } // Public Key Set files type jwkSet struct { Keys []json.RawMessage `json:"keys"` } // LoadKeySetFile loads a key set func LoadKeySetFile(filename string) ([]PublicKey, error) { if strings.HasSuffix(filename, ".json") || strings.HasSuffix(filename, ".jwk") { return loadJSONKeySetFile(filename) } // Must be a PEM format file return loadPEMKeySetFile(filename) } func loadJSONKeySetRaw(data []byte) ([]json.RawMessage, error) { if len(data) == 0 { // This is okay, just return an empty slice. return []json.RawMessage{}, nil } keySet := jwkSet{} err := json.Unmarshal(data, &keySet) if err != nil { return nil, fmt.Errorf("unable to decode JSON Web Key Set: %s", err) } return keySet.Keys, nil } func loadJSONKeySetFile(filename string) ([]PublicKey, error) { contents, err := readKeyFileBytes(filename) if err != nil && err != ErrKeyFileDoesNotExist { return nil, err } return UnmarshalPublicKeyJWKSet(contents) } func loadPEMKeySetFile(filename string) ([]PublicKey, error) { data, err := readKeyFileBytes(filename) if err != nil && err != ErrKeyFileDoesNotExist { return nil, err } return UnmarshalPublicKeyPEMBundle(data) } // AddKeySetFile adds a key to a key set func AddKeySetFile(filename string, key PublicKey) error { if strings.HasSuffix(filename, ".json") || strings.HasSuffix(filename, ".jwk") { return addKeySetJSONFile(filename, key) } // Must be a PEM format file return addKeySetPEMFile(filename, key) } func addKeySetJSONFile(filename string, key PublicKey) error { encodedKey, err := json.Marshal(key) if err != nil { return fmt.Errorf("unable to encode trusted client key: %s", err) } contents, err := readKeyFileBytes(filename) if err != nil && err != ErrKeyFileDoesNotExist { return err } rawEntries, err := loadJSONKeySetRaw(contents) if err != nil { return err } rawEntries = append(rawEntries, json.RawMessage(encodedKey)) entriesWrapper := jwkSet{Keys: rawEntries} encodedEntries, err := json.MarshalIndent(entriesWrapper, "", " ") if err != nil { return fmt.Errorf("unable to encode trusted client keys: %s", err) } err = ioutil.WriteFile(filename, encodedEntries, os.FileMode(0644)) if err != nil { return fmt.Errorf("unable to write trusted client keys file %s: %s", filename, err) } return nil } func addKeySetPEMFile(filename string, key PublicKey) error { // Encode to PEM, open file for appending, write PEM. file, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, os.FileMode(0644)) if err != nil { return fmt.Errorf("unable to open trusted client keys file %s: %s", filename, err) } defer file.Close() pemBlock, err := key.PEMBlock() if err != nil { return fmt.Errorf("unable to encoded trusted key: %s", err) } _, err = file.Write(pem.EncodeToMemory(pemBlock)) if err != nil { return fmt.Errorf("unable to write trusted keys file: %s", err) } return nil } ================================================ FILE: vendor/github.com/containers/libtrust/key_manager.go ================================================ package libtrust import ( "crypto/tls" "crypto/x509" "fmt" "io/ioutil" "net" "os" "path" "sync" ) // ClientKeyManager manages client keys on the filesystem type ClientKeyManager struct { key PrivateKey clientFile string clientDir string clientLock sync.RWMutex clients []PublicKey configLock sync.Mutex configs []*tls.Config } // NewClientKeyManager loads a new manager from a set of key files // and managed by the given private key. func NewClientKeyManager(trustKey PrivateKey, clientFile, clientDir string) (*ClientKeyManager, error) { m := &ClientKeyManager{ key: trustKey, clientFile: clientFile, clientDir: clientDir, } if err := m.loadKeys(); err != nil { return nil, err } // TODO Start watching file and directory return m, nil } func (c *ClientKeyManager) loadKeys() (err error) { // Load authorized keys file var clients []PublicKey if c.clientFile != "" { clients, err = LoadKeySetFile(c.clientFile) if err != nil { return fmt.Errorf("unable to load authorized keys: %s", err) } } // Add clients from authorized keys directory files, err := ioutil.ReadDir(c.clientDir) if err != nil && !os.IsNotExist(err) { return fmt.Errorf("unable to open authorized keys directory: %s", err) } for _, f := range files { if !f.IsDir() { publicKey, err := LoadPublicKeyFile(path.Join(c.clientDir, f.Name())) if err != nil { return fmt.Errorf("unable to load authorized key file: %s", err) } clients = append(clients, publicKey) } } c.clientLock.Lock() c.clients = clients c.clientLock.Unlock() return nil } // RegisterTLSConfig registers a tls configuration to manager // such that any changes to the keys may be reflected in // the tls client CA pool func (c *ClientKeyManager) RegisterTLSConfig(tlsConfig *tls.Config) error { c.clientLock.RLock() certPool, err := GenerateCACertPool(c.key, c.clients) if err != nil { return fmt.Errorf("CA pool generation error: %s", err) } c.clientLock.RUnlock() tlsConfig.ClientCAs = certPool c.configLock.Lock() c.configs = append(c.configs, tlsConfig) c.configLock.Unlock() return nil } // NewIdentityAuthTLSConfig creates a tls.Config for the server to use for // libtrust identity authentication for the domain specified func NewIdentityAuthTLSConfig(trustKey PrivateKey, clients *ClientKeyManager, addr string, domain string) (*tls.Config, error) { tlsConfig := newTLSConfig() tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert if err := clients.RegisterTLSConfig(tlsConfig); err != nil { return nil, err } // Generate cert ips, domains, err := parseAddr(addr) if err != nil { return nil, err } // add domain that it expects clients to use domains = append(domains, domain) x509Cert, err := GenerateSelfSignedServerCert(trustKey, domains, ips) if err != nil { return nil, fmt.Errorf("certificate generation error: %s", err) } tlsConfig.Certificates = []tls.Certificate{{ Certificate: [][]byte{x509Cert.Raw}, PrivateKey: trustKey.CryptoPrivateKey(), Leaf: x509Cert, }} return tlsConfig, nil } // NewCertAuthTLSConfig creates a tls.Config for the server to use for // certificate authentication func NewCertAuthTLSConfig(caPath, certPath, keyPath string) (*tls.Config, error) { tlsConfig := newTLSConfig() cert, err := tls.LoadX509KeyPair(certPath, keyPath) if err != nil { return nil, fmt.Errorf("Couldn't load X509 key pair (%s, %s): %s. Key encrypted?", certPath, keyPath, err) } tlsConfig.Certificates = []tls.Certificate{cert} // Verify client certificates against a CA? if caPath != "" { certPool := x509.NewCertPool() file, err := ioutil.ReadFile(caPath) if err != nil { return nil, fmt.Errorf("Couldn't read CA certificate: %s", err) } certPool.AppendCertsFromPEM(file) tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert tlsConfig.ClientCAs = certPool } return tlsConfig, nil } func newTLSConfig() *tls.Config { return &tls.Config{ NextProtos: []string{"http/1.1"}, // Avoid fallback on insecure SSL protocols MinVersion: tls.VersionTLS10, } } // parseAddr parses an address into an array of IPs and domains func parseAddr(addr string) ([]net.IP, []string, error) { host, _, err := net.SplitHostPort(addr) if err != nil { return nil, nil, err } var domains []string var ips []net.IP ip := net.ParseIP(host) if ip != nil { ips = []net.IP{ip} } else { domains = []string{host} } return ips, domains, nil } ================================================ FILE: vendor/github.com/containers/libtrust/rsa_key.go ================================================ package libtrust import ( "crypto" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/json" "encoding/pem" "errors" "fmt" "io" "math/big" ) /* * RSA DSA PUBLIC KEY */ // rsaPublicKey implements a JWK Public Key using RSA digital signature algorithms. type rsaPublicKey struct { *rsa.PublicKey extended map[string]interface{} } func fromRSAPublicKey(cryptoPublicKey *rsa.PublicKey) *rsaPublicKey { return &rsaPublicKey{cryptoPublicKey, map[string]interface{}{}} } // KeyType returns the JWK key type for RSA keys, i.e., "RSA". func (k *rsaPublicKey) KeyType() string { return "RSA" } // KeyID returns a distinct identifier which is unique to this Public Key. func (k *rsaPublicKey) KeyID() string { return keyIDFromCryptoKey(k) } func (k *rsaPublicKey) String() string { return fmt.Sprintf("RSA Public Key <%s>", k.KeyID()) } // Verify verifyies the signature of the data in the io.Reader using this Public Key. // The alg parameter should be the name of the JWA digital signature algorithm // which was used to produce the signature and should be supported by this // public key. Returns a nil error if the signature is valid. func (k *rsaPublicKey) Verify(data io.Reader, alg string, signature []byte) error { // Verify the signature of the given date, return non-nil error if valid. sigAlg, err := rsaSignatureAlgorithmByName(alg) if err != nil { return fmt.Errorf("unable to verify Signature: %s", err) } hasher := sigAlg.HashID().New() _, err = io.Copy(hasher, data) if err != nil { return fmt.Errorf("error reading data to sign: %s", err) } hash := hasher.Sum(nil) err = rsa.VerifyPKCS1v15(k.PublicKey, sigAlg.HashID(), hash, signature) if err != nil { return fmt.Errorf("invalid %s signature: %s", sigAlg.HeaderParam(), err) } return nil } // CryptoPublicKey returns the internal object which can be used as a // crypto.PublicKey for use with other standard library operations. The type // is either *rsa.PublicKey or *ecdsa.PublicKey func (k *rsaPublicKey) CryptoPublicKey() crypto.PublicKey { return k.PublicKey } func (k *rsaPublicKey) toMap() map[string]interface{} { jwk := make(map[string]interface{}) for k, v := range k.extended { jwk[k] = v } jwk["kty"] = k.KeyType() jwk["kid"] = k.KeyID() jwk["n"] = joseBase64UrlEncode(k.N.Bytes()) jwk["e"] = joseBase64UrlEncode(serializeRSAPublicExponentParam(k.E)) return jwk } // MarshalJSON serializes this Public Key using the JWK JSON serialization format for // RSA keys. func (k *rsaPublicKey) MarshalJSON() (data []byte, err error) { return json.Marshal(k.toMap()) } // PEMBlock serializes this Public Key to DER-encoded PKIX format. func (k *rsaPublicKey) PEMBlock() (*pem.Block, error) { derBytes, err := x509.MarshalPKIXPublicKey(k.PublicKey) if err != nil { return nil, fmt.Errorf("unable to serialize RSA PublicKey to DER-encoded PKIX format: %s", err) } k.extended["kid"] = k.KeyID() // For display purposes. return createPemBlock("PUBLIC KEY", derBytes, k.extended) } func (k *rsaPublicKey) AddExtendedField(field string, value interface{}) { k.extended[field] = value } func (k *rsaPublicKey) GetExtendedField(field string) interface{} { v, ok := k.extended[field] if !ok { return nil } return v } func rsaPublicKeyFromMap(jwk map[string]interface{}) (*rsaPublicKey, error) { // JWK key type (kty) has already been determined to be "RSA". // Need to extract 'n', 'e', and 'kid' and check for // consistency. // Get the modulus parameter N. nB64Url, err := stringFromMap(jwk, "n") if err != nil { return nil, fmt.Errorf("JWK RSA Public Key modulus: %s", err) } n, err := parseRSAModulusParam(nB64Url) if err != nil { return nil, fmt.Errorf("JWK RSA Public Key modulus: %s", err) } // Get the public exponent E. eB64Url, err := stringFromMap(jwk, "e") if err != nil { return nil, fmt.Errorf("JWK RSA Public Key exponent: %s", err) } e, err := parseRSAPublicExponentParam(eB64Url) if err != nil { return nil, fmt.Errorf("JWK RSA Public Key exponent: %s", err) } key := &rsaPublicKey{ PublicKey: &rsa.PublicKey{N: n, E: e}, } // Key ID is optional, but if it exists, it should match the key. _, ok := jwk["kid"] if ok { kid, err := stringFromMap(jwk, "kid") if err != nil { return nil, fmt.Errorf("JWK RSA Public Key ID: %s", err) } if kid != key.KeyID() { return nil, fmt.Errorf("JWK RSA Public Key ID does not match: %s", kid) } } if _, ok := jwk["d"]; ok { return nil, fmt.Errorf("JWK RSA Public Key cannot contain private exponent") } key.extended = jwk return key, nil } /* * RSA DSA PRIVATE KEY */ // rsaPrivateKey implements a JWK Private Key using RSA digital signature algorithms. type rsaPrivateKey struct { rsaPublicKey *rsa.PrivateKey } func fromRSAPrivateKey(cryptoPrivateKey *rsa.PrivateKey) *rsaPrivateKey { return &rsaPrivateKey{ *fromRSAPublicKey(&cryptoPrivateKey.PublicKey), cryptoPrivateKey, } } // PublicKey returns the Public Key data associated with this Private Key. func (k *rsaPrivateKey) PublicKey() PublicKey { return &k.rsaPublicKey } func (k *rsaPrivateKey) String() string { return fmt.Sprintf("RSA Private Key <%s>", k.KeyID()) } // Sign signs the data read from the io.Reader using a signature algorithm supported // by the RSA private key. If the specified hashing algorithm is supported by // this key, that hash function is used to generate the signature otherwise the // the default hashing algorithm for this key is used. Returns the signature // and the name of the JWK signature algorithm used, e.g., "RS256", "RS384", // "RS512". func (k *rsaPrivateKey) Sign(data io.Reader, hashID crypto.Hash) (signature []byte, alg string, err error) { // Generate a signature of the data using the internal alg. sigAlg := rsaPKCS1v15SignatureAlgorithmForHashID(hashID) hasher := sigAlg.HashID().New() _, err = io.Copy(hasher, data) if err != nil { return nil, "", fmt.Errorf("error reading data to sign: %s", err) } hash := hasher.Sum(nil) signature, err = rsa.SignPKCS1v15(rand.Reader, k.PrivateKey, sigAlg.HashID(), hash) if err != nil { return nil, "", fmt.Errorf("error producing signature: %s", err) } alg = sigAlg.HeaderParam() return } // CryptoPrivateKey returns the internal object which can be used as a // crypto.PublicKey for use with other standard library operations. The type // is either *rsa.PublicKey or *ecdsa.PublicKey func (k *rsaPrivateKey) CryptoPrivateKey() crypto.PrivateKey { return k.PrivateKey } func (k *rsaPrivateKey) toMap() map[string]interface{} { k.Precompute() // Make sure the precomputed values are stored. jwk := k.rsaPublicKey.toMap() jwk["d"] = joseBase64UrlEncode(k.D.Bytes()) jwk["p"] = joseBase64UrlEncode(k.Primes[0].Bytes()) jwk["q"] = joseBase64UrlEncode(k.Primes[1].Bytes()) jwk["dp"] = joseBase64UrlEncode(k.Precomputed.Dp.Bytes()) jwk["dq"] = joseBase64UrlEncode(k.Precomputed.Dq.Bytes()) jwk["qi"] = joseBase64UrlEncode(k.Precomputed.Qinv.Bytes()) otherPrimes := k.Primes[2:] if len(otherPrimes) > 0 { otherPrimesInfo := make([]interface{}, len(otherPrimes)) for i, r := range otherPrimes { otherPrimeInfo := make(map[string]string, 3) otherPrimeInfo["r"] = joseBase64UrlEncode(r.Bytes()) crtVal := k.Precomputed.CRTValues[i] otherPrimeInfo["d"] = joseBase64UrlEncode(crtVal.Exp.Bytes()) otherPrimeInfo["t"] = joseBase64UrlEncode(crtVal.Coeff.Bytes()) otherPrimesInfo[i] = otherPrimeInfo } jwk["oth"] = otherPrimesInfo } return jwk } // MarshalJSON serializes this Private Key using the JWK JSON serialization format for // RSA keys. func (k *rsaPrivateKey) MarshalJSON() (data []byte, err error) { return json.Marshal(k.toMap()) } // PEMBlock serializes this Private Key to DER-encoded PKIX format. func (k *rsaPrivateKey) PEMBlock() (*pem.Block, error) { derBytes := x509.MarshalPKCS1PrivateKey(k.PrivateKey) k.extended["keyID"] = k.KeyID() // For display purposes. return createPemBlock("RSA PRIVATE KEY", derBytes, k.extended) } func rsaPrivateKeyFromMap(jwk map[string]interface{}) (*rsaPrivateKey, error) { // The JWA spec for RSA Private Keys (draft rfc section 5.3.2) states that // only the private key exponent 'd' is REQUIRED, the others are just for // signature/decryption optimizations and SHOULD be included when the JWK // is produced. We MAY choose to accept a JWK which only includes 'd', but // we're going to go ahead and not choose to accept it without the extra // fields. Only the 'oth' field will be optional (for multi-prime keys). privateExponent, err := parseRSAPrivateKeyParamFromMap(jwk, "d") if err != nil { return nil, fmt.Errorf("JWK RSA Private Key exponent: %s", err) } firstPrimeFactor, err := parseRSAPrivateKeyParamFromMap(jwk, "p") if err != nil { return nil, fmt.Errorf("JWK RSA Private Key prime factor: %s", err) } secondPrimeFactor, err := parseRSAPrivateKeyParamFromMap(jwk, "q") if err != nil { return nil, fmt.Errorf("JWK RSA Private Key prime factor: %s", err) } firstFactorCRT, err := parseRSAPrivateKeyParamFromMap(jwk, "dp") if err != nil { return nil, fmt.Errorf("JWK RSA Private Key CRT exponent: %s", err) } secondFactorCRT, err := parseRSAPrivateKeyParamFromMap(jwk, "dq") if err != nil { return nil, fmt.Errorf("JWK RSA Private Key CRT exponent: %s", err) } crtCoeff, err := parseRSAPrivateKeyParamFromMap(jwk, "qi") if err != nil { return nil, fmt.Errorf("JWK RSA Private Key CRT coefficient: %s", err) } var oth interface{} if _, ok := jwk["oth"]; ok { oth = jwk["oth"] delete(jwk, "oth") } // JWK key type (kty) has already been determined to be "RSA". // Need to extract the public key information, then extract the private // key values. publicKey, err := rsaPublicKeyFromMap(jwk) if err != nil { return nil, err } privateKey := &rsa.PrivateKey{ PublicKey: *publicKey.PublicKey, D: privateExponent, Primes: []*big.Int{firstPrimeFactor, secondPrimeFactor}, Precomputed: rsa.PrecomputedValues{ Dp: firstFactorCRT, Dq: secondFactorCRT, Qinv: crtCoeff, }, } if oth != nil { // Should be an array of more JSON objects. otherPrimesInfo, ok := oth.([]interface{}) if !ok { return nil, errors.New("JWK RSA Private Key: Invalid other primes info: must be an array") } numOtherPrimeFactors := len(otherPrimesInfo) if numOtherPrimeFactors == 0 { return nil, errors.New("JWK RSA Privake Key: Invalid other primes info: must be absent or non-empty") } otherPrimeFactors := make([]*big.Int, numOtherPrimeFactors) productOfPrimes := new(big.Int).Mul(firstPrimeFactor, secondPrimeFactor) crtValues := make([]rsa.CRTValue, numOtherPrimeFactors) for i, val := range otherPrimesInfo { otherPrimeinfo, ok := val.(map[string]interface{}) if !ok { return nil, errors.New("JWK RSA Private Key: Invalid other prime info: must be a JSON object") } otherPrimeFactor, err := parseRSAPrivateKeyParamFromMap(otherPrimeinfo, "r") if err != nil { return nil, fmt.Errorf("JWK RSA Private Key prime factor: %s", err) } otherFactorCRT, err := parseRSAPrivateKeyParamFromMap(otherPrimeinfo, "d") if err != nil { return nil, fmt.Errorf("JWK RSA Private Key CRT exponent: %s", err) } otherCrtCoeff, err := parseRSAPrivateKeyParamFromMap(otherPrimeinfo, "t") if err != nil { return nil, fmt.Errorf("JWK RSA Private Key CRT coefficient: %s", err) } crtValue := crtValues[i] crtValue.Exp = otherFactorCRT crtValue.Coeff = otherCrtCoeff crtValue.R = productOfPrimes otherPrimeFactors[i] = otherPrimeFactor productOfPrimes = new(big.Int).Mul(productOfPrimes, otherPrimeFactor) } privateKey.Primes = append(privateKey.Primes, otherPrimeFactors...) privateKey.Precomputed.CRTValues = crtValues } key := &rsaPrivateKey{ rsaPublicKey: *publicKey, PrivateKey: privateKey, } return key, nil } /* * Key Generation Functions. */ func generateRSAPrivateKey(bits int) (k *rsaPrivateKey, err error) { k = new(rsaPrivateKey) k.PrivateKey, err = rsa.GenerateKey(rand.Reader, bits) if err != nil { return nil, err } k.rsaPublicKey.PublicKey = &k.PrivateKey.PublicKey k.extended = make(map[string]interface{}) return } // GenerateRSA2048PrivateKey generates a key pair using 2048-bit RSA. func GenerateRSA2048PrivateKey() (PrivateKey, error) { k, err := generateRSAPrivateKey(2048) if err != nil { return nil, fmt.Errorf("error generating RSA 2048-bit key: %s", err) } return k, nil } // GenerateRSA3072PrivateKey generates a key pair using 3072-bit RSA. func GenerateRSA3072PrivateKey() (PrivateKey, error) { k, err := generateRSAPrivateKey(3072) if err != nil { return nil, fmt.Errorf("error generating RSA 3072-bit key: %s", err) } return k, nil } // GenerateRSA4096PrivateKey generates a key pair using 4096-bit RSA. func GenerateRSA4096PrivateKey() (PrivateKey, error) { k, err := generateRSAPrivateKey(4096) if err != nil { return nil, fmt.Errorf("error generating RSA 4096-bit key: %s", err) } return k, nil } ================================================ FILE: vendor/github.com/containers/libtrust/util.go ================================================ package libtrust import ( "bytes" "crypto" "crypto/elliptic" "crypto/tls" "crypto/x509" "encoding/base32" "encoding/base64" "encoding/binary" "encoding/pem" "errors" "fmt" "math/big" "net/url" "os" "path/filepath" "strings" "time" ) // LoadOrCreateTrustKey will load a PrivateKey from the specified path func LoadOrCreateTrustKey(trustKeyPath string) (PrivateKey, error) { if err := os.MkdirAll(filepath.Dir(trustKeyPath), 0700); err != nil { return nil, err } trustKey, err := LoadKeyFile(trustKeyPath) if err == ErrKeyFileDoesNotExist { trustKey, err = GenerateECP256PrivateKey() if err != nil { return nil, fmt.Errorf("error generating key: %s", err) } if err := SaveKey(trustKeyPath, trustKey); err != nil { return nil, fmt.Errorf("error saving key file: %s", err) } dir, file := filepath.Split(trustKeyPath) if err := SavePublicKey(filepath.Join(dir, "public-"+file), trustKey.PublicKey()); err != nil { return nil, fmt.Errorf("error saving public key file: %s", err) } } else if err != nil { return nil, fmt.Errorf("error loading key file: %s", err) } return trustKey, nil } // NewIdentityAuthTLSClientConfig returns a tls.Config configured to use identity // based authentication from the specified dockerUrl, the rootConfigPath and // the server name to which it is connecting. // If trustUnknownHosts is true it will automatically add the host to the // known-hosts.json in rootConfigPath. func NewIdentityAuthTLSClientConfig(dockerUrl string, trustUnknownHosts bool, rootConfigPath string, serverName string) (*tls.Config, error) { tlsConfig := newTLSConfig() trustKeyPath := filepath.Join(rootConfigPath, "key.json") knownHostsPath := filepath.Join(rootConfigPath, "known-hosts.json") u, err := url.Parse(dockerUrl) if err != nil { return nil, fmt.Errorf("unable to parse machine url") } if u.Scheme == "unix" { return nil, nil } addr := u.Host proto := "tcp" trustKey, err := LoadOrCreateTrustKey(trustKeyPath) if err != nil { return nil, fmt.Errorf("unable to load trust key: %s", err) } knownHosts, err := LoadKeySetFile(knownHostsPath) if err != nil { return nil, fmt.Errorf("could not load trusted hosts file: %s", err) } allowedHosts, err := FilterByHosts(knownHosts, addr, false) if err != nil { return nil, fmt.Errorf("error filtering hosts: %s", err) } certPool, err := GenerateCACertPool(trustKey, allowedHosts) if err != nil { return nil, fmt.Errorf("Could not create CA pool: %s", err) } tlsConfig.ServerName = serverName tlsConfig.RootCAs = certPool x509Cert, err := GenerateSelfSignedClientCert(trustKey) if err != nil { return nil, fmt.Errorf("certificate generation error: %s", err) } tlsConfig.Certificates = []tls.Certificate{{ Certificate: [][]byte{x509Cert.Raw}, PrivateKey: trustKey.CryptoPrivateKey(), Leaf: x509Cert, }} tlsConfig.InsecureSkipVerify = true testConn, err := tls.Dial(proto, addr, tlsConfig) if err != nil { return nil, fmt.Errorf("tls Handshake error: %s", err) } opts := x509.VerifyOptions{ Roots: tlsConfig.RootCAs, CurrentTime: time.Now(), DNSName: tlsConfig.ServerName, Intermediates: x509.NewCertPool(), } certs := testConn.ConnectionState().PeerCertificates for i, cert := range certs { if i == 0 { continue } opts.Intermediates.AddCert(cert) } if _, err := certs[0].Verify(opts); err != nil { if _, ok := err.(x509.UnknownAuthorityError); ok { if trustUnknownHosts { pubKey, err := FromCryptoPublicKey(certs[0].PublicKey) if err != nil { return nil, fmt.Errorf("error extracting public key from cert: %s", err) } pubKey.AddExtendedField("hosts", []string{addr}) if err := AddKeySetFile(knownHostsPath, pubKey); err != nil { return nil, fmt.Errorf("error adding machine to known hosts: %s", err) } } else { return nil, fmt.Errorf("unable to connect. unknown host: %s", addr) } } } testConn.Close() tlsConfig.InsecureSkipVerify = false return tlsConfig, nil } // joseBase64UrlEncode encodes the given data using the standard base64 url // encoding format but with all trailing '=' characters omitted in accordance // with the jose specification. // http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31#section-2 func joseBase64UrlEncode(b []byte) string { return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=") } // joseBase64UrlDecode decodes the given string using the standard base64 url // decoder but first adds the appropriate number of trailing '=' characters in // accordance with the jose specification. // http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31#section-2 func joseBase64UrlDecode(s string) ([]byte, error) { s = strings.Replace(s, "\n", "", -1) s = strings.Replace(s, " ", "", -1) switch len(s) % 4 { case 0: case 2: s += "==" case 3: s += "=" default: return nil, errors.New("illegal base64url string") } return base64.URLEncoding.DecodeString(s) } func keyIDEncode(b []byte) string { s := strings.TrimRight(base32.StdEncoding.EncodeToString(b), "=") var buf bytes.Buffer var i int for i = 0; i < len(s)/4-1; i++ { start := i * 4 end := start + 4 buf.WriteString(s[start:end] + ":") } buf.WriteString(s[i*4:]) return buf.String() } func keyIDFromCryptoKey(pubKey PublicKey) string { // Generate and return a 'libtrust' fingerprint of the public key. // For an RSA key this should be: // SHA256(DER encoded ASN1) // Then truncated to 240 bits and encoded into 12 base32 groups like so: // ABCD:EFGH:IJKL:MNOP:QRST:UVWX:YZ23:4567:ABCD:EFGH:IJKL:MNOP derBytes, err := x509.MarshalPKIXPublicKey(pubKey.CryptoPublicKey()) if err != nil { return "" } hasher := crypto.SHA256.New() hasher.Write(derBytes) return keyIDEncode(hasher.Sum(nil)[:30]) } func stringFromMap(m map[string]interface{}, key string) (string, error) { val, ok := m[key] if !ok { return "", fmt.Errorf("%q value not specified", key) } str, ok := val.(string) if !ok { return "", fmt.Errorf("%q value must be a string", key) } delete(m, key) return str, nil } func parseECCoordinate(cB64Url string, curve elliptic.Curve) (*big.Int, error) { curveByteLen := (curve.Params().BitSize + 7) >> 3 cBytes, err := joseBase64UrlDecode(cB64Url) if err != nil { return nil, fmt.Errorf("invalid base64 URL encoding: %s", err) } cByteLength := len(cBytes) if cByteLength != curveByteLen { return nil, fmt.Errorf("invalid number of octets: got %d, should be %d", cByteLength, curveByteLen) } return new(big.Int).SetBytes(cBytes), nil } func parseECPrivateParam(dB64Url string, curve elliptic.Curve) (*big.Int, error) { dBytes, err := joseBase64UrlDecode(dB64Url) if err != nil { return nil, fmt.Errorf("invalid base64 URL encoding: %s", err) } // The length of this octet string MUST be ceiling(log-base-2(n)/8) // octets (where n is the order of the curve). This is because the private // key d must be in the interval [1, n-1] so the bitlength of d should be // no larger than the bitlength of n-1. The easiest way to find the octet // length is to take bitlength(n-1), add 7 to force a carry, and shift this // bit sequence right by 3, which is essentially dividing by 8 and adding // 1 if there is any remainder. Thus, the private key value d should be // output to (bitlength(n-1)+7)>>3 octets. n := curve.Params().N octetLength := (new(big.Int).Sub(n, big.NewInt(1)).BitLen() + 7) >> 3 dByteLength := len(dBytes) if dByteLength != octetLength { return nil, fmt.Errorf("invalid number of octets: got %d, should be %d", dByteLength, octetLength) } return new(big.Int).SetBytes(dBytes), nil } func parseRSAModulusParam(nB64Url string) (*big.Int, error) { nBytes, err := joseBase64UrlDecode(nB64Url) if err != nil { return nil, fmt.Errorf("invalid base64 URL encoding: %s", err) } return new(big.Int).SetBytes(nBytes), nil } func serializeRSAPublicExponentParam(e int) []byte { // We MUST use the minimum number of octets to represent E. // E is supposed to be 65537 for performance and security reasons // and is what golang's rsa package generates, but it might be // different if imported from some other generator. buf := make([]byte, 4) binary.BigEndian.PutUint32(buf, uint32(e)) var i int for i = 0; i < 8; i++ { if buf[i] != 0 { break } } return buf[i:] } func parseRSAPublicExponentParam(eB64Url string) (int, error) { eBytes, err := joseBase64UrlDecode(eB64Url) if err != nil { return 0, fmt.Errorf("invalid base64 URL encoding: %s", err) } // Only the minimum number of bytes were used to represent E, but // binary.BigEndian.Uint32 expects at least 4 bytes, so we need // to add zero padding if necassary. byteLen := len(eBytes) buf := make([]byte, 4-byteLen, 4) eBytes = append(buf, eBytes...) return int(binary.BigEndian.Uint32(eBytes)), nil } func parseRSAPrivateKeyParamFromMap(m map[string]interface{}, key string) (*big.Int, error) { b64Url, err := stringFromMap(m, key) if err != nil { return nil, err } paramBytes, err := joseBase64UrlDecode(b64Url) if err != nil { return nil, fmt.Errorf("invaled base64 URL encoding: %s", err) } return new(big.Int).SetBytes(paramBytes), nil } func createPemBlock(name string, derBytes []byte, headers map[string]interface{}) (*pem.Block, error) { pemBlock := &pem.Block{Type: name, Bytes: derBytes, Headers: map[string]string{}} for k, v := range headers { switch val := v.(type) { case string: pemBlock.Headers[k] = val case []string: if k == "hosts" { pemBlock.Headers[k] = strings.Join(val, ",") } else { // Return error, non-encodable type } default: // Return error, non-encodable type } } return pemBlock, nil } func pubKeyFromPEMBlock(pemBlock *pem.Block) (PublicKey, error) { cryptoPublicKey, err := x509.ParsePKIXPublicKey(pemBlock.Bytes) if err != nil { return nil, fmt.Errorf("unable to decode Public Key PEM data: %s", err) } pubKey, err := FromCryptoPublicKey(cryptoPublicKey) if err != nil { return nil, err } addPEMHeadersToKey(pemBlock, pubKey) return pubKey, nil } func addPEMHeadersToKey(pemBlock *pem.Block, pubKey PublicKey) { for key, value := range pemBlock.Headers { var safeVal interface{} if key == "hosts" { safeVal = strings.Split(value, ",") } else { safeVal = value } pubKey.AddExtendedField(key, safeVal) } } ================================================ FILE: vendor/github.com/containers/luksy/.cirrus.yml ================================================ docker_builder: name: CI env: HOME: /root DEBIAN_FRONTEND: noninteractive CIRRUS_LOG_TIMESTAMP: true GOVERSION: 1.23 PATH: /usr/lib/go-1.23/bin:$PATH setup_script: | apt-get -q update apt-get -q install -y bats cryptsetup golang-${GOVERSION} go version make unit_test_script: | go test -timeout 45m -v -cover case $(go env GOARCH) in amd64) otherarch=386;; arm64) otherarch=arm;; mips64) otherarch=mips;; mips64le) otherarch=mipsle;; esac if test -n "$otherarch" ; then echo running unit tests again with GOARCH=$otherarch GOARCH=$otherarch go test -timeout 45m -v -cover fi : defaults_script: | bats -f defaults ./tests aes_script: | bats -f aes ./tests ================================================ FILE: vendor/github.com/containers/luksy/.dockerignore ================================================ lukstool lukstool.test ================================================ FILE: vendor/github.com/containers/luksy/.gitignore ================================================ # If you prefer the allow list template instead of the deny list, see community template: # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore # # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ # Go workspace file go.work ================================================ FILE: vendor/github.com/containers/luksy/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: vendor/github.com/containers/luksy/Makefile ================================================ GO = go BATS = bats all: luksy luksy: cmd/luksy/*.go *.go $(GO) build -o luksy$(shell go env GOEXE) ./cmd/luksy clean: $(RM) luksy$(shell go env GOEXE) luksy.test test: $(GO) test -timeout 45m -v -cover $(BATS) ./tests ================================================ FILE: vendor/github.com/containers/luksy/OWNERS ================================================ approvers: - nalind reviewers: - nalind ================================================ FILE: vendor/github.com/containers/luksy/README.md ================================================ luksy: offline encryption/decryption using LUKS formats [![Cirrus CI Status](https://img.shields.io/cirrus/github/containers/luksy/main)](https://cirrus-ci.com/github/containers/luksy/main) - luksy implements encryption and decryption using LUKSv1 and LUKSv2 formats. Think of it as a clunkier cousin of gzip/bzip2/xz that doesn't actually produce smaller output than input, but it encrypts, and that's nice. * The main goal is to be able to encrypt/decrypt when we don't have access to the Linux device mapper. Duplicating functions of cryptsetup that it can perform without accessing the Linux device mapper is not a priority. * If you can use cryptsetup instead, use cryptsetup instead. ================================================ FILE: vendor/github.com/containers/luksy/decrypt.go ================================================ package luksy import ( "bytes" "errors" "fmt" "io" "os" "strconv" "golang.org/x/crypto/argon2" "golang.org/x/crypto/pbkdf2" ) // ReaderAtSeekCloser is a combination of io.ReaderAt, io.Seeker, and io.Closer, // which is all we really need from an encrypted file. type ReaderAtSeekCloser interface { io.ReaderAt io.Seeker io.Closer } // Decrypt attempts to verify the specified password using information from the // header and read from the specified file. // // Returns a function which will decrypt payload blocks in succession, the size // of chunks of data that the function expects, the offset in the file where // the payload begins, and the size of the payload, assuming the payload runs // to the end of the file. func (h V1Header) Decrypt(password string, f ReaderAtSeekCloser) (func([]byte) ([]byte, error), int, int64, int64, error) { size, err := f.Seek(0, io.SeekEnd) if err != nil { return nil, -1, -1, -1, err } hasher, err := hasherByName(h.HashSpec()) if err != nil { return nil, -1, -1, -1, fmt.Errorf("unsupported digest algorithm %q: %w", h.HashSpec(), err) } activeKeys := 0 for k := 0; k < v1NumKeys; k++ { keyslot, err := h.KeySlot(k) if err != nil { return nil, -1, -1, -1, fmt.Errorf("reading key slot %d: %w", k, err) } active, err := keyslot.Active() if err != nil { return nil, -1, -1, -1, fmt.Errorf("checking if key slot %d is active: %w", k, err) } if !active { continue } activeKeys++ passwordDerived := pbkdf2.Key([]byte(password), keyslot.KeySlotSalt(), int(keyslot.Iterations()), int(h.KeyBytes()), hasher) striped := make([]byte, h.KeyBytes()*keyslot.Stripes()) n, err := f.ReadAt(striped, int64(keyslot.KeyMaterialOffset())*V1SectorSize) if err != nil { return nil, -1, -1, -1, fmt.Errorf("reading diffuse material for keyslot %d: %w", k, err) } if n != len(striped) { return nil, -1, -1, -1, fmt.Errorf("short read while reading diffuse material for keyslot %d: expected %d, got %d", k, len(striped), n) } splitKey, err := v1decrypt(h.CipherName(), h.CipherMode(), 0, passwordDerived, striped, V1SectorSize, false) if err != nil { fmt.Fprintf(os.Stderr, "error attempting to decrypt main key: %v\n", err) continue } mkCandidate, err := afMerge(splitKey, hasher(), int(h.KeyBytes()), int(keyslot.Stripes())) if err != nil { fmt.Fprintf(os.Stderr, "error attempting to compute main key: %v\n", err) continue } mkcandidateDerived := pbkdf2.Key(mkCandidate, h.MKDigestSalt(), int(h.MKDigestIter()), v1DigestSize, hasher) ivTweak := 0 decryptStream := func(ciphertext []byte) ([]byte, error) { plaintext, err := v1decrypt(h.CipherName(), h.CipherMode(), ivTweak, mkCandidate, ciphertext, V1SectorSize, false) ivTweak += len(ciphertext) / V1SectorSize return plaintext, err } if bytes.Equal(mkcandidateDerived, h.MKDigest()) { payloadOffset := int64(h.PayloadOffset() * V1SectorSize) return decryptStream, V1SectorSize, payloadOffset, size - payloadOffset, nil } } if activeKeys == 0 { return nil, -1, -1, -1, errors.New("no passwords set on LUKS1 volume") } return nil, -1, -1, -1, errors.New("decryption error: incorrect password") } // Decrypt attempts to verify the specified password using information from the // header, JSON block, and read from the specified file. // // Returns a function which will decrypt payload blocks in succession, the size // of chunks of data that the function expects, the offset in the file where // the payload begins, and the size of the payload, assuming the payload runs // to the end of the file. func (h V2Header) Decrypt(password string, f ReaderAtSeekCloser, j V2JSON) (func([]byte) ([]byte, error), int, int64, int64, error) { foundDigests := 0 for d, digest := range j.Digests { if digest.Type != "pbkdf2" { continue } if digest.V2JSONDigestPbkdf2 == nil { return nil, -1, -1, -1, fmt.Errorf("digest %q is corrupt: no pbkdf2 parameters", d) } foundDigests++ if len(digest.Segments) == 0 || len(digest.Digest) == 0 { continue } payloadOffset := int64(-1) payloadSectorSize := V1SectorSize payloadEncryption := "" payloadSize := int64(0) ivTweak := 0 for _, segmentID := range digest.Segments { segment, ok := j.Segments[segmentID] if !ok { continue // well, that was misleading } if segment.Type != "crypt" { continue } tmp, err := strconv.ParseInt(segment.Offset, 10, 64) if err != nil { continue } payloadOffset = tmp if segment.Size == "dynamic" { size, err := f.Seek(0, io.SeekEnd) if err != nil { continue } payloadSize = size - payloadOffset } else { payloadSize, err = strconv.ParseInt(segment.Size, 10, 64) if err != nil { continue } } payloadSectorSize = segment.SectorSize payloadEncryption = segment.Encryption ivTweak = segment.IVTweak break } if payloadEncryption == "" { continue } activeKeys := 0 for k, keyslot := range j.Keyslots { if keyslot.Priority != nil && *keyslot.Priority == V2JSONKeyslotPriorityIgnore { continue } applicable := true if len(digest.Keyslots) > 0 { applicable = false for i := 0; i < len(digest.Keyslots); i++ { if k == digest.Keyslots[i] { applicable = true break } } } if !applicable { continue } if keyslot.Type != "luks2" { continue } if keyslot.V2JSONKeyslotLUKS2 == nil { return nil, -1, -1, -1, fmt.Errorf("key slot %q is corrupt", k) } if keyslot.V2JSONKeyslotLUKS2.AF.Type != "luks1" { continue } if keyslot.V2JSONKeyslotLUKS2.AF.V2JSONAFLUKS1 == nil { return nil, -1, -1, -1, fmt.Errorf("key slot %q is corrupt: no AF parameters", k) } if keyslot.Area.Type != "raw" { return nil, -1, -1, -1, fmt.Errorf("key slot %q is corrupt: key data area is not raw", k) } if keyslot.Area.KeySize*V2SectorSize < keyslot.KeySize*keyslot.AF.Stripes { return nil, -1, -1, -1, fmt.Errorf("key slot %q is corrupt: key data area is too small (%d < %d)", k, keyslot.Area.KeySize*V2SectorSize, keyslot.KeySize*keyslot.AF.Stripes) } var passwordDerived []byte switch keyslot.V2JSONKeyslotLUKS2.Kdf.Type { default: continue case "pbkdf2": if keyslot.V2JSONKeyslotLUKS2.Kdf.V2JSONKdfPbkdf2 == nil { return nil, -1, -1, -1, fmt.Errorf("key slot %q is corrupt: no pbkdf2 parameters", k) } hasher, err := hasherByName(keyslot.Kdf.Hash) if err != nil { return nil, -1, -1, -1, fmt.Errorf("unsupported digest algorithm %q: %w", keyslot.Kdf.Hash, err) } passwordDerived = pbkdf2.Key([]byte(password), keyslot.Kdf.Salt, keyslot.Kdf.Iterations, keyslot.KeySize, hasher) case "argon2i": if keyslot.V2JSONKeyslotLUKS2.Kdf.V2JSONKdfArgon2i == nil { return nil, -1, -1, -1, fmt.Errorf("key slot %q is corrupt: no argon2i parameters", k) } passwordDerived = argon2.Key([]byte(password), keyslot.Kdf.Salt, uint32(keyslot.Kdf.Time), uint32(keyslot.Kdf.Memory), uint8(keyslot.Kdf.CPUs), uint32(keyslot.KeySize)) case "argon2id": if keyslot.V2JSONKeyslotLUKS2.Kdf.V2JSONKdfArgon2i == nil { return nil, -1, -1, -1, fmt.Errorf("key slot %q is corrupt: no argon2id parameters", k) } passwordDerived = argon2.IDKey([]byte(password), keyslot.Kdf.Salt, uint32(keyslot.Kdf.Time), uint32(keyslot.Kdf.Memory), uint8(keyslot.Kdf.CPUs), uint32(keyslot.KeySize)) } striped := make([]byte, keyslot.KeySize*keyslot.AF.Stripes) n, err := f.ReadAt(striped, int64(keyslot.Area.Offset)) if err != nil { return nil, -1, -1, -1, fmt.Errorf("reading diffuse material for keyslot %q: %w", k, err) } if n != len(striped) { return nil, -1, -1, -1, fmt.Errorf("short read while reading diffuse material for keyslot %q: expected %d, got %d", k, len(striped), n) } splitKey, err := v2decrypt(keyslot.Area.Encryption, 0, passwordDerived, striped, V1SectorSize, false) if err != nil { fmt.Fprintf(os.Stderr, "error attempting to decrypt main key: %v\n", err) continue } afhasher, err := hasherByName(keyslot.AF.Hash) if err != nil { return nil, -1, -1, -1, fmt.Errorf("unsupported digest algorithm %q: %w", keyslot.AF.Hash, err) } mkCandidate, err := afMerge(splitKey, afhasher(), int(keyslot.KeySize), int(keyslot.AF.Stripes)) if err != nil { fmt.Fprintf(os.Stderr, "error attempting to compute main key: %v\n", err) continue } digester, err := hasherByName(digest.Hash) if err != nil { return nil, -1, -1, -1, fmt.Errorf("unsupported digest algorithm %q: %w", digest.Hash, err) } mkcandidateDerived := pbkdf2.Key(mkCandidate, digest.Salt, digest.Iterations, len(digest.Digest), digester) decryptStream := func(ciphertext []byte) ([]byte, error) { plaintext, err := v2decrypt(payloadEncryption, ivTweak, mkCandidate, ciphertext, payloadSectorSize, true) ivTweak += len(ciphertext) / payloadSectorSize return plaintext, err } if bytes.Equal(mkcandidateDerived, digest.Digest) { return decryptStream, payloadSectorSize, payloadOffset, payloadSize, nil } activeKeys++ } if activeKeys == 0 { return nil, -1, -1, -1, fmt.Errorf("no passwords set on LUKS2 volume for digest %q", d) } } if foundDigests == 0 { return nil, -1, -1, -1, errors.New("no usable password-verification digests set on LUKS2 volume") } return nil, -1, -1, -1, errors.New("decryption error: incorrect password") } ================================================ FILE: vendor/github.com/containers/luksy/encrypt.go ================================================ package luksy import ( "crypto/rand" "encoding/json" "errors" "fmt" "strconv" "strings" "github.com/google/uuid" "golang.org/x/crypto/argon2" "golang.org/x/crypto/pbkdf2" ) // EncryptV1 prepares to encrypt data using one or more passwords and the // specified cipher (or a default, if the specified cipher is ""). // // Returns a fixed LUKSv1 header which contains keying information, a function // which will encrypt blocks of data in succession, and the size of chunks of // data that it expects. func EncryptV1(password []string, cipher string) ([]byte, func([]byte) ([]byte, error), int, error) { if len(password) == 0 { return nil, nil, -1, errors.New("at least one password is required") } if len(password) > v1NumKeys { return nil, nil, -1, fmt.Errorf("attempted to use %d passwords, only %d possible", len(password), v1NumKeys) } if cipher == "" { cipher = "aes-xts-plain64" } salt := make([]byte, v1SaltSize) n, err := rand.Read(salt) if err != nil { return nil, nil, -1, fmt.Errorf("reading random data: %w", err) } if n != len(salt) { return nil, nil, -1, errors.New("short read") } cipherSpec := strings.SplitN(cipher, "-", 3) if len(cipherSpec) != 3 || len(cipherSpec[0]) == 0 || len(cipherSpec[1]) == 0 || len(cipherSpec[2]) == 0 { return nil, nil, -1, fmt.Errorf("invalid cipher %q", cipher) } var h V1Header if err := h.SetMagic(V1Magic); err != nil { return nil, nil, -1, fmt.Errorf("setting magic to v1: %w", err) } if err := h.SetVersion(1); err != nil { return nil, nil, -1, fmt.Errorf("setting version to 1: %w", err) } h.SetCipherName(cipherSpec[0]) h.SetCipherMode(cipherSpec[1] + "-" + cipherSpec[2]) h.SetHashSpec("sha256") h.SetKeyBytes(32) if cipherSpec[1] == "xts" { h.SetKeyBytes(64) } h.SetMKDigestSalt(salt) h.SetMKDigestIter(V1Stripes) h.SetUUID(uuid.NewString()) mkey := make([]byte, h.KeyBytes()) n, err = rand.Read(mkey) if err != nil { return nil, nil, -1, fmt.Errorf("reading random data: %w", err) } if n != len(mkey) { return nil, nil, -1, errors.New("short read") } hasher, err := hasherByName(h.HashSpec()) if err != nil { return nil, nil, -1, errors.New("internal error") } mkdigest := pbkdf2.Key(mkey, h.MKDigestSalt(), int(h.MKDigestIter()), v1DigestSize, hasher) h.SetMKDigest(mkdigest) headerLength := roundUpToMultiple(v1HeaderStructSize, V1AlignKeyslots) iterations := IterationsPBKDF2(salt, int(h.KeyBytes()), hasher) var stripes [][]byte ksSalt := make([]byte, v1KeySlotSaltLength) for i := 0; i < v1NumKeys; i++ { n, err = rand.Read(ksSalt) if err != nil { return nil, nil, -1, fmt.Errorf("reading random data: %w", err) } if n != len(ksSalt) { return nil, nil, -1, errors.New("short read") } var keyslot V1KeySlot keyslot.SetActive(i < len(password)) keyslot.SetIterations(uint32(iterations)) keyslot.SetStripes(V1Stripes) keyslot.SetKeySlotSalt(ksSalt) if i < len(password) { splitKey, err := afSplit(mkey, hasher(), int(h.MKDigestIter())) if err != nil { return nil, nil, -1, fmt.Errorf("splitting key: %w", err) } passwordDerived := pbkdf2.Key([]byte(password[i]), keyslot.KeySlotSalt(), int(keyslot.Iterations()), int(h.KeyBytes()), hasher) striped, err := v1encrypt(h.CipherName(), h.CipherMode(), 0, passwordDerived, splitKey, V1SectorSize, false) if err != nil { return nil, nil, -1, fmt.Errorf("encrypting split key with password: %w", err) } if len(striped) != len(mkey)*int(keyslot.Stripes()) { return nil, nil, -1, fmt.Errorf("internal error: got %d stripe bytes, expected %d", len(striped), len(mkey)*int(keyslot.Stripes())) } stripes = append(stripes, striped) } keyslot.SetKeyMaterialOffset(uint32(headerLength / V1SectorSize)) if err := h.SetKeySlot(i, keyslot); err != nil { return nil, nil, -1, fmt.Errorf("internal error: setting value for key slot %d: %w", i, err) } headerLength += len(mkey) * int(keyslot.Stripes()) headerLength = roundUpToMultiple(headerLength, V1AlignKeyslots) } headerLength = roundUpToMultiple(headerLength, V1SectorSize) h.SetPayloadOffset(uint32(headerLength / V1SectorSize)) head := make([]byte, headerLength) offset := copy(head, h[:]) offset = roundUpToMultiple(offset, V1AlignKeyslots) for _, stripe := range stripes { copy(head[offset:], stripe) offset = roundUpToMultiple(offset+len(stripe), V1AlignKeyslots) } ivTweak := 0 encryptStream := func(plaintext []byte) ([]byte, error) { ciphertext, err := v1encrypt(h.CipherName(), h.CipherMode(), ivTweak, mkey, plaintext, V1SectorSize, true) ivTweak += len(plaintext) / V1SectorSize return ciphertext, err } return head, encryptStream, V1SectorSize, nil } // EncryptV2 prepares to encrypt data using one or more passwords and the // specified cipher (or a default, if the specified cipher is ""). // // Returns a fixed LUKSv2 header which contains keying information, a // function which will encrypt blocks of data in succession, and the size of // chunks of data that it expects. func EncryptV2(password []string, cipher string, payloadSectorSize int) ([]byte, func([]byte) ([]byte, error), int, error) { if len(password) == 0 { return nil, nil, -1, errors.New("at least one password is required") } if cipher == "" { cipher = "aes-xts-plain64" } cipherSpec := strings.SplitN(cipher, "-", 3) if len(cipherSpec) != 3 || len(cipherSpec[0]) == 0 || len(cipherSpec[1]) == 0 || len(cipherSpec[2]) == 0 { return nil, nil, -1, fmt.Errorf("invalid cipher %q", cipher) } if payloadSectorSize == 0 { payloadSectorSize = V2SectorSize } switch payloadSectorSize { default: return nil, nil, -1, fmt.Errorf("invalid sector size %d", payloadSectorSize) case 512, 1024, 2048, 4096: } headerSalts := make([]byte, v1SaltSize*3) n, err := rand.Read(headerSalts) if err != nil { return nil, nil, -1, err } if n != len(headerSalts) { return nil, nil, -1, errors.New("short read") } hSalt1 := headerSalts[:v1SaltSize] hSalt2 := headerSalts[v1SaltSize : v1SaltSize*2] mkeySalt := headerSalts[v1SaltSize*2:] roundHeaderSize := func(size int) (int, error) { switch { case size < 0x4000: return 0x4000, nil case size < 0x8000: return 0x8000, nil case size < 0x10000: return 0x10000, nil case size < 0x20000: return 0x20000, nil case size < 0x40000: return 0x40000, nil case size < 0x80000: return 0x80000, nil case size < 0x100000: return 0x100000, nil case size < 0x200000: return 0x200000, nil case size < 0x400000: return 0x400000, nil } return 0, fmt.Errorf("internal error: unsupported header size %d", size) } var h1, h2 V2Header if err := h1.SetMagic(V2Magic1); err != nil { return nil, nil, -1, fmt.Errorf("setting magic to v2: %w", err) } if err := h2.SetMagic(V2Magic2); err != nil { return nil, nil, -1, fmt.Errorf("setting magic to v2: %w", err) } if err := h1.SetVersion(2); err != nil { return nil, nil, -1, fmt.Errorf("setting version to 2: %w", err) } if err := h2.SetVersion(2); err != nil { return nil, nil, -1, fmt.Errorf("setting version to 2: %w", err) } h1.SetSequenceID(1) h2.SetSequenceID(1) h1.SetLabel("") h2.SetLabel("") h1.SetChecksumAlgorithm("sha256") h2.SetChecksumAlgorithm("sha256") h1.SetSalt(hSalt1) h2.SetSalt(hSalt2) uuidString := uuid.NewString() h1.SetUUID(uuidString) h2.SetUUID(uuidString) h1.SetHeaderOffset(0) h2.SetHeaderOffset(0) h1.SetChecksum(nil) h2.SetChecksum(nil) mkey := make([]byte, 32) if cipherSpec[1] == "xts" { mkey = make([]byte, 64) } n, err = rand.Read(mkey) if err != nil { return nil, nil, -1, fmt.Errorf("reading random data: %w", err) } if n != len(mkey) { return nil, nil, -1, errors.New("short read") } tuningSalt := make([]byte, v1SaltSize) hasher, err := hasherByName(h1.ChecksumAlgorithm()) if err != nil { return nil, nil, -1, errors.New("internal error") } iterations := IterationsPBKDF2(tuningSalt, len(mkey), hasher) timeCost := 16 threadsCost := 16 memoryCost := MemoryCostArgon2(tuningSalt, len(mkey), timeCost, threadsCost) priority := V2JSONKeyslotPriorityNormal var stripes [][]byte var keyslots []V2JSONKeyslot mdigest := pbkdf2.Key(mkey, mkeySalt, iterations, len(hasher().Sum([]byte{})), hasher) digest0 := V2JSONDigest{ Type: "pbkdf2", Salt: mkeySalt, Digest: mdigest, Segments: []string{"0"}, V2JSONDigestPbkdf2: &V2JSONDigestPbkdf2{ Hash: h1.ChecksumAlgorithm(), Iterations: iterations, }, } for i := range password { keyslotSalt := make([]byte, v1SaltSize) n, err := rand.Read(keyslotSalt) if err != nil { return nil, nil, -1, err } if n != len(keyslotSalt) { return nil, nil, -1, errors.New("short read") } key := argon2.Key([]byte(password[i]), keyslotSalt, uint32(timeCost), uint32(memoryCost), uint8(threadsCost), uint32(len(mkey))) split, err := afSplit(mkey, hasher(), V2Stripes) if err != nil { return nil, nil, -1, fmt.Errorf("splitting: %w", err) } striped, err := v2encrypt(cipher, 0, key, split, V1SectorSize, false) if err != nil { return nil, nil, -1, fmt.Errorf("encrypting: %w", err) } stripes = append(stripes, striped) keyslot := V2JSONKeyslot{ Type: "luks2", KeySize: len(mkey), Area: V2JSONArea{ Type: "raw", Offset: 10000000, // gets updated later Size: int64(roundUpToMultiple(len(striped), V2AlignKeyslots)), V2JSONAreaRaw: &V2JSONAreaRaw{ Encryption: cipher, KeySize: len(key), }, }, Priority: &priority, V2JSONKeyslotLUKS2: &V2JSONKeyslotLUKS2{ AF: V2JSONAF{ Type: "luks1", V2JSONAFLUKS1: &V2JSONAFLUKS1{ Stripes: V2Stripes, Hash: h1.ChecksumAlgorithm(), }, }, Kdf: V2JSONKdf{ Type: "argon2i", Salt: keyslotSalt, V2JSONKdfArgon2i: &V2JSONKdfArgon2i{ Time: timeCost, Memory: memoryCost, CPUs: threadsCost, }, }, }, } keyslots = append(keyslots, keyslot) digest0.Keyslots = append(digest0.Keyslots, strconv.Itoa(i)) } segment0 := V2JSONSegment{ Type: "crypt", Offset: "10000000", // gets updated later Size: "dynamic", V2JSONSegmentCrypt: &V2JSONSegmentCrypt{ IVTweak: 0, Encryption: cipher, SectorSize: payloadSectorSize, }, } j := V2JSON{ Config: V2JSONConfig{}, Keyslots: map[string]V2JSONKeyslot{}, Digests: map[string]V2JSONDigest{}, Segments: map[string]V2JSONSegment{}, Tokens: map[string]V2JSONToken{}, } rebuild: j.Digests["0"] = digest0 j.Segments["0"] = segment0 encodedJSON, err := json.Marshal(j) if err != nil { return nil, nil, -1, err } headerPlusPaddedJsonSize, err := roundHeaderSize(int(V2SectorSize) /* binary header */ + len(encodedJSON) + 1) if err != nil { return nil, nil, -1, err } if j.Config.JsonSize != headerPlusPaddedJsonSize-V2SectorSize { j.Config.JsonSize = headerPlusPaddedJsonSize - V2SectorSize goto rebuild } if h1.HeaderSize() != uint64(headerPlusPaddedJsonSize) { h1.SetHeaderSize(uint64(headerPlusPaddedJsonSize)) h2.SetHeaderSize(uint64(headerPlusPaddedJsonSize)) h1.SetHeaderOffset(0) h2.SetHeaderOffset(uint64(headerPlusPaddedJsonSize)) goto rebuild } keyslotsOffset := h2.HeaderOffset() * 2 maxKeys := len(password) if maxKeys < 64 { maxKeys = 64 } for i := 0; i < len(password); i++ { oldOffset := keyslots[i].Area.Offset keyslots[i].Area.Offset = int64(keyslotsOffset) + int64(roundUpToMultiple(len(mkey)*V2Stripes, V2AlignKeyslots))*int64(i) j.Keyslots[strconv.Itoa(i)] = keyslots[i] if keyslots[i].Area.Offset != oldOffset { goto rebuild } } keyslotsSize := roundUpToMultiple(len(mkey)*V2Stripes, V2AlignKeyslots) * maxKeys if j.Config.KeyslotsSize != keyslotsSize { j.Config.KeyslotsSize = keyslotsSize goto rebuild } segmentOffsetInt := roundUpToMultiple(int(keyslotsOffset)+j.Config.KeyslotsSize, V2SectorSize) segmentOffset := strconv.Itoa(segmentOffsetInt) if segment0.Offset != segmentOffset { segment0.Offset = segmentOffset goto rebuild } d1 := hasher() h1.SetChecksum(nil) d1.Write(h1[:]) d1.Write(encodedJSON) zeropad := make([]byte, headerPlusPaddedJsonSize-len(h1)-len(encodedJSON)) d1.Write(zeropad) h1.SetChecksum(d1.Sum(nil)) d2 := hasher() h2.SetChecksum(nil) d2.Write(h2[:]) d2.Write(encodedJSON) d1.Write(zeropad) h2.SetChecksum(d2.Sum(nil)) head := make([]byte, segmentOffsetInt) copy(head, h1[:]) copy(head[V2SectorSize:], encodedJSON) copy(head[h2.HeaderOffset():], h2[:]) copy(head[h2.HeaderOffset()+V2SectorSize:], encodedJSON) for i := 0; i < len(password); i++ { iAsString := strconv.Itoa(i) copy(head[j.Keyslots[iAsString].Area.Offset:], stripes[i]) } ivTweak := 0 encryptStream := func(plaintext []byte) ([]byte, error) { ciphertext, err := v2encrypt(cipher, ivTweak, mkey, plaintext, payloadSectorSize, true) ivTweak += len(plaintext) / payloadSectorSize return ciphertext, err } return head, encryptStream, segment0.SectorSize, nil } ================================================ FILE: vendor/github.com/containers/luksy/encryption.go ================================================ package luksy import ( "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha1" "crypto/sha256" "crypto/sha512" "encoding/binary" "errors" "fmt" "hash" "io" "strings" "github.com/aead/serpent" "golang.org/x/crypto/cast5" "golang.org/x/crypto/ripemd160" "golang.org/x/crypto/twofish" "golang.org/x/crypto/xts" ) func v1encrypt(cipherName, cipherMode string, ivTweak int, key []byte, plaintext []byte, sectorSize int, bulk bool) ([]byte, error) { var err error var newBlockCipher func([]byte) (cipher.Block, error) ciphertext := make([]byte, len(plaintext)) switch cipherName { case "aes": newBlockCipher = aes.NewCipher case "twofish": newBlockCipher = func(key []byte) (cipher.Block, error) { return twofish.NewCipher(key) } case "cast5": newBlockCipher = func(key []byte) (cipher.Block, error) { return cast5.NewCipher(key) } case "serpent": newBlockCipher = serpent.NewCipher default: return nil, fmt.Errorf("unsupported cipher %s", cipherName) } if sectorSize == 0 { sectorSize = V1SectorSize } switch sectorSize { default: return nil, fmt.Errorf("invalid sector size %d", sectorSize) case 512, 1024, 2048, 4096: } switch cipherMode { case "ecb": cipher, err := newBlockCipher(key) if err != nil { return nil, fmt.Errorf("initializing encryption: %w", err) } for processed := 0; processed < len(plaintext); processed += cipher.BlockSize() { blockLeft := sectorSize if processed+blockLeft > len(plaintext) { blockLeft = len(plaintext) - processed } cipher.Encrypt(ciphertext[processed:processed+blockLeft], plaintext[processed:processed+blockLeft]) } case "cbc-plain": block, err := newBlockCipher(key) if err != nil { return nil, fmt.Errorf("initializing encryption: %w", err) } for processed := 0; processed < len(plaintext); processed += sectorSize { blockLeft := sectorSize if processed+blockLeft > len(plaintext) { blockLeft = len(plaintext) - processed } ivValue := processed/sectorSize + ivTweak if bulk { // iv_large_sectors is not being used ivValue *= sectorSize / V1SectorSize } iv0 := make([]byte, block.BlockSize()) binary.LittleEndian.PutUint32(iv0, uint32(ivValue)) cipher := cipher.NewCBCEncrypter(block, iv0) cipher.CryptBlocks(ciphertext[processed:processed+blockLeft], plaintext[processed:processed+blockLeft]) } case "cbc-plain64": block, err := newBlockCipher(key) if err != nil { return nil, fmt.Errorf("initializing encryption: %w", err) } for processed := 0; processed < len(plaintext); processed += sectorSize { blockLeft := sectorSize if processed+blockLeft > len(plaintext) { blockLeft = len(plaintext) - processed } ivValue := processed/sectorSize + ivTweak if bulk { // iv_large_sectors is not being used ivValue *= sectorSize / V1SectorSize } iv0 := make([]byte, block.BlockSize()) binary.LittleEndian.PutUint64(iv0, uint64(ivValue)) cipher := cipher.NewCBCEncrypter(block, iv0) cipher.CryptBlocks(ciphertext[processed:processed+blockLeft], plaintext[processed:processed+blockLeft]) } case "cbc-essiv:sha256": hasherName := strings.TrimPrefix(cipherMode, "cbc-essiv:") hasher, err := hasherByName(hasherName) if err != nil { return nil, fmt.Errorf("initializing encryption using hash %s: %w", hasherName, err) } h := hasher() h.Write(key) makeiv, err := newBlockCipher(h.Sum(nil)) if err != nil { return nil, fmt.Errorf("initializing encryption: %w", err) } block, err := newBlockCipher(key) if err != nil { return nil, fmt.Errorf("initializing encryption: %w", err) } for processed := 0; processed < len(plaintext); processed += sectorSize { blockLeft := sectorSize if processed+blockLeft > len(plaintext) { blockLeft = len(plaintext) - processed } ivValue := (processed/sectorSize + ivTweak) if bulk { // iv_large_sectors is not being used ivValue *= sectorSize / V1SectorSize } plain0 := make([]byte, makeiv.BlockSize()) binary.LittleEndian.PutUint64(plain0, uint64(ivValue)) iv0 := make([]byte, makeiv.BlockSize()) makeiv.Encrypt(iv0, plain0) cipher := cipher.NewCBCEncrypter(block, iv0) cipher.CryptBlocks(ciphertext[processed:processed+blockLeft], plaintext[processed:processed+blockLeft]) } case "xts-plain": cipher, err := xts.NewCipher(newBlockCipher, key) if err != nil { return nil, fmt.Errorf("initializing encryption: %w", err) } for processed := 0; processed < len(plaintext); processed += sectorSize { blockLeft := sectorSize if processed+blockLeft > len(plaintext) { blockLeft = len(plaintext) - processed } sector := uint64(processed/sectorSize + ivTweak) if bulk { // iv_large_sectors is not being used sector *= uint64(sectorSize / V1SectorSize) } sector = sector % 0x100000000 cipher.Encrypt(ciphertext[processed:processed+blockLeft], plaintext[processed:processed+blockLeft], sector) } case "xts-plain64": cipher, err := xts.NewCipher(newBlockCipher, key) if err != nil { return nil, fmt.Errorf("initializing encryption: %w", err) } for processed := 0; processed < len(plaintext); processed += sectorSize { blockLeft := sectorSize if processed+blockLeft > len(plaintext) { blockLeft = len(plaintext) - processed } sector := uint64(processed/sectorSize + ivTweak) if bulk { // iv_large_sectors is not being used sector *= uint64(sectorSize / V1SectorSize) } cipher.Encrypt(ciphertext[processed:processed+blockLeft], plaintext[processed:processed+blockLeft], sector) } default: return nil, fmt.Errorf("unsupported cipher mode %s", cipherMode) } if err != nil { return nil, fmt.Errorf("cipher error: %w", err) } return ciphertext, nil } func v1decrypt(cipherName, cipherMode string, ivTweak int, key []byte, ciphertext []byte, sectorSize int, bulk bool) ([]byte, error) { var err error var newBlockCipher func([]byte) (cipher.Block, error) plaintext := make([]byte, len(ciphertext)) switch cipherName { case "aes": newBlockCipher = aes.NewCipher case "twofish": newBlockCipher = func(key []byte) (cipher.Block, error) { return twofish.NewCipher(key) } case "cast5": newBlockCipher = func(key []byte) (cipher.Block, error) { return cast5.NewCipher(key) } case "serpent": newBlockCipher = serpent.NewCipher default: return nil, fmt.Errorf("unsupported cipher %s", cipherName) } if sectorSize == 0 { sectorSize = V1SectorSize } switch sectorSize { default: return nil, fmt.Errorf("invalid sector size %d", sectorSize) case 512, 1024, 2048, 4096: } switch cipherMode { case "ecb": cipher, err := newBlockCipher(key) if err != nil { return nil, fmt.Errorf("initializing decryption: %w", err) } for processed := 0; processed < len(ciphertext); processed += cipher.BlockSize() { blockLeft := sectorSize if processed+blockLeft > len(ciphertext) { blockLeft = len(ciphertext) - processed } cipher.Decrypt(plaintext[processed:processed+blockLeft], ciphertext[processed:processed+blockLeft]) } case "cbc-plain": block, err := newBlockCipher(key) if err != nil { return nil, fmt.Errorf("initializing decryption: %w", err) } for processed := 0; processed < len(plaintext); processed += sectorSize { blockLeft := sectorSize if processed+blockLeft > len(plaintext) { blockLeft = len(plaintext) - processed } ivValue := processed/sectorSize + ivTweak if bulk { // iv_large_sectors is not being used ivValue *= sectorSize / V1SectorSize } iv0 := make([]byte, block.BlockSize()) binary.LittleEndian.PutUint32(iv0, uint32(ivValue)) cipher := cipher.NewCBCDecrypter(block, iv0) cipher.CryptBlocks(plaintext[processed:processed+blockLeft], ciphertext[processed:processed+blockLeft]) } case "cbc-plain64": block, err := newBlockCipher(key) if err != nil { return nil, fmt.Errorf("initializing decryption: %w", err) } for processed := 0; processed < len(plaintext); processed += sectorSize { blockLeft := sectorSize if processed+blockLeft > len(plaintext) { blockLeft = len(plaintext) - processed } ivValue := processed/sectorSize + ivTweak if bulk { // iv_large_sectors is not being used ivValue *= sectorSize / V1SectorSize } iv0 := make([]byte, block.BlockSize()) binary.LittleEndian.PutUint64(iv0, uint64(ivValue)) cipher := cipher.NewCBCDecrypter(block, iv0) cipher.CryptBlocks(plaintext[processed:processed+blockLeft], ciphertext[processed:processed+blockLeft]) } case "cbc-essiv:sha256": hasherName := strings.TrimPrefix(cipherMode, "cbc-essiv:") hasher, err := hasherByName(hasherName) if err != nil { return nil, fmt.Errorf("initializing decryption using hash %s: %w", hasherName, err) } h := hasher() h.Write(key) makeiv, err := newBlockCipher(h.Sum(nil)) if err != nil { return nil, fmt.Errorf("initializing decryption: %w", err) } block, err := newBlockCipher(key) if err != nil { return nil, fmt.Errorf("initializing decryption: %w", err) } for processed := 0; processed < len(plaintext); processed += sectorSize { blockLeft := sectorSize if processed+blockLeft > len(plaintext) { blockLeft = len(plaintext) - processed } ivValue := (processed/sectorSize + ivTweak) if bulk { // iv_large_sectors is not being used ivValue *= sectorSize / V1SectorSize } plain0 := make([]byte, makeiv.BlockSize()) binary.LittleEndian.PutUint64(plain0, uint64(ivValue)) iv0 := make([]byte, makeiv.BlockSize()) makeiv.Encrypt(iv0, plain0) cipher := cipher.NewCBCDecrypter(block, iv0) cipher.CryptBlocks(plaintext[processed:processed+blockLeft], ciphertext[processed:processed+blockLeft]) } case "xts-plain": cipher, err := xts.NewCipher(newBlockCipher, key) if err != nil { return nil, fmt.Errorf("initializing decryption: %w", err) } for processed := 0; processed < len(ciphertext); processed += sectorSize { blockLeft := sectorSize if processed+blockLeft > len(ciphertext) { blockLeft = len(ciphertext) - processed } sector := uint64(processed/sectorSize + ivTweak) if bulk { // iv_large_sectors is not being used sector *= uint64(sectorSize / V1SectorSize) } sector = sector % 0x100000000 cipher.Decrypt(plaintext[processed:processed+blockLeft], ciphertext[processed:processed+blockLeft], sector) } case "xts-plain64": cipher, err := xts.NewCipher(newBlockCipher, key) if err != nil { return nil, fmt.Errorf("initializing decryption: %w", err) } for processed := 0; processed < len(ciphertext); processed += sectorSize { blockLeft := sectorSize if processed+blockLeft > len(ciphertext) { blockLeft = len(ciphertext) - processed } sector := uint64(processed/sectorSize + ivTweak) if bulk { // iv_large_sectors is not being used sector *= uint64(sectorSize / V1SectorSize) } cipher.Decrypt(plaintext[processed:processed+blockLeft], ciphertext[processed:processed+blockLeft], sector) } default: return nil, fmt.Errorf("unsupported cipher mode %s", cipherMode) } if err != nil { return nil, fmt.Errorf("cipher error: %w", err) } return plaintext, nil } func v2encrypt(cipherSuite string, ivTweak int, key []byte, ciphertext []byte, sectorSize int, bulk bool) ([]byte, error) { var cipherName, cipherMode string switch { default: cipherSpec := strings.SplitN(cipherSuite, "-", 2) if len(cipherSpec) < 2 { return nil, fmt.Errorf("unrecognized cipher suite %q", cipherSuite) } cipherName = cipherSpec[0] cipherMode = cipherSpec[1] } return v1encrypt(cipherName, cipherMode, ivTweak, key, ciphertext, sectorSize, bulk) } func v2decrypt(cipherSuite string, ivTweak int, key []byte, ciphertext []byte, sectorSize int, bulk bool) ([]byte, error) { var cipherName, cipherMode string switch { default: cipherSpec := strings.SplitN(cipherSuite, "-", 2) if len(cipherSpec) < 2 { return nil, fmt.Errorf("unrecognized cipher suite %q", cipherSuite) } cipherName = cipherSpec[0] cipherMode = cipherSpec[1] } return v1decrypt(cipherName, cipherMode, ivTweak, key, ciphertext, sectorSize, bulk) } func diffuse(key []byte, h hash.Hash) []byte { sum := make([]byte, len(key)) counter := uint32(0) for summed := 0; summed < len(key); summed += h.Size() { h.Reset() var buf [4]byte binary.BigEndian.PutUint32(buf[:], counter) h.Write(buf[:]) needed := len(key) - summed if needed > h.Size() { needed = h.Size() } h.Write(key[summed : summed+needed]) partial := h.Sum(nil) copy(sum[summed:summed+needed], partial) counter++ } return sum } func afMerge(splitKey []byte, h hash.Hash, keysize int, stripes int) ([]byte, error) { if len(splitKey) != keysize*stripes { return nil, fmt.Errorf("expected %d af bytes, got %d", keysize*stripes, len(splitKey)) } d := make([]byte, keysize) for i := 0; i < stripes-1; i++ { for j := 0; j < keysize; j++ { d[j] = d[j] ^ splitKey[i*keysize+j] } d = diffuse(d, h) } for j := 0; j < keysize; j++ { d[j] = d[j] ^ splitKey[(stripes-1)*keysize+j] } return d, nil } func afSplit(key []byte, h hash.Hash, stripes int) ([]byte, error) { keysize := len(key) s := make([]byte, keysize*stripes) d := make([]byte, keysize) n, err := rand.Read(s[0 : (keysize-1)*stripes]) if err != nil { return nil, err } if n != (keysize-1)*stripes { return nil, fmt.Errorf("short read when attempting to read random data: %d < %d", n, (keysize-1)*stripes) } for i := 0; i < stripes-1; i++ { for j := 0; j < keysize; j++ { d[j] = d[j] ^ s[i*keysize+j] } d = diffuse(d, h) } for j := 0; j < keysize; j++ { s[(stripes-1)*keysize+j] = d[j] ^ key[j] } return s, nil } func roundUpToMultiple(i, factor int) int { if i < 0 { return 0 } if factor < 1 { return i } return i + ((factor - (i % factor)) % factor) } func roundDownToMultiple(i, factor int) int { if i < 0 { return 0 } if factor < 1 { return i } return i - (i % factor) } func hasherByName(name string) (func() hash.Hash, error) { switch name { case "sha1": return sha1.New, nil case "sha256": return sha256.New, nil case "sha512": return sha512.New, nil case "ripemd160": return ripemd160.New, nil default: return nil, fmt.Errorf("unsupported digest algorithm %q", name) } } type wrapper struct { fn func(plaintext []byte) ([]byte, error) blockSize int buf []byte buffered int processed int reader io.Reader eof bool writer io.Writer } func (w *wrapper) partialWrite() error { if w.buffered-w.processed >= w.blockSize { toProcess := roundDownToMultiple(w.buffered-w.processed, w.blockSize) processed, err := w.fn(w.buf[w.processed : w.processed+toProcess]) if err != nil { return err } nProcessed := copy(w.buf[w.processed:], processed) w.processed += nProcessed } if w.processed >= w.blockSize { nWritten, err := w.writer.Write(w.buf[:w.processed]) if err != nil { return err } copy(w.buf, w.buf[nWritten:w.buffered]) w.buffered -= nWritten w.processed -= nWritten if w.processed != 0 { return fmt.Errorf("short write: %d != %d", nWritten, nWritten+w.processed) } } return nil } func (w *wrapper) Write(buf []byte) (int, error) { n := 0 for n < len(buf) { nBuffered := copy(w.buf[w.buffered:], buf[n:]) w.buffered += nBuffered n += nBuffered if err := w.partialWrite(); err != nil { return n, err } } return n, nil } func (w *wrapper) Read(buf []byte) (int, error) { n := 0 for n < len(buf) { if !w.eof { nRead, err := w.reader.Read(w.buf[w.buffered:]) if err != nil { if !errors.Is(err, io.EOF) { w.buffered += nRead return n, err } w.eof = true } w.buffered += nRead } if w.buffered == 0 && w.eof { return n, io.EOF } if w.buffered-w.processed >= w.blockSize { toProcess := roundDownToMultiple(w.buffered-w.processed, w.blockSize) processed, err := w.fn(w.buf[w.processed : w.processed+toProcess]) if err != nil { return n, err } nProcessed := copy(w.buf[w.processed:], processed) w.processed += nProcessed } nRead := copy(buf[n:], w.buf[:w.processed]) n += nRead copy(w.buf, w.buf[nRead:w.buffered]) w.processed -= nRead w.buffered -= nRead if w.buffered-w.processed < w.blockSize { break } } return n, nil } func (w *wrapper) Close() error { if w.writer != nil { if w.buffered%w.blockSize != 0 { nPadding := w.blockSize - w.buffered%w.blockSize nWritten, err := w.Write(make([]byte, nPadding)) if err != nil { return fmt.Errorf("flushing write: %v", err) } if nWritten < nPadding { return fmt.Errorf("flushing write: %d != %d", nPadding, nWritten) } } } return nil } // EncryptWriter creates an io.WriteCloser which buffers writes through an // encryption function, transforming and writing multiples of the blockSize. // After writing a final block, the returned writer should be closed. // If only a partial block has been written when Close() is called, a final // block with its length padded with zero bytes will be transformed and // written. func EncryptWriter(fn func(plaintext []byte) ([]byte, error), writer io.Writer, blockSize int) io.WriteCloser { bufferSize := roundUpToMultiple(1024*1024, blockSize) return &wrapper{fn: fn, blockSize: blockSize, buf: make([]byte, bufferSize), writer: writer} } // DecryptReader creates an io.ReadCloser which buffers reads through a // decryption function, decrypting and returning multiples of the blockSize // until it reaches the end of the file. When data will no longer be read, the // returned reader should be closed. func DecryptReader(fn func(ciphertext []byte) ([]byte, error), reader io.Reader, blockSize int) io.ReadCloser { bufferSize := roundUpToMultiple(1024*1024, blockSize) return &wrapper{fn: fn, blockSize: blockSize, buf: make([]byte, bufferSize), reader: reader} } ================================================ FILE: vendor/github.com/containers/luksy/luks.go ================================================ package luksy import ( "bytes" "encoding/json" "fmt" "io" ) // ReadHeaderOptions can control some of what ReadHeaders() does. type ReadHeaderOptions struct{} // ReadHeaders reads LUKS headers from the specified file, returning either a // LUKSv1 header, or two LUKSv2 headers and a LUKSv2 JSON block, depending on // which format is detected. func ReadHeaders(f io.ReaderAt, options ReadHeaderOptions) (*V1Header, *V2Header, *V2Header, *V2JSON, error) { var v1 V1Header var v2a, v2b V2Header n, err := f.ReadAt(v2a[:], 0) if err != nil { return nil, nil, nil, nil, err } if n != len(v2a) { return nil, nil, nil, nil, fmt.Errorf("only able to read %d bytes - file truncated?", n) } if n, err = f.ReadAt(v1[:], 0); err != nil { return nil, nil, nil, nil, err } if n != len(v1) { return nil, nil, nil, nil, fmt.Errorf("only able to read %d bytes - file truncated?", n) } if v2a.Magic() != V2Magic1 { return nil, nil, nil, nil, fmt.Errorf("internal error: magic mismatch in LUKS header (%q)", v2a.Magic()) } switch v2a.Version() { // is it a v1 header, or the first v2 header? case 1: return &v1, nil, nil, nil, nil case 2: size := v2a.HeaderSize() if size > 0x7fffffffffffffff { return nil, nil, nil, nil, fmt.Errorf("unsupported header size while looking for second header") } if size < 4096 { return nil, nil, nil, nil, fmt.Errorf("unsupported header size while looking for JSON data") } if n, err = f.ReadAt(v2b[:], int64(size)); err != nil || n != len(v2b) { if err == nil && n != len(v2b) { err = fmt.Errorf("short read: read only %d bytes, should have read %d", n, len(v2b)) } return nil, nil, nil, nil, err } if v2b.Magic() != V2Magic2 { return nil, nil, nil, nil, fmt.Errorf("internal error: magic mismatch in second LUKS header (%q)", v2b.Magic()) } jsonSize := size - 4096 buf := make([]byte, jsonSize) n, err = f.ReadAt(buf[:], 4096) if err != nil { return nil, nil, nil, nil, fmt.Errorf("internal error: while reading JSON data: %w", err) } if n < 0 || uint64(n) != jsonSize { return nil, nil, nil, nil, fmt.Errorf("internal error: short read while reading JSON data (wanted %d, got %d)", jsonSize, n) } var jsonData V2JSON buf = bytes.TrimRightFunc(buf, func(r rune) bool { return r == 0 }) if err = json.Unmarshal(buf, &jsonData); err != nil { return nil, nil, nil, nil, fmt.Errorf("internal error: decoding JSON data: %w", err) } if uint64(jsonData.Config.JsonSize) != jsonSize { return nil, nil, nil, nil, fmt.Errorf("internal error: JSON data size mismatch: (expected %d, used %d)", jsonData.Config.JsonSize, jsonSize) } return nil, &v2a, &v2b, &jsonData, nil } return nil, nil, nil, nil, fmt.Errorf("error reading LUKS header - magic identifier not found") } ================================================ FILE: vendor/github.com/containers/luksy/tune.go ================================================ package luksy import ( "hash" "time" "golang.org/x/crypto/argon2" "golang.org/x/crypto/pbkdf2" ) func durationOf(f func()) time.Duration { start := time.Now() f() return time.Since(start) } func IterationsPBKDF2(salt []byte, keyLen int, h func() hash.Hash) int { iterations := 2 var d time.Duration for d < time.Second { d = durationOf(func() { _ = pbkdf2.Key([]byte{}, salt, iterations, keyLen, h) }) if d < time.Second/10 { iterations *= 2 } else { return iterations * int(time.Second) / int(d) } } return iterations } func memoryCostArgon2(salt []byte, keyLen, timeCost, threadsCost int, kdf func([]byte, []byte, uint32, uint32, uint8, uint32) []byte) int { memoryCost := 2 var d time.Duration for d < time.Second { d = durationOf(func() { _ = kdf([]byte{}, salt, uint32(timeCost), uint32(memoryCost), uint8(threadsCost), uint32(keyLen)) }) if d < time.Second/10 { memoryCost *= 2 } else { return memoryCost * int(float64(time.Second)/float64(d)) } } return memoryCost } func MemoryCostArgon2(salt []byte, keyLen, timeCost, threadsCost int) int { return memoryCostArgon2(salt, keyLen, timeCost, threadsCost, argon2.Key) } func MemoryCostArgon2i(salt []byte, keyLen, timeCost, threadsCost int) int { return memoryCostArgon2(salt, keyLen, timeCost, threadsCost, argon2.IDKey) } ================================================ FILE: vendor/github.com/containers/luksy/v1header.go ================================================ package luksy import ( "encoding/binary" "fmt" "syscall" ) type ( V1Header [592]uint8 V1KeySlot [48]uint8 ) const ( // Mostly verbatim from LUKS1 On-Disk Format Specification version 1.2.3 V1Magic = "LUKS\xba\xbe" v1MagicStart = 0 v1MagicLength = 6 v1VersionStart = v1MagicStart + v1MagicLength v1VersionLength = 2 v1CipherNameStart = v1VersionStart + v1VersionLength v1CipherNameLength = 32 v1CipherModeStart = v1CipherNameStart + v1CipherNameLength v1CipherModeLength = 32 v1HashSpecStart = v1CipherModeStart + v1CipherModeLength v1HashSpecLength = 32 v1PayloadOffsetStart = v1HashSpecStart + v1HashSpecLength v1PayloadOffsetLength = 4 v1KeyBytesStart = v1PayloadOffsetStart + v1PayloadOffsetLength v1KeyBytesLength = 4 v1MKDigestStart = v1KeyBytesStart + v1KeyBytesLength v1MKDigestLength = v1DigestSize v1MKDigestSaltStart = v1MKDigestStart + v1MKDigestLength v1MKDigestSaltLength = v1SaltSize v1MKDigestIterStart = v1MKDigestSaltStart + v1MKDigestSaltLength v1MKDigestIterLength = 4 v1UUIDStart = v1MKDigestIterStart + v1MKDigestIterLength v1UUIDLength = 40 v1KeySlot1Start = v1UUIDStart + v1UUIDLength v1KeySlot1Length = 48 v1KeySlot2Start = v1KeySlot1Start + v1KeySlot1Length v1KeySlot2Length = 48 v1KeySlot3Start = v1KeySlot2Start + v1KeySlot2Length v1KeySlot3Length = 48 v1KeySlot4Start = v1KeySlot3Start + v1KeySlot3Length v1KeySlot4Length = 48 v1KeySlot5Start = v1KeySlot4Start + v1KeySlot4Length v1KeySlot5Length = 48 v1KeySlot6Start = v1KeySlot5Start + v1KeySlot5Length v1KeySlot6Length = 48 v1KeySlot7Start = v1KeySlot6Start + v1KeySlot6Length v1KeySlot7Length = 48 v1KeySlot8Start = v1KeySlot7Start + v1KeySlot7Length v1KeySlot8Length = 48 v1HeaderStructSize = v1KeySlot8Start + v1KeySlot8Length v1KeySlotActiveStart = 0 v1KeySlotActiveLength = 4 v1KeySlotIterationsStart = v1KeySlotActiveStart + v1KeySlotActiveLength v1KeySlotIterationsLength = 4 v1KeySlotSaltStart = v1KeySlotIterationsStart + v1KeySlotIterationsLength v1KeySlotSaltLength = v1SaltSize v1KeySlotKeyMaterialOffsetStart = v1KeySlotSaltStart + v1KeySlotSaltLength v1KeySlotKeyMaterialOffsetLength = 4 v1KeySlotStripesStart = v1KeySlotKeyMaterialOffsetStart + v1KeySlotKeyMaterialOffsetLength v1KeySlotStripesLength = 4 v1KeySlotStructSize = v1KeySlotStripesStart + v1KeySlotStripesLength v1DigestSize = 20 v1SaltSize = 32 v1NumKeys = 8 v1KeySlotActiveKeyDisabled = 0x0000dead v1KeySlotActiveKeyEnabled = 0x00ac71f3 V1Stripes = 4000 V1AlignKeyslots = 4096 V1SectorSize = 512 ) func (h V1Header) readu2(offset int) uint16 { return binary.BigEndian.Uint16(h[offset:]) } func (h V1Header) readu4(offset int) uint32 { return binary.BigEndian.Uint32(h[offset:]) } func (h *V1Header) writeu2(offset int, value uint16) { binary.BigEndian.PutUint16(h[offset:], value) } func (h *V1Header) writeu4(offset int, value uint32) { binary.BigEndian.PutUint32(h[offset:], value) } func (h V1Header) Magic() string { return trimZeroPad(string(h[v1MagicStart : v1MagicStart+v1MagicLength])) } func (h *V1Header) SetMagic(magic string) error { switch magic { case V1Magic: copy(h[v1MagicStart:v1MagicStart+v1MagicLength], []uint8(magic)) return nil } return fmt.Errorf("magic %q not acceptable, only %q is an acceptable magic value: %w", magic, V1Magic, syscall.EINVAL) } func (h V1Header) Version() uint16 { return h.readu2(v1VersionStart) } func (h *V1Header) SetVersion(version uint16) error { switch version { case 1: h.writeu2(v1VersionStart, version) return nil } return fmt.Errorf("version %d not acceptable, only 1 is an acceptable version: %w", version, syscall.EINVAL) } func (h *V1Header) setZeroString(offset int, value string, length int) { for len(value) < length { value = value + "\000" } copy(h[offset:offset+length], []uint8(value)) } func (h *V1Header) setInt8(offset int, s []uint8, length int) { t := make([]byte, length) copy(t, s) copy(h[offset:offset+length], s) } func (h V1Header) CipherName() string { return trimZeroPad(string(h[v1CipherNameStart : v1CipherNameStart+v1CipherNameLength])) } func (h *V1Header) SetCipherName(name string) { h.setZeroString(v1CipherNameStart, name, v1CipherNameLength) } func (h V1Header) CipherMode() string { return trimZeroPad(string(h[v1CipherModeStart : v1CipherModeStart+v1CipherModeLength])) } func (h *V1Header) SetCipherMode(mode string) { h.setZeroString(v1CipherModeStart, mode, v1CipherModeLength) } func (h V1Header) HashSpec() string { return trimZeroPad(string(h[v1HashSpecStart : v1HashSpecStart+v1HashSpecLength])) } func (h *V1Header) SetHashSpec(spec string) { h.setZeroString(v1HashSpecStart, spec, v1HashSpecLength) } func (h V1Header) PayloadOffset() uint32 { return h.readu4(v1PayloadOffsetStart) } func (h *V1Header) SetPayloadOffset(offset uint32) { h.writeu4(v1PayloadOffsetStart, offset) } func (h V1Header) KeyBytes() uint32 { return h.readu4(v1KeyBytesStart) } func (h *V1Header) SetKeyBytes(bytes uint32) { h.writeu4(v1KeyBytesStart, bytes) } func (h *V1Header) KeySlot(slot int) (V1KeySlot, error) { var ks V1KeySlot if slot < 0 || slot >= v1NumKeys { return ks, fmt.Errorf("invalid key slot number (must be 0..%d)", v1NumKeys-1) } switch slot { case 0: copy(ks[:], h[v1KeySlot1Start:v1KeySlot1Start+v1KeySlot1Length]) case 1: copy(ks[:], h[v1KeySlot2Start:v1KeySlot2Start+v1KeySlot2Length]) case 2: copy(ks[:], h[v1KeySlot3Start:v1KeySlot3Start+v1KeySlot3Length]) case 3: copy(ks[:], h[v1KeySlot4Start:v1KeySlot4Start+v1KeySlot4Length]) case 4: copy(ks[:], h[v1KeySlot5Start:v1KeySlot5Start+v1KeySlot5Length]) case 5: copy(ks[:], h[v1KeySlot6Start:v1KeySlot6Start+v1KeySlot6Length]) case 6: copy(ks[:], h[v1KeySlot7Start:v1KeySlot7Start+v1KeySlot7Length]) case 7: copy(ks[:], h[v1KeySlot8Start:v1KeySlot8Start+v1KeySlot8Length]) } return ks, nil } func (h *V1Header) SetKeySlot(slot int, ks V1KeySlot) error { if slot < 0 || slot >= v1NumKeys { return fmt.Errorf("invalid key slot number (must be 0..%d)", v1NumKeys-1) } switch slot { case 0: copy(h[v1KeySlot1Start:v1KeySlot1Start+v1KeySlot1Length], ks[:]) case 1: copy(h[v1KeySlot2Start:v1KeySlot2Start+v1KeySlot2Length], ks[:]) case 2: copy(h[v1KeySlot3Start:v1KeySlot3Start+v1KeySlot3Length], ks[:]) case 3: copy(h[v1KeySlot4Start:v1KeySlot4Start+v1KeySlot4Length], ks[:]) case 4: copy(h[v1KeySlot5Start:v1KeySlot5Start+v1KeySlot5Length], ks[:]) case 5: copy(h[v1KeySlot6Start:v1KeySlot6Start+v1KeySlot6Length], ks[:]) case 6: copy(h[v1KeySlot7Start:v1KeySlot7Start+v1KeySlot7Length], ks[:]) case 7: copy(h[v1KeySlot8Start:v1KeySlot8Start+v1KeySlot8Length], ks[:]) } return nil } func (h V1Header) MKDigest() []uint8 { return dupInt8(h[v1MKDigestStart : v1MKDigestStart+v1MKDigestLength]) } func (h *V1Header) SetMKDigest(digest []uint8) { h.setInt8(v1MKDigestStart, digest, v1MKDigestLength) } func (h V1Header) MKDigestSalt() []uint8 { return dupInt8(h[v1MKDigestSaltStart : v1MKDigestSaltStart+v1MKDigestSaltLength]) } func (h *V1Header) SetMKDigestSalt(salt []uint8) { h.setInt8(v1MKDigestSaltStart, salt, v1MKDigestSaltLength) } func (h V1Header) MKDigestIter() uint32 { return h.readu4(v1MKDigestIterStart) } func (h *V1Header) SetMKDigestIter(bytes uint32) { h.writeu4(v1MKDigestIterStart, bytes) } func (h V1Header) UUID() string { return trimZeroPad(string(h[v1UUIDStart : v1UUIDStart+v1UUIDLength])) } func (h *V1Header) SetUUID(uuid string) { h.setZeroString(v1UUIDStart, uuid, v1UUIDLength) } func (s V1KeySlot) readu4(offset int) uint32 { return binary.BigEndian.Uint32(s[offset:]) } func (s *V1KeySlot) writeu4(offset int, value uint32) { binary.BigEndian.PutUint32(s[offset:], value) } func (s *V1KeySlot) setInt8(offset int, i []uint8, length int) { for len(s) < length { i = append(i, 0) } copy(s[offset:offset+length], i) } func (s V1KeySlot) Active() (bool, error) { active := s.readu4(v1KeySlotActiveStart) switch active { case v1KeySlotActiveKeyDisabled: return false, nil case v1KeySlotActiveKeyEnabled: return true, nil } return false, fmt.Errorf("got invalid active value %#0x: %w", active, syscall.EINVAL) } func (s *V1KeySlot) SetActive(active bool) { if active { s.writeu4(v1KeySlotActiveStart, v1KeySlotActiveKeyEnabled) return } s.writeu4(v1KeySlotActiveStart, v1KeySlotActiveKeyDisabled) } func (s V1KeySlot) Iterations() uint32 { return s.readu4(v1KeySlotIterationsStart) } func (s *V1KeySlot) SetIterations(iterations uint32) { s.writeu4(v1KeySlotIterationsStart, iterations) } func (s V1KeySlot) KeySlotSalt() []uint8 { return dupInt8(s[v1KeySlotSaltStart : v1KeySlotSaltStart+v1KeySlotSaltLength]) } func (s *V1KeySlot) SetKeySlotSalt(salt []uint8) { s.setInt8(v1KeySlotSaltStart, salt, v1KeySlotSaltLength) } func (s V1KeySlot) KeyMaterialOffset() uint32 { return s.readu4(v1KeySlotKeyMaterialOffsetStart) } func (s *V1KeySlot) SetKeyMaterialOffset(material uint32) { s.writeu4(v1KeySlotKeyMaterialOffsetStart, material) } func (s V1KeySlot) Stripes() uint32 { return s.readu4(v1KeySlotStripesStart) } func (s *V1KeySlot) SetStripes(stripes uint32) { s.writeu4(v1KeySlotStripesStart, stripes) } ================================================ FILE: vendor/github.com/containers/luksy/v2header.go ================================================ package luksy import ( "fmt" "strings" "syscall" ) type V2Header [4096]uint8 const ( // Mostly verbatim from LUKS2 On-Disk Format Specification version 1.1.1 V2Magic1 = V1Magic V2Magic2 = "SKUL\xba\xbe" v2MagicStart = 0 v2MagicLength = 6 v2VersionStart = v2MagicStart + v2MagicLength v2VersionLength = 2 v2HeaderSizeStart = v2VersionStart + v2VersionLength v2HeaderSizeLength = 8 v2SequenceIDStart = v2HeaderSizeStart + v2HeaderSizeLength v2SequenceIDLength = 8 v2LabelStart = v2SequenceIDStart + v2SequenceIDLength v2LabelLength = 48 v2ChecksumAlgorithmStart = v2LabelStart + v2LabelLength v2ChecksumAlgorithmLength = 32 v2SaltStart = v2ChecksumAlgorithmStart + v2ChecksumAlgorithmLength v2SaltLength = 64 v2UUIDStart = v2SaltStart + v2SaltLength v2UUIDLength = 40 v2SubsystemStart = v2UUIDStart + v2UUIDLength v2SubsystemLength = v2LabelLength v2HeaderOffsetStart = v2SubsystemStart + v2SubsystemLength v2HeaderOffsetLength = 8 v2Padding1Start = v2HeaderOffsetStart + v2HeaderOffsetLength v2Padding1Length = 184 v2ChecksumStart = v2Padding1Start + v2Padding1Length v2ChecksumLength = 64 v2Padding4096Start = v2ChecksumStart + v2ChecksumLength v2Padding4096Length = 7 * 512 v2HeaderStructSize = v2Padding4096Start + v2Padding4096Length V2Stripes = 4000 V2AlignKeyslots = 4096 V2SectorSize = 4096 ) func (h V2Header) Magic() string { return string(h[v2MagicStart : v2MagicStart+v2MagicLength]) } func (h *V2Header) SetMagic(magic string) error { switch magic { case V2Magic1, V2Magic2: copy(h[v2MagicStart:v2MagicStart+v2MagicLength], []uint8(magic)) return nil } return fmt.Errorf("magic %q not acceptable, only %q and %q are acceptable magic values: %w", magic, V2Magic1, V2Magic2, syscall.EINVAL) } func (h V2Header) readu2(offset int) uint16 { t := uint16(0) for i := 0; i < 2; i++ { t = (t << 8) + uint16(h[offset+i]) } return t } func (h V2Header) readu8(offset int) uint64 { t := uint64(0) for i := 0; i < 8; i++ { t = (t << 8) + uint64(h[offset+i]) } return t } func (h *V2Header) writeu2(offset int, value uint16) { t := value for i := 0; i < 2; i++ { h[offset+1-i] = uint8(uint64(t) & 0xff) t >>= 8 } } func (h *V2Header) writeu8(offset int, value uint64) { t := value for i := 0; i < 8; i++ { h[offset+7-i] = uint8(uint64(t) & 0xff) t >>= 8 } } func (h V2Header) Version() uint16 { return h.readu2(v2VersionStart) } func (h *V2Header) SetVersion(version uint16) error { switch version { case 2: h.writeu2(v2VersionStart, version) return nil } return fmt.Errorf("version %d not acceptable, only 2 is an acceptable version: %w", version, syscall.EINVAL) } func (h V2Header) HeaderSize() uint64 { return h.readu8(v2HeaderSizeStart) } func (h *V2Header) SetHeaderSize(size uint64) { h.writeu8(v2HeaderSizeStart, size) } func (h V2Header) SequenceID() uint64 { return h.readu8(v2SequenceIDStart) } func (h *V2Header) SetSequenceID(id uint64) { h.writeu8(v2SequenceIDStart, id) } func trimZeroPad(s string) string { return strings.TrimRightFunc(s, func(r rune) bool { return r == 0 }) } func (h V2Header) Label() string { return trimZeroPad(string(h[v2LabelStart : v2LabelStart+v2LabelLength])) } func (h *V2Header) setZeroString(offset int, value string, length int) { for len(value) < length { value = value + "\000" } copy(h[offset:offset+length], []uint8(value)) } func (h *V2Header) SetLabel(label string) { h.setZeroString(v2LabelStart, label, v2LabelLength) } func (h V2Header) ChecksumAlgorithm() string { return trimZeroPad(string(h[v2ChecksumAlgorithmStart : v2ChecksumAlgorithmStart+v2ChecksumAlgorithmLength])) } func (h *V2Header) SetChecksumAlgorithm(alg string) { h.setZeroString(v2ChecksumAlgorithmStart, alg, v2ChecksumAlgorithmLength) } func dupInt8(s []uint8) []uint8 { c := make([]uint8, len(s)) copy(c, s) return c } func (h *V2Header) setInt8(offset int, s []uint8, length int) { t := make([]byte, length) copy(t, s) copy(h[offset:offset+length], t) } func (h V2Header) Salt() []uint8 { return dupInt8(h[v2SaltStart : v2SaltStart+v2SaltLength]) } func (h *V2Header) SetSalt(salt []uint8) { h.setInt8(v2SaltStart, salt, v2SaltLength) } func (h V2Header) UUID() string { return trimZeroPad(string(h[v2UUIDStart : v2UUIDStart+v2UUIDLength])) } func (h *V2Header) SetUUID(uuid string) { h.setZeroString(v2UUIDStart, uuid, v2UUIDLength) } func (h V2Header) Subsystem() string { return trimZeroPad(string(h[v2SubsystemStart : v2SubsystemStart+v2SubsystemLength])) } func (h *V2Header) SetSubsystem(ss string) { h.setZeroString(v2SubsystemStart, ss, v2SubsystemLength) } func (h V2Header) HeaderOffset() uint64 { return h.readu8(v2HeaderOffsetStart) } func (h *V2Header) SetHeaderOffset(o uint64) { h.writeu8(v2HeaderOffsetStart, o) } func (h V2Header) Checksum() []uint8 { hasher, err := hasherByName(h.ChecksumAlgorithm()) if err == nil { return dupInt8(h[v2ChecksumStart : v2ChecksumStart+hasher().Size()]) } return dupInt8(h[v2ChecksumStart : v2ChecksumStart+v2ChecksumLength]) } func (h *V2Header) SetChecksum(sum []uint8) { h.setInt8(v2ChecksumStart, sum, v2ChecksumLength) } ================================================ FILE: vendor/github.com/containers/luksy/v2json.go ================================================ package luksy type V2JSON struct { Config V2JSONConfig `json:"config"` Keyslots map[string]V2JSONKeyslot `json:"keyslots"` Digests map[string]V2JSONDigest `json:"digests"` Segments map[string]V2JSONSegment `json:"segments"` Tokens map[string]V2JSONToken `json:"tokens"` } type V2JSONKeyslotPriority int func (p V2JSONKeyslotPriority) String() string { switch p { case V2JSONKeyslotPriorityIgnore: return "ignore" case V2JSONKeyslotPriorityNormal: return "normal" case V2JSONKeyslotPriorityHigh: return "high" } return "unknown" } const ( V2JSONKeyslotPriorityIgnore = V2JSONKeyslotPriority(0) V2JSONKeyslotPriorityNormal = V2JSONKeyslotPriority(1) V2JSONKeyslotPriorityHigh = V2JSONKeyslotPriority(2) ) type V2JSONKeyslot struct { Type string `json:"type"` KeySize int `json:"key_size"` Area V2JSONArea `json:"area"` Priority *V2JSONKeyslotPriority `json:"priority,omitempty"` *V2JSONKeyslotLUKS2 // type = "luks2" *V2JSONKeyslotReencrypt // type = "reencrypt" } type V2JSONKeyslotLUKS2 struct { AF V2JSONAF `json:"af"` Kdf V2JSONKdf `json:"kdf"` } type V2JSONKeyslotReencrypt struct { Mode string `json:"mode"` // only "reencrypt", "encrypt", "decrypt" Direction string `json:"direction"` // only "forward", "backward" } type V2JSONArea struct { Type string `json:"type"` // only "raw", "none", "journal", "checksum", "datashift", "datashift-journal", "datashift-checksum" Offset int64 `json:"offset,string"` Size int64 `json:"size,string"` *V2JSONAreaRaw // type = "raw" *V2JSONAreaChecksum // type = "checksum" *V2JSONAreaDatashift // type = "datashift" *V2JSONAreaDatashiftChecksum // type = "datashift-checksum" } type V2JSONAreaRaw struct { Encryption string `json:"encryption"` KeySize int `json:"key_size"` } type V2JSONAreaChecksum struct { Hash string `json:"hash"` SectorSize int `json:"sector_size"` } type V2JSONAreaDatashift struct { ShiftSize int `json:"shift_size,string"` } type V2JSONAreaDatashiftChecksum struct { V2JSONAreaChecksum V2JSONAreaDatashift } type V2JSONAF struct { Type string `json:"type"` // "luks1" *V2JSONAFLUKS1 // type == "luks1" } type V2JSONAFLUKS1 struct { Stripes int `json:"stripes"` // 4000 Hash string `json:"hash"` // "sha256" } type V2JSONKdf struct { Type string `json:"type"` Salt []byte `json:"salt"` *V2JSONKdfPbkdf2 // type = "pbkdf2" *V2JSONKdfArgon2i // type = "argon2i" or type = "argon2id" } type V2JSONKdfPbkdf2 struct { Hash string `json:"hash"` Iterations int `json:"iterations"` } type V2JSONKdfArgon2i struct { Time int `json:"time"` Memory int `json:"memory"` CPUs int `json:"cpus"` } type V2JSONSegment struct { Type string `json:"type"` // only "linear", "crypt" Offset string `json:"offset"` Size string `json:"size"` // numeric value or "dynamic" Flags []string `json:"flags,omitempty"` *V2JSONSegmentCrypt `json:",omitempty"` // type = "crypt" } type V2JSONSegmentCrypt struct { IVTweak int `json:"iv_tweak,string"` Encryption string `json:"encryption"` SectorSize int `json:"sector_size"` // 512 or 1024 or 2048 or 4096 Integrity *V2JSONSegmentIntegrity `json:"integrity,omitempty"` } type V2JSONSegmentIntegrity struct { Type string `json:"type"` JournalEncryption string `json:"journal_encryption"` JournalIntegrity string `json:"journal_integrity"` } type V2JSONDigest struct { Type string `json:"type"` Keyslots []string `json:"keyslots"` Segments []string `json:"segments"` Salt []byte `json:"salt"` Digest []byte `json:"digest"` *V2JSONDigestPbkdf2 // type == "pbkdf2" } type V2JSONDigestPbkdf2 struct { Hash string `json:"hash"` Iterations int `json:"iterations"` } type V2JSONConfig struct { JsonSize int `json:"json_size,string"` KeyslotsSize int `json:"keyslots_size,string,omitempty"` Flags []string `json:"flags,omitempty"` // one or more of "allow-discards", "same-cpu-crypt", "submit-from-crypt-cpus", "no-journal", "no-read-workqueue", "no-write-workqueue" Requirements []string `json:"requirements,omitempty"` } type V2JSONToken struct { Type string `json:"type"` // "luks2-keyring" Keyslots []string `json:"keyslots,omitempty"` *V2JSONTokenLUKS2Keyring // type == "luks2-keyring" } type V2JSONTokenLUKS2Keyring struct { KeyDescription string `json:"key_description"` } ================================================ FILE: vendor/github.com/containers/ocicrypt/.gitignore ================================================ *~ ================================================ FILE: vendor/github.com/containers/ocicrypt/.golangci.yml ================================================ linters: enable: - depguard - staticcheck - unconvert - gofmt - goimports - revive - ineffassign - govet - unused - misspell linters-settings: depguard: rules: main: files: - $all deny: - pkg: "io/ioutil" revive: severity: error rules: - name: indent-error-flow severity: warning disabled: false - name: error-strings disabled: false staticcheck: # Suppress reports of deprecated packages checks: ["-SA1019"] ================================================ FILE: vendor/github.com/containers/ocicrypt/ADOPTERS.md ================================================ Below are list of adopters of the `ocicrypt` library or supports use of OCI encrypted images: - [skopeo](https://github.com/containers/skopeo) - [buildah](https://github.com/containers/buildah) - [containerd](https://github.com/containerd/imgcrypt) - [nerdctl](https://github.com/containerd/nerdctl) - [distribution](https://github.com/distribution/distribution) Below are the list of projects that are in the process of adopting support: - [quay](https://github.com/quay/quay) - [kata-containers](https://github.com/kata-containers/kata-containers) ================================================ FILE: vendor/github.com/containers/ocicrypt/CODE-OF-CONDUCT.md ================================================ ## The OCIcrypt Library Project Community Code of Conduct The OCIcrypt Library project follows the [Containers Community Code of Conduct](https://github.com/containers/common/blob/main/CODE-OF-CONDUCT.md). ================================================ FILE: vendor/github.com/containers/ocicrypt/LICENSE ================================================ Apache License Version 2.0, January 2004 https://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 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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: vendor/github.com/containers/ocicrypt/MAINTAINERS ================================================ # ocicrypt maintainers # # Github ID, Name, Email Address lumjjb, Brandon Lum, lumjjb@gmail.com stefanberger, Stefan Berger, stefanb@linux.ibm.com arronwy, Arron Wang, arron.wang@intel.com ================================================ FILE: vendor/github.com/containers/ocicrypt/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. .PHONY: check build decoder generate-protobuf all: build FORCE: check: golangci-lint run build: vendor go build ./... vendor: go mod tidy test: go clean -testcache go test ./... -test.v generate-protobuf: protoc -I utils/keyprovider/ utils/keyprovider/keyprovider.proto --go_out=plugins=grpc:utils/keyprovider ================================================ FILE: vendor/github.com/containers/ocicrypt/README.md ================================================ # OCIcrypt Library The `ocicrypt` library is the OCI image spec implementation of container image encryption. More details of the spec can be seen in the [OCI repository](https://github.com/opencontainers/image-spec/pull/775). The purpose of this library is to encode spec structures and consts in code, as well as provide a consistent implementation of image encryption across container runtimes and build tools. Consumers of OCIcrypt: - [containerd/imgcrypt](https://github.com/containerd/imgcrypt) - [cri-o](https://github.com/cri-o/cri-o) - [skopeo](https://github.com/containers/skopeo) ## Usage There are various levels of usage for this library. The main consumers of these would be runtime/build tools, and a more specific use would be in the ability to extend cryptographic function. ### Runtime/Build tool usage The general exposed interface a runtime/build tool would use, would be to perform encryption or decryption of layers: ``` package "github.com/containers/ocicrypt" func EncryptLayer(ec *config.EncryptConfig, encOrPlainLayerReader io.Reader, desc ocispec.Descriptor) (io.Reader, EncryptLayerFinalizer, error) func DecryptLayer(dc *config.DecryptConfig, encLayerReader io.Reader, desc ocispec.Descriptor, unwrapOnly bool) (io.Reader, digest.Digest, error) ``` The settings/parameters to these functions can be specified via creation of an encryption config with the `github.com/containers/ocicrypt/config` package. We note that because setting of annotations and other fields of the layer descriptor is done through various means in different runtimes/build tools, it is the responsibility of the caller to still ensure that the layer descriptor follows the OCI specification (i.e. encoding, setting annotations, etc.). ### Crypto Agility and Extensibility The implementation for both symmetric and asymmetric encryption used in this library are behind 2 main interfaces, which users can extend if need be. These are in the following packages: - github.com/containers/ocicrypt/blockcipher - LayerBlockCipher interface for block ciphers - github.com/containers/ocicrypt/keywrap - KeyWrapper interface for key wrapping We note that adding interfaces here is risky outside the OCI spec is not recommended, unless for very specialized and confined usecases. Please open an issue or PR if there is a general usecase that could be added to the OCI spec. #### Keyprovider interface As part of the keywrap interface, there is a [keyprovider](https://github.com/containers/ocicrypt/blob/main/docs/keyprovider.md) implementation that allows one to call out to a binary or service. ## Security Issues We consider security issues related to this library critical. Please report and security related issues by emailing maintainers in the [MAINTAINERS](MAINTAINERS) file. ## Ocicrypt Pkcs11 Support Ocicrypt Pkcs11 support is currently experiemental. For more details, please refer to the [this document](docs/pkcs11.md). ================================================ FILE: vendor/github.com/containers/ocicrypt/SECURITY.md ================================================ ## Security and Disclosure Information Policy for the OCIcrypt Library Project The OCIcrypt Library Project follows the [Security and Disclosure Information Policy](https://github.com/containers/common/blob/main/SECURITY.md) for the Containers Projects. ================================================ FILE: vendor/github.com/containers/ocicrypt/blockcipher/blockcipher.go ================================================ /* Copyright The ocicrypt Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package blockcipher import ( "errors" "fmt" "io" "github.com/opencontainers/go-digest" ) // LayerCipherType is the ciphertype as specified in the layer metadata type LayerCipherType string // TODO: Should be obtained from OCI spec once included const ( AES256CTR LayerCipherType = "AES_256_CTR_HMAC_SHA256" ) // PrivateLayerBlockCipherOptions includes the information required to encrypt/decrypt // an image which are sensitive and should not be in plaintext type PrivateLayerBlockCipherOptions struct { // SymmetricKey represents the symmetric key used for encryption/decryption // This field should be populated by Encrypt/Decrypt calls SymmetricKey []byte `json:"symkey"` // Digest is the digest of the original data for verification. // This is NOT populated by Encrypt/Decrypt calls Digest digest.Digest `json:"digest"` // CipherOptions contains the cipher metadata used for encryption/decryption // This field should be populated by Encrypt/Decrypt calls CipherOptions map[string][]byte `json:"cipheroptions"` } // PublicLayerBlockCipherOptions includes the information required to encrypt/decrypt // an image which are public and can be deduplicated in plaintext across multiple // recipients type PublicLayerBlockCipherOptions struct { // CipherType denotes the cipher type according to the list of OCI suppported // cipher types. CipherType LayerCipherType `json:"cipher"` // Hmac contains the hmac string to help verify encryption Hmac []byte `json:"hmac"` // CipherOptions contains the cipher metadata used for encryption/decryption // This field should be populated by Encrypt/Decrypt calls CipherOptions map[string][]byte `json:"cipheroptions"` } // LayerBlockCipherOptions contains the public and private LayerBlockCipherOptions // required to encrypt/decrypt an image type LayerBlockCipherOptions struct { Public PublicLayerBlockCipherOptions Private PrivateLayerBlockCipherOptions } // LayerBlockCipher returns a provider for encrypt/decrypt functionality // for handling the layer data for a specific algorithm type LayerBlockCipher interface { // GenerateKey creates a symmetric key GenerateKey() ([]byte, error) // Encrypt takes in layer data and returns the ciphertext and relevant LayerBlockCipherOptions Encrypt(layerDataReader io.Reader, opt LayerBlockCipherOptions) (io.Reader, Finalizer, error) // Decrypt takes in layer ciphertext data and returns the plaintext and relevant LayerBlockCipherOptions Decrypt(layerDataReader io.Reader, opt LayerBlockCipherOptions) (io.Reader, LayerBlockCipherOptions, error) } // LayerBlockCipherHandler is the handler for encrypt/decrypt for layers type LayerBlockCipherHandler struct { cipherMap map[LayerCipherType]LayerBlockCipher } // Finalizer is called after data blobs are written, and returns the LayerBlockCipherOptions for the encrypted blob type Finalizer func() (LayerBlockCipherOptions, error) // GetOpt returns the value of the cipher option and if the option exists func (lbco LayerBlockCipherOptions) GetOpt(key string) (value []byte, ok bool) { if v, ok := lbco.Public.CipherOptions[key]; ok { return v, ok } else if v, ok := lbco.Private.CipherOptions[key]; ok { return v, ok } return nil, false } func wrapFinalizerWithType(fin Finalizer, typ LayerCipherType) Finalizer { return func() (LayerBlockCipherOptions, error) { lbco, err := fin() if err != nil { return LayerBlockCipherOptions{}, err } lbco.Public.CipherType = typ return lbco, err } } // Encrypt is the handler for the layer decryption routine func (h *LayerBlockCipherHandler) Encrypt(plainDataReader io.Reader, typ LayerCipherType) (io.Reader, Finalizer, error) { if c, ok := h.cipherMap[typ]; ok { sk, err := c.GenerateKey() if err != nil { return nil, nil, err } opt := LayerBlockCipherOptions{ Private: PrivateLayerBlockCipherOptions{ SymmetricKey: sk, }, } encDataReader, fin, err := c.Encrypt(plainDataReader, opt) if err == nil { fin = wrapFinalizerWithType(fin, typ) } return encDataReader, fin, err } return nil, nil, fmt.Errorf("unsupported cipher type: %s", typ) } // Decrypt is the handler for the layer decryption routine func (h *LayerBlockCipherHandler) Decrypt(encDataReader io.Reader, opt LayerBlockCipherOptions) (io.Reader, LayerBlockCipherOptions, error) { typ := opt.Public.CipherType if typ == "" { return nil, LayerBlockCipherOptions{}, errors.New("no cipher type provided") } if c, ok := h.cipherMap[typ]; ok { return c.Decrypt(encDataReader, opt) } return nil, LayerBlockCipherOptions{}, fmt.Errorf("unsupported cipher type: %s", typ) } // NewLayerBlockCipherHandler returns a new default handler func NewLayerBlockCipherHandler() (*LayerBlockCipherHandler, error) { h := LayerBlockCipherHandler{ cipherMap: map[LayerCipherType]LayerBlockCipher{}, } var err error h.cipherMap[AES256CTR], err = NewAESCTRLayerBlockCipher(256) if err != nil { return nil, fmt.Errorf("unable to set up Cipher AES-256-CTR: %w", err) } return &h, nil } ================================================ FILE: vendor/github.com/containers/ocicrypt/blockcipher/blockcipher_aes_ctr.go ================================================ /* Copyright The ocicrypt Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package blockcipher import ( "crypto/aes" "crypto/cipher" "crypto/hmac" "crypto/rand" "crypto/sha256" "errors" "fmt" "hash" "io" "github.com/containers/ocicrypt/utils" ) // AESCTRLayerBlockCipher implements the AES CTR stream cipher type AESCTRLayerBlockCipher struct { keylen int // in bytes reader io.Reader encrypt bool stream cipher.Stream err error hmac hash.Hash expHmac []byte doneEncrypting bool } type aesctrcryptor struct { bc *AESCTRLayerBlockCipher } // NewAESCTRLayerBlockCipher returns a new AES SIV block cipher of 256 or 512 bits func NewAESCTRLayerBlockCipher(bits int) (LayerBlockCipher, error) { if bits != 256 { return nil, errors.New("AES CTR bit count not supported") } return &AESCTRLayerBlockCipher{keylen: bits / 8}, nil } func (r *aesctrcryptor) Read(p []byte) (int, error) { var ( o int ) if r.bc.err != nil { return 0, r.bc.err } o, err := utils.FillBuffer(r.bc.reader, p) if err != nil { if err == io.EOF { r.bc.err = err } else { return 0, err } } if !r.bc.encrypt { if _, err := r.bc.hmac.Write(p[:o]); err != nil { r.bc.err = fmt.Errorf("could not write to hmac: %w", err) return 0, r.bc.err } if r.bc.err == io.EOF { // Before we return EOF we let the HMAC comparison // provide a verdict if !hmac.Equal(r.bc.hmac.Sum(nil), r.bc.expHmac) { r.bc.err = fmt.Errorf("could not properly decrypt byte stream; exp hmac: '%x', actual hmac: '%s'", r.bc.expHmac, r.bc.hmac.Sum(nil)) return 0, r.bc.err } } } r.bc.stream.XORKeyStream(p[:o], p[:o]) if r.bc.encrypt { if _, err := r.bc.hmac.Write(p[:o]); err != nil { r.bc.err = fmt.Errorf("could not write to hmac: %w", err) return 0, r.bc.err } if r.bc.err == io.EOF { // Final data encrypted; Do the 'then-MAC' part r.bc.doneEncrypting = true } } return o, r.bc.err } // init initializes an instance func (bc *AESCTRLayerBlockCipher) init(encrypt bool, reader io.Reader, opts LayerBlockCipherOptions) (LayerBlockCipherOptions, error) { var ( err error ) key := opts.Private.SymmetricKey if len(key) != bc.keylen { return LayerBlockCipherOptions{}, fmt.Errorf("invalid key length of %d bytes; need %d bytes", len(key), bc.keylen) } nonce, ok := opts.GetOpt("nonce") if !ok { nonce = make([]byte, aes.BlockSize) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return LayerBlockCipherOptions{}, fmt.Errorf("unable to generate random nonce: %w", err) } } block, err := aes.NewCipher(key) if err != nil { return LayerBlockCipherOptions{}, fmt.Errorf("aes.NewCipher failed: %w", err) } bc.reader = reader bc.encrypt = encrypt bc.stream = cipher.NewCTR(block, nonce) bc.err = nil bc.hmac = hmac.New(sha256.New, key) bc.expHmac = opts.Public.Hmac bc.doneEncrypting = false if !encrypt && len(bc.expHmac) == 0 { return LayerBlockCipherOptions{}, errors.New("HMAC is not provided for decryption process") } lbco := LayerBlockCipherOptions{ Private: PrivateLayerBlockCipherOptions{ SymmetricKey: key, CipherOptions: map[string][]byte{ "nonce": nonce, }, }, } return lbco, nil } // GenerateKey creates a synmmetric key func (bc *AESCTRLayerBlockCipher) GenerateKey() ([]byte, error) { key := make([]byte, bc.keylen) if _, err := io.ReadFull(rand.Reader, key); err != nil { return nil, err } return key, nil } // Encrypt takes in layer data and returns the ciphertext and relevant LayerBlockCipherOptions func (bc *AESCTRLayerBlockCipher) Encrypt(plainDataReader io.Reader, opt LayerBlockCipherOptions) (io.Reader, Finalizer, error) { lbco, err := bc.init(true, plainDataReader, opt) if err != nil { return nil, nil, err } finalizer := func() (LayerBlockCipherOptions, error) { if !bc.doneEncrypting { return LayerBlockCipherOptions{}, errors.New("Read()ing not complete, unable to finalize") } if lbco.Public.CipherOptions == nil { lbco.Public.CipherOptions = map[string][]byte{} } lbco.Public.Hmac = bc.hmac.Sum(nil) return lbco, nil } return &aesctrcryptor{bc}, finalizer, nil } // Decrypt takes in layer ciphertext data and returns the plaintext and relevant LayerBlockCipherOptions func (bc *AESCTRLayerBlockCipher) Decrypt(encDataReader io.Reader, opt LayerBlockCipherOptions) (io.Reader, LayerBlockCipherOptions, error) { lbco, err := bc.init(false, encDataReader, opt) if err != nil { return nil, LayerBlockCipherOptions{}, err } return utils.NewDelayedReader(&aesctrcryptor{bc}, 1024*10), lbco, nil } ================================================ FILE: vendor/github.com/containers/ocicrypt/config/config.go ================================================ /* Copyright The ocicrypt Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config // EncryptConfig is the container image PGP encryption configuration holding // the identifiers of those that will be able to decrypt the container and // the PGP public keyring file data that contains their public keys. type EncryptConfig struct { // map holding 'gpg-recipients', 'gpg-pubkeyringfile', 'pubkeys', 'x509s' Parameters map[string][][]byte DecryptConfig DecryptConfig } // DecryptConfig wraps the Parameters map that holds the decryption key type DecryptConfig struct { // map holding 'privkeys', 'x509s', 'gpg-privatekeys' Parameters map[string][][]byte } // CryptoConfig is a common wrapper for EncryptConfig and DecrypConfig that can // be passed through functions that share much code for encryption and decryption type CryptoConfig struct { EncryptConfig *EncryptConfig DecryptConfig *DecryptConfig } // InitDecryption initialized a CryptoConfig object with parameters used for decryption func InitDecryption(dcparameters map[string][][]byte) CryptoConfig { return CryptoConfig{ DecryptConfig: &DecryptConfig{ Parameters: dcparameters, }, } } // InitEncryption initializes a CryptoConfig object with parameters used for encryption // It also takes dcparameters that may be needed for decryption when adding a recipient // to an already encrypted image func InitEncryption(parameters, dcparameters map[string][][]byte) CryptoConfig { return CryptoConfig{ EncryptConfig: &EncryptConfig{ Parameters: parameters, DecryptConfig: DecryptConfig{ Parameters: dcparameters, }, }, } } // CombineCryptoConfigs takes a CryptoConfig list and creates a single CryptoConfig // containing the crypto configuration of all the key bundles func CombineCryptoConfigs(ccs []CryptoConfig) CryptoConfig { ecparam := map[string][][]byte{} ecdcparam := map[string][][]byte{} dcparam := map[string][][]byte{} for _, cc := range ccs { if ec := cc.EncryptConfig; ec != nil { addToMap(ecparam, ec.Parameters) addToMap(ecdcparam, ec.DecryptConfig.Parameters) } if dc := cc.DecryptConfig; dc != nil { addToMap(dcparam, dc.Parameters) } } return CryptoConfig{ EncryptConfig: &EncryptConfig{ Parameters: ecparam, DecryptConfig: DecryptConfig{ Parameters: ecdcparam, }, }, DecryptConfig: &DecryptConfig{ Parameters: dcparam, }, } } // AttachDecryptConfig adds DecryptConfig to the field of EncryptConfig so that // the decryption parameters can be used to add recipients to an existing image // if the user is able to decrypt it. func (ec *EncryptConfig) AttachDecryptConfig(dc *DecryptConfig) { if dc != nil { addToMap(ec.DecryptConfig.Parameters, dc.Parameters) } } func addToMap(orig map[string][][]byte, add map[string][][]byte) { for k, v := range add { if ov, ok := orig[k]; ok { orig[k] = append(ov, v...) } else { orig[k] = v } } } ================================================ FILE: vendor/github.com/containers/ocicrypt/config/constructors.go ================================================ /* Copyright The ocicrypt Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "errors" "fmt" "strings" "github.com/containers/ocicrypt/crypto/pkcs11" "gopkg.in/yaml.v3" ) // EncryptWithJwe returns a CryptoConfig to encrypt with jwe public keys func EncryptWithJwe(pubKeys [][]byte) (CryptoConfig, error) { dc := DecryptConfig{} ep := map[string][][]byte{ "pubkeys": pubKeys, } return CryptoConfig{ EncryptConfig: &EncryptConfig{ Parameters: ep, DecryptConfig: dc, }, DecryptConfig: &dc, }, nil } // EncryptWithPkcs7 returns a CryptoConfig to encrypt with pkcs7 x509 certs func EncryptWithPkcs7(x509s [][]byte) (CryptoConfig, error) { dc := DecryptConfig{} ep := map[string][][]byte{ "x509s": x509s, } return CryptoConfig{ EncryptConfig: &EncryptConfig{ Parameters: ep, DecryptConfig: dc, }, DecryptConfig: &dc, }, nil } // EncryptWithGpg returns a CryptoConfig to encrypt with configured gpg parameters func EncryptWithGpg(gpgRecipients [][]byte, gpgPubRingFile []byte) (CryptoConfig, error) { dc := DecryptConfig{} ep := map[string][][]byte{ "gpg-recipients": gpgRecipients, "gpg-pubkeyringfile": {gpgPubRingFile}, } return CryptoConfig{ EncryptConfig: &EncryptConfig{ Parameters: ep, DecryptConfig: dc, }, DecryptConfig: &dc, }, nil } // EncryptWithPkcs11 returns a CryptoConfig to encrypt with configured pkcs11 parameters func EncryptWithPkcs11(pkcs11Config *pkcs11.Pkcs11Config, pkcs11Pubkeys, pkcs11Yamls [][]byte) (CryptoConfig, error) { dc := DecryptConfig{} ep := map[string][][]byte{} if len(pkcs11Yamls) > 0 { if pkcs11Config == nil { return CryptoConfig{}, errors.New("pkcs11Config must not be nil") } p11confYaml, err := yaml.Marshal(pkcs11Config) if err != nil { return CryptoConfig{}, fmt.Errorf("Could not marshal Pkcs11Config to Yaml: %w", err) } dc = DecryptConfig{ Parameters: map[string][][]byte{ "pkcs11-config": {p11confYaml}, }, } ep["pkcs11-yamls"] = pkcs11Yamls } if len(pkcs11Pubkeys) > 0 { ep["pkcs11-pubkeys"] = pkcs11Pubkeys } return CryptoConfig{ EncryptConfig: &EncryptConfig{ Parameters: ep, DecryptConfig: dc, }, DecryptConfig: &dc, }, nil } // EncryptWithKeyProvider returns a CryptoConfig to encrypt with configured keyprovider parameters func EncryptWithKeyProvider(keyProviders [][]byte) (CryptoConfig, error) { dc := DecryptConfig{} ep := make(map[string][][]byte) for _, keyProvider := range keyProviders { keyProvidersStr := string(keyProvider) idx := strings.Index(keyProvidersStr, ":") if idx > 0 { ep[keyProvidersStr[:idx]] = append(ep[keyProvidersStr[:idx]], []byte(keyProvidersStr[idx+1:])) } else { ep[keyProvidersStr] = append(ep[keyProvidersStr], []byte("Enabled")) } } return CryptoConfig{ EncryptConfig: &EncryptConfig{ Parameters: ep, DecryptConfig: dc, }, DecryptConfig: &dc, }, nil } // DecryptWithKeyProvider returns a CryptoConfig to decrypt with configured keyprovider parameters func DecryptWithKeyProvider(keyProviders [][]byte) (CryptoConfig, error) { dp := make(map[string][][]byte) ep := map[string][][]byte{} for _, keyProvider := range keyProviders { keyProvidersStr := string(keyProvider) idx := strings.Index(keyProvidersStr, ":") if idx > 0 { dp[keyProvidersStr[:idx]] = append(dp[keyProvidersStr[:idx]], []byte(keyProvidersStr[idx+1:])) } else { dp[keyProvidersStr] = append(dp[keyProvidersStr], []byte("Enabled")) } } dc := DecryptConfig{ Parameters: dp, } return CryptoConfig{ EncryptConfig: &EncryptConfig{ Parameters: ep, DecryptConfig: dc, }, DecryptConfig: &dc, }, nil } // DecryptWithPrivKeys returns a CryptoConfig to decrypt with configured private keys func DecryptWithPrivKeys(privKeys [][]byte, privKeysPasswords [][]byte) (CryptoConfig, error) { if len(privKeys) != len(privKeysPasswords) { return CryptoConfig{}, errors.New("Length of privKeys should match length of privKeysPasswords") } dc := DecryptConfig{ Parameters: map[string][][]byte{ "privkeys": privKeys, "privkeys-passwords": privKeysPasswords, }, } ep := map[string][][]byte{} return CryptoConfig{ EncryptConfig: &EncryptConfig{ Parameters: ep, DecryptConfig: dc, }, DecryptConfig: &dc, }, nil } // DecryptWithX509s returns a CryptoConfig to decrypt with configured x509 certs func DecryptWithX509s(x509s [][]byte) (CryptoConfig, error) { dc := DecryptConfig{ Parameters: map[string][][]byte{ "x509s": x509s, }, } ep := map[string][][]byte{} return CryptoConfig{ EncryptConfig: &EncryptConfig{ Parameters: ep, DecryptConfig: dc, }, DecryptConfig: &dc, }, nil } // DecryptWithGpgPrivKeys returns a CryptoConfig to decrypt with configured gpg private keys func DecryptWithGpgPrivKeys(gpgPrivKeys, gpgPrivKeysPwds [][]byte) (CryptoConfig, error) { dc := DecryptConfig{ Parameters: map[string][][]byte{ "gpg-privatekeys": gpgPrivKeys, "gpg-privatekeys-passwords": gpgPrivKeysPwds, }, } ep := map[string][][]byte{} return CryptoConfig{ EncryptConfig: &EncryptConfig{ Parameters: ep, DecryptConfig: dc, }, DecryptConfig: &dc, }, nil } // DecryptWithPkcs11Yaml returns a CryptoConfig to decrypt with pkcs11 YAML formatted key files func DecryptWithPkcs11Yaml(pkcs11Config *pkcs11.Pkcs11Config, pkcs11Yamls [][]byte) (CryptoConfig, error) { p11confYaml, err := yaml.Marshal(pkcs11Config) if err != nil { return CryptoConfig{}, fmt.Errorf("Could not marshal Pkcs11Config to Yaml: %w", err) } dc := DecryptConfig{ Parameters: map[string][][]byte{ "pkcs11-yamls": pkcs11Yamls, "pkcs11-config": {p11confYaml}, }, } ep := map[string][][]byte{} return CryptoConfig{ EncryptConfig: &EncryptConfig{ Parameters: ep, DecryptConfig: dc, }, DecryptConfig: &dc, }, nil } ================================================ FILE: vendor/github.com/containers/ocicrypt/config/keyprovider-config/config.go ================================================ /* Copyright The ocicrypt Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "encoding/json" "fmt" "os" ) // Command describes the structure of command, it consist of path and args, where path defines the location of // binary executable and args are passed on to the binary executable type Command struct { Path string `json:"path,omitempty"` Args []string `json:"args,omitempty"` } // KeyProviderAttrs describes the structure of key provider, it defines the way of invocation to key provider type KeyProviderAttrs struct { Command *Command `json:"cmd,omitempty"` Grpc string `json:"grpc,omitempty"` } // OcicryptConfig represents the format of an ocicrypt_provider.conf config file type OcicryptConfig struct { KeyProviderConfig map[string]KeyProviderAttrs `json:"key-providers"` } const ENVVARNAME = "OCICRYPT_KEYPROVIDER_CONFIG" // parseConfigFile parses a configuration file; it is not an error if the configuration file does // not exist, so no error is returned. func parseConfigFile(filename string) (*OcicryptConfig, error) { // a non-existent config file is not an error _, err := os.Stat(filename) if os.IsNotExist(err) { return nil, nil } data, err := os.ReadFile(filename) if err != nil { return nil, err } ic := &OcicryptConfig{} err = json.Unmarshal(data, ic) return ic, err } // getConfiguration tries to read the configuration file at the following locations // ${OCICRYPT_KEYPROVIDER_CONFIG} == "/etc/ocicrypt_keyprovider.yaml" // If no configuration file could be found or read a null pointer is returned func GetConfiguration() (*OcicryptConfig, error) { var ic *OcicryptConfig var err error filename := os.Getenv(ENVVARNAME) if len(filename) > 0 { ic, err = parseConfigFile(filename) if err != nil { return nil, fmt.Errorf("Error while parsing keyprovider config file: %w", err) } } else { return nil, nil } return ic, nil } ================================================ FILE: vendor/github.com/containers/ocicrypt/config/pkcs11config/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 pkcs11config import ( "errors" "fmt" "os" "path" "github.com/containers/ocicrypt/crypto/pkcs11" "gopkg.in/yaml.v3" ) // OcicryptConfig represents the format of an imgcrypt.conf config file type OcicryptConfig struct { Pkcs11Config pkcs11.Pkcs11Config `yaml:"pkcs11"` } const CONFIGFILE = "ocicrypt.conf" const ENVVARNAME = "OCICRYPT_CONFIG" // parseConfigFile parses a configuration file; it is not an error if the configuration file does // not exist, so no error is returned. // A config file may look like this: // module-directories: // - /usr/lib64/pkcs11/ // - /usr/lib/pkcs11/ // allowed-module-paths: // - /usr/lib64/pkcs11/ // - /usr/lib/pkcs11/ func parseConfigFile(filename string) (*OcicryptConfig, error) { // a non-existent config file is not an error _, err := os.Stat(filename) if os.IsNotExist(err) { return nil, nil } data, err := os.ReadFile(filename) if err != nil { return nil, err } ic := &OcicryptConfig{} err = yaml.Unmarshal(data, ic) return ic, err } // getConfiguration tries to read the configuration file at the following locations // 1) ${OCICRYPT_CONFIG} == "internal": use internal default allow-all policy // 2) ${OCICRYPT_CONFIG} // 3) ${XDG_CONFIG_HOME}/ocicrypt-pkcs11.conf // 4) ${HOME}/.config/ocicrypt-pkcs11.conf // 5) /etc/ocicrypt-pkcs11.conf // If no configuration file could be found or read a null pointer is returned func getConfiguration() (*OcicryptConfig, error) { filename := os.Getenv(ENVVARNAME) if len(filename) > 0 { if filename == "internal" { return getDefaultCryptoConfigOpts() } ic, err := parseConfigFile(filename) if err != nil || ic != nil { return ic, err } } envvar := os.Getenv("XDG_CONFIG_HOME") if len(envvar) > 0 { ic, err := parseConfigFile(path.Join(envvar, CONFIGFILE)) if err != nil || ic != nil { return ic, err } } envvar = os.Getenv("HOME") if len(envvar) > 0 { ic, err := parseConfigFile(path.Join(envvar, ".config", CONFIGFILE)) if err != nil || ic != nil { return ic, err } } return parseConfigFile(path.Join("etc", CONFIGFILE)) } // getDefaultCryptoConfigOpts returns default crypto config opts needed for pkcs11 module access func getDefaultCryptoConfigOpts() (*OcicryptConfig, error) { mdyaml := pkcs11.GetDefaultModuleDirectoriesYaml("") config := fmt.Sprintf("module-directories:\n"+ "%s"+ "allowed-module-paths:\n"+ "%s", mdyaml, mdyaml) p11conf, err := pkcs11.ParsePkcs11ConfigFile([]byte(config)) return &OcicryptConfig{ Pkcs11Config: *p11conf, }, err } // GetUserPkcs11Config gets the user's Pkcs11Conig either from a configuration file or if none is // found the default ones are returned func GetUserPkcs11Config() (*pkcs11.Pkcs11Config, error) { fmt.Print("Note: pkcs11 support is currently experimental\n") ic, err := getConfiguration() if err != nil { return &pkcs11.Pkcs11Config{}, err } if ic == nil { return &pkcs11.Pkcs11Config{}, errors.New("No ocicrypt config file was found") } return &ic.Pkcs11Config, nil } ================================================ FILE: vendor/github.com/containers/ocicrypt/crypto/pkcs11/common.go ================================================ /* Copyright The ocicrypt Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package pkcs11 import ( "fmt" pkcs11uri "github.com/stefanberger/go-pkcs11uri" "gopkg.in/yaml.v3" ) // Pkcs11KeyFile describes the format of the pkcs11 (private) key file. // It also carries pkcs11 module related environment variables that are transferred to the // Pkcs11URI object and activated when the pkcs11 module is used. type Pkcs11KeyFile struct { Pkcs11 struct { Uri string `yaml:"uri"` } `yaml:"pkcs11"` Module struct { Env map[string]string `yaml:"env,omitempty"` } `yaml:"module"` } // Pkcs11KeyFileObject is a representation of the Pkcs11KeyFile with the pkcs11 URI as an object type Pkcs11KeyFileObject struct { Uri *pkcs11uri.Pkcs11URI } // ParsePkcs11Uri parses a pkcs11 URI func ParsePkcs11Uri(uri string) (*pkcs11uri.Pkcs11URI, error) { p11uri := pkcs11uri.New() err := p11uri.Parse(uri) if err != nil { return nil, fmt.Errorf("Could not parse Pkcs11URI from file: %w", err) } return p11uri, err } // ParsePkcs11KeyFile parses a pkcs11 key file holding a pkcs11 URI describing a private key. // The file has the following yaml format: // pkcs11: // - uri : // An error is returned if the pkcs11 URI is malformed func ParsePkcs11KeyFile(yamlstr []byte) (*Pkcs11KeyFileObject, error) { p11keyfile := Pkcs11KeyFile{} err := yaml.Unmarshal(yamlstr, &p11keyfile) if err != nil { return nil, fmt.Errorf("Could not unmarshal pkcs11 keyfile: %w", err) } p11uri, err := ParsePkcs11Uri(p11keyfile.Pkcs11.Uri) if err != nil { return nil, err } p11uri.SetEnvMap(p11keyfile.Module.Env) return &Pkcs11KeyFileObject{Uri: p11uri}, err } // IsPkcs11PrivateKey checks whether the given YAML represents a Pkcs11 private key func IsPkcs11PrivateKey(yamlstr []byte) bool { _, err := ParsePkcs11KeyFile(yamlstr) return err == nil } // IsPkcs11PublicKey checks whether the given YAML represents a Pkcs11 public key func IsPkcs11PublicKey(yamlstr []byte) bool { _, err := ParsePkcs11KeyFile(yamlstr) return err == nil } // Pkcs11Config describes the layout of a pkcs11 config file // The file has the following yaml format: // module-directories: // - /usr/lib64/pkcs11/ // allowd-module-paths // - /usr/lib64/pkcs11/libsofthsm2.so type Pkcs11Config struct { ModuleDirectories []string `yaml:"module-directories"` AllowedModulePaths []string `yaml:"allowed-module-paths"` } // GetDefaultModuleDirectories returns module directories covering // a variety of Linux distros func GetDefaultModuleDirectories() []string { dirs := []string{ "/usr/lib64/pkcs11/", // Fedora,RHEL,openSUSE "/usr/lib/pkcs11/", // Fedora,ArchLinux "/usr/local/lib/pkcs11/", "/usr/lib/softhsm/", // Debian,Ubuntu } // Debian directory: /usr/lib/(x86_64|aarch64|arm|powerpc64le|riscv64|s390x)-linux-gnu/ hosttype, ostype, q := getHostAndOsType() if len(hosttype) > 0 { dir := fmt.Sprintf("/usr/lib/%s-%s-%s/", hosttype, ostype, q) dirs = append(dirs, dir) } return dirs } // GetDefaultModuleDirectoresFormatted returns the default module directories formatted for YAML func GetDefaultModuleDirectoriesYaml(indent string) string { res := "" for _, dir := range GetDefaultModuleDirectories() { res += indent + "- " + dir + "\n" } return res } // ParsePkcs11ConfigFile parses a pkcs11 config file hat influences the module search behavior // as well as the set of modules that users are allowed to use func ParsePkcs11ConfigFile(yamlstr []byte) (*Pkcs11Config, error) { p11conf := Pkcs11Config{} err := yaml.Unmarshal(yamlstr, &p11conf) if err != nil { return &p11conf, fmt.Errorf("Could not parse Pkcs11Config: %w", err) } return &p11conf, nil } ================================================ FILE: vendor/github.com/containers/ocicrypt/crypto/pkcs11/pkcs11helpers.go ================================================ //go:build cgo // +build cgo /* Copyright The ocicrypt Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package pkcs11 import ( "crypto/rand" "crypto/rsa" "crypto/sha1" "crypto/sha256" "encoding/base64" "encoding/json" "errors" "fmt" "hash" "net/url" "os" "strconv" "strings" "github.com/miekg/pkcs11" pkcs11uri "github.com/stefanberger/go-pkcs11uri" ) var ( // OAEPLabel defines the label we use for OAEP encryption; this cannot be changed OAEPLabel = []byte("") // OAEPSha1Params describes the OAEP parameters with sha1 hash algorithm; needed by SoftHSM OAEPSha1Params = &pkcs11.OAEPParams{ HashAlg: pkcs11.CKM_SHA_1, MGF: pkcs11.CKG_MGF1_SHA1, SourceType: pkcs11.CKZ_DATA_SPECIFIED, SourceData: OAEPLabel, } // OAEPSha256Params describes the OAEP parameters with sha256 hash algorithm OAEPSha256Params = &pkcs11.OAEPParams{ HashAlg: pkcs11.CKM_SHA256, MGF: pkcs11.CKG_MGF1_SHA256, SourceType: pkcs11.CKZ_DATA_SPECIFIED, SourceData: OAEPLabel, } ) // rsaPublicEncryptOAEP encrypts the given plaintext with the given *rsa.PublicKey; the // environment variable OCICRYPT_OAEP_HASHALG can be set to 'sha1' to force usage of sha1 for OAEP (SoftHSM). // This function is needed by clients who are using a public key file for pkcs11 encryption func rsaPublicEncryptOAEP(pubKey *rsa.PublicKey, plaintext []byte) ([]byte, string, error) { var ( hashfunc hash.Hash hashalg string ) oaephash := os.Getenv("OCICRYPT_OAEP_HASHALG") // The default is sha256 (previously was sha1) switch strings.ToLower(oaephash) { case "sha1": hashfunc = sha1.New() hashalg = "sha1" case "sha256", "": hashfunc = sha256.New() hashalg = "sha256" default: return nil, "", fmt.Errorf("Unsupported OAEP hash '%s'", oaephash) } ciphertext, err := rsa.EncryptOAEP(hashfunc, rand.Reader, pubKey, plaintext, OAEPLabel) if err != nil { return nil, "", fmt.Errorf("rss.EncryptOAEP failed: %w", err) } return ciphertext, hashalg, nil } // pkcs11UriGetLoginParameters gets the parameters necessary for login from the Pkcs11URI // PIN and module are mandatory; slot-id is optional and if not found -1 will be returned // For a privateKeyOperation a PIN is required and if none is given, this function will return an error func pkcs11UriGetLoginParameters(p11uri *pkcs11uri.Pkcs11URI, privateKeyOperation bool) (string, string, int64, error) { var ( pin string err error ) if privateKeyOperation { if !p11uri.HasPIN() { return "", "", 0, errors.New("Missing PIN for private key operation") } } // some devices require a PIN to find a *public* key object, others don't pin, _ = p11uri.GetPIN() module, err := p11uri.GetModule() if err != nil { return "", "", 0, fmt.Errorf("No module available in pkcs11 URI: %w", err) } slotid := int64(-1) slot, ok := p11uri.GetPathAttribute("slot-id", false) if ok { slotid, err = strconv.ParseInt(slot, 10, 64) if err != nil { return "", "", 0, fmt.Errorf("slot-id is not a valid number: %w", err) } if slotid < 0 { return "", "", 0, fmt.Errorf("slot-id is a negative number") } if uint64(slotid) > 0xffffffff { return "", "", 0, fmt.Errorf("slot-id is larger than 32 bit") } } return pin, module, slotid, nil } // pkcs11UriGetKeyIdAndLabel gets the key label by retrieving the value of the 'object' attribute func pkcs11UriGetKeyIdAndLabel(p11uri *pkcs11uri.Pkcs11URI) (string, string, error) { keyid, ok2 := p11uri.GetPathAttribute("id", false) label, ok1 := p11uri.GetPathAttribute("object", false) if !ok1 && !ok2 { return "", "", errors.New("Neither 'id' nor 'object' attributes were found in pkcs11 URI") } return keyid, label, nil } // pkcs11OpenSession opens a session with a pkcs11 device at the given slot and logs in with the given PIN func pkcs11OpenSession(p11ctx *pkcs11.Ctx, slotid uint, pin string) (session pkcs11.SessionHandle, err error) { session, err = p11ctx.OpenSession(slotid, pkcs11.CKF_SERIAL_SESSION|pkcs11.CKF_RW_SESSION) if err != nil { return 0, fmt.Errorf("OpenSession to slot %d failed: %w", slotid, err) } if len(pin) > 0 { err = p11ctx.Login(session, pkcs11.CKU_USER, pin) if err != nil { _ = p11ctx.CloseSession(session) return 0, fmt.Errorf("Could not login to device: %w", err) } } return session, nil } // pkcs11UriLogin uses the given pkcs11 URI to select the pkcs11 module (shared library) and to get // the PIN to use for login; if the URI contains a slot-id, the given slot-id will be used, otherwise // one slot after the other will be attempted and the first one where login succeeds will be used func pkcs11UriLogin(p11uri *pkcs11uri.Pkcs11URI, privateKeyOperation bool) (ctx *pkcs11.Ctx, session pkcs11.SessionHandle, err error) { pin, module, slotid, err := pkcs11UriGetLoginParameters(p11uri, privateKeyOperation) if err != nil { return nil, 0, err } p11ctx := pkcs11.New(module) if p11ctx == nil { return nil, 0, errors.New("Please check module path, input is: " + module) } err = p11ctx.Initialize() if err != nil { p11Err := err.(pkcs11.Error) if p11Err != pkcs11.CKR_CRYPTOKI_ALREADY_INITIALIZED { return nil, 0, fmt.Errorf("Initialize failed: %w", err) } } if slotid >= 0 { session, err := pkcs11OpenSession(p11ctx, uint(slotid), pin) return p11ctx, session, err } slots, err := p11ctx.GetSlotList(true) if err != nil { return nil, 0, fmt.Errorf("GetSlotList failed: %w", err) } tokenlabel, ok := p11uri.GetPathAttribute("token", false) if !ok { return nil, 0, errors.New("Missing 'token' attribute since 'slot-id' was not given") } for _, slot := range slots { ti, err := p11ctx.GetTokenInfo(slot) if err != nil || ti.Label != tokenlabel { continue } session, err = pkcs11OpenSession(p11ctx, slot, pin) if err == nil { return p11ctx, session, err } } if len(pin) > 0 { return nil, 0, errors.New("Could not create session to any slot and/or log in") } return nil, 0, errors.New("Could not create session to any slot") } func pkcs11Logout(ctx *pkcs11.Ctx, session pkcs11.SessionHandle) { _ = ctx.Logout(session) _ = ctx.CloseSession(session) _ = ctx.Finalize() ctx.Destroy() } // findObject finds an object of the given class with the given keyid and/or label func findObject(p11ctx *pkcs11.Ctx, session pkcs11.SessionHandle, class uint, keyid, label string) (pkcs11.ObjectHandle, error) { msg := "" template := []*pkcs11.Attribute{ pkcs11.NewAttribute(pkcs11.CKA_CLASS, class), } if len(label) > 0 { template = append(template, pkcs11.NewAttribute(pkcs11.CKA_LABEL, label)) msg = fmt.Sprintf("label '%s'", label) } if len(keyid) > 0 { template = append(template, pkcs11.NewAttribute(pkcs11.CKA_ID, keyid)) if len(msg) > 0 { msg += " and " } msg += url.PathEscape(keyid) } if err := p11ctx.FindObjectsInit(session, template); err != nil { return 0, fmt.Errorf("FindObjectsInit failed: %w", err) } obj, _, err := p11ctx.FindObjects(session, 100) if err != nil { return 0, fmt.Errorf("FindObjects failed: %w", err) } if err := p11ctx.FindObjectsFinal(session); err != nil { return 0, fmt.Errorf("FindObjectsFinal failed: %w", err) } if len(obj) > 1 { return 0, fmt.Errorf("There are too many (=%d) keys with %s", len(obj), msg) } else if len(obj) == 1 { return obj[0], nil } return 0, fmt.Errorf("Could not find any object with %s", msg) } // publicEncryptOAEP uses a public key described by a pkcs11 URI to OAEP encrypt the given plaintext func publicEncryptOAEP(pubKey *Pkcs11KeyFileObject, plaintext []byte) ([]byte, string, error) { oldenv, err := setEnvVars(pubKey.Uri.GetEnvMap()) if err != nil { return nil, "", err } defer restoreEnv(oldenv) p11ctx, session, err := pkcs11UriLogin(pubKey.Uri, false) if err != nil { return nil, "", err } defer pkcs11Logout(p11ctx, session) keyid, label, err := pkcs11UriGetKeyIdAndLabel(pubKey.Uri) if err != nil { return nil, "", err } p11PubKey, err := findObject(p11ctx, session, pkcs11.CKO_PUBLIC_KEY, keyid, label) if err != nil { return nil, "", err } var hashalg string var oaep *pkcs11.OAEPParams oaephash := os.Getenv("OCICRYPT_OAEP_HASHALG") // The default is sha256 (previously was sha1) switch strings.ToLower(oaephash) { case "sha1": oaep = OAEPSha1Params hashalg = "sha1" case "sha256", "": oaep = OAEPSha256Params hashalg = "sha256" default: return nil, "", fmt.Errorf("Unsupported OAEP hash '%s'", oaephash) } err = p11ctx.EncryptInit(session, []*pkcs11.Mechanism{pkcs11.NewMechanism(pkcs11.CKM_RSA_PKCS_OAEP, oaep)}, p11PubKey) if err != nil { return nil, "", fmt.Errorf("EncryptInit error: %w", err) } ciphertext, err := p11ctx.Encrypt(session, plaintext) if err != nil { return nil, "", fmt.Errorf("Encrypt failed: %w", err) } return ciphertext, hashalg, nil } // privateDecryptOAEP uses a pkcs11 URI describing a private key to OAEP decrypt a ciphertext func privateDecryptOAEP(privKeyObj *Pkcs11KeyFileObject, ciphertext []byte, hashalg string) ([]byte, error) { oldenv, err := setEnvVars(privKeyObj.Uri.GetEnvMap()) if err != nil { return nil, err } defer restoreEnv(oldenv) p11ctx, session, err := pkcs11UriLogin(privKeyObj.Uri, true) if err != nil { return nil, err } defer pkcs11Logout(p11ctx, session) keyid, label, err := pkcs11UriGetKeyIdAndLabel(privKeyObj.Uri) if err != nil { return nil, err } p11PrivKey, err := findObject(p11ctx, session, pkcs11.CKO_PRIVATE_KEY, keyid, label) if err != nil { return nil, err } var oaep *pkcs11.OAEPParams // An empty string from the Hash in the JSON historically defaults to sha1. switch hashalg { case "sha1", "": oaep = OAEPSha1Params case "sha256": oaep = OAEPSha256Params default: return nil, fmt.Errorf("Unsupported hash algorithm '%s' for decryption", hashalg) } err = p11ctx.DecryptInit(session, []*pkcs11.Mechanism{pkcs11.NewMechanism(pkcs11.CKM_RSA_PKCS_OAEP, oaep)}, p11PrivKey) if err != nil { return nil, fmt.Errorf("DecryptInit failed: %w", err) } plaintext, err := p11ctx.Decrypt(session, ciphertext) if err != nil { return nil, fmt.Errorf("Decrypt failed: %w", err) } return plaintext, err } // // The following part deals with the JSON formatted message for multiple pkcs11 recipients // // Pkcs11Blob holds the encrypted blobs for all recipients; this is what we will put into the image's annotations type Pkcs11Blob struct { Version uint `json:"version"` Recipients []Pkcs11Recipient `json:"recipients"` } // Pkcs11Recipient holds the b64-encoded and encrypted blob for a particular recipient type Pkcs11Recipient struct { Version uint `json:"version"` Blob string `json:"blob"` Hash string `json:"hash,omitempty"` } // EncryptMultiple encrypts for one or multiple pkcs11 devices; the public keys passed to this function // may either be *rsa.PublicKey or *pkcs11uri.Pkcs11URI; the returned byte array is a JSON string of the // following format: // { // recipients: [ // recipient list // { // "version": 0, // "blob": , // "hash": // } , // { // "version": 0, // "blob": , // "hash": // } , // [...] // ] // } func EncryptMultiple(pubKeys []interface{}, data []byte) ([]byte, error) { var ( ciphertext []byte err error pkcs11blob Pkcs11Blob = Pkcs11Blob{Version: 0} hashalg string ) for _, pubKey := range pubKeys { switch pkey := pubKey.(type) { case *rsa.PublicKey: ciphertext, hashalg, err = rsaPublicEncryptOAEP(pkey, data) case *Pkcs11KeyFileObject: ciphertext, hashalg, err = publicEncryptOAEP(pkey, data) default: err = fmt.Errorf("Unsupported key object type for pkcs11 public key") } if err != nil { return nil, err } recipient := Pkcs11Recipient{ Version: 0, Blob: base64.StdEncoding.EncodeToString(ciphertext), Hash: hashalg, } pkcs11blob.Recipients = append(pkcs11blob.Recipients, recipient) } return json.Marshal(&pkcs11blob) } // Decrypt tries to decrypt one of the recipients' blobs using a pkcs11 private key. // The input pkcs11blobstr is a string with the following format: // { // recipients: [ // recipient list // { // "version": 0, // "blob": , // "hash": // } , // { // "version": 0, // "blob": , // "hash": // } , // [...] // } // Note: More recent versions of this code explicitly write 'sha1' // while older versions left it empty in case of 'sha1'. func Decrypt(privKeyObjs []*Pkcs11KeyFileObject, pkcs11blobstr []byte) ([]byte, error) { pkcs11blob := Pkcs11Blob{} err := json.Unmarshal(pkcs11blobstr, &pkcs11blob) if err != nil { return nil, fmt.Errorf("Could not parse Pkcs11Blob: %w", err) } switch pkcs11blob.Version { case 0: // latest supported version default: return nil, fmt.Errorf("found Pkcs11Blob with version %d but maximum supported version is 0", pkcs11blob.Version) } // since we do trial and error, collect all encountered errors errs := "" for _, recipient := range pkcs11blob.Recipients { switch recipient.Version { case 0: // last supported version default: return nil, fmt.Errorf("found Pkcs11Recipient with version %d but maximum supported version is 0", recipient.Version) } ciphertext, err := base64.StdEncoding.DecodeString(recipient.Blob) if err != nil || len(ciphertext) == 0 { // This should never happen... we skip over decoding issues errs += fmt.Sprintf("Base64 decoding failed: %s\n", err) continue } // try all keys until one works for _, privKeyObj := range privKeyObjs { plaintext, err := privateDecryptOAEP(privKeyObj, ciphertext, recipient.Hash) if err == nil { return plaintext, nil } if uri, err2 := privKeyObj.Uri.Format(); err2 == nil { errs += fmt.Sprintf("%s : %s\n", uri, err) } else { errs += fmt.Sprintf("%s\n", err) } } } return nil, fmt.Errorf("Could not find a pkcs11 key for decryption:\n%s", errs) } ================================================ FILE: vendor/github.com/containers/ocicrypt/crypto/pkcs11/pkcs11helpers_nocgo.go ================================================ //go:build !cgo // +build !cgo /* Copyright The ocicrypt Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package pkcs11 import "fmt" func EncryptMultiple(pubKeys []interface{}, data []byte) ([]byte, error) { return nil, fmt.Errorf("ocicrypt pkcs11 not supported on this build") } func Decrypt(privKeyObjs []*Pkcs11KeyFileObject, pkcs11blobstr []byte) ([]byte, error) { return nil, fmt.Errorf("ocicrypt pkcs11 not supported on this build") } ================================================ FILE: vendor/github.com/containers/ocicrypt/crypto/pkcs11/utils.go ================================================ /* Copyright The ocicrypt Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package pkcs11 import ( "fmt" "os" "runtime" "strings" "sync" ) var ( envLock sync.Mutex ) // setEnvVars sets the environment variables given in the map and locks the environment from // modification with the same function; if successful, you *must* call restoreEnv with the return // value from this function func setEnvVars(env map[string]string) ([]string, error) { envLock.Lock() if len(env) == 0 { return nil, nil } oldenv := os.Environ() for k, v := range env { err := os.Setenv(k, v) if err != nil { restoreEnv(oldenv) return nil, fmt.Errorf("Could not set environment variable '%s' to '%s': %w", k, v, err) } } return oldenv, nil } func arrayToMap(elements []string) map[string]string { o := make(map[string]string) for _, element := range elements { p := strings.SplitN(element, "=", 2) if len(p) == 2 { o[p[0]] = p[1] } } return o } // restoreEnv restores the environment to be exactly as given in the array of strings // and unlocks the lock func restoreEnv(envs []string) { if envs != nil && len(envs) >= 0 { target := arrayToMap(envs) curr := arrayToMap(os.Environ()) for nc, vc := range curr { vt, ok := target[nc] if !ok { os.Unsetenv(nc) } else if vc == vt { delete(target, nc) } } for nt, vt := range target { os.Setenv(nt, vt) } } envLock.Unlock() } func getHostAndOsType() (string, string, string) { ht := "" ot := "" st := "" switch runtime.GOOS { case "linux": ot = "linux" st = "gnu" switch runtime.GOARCH { case "arm": ht = "arm" case "arm64": ht = "aarch64" case "amd64": ht = "x86_64" case "ppc64le": ht = "powerpc64le" case "riscv64": ht = "riscv64" case "s390x": ht = "s390x" } } return ht, ot, st } ================================================ FILE: vendor/github.com/containers/ocicrypt/encryption.go ================================================ /* Copyright The ocicrypt Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ocicrypt import ( "encoding/base64" "encoding/json" "errors" "fmt" "io" "strings" "github.com/containers/ocicrypt/blockcipher" "github.com/containers/ocicrypt/config" keyproviderconfig "github.com/containers/ocicrypt/config/keyprovider-config" "github.com/containers/ocicrypt/keywrap" "github.com/containers/ocicrypt/keywrap/jwe" "github.com/containers/ocicrypt/keywrap/keyprovider" "github.com/containers/ocicrypt/keywrap/pgp" "github.com/containers/ocicrypt/keywrap/pkcs11" "github.com/containers/ocicrypt/keywrap/pkcs7" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" log "github.com/sirupsen/logrus" ) // EncryptLayerFinalizer is a finalizer run to return the annotations to set for // the encrypted layer type EncryptLayerFinalizer func() (map[string]string, error) func init() { keyWrappers = make(map[string]keywrap.KeyWrapper) keyWrapperAnnotations = make(map[string]string) RegisterKeyWrapper("pgp", pgp.NewKeyWrapper()) RegisterKeyWrapper("jwe", jwe.NewKeyWrapper()) RegisterKeyWrapper("pkcs7", pkcs7.NewKeyWrapper()) RegisterKeyWrapper("pkcs11", pkcs11.NewKeyWrapper()) ic, err := keyproviderconfig.GetConfiguration() if err != nil { log.Error(err) } else if ic != nil { for provider, attrs := range ic.KeyProviderConfig { RegisterKeyWrapper("provider."+provider, keyprovider.NewKeyWrapper(provider, attrs)) } } } var keyWrappers map[string]keywrap.KeyWrapper var keyWrapperAnnotations map[string]string // RegisterKeyWrapper allows to register key wrappers by their encryption scheme func RegisterKeyWrapper(scheme string, iface keywrap.KeyWrapper) { keyWrappers[scheme] = iface keyWrapperAnnotations[iface.GetAnnotationID()] = scheme } // GetKeyWrapper looks up the encryptor interface given an encryption scheme (gpg, jwe) func GetKeyWrapper(scheme string) keywrap.KeyWrapper { return keyWrappers[scheme] } // GetWrappedKeysMap returns a map of wrappedKeys as values in a // map with the encryption scheme(s) as the key(s) func GetWrappedKeysMap(desc ocispec.Descriptor) map[string]string { wrappedKeysMap := make(map[string]string) for annotationsID, scheme := range keyWrapperAnnotations { if annotation, ok := desc.Annotations[annotationsID]; ok { wrappedKeysMap[scheme] = annotation } } return wrappedKeysMap } // EncryptLayer encrypts the layer by running one encryptor after the other func EncryptLayer(ec *config.EncryptConfig, encOrPlainLayerReader io.Reader, desc ocispec.Descriptor) (io.Reader, EncryptLayerFinalizer, error) { var ( encLayerReader io.Reader err error encrypted bool bcFin blockcipher.Finalizer privOptsData []byte pubOptsData []byte ) if ec == nil { return nil, nil, errors.New("EncryptConfig must not be nil") } for annotationsID := range keyWrapperAnnotations { annotation := desc.Annotations[annotationsID] if annotation != "" { privOptsData, err = decryptLayerKeyOptsData(&ec.DecryptConfig, desc) if err != nil { return nil, nil, err } pubOptsData, err = getLayerPubOpts(desc) if err != nil { return nil, nil, err } // already encrypted! encrypted = true } } if !encrypted { encLayerReader, bcFin, err = commonEncryptLayer(encOrPlainLayerReader, desc.Digest, blockcipher.AES256CTR) if err != nil { return nil, nil, err } } encLayerFinalizer := func() (map[string]string, error) { // If layer was already encrypted, bcFin should be nil, use existing optsData if bcFin != nil { opts, err := bcFin() if err != nil { return nil, err } privOptsData, err = json.Marshal(opts.Private) if err != nil { return nil, fmt.Errorf("could not JSON marshal opts: %w", err) } pubOptsData, err = json.Marshal(opts.Public) if err != nil { return nil, fmt.Errorf("could not JSON marshal opts: %w", err) } } newAnnotations := make(map[string]string) keysWrapped := false if len(keyWrapperAnnotations) == 0 { return nil, errors.New("missing Annotations needed for decryption") } for annotationsID, scheme := range keyWrapperAnnotations { b64Annotations := desc.Annotations[annotationsID] keywrapper := GetKeyWrapper(scheme) b64Annotations, err = preWrapKeys(keywrapper, ec, b64Annotations, privOptsData) if err != nil { return nil, err } if b64Annotations != "" { keysWrapped = true newAnnotations[annotationsID] = b64Annotations } } if !keysWrapped { return nil, errors.New("no wrapped keys produced by encryption") } newAnnotations["org.opencontainers.image.enc.pubopts"] = base64.StdEncoding.EncodeToString(pubOptsData) if len(newAnnotations) == 0 { return nil, errors.New("no encryptor found to handle encryption") } return newAnnotations, err } // if nothing was encrypted, we just return encLayer = nil return encLayerReader, encLayerFinalizer, err } // preWrapKeys calls WrapKeys and handles the base64 encoding and concatenation of the // annotation data func preWrapKeys(keywrapper keywrap.KeyWrapper, ec *config.EncryptConfig, b64Annotations string, optsData []byte) (string, error) { newAnnotation, err := keywrapper.WrapKeys(ec, optsData) if err != nil || len(newAnnotation) == 0 { return b64Annotations, err } b64newAnnotation := base64.StdEncoding.EncodeToString(newAnnotation) if b64Annotations == "" { return b64newAnnotation, nil } return b64Annotations + "," + b64newAnnotation, nil } // DecryptLayer decrypts a layer trying one keywrap.KeyWrapper after the other to see whether it // can apply the provided private key // If unwrapOnly is set we will only try to decrypt the layer encryption key and return func DecryptLayer(dc *config.DecryptConfig, encLayerReader io.Reader, desc ocispec.Descriptor, unwrapOnly bool) (io.Reader, digest.Digest, error) { if dc == nil { return nil, "", errors.New("DecryptConfig must not be nil") } privOptsData, err := decryptLayerKeyOptsData(dc, desc) if err != nil || unwrapOnly { return nil, "", err } var pubOptsData []byte pubOptsData, err = getLayerPubOpts(desc) if err != nil { return nil, "", err } return commonDecryptLayer(encLayerReader, privOptsData, pubOptsData) } func decryptLayerKeyOptsData(dc *config.DecryptConfig, desc ocispec.Descriptor) ([]byte, error) { privKeyGiven := false errs := "" if len(keyWrapperAnnotations) == 0 { return nil, errors.New("missing Annotations needed for decryption") } for annotationsID, scheme := range keyWrapperAnnotations { b64Annotation := desc.Annotations[annotationsID] if b64Annotation != "" { keywrapper := GetKeyWrapper(scheme) if keywrapper.NoPossibleKeys(dc.Parameters) { continue } if len(keywrapper.GetPrivateKeys(dc.Parameters)) > 0 { privKeyGiven = true } optsData, err := preUnwrapKey(keywrapper, dc, b64Annotation) if err != nil { // try next keywrap.KeyWrapper errs += fmt.Sprintf("%s\n", err) continue } if optsData == nil { // try next keywrap.KeyWrapper continue } return optsData, nil } } if !privKeyGiven { return nil, fmt.Errorf("missing private key needed for decryption:\n%s", errs) } return nil, fmt.Errorf("no suitable key unwrapper found or none of the private keys could be used for decryption:\n%s", errs) } func getLayerPubOpts(desc ocispec.Descriptor) ([]byte, error) { pubOptsString := desc.Annotations["org.opencontainers.image.enc.pubopts"] if pubOptsString == "" { return json.Marshal(blockcipher.PublicLayerBlockCipherOptions{}) } return base64.StdEncoding.DecodeString(pubOptsString) } // preUnwrapKey decodes the comma separated base64 strings and calls the Unwrap function // of the given keywrapper with it and returns the result in case the Unwrap functions // does not return an error. If all attempts fail, an error is returned. func preUnwrapKey(keywrapper keywrap.KeyWrapper, dc *config.DecryptConfig, b64Annotations string) ([]byte, error) { if b64Annotations == "" { return nil, nil } errs := "" for _, b64Annotation := range strings.Split(b64Annotations, ",") { annotation, err := base64.StdEncoding.DecodeString(b64Annotation) if err != nil { return nil, errors.New("could not base64 decode the annotation") } optsData, err := keywrapper.UnwrapKey(dc, annotation) if err != nil { errs += fmt.Sprintf("- %s\n", err) continue } return optsData, nil } return nil, fmt.Errorf("no suitable key found for decrypting layer key:\n%s", errs) } // commonEncryptLayer is a function to encrypt the plain layer using a new random // symmetric key and return the LayerBlockCipherHandler's JSON in string form for // later use during decryption func commonEncryptLayer(plainLayerReader io.Reader, d digest.Digest, typ blockcipher.LayerCipherType) (io.Reader, blockcipher.Finalizer, error) { lbch, err := blockcipher.NewLayerBlockCipherHandler() if err != nil { return nil, nil, err } encLayerReader, bcFin, err := lbch.Encrypt(plainLayerReader, typ) if err != nil { return nil, nil, err } newBcFin := func() (blockcipher.LayerBlockCipherOptions, error) { lbco, err := bcFin() if err != nil { return blockcipher.LayerBlockCipherOptions{}, err } lbco.Private.Digest = d return lbco, nil } return encLayerReader, newBcFin, err } // commonDecryptLayer decrypts an encrypted layer previously encrypted with commonEncryptLayer // by passing along the optsData func commonDecryptLayer(encLayerReader io.Reader, privOptsData []byte, pubOptsData []byte) (io.Reader, digest.Digest, error) { privOpts := blockcipher.PrivateLayerBlockCipherOptions{} err := json.Unmarshal(privOptsData, &privOpts) if err != nil { return nil, "", fmt.Errorf("could not JSON unmarshal privOptsData: %w", err) } lbch, err := blockcipher.NewLayerBlockCipherHandler() if err != nil { return nil, "", err } pubOpts := blockcipher.PublicLayerBlockCipherOptions{} if len(pubOptsData) > 0 { err := json.Unmarshal(pubOptsData, &pubOpts) if err != nil { return nil, "", fmt.Errorf("could not JSON unmarshal pubOptsData: %w", err) } } opts := blockcipher.LayerBlockCipherOptions{ Private: privOpts, Public: pubOpts, } plainLayerReader, opts, err := lbch.Decrypt(encLayerReader, opts) if err != nil { return nil, "", err } return plainLayerReader, opts.Private.Digest, nil } // FilterOutAnnotations filters out the annotations belonging to the image encryption 'namespace' // and returns a map with those taken out func FilterOutAnnotations(annotations map[string]string) map[string]string { a := make(map[string]string) if len(annotations) > 0 { for k, v := range annotations { if strings.HasPrefix(k, "org.opencontainers.image.enc.") { continue } a[k] = v } } return a } ================================================ FILE: vendor/github.com/containers/ocicrypt/gpg.go ================================================ /* Copyright The ocicrypt Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ocicrypt import ( "errors" "fmt" "io" "os" "os/exec" "regexp" "strconv" "strings" "sync" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "golang.org/x/term" ) // GPGVersion enum representing the GPG client version to use. type GPGVersion int const ( // GPGv2 signifies gpgv2+ GPGv2 GPGVersion = iota // GPGv1 signifies gpgv1+ GPGv1 // GPGVersionUndetermined signifies gpg client version undetermined GPGVersionUndetermined ) // GPGClient defines an interface for wrapping the gpg command line tools type GPGClient interface { // ReadGPGPubRingFile gets the byte sequence of the gpg public keyring ReadGPGPubRingFile() ([]byte, error) // GetGPGPrivateKey gets the private key bytes of a keyid given a passphrase GetGPGPrivateKey(keyid uint64, passphrase string) ([]byte, error) // GetSecretKeyDetails gets the details of a secret key GetSecretKeyDetails(keyid uint64) ([]byte, bool, error) // GetKeyDetails gets the details of a public key GetKeyDetails(keyid uint64) ([]byte, bool, error) // ResolveRecipients resolves PGP key ids to user names ResolveRecipients([]string) []string } // gpgClient contains generic gpg client information type gpgClient struct { gpgHomeDir string } // gpgv2Client is a gpg2 client type gpgv2Client struct { gpgClient } // gpgv1Client is a gpg client type gpgv1Client struct { gpgClient } // GuessGPGVersion guesses the version of gpg. Defaults to gpg2 if exists, if // not defaults to regular gpg. func GuessGPGVersion() GPGVersion { if err := exec.Command("gpg2", "--version").Run(); err == nil { return GPGv2 } else if err := exec.Command("gpg", "--version").Run(); err == nil { return GPGv1 } return GPGVersionUndetermined } // NewGPGClient creates a new GPGClient object representing the given version // and using the given home directory func NewGPGClient(gpgVersion, gpgHomeDir string) (GPGClient, error) { v := new(GPGVersion) switch gpgVersion { case "v1": *v = GPGv1 case "v2": *v = GPGv2 default: v = nil } return newGPGClient(v, gpgHomeDir) } func newGPGClient(version *GPGVersion, homedir string) (GPGClient, error) { var gpgVersion GPGVersion if version != nil { gpgVersion = *version } else { gpgVersion = GuessGPGVersion() } switch gpgVersion { case GPGv1: return &gpgv1Client{ gpgClient: gpgClient{gpgHomeDir: homedir}, }, nil case GPGv2: return &gpgv2Client{ gpgClient: gpgClient{gpgHomeDir: homedir}, }, nil case GPGVersionUndetermined: return nil, fmt.Errorf("unable to determine GPG version") default: return nil, fmt.Errorf("unhandled case: NewGPGClient") } } // GetGPGPrivateKey gets the bytes of a specified keyid, supplying a passphrase func (gc *gpgv2Client) GetGPGPrivateKey(keyid uint64, passphrase string) ([]byte, error) { var args []string if gc.gpgHomeDir != "" { args = append(args, []string{"--homedir", gc.gpgHomeDir}...) } rfile, wfile, err := os.Pipe() if err != nil { return nil, fmt.Errorf("could not create pipe: %w", err) } defer func() { rfile.Close() wfile.Close() }() // fill pipe in background go func(passphrase string) { _, _ = wfile.Write([]byte(passphrase)) wfile.Close() }(passphrase) args = append(args, []string{"--pinentry-mode", "loopback", "--batch", "--passphrase-fd", fmt.Sprintf("%d", 3), "--export-secret-key", fmt.Sprintf("0x%x", keyid)}...) cmd := exec.Command("gpg2", args...) cmd.ExtraFiles = []*os.File{rfile} return runGPGGetOutput(cmd) } // ReadGPGPubRingFile reads the GPG public key ring file func (gc *gpgv2Client) ReadGPGPubRingFile() ([]byte, error) { var args []string if gc.gpgHomeDir != "" { args = append(args, []string{"--homedir", gc.gpgHomeDir}...) } args = append(args, []string{"--batch", "--export"}...) cmd := exec.Command("gpg2", args...) return runGPGGetOutput(cmd) } func (gc *gpgv2Client) getKeyDetails(option string, keyid uint64) ([]byte, bool, error) { var args []string if gc.gpgHomeDir != "" { args = []string{"--homedir", gc.gpgHomeDir} } args = append(args, option, fmt.Sprintf("0x%x", keyid)) cmd := exec.Command("gpg2", args...) keydata, err := runGPGGetOutput(cmd) return keydata, err == nil, err } // GetSecretKeyDetails retrieves the secret key details of key with keyid. // returns a byte array of the details and a bool if the key exists func (gc *gpgv2Client) GetSecretKeyDetails(keyid uint64) ([]byte, bool, error) { return gc.getKeyDetails("-K", keyid) } // GetKeyDetails retrieves the public key details of key with keyid. // returns a byte array of the details and a bool if the key exists func (gc *gpgv2Client) GetKeyDetails(keyid uint64) ([]byte, bool, error) { return gc.getKeyDetails("-k", keyid) } // ResolveRecipients converts PGP keyids to email addresses, if possible func (gc *gpgv2Client) ResolveRecipients(recipients []string) []string { return resolveRecipients(gc, recipients) } // GetGPGPrivateKey gets the bytes of a specified keyid, supplying a passphrase func (gc *gpgv1Client) GetGPGPrivateKey(keyid uint64, _ string) ([]byte, error) { var args []string if gc.gpgHomeDir != "" { args = append(args, []string{"--homedir", gc.gpgHomeDir}...) } args = append(args, []string{"--batch", "--export-secret-key", fmt.Sprintf("0x%x", keyid)}...) cmd := exec.Command("gpg", args...) return runGPGGetOutput(cmd) } // ReadGPGPubRingFile reads the GPG public key ring file func (gc *gpgv1Client) ReadGPGPubRingFile() ([]byte, error) { var args []string if gc.gpgHomeDir != "" { args = append(args, []string{"--homedir", gc.gpgHomeDir}...) } args = append(args, []string{"--batch", "--export"}...) cmd := exec.Command("gpg", args...) return runGPGGetOutput(cmd) } func (gc *gpgv1Client) getKeyDetails(option string, keyid uint64) ([]byte, bool, error) { var args []string if gc.gpgHomeDir != "" { args = []string{"--homedir", gc.gpgHomeDir} } args = append(args, option, fmt.Sprintf("0x%x", keyid)) cmd := exec.Command("gpg", args...) keydata, err := runGPGGetOutput(cmd) return keydata, err == nil, err } // GetSecretKeyDetails retrieves the secret key details of key with keyid. // returns a byte array of the details and a bool if the key exists func (gc *gpgv1Client) GetSecretKeyDetails(keyid uint64) ([]byte, bool, error) { return gc.getKeyDetails("-K", keyid) } // GetKeyDetails retrieves the public key details of key with keyid. // returns a byte array of the details and a bool if the key exists func (gc *gpgv1Client) GetKeyDetails(keyid uint64) ([]byte, bool, error) { return gc.getKeyDetails("-k", keyid) } // ResolveRecipients converts PGP keyids to email addresses, if possible func (gc *gpgv1Client) ResolveRecipients(recipients []string) []string { return resolveRecipients(gc, recipients) } // runGPGGetOutput runs the GPG commandline and returns stdout as byte array // and any stderr in the error func runGPGGetOutput(cmd *exec.Cmd) ([]byte, error) { stdout, err := cmd.StdoutPipe() if err != nil { return nil, err } stderr, err := cmd.StderrPipe() if err != nil { return nil, err } if err := cmd.Start(); err != nil { return nil, err } stdoutstr, err2 := io.ReadAll(stdout) stderrstr, _ := io.ReadAll(stderr) if err := cmd.Wait(); err != nil { return nil, fmt.Errorf("error from %s: %s", cmd.Path, string(stderrstr)) } return stdoutstr, err2 } // resolveRecipients walks the list of recipients and attempts to convert // all keyIds to email addresses; if something goes wrong during the // conversion of a recipient, the original string is returned for that // recpient func resolveRecipients(gc GPGClient, recipients []string) []string { var result []string for _, recipient := range recipients { keyID, err := strconv.ParseUint(recipient, 0, 64) if err != nil { result = append(result, recipient) } else { details, found, _ := gc.GetKeyDetails(keyID) if !found { result = append(result, recipient) } else { email := extractEmailFromDetails(details) if email == "" { result = append(result, recipient) } else { result = append(result, email) } } } } return result } var ( onceRegexp sync.Once emailPattern *regexp.Regexp ) func extractEmailFromDetails(details []byte) string { onceRegexp.Do(func() { emailPattern = regexp.MustCompile(`uid\s+\[.*\]\s.*\s<(?P.+)>`) }) loc := emailPattern.FindSubmatchIndex(details) if len(loc) == 0 { return "" } return string(emailPattern.Expand(nil, []byte("$email"), details, loc)) } // uint64ToStringArray converts an array of uint64's to an array of strings // by applying a format string to each uint64 func uint64ToStringArray(format string, in []uint64) []string { var ret []string for _, v := range in { ret = append(ret, fmt.Sprintf(format, v)) } return ret } // GPGGetPrivateKey walks the list of layerInfos and tries to decrypt the // wrapped symmetric keys. For this it determines whether a private key is // in the GPGVault or on this system and prompts for the passwords for those // that are available. If we do not find a private key on the system for // getting to the symmetric key of a layer then an error is generated. func GPGGetPrivateKey(descs []ocispec.Descriptor, gpgClient GPGClient, gpgVault GPGVault, mustFindKey bool) (gpgPrivKeys [][]byte, gpgPrivKeysPwds [][]byte, err error) { // PrivateKeyData describes a private key type PrivateKeyData struct { KeyData []byte KeyDataPassword []byte } var pkd PrivateKeyData keyIDPasswordMap := make(map[uint64]PrivateKeyData) for _, desc := range descs { for scheme, b64pgpPackets := range GetWrappedKeysMap(desc) { if scheme != "pgp" { continue } keywrapper := GetKeyWrapper(scheme) if keywrapper == nil { return nil, nil, fmt.Errorf("could not get KeyWrapper for %s", scheme) } keyIds, err := keywrapper.GetKeyIdsFromPacket(b64pgpPackets) if err != nil { return nil, nil, err } found := false for _, keyid := range keyIds { // do we have this key? -- first check the vault if gpgVault != nil { _, keydata := gpgVault.GetGPGPrivateKey(keyid) if len(keydata) > 0 { pkd = PrivateKeyData{ KeyData: keydata, KeyDataPassword: nil, // password not supported in this case } keyIDPasswordMap[keyid] = pkd found = true break } } else if gpgClient != nil { // check the local system's gpg installation keyinfo, haveKey, _ := gpgClient.GetSecretKeyDetails(keyid) // this may fail if the key is not here; we ignore the error if !haveKey { // key not on this system continue } _, found = keyIDPasswordMap[keyid] if !found { fmt.Printf("Passphrase required for Key id 0x%x: \n%v", keyid, string(keyinfo)) fmt.Printf("Enter passphrase for key with Id 0x%x: ", keyid) password, err := term.ReadPassword(int(os.Stdin.Fd())) fmt.Printf("\n") if err != nil { return nil, nil, err } keydata, err := gpgClient.GetGPGPrivateKey(keyid, string(password)) if err != nil { return nil, nil, err } pkd = PrivateKeyData{ KeyData: keydata, KeyDataPassword: password, } keyIDPasswordMap[keyid] = pkd found = true } break } else { return nil, nil, errors.New("no GPGVault or GPGClient passed") } } if !found && len(b64pgpPackets) > 0 && mustFindKey { ids := uint64ToStringArray("0x%x", keyIds) return nil, nil, fmt.Errorf("missing key for decryption of layer %x of %s. Need one of the following keys: %s", desc.Digest, desc.Platform, strings.Join(ids, ", ")) } } } for _, pkd := range keyIDPasswordMap { gpgPrivKeys = append(gpgPrivKeys, pkd.KeyData) gpgPrivKeysPwds = append(gpgPrivKeysPwds, pkd.KeyDataPassword) } return gpgPrivKeys, gpgPrivKeysPwds, nil } ================================================ FILE: vendor/github.com/containers/ocicrypt/gpgvault.go ================================================ /* Copyright The ocicrypt Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ocicrypt import ( "bytes" "fmt" "os" "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/packet" ) // GPGVault defines an interface for wrapping multiple secret key rings type GPGVault interface { // AddSecretKeyRingData adds a secret keyring via its raw byte array AddSecretKeyRingData(gpgSecretKeyRingData []byte) error // AddSecretKeyRingDataArray adds secret keyring via its raw byte arrays AddSecretKeyRingDataArray(gpgSecretKeyRingDataArray [][]byte) error // AddSecretKeyRingFiles adds secret keyrings given their filenames AddSecretKeyRingFiles(filenames []string) error // GetGPGPrivateKey gets the private key bytes of a keyid given a passphrase GetGPGPrivateKey(keyid uint64) ([]openpgp.Key, []byte) } // gpgVault wraps an array of gpgSecretKeyRing type gpgVault struct { entityLists []openpgp.EntityList keyDataList [][]byte // the raw data original passed in } // NewGPGVault creates an empty GPGVault func NewGPGVault() GPGVault { return &gpgVault{} } // AddSecretKeyRingData adds a secret keyring's to the gpgVault; the raw byte // array read from the file must be passed and will be parsed by this function func (g *gpgVault) AddSecretKeyRingData(gpgSecretKeyRingData []byte) error { // read the private keys r := bytes.NewReader(gpgSecretKeyRingData) entityList, err := openpgp.ReadKeyRing(r) if err != nil { return fmt.Errorf("could not read keyring: %w", err) } g.entityLists = append(g.entityLists, entityList) g.keyDataList = append(g.keyDataList, gpgSecretKeyRingData) return nil } // AddSecretKeyRingDataArray adds secret keyrings to the gpgVault; the raw byte // arrays read from files must be passed func (g *gpgVault) AddSecretKeyRingDataArray(gpgSecretKeyRingDataArray [][]byte) error { for _, gpgSecretKeyRingData := range gpgSecretKeyRingDataArray { if err := g.AddSecretKeyRingData(gpgSecretKeyRingData); err != nil { return err } } return nil } // AddSecretKeyRingFiles adds the secret key rings given their filenames func (g *gpgVault) AddSecretKeyRingFiles(filenames []string) error { for _, filename := range filenames { gpgSecretKeyRingData, err := os.ReadFile(filename) if err != nil { return err } err = g.AddSecretKeyRingData(gpgSecretKeyRingData) if err != nil { return err } } return nil } // GetGPGPrivateKey gets the bytes of a specified keyid, supplying a passphrase func (g *gpgVault) GetGPGPrivateKey(keyid uint64) ([]openpgp.Key, []byte) { for i, el := range g.entityLists { decKeys := el.KeysByIdUsage(keyid, packet.KeyFlagEncryptCommunications) if len(decKeys) > 0 { return decKeys, g.keyDataList[i] } } return nil, nil } ================================================ FILE: vendor/github.com/containers/ocicrypt/helpers/parse_helpers.go ================================================ package helpers import ( "errors" "fmt" "os" "strconv" "strings" "github.com/containers/ocicrypt" encconfig "github.com/containers/ocicrypt/config" "github.com/containers/ocicrypt/config/pkcs11config" "github.com/containers/ocicrypt/crypto/pkcs11" encutils "github.com/containers/ocicrypt/utils" ) // processRecipientKeys sorts the array of recipients by type. Recipients may be either // x509 certificates, public keys, or PGP public keys identified by email address or name func processRecipientKeys(recipients []string) ([][]byte, [][]byte, [][]byte, [][]byte, [][]byte, [][]byte, error) { var ( gpgRecipients [][]byte pubkeys [][]byte x509s [][]byte pkcs11Pubkeys [][]byte pkcs11Yamls [][]byte keyProviders [][]byte ) for _, recipient := range recipients { idx := strings.Index(recipient, ":") if idx < 0 { return nil, nil, nil, nil, nil, nil, errors.New("Invalid recipient format") } protocol := recipient[:idx] value := recipient[idx+1:] switch protocol { case "pgp": gpgRecipients = append(gpgRecipients, []byte(value)) case "jwe": tmp, err := os.ReadFile(value) if err != nil { return nil, nil, nil, nil, nil, nil, fmt.Errorf("Unable to read file: %w", err) } if !encutils.IsPublicKey(tmp) { return nil, nil, nil, nil, nil, nil, errors.New("File provided is not a public key") } pubkeys = append(pubkeys, tmp) case "pkcs7": tmp, err := os.ReadFile(value) if err != nil { return nil, nil, nil, nil, nil, nil, fmt.Errorf("Unable to read file: %w", err) } if !encutils.IsCertificate(tmp) { return nil, nil, nil, nil, nil, nil, errors.New("File provided is not an x509 cert") } x509s = append(x509s, tmp) case "pkcs11": tmp, err := os.ReadFile(value) if err != nil { return nil, nil, nil, nil, nil, nil, fmt.Errorf("Unable to read file: %w", err) } if encutils.IsPkcs11PublicKey(tmp) { pkcs11Yamls = append(pkcs11Yamls, tmp) } else if encutils.IsPublicKey(tmp) { pkcs11Pubkeys = append(pkcs11Pubkeys, tmp) } else { return nil, nil, nil, nil, nil, nil, errors.New("Provided file is not a public key") } case "provider": keyProviders = append(keyProviders, []byte(value)) default: return nil, nil, nil, nil, nil, nil, errors.New("Provided protocol not recognized") } } return gpgRecipients, pubkeys, x509s, pkcs11Pubkeys, pkcs11Yamls, keyProviders, nil } // processx509Certs processes x509 certificate files func processx509Certs(keys []string) ([][]byte, error) { var x509s [][]byte for _, key := range keys { fileName := strings.Split(key, ":")[0] if _, err := os.Stat(fileName); os.IsNotExist(err) { continue } tmp, err := os.ReadFile(fileName) if err != nil { return nil, fmt.Errorf("Unable to read file: %w", err) } if !encutils.IsCertificate(tmp) { continue } x509s = append(x509s, tmp) } return x509s, nil } // processPwdString process a password that may be in any of the following formats: // - file= // - pass= // - fd= // - func processPwdString(pwdString string) ([]byte, error) { if strings.HasPrefix(pwdString, "file=") { return os.ReadFile(pwdString[5:]) } else if strings.HasPrefix(pwdString, "pass=") { return []byte(pwdString[5:]), nil } else if strings.HasPrefix(pwdString, "fd=") { fdStr := pwdString[3:] fd, err := strconv.Atoi(fdStr) if err != nil { return nil, fmt.Errorf("could not parse file descriptor %s: %w", fdStr, err) } f := os.NewFile(uintptr(fd), "pwdfile") if f == nil { return nil, fmt.Errorf("%s is not a valid file descriptor", fdStr) } defer f.Close() pwd := make([]byte, 64) n, err := f.Read(pwd) if err != nil { return nil, fmt.Errorf("could not read from file descriptor: %w", err) } return pwd[:n], nil } return []byte(pwdString), nil } // processPrivateKeyFiles sorts the different types of private key files; private key files may either be // private keys or GPG private key ring files. The private key files may include the password for the // private key and take any of the following forms: // - // - :file= // - :pass= // - :fd= // - : // - keyprovider:<...> func processPrivateKeyFiles(keyFilesAndPwds []string) ([][]byte, [][]byte, [][]byte, [][]byte, [][]byte, [][]byte, error) { var ( gpgSecretKeyRingFiles [][]byte gpgSecretKeyPasswords [][]byte privkeys [][]byte privkeysPasswords [][]byte pkcs11Yamls [][]byte keyProviders [][]byte err error ) // keys needed for decryption in case of adding a recipient for _, keyfileAndPwd := range keyFilesAndPwds { var password []byte // treat "provider" protocol separately if strings.HasPrefix(keyfileAndPwd, "provider:") { keyProviders = append(keyProviders, []byte(keyfileAndPwd[len("provider:"):])) continue } parts := strings.Split(keyfileAndPwd, ":") if len(parts) == 2 { password, err = processPwdString(parts[1]) if err != nil { return nil, nil, nil, nil, nil, nil, err } } keyfile := parts[0] tmp, err := os.ReadFile(keyfile) if err != nil { return nil, nil, nil, nil, nil, nil, err } isPrivKey, err := encutils.IsPrivateKey(tmp, password) if encutils.IsPasswordError(err) { return nil, nil, nil, nil, nil, nil, err } if encutils.IsPkcs11PrivateKey(tmp) { pkcs11Yamls = append(pkcs11Yamls, tmp) } else if isPrivKey { privkeys = append(privkeys, tmp) privkeysPasswords = append(privkeysPasswords, password) } else if encutils.IsGPGPrivateKeyRing(tmp) { gpgSecretKeyRingFiles = append(gpgSecretKeyRingFiles, tmp) gpgSecretKeyPasswords = append(gpgSecretKeyPasswords, password) } else { // ignore if file is not recognized, so as not to error if additional // metadata/cert files exists continue } } return gpgSecretKeyRingFiles, gpgSecretKeyPasswords, privkeys, privkeysPasswords, pkcs11Yamls, keyProviders, nil } // CreateDecryptCryptoConfig creates the CryptoConfig object that contains the necessary // information to perform decryption from command line options. func CreateDecryptCryptoConfig(keys []string, decRecipients []string) (encconfig.CryptoConfig, error) { ccs := []encconfig.CryptoConfig{} // x509 cert is needed for PKCS7 decryption _, _, x509s, _, _, _, err := processRecipientKeys(decRecipients) if err != nil { return encconfig.CryptoConfig{}, err } // x509 certs can also be passed in via keys x509FromKeys, err := processx509Certs(keys) if err != nil { return encconfig.CryptoConfig{}, err } x509s = append(x509s, x509FromKeys...) gpgSecretKeyRingFiles, gpgSecretKeyPasswords, privKeys, privKeysPasswords, pkcs11Yamls, keyProviders, err := processPrivateKeyFiles(keys) if err != nil { return encconfig.CryptoConfig{}, err } if len(gpgSecretKeyRingFiles) > 0 { gpgCc, err := encconfig.DecryptWithGpgPrivKeys(gpgSecretKeyRingFiles, gpgSecretKeyPasswords) if err != nil { return encconfig.CryptoConfig{}, err } ccs = append(ccs, gpgCc) } /* TODO: Add in GPG client query for secret keys in the future. _, err = createGPGClient(context) gpgInstalled := err == nil if gpgInstalled { if len(gpgSecretKeyRingFiles) == 0 && len(privKeys) == 0 && len(pkcs11Yamls) == 0 && len(keyProviders) == 0 && descs != nil { // Get pgp private keys from keyring only if no private key was passed gpgPrivKeys, gpgPrivKeyPasswords, err := getGPGPrivateKeys(context, gpgSecretKeyRingFiles, descs, true) if err != nil { return encconfig.CryptoConfig{}, err } gpgCc, err := encconfig.DecryptWithGpgPrivKeys(gpgPrivKeys, gpgPrivKeyPasswords) if err != nil { return encconfig.CryptoConfig{}, err } ccs = append(ccs, gpgCc) } else if len(gpgSecretKeyRingFiles) > 0 { gpgCc, err := encconfig.DecryptWithGpgPrivKeys(gpgSecretKeyRingFiles, gpgSecretKeyPasswords) if err != nil { return encconfig.CryptoConfig{}, err } ccs = append(ccs, gpgCc) } } */ if len(x509s) > 0 { x509sCc, err := encconfig.DecryptWithX509s(x509s) if err != nil { return encconfig.CryptoConfig{}, err } ccs = append(ccs, x509sCc) } if len(privKeys) > 0 { privKeysCc, err := encconfig.DecryptWithPrivKeys(privKeys, privKeysPasswords) if err != nil { return encconfig.CryptoConfig{}, err } ccs = append(ccs, privKeysCc) } if len(pkcs11Yamls) > 0 { p11conf, err := pkcs11config.GetUserPkcs11Config() if err != nil { return encconfig.CryptoConfig{}, err } pkcs11PrivKeysCc, err := encconfig.DecryptWithPkcs11Yaml(p11conf, pkcs11Yamls) if err != nil { return encconfig.CryptoConfig{}, err } ccs = append(ccs, pkcs11PrivKeysCc) } if len(keyProviders) > 0 { keyProviderCc, err := encconfig.DecryptWithKeyProvider(keyProviders) if err != nil { return encconfig.CryptoConfig{}, err } ccs = append(ccs, keyProviderCc) } return encconfig.CombineCryptoConfigs(ccs), nil } // CreateCryptoConfig from the list of recipient strings and list of key paths of private keys func CreateCryptoConfig(recipients []string, keys []string) (encconfig.CryptoConfig, error) { var decryptCc *encconfig.CryptoConfig ccs := []encconfig.CryptoConfig{} if len(keys) > 0 { dcc, err := CreateDecryptCryptoConfig(keys, []string{}) if err != nil { return encconfig.CryptoConfig{}, err } decryptCc = &dcc ccs = append(ccs, dcc) } if len(recipients) > 0 { gpgRecipients, pubKeys, x509s, pkcs11Pubkeys, pkcs11Yamls, keyProvider, err := processRecipientKeys(recipients) if err != nil { return encconfig.CryptoConfig{}, err } encryptCcs := []encconfig.CryptoConfig{} // Create GPG client with guessed GPG version and default homedir gpgClient, err := ocicrypt.NewGPGClient("", "") gpgInstalled := err == nil if len(gpgRecipients) > 0 && gpgInstalled { gpgPubRingFile, err := gpgClient.ReadGPGPubRingFile() if err != nil { return encconfig.CryptoConfig{}, err } gpgCc, err := encconfig.EncryptWithGpg(gpgRecipients, gpgPubRingFile) if err != nil { return encconfig.CryptoConfig{}, err } encryptCcs = append(encryptCcs, gpgCc) } // Create Encryption Crypto Config if len(x509s) > 0 { pkcs7Cc, err := encconfig.EncryptWithPkcs7(x509s) if err != nil { return encconfig.CryptoConfig{}, err } encryptCcs = append(encryptCcs, pkcs7Cc) } if len(pubKeys) > 0 { jweCc, err := encconfig.EncryptWithJwe(pubKeys) if err != nil { return encconfig.CryptoConfig{}, err } encryptCcs = append(encryptCcs, jweCc) } var p11conf *pkcs11.Pkcs11Config if len(pkcs11Yamls) > 0 || len(pkcs11Pubkeys) > 0 { p11conf, err = pkcs11config.GetUserPkcs11Config() if err != nil { return encconfig.CryptoConfig{}, err } pkcs11Cc, err := encconfig.EncryptWithPkcs11(p11conf, pkcs11Pubkeys, pkcs11Yamls) if err != nil { return encconfig.CryptoConfig{}, err } encryptCcs = append(encryptCcs, pkcs11Cc) } if len(keyProvider) > 0 { keyProviderCc, err := encconfig.EncryptWithKeyProvider(keyProvider) if err != nil { return encconfig.CryptoConfig{}, err } encryptCcs = append(encryptCcs, keyProviderCc) } ecc := encconfig.CombineCryptoConfigs(encryptCcs) if decryptCc != nil { ecc.EncryptConfig.AttachDecryptConfig(decryptCc.DecryptConfig) } ccs = append(ccs, ecc) } if len(ccs) > 0 { return encconfig.CombineCryptoConfigs(ccs), nil } return encconfig.CryptoConfig{}, nil } ================================================ FILE: vendor/github.com/containers/ocicrypt/keywrap/jwe/keywrapper_jwe.go ================================================ /* Copyright The ocicrypt Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package jwe import ( "crypto/ecdsa" "errors" "fmt" "github.com/containers/ocicrypt/config" "github.com/containers/ocicrypt/keywrap" "github.com/containers/ocicrypt/utils" "github.com/go-jose/go-jose/v4" ) type jweKeyWrapper struct { } func (kw *jweKeyWrapper) GetAnnotationID() string { return "org.opencontainers.image.enc.keys.jwe" } // NewKeyWrapper returns a new key wrapping interface using jwe func NewKeyWrapper() keywrap.KeyWrapper { return &jweKeyWrapper{} } // WrapKeys wraps the session key for recpients and encrypts the optsData, which // describe the symmetric key used for encrypting the layer func (kw *jweKeyWrapper) WrapKeys(ec *config.EncryptConfig, optsData []byte) ([]byte, error) { var joseRecipients []jose.Recipient err := addPubKeys(&joseRecipients, ec.Parameters["pubkeys"]) if err != nil { return nil, err } // no recipients is not an error... if len(joseRecipients) == 0 { return nil, nil } encrypter, err := jose.NewMultiEncrypter(jose.A256GCM, joseRecipients, nil) if err != nil { return nil, fmt.Errorf("jose.NewMultiEncrypter failed: %w", err) } jwe, err := encrypter.Encrypt(optsData) if err != nil { return nil, fmt.Errorf("JWE Encrypt failed: %w", err) } return []byte(jwe.FullSerialize()), nil } func (kw *jweKeyWrapper) UnwrapKey(dc *config.DecryptConfig, jweString []byte) ([]byte, error) { // cf. list of algorithms in func addPubKeys() below keyEncryptionAlgorithms := []jose.KeyAlgorithm{jose.RSA_OAEP, jose.RSA_OAEP_256, jose.ECDH_ES_A128KW, jose.ECDH_ES_A192KW, jose.ECDH_ES_A256KW} // accept all algorithms defined in RFC 7518, section 5.1 contentEncryption := []jose.ContentEncryption{jose.A128CBC_HS256, jose.A192CBC_HS384, jose.A256CBC_HS512, jose.A128GCM, jose.A192GCM, jose.A256GCM} jwe, err := jose.ParseEncrypted(string(jweString), keyEncryptionAlgorithms, contentEncryption) if err != nil { return nil, errors.New("jose.ParseEncrypted failed") } privKeys := kw.GetPrivateKeys(dc.Parameters) if len(privKeys) == 0 { return nil, errors.New("No private keys found for JWE decryption") } privKeysPasswords := kw.getPrivateKeysPasswords(dc.Parameters) if len(privKeysPasswords) != len(privKeys) { return nil, errors.New("Private key password array length must be same as that of private keys") } for idx, privKey := range privKeys { key, err := utils.ParsePrivateKey(privKey, privKeysPasswords[idx], "JWE") if err != nil { return nil, err } _, _, plain, err := jwe.DecryptMulti(key) if err == nil { return plain, nil } } return nil, errors.New("JWE: No suitable private key found for decryption") } func (kw *jweKeyWrapper) NoPossibleKeys(dcparameters map[string][][]byte) bool { return len(kw.GetPrivateKeys(dcparameters)) == 0 } func (kw *jweKeyWrapper) GetPrivateKeys(dcparameters map[string][][]byte) [][]byte { return dcparameters["privkeys"] } func (kw *jweKeyWrapper) getPrivateKeysPasswords(dcparameters map[string][][]byte) [][]byte { return dcparameters["privkeys-passwords"] } func (kw *jweKeyWrapper) GetKeyIdsFromPacket(b64jwes string) ([]uint64, error) { return nil, nil } func (kw *jweKeyWrapper) GetRecipients(b64jwes string) ([]string, error) { return []string{"[jwe]"}, nil } func addPubKeys(joseRecipients *[]jose.Recipient, pubKeys [][]byte) error { if len(pubKeys) == 0 { return nil } for _, pubKey := range pubKeys { key, err := utils.ParsePublicKey(pubKey, "JWE") if err != nil { return err } alg := jose.RSA_OAEP switch key := key.(type) { case *ecdsa.PublicKey: alg = jose.ECDH_ES_A256KW case *jose.JSONWebKey: if key.Algorithm != "" { alg = jose.KeyAlgorithm(key.Algorithm) switch alg { /* accepted algorithms */ case jose.RSA_OAEP: case jose.RSA_OAEP_256: case jose.ECDH_ES_A128KW: case jose.ECDH_ES_A192KW: case jose.ECDH_ES_A256KW: /* all others are rejected */ default: return fmt.Errorf("%s is an unsupported JWE key algorithm", alg) } } } *joseRecipients = append(*joseRecipients, jose.Recipient{ Algorithm: alg, Key: key, }) } return nil } ================================================ FILE: vendor/github.com/containers/ocicrypt/keywrap/keyprovider/keyprovider.go ================================================ /* Copyright The ocicrypt Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package keyprovider import ( "context" "encoding/json" "errors" "fmt" "github.com/containers/ocicrypt/config" keyproviderconfig "github.com/containers/ocicrypt/config/keyprovider-config" "github.com/containers/ocicrypt/keywrap" "github.com/containers/ocicrypt/utils" keyproviderpb "github.com/containers/ocicrypt/utils/keyprovider" log "github.com/sirupsen/logrus" "google.golang.org/grpc" ) type keyProviderKeyWrapper struct { provider string attrs keyproviderconfig.KeyProviderAttrs } func (kw *keyProviderKeyWrapper) GetAnnotationID() string { return "org.opencontainers.image.enc.keys.provider." + kw.provider } // NewKeyWrapper returns a new key wrapping interface using keyprovider func NewKeyWrapper(p string, a keyproviderconfig.KeyProviderAttrs) keywrap.KeyWrapper { return &keyProviderKeyWrapper{provider: p, attrs: a} } type KeyProviderKeyWrapProtocolOperation string var ( OpKeyWrap KeyProviderKeyWrapProtocolOperation = "keywrap" OpKeyUnwrap KeyProviderKeyWrapProtocolOperation = "keyunwrap" ) // KeyProviderKeyWrapProtocolInput defines the input to the key provider binary or grpc method. type KeyProviderKeyWrapProtocolInput struct { // Operation is either "keywrap" or "keyunwrap" Operation KeyProviderKeyWrapProtocolOperation `json:"op"` // KeyWrapParams encodes the arguments to key wrap if operation is set to wrap KeyWrapParams KeyWrapParams `json:"keywrapparams,omitempty"` // KeyUnwrapParams encodes the arguments to key unwrap if operation is set to unwrap KeyUnwrapParams KeyUnwrapParams `json:"keyunwrapparams,omitempty"` } // KeyProviderKeyWrapProtocolOutput defines the output of the key provider binary or grpc method. type KeyProviderKeyWrapProtocolOutput struct { // KeyWrapResult encodes the results to key wrap if operation is to wrap KeyWrapResults KeyWrapResults `json:"keywrapresults,omitempty"` // KeyUnwrapResult encodes the result to key unwrap if operation is to unwrap KeyUnwrapResults KeyUnwrapResults `json:"keyunwrapresults,omitempty"` } type KeyWrapParams struct { Ec *config.EncryptConfig `json:"ec"` OptsData []byte `json:"optsdata"` } type KeyUnwrapParams struct { Dc *config.DecryptConfig `json:"dc"` Annotation []byte `json:"annotation"` } type KeyUnwrapResults struct { OptsData []byte `json:"optsdata"` } type KeyWrapResults struct { Annotation []byte `json:"annotation"` } var runner utils.CommandExecuter func init() { runner = utils.Runner{} } // WrapKeys calls appropriate binary executable/grpc server for wrapping the session key for recipients and gets encrypted optsData, which // describe the symmetric key used for encrypting the layer func (kw *keyProviderKeyWrapper) WrapKeys(ec *config.EncryptConfig, optsData []byte) ([]byte, error) { input, err := json.Marshal(KeyProviderKeyWrapProtocolInput{ Operation: OpKeyWrap, KeyWrapParams: KeyWrapParams{ Ec: ec, OptsData: optsData, }, }) if err != nil { return nil, err } if _, ok := ec.Parameters[kw.provider]; ok { if kw.attrs.Command != nil { protocolOuput, err := getProviderCommandOutput(input, kw.attrs.Command) if err != nil { return nil, fmt.Errorf("error while retrieving keyprovider protocol command output: %w", err) } return protocolOuput.KeyWrapResults.Annotation, nil } else if kw.attrs.Grpc != "" { protocolOuput, err := getProviderGRPCOutput(input, kw.attrs.Grpc, OpKeyWrap) if err != nil { return nil, fmt.Errorf("error while retrieving keyprovider protocol grpc output: %w", err) } return protocolOuput.KeyWrapResults.Annotation, nil } return nil, errors.New("Unsupported keyprovider invocation. Supported invocation methods are grpc and cmd") } return nil, nil } // UnwrapKey calls appropriate binary executable/grpc server for unwrapping the session key based on the protocol given in annotation for recipients and gets decrypted optsData, // which describe the symmetric key used for decrypting the layer func (kw *keyProviderKeyWrapper) UnwrapKey(dc *config.DecryptConfig, jsonString []byte) ([]byte, error) { input, err := json.Marshal(KeyProviderKeyWrapProtocolInput{ Operation: OpKeyUnwrap, KeyUnwrapParams: KeyUnwrapParams{ Dc: dc, Annotation: jsonString, }, }) if err != nil { return nil, err } if kw.attrs.Command != nil { protocolOuput, err := getProviderCommandOutput(input, kw.attrs.Command) if err != nil { // If err is not nil, then ignore it and continue with rest of the given keyproviders return nil, err } return protocolOuput.KeyUnwrapResults.OptsData, nil } else if kw.attrs.Grpc != "" { protocolOuput, err := getProviderGRPCOutput(input, kw.attrs.Grpc, OpKeyUnwrap) if err != nil { // If err is not nil, then ignore it and continue with rest of the given keyproviders return nil, err } return protocolOuput.KeyUnwrapResults.OptsData, nil } return nil, errors.New("Unsupported keyprovider invocation. Supported invocation methods are grpc and cmd") } func getProviderGRPCOutput(input []byte, connString string, operation KeyProviderKeyWrapProtocolOperation) (*KeyProviderKeyWrapProtocolOutput, error) { var protocolOuput KeyProviderKeyWrapProtocolOutput var grpcOutput *keyproviderpb.KeyProviderKeyWrapProtocolOutput cc, err := grpc.Dial(connString, grpc.WithInsecure()) if err != nil { return nil, fmt.Errorf("error while dialing rpc server: %w", err) } defer func() { derr := cc.Close() if derr != nil { log.WithError(derr).Error("Error closing grpc socket") } }() client := keyproviderpb.NewKeyProviderServiceClient(cc) req := &keyproviderpb.KeyProviderKeyWrapProtocolInput{ KeyProviderKeyWrapProtocolInput: input, } if operation == OpKeyWrap { grpcOutput, err = client.WrapKey(context.Background(), req) if err != nil { return nil, fmt.Errorf("Error from grpc method: %w", err) } } else if operation == OpKeyUnwrap { grpcOutput, err = client.UnWrapKey(context.Background(), req) if err != nil { return nil, fmt.Errorf("Error from grpc method: %w", err) } } else { return nil, errors.New("Unsupported operation") } respBytes := grpcOutput.GetKeyProviderKeyWrapProtocolOutput() err = json.Unmarshal(respBytes, &protocolOuput) if err != nil { return nil, fmt.Errorf("Error while unmarshalling grpc method output: %w", err) } return &protocolOuput, nil } func getProviderCommandOutput(input []byte, command *keyproviderconfig.Command) (*KeyProviderKeyWrapProtocolOutput, error) { var protocolOuput KeyProviderKeyWrapProtocolOutput // Convert interface to command structure respBytes, err := runner.Exec(command.Path, command.Args, input) if err != nil { return nil, err } err = json.Unmarshal(respBytes, &protocolOuput) if err != nil { return nil, fmt.Errorf("Error while unmarshalling binary executable command output: %w", err) } return &protocolOuput, nil } // Return false as it is not applicable to keyprovider protocol func (kw *keyProviderKeyWrapper) NoPossibleKeys(dcparameters map[string][][]byte) bool { return false } // Return nil as it is not applicable to keyprovider protocol func (kw *keyProviderKeyWrapper) GetPrivateKeys(dcparameters map[string][][]byte) [][]byte { return nil } // Return nil as it is not applicable to keyprovider protocol func (kw *keyProviderKeyWrapper) GetKeyIdsFromPacket(_ string) ([]uint64, error) { return nil, nil } // Return nil as it is not applicable to keyprovider protocol func (kw *keyProviderKeyWrapper) GetRecipients(_ string) ([]string, error) { return nil, nil } ================================================ FILE: vendor/github.com/containers/ocicrypt/keywrap/keywrap.go ================================================ /* Copyright The ocicrypt Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package keywrap import ( "github.com/containers/ocicrypt/config" ) // KeyWrapper is the interface used for wrapping keys using // a specific encryption technology (pgp, jwe) type KeyWrapper interface { WrapKeys(ec *config.EncryptConfig, optsData []byte) ([]byte, error) UnwrapKey(dc *config.DecryptConfig, annotation []byte) ([]byte, error) GetAnnotationID() string // NoPossibleKeys returns true if there is no possibility of performing // decryption for parameters provided. NoPossibleKeys(dcparameters map[string][][]byte) bool // GetPrivateKeys (optional) gets the array of private keys. It is an optional implementation // as in some key services, a private key may not be exportable (i.e. HSM) // If not implemented, return nil GetPrivateKeys(dcparameters map[string][][]byte) [][]byte // GetKeyIdsFromPacket (optional) gets a list of key IDs. This is optional as some encryption // schemes may not have a notion of key IDs // If not implemented, return the nil slice GetKeyIdsFromPacket(packet string) ([]uint64, error) // GetRecipients (optional) gets a list of recipients. It is optional due to the validity of // recipients in a particular encryptiong scheme // If not implemented, return the nil slice GetRecipients(packet string) ([]string, error) } ================================================ FILE: vendor/github.com/containers/ocicrypt/keywrap/pgp/keywrapper_gpg.go ================================================ /* Copyright The ocicrypt Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package pgp import ( "bytes" "crypto" "crypto/rand" "encoding/base64" "errors" "fmt" "io" "net/mail" "strconv" "strings" "github.com/containers/ocicrypt/config" "github.com/containers/ocicrypt/keywrap" "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/packet" ) type gpgKeyWrapper struct { } // NewKeyWrapper returns a new key wrapping interface for pgp func NewKeyWrapper() keywrap.KeyWrapper { return &gpgKeyWrapper{} } var ( // GPGDefaultEncryptConfig is the default configuration for layer encryption/decryption GPGDefaultEncryptConfig = &packet.Config{ Rand: rand.Reader, DefaultHash: crypto.SHA256, DefaultCipher: packet.CipherAES256, CompressionConfig: &packet.CompressionConfig{Level: 0}, // No compression RSABits: 2048, } ) func (kw *gpgKeyWrapper) GetAnnotationID() string { return "org.opencontainers.image.enc.keys.pgp" } // WrapKeys wraps the session key for recpients and encrypts the optsData, which // describe the symmetric key used for encrypting the layer func (kw *gpgKeyWrapper) WrapKeys(ec *config.EncryptConfig, optsData []byte) ([]byte, error) { ciphertext := new(bytes.Buffer) el, err := kw.createEntityList(ec) if err != nil { return nil, fmt.Errorf("unable to create entity list: %w", err) } if len(el) == 0 { // nothing to do -- not an error return nil, nil } plaintextWriter, err := openpgp.Encrypt(ciphertext, el, /*EntityList*/ nil, /* Sign*/ nil, /* FileHint */ GPGDefaultEncryptConfig) if err != nil { return nil, err } if _, err = plaintextWriter.Write(optsData); err != nil { return nil, err } else if err = plaintextWriter.Close(); err != nil { return nil, err } return ciphertext.Bytes(), err } // UnwrapKey unwraps the symmetric key with which the layer is encrypted // This symmetric key is encrypted in the PGP payload. func (kw *gpgKeyWrapper) UnwrapKey(dc *config.DecryptConfig, pgpPacket []byte) ([]byte, error) { pgpPrivateKeys, pgpPrivateKeysPwd, err := kw.getKeyParameters(dc.Parameters) if err != nil { return nil, err } for idx, pgpPrivateKey := range pgpPrivateKeys { r := bytes.NewBuffer(pgpPrivateKey) entityList, err := openpgp.ReadKeyRing(r) if err != nil { return nil, fmt.Errorf("unable to parse private keys: %w", err) } var prompt openpgp.PromptFunction if len(pgpPrivateKeysPwd) > idx { responded := false prompt = func(keys []openpgp.Key, symmetric bool) ([]byte, error) { if responded { return nil, fmt.Errorf("don't seem to have the right password") } responded = true for _, key := range keys { if key.PrivateKey != nil { _ = key.PrivateKey.Decrypt(pgpPrivateKeysPwd[idx]) } } return pgpPrivateKeysPwd[idx], nil } } r = bytes.NewBuffer(pgpPacket) md, err := openpgp.ReadMessage(r, entityList, prompt, GPGDefaultEncryptConfig) if err != nil { continue } // we get the plain key options back optsData, err := io.ReadAll(md.UnverifiedBody) if err != nil { continue } return optsData, nil } return nil, errors.New("PGP: No suitable key found to unwrap key") } // GetKeyIdsFromWrappedKeys converts the base64 encoded PGPPacket to uint64 keyIds func (kw *gpgKeyWrapper) GetKeyIdsFromPacket(b64pgpPackets string) ([]uint64, error) { var keyids []uint64 for _, b64pgpPacket := range strings.Split(b64pgpPackets, ",") { pgpPacket, err := base64.StdEncoding.DecodeString(b64pgpPacket) if err != nil { return nil, fmt.Errorf("could not decode base64 encoded PGP packet: %w", err) } newids, err := kw.getKeyIDs(pgpPacket) if err != nil { return nil, err } keyids = append(keyids, newids...) } return keyids, nil } // getKeyIDs parses a PGPPacket and gets the list of recipients' key IDs func (kw *gpgKeyWrapper) getKeyIDs(pgpPacket []byte) ([]uint64, error) { var keyids []uint64 kbuf := bytes.NewBuffer(pgpPacket) packets := packet.NewReader(kbuf) ParsePackets: for { p, err := packets.Next() if err == io.EOF { break ParsePackets } if err != nil { return []uint64{}, fmt.Errorf("packets.Next() failed: %w", err) } switch p := p.(type) { case *packet.EncryptedKey: keyids = append(keyids, p.KeyId) case *packet.SymmetricallyEncrypted: break ParsePackets } } return keyids, nil } // GetRecipients converts the wrappedKeys to an array of recipients func (kw *gpgKeyWrapper) GetRecipients(b64pgpPackets string) ([]string, error) { keyIds, err := kw.GetKeyIdsFromPacket(b64pgpPackets) if err != nil { return nil, err } var array []string for _, keyid := range keyIds { array = append(array, "0x"+strconv.FormatUint(keyid, 16)) } return array, nil } func (kw *gpgKeyWrapper) NoPossibleKeys(dcparameters map[string][][]byte) bool { return len(kw.GetPrivateKeys(dcparameters)) == 0 } func (kw *gpgKeyWrapper) GetPrivateKeys(dcparameters map[string][][]byte) [][]byte { return dcparameters["gpg-privatekeys"] } func (kw *gpgKeyWrapper) getKeyParameters(dcparameters map[string][][]byte) ([][]byte, [][]byte, error) { privKeys := kw.GetPrivateKeys(dcparameters) if len(privKeys) == 0 { return nil, nil, errors.New("GPG: Missing private key parameter") } return privKeys, dcparameters["gpg-privatekeys-passwords"], nil } // createEntityList creates the opengpg EntityList by reading the KeyRing // first and then filtering out recipients' keys func (kw *gpgKeyWrapper) createEntityList(ec *config.EncryptConfig) (openpgp.EntityList, error) { pgpPubringFile := ec.Parameters["gpg-pubkeyringfile"] if len(pgpPubringFile) == 0 { return nil, nil } r := bytes.NewReader(pgpPubringFile[0]) entityList, err := openpgp.ReadKeyRing(r) if err != nil { return nil, err } gpgRecipients := ec.Parameters["gpg-recipients"] if len(gpgRecipients) == 0 { return nil, nil } rSet := make(map[string]int) for _, r := range gpgRecipients { rSet[string(r)] = 0 } var filteredList openpgp.EntityList for _, entity := range entityList { for k := range entity.Identities { addr, err := mail.ParseAddress(k) if err != nil { return nil, err } for _, r := range gpgRecipients { recp := string(r) if strings.Compare(addr.Name, recp) == 0 || strings.Compare(addr.Address, recp) == 0 { filteredList = append(filteredList, entity) rSet[recp] = rSet[recp] + 1 } } } } // make sure we found keys for all the Recipients... var buffer bytes.Buffer notFound := false buffer.WriteString("PGP: No key found for the following recipients: ") for k, v := range rSet { if v == 0 { if notFound { buffer.WriteString(", ") } buffer.WriteString(k) notFound = true } } if notFound { return nil, errors.New(buffer.String()) } return filteredList, nil } ================================================ FILE: vendor/github.com/containers/ocicrypt/keywrap/pkcs11/keywrapper_pkcs11.go ================================================ /* Copyright The ocicrypt Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package pkcs11 import ( "errors" "fmt" "github.com/containers/ocicrypt/config" "github.com/containers/ocicrypt/crypto/pkcs11" "github.com/containers/ocicrypt/keywrap" "github.com/containers/ocicrypt/utils" ) type pkcs11KeyWrapper struct { } func (kw *pkcs11KeyWrapper) GetAnnotationID() string { return "org.opencontainers.image.enc.keys.pkcs11" } // NewKeyWrapper returns a new key wrapping interface using pkcs11 func NewKeyWrapper() keywrap.KeyWrapper { return &pkcs11KeyWrapper{} } // WrapKeys wraps the session key for recpients and encrypts the optsData, which // describe the symmetric key used for encrypting the layer func (kw *pkcs11KeyWrapper) WrapKeys(ec *config.EncryptConfig, optsData []byte) ([]byte, error) { // append({}, ...) allocates a fresh backing array, and that's necessary to guarantee concurrent calls to WrapKeys (as in c/image/copy.Image) // can't race writing to the same backing array. pubKeys := append([][]byte{}, ec.Parameters["pkcs11-pubkeys"]...) // In Go 1.21, slices.Clone(ec.Parameters["pkcs11-pubkeys"]) pubKeys = append(pubKeys, ec.Parameters["pkcs11-yamls"]...) pkcs11Recipients, err := addPubKeys(&ec.DecryptConfig, pubKeys) if err != nil { return nil, err } // no recipients is not an error... if len(pkcs11Recipients) == 0 { return nil, nil } jsonString, err := pkcs11.EncryptMultiple(pkcs11Recipients, optsData) if err != nil { return nil, fmt.Errorf("PKCS11 EncryptMulitple failed: %w", err) } return jsonString, nil } func (kw *pkcs11KeyWrapper) UnwrapKey(dc *config.DecryptConfig, jsonString []byte) ([]byte, error) { var pkcs11PrivKeys []*pkcs11.Pkcs11KeyFileObject privKeys := kw.GetPrivateKeys(dc.Parameters) if len(privKeys) == 0 { return nil, errors.New("No private keys found for PKCS11 decryption") } p11conf, err := p11confFromParameters(dc.Parameters) if err != nil { return nil, err } for _, privKey := range privKeys { key, err := utils.ParsePrivateKey(privKey, nil, "PKCS11") if err != nil { return nil, err } switch pkcs11PrivKey := key.(type) { case *pkcs11.Pkcs11KeyFileObject: if p11conf != nil { pkcs11PrivKey.Uri.SetModuleDirectories(p11conf.ModuleDirectories) pkcs11PrivKey.Uri.SetAllowedModulePaths(p11conf.AllowedModulePaths) } pkcs11PrivKeys = append(pkcs11PrivKeys, pkcs11PrivKey) default: continue } } plaintext, err := pkcs11.Decrypt(pkcs11PrivKeys, jsonString) if err == nil { return plaintext, nil } return nil, fmt.Errorf("PKCS11: No suitable private key found for decryption: %w", err) } func (kw *pkcs11KeyWrapper) NoPossibleKeys(dcparameters map[string][][]byte) bool { return len(kw.GetPrivateKeys(dcparameters)) == 0 } func (kw *pkcs11KeyWrapper) GetPrivateKeys(dcparameters map[string][][]byte) [][]byte { return dcparameters["pkcs11-yamls"] } func (kw *pkcs11KeyWrapper) GetKeyIdsFromPacket(_ string) ([]uint64, error) { return nil, nil } func (kw *pkcs11KeyWrapper) GetRecipients(_ string) ([]string, error) { return []string{"[pkcs11]"}, nil } func addPubKeys(dc *config.DecryptConfig, pubKeys [][]byte) ([]interface{}, error) { var pkcs11Keys []interface{} if len(pubKeys) == 0 { return pkcs11Keys, nil } p11conf, err := p11confFromParameters(dc.Parameters) if err != nil { return nil, err } for _, pubKey := range pubKeys { key, err := utils.ParsePublicKey(pubKey, "PKCS11") if err != nil { return nil, err } switch pkcs11PubKey := key.(type) { case *pkcs11.Pkcs11KeyFileObject: if p11conf != nil { pkcs11PubKey.Uri.SetModuleDirectories(p11conf.ModuleDirectories) pkcs11PubKey.Uri.SetAllowedModulePaths(p11conf.AllowedModulePaths) } } pkcs11Keys = append(pkcs11Keys, key) } return pkcs11Keys, nil } func p11confFromParameters(dcparameters map[string][][]byte) (*pkcs11.Pkcs11Config, error) { if _, ok := dcparameters["pkcs11-config"]; ok { return pkcs11.ParsePkcs11ConfigFile(dcparameters["pkcs11-config"][0]) } return nil, nil } ================================================ FILE: vendor/github.com/containers/ocicrypt/keywrap/pkcs7/keywrapper_pkcs7.go ================================================ /* Copyright The ocicrypt Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package pkcs7 import ( "crypto" "crypto/x509" "errors" "fmt" "github.com/containers/ocicrypt/config" "github.com/containers/ocicrypt/keywrap" "github.com/containers/ocicrypt/utils" "github.com/smallstep/pkcs7" ) type pkcs7KeyWrapper struct { } // NewKeyWrapper returns a new key wrapping interface using jwe func NewKeyWrapper() keywrap.KeyWrapper { return &pkcs7KeyWrapper{} } func (kw *pkcs7KeyWrapper) GetAnnotationID() string { return "org.opencontainers.image.enc.keys.pkcs7" } // WrapKeys wraps the session key for recpients and encrypts the optsData, which // describe the symmetric key used for encrypting the layer func (kw *pkcs7KeyWrapper) WrapKeys(ec *config.EncryptConfig, optsData []byte) ([]byte, error) { x509Certs, err := collectX509s(ec.Parameters["x509s"]) if err != nil { return nil, err } // no recipients is not an error... if len(x509Certs) == 0 { return nil, nil } pkcs7.ContentEncryptionAlgorithm = pkcs7.EncryptionAlgorithmAES128GCM return pkcs7.Encrypt(optsData, x509Certs) } func collectX509s(x509s [][]byte) ([]*x509.Certificate, error) { if len(x509s) == 0 { return nil, nil } var x509Certs []*x509.Certificate for _, x509 := range x509s { x509Cert, err := utils.ParseCertificate(x509, "PKCS7") if err != nil { return nil, err } x509Certs = append(x509Certs, x509Cert) } return x509Certs, nil } func (kw *pkcs7KeyWrapper) NoPossibleKeys(dcparameters map[string][][]byte) bool { return len(kw.GetPrivateKeys(dcparameters)) == 0 } func (kw *pkcs7KeyWrapper) GetPrivateKeys(dcparameters map[string][][]byte) [][]byte { return dcparameters["privkeys"] } func (kw *pkcs7KeyWrapper) getPrivateKeysPasswords(dcparameters map[string][][]byte) [][]byte { return dcparameters["privkeys-passwords"] } // UnwrapKey unwraps the symmetric key with which the layer is encrypted // This symmetric key is encrypted in the PKCS7 payload. func (kw *pkcs7KeyWrapper) UnwrapKey(dc *config.DecryptConfig, pkcs7Packet []byte) ([]byte, error) { privKeys := kw.GetPrivateKeys(dc.Parameters) if len(privKeys) == 0 { return nil, errors.New("no private keys found for PKCS7 decryption") } privKeysPasswords := kw.getPrivateKeysPasswords(dc.Parameters) if len(privKeysPasswords) != len(privKeys) { return nil, errors.New("private key password array length must be same as that of private keys") } x509Certs, err := collectX509s(dc.Parameters["x509s"]) if err != nil { return nil, err } if len(x509Certs) == 0 { return nil, errors.New("no x509 certificates found needed for PKCS7 decryption") } p7, err := pkcs7.Parse(pkcs7Packet) if err != nil { return nil, fmt.Errorf("could not parse PKCS7 packet: %w", err) } for idx, privKey := range privKeys { key, err := utils.ParsePrivateKey(privKey, privKeysPasswords[idx], "PKCS7") if err != nil { return nil, err } for _, x509Cert := range x509Certs { optsData, err := p7.Decrypt(x509Cert, crypto.PrivateKey(key)) if err != nil { continue } return optsData, nil } } return nil, errors.New("PKCS7: No suitable private key found for decryption") } // GetKeyIdsFromWrappedKeys converts the base64 encoded Packet to uint64 keyIds; // We cannot do this with pkcs7 func (kw *pkcs7KeyWrapper) GetKeyIdsFromPacket(b64pkcs7Packets string) ([]uint64, error) { return nil, nil } // GetRecipients converts the wrappedKeys to an array of recipients // We cannot do this with pkcs7 func (kw *pkcs7KeyWrapper) GetRecipients(b64pkcs7Packets string) ([]string, error) { return []string{"[pkcs7]"}, nil } ================================================ FILE: vendor/github.com/containers/ocicrypt/reader.go ================================================ /* Copyright The ocicrypt Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ocicrypt import ( "io" ) type readerAtReader struct { r io.ReaderAt off int64 } // ReaderFromReaderAt takes an io.ReaderAt and returns an io.Reader func ReaderFromReaderAt(r io.ReaderAt) io.Reader { return &readerAtReader{ r: r, off: 0, } } func (rar *readerAtReader) Read(p []byte) (n int, err error) { n, err = rar.r.ReadAt(p, rar.off) rar.off += int64(n) return n, err } ================================================ FILE: vendor/github.com/containers/ocicrypt/spec/spec.go ================================================ package spec const ( // MediaTypeLayerEnc is MIME type used for encrypted layers. MediaTypeLayerEnc = "application/vnd.oci.image.layer.v1.tar+encrypted" // MediaTypeLayerGzipEnc is MIME type used for encrypted gzip-compressed layers. MediaTypeLayerGzipEnc = "application/vnd.oci.image.layer.v1.tar+gzip+encrypted" // MediaTypeLayerZstdEnc is MIME type used for encrypted zstd-compressed layers. MediaTypeLayerZstdEnc = "application/vnd.oci.image.layer.v1.tar+zstd+encrypted" // MediaTypeLayerNonDistributableEnc is MIME type used for non distributable encrypted layers. MediaTypeLayerNonDistributableEnc = "application/vnd.oci.image.layer.nondistributable.v1.tar+encrypted" // MediaTypeLayerNonDistributableGzipEnc is MIME type used for non distributable encrypted gzip-compressed layers. MediaTypeLayerNonDistributableGzipEnc = "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip+encrypted" // MediaTypeLayerNonDistributableZstdEnc is MIME type used for non distributable encrypted zstd-compressed layers. MediaTypeLayerNonDistributableZstdEnc = "application/vnd.oci.image.layer.nondistributable.v1.tar+zstd+encrypted" // MediaTypeLayerNonDistributableZsdtEnc is MIME type used for non distributable encrypted zstd-compressed layers. // // Deprecated: Use [MediaTypeLayerNonDistributableZstdEnc]. MediaTypeLayerNonDistributableZsdtEnc = MediaTypeLayerNonDistributableZstdEnc ) ================================================ FILE: vendor/github.com/containers/ocicrypt/utils/delayedreader.go ================================================ /* Copyright The ocicrypt Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package utils import ( "io" ) func min(a, b int) int { if a < b { return a } return b } // DelayedReader wraps a io.Reader and allows a client to use the Reader // interface. The DelayedReader holds back some buffer to the client // so that it can report any error that occurred on the Reader it wraps // early to the client while it may still have held some data back. type DelayedReader struct { reader io.Reader // Reader to Read() bytes from and delay them err error // error that occurred on the reader buffer []byte // delay buffer bufbytes int // number of bytes in the delay buffer to give to Read(); on '0' we return 'EOF' to caller bufoff int // offset in the delay buffer to give to Read() } // NewDelayedReader wraps a io.Reader and allocates a delay buffer of bufsize bytes func NewDelayedReader(reader io.Reader, bufsize uint) io.Reader { return &DelayedReader{ reader: reader, buffer: make([]byte, bufsize), } } // Read implements the io.Reader interface func (dr *DelayedReader) Read(p []byte) (int, error) { if dr.err != nil && dr.err != io.EOF { return 0, dr.err } // if we are completely drained, return io.EOF if dr.err == io.EOF && dr.bufbytes == 0 { return 0, io.EOF } // only at the beginning we fill our delay buffer in an extra step if dr.bufbytes < len(dr.buffer) && dr.err == nil { dr.bufbytes, dr.err = FillBuffer(dr.reader, dr.buffer) if dr.err != nil && dr.err != io.EOF { return 0, dr.err } } // dr.err != nil means we have EOF and can drain the delay buffer // otherwise we need to still read from the reader var tmpbuf []byte tmpbufbytes := 0 if dr.err == nil { tmpbuf = make([]byte, len(p)) tmpbufbytes, dr.err = FillBuffer(dr.reader, tmpbuf) if dr.err != nil && dr.err != io.EOF { return 0, dr.err } } // copy out of the delay buffer into 'p' tocopy1 := min(len(p), dr.bufbytes) c1 := copy(p[:tocopy1], dr.buffer[dr.bufoff:]) dr.bufoff += c1 dr.bufbytes -= c1 c2 := 0 // can p still hold more data? if c1 < len(p) { // copy out of the tmpbuf into 'p' c2 = copy(p[tocopy1:], tmpbuf[:tmpbufbytes]) } // if tmpbuf holds data we need to hold onto, copy them // into the delay buffer if tmpbufbytes-c2 > 0 { // left-shift the delay buffer and append the tmpbuf's remaining data dr.buffer = dr.buffer[dr.bufoff : dr.bufoff+dr.bufbytes] dr.buffer = append(dr.buffer, tmpbuf[c2:tmpbufbytes]...) dr.bufoff = 0 dr.bufbytes = len(dr.buffer) } var err error if dr.bufbytes == 0 { err = io.EOF } return c1 + c2, err } ================================================ FILE: vendor/github.com/containers/ocicrypt/utils/ioutils.go ================================================ /* Copyright The ocicrypt Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package utils import ( "bytes" "fmt" "io" "os/exec" ) // FillBuffer fills the given buffer with as many bytes from the reader as possible. It returns // EOF if an EOF was encountered or any other error. func FillBuffer(reader io.Reader, buffer []byte) (int, error) { n, err := io.ReadFull(reader, buffer) if err == io.ErrUnexpectedEOF { return n, io.EOF } return n, err } // first argument is the command, like cat or echo, // the second is the list of args to pass to it type CommandExecuter interface { Exec(string, []string, []byte) ([]byte, error) } type Runner struct{} // ExecuteCommand is used to execute a linux command line command and return the output of the command with an error if it exists. func (r Runner) Exec(cmdName string, args []string, input []byte) ([]byte, error) { var out bytes.Buffer var stderr bytes.Buffer stdInputBuffer := bytes.NewBuffer(input) cmd := exec.Command(cmdName, args...) cmd.Stdin = stdInputBuffer cmd.Stdout = &out cmd.Stderr = &stderr err := cmd.Run() if err != nil { return nil, fmt.Errorf("Error while running command: %s. stderr: %s: %w", cmdName, stderr.String(), err) } return out.Bytes(), nil } ================================================ FILE: vendor/github.com/containers/ocicrypt/utils/keyprovider/keyprovider.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // source: keyprovider.proto package keyprovider import ( context "context" fmt "fmt" proto "github.com/golang/protobuf/proto" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" math "math" ) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package type KeyProviderKeyWrapProtocolInput struct { KeyProviderKeyWrapProtocolInput []byte `protobuf:"bytes,1,opt,name=KeyProviderKeyWrapProtocolInput,proto3" json:"KeyProviderKeyWrapProtocolInput,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *KeyProviderKeyWrapProtocolInput) Reset() { *m = KeyProviderKeyWrapProtocolInput{} } func (m *KeyProviderKeyWrapProtocolInput) String() string { return proto.CompactTextString(m) } func (*KeyProviderKeyWrapProtocolInput) ProtoMessage() {} func (*KeyProviderKeyWrapProtocolInput) Descriptor() ([]byte, []int) { return fileDescriptor_da74c8e785ad390c, []int{0} } func (m *KeyProviderKeyWrapProtocolInput) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_KeyProviderKeyWrapProtocolInput.Unmarshal(m, b) } func (m *KeyProviderKeyWrapProtocolInput) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_KeyProviderKeyWrapProtocolInput.Marshal(b, m, deterministic) } func (m *KeyProviderKeyWrapProtocolInput) XXX_Merge(src proto.Message) { xxx_messageInfo_KeyProviderKeyWrapProtocolInput.Merge(m, src) } func (m *KeyProviderKeyWrapProtocolInput) XXX_Size() int { return xxx_messageInfo_KeyProviderKeyWrapProtocolInput.Size(m) } func (m *KeyProviderKeyWrapProtocolInput) XXX_DiscardUnknown() { xxx_messageInfo_KeyProviderKeyWrapProtocolInput.DiscardUnknown(m) } var xxx_messageInfo_KeyProviderKeyWrapProtocolInput proto.InternalMessageInfo func (m *KeyProviderKeyWrapProtocolInput) GetKeyProviderKeyWrapProtocolInput() []byte { if m != nil { return m.KeyProviderKeyWrapProtocolInput } return nil } type KeyProviderKeyWrapProtocolOutput struct { KeyProviderKeyWrapProtocolOutput []byte `protobuf:"bytes,1,opt,name=KeyProviderKeyWrapProtocolOutput,proto3" json:"KeyProviderKeyWrapProtocolOutput,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *KeyProviderKeyWrapProtocolOutput) Reset() { *m = KeyProviderKeyWrapProtocolOutput{} } func (m *KeyProviderKeyWrapProtocolOutput) String() string { return proto.CompactTextString(m) } func (*KeyProviderKeyWrapProtocolOutput) ProtoMessage() {} func (*KeyProviderKeyWrapProtocolOutput) Descriptor() ([]byte, []int) { return fileDescriptor_da74c8e785ad390c, []int{1} } func (m *KeyProviderKeyWrapProtocolOutput) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_KeyProviderKeyWrapProtocolOutput.Unmarshal(m, b) } func (m *KeyProviderKeyWrapProtocolOutput) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_KeyProviderKeyWrapProtocolOutput.Marshal(b, m, deterministic) } func (m *KeyProviderKeyWrapProtocolOutput) XXX_Merge(src proto.Message) { xxx_messageInfo_KeyProviderKeyWrapProtocolOutput.Merge(m, src) } func (m *KeyProviderKeyWrapProtocolOutput) XXX_Size() int { return xxx_messageInfo_KeyProviderKeyWrapProtocolOutput.Size(m) } func (m *KeyProviderKeyWrapProtocolOutput) XXX_DiscardUnknown() { xxx_messageInfo_KeyProviderKeyWrapProtocolOutput.DiscardUnknown(m) } var xxx_messageInfo_KeyProviderKeyWrapProtocolOutput proto.InternalMessageInfo func (m *KeyProviderKeyWrapProtocolOutput) GetKeyProviderKeyWrapProtocolOutput() []byte { if m != nil { return m.KeyProviderKeyWrapProtocolOutput } return nil } func init() { proto.RegisterType((*KeyProviderKeyWrapProtocolInput)(nil), "keyprovider.keyProviderKeyWrapProtocolInput") proto.RegisterType((*KeyProviderKeyWrapProtocolOutput)(nil), "keyprovider.keyProviderKeyWrapProtocolOutput") } func init() { proto.RegisterFile("keyprovider.proto", fileDescriptor_da74c8e785ad390c) } var fileDescriptor_da74c8e785ad390c = []byte{ // 169 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0xcc, 0x4e, 0xad, 0x2c, 0x28, 0xca, 0x2f, 0xcb, 0x4c, 0x49, 0x2d, 0xd2, 0x03, 0x32, 0x4a, 0xf2, 0x85, 0xb8, 0x91, 0x84, 0x94, 0xb2, 0xb9, 0xe4, 0x81, 0xdc, 0x00, 0x28, 0xd7, 0x3b, 0xb5, 0x32, 0xbc, 0x28, 0xb1, 0x20, 0x00, 0xa4, 0x2e, 0x39, 0x3f, 0xc7, 0x33, 0xaf, 0xa0, 0xb4, 0x44, 0xc8, 0x83, 0x4b, 0xde, 0x1b, 0xbf, 0x12, 0x09, 0x46, 0x05, 0x46, 0x0d, 0x9e, 0x20, 0x42, 0xca, 0x94, 0xf2, 0xb8, 0x14, 0x70, 0x5b, 0xe6, 0x5f, 0x5a, 0x02, 0xb2, 0xcd, 0x8b, 0x4b, 0xc1, 0x9b, 0x80, 0x1a, 0xa8, 0x75, 0x04, 0xd5, 0x19, 0xbd, 0x62, 0xe4, 0x12, 0x42, 0x52, 0x14, 0x9c, 0x5a, 0x54, 0x96, 0x99, 0x9c, 0x2a, 0x94, 0xc1, 0xc5, 0x0e, 0x52, 0x0c, 0x94, 0x11, 0xd2, 0xd1, 0x43, 0x0e, 0x1f, 0x02, 0x21, 0x21, 0xa5, 0x4b, 0xa4, 0x6a, 0x88, 0xf5, 0x4a, 0x0c, 0x42, 0x59, 0x5c, 0x9c, 0xa1, 0x79, 0xf4, 0xb1, 0xcb, 0x89, 0x37, 0x0a, 0x39, 0x62, 0x93, 0xd8, 0xc0, 0x91, 0x6d, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0x9a, 0x10, 0xcb, 0xf9, 0x01, 0x02, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. var _ context.Context var _ grpc.ClientConnInterface // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. const _ = grpc.SupportPackageIsVersion6 // KeyProviderServiceClient is the client API for KeyProviderService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type KeyProviderServiceClient interface { WrapKey(ctx context.Context, in *KeyProviderKeyWrapProtocolInput, opts ...grpc.CallOption) (*KeyProviderKeyWrapProtocolOutput, error) UnWrapKey(ctx context.Context, in *KeyProviderKeyWrapProtocolInput, opts ...grpc.CallOption) (*KeyProviderKeyWrapProtocolOutput, error) } type keyProviderServiceClient struct { cc grpc.ClientConnInterface } func NewKeyProviderServiceClient(cc grpc.ClientConnInterface) KeyProviderServiceClient { return &keyProviderServiceClient{cc} } func (c *keyProviderServiceClient) WrapKey(ctx context.Context, in *KeyProviderKeyWrapProtocolInput, opts ...grpc.CallOption) (*KeyProviderKeyWrapProtocolOutput, error) { out := new(KeyProviderKeyWrapProtocolOutput) err := c.cc.Invoke(ctx, "/keyprovider.KeyProviderService/WrapKey", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *keyProviderServiceClient) UnWrapKey(ctx context.Context, in *KeyProviderKeyWrapProtocolInput, opts ...grpc.CallOption) (*KeyProviderKeyWrapProtocolOutput, error) { out := new(KeyProviderKeyWrapProtocolOutput) err := c.cc.Invoke(ctx, "/keyprovider.KeyProviderService/UnWrapKey", in, out, opts...) if err != nil { return nil, err } return out, nil } // KeyProviderServiceServer is the server API for KeyProviderService service. type KeyProviderServiceServer interface { WrapKey(context.Context, *KeyProviderKeyWrapProtocolInput) (*KeyProviderKeyWrapProtocolOutput, error) UnWrapKey(context.Context, *KeyProviderKeyWrapProtocolInput) (*KeyProviderKeyWrapProtocolOutput, error) } // UnimplementedKeyProviderServiceServer can be embedded to have forward compatible implementations. type UnimplementedKeyProviderServiceServer struct { } func (*UnimplementedKeyProviderServiceServer) WrapKey(ctx context.Context, req *KeyProviderKeyWrapProtocolInput) (*KeyProviderKeyWrapProtocolOutput, error) { return nil, status.Errorf(codes.Unimplemented, "method WrapKey not implemented") } func (*UnimplementedKeyProviderServiceServer) UnWrapKey(ctx context.Context, req *KeyProviderKeyWrapProtocolInput) (*KeyProviderKeyWrapProtocolOutput, error) { return nil, status.Errorf(codes.Unimplemented, "method UnWrapKey not implemented") } func RegisterKeyProviderServiceServer(s *grpc.Server, srv KeyProviderServiceServer) { s.RegisterService(&_KeyProviderService_serviceDesc, srv) } func _KeyProviderService_WrapKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(KeyProviderKeyWrapProtocolInput) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(KeyProviderServiceServer).WrapKey(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/keyprovider.KeyProviderService/WrapKey", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(KeyProviderServiceServer).WrapKey(ctx, req.(*KeyProviderKeyWrapProtocolInput)) } return interceptor(ctx, in, info, handler) } func _KeyProviderService_UnWrapKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(KeyProviderKeyWrapProtocolInput) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(KeyProviderServiceServer).UnWrapKey(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/keyprovider.KeyProviderService/UnWrapKey", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(KeyProviderServiceServer).UnWrapKey(ctx, req.(*KeyProviderKeyWrapProtocolInput)) } return interceptor(ctx, in, info, handler) } var _KeyProviderService_serviceDesc = grpc.ServiceDesc{ ServiceName: "keyprovider.KeyProviderService", HandlerType: (*KeyProviderServiceServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "WrapKey", Handler: _KeyProviderService_WrapKey_Handler, }, { MethodName: "UnWrapKey", Handler: _KeyProviderService_UnWrapKey_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "keyprovider.proto", } ================================================ FILE: vendor/github.com/containers/ocicrypt/utils/keyprovider/keyprovider.proto ================================================ syntax = "proto3"; package keyprovider; option go_package = "keyprovider"; message keyProviderKeyWrapProtocolInput { bytes KeyProviderKeyWrapProtocolInput = 1; } message keyProviderKeyWrapProtocolOutput { bytes KeyProviderKeyWrapProtocolOutput = 1; } service KeyProviderService { rpc WrapKey(keyProviderKeyWrapProtocolInput) returns (keyProviderKeyWrapProtocolOutput) {}; rpc UnWrapKey(keyProviderKeyWrapProtocolInput) returns (keyProviderKeyWrapProtocolOutput) {}; } ================================================ FILE: vendor/github.com/containers/ocicrypt/utils/testing.go ================================================ /* Copyright The ocicrypt Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package utils import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "fmt" "math/big" "time" ) // CreateRSAKey creates an RSA key func CreateRSAKey(bits int) (*rsa.PrivateKey, error) { key, err := rsa.GenerateKey(rand.Reader, bits) if err != nil { return nil, fmt.Errorf("rsa.GenerateKey failed: %w", err) } return key, nil } // CreateECDSAKey creates an elliptic curve key for the given curve func CreateECDSAKey(curve elliptic.Curve) (*ecdsa.PrivateKey, error) { key, err := ecdsa.GenerateKey(curve, rand.Reader) if err != nil { return nil, fmt.Errorf("ecdsa.GenerateKey failed: %w", err) } return key, nil } // CreateRSATestKey creates an RSA key of the given size and returns // the public and private key in PEM or DER format func CreateRSATestKey(bits int, password []byte, pemencode bool) ([]byte, []byte, error) { key, err := CreateRSAKey(bits) if err != nil { return nil, nil, err } pubData, err := x509.MarshalPKIXPublicKey(&key.PublicKey) if err != nil { return nil, nil, fmt.Errorf("x509.MarshalPKIXPublicKey failed: %w", err) } privData := x509.MarshalPKCS1PrivateKey(key) // no more encoding needed for DER if !pemencode { return pubData, privData, nil } publicKey := pem.EncodeToMemory(&pem.Block{ Type: "PUBLIC KEY", Bytes: pubData, }) var block *pem.Block typ := "RSA PRIVATE KEY" if len(password) > 0 { block, err = x509.EncryptPEMBlock(rand.Reader, typ, privData, password, x509.PEMCipherAES256) //nolint:staticcheck // ignore SA1019, which is kept for backward compatibility if err != nil { return nil, nil, fmt.Errorf("x509.EncryptPEMBlock failed: %w", err) } } else { block = &pem.Block{ Type: typ, Bytes: privData, } } privateKey := pem.EncodeToMemory(block) return publicKey, privateKey, nil } // CreateECDSATestKey creates and elliptic curve key for the given curve and returns // the public and private key in DER format func CreateECDSATestKey(curve elliptic.Curve) ([]byte, []byte, error) { key, err := CreateECDSAKey(curve) if err != nil { return nil, nil, err } pubData, err := x509.MarshalPKIXPublicKey(&key.PublicKey) if err != nil { return nil, nil, fmt.Errorf("x509.MarshalPKIXPublicKey failed: %w", err) } privData, err := x509.MarshalECPrivateKey(key) if err != nil { return nil, nil, fmt.Errorf("x509.MarshalECPrivateKey failed: %w", err) } return pubData, privData, nil } // CreateTestCA creates a root CA for testing func CreateTestCA() (*rsa.PrivateKey, *x509.Certificate, error) { key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return nil, nil, fmt.Errorf("rsa.GenerateKey failed: %w", err) } ca := &x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{ CommonName: "test-ca", }, NotBefore: time.Now(), NotAfter: time.Now().AddDate(1, 0, 0), IsCA: true, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, BasicConstraintsValid: true, } caCert, err := certifyKey(&key.PublicKey, ca, key, ca) return key, caCert, err } // CertifyKey certifies a public key using the given CA's private key and cert; // The certificate template for the public key is optional func CertifyKey(pubbytes []byte, template *x509.Certificate, caKey *rsa.PrivateKey, caCert *x509.Certificate) (*x509.Certificate, error) { pubKey, err := ParsePublicKey(pubbytes, "CertifyKey") if err != nil { return nil, err } return certifyKey(pubKey, template, caKey, caCert) } func certifyKey(pub interface{}, template *x509.Certificate, caKey *rsa.PrivateKey, caCert *x509.Certificate) (*x509.Certificate, error) { if template == nil { template = &x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{ CommonName: "testkey", }, NotBefore: time.Now(), NotAfter: time.Now().Add(time.Hour), IsCA: false, KeyUsage: x509.KeyUsageDigitalSignature, BasicConstraintsValid: true, } } certDER, err := x509.CreateCertificate(rand.Reader, template, caCert, pub, caKey) if err != nil { return nil, fmt.Errorf("x509.CreateCertificate failed: %w", err) } cert, err := x509.ParseCertificate(certDER) if err != nil { return nil, fmt.Errorf("x509.ParseCertificate failed: %w", err) } return cert, nil } ================================================ FILE: vendor/github.com/containers/ocicrypt/utils/utils.go ================================================ /* Copyright The ocicrypt Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package utils import ( "bytes" "crypto/x509" "encoding/base64" "encoding/pem" "errors" "fmt" "strings" "github.com/containers/ocicrypt/crypto/pkcs11" "github.com/go-jose/go-jose/v4" "golang.org/x/crypto/openpgp" ) // parseJWKPrivateKey parses the input byte array as a JWK and makes sure it's a private key func parseJWKPrivateKey(privKey []byte, prefix string) (interface{}, error) { jwk := jose.JSONWebKey{} err := jwk.UnmarshalJSON(privKey) if err != nil { return nil, fmt.Errorf("%s: Could not parse input as JWK: %w", prefix, err) } if jwk.IsPublic() { return nil, fmt.Errorf("%s: JWK is not a private key", prefix) } return &jwk, nil } // parseJWKPublicKey parses the input byte array as a JWK func parseJWKPublicKey(privKey []byte, prefix string) (interface{}, error) { jwk := jose.JSONWebKey{} err := jwk.UnmarshalJSON(privKey) if err != nil { return nil, fmt.Errorf("%s: Could not parse input as JWK: %w", prefix, err) } if !jwk.IsPublic() { return nil, fmt.Errorf("%s: JWK is not a public key", prefix) } return &jwk, nil } // parsePkcs11PrivateKeyYaml parses the input byte array as pkcs11 key file yaml format) func parsePkcs11PrivateKeyYaml(yaml []byte, prefix string) (*pkcs11.Pkcs11KeyFileObject, error) { // if the URI does not have enough attributes, we will throw an error when decrypting return pkcs11.ParsePkcs11KeyFile(yaml) } // parsePkcs11URIPublicKey parses the input byte array as a pkcs11 key file yaml func parsePkcs11PublicKeyYaml(yaml []byte, prefix string) (*pkcs11.Pkcs11KeyFileObject, error) { // if the URI does not have enough attributes, we will throw an error when decrypting return pkcs11.ParsePkcs11KeyFile(yaml) } // IsPasswordError checks whether an error is related to a missing or wrong // password func IsPasswordError(err error) bool { if err == nil { return false } msg := strings.ToLower(err.Error()) return strings.Contains(msg, "password") && (strings.Contains(msg, "missing") || strings.Contains(msg, "wrong")) } // ParsePrivateKey tries to parse a private key in DER format first and // PEM format after, returning an error if the parsing failed func ParsePrivateKey(privKey, privKeyPassword []byte, prefix string) (interface{}, error) { key, err := x509.ParsePKCS8PrivateKey(privKey) if err != nil { key, err = x509.ParsePKCS1PrivateKey(privKey) if err != nil { key, err = x509.ParseECPrivateKey(privKey) } } if err != nil { block, _ := pem.Decode(privKey) if block != nil { var der []byte if x509.IsEncryptedPEMBlock(block) { //nolint:staticcheck // ignore SA1019, which is kept for backward compatibility if privKeyPassword == nil { return nil, fmt.Errorf("%s: Missing password for encrypted private key", prefix) } der, err = x509.DecryptPEMBlock(block, privKeyPassword) //nolint:staticcheck // ignore SA1019, which is kept for backward compatibility if err != nil { return nil, fmt.Errorf("%s: Wrong password: could not decrypt private key", prefix) } } else { der = block.Bytes } key, err = x509.ParsePKCS8PrivateKey(der) if err != nil { key, err = x509.ParsePKCS1PrivateKey(der) if err != nil { return nil, fmt.Errorf("%s: Could not parse private key: %w", prefix, err) } } } else { key, err = parseJWKPrivateKey(privKey, prefix) if err != nil { key, err = parsePkcs11PrivateKeyYaml(privKey, prefix) } } } return key, err } // IsPrivateKey returns true in case the given byte array represents a private key // It returns an error if for example the password is wrong func IsPrivateKey(data []byte, password []byte) (bool, error) { _, err := ParsePrivateKey(data, password, "") return err == nil, err } // IsPkcs11PrivateKey returns true in case the given byte array represents a pkcs11 private key func IsPkcs11PrivateKey(data []byte) bool { return pkcs11.IsPkcs11PrivateKey(data) } // ParsePublicKey tries to parse a public key in DER format first and // PEM format after, returning an error if the parsing failed func ParsePublicKey(pubKey []byte, prefix string) (interface{}, error) { key, err := x509.ParsePKIXPublicKey(pubKey) if err != nil { block, _ := pem.Decode(pubKey) if block != nil { key, err = x509.ParsePKIXPublicKey(block.Bytes) if err != nil { return nil, fmt.Errorf("%s: Could not parse public key: %w", prefix, err) } } else { key, err = parseJWKPublicKey(pubKey, prefix) if err != nil { key, err = parsePkcs11PublicKeyYaml(pubKey, prefix) } } } return key, err } // IsPublicKey returns true in case the given byte array represents a public key func IsPublicKey(data []byte) bool { _, err := ParsePublicKey(data, "") return err == nil } // IsPkcs11PublicKey returns true in case the given byte array represents a pkcs11 public key func IsPkcs11PublicKey(data []byte) bool { return pkcs11.IsPkcs11PublicKey(data) } // ParseCertificate tries to parse a public key in DER format first and // PEM format after, returning an error if the parsing failed func ParseCertificate(certBytes []byte, prefix string) (*x509.Certificate, error) { x509Cert, err := x509.ParseCertificate(certBytes) if err != nil { block, _ := pem.Decode(certBytes) if block == nil { return nil, fmt.Errorf("%s: Could not PEM decode x509 certificate", prefix) } x509Cert, err = x509.ParseCertificate(block.Bytes) if err != nil { return nil, fmt.Errorf("%s: Could not parse x509 certificate: %w", prefix, err) } } return x509Cert, err } // IsCertificate returns true in case the given byte array represents an x.509 certificate func IsCertificate(data []byte) bool { _, err := ParseCertificate(data, "") return err == nil } // IsGPGPrivateKeyRing returns true in case the given byte array represents a GPG private key ring file func IsGPGPrivateKeyRing(data []byte) bool { r := bytes.NewBuffer(data) _, err := openpgp.ReadKeyRing(r) return err == nil } // SortDecryptionKeys parses a list of comma separated base64 entries and sorts the data into // a map. Each entry in the list may be either a GPG private key ring, private key, or x.509 // certificate func SortDecryptionKeys(b64ItemList string) (map[string][][]byte, error) { dcparameters := make(map[string][][]byte) for _, b64Item := range strings.Split(b64ItemList, ",") { var password []byte b64Data := strings.Split(b64Item, ":") keyData, err := base64.StdEncoding.DecodeString(b64Data[0]) if err != nil { return nil, errors.New("Could not base64 decode a passed decryption key") } if len(b64Data) == 2 { password, err = base64.StdEncoding.DecodeString(b64Data[1]) if err != nil { return nil, errors.New("Could not base64 decode a passed decryption key password") } } var key string isPrivKey, err := IsPrivateKey(keyData, password) if IsPasswordError(err) { return nil, err } if isPrivKey { key = "privkeys" if _, ok := dcparameters["privkeys-passwords"]; !ok { dcparameters["privkeys-passwords"] = [][]byte{password} } else { dcparameters["privkeys-passwords"] = append(dcparameters["privkeys-passwords"], password) } } else if IsCertificate(keyData) { key = "x509s" } else if IsGPGPrivateKeyRing(keyData) { key = "gpg-privatekeys" } if key != "" { values := dcparameters[key] if values == nil { dcparameters[key] = [][]byte{keyData} } else { dcparameters[key] = append(dcparameters[key], keyData) } } else { return nil, errors.New("Unknown decryption key type") } } return dcparameters, nil } ================================================ FILE: vendor/github.com/coreos/go-systemd/v22/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: You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and 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 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: vendor/github.com/coreos/go-systemd/v22/NOTICE ================================================ CoreOS Project Copyright 2018 CoreOS, Inc This product includes software developed at CoreOS, Inc. (http://www.coreos.com/). ================================================ FILE: vendor/github.com/coreos/go-systemd/v22/dbus/dbus.go ================================================ // Copyright 2015 CoreOS, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package dbus provides integration with the systemd D-Bus API. // See http://www.freedesktop.org/wiki/Software/systemd/dbus/ package dbus import ( "context" "encoding/hex" "fmt" "os" "strconv" "strings" "sync" "github.com/godbus/dbus/v5" ) const ( alpha = `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ` num = `0123456789` alphanum = alpha + num signalBuffer = 100 ) // needsEscape checks whether a byte in a potential dbus ObjectPath needs to be escaped func needsEscape(i int, b byte) bool { // Escape everything that is not a-z-A-Z-0-9 // Also escape 0-9 if it's the first character return strings.IndexByte(alphanum, b) == -1 || (i == 0 && strings.IndexByte(num, b) != -1) } // PathBusEscape sanitizes a constituent string of a dbus ObjectPath using the // rules that systemd uses for serializing special characters. func PathBusEscape(path string) string { // Special case the empty string if len(path) == 0 { return "_" } n := []byte{} for i := 0; i < len(path); i++ { c := path[i] if needsEscape(i, c) { e := fmt.Sprintf("_%x", c) n = append(n, []byte(e)...) } else { n = append(n, c) } } return string(n) } // pathBusUnescape is the inverse of PathBusEscape. func pathBusUnescape(path string) string { if path == "_" { return "" } n := []byte{} for i := 0; i < len(path); i++ { c := path[i] if c == '_' && i+2 < len(path) { res, err := hex.DecodeString(path[i+1 : i+3]) if err == nil { n = append(n, res...) } i += 2 } else { n = append(n, c) } } return string(n) } // Conn is a connection to systemd's dbus endpoint. type Conn struct { // sysconn/sysobj are only used to call dbus methods sysconn *dbus.Conn sysobj dbus.BusObject // sigconn/sigobj are only used to receive dbus signals sigconn *dbus.Conn sigobj dbus.BusObject jobListener struct { jobs map[dbus.ObjectPath][]chan<- string sync.Mutex } subStateSubscriber struct { updateCh chan<- *SubStateUpdate errCh chan<- error sync.Mutex ignore map[dbus.ObjectPath]int64 cleanIgnore int64 } propertiesSubscriber struct { updateCh chan<- *PropertiesUpdate errCh chan<- error sync.Mutex } } // Deprecated: use NewWithContext instead. func New() (*Conn, error) { return NewWithContext(context.Background()) } // NewWithContext establishes a connection to any available bus and authenticates. // Callers should call Close() when done with the connection. func NewWithContext(ctx context.Context) (*Conn, error) { conn, err := NewSystemConnectionContext(ctx) if err != nil && os.Geteuid() == 0 { return NewSystemdConnectionContext(ctx) } return conn, err } // Deprecated: use NewSystemConnectionContext instead. func NewSystemConnection() (*Conn, error) { return NewSystemConnectionContext(context.Background()) } // NewSystemConnectionContext establishes a connection to the system bus and authenticates. // Callers should call Close() when done with the connection. func NewSystemConnectionContext(ctx context.Context) (*Conn, error) { return NewConnection(func() (*dbus.Conn, error) { return dbusAuthHelloConnection(ctx, dbus.SystemBusPrivate) }) } // Deprecated: use NewUserConnectionContext instead. func NewUserConnection() (*Conn, error) { return NewUserConnectionContext(context.Background()) } // NewUserConnectionContext establishes a connection to the session bus and // authenticates. This can be used to connect to systemd user instances. // Callers should call Close() when done with the connection. func NewUserConnectionContext(ctx context.Context) (*Conn, error) { return NewConnection(func() (*dbus.Conn, error) { return dbusAuthHelloConnection(ctx, dbus.SessionBusPrivate) }) } // Deprecated: use NewSystemdConnectionContext instead. func NewSystemdConnection() (*Conn, error) { return NewSystemdConnectionContext(context.Background()) } // NewSystemdConnectionContext establishes a private, direct connection to systemd. // This can be used for communicating with systemd without a dbus daemon. // Callers should call Close() when done with the connection. func NewSystemdConnectionContext(ctx context.Context) (*Conn, error) { return NewConnection(func() (*dbus.Conn, error) { // We skip Hello when talking directly to systemd. return dbusAuthConnection(ctx, func(opts ...dbus.ConnOption) (*dbus.Conn, error) { return dbus.Dial("unix:path=/run/systemd/private", opts...) }) }) } // Close closes an established connection. func (c *Conn) Close() { c.sysconn.Close() c.sigconn.Close() } // Connected returns whether conn is connected func (c *Conn) Connected() bool { return c.sysconn.Connected() && c.sigconn.Connected() } // NewConnection establishes a connection to a bus using a caller-supplied function. // This allows connecting to remote buses through a user-supplied mechanism. // The supplied function may be called multiple times, and should return independent connections. // The returned connection must be fully initialised: the org.freedesktop.DBus.Hello call must have succeeded, // and any authentication should be handled by the function. func NewConnection(dialBus func() (*dbus.Conn, error)) (*Conn, error) { sysconn, err := dialBus() if err != nil { return nil, err } sigconn, err := dialBus() if err != nil { sysconn.Close() return nil, err } c := &Conn{ sysconn: sysconn, sysobj: systemdObject(sysconn), sigconn: sigconn, sigobj: systemdObject(sigconn), } c.subStateSubscriber.ignore = make(map[dbus.ObjectPath]int64) c.jobListener.jobs = make(map[dbus.ObjectPath][]chan<- string) // Setup the listeners on jobs so that we can get completions c.sigconn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, "type='signal', interface='org.freedesktop.systemd1.Manager', member='JobRemoved'") c.dispatch() return c, nil } // GetManagerProperty returns the value of a property on the org.freedesktop.systemd1.Manager // interface. The value is returned in its string representation, as defined at // https://developer.gnome.org/glib/unstable/gvariant-text.html. func (c *Conn) GetManagerProperty(prop string) (string, error) { variant, err := c.sysobj.GetProperty("org.freedesktop.systemd1.Manager." + prop) if err != nil { return "", err } return variant.String(), nil } func dbusAuthConnection(ctx context.Context, createBus func(opts ...dbus.ConnOption) (*dbus.Conn, error)) (*dbus.Conn, error) { conn, err := createBus(dbus.WithContext(ctx)) if err != nil { return nil, err } // Only use EXTERNAL method, and hardcode the uid (not username) // to avoid a username lookup (which requires a dynamically linked // libc) methods := []dbus.Auth{dbus.AuthExternal(strconv.Itoa(os.Getuid()))} err = conn.Auth(methods) if err != nil { conn.Close() return nil, err } return conn, nil } func dbusAuthHelloConnection(ctx context.Context, createBus func(opts ...dbus.ConnOption) (*dbus.Conn, error)) (*dbus.Conn, error) { conn, err := dbusAuthConnection(ctx, createBus) if err != nil { return nil, err } if err = conn.Hello(); err != nil { conn.Close() return nil, err } return conn, nil } func systemdObject(conn *dbus.Conn) dbus.BusObject { return conn.Object("org.freedesktop.systemd1", dbus.ObjectPath("/org/freedesktop/systemd1")) } ================================================ FILE: vendor/github.com/coreos/go-systemd/v22/dbus/methods.go ================================================ // Copyright 2015, 2018 CoreOS, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package dbus import ( "context" "errors" "fmt" "path" "strconv" "github.com/godbus/dbus/v5" ) // Who specifies which process to send a signal to via the [Conn.KillUnitWithTarget]. type Who string const ( // All sends the signal to all processes in the unit. All Who = "all" // Main sends the signal to the main process of the unit. Main Who = "main" // Control sends the signal to the control process of the unit. Control Who = "control" ) func (c *Conn) jobComplete(signal *dbus.Signal) { var id uint32 var job dbus.ObjectPath var unit string var result string _ = dbus.Store(signal.Body, &id, &job, &unit, &result) c.jobListener.Lock() for _, out := range c.jobListener.jobs[job] { out <- result } delete(c.jobListener.jobs, job) c.jobListener.Unlock() } func (c *Conn) startJob(ctx context.Context, ch chan<- string, job string, args ...any) (int, error) { if ch != nil { c.jobListener.Lock() defer c.jobListener.Unlock() } var p dbus.ObjectPath err := c.sysobj.CallWithContext(ctx, job, 0, args...).Store(&p) if err != nil { return 0, err } if ch != nil { c.jobListener.jobs[p] = append(c.jobListener.jobs[p], ch) } // ignore error since 0 is fine if conversion fails jobID, _ := strconv.Atoi(path.Base(string(p))) return jobID, nil } // Deprecated: use StartUnitContext instead. func (c *Conn) StartUnit(name string, mode string, ch chan<- string) (int, error) { return c.StartUnitContext(context.Background(), name, mode, ch) } // StartUnitContext enqueues a start job and depending jobs, if any (unless otherwise // specified by the mode string). // // Takes the unit to activate, plus a mode string. The mode needs to be one of // replace, fail, isolate, ignore-dependencies, ignore-requirements. If // "replace" the call will start the unit and its dependencies, possibly // replacing already queued jobs that conflict with this. If "fail" the call // will start the unit and its dependencies, but will fail if this would change // an already queued job. If "isolate" the call will start the unit in question // and terminate all units that aren't dependencies of it. If // "ignore-dependencies" it will start a unit but ignore all its dependencies. // If "ignore-requirements" it will start a unit but only ignore the // requirement dependencies. It is not recommended to make use of the latter // two options. // // If the provided channel is non-nil, a result string will be sent to it upon // job completion: one of done, canceled, timeout, failed, dependency, skipped. // done indicates successful execution of a job. canceled indicates that a job // has been canceled before it finished execution. timeout indicates that the // job timeout was reached. failed indicates that the job failed. dependency // indicates that a job this job has been depending on failed and the job hence // has been removed too. skipped indicates that a job was skipped because it // didn't apply to the units current state. // // Important: It is the caller's responsibility to unblock the provided channel write, // either by reading from the channel or by using a buffered channel. Until the write // is unblocked, the Conn object cannot handle other jobs. // // If no error occurs, the ID of the underlying systemd job will be returned. There // does exist the possibility for no error to be returned, but for the returned job // ID to be 0. In this case, the actual underlying ID is not 0 and this datapoint // should not be considered authoritative. // // If an error does occur, it will be returned to the user alongside a job ID of 0. func (c *Conn) StartUnitContext(ctx context.Context, name string, mode string, ch chan<- string) (int, error) { return c.startJob(ctx, ch, "org.freedesktop.systemd1.Manager.StartUnit", name, mode) } // Deprecated: use StopUnitContext instead. func (c *Conn) StopUnit(name string, mode string, ch chan<- string) (int, error) { return c.StopUnitContext(context.Background(), name, mode, ch) } // StopUnitContext is similar to StartUnitContext, but stops the specified unit // rather than starting it. func (c *Conn) StopUnitContext(ctx context.Context, name string, mode string, ch chan<- string) (int, error) { return c.startJob(ctx, ch, "org.freedesktop.systemd1.Manager.StopUnit", name, mode) } // Deprecated: use ReloadUnitContext instead. func (c *Conn) ReloadUnit(name string, mode string, ch chan<- string) (int, error) { return c.ReloadUnitContext(context.Background(), name, mode, ch) } // ReloadUnitContext reloads a unit. Reloading is done only if the unit // is already running, and fails otherwise. func (c *Conn) ReloadUnitContext(ctx context.Context, name string, mode string, ch chan<- string) (int, error) { return c.startJob(ctx, ch, "org.freedesktop.systemd1.Manager.ReloadUnit", name, mode) } // Deprecated: use RestartUnitContext instead. func (c *Conn) RestartUnit(name string, mode string, ch chan<- string) (int, error) { return c.RestartUnitContext(context.Background(), name, mode, ch) } // RestartUnitContext restarts a service. If a service is restarted that isn't // running it will be started. func (c *Conn) RestartUnitContext(ctx context.Context, name string, mode string, ch chan<- string) (int, error) { return c.startJob(ctx, ch, "org.freedesktop.systemd1.Manager.RestartUnit", name, mode) } // Deprecated: use TryRestartUnitContext instead. func (c *Conn) TryRestartUnit(name string, mode string, ch chan<- string) (int, error) { return c.TryRestartUnitContext(context.Background(), name, mode, ch) } // TryRestartUnitContext is like RestartUnitContext, except that a service that // isn't running is not affected by the restart. func (c *Conn) TryRestartUnitContext(ctx context.Context, name string, mode string, ch chan<- string) (int, error) { return c.startJob(ctx, ch, "org.freedesktop.systemd1.Manager.TryRestartUnit", name, mode) } // Deprecated: use ReloadOrRestartUnitContext instead. func (c *Conn) ReloadOrRestartUnit(name string, mode string, ch chan<- string) (int, error) { return c.ReloadOrRestartUnitContext(context.Background(), name, mode, ch) } // ReloadOrRestartUnitContext attempts a reload if the unit supports it and use // a restart otherwise. func (c *Conn) ReloadOrRestartUnitContext(ctx context.Context, name string, mode string, ch chan<- string) (int, error) { return c.startJob(ctx, ch, "org.freedesktop.systemd1.Manager.ReloadOrRestartUnit", name, mode) } // Deprecated: use ReloadOrTryRestartUnitContext instead. func (c *Conn) ReloadOrTryRestartUnit(name string, mode string, ch chan<- string) (int, error) { return c.ReloadOrTryRestartUnitContext(context.Background(), name, mode, ch) } // ReloadOrTryRestartUnitContext attempts a reload if the unit supports it, // and use a "Try" flavored restart otherwise. func (c *Conn) ReloadOrTryRestartUnitContext(ctx context.Context, name string, mode string, ch chan<- string) (int, error) { return c.startJob(ctx, ch, "org.freedesktop.systemd1.Manager.ReloadOrTryRestartUnit", name, mode) } // Deprecated: use StartTransientUnitContext instead. func (c *Conn) StartTransientUnit(name string, mode string, properties []Property, ch chan<- string) (int, error) { return c.StartTransientUnitContext(context.Background(), name, mode, properties, ch) } // StartTransientUnitContext may be used to create and start a transient unit, which // will be released as soon as it is not running or referenced anymore or the // system is rebooted. name is the unit name including suffix, and must be // unique. mode is the same as in StartUnitContext, properties contains properties // of the unit. func (c *Conn) StartTransientUnitContext(ctx context.Context, name string, mode string, properties []Property, ch chan<- string) (int, error) { return c.StartTransientUnitAux(ctx, name, mode, properties, make([]PropertyCollection, 0), ch) } // StartTransientUnitAux is the same as StartTransientUnitContext but allows passing // auxiliary units in the aux parameter. func (c *Conn) StartTransientUnitAux(ctx context.Context, name string, mode string, properties []Property, aux []PropertyCollection, ch chan<- string) (int, error) { return c.startJob(ctx, ch, "org.freedesktop.systemd1.Manager.StartTransientUnit", name, mode, properties, aux) } // Deprecated: use [Conn.KillUnitWithTarget] instead. func (c *Conn) KillUnit(name string, signal int32) { c.KillUnitContext(context.Background(), name, signal) } // KillUnitContext takes the unit name and a UNIX signal number to send. // All of the unit's processes are killed. // // Deprecated: use [Conn.KillUnitWithTarget] instead, with target argument set to [All]. func (c *Conn) KillUnitContext(ctx context.Context, name string, signal int32) { _ = c.KillUnitWithTarget(ctx, name, All, signal) } // KillUnitWithTarget sends a signal to the specified unit. // The target argument can be one of [All], [Main], or [Control]. func (c *Conn) KillUnitWithTarget(ctx context.Context, name string, target Who, signal int32) error { return c.sysobj.CallWithContext(ctx, "org.freedesktop.systemd1.Manager.KillUnit", 0, name, string(target), signal).Store() } // Deprecated: use ResetFailedUnitContext instead. func (c *Conn) ResetFailedUnit(name string) error { return c.ResetFailedUnitContext(context.Background(), name) } // ResetFailedUnitContext resets the "failed" state of a specific unit. func (c *Conn) ResetFailedUnitContext(ctx context.Context, name string) error { return c.sysobj.CallWithContext(ctx, "org.freedesktop.systemd1.Manager.ResetFailedUnit", 0, name).Store() } // Deprecated: use SystemStateContext instead. func (c *Conn) SystemState() (*Property, error) { return c.SystemStateContext(context.Background()) } // SystemStateContext returns the systemd state. Equivalent to // systemctl is-system-running. func (c *Conn) SystemStateContext(ctx context.Context) (*Property, error) { var err error var prop dbus.Variant obj := c.sysconn.Object("org.freedesktop.systemd1", "/org/freedesktop/systemd1") err = obj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.systemd1.Manager", "SystemState").Store(&prop) if err != nil { return nil, err } return &Property{Name: "SystemState", Value: prop}, nil } // getProperties takes the unit path and returns all of its dbus object properties, for the given dbus interface. func (c *Conn) getProperties(ctx context.Context, path dbus.ObjectPath, dbusInterface string) (map[string]any, error) { var err error var props map[string]dbus.Variant if !path.IsValid() { return nil, fmt.Errorf("invalid unit name: %v", path) } obj := c.sysconn.Object("org.freedesktop.systemd1", path) err = obj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.GetAll", 0, dbusInterface).Store(&props) if err != nil { return nil, err } out := make(map[string]any, len(props)) for k, v := range props { out[k] = v.Value() } return out, nil } // Deprecated: use GetUnitPropertiesContext instead. func (c *Conn) GetUnitProperties(unit string) (map[string]any, error) { return c.GetUnitPropertiesContext(context.Background(), unit) } // GetUnitPropertiesContext takes the (unescaped) unit name and returns all of // its dbus object properties. func (c *Conn) GetUnitPropertiesContext(ctx context.Context, unit string) (map[string]any, error) { path := unitPath(unit) return c.getProperties(ctx, path, "org.freedesktop.systemd1.Unit") } // Deprecated: use GetUnitPathPropertiesContext instead. func (c *Conn) GetUnitPathProperties(path dbus.ObjectPath) (map[string]any, error) { return c.GetUnitPathPropertiesContext(context.Background(), path) } // GetUnitPathPropertiesContext takes the (escaped) unit path and returns all // of its dbus object properties. func (c *Conn) GetUnitPathPropertiesContext(ctx context.Context, path dbus.ObjectPath) (map[string]any, error) { return c.getProperties(ctx, path, "org.freedesktop.systemd1.Unit") } // Deprecated: use GetAllPropertiesContext instead. func (c *Conn) GetAllProperties(unit string) (map[string]any, error) { return c.GetAllPropertiesContext(context.Background(), unit) } // GetAllPropertiesContext takes the (unescaped) unit name and returns all of // its dbus object properties. func (c *Conn) GetAllPropertiesContext(ctx context.Context, unit string) (map[string]any, error) { path := unitPath(unit) return c.getProperties(ctx, path, "") } func (c *Conn) getProperty(ctx context.Context, unit string, dbusInterface string, propertyName string) (*Property, error) { var err error var prop dbus.Variant path := unitPath(unit) if !path.IsValid() { return nil, errors.New("invalid unit name: " + unit) } obj := c.sysconn.Object("org.freedesktop.systemd1", path) err = obj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, dbusInterface, propertyName).Store(&prop) if err != nil { return nil, err } return &Property{Name: propertyName, Value: prop}, nil } // Deprecated: use GetUnitPropertyContext instead. func (c *Conn) GetUnitProperty(unit string, propertyName string) (*Property, error) { return c.GetUnitPropertyContext(context.Background(), unit, propertyName) } // GetUnitPropertyContext takes an (unescaped) unit name, and a property name, // and returns the property value. func (c *Conn) GetUnitPropertyContext(ctx context.Context, unit string, propertyName string) (*Property, error) { return c.getProperty(ctx, unit, "org.freedesktop.systemd1.Unit", propertyName) } // Deprecated: use GetServicePropertyContext instead. func (c *Conn) GetServiceProperty(service string, propertyName string) (*Property, error) { return c.GetServicePropertyContext(context.Background(), service, propertyName) } // GetServicePropertyContext returns property for given service name and property name. func (c *Conn) GetServicePropertyContext(ctx context.Context, service string, propertyName string) (*Property, error) { return c.getProperty(ctx, service, "org.freedesktop.systemd1.Service", propertyName) } // Deprecated: use GetUnitTypePropertiesContext instead. func (c *Conn) GetUnitTypeProperties(unit string, unitType string) (map[string]any, error) { return c.GetUnitTypePropertiesContext(context.Background(), unit, unitType) } // GetUnitTypePropertiesContext returns the extra properties for a unit, specific to the unit type. // Valid values for unitType: Service, Socket, Target, Device, Mount, Automount, Snapshot, Timer, Swap, Path, Slice, Scope. // Returns "dbus.Error: Unknown interface" error if the unitType is not the correct type of the unit. func (c *Conn) GetUnitTypePropertiesContext(ctx context.Context, unit string, unitType string) (map[string]any, error) { path := unitPath(unit) return c.getProperties(ctx, path, "org.freedesktop.systemd1."+unitType) } // Deprecated: use SetUnitPropertiesContext instead. func (c *Conn) SetUnitProperties(name string, runtime bool, properties ...Property) error { return c.SetUnitPropertiesContext(context.Background(), name, runtime, properties...) } // SetUnitPropertiesContext may be used to modify certain unit properties at runtime. // Not all properties may be changed at runtime, but many resource management // settings (primarily those in systemd.cgroup(5)) may. The changes are applied // instantly, and stored on disk for future boots, unless runtime is true, in which // case the settings only apply until the next reboot. name is the name of the unit // to modify. properties are the settings to set, encoded as an array of property // name and value pairs. func (c *Conn) SetUnitPropertiesContext(ctx context.Context, name string, runtime bool, properties ...Property) error { return c.sysobj.CallWithContext(ctx, "org.freedesktop.systemd1.Manager.SetUnitProperties", 0, name, runtime, properties).Store() } // Deprecated: use GetUnitTypePropertyContext instead. func (c *Conn) GetUnitTypeProperty(unit string, unitType string, propertyName string) (*Property, error) { return c.GetUnitTypePropertyContext(context.Background(), unit, unitType, propertyName) } // GetUnitTypePropertyContext takes a property name, a unit name, and a unit type, // and returns a property value. For valid values of unitType, see GetUnitTypePropertiesContext. func (c *Conn) GetUnitTypePropertyContext(ctx context.Context, unit string, unitType string, propertyName string) (*Property, error) { return c.getProperty(ctx, unit, "org.freedesktop.systemd1."+unitType, propertyName) } type UnitStatus struct { Name string // The primary unit name as string Description string // The human readable description string LoadState string // The load state (i.e. whether the unit file has been loaded successfully) ActiveState string // The active state (i.e. whether the unit is currently started or not) SubState string // The sub state (a more fine-grained version of the active state that is specific to the unit type, which the active state is not) Followed string // A unit that is being followed in its state by this unit, if there is any, otherwise the empty string. Path dbus.ObjectPath // The unit object path JobId uint32 // If there is a job queued for the job unit the numeric job id, 0 otherwise JobType string // The job type as string JobPath dbus.ObjectPath // The job object path } type storeFunc func(retvalues ...any) error // convertSlice converts a []any result into a slice of the target type T // using dbus.Store to handle the type conversion. func convertSlice[T any](result []any) ([]T, error) { converted := make([]T, len(result)) convertedInterface := make([]any, len(converted)) for i := range converted { convertedInterface[i] = &converted[i] } err := dbus.Store(result, convertedInterface...) if err != nil { return nil, err } return converted, nil } // storeSlice fetches D-Bus array results via the provided storeFunc // and converts them into a slice of the target type T. func storeSlice[T any](f storeFunc) ([]T, error) { var result []any err := f(&result) if err != nil { return nil, err } return convertSlice[T](result) } // GetUnitByPID returns the unit object path of the unit a process ID // belongs to. It takes a UNIX PID and returns the object path. The PID must // refer to an existing system process func (c *Conn) GetUnitByPID(ctx context.Context, pid uint32) (dbus.ObjectPath, error) { var result dbus.ObjectPath err := c.sysobj.CallWithContext(ctx, "org.freedesktop.systemd1.Manager.GetUnitByPID", 0, pid).Store(&result) return result, err } // GetUnitNameByPID returns the name of the unit a process ID belongs to. It // takes a UNIX PID and returns the object path. The PID must refer to an // existing system process func (c *Conn) GetUnitNameByPID(ctx context.Context, pid uint32) (string, error) { path, err := c.GetUnitByPID(ctx, pid) if err != nil { return "", err } return unitName(path), nil } // Deprecated: use ListUnitsContext instead. func (c *Conn) ListUnits() ([]UnitStatus, error) { return c.ListUnitsContext(context.Background()) } // ListUnitsContext returns an array with all currently loaded units. Note that // units may be known by multiple names at the same time, and hence there might // be more unit names loaded than actual units behind them. // Also note that a unit is only loaded if it is active and/or enabled. // Units that are both disabled and inactive will thus not be returned. func (c *Conn) ListUnitsContext(ctx context.Context) ([]UnitStatus, error) { return storeSlice[UnitStatus](c.sysobj.CallWithContext(ctx, "org.freedesktop.systemd1.Manager.ListUnits", 0).Store) } // Deprecated: use ListUnitsFilteredContext instead. func (c *Conn) ListUnitsFiltered(states []string) ([]UnitStatus, error) { return c.ListUnitsFilteredContext(context.Background(), states) } // ListUnitsFilteredContext returns an array with units filtered by state. // It takes a list of units' statuses to filter. func (c *Conn) ListUnitsFilteredContext(ctx context.Context, states []string) ([]UnitStatus, error) { return storeSlice[UnitStatus](c.sysobj.CallWithContext(ctx, "org.freedesktop.systemd1.Manager.ListUnitsFiltered", 0, states).Store) } // Deprecated: use ListUnitsByPatternsContext instead. func (c *Conn) ListUnitsByPatterns(states []string, patterns []string) ([]UnitStatus, error) { return c.ListUnitsByPatternsContext(context.Background(), states, patterns) } // ListUnitsByPatternsContext returns an array with units. // It takes a list of units' statuses and names to filter. // Note that units may be known by multiple names at the same time, // and hence there might be more unit names loaded than actual units behind them. func (c *Conn) ListUnitsByPatternsContext(ctx context.Context, states []string, patterns []string) ([]UnitStatus, error) { return storeSlice[UnitStatus](c.sysobj.CallWithContext(ctx, "org.freedesktop.systemd1.Manager.ListUnitsByPatterns", 0, states, patterns).Store) } // Deprecated: use ListUnitsByNamesContext instead. func (c *Conn) ListUnitsByNames(units []string) ([]UnitStatus, error) { return c.ListUnitsByNamesContext(context.Background(), units) } // ListUnitsByNamesContext returns an array with units. It takes a list of units' // names and returns an UnitStatus array. Comparing to ListUnitsByPatternsContext // method, this method returns statuses even for inactive or non-existing // units. Input array should contain exact unit names, but not patterns. // // Requires systemd v230 or higher. func (c *Conn) ListUnitsByNamesContext(ctx context.Context, units []string) ([]UnitStatus, error) { return storeSlice[UnitStatus](c.sysobj.CallWithContext(ctx, "org.freedesktop.systemd1.Manager.ListUnitsByNames", 0, units).Store) } type UnitFile struct { Path string Type string } // Deprecated: use ListUnitFilesContext instead. func (c *Conn) ListUnitFiles() ([]UnitFile, error) { return c.ListUnitFilesContext(context.Background()) } // ListUnitFilesContext returns an array of all available units on disk. func (c *Conn) ListUnitFilesContext(ctx context.Context) ([]UnitFile, error) { return storeSlice[UnitFile](c.sysobj.CallWithContext(ctx, "org.freedesktop.systemd1.Manager.ListUnitFiles", 0).Store) } // Deprecated: use ListUnitFilesByPatternsContext instead. func (c *Conn) ListUnitFilesByPatterns(states []string, patterns []string) ([]UnitFile, error) { return c.ListUnitFilesByPatternsContext(context.Background(), states, patterns) } // ListUnitFilesByPatternsContext returns an array of all available units on disk matched the patterns. func (c *Conn) ListUnitFilesByPatternsContext(ctx context.Context, states []string, patterns []string) ([]UnitFile, error) { return storeSlice[UnitFile](c.sysobj.CallWithContext(ctx, "org.freedesktop.systemd1.Manager.ListUnitFilesByPatterns", 0, states, patterns).Store) } type LinkUnitFileChange EnableUnitFileChange // Deprecated: use LinkUnitFilesContext instead. func (c *Conn) LinkUnitFiles(files []string, runtime bool, force bool) ([]LinkUnitFileChange, error) { return c.LinkUnitFilesContext(context.Background(), files, runtime, force) } // LinkUnitFilesContext links unit files (that are located outside of the // usual unit search paths) into the unit search path. // // It takes a list of absolute paths to unit files to link and two // booleans. // // The first boolean controls whether the unit shall be // enabled for runtime only (true, /run), or persistently (false, // /etc). // // The second controls whether symlinks pointing to other units shall // be replaced if necessary. // // This call returns a list of the changes made. The list consists of // structures with three strings: the type of the change (one of symlink // or unlink), the file name of the symlink and the destination of the // symlink. func (c *Conn) LinkUnitFilesContext(ctx context.Context, files []string, runtime bool, force bool) ([]LinkUnitFileChange, error) { return storeSlice[LinkUnitFileChange](c.sysobj.CallWithContext(ctx, "org.freedesktop.systemd1.Manager.LinkUnitFiles", 0, files, runtime, force).Store) } // Deprecated: use EnableUnitFilesContext instead. func (c *Conn) EnableUnitFiles(files []string, runtime bool, force bool) (bool, []EnableUnitFileChange, error) { return c.EnableUnitFilesContext(context.Background(), files, runtime, force) } // EnableUnitFilesContext may be used to enable one or more units in the system // (by creating symlinks to them in /etc or /run). // // It takes a list of unit files to enable (either just file names or full // absolute paths if the unit files are residing outside the usual unit // search paths), and two booleans: the first controls whether the unit shall // be enabled for runtime only (true, /run), or persistently (false, /etc). // The second one controls whether symlinks pointing to other units shall // be replaced if necessary. // // This call returns one boolean and an array with the changes made. The // boolean signals whether the unit files contained any enablement // information (i.e. an [Install]) section. The changes list consists of // structures with three strings: the type of the change (one of symlink // or unlink), the file name of the symlink and the destination of the // symlink. func (c *Conn) EnableUnitFilesContext(ctx context.Context, files []string, runtime bool, force bool) (bool, []EnableUnitFileChange, error) { var carries_install_info bool var result []any err := c.sysobj.CallWithContext(ctx, "org.freedesktop.systemd1.Manager.EnableUnitFiles", 0, files, runtime, force).Store(&carries_install_info, &result) if err != nil { return false, nil, err } changes, err := convertSlice[EnableUnitFileChange](result) if err != nil { return false, nil, err } return carries_install_info, changes, nil } type EnableUnitFileChange struct { Type string // Type of the change (one of symlink or unlink) Filename string // File name of the symlink Destination string // Destination of the symlink } // Deprecated: use DisableUnitFilesContext instead. func (c *Conn) DisableUnitFiles(files []string, runtime bool) ([]DisableUnitFileChange, error) { return c.DisableUnitFilesContext(context.Background(), files, runtime) } // DisableUnitFilesContext may be used to disable one or more units in the // system (by removing symlinks to them from /etc or /run). // // It takes a list of unit files to disable (either just file names or full // absolute paths if the unit files are residing outside the usual unit // search paths), and one boolean: whether the unit was enabled for runtime // only (true, /run), or persistently (false, /etc). // // This call returns an array with the changes made. The changes list // consists of structures with three strings: the type of the change (one of // symlink or unlink), the file name of the symlink and the destination of the // symlink. func (c *Conn) DisableUnitFilesContext(ctx context.Context, files []string, runtime bool) ([]DisableUnitFileChange, error) { return storeSlice[DisableUnitFileChange](c.sysobj.CallWithContext(ctx, "org.freedesktop.systemd1.Manager.DisableUnitFiles", 0, files, runtime).Store) } type DisableUnitFileChange struct { Type string // Type of the change (one of symlink or unlink) Filename string // File name of the symlink Destination string // Destination of the symlink } // Deprecated: use MaskUnitFilesContext instead. func (c *Conn) MaskUnitFiles(files []string, runtime bool, force bool) ([]MaskUnitFileChange, error) { return c.MaskUnitFilesContext(context.Background(), files, runtime, force) } // MaskUnitFilesContext masks one or more units in the system. // // The files argument contains a list of units to mask (either just file names // or full absolute paths if the unit files are residing outside the usual unit // search paths). // // The runtime argument is used to specify whether the unit was enabled for // runtime only (true, /run/systemd/..), or persistently (false, // /etc/systemd/..). func (c *Conn) MaskUnitFilesContext(ctx context.Context, files []string, runtime bool, force bool) ([]MaskUnitFileChange, error) { return storeSlice[MaskUnitFileChange](c.sysobj.CallWithContext(ctx, "org.freedesktop.systemd1.Manager.MaskUnitFiles", 0, files, runtime, force).Store) } type MaskUnitFileChange struct { Type string // Type of the change (one of symlink or unlink) Filename string // File name of the symlink Destination string // Destination of the symlink } // Deprecated: use UnmaskUnitFilesContext instead. func (c *Conn) UnmaskUnitFiles(files []string, runtime bool) ([]UnmaskUnitFileChange, error) { return c.UnmaskUnitFilesContext(context.Background(), files, runtime) } // UnmaskUnitFilesContext unmasks one or more units in the system. // // It takes the list of unit files to mask (either just file names or full // absolute paths if the unit files are residing outside the usual unit search // paths), and a boolean runtime flag to specify whether the unit was enabled // for runtime only (true, /run/systemd/..), or persistently (false, // /etc/systemd/..). func (c *Conn) UnmaskUnitFilesContext(ctx context.Context, files []string, runtime bool) ([]UnmaskUnitFileChange, error) { return storeSlice[UnmaskUnitFileChange](c.sysobj.CallWithContext(ctx, "org.freedesktop.systemd1.Manager.UnmaskUnitFiles", 0, files, runtime).Store) } type UnmaskUnitFileChange struct { Type string // Type of the change (one of symlink or unlink) Filename string // File name of the symlink Destination string // Destination of the symlink } // Deprecated: use ReloadContext instead. func (c *Conn) Reload() error { return c.ReloadContext(context.Background()) } // ReloadContext instructs systemd to scan for and reload unit files. This is // an equivalent to systemctl daemon-reload. func (c *Conn) ReloadContext(ctx context.Context) error { return c.sysobj.CallWithContext(ctx, "org.freedesktop.systemd1.Manager.Reload", 0).Store() } func unitPath(name string) dbus.ObjectPath { return dbus.ObjectPath("/org/freedesktop/systemd1/unit/" + PathBusEscape(name)) } // unitName returns the unescaped base element of the supplied escaped path. func unitName(dpath dbus.ObjectPath) string { return pathBusUnescape(path.Base(string(dpath))) } // JobStatus holds a currently queued job definition. type JobStatus struct { Id uint32 // The numeric job id Unit string // The primary unit name for this job JobType string // The job type as string Status string // The job state as string JobPath dbus.ObjectPath // The job object path UnitPath dbus.ObjectPath // The unit object path } // Deprecated: use ListJobsContext instead. func (c *Conn) ListJobs() ([]JobStatus, error) { return c.ListJobsContext(context.Background()) } // ListJobsContext returns an array with all currently queued jobs. func (c *Conn) ListJobsContext(ctx context.Context) ([]JobStatus, error) { return storeSlice[JobStatus](c.sysobj.CallWithContext(ctx, "org.freedesktop.systemd1.Manager.ListJobs", 0).Store) } // FreezeUnit freezes the cgroup associated with the unit. // Note that FreezeUnit and [Conn.ThawUnit] are only supported on systems running with cgroup v2. func (c *Conn) FreezeUnit(ctx context.Context, unit string) error { return c.sysobj.CallWithContext(ctx, "org.freedesktop.systemd1.Manager.FreezeUnit", 0, unit).Store() } // ThawUnit unfreezes the cgroup associated with the unit. func (c *Conn) ThawUnit(ctx context.Context, unit string) error { return c.sysobj.CallWithContext(ctx, "org.freedesktop.systemd1.Manager.ThawUnit", 0, unit).Store() } // AttachProcessesToUnit moves existing processes, identified by pids, into an existing systemd unit. func (c *Conn) AttachProcessesToUnit(ctx context.Context, unit, subcgroup string, pids []uint32) error { return c.sysobj.CallWithContext(ctx, "org.freedesktop.systemd1.Manager.AttachProcessesToUnit", 0, unit, subcgroup, pids).Store() } ================================================ FILE: vendor/github.com/coreos/go-systemd/v22/dbus/properties.go ================================================ // Copyright 2015 CoreOS, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package dbus import ( "github.com/godbus/dbus/v5" ) // From the systemd docs: // // The properties array of StartTransientUnit() may take many of the settings // that may also be configured in unit files. Not all parameters are currently // accepted though, but we plan to cover more properties with future release. // Currently you may set the Description, Slice and all dependency types of // units, as well as RemainAfterExit, ExecStart for service units, // TimeoutStopUSec and PIDs for scope units, and CPUAccounting, CPUShares, // BlockIOAccounting, BlockIOWeight, BlockIOReadBandwidth, // BlockIOWriteBandwidth, BlockIODeviceWeight, MemoryAccounting, MemoryLimit, // DevicePolicy, DeviceAllow for services/scopes/slices. These fields map // directly to their counterparts in unit files and as normal D-Bus object // properties. The exception here is the PIDs field of scope units which is // used for construction of the scope only and specifies the initial PIDs to // add to the scope object. type Property struct { Name string Value dbus.Variant } type PropertyCollection struct { Name string Properties []Property } type execStart struct { Path string // the binary path to execute Args []string // an array with all arguments to pass to the executed command, starting with argument 0 UncleanIsFailure bool // a boolean whether it should be considered a failure if the process exits uncleanly } // PropExecStart sets the ExecStart service property. The first argument is a // slice with the binary path to execute followed by the arguments to pass to // the executed command. See // http://www.freedesktop.org/software/systemd/man/systemd.service.html#ExecStart= func PropExecStart(command []string, uncleanIsFailure bool) Property { execStarts := []execStart{ { Path: command[0], Args: command, UncleanIsFailure: uncleanIsFailure, }, } return Property{ Name: "ExecStart", Value: dbus.MakeVariant(execStarts), } } // PropRemainAfterExit sets the RemainAfterExit service property. See // http://www.freedesktop.org/software/systemd/man/systemd.service.html#RemainAfterExit= func PropRemainAfterExit(b bool) Property { return Property{ Name: "RemainAfterExit", Value: dbus.MakeVariant(b), } } // PropType sets the Type service property. See // http://www.freedesktop.org/software/systemd/man/systemd.service.html#Type= func PropType(t string) Property { return Property{ Name: "Type", Value: dbus.MakeVariant(t), } } // PropDescription sets the Description unit property. See // http://www.freedesktop.org/software/systemd/man/systemd.unit#Description= func PropDescription(desc string) Property { return Property{ Name: "Description", Value: dbus.MakeVariant(desc), } } func propDependency(name string, units []string) Property { return Property{ Name: name, Value: dbus.MakeVariant(units), } } // PropRequires sets the Requires unit property. See // http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Requires= func PropRequires(units ...string) Property { return propDependency("Requires", units) } // PropRequiresOverridable sets the RequiresOverridable unit property. See // http://www.freedesktop.org/software/systemd/man/systemd.unit.html#RequiresOverridable= func PropRequiresOverridable(units ...string) Property { return propDependency("RequiresOverridable", units) } // PropRequisite sets the Requisite unit property. See // http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Requisite= func PropRequisite(units ...string) Property { return propDependency("Requisite", units) } // PropRequisiteOverridable sets the RequisiteOverridable unit property. See // http://www.freedesktop.org/software/systemd/man/systemd.unit.html#RequisiteOverridable= func PropRequisiteOverridable(units ...string) Property { return propDependency("RequisiteOverridable", units) } // PropWants sets the Wants unit property. See // http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Wants= func PropWants(units ...string) Property { return propDependency("Wants", units) } // PropBindsTo sets the BindsTo unit property. See // http://www.freedesktop.org/software/systemd/man/systemd.unit.html#BindsTo= func PropBindsTo(units ...string) Property { return propDependency("BindsTo", units) } // PropRequiredBy sets the RequiredBy unit property. See // http://www.freedesktop.org/software/systemd/man/systemd.unit.html#RequiredBy= func PropRequiredBy(units ...string) Property { return propDependency("RequiredBy", units) } // PropRequiredByOverridable sets the RequiredByOverridable unit property. See // http://www.freedesktop.org/software/systemd/man/systemd.unit.html#RequiredByOverridable= func PropRequiredByOverridable(units ...string) Property { return propDependency("RequiredByOverridable", units) } // PropWantedBy sets the WantedBy unit property. See // http://www.freedesktop.org/software/systemd/man/systemd.unit.html#WantedBy= func PropWantedBy(units ...string) Property { return propDependency("WantedBy", units) } // PropBoundBy sets the BoundBy unit property. See // http://www.freedesktop.org/software/systemd/main/systemd.unit.html#BoundBy= func PropBoundBy(units ...string) Property { return propDependency("BoundBy", units) } // PropConflicts sets the Conflicts unit property. See // http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Conflicts= func PropConflicts(units ...string) Property { return propDependency("Conflicts", units) } // PropConflictedBy sets the ConflictedBy unit property. See // http://www.freedesktop.org/software/systemd/man/systemd.unit.html#ConflictedBy= func PropConflictedBy(units ...string) Property { return propDependency("ConflictedBy", units) } // PropBefore sets the Before unit property. See // http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Before= func PropBefore(units ...string) Property { return propDependency("Before", units) } // PropAfter sets the After unit property. See // http://www.freedesktop.org/software/systemd/man/systemd.unit.html#After= func PropAfter(units ...string) Property { return propDependency("After", units) } // PropOnFailure sets the OnFailure unit property. See // http://www.freedesktop.org/software/systemd/man/systemd.unit.html#OnFailure= func PropOnFailure(units ...string) Property { return propDependency("OnFailure", units) } // PropTriggers sets the Triggers unit property. See // http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Triggers= func PropTriggers(units ...string) Property { return propDependency("Triggers", units) } // PropTriggeredBy sets the TriggeredBy unit property. See // http://www.freedesktop.org/software/systemd/man/systemd.unit.html#TriggeredBy= func PropTriggeredBy(units ...string) Property { return propDependency("TriggeredBy", units) } // PropPropagatesReloadTo sets the PropagatesReloadTo unit property. See // http://www.freedesktop.org/software/systemd/man/systemd.unit.html#PropagatesReloadTo= func PropPropagatesReloadTo(units ...string) Property { return propDependency("PropagatesReloadTo", units) } // PropRequiresMountsFor sets the RequiresMountsFor unit property. See // http://www.freedesktop.org/software/systemd/man/systemd.unit.html#RequiresMountsFor= func PropRequiresMountsFor(units ...string) Property { return propDependency("RequiresMountsFor", units) } // PropSlice sets the Slice unit property. See // http://www.freedesktop.org/software/systemd/man/systemd.resource-control.html#Slice= func PropSlice(slice string) Property { return Property{ Name: "Slice", Value: dbus.MakeVariant(slice), } } // PropPids sets the PIDs field of scope units used in the initial construction // of the scope only and specifies the initial PIDs to add to the scope object. // See https://www.freedesktop.org/wiki/Software/systemd/ControlGroupInterface/#properties func PropPids(pids ...uint32) Property { return Property{ Name: "PIDs", Value: dbus.MakeVariant(pids), } } ================================================ FILE: vendor/github.com/coreos/go-systemd/v22/dbus/set.go ================================================ // Copyright 2015 CoreOS, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package dbus import ( "sync" ) type set struct { data map[string]bool mu sync.Mutex } func (s *set) Add(value string) { s.mu.Lock() defer s.mu.Unlock() s.data[value] = true } func (s *set) Remove(value string) { s.mu.Lock() defer s.mu.Unlock() delete(s.data, value) } func (s *set) Contains(value string) (exists bool) { s.mu.Lock() defer s.mu.Unlock() _, exists = s.data[value] return } func (s *set) Length() int { s.mu.Lock() defer s.mu.Unlock() return len(s.data) } func (s *set) Values() (values []string) { s.mu.Lock() defer s.mu.Unlock() for val := range s.data { values = append(values, val) } return } func newSet() *set { return &set{data: make(map[string]bool)} } ================================================ FILE: vendor/github.com/coreos/go-systemd/v22/dbus/subscription.go ================================================ // Copyright 2015 CoreOS, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package dbus import ( "context" "errors" "log" "time" "github.com/godbus/dbus/v5" ) const ( cleanIgnoreInterval = int64(10 * time.Second) ignoreInterval = int64(30 * time.Millisecond) ) // Subscribe sets up this connection to subscribe to all systemd dbus events. // This is required before calling SubscribeUnits. When the connection closes // systemd will automatically stop sending signals so there is no need to // explicitly call Unsubscribe(). func (c *Conn) Subscribe() error { c.sigconn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, "type='signal',interface='org.freedesktop.systemd1.Manager',member='UnitNew'") c.sigconn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, "type='signal',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'") return c.sigobj.Call("org.freedesktop.systemd1.Manager.Subscribe", 0).Store() } // Unsubscribe this connection from systemd dbus events. func (c *Conn) Unsubscribe() error { return c.sigobj.Call("org.freedesktop.systemd1.Manager.Unsubscribe", 0).Store() } func (c *Conn) dispatch() { ch := make(chan *dbus.Signal, signalBuffer) c.sigconn.Signal(ch) go func() { for { signal, ok := <-ch if !ok { return } if signal.Name == "org.freedesktop.systemd1.Manager.JobRemoved" { c.jobComplete(signal) } if c.subStateSubscriber.updateCh == nil && c.propertiesSubscriber.updateCh == nil { continue } var unitPath dbus.ObjectPath switch signal.Name { case "org.freedesktop.systemd1.Manager.JobRemoved": unitName := signal.Body[2].(string) _ = c.sysobj.Call("org.freedesktop.systemd1.Manager.GetUnit", 0, unitName).Store(&unitPath) case "org.freedesktop.systemd1.Manager.UnitNew": unitPath = signal.Body[1].(dbus.ObjectPath) case "org.freedesktop.DBus.Properties.PropertiesChanged": if signal.Body[0].(string) == "org.freedesktop.systemd1.Unit" { unitPath = signal.Path if len(signal.Body) >= 2 { if changed, ok := signal.Body[1].(map[string]dbus.Variant); ok { c.sendPropertiesUpdate(unitPath, changed) } } } } if unitPath == dbus.ObjectPath("") { continue } c.sendSubStateUpdate(unitPath) } }() } // Deprecated: use SubscribeUnitsContext instead. func (c *Conn) SubscribeUnits(interval time.Duration) (<-chan map[string]*UnitStatus, <-chan error) { return c.SubscribeUnitsContext(context.Background(), interval) } // SubscribeUnitsContext returns two unbuffered channels which will receive all changed units every // interval. Deleted units are sent as nil. func (c *Conn) SubscribeUnitsContext(ctx context.Context, interval time.Duration) (<-chan map[string]*UnitStatus, <-chan error) { return c.SubscribeUnitsCustomContext(ctx, interval, 0, func(u1, u2 *UnitStatus) bool { return *u1 != *u2 }, nil) } // Deprecated: use SubscribeUnitsCustomContext instead. func (c *Conn) SubscribeUnitsCustom(interval time.Duration, buffer int, isChanged func(*UnitStatus, *UnitStatus) bool, filterUnit func(string) bool) (<-chan map[string]*UnitStatus, <-chan error) { return c.SubscribeUnitsCustomContext(context.Background(), interval, buffer, isChanged, filterUnit) } // SubscribeUnitsCustomContext is like [Conn.SubscribeUnitsContext] but lets you specify the buffer // size of the channels, the comparison function for detecting changes and a filter // function for cutting down on the noise that your channel receives. func (c *Conn) SubscribeUnitsCustomContext(ctx context.Context, interval time.Duration, buffer int, isChanged func(*UnitStatus, *UnitStatus) bool, filterUnit func(string) bool) (<-chan map[string]*UnitStatus, <-chan error) { old := make(map[string]*UnitStatus) statusChan := make(chan map[string]*UnitStatus, buffer) errChan := make(chan error, buffer) go func() { for { timerChan := time.After(interval) units, err := c.ListUnitsContext(ctx) if err == nil { cur := make(map[string]*UnitStatus) for i := range units { if filterUnit != nil && filterUnit(units[i].Name) { continue } cur[units[i].Name] = &units[i] } // add all new or changed units changed := make(map[string]*UnitStatus) for n, u := range cur { if oldU, ok := old[n]; !ok || isChanged(oldU, u) { changed[n] = u } delete(old, n) } // add all deleted units for oldN := range old { changed[oldN] = nil } old = cur if len(changed) != 0 { statusChan <- changed } } else { errChan <- err } select { case <-timerChan: continue case <-ctx.Done(): close(statusChan) close(errChan) return } } }() return statusChan, errChan } type SubStateUpdate struct { UnitName string SubState string } // SetSubStateSubscriber writes to updateCh when any unit's substate changes. // Although this writes to updateCh on every state change, the reported state // may be more recent than the change that generated it (due to an unavoidable // race in the systemd dbus interface). That is, this method provides a good // way to keep a current view of all units' states, but is not guaranteed to // show every state transition they go through. Furthermore, state changes // will only be written to the channel with non-blocking writes. If updateCh // is full, it attempts to write an error to errCh; if errCh is full, the error // passes silently. func (c *Conn) SetSubStateSubscriber(updateCh chan<- *SubStateUpdate, errCh chan<- error) { if c == nil { msg := "nil receiver" select { case errCh <- errors.New(msg): default: log.Printf("full error channel while reporting: %s\n", msg) } return } c.subStateSubscriber.Lock() defer c.subStateSubscriber.Unlock() c.subStateSubscriber.updateCh = updateCh c.subStateSubscriber.errCh = errCh } func (c *Conn) sendSubStateUpdate(unitPath dbus.ObjectPath) { c.subStateSubscriber.Lock() defer c.subStateSubscriber.Unlock() if c.subStateSubscriber.updateCh == nil { return } isIgnored := c.shouldIgnore(unitPath) defer c.cleanIgnore() if isIgnored { return } info, err := c.GetUnitPathProperties(unitPath) if err != nil { select { case c.subStateSubscriber.errCh <- err: default: log.Printf("full error channel while reporting: %s\n", err) } return } defer c.updateIgnore(unitPath, info) name, ok := info["Id"].(string) if !ok { msg := "failed to cast info.Id" select { case c.subStateSubscriber.errCh <- errors.New(msg): default: log.Printf("full error channel while reporting: %s\n", err) } return } substate, ok := info["SubState"].(string) if !ok { msg := "failed to cast info.SubState" select { case c.subStateSubscriber.errCh <- errors.New(msg): default: log.Printf("full error channel while reporting: %s\n", msg) } return } update := &SubStateUpdate{name, substate} select { case c.subStateSubscriber.updateCh <- update: default: msg := "update channel is full" select { case c.subStateSubscriber.errCh <- errors.New(msg): default: log.Printf("full error channel while reporting: %s\n", msg) } return } } // The ignore functions work around a wart in the systemd dbus interface. // Requesting the properties of an unloaded unit will cause systemd to send a // pair of UnitNew/UnitRemoved signals. Because we need to get a unit's // properties on UnitNew (as that's the only indication of a new unit coming up // for the first time), we would enter an infinite loop if we did not attempt // to detect and ignore these spurious signals. The signal themselves are // indistinguishable from relevant ones, so we (somewhat hackishly) ignore an // unloaded unit's signals for a short time after requesting its properties. // This means that we will miss e.g. a transient unit being restarted // *immediately* upon failure and also a transient unit being started // immediately after requesting its status (with systemctl status, for example, // because this causes a UnitNew signal to be sent which then causes us to fetch // the properties). func (c *Conn) shouldIgnore(path dbus.ObjectPath) bool { t, ok := c.subStateSubscriber.ignore[path] return ok && t >= time.Now().UnixNano() } func (c *Conn) updateIgnore(path dbus.ObjectPath, info map[string]any) { loadState, ok := info["LoadState"].(string) if !ok { return } // unit is unloaded - it will trigger bad systemd dbus behavior if loadState == "not-found" { c.subStateSubscriber.ignore[path] = time.Now().UnixNano() + ignoreInterval } } // without this, ignore would grow unboundedly over time func (c *Conn) cleanIgnore() { now := time.Now().UnixNano() if c.subStateSubscriber.cleanIgnore < now { c.subStateSubscriber.cleanIgnore = now + cleanIgnoreInterval for p, t := range c.subStateSubscriber.ignore { if t < now { delete(c.subStateSubscriber.ignore, p) } } } } // PropertiesUpdate holds a map of a unit's changed properties type PropertiesUpdate struct { UnitName string Changed map[string]dbus.Variant } // SetPropertiesSubscriber writes to updateCh when any unit's properties // change. Every property change reported by systemd will be sent; that is, no // transitions will be "missed" (as they might be with SetSubStateSubscriber). // However, state changes will only be written to the channel with non-blocking // writes. If updateCh is full, it attempts to write an error to errCh; if // errCh is full, the error passes silently. func (c *Conn) SetPropertiesSubscriber(updateCh chan<- *PropertiesUpdate, errCh chan<- error) { c.propertiesSubscriber.Lock() defer c.propertiesSubscriber.Unlock() c.propertiesSubscriber.updateCh = updateCh c.propertiesSubscriber.errCh = errCh } // we don't need to worry about shouldIgnore() here because // sendPropertiesUpdate doesn't call GetProperties() func (c *Conn) sendPropertiesUpdate(unitPath dbus.ObjectPath, changedProps map[string]dbus.Variant) { c.propertiesSubscriber.Lock() defer c.propertiesSubscriber.Unlock() if c.propertiesSubscriber.updateCh == nil { return } update := &PropertiesUpdate{unitName(unitPath), changedProps} select { case c.propertiesSubscriber.updateCh <- update: default: msg := "update channel is full" select { case c.propertiesSubscriber.errCh <- errors.New(msg): default: log.Printf("full error channel while reporting: %s\n", msg) } return } } ================================================ FILE: vendor/github.com/coreos/go-systemd/v22/dbus/subscription_set.go ================================================ // Copyright 2015 CoreOS, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package dbus import ( "context" "time" ) // SubscriptionSet returns a subscription set which is like conn.Subscribe but // can filter to only return events for a set of units. type SubscriptionSet struct { *set conn *Conn } func (s *SubscriptionSet) filter(unit string) bool { return !s.Contains(unit) } // SubscribeContext starts listening for dbus events for all of the units in the set. // Returns channels identical to conn.SubscribeUnits. func (s *SubscriptionSet) SubscribeContext(ctx context.Context) (<-chan map[string]*UnitStatus, <-chan error) { // TODO: Make fully evented by using systemd 209 with properties changed values return s.conn.SubscribeUnitsCustomContext(ctx, time.Second, 0, mismatchUnitStatus, func(unit string) bool { return s.filter(unit) }, ) } // Deprecated: use SubscribeContext instead. func (s *SubscriptionSet) Subscribe() (<-chan map[string]*UnitStatus, <-chan error) { return s.SubscribeContext(context.Background()) } // NewSubscriptionSet returns a new subscription set. func (c *Conn) NewSubscriptionSet() *SubscriptionSet { return &SubscriptionSet{newSet(), c} } // mismatchUnitStatus returns true if the provided UnitStatus objects // are not equivalent. false is returned if the objects are equivalent. // Only the Name, Description and state-related fields are used in // the comparison. func mismatchUnitStatus(u1, u2 *UnitStatus) bool { return u1.Name != u2.Name || u1.Description != u2.Description || u1.LoadState != u2.LoadState || u1.ActiveState != u2.ActiveState || u1.SubState != u2.SubState } ================================================ FILE: vendor/github.com/cyberphone/json-canonicalization/LICENSE ================================================ Copyright 2018 Anders Rundgren Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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: vendor/github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer/es6numfmt.go ================================================ // // Copyright 2006-2019 WebPKI.org (http://webpki.org). // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // This package converts numbers in IEEE-754 double precision into the // format specified for JSON in EcmaScript Version 6 and forward. // The core application for this is canonicalization: // https://tools.ietf.org/html/draft-rundgren-json-canonicalization-scheme-02 package jsoncanonicalizer import ( "errors" "math" "strconv" "strings" ) const invalidPattern uint64 = 0x7ff0000000000000 func NumberToJSON(ieeeF64 float64) (res string, err error) { ieeeU64 := math.Float64bits(ieeeF64) // Special case: NaN and Infinity are invalid in JSON if (ieeeU64 & invalidPattern) == invalidPattern { return "null", errors.New("Invalid JSON number: " + strconv.FormatUint(ieeeU64, 16)) } // Special case: eliminate "-0" as mandated by the ES6-JSON/JCS specifications if ieeeF64 == 0 { // Right, this line takes both -0 and 0 return "0", nil } // Deal with the sign separately var sign string = "" if ieeeF64 < 0 { ieeeF64 =-ieeeF64 sign = "-" } // ES6 has a unique "g" format var format byte = 'e' if ieeeF64 < 1e+21 && ieeeF64 >= 1e-6 { format = 'f' } // The following should do the trick: es6Formatted := strconv.FormatFloat(ieeeF64, format, -1, 64) // Minor cleanup exponent := strings.IndexByte(es6Formatted, 'e') if exponent > 0 { // Go outputs "1e+09" which must be rewritten as "1e+9" if es6Formatted[exponent + 2] == '0' { es6Formatted = es6Formatted[:exponent + 2] + es6Formatted[exponent + 3:] } } return sign + es6Formatted, nil } ================================================ FILE: vendor/github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer/jsoncanonicalizer.go ================================================ // // Copyright 2006-2019 WebPKI.org (http://webpki.org). // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // This package transforms JSON data in UTF-8 according to: // https://tools.ietf.org/html/draft-rundgren-json-canonicalization-scheme-02 package jsoncanonicalizer import ( "errors" "container/list" "fmt" "strconv" "strings" "unicode/utf16" ) type nameValueType struct { name string sortKey []uint16 value string } // JSON standard escapes (modulo \u) var asciiEscapes = []byte{'\\', '"', 'b', 'f', 'n', 'r', 't'} var binaryEscapes = []byte{'\\', '"', '\b', '\f', '\n', '\r', '\t'} // JSON literals var literals = []string{"true", "false", "null"} func Transform(jsonData []byte) (result []byte, e error) { // JSON data MUST be UTF-8 encoded var jsonDataLength int = len(jsonData) // Current pointer in jsonData var index int = 0 // "Forward" declarations are needed for closures referring each other var parseElement func() string var parseSimpleType func() string var parseQuotedString func() string var parseObject func() string var parseArray func() string var globalError error = nil checkError := func(e error) { // We only honor the first reported error if globalError == nil { globalError = e } } setError := func(msg string) { checkError(errors.New(msg)) } isWhiteSpace := func(c byte) bool { return c == 0x20 || c == 0x0a || c == 0x0d || c == 0x09 } nextChar := func() byte { if index < jsonDataLength { c := jsonData[index] if c > 0x7f { setError("Unexpected non-ASCII character") } index++ return c } setError("Unexpected EOF reached") return '"' } scan := func() byte { for { c := nextChar() if isWhiteSpace(c) { continue; } return c } } scanFor := func(expected byte) { c := scan() if c != expected { setError("Expected '" + string(expected) + "' but got '" + string(c) + "'") } } getUEscape := func() rune { start := index nextChar() nextChar() nextChar() nextChar() if globalError != nil { return 0 } u16, err := strconv.ParseUint(string(jsonData[start:index]), 16, 64) checkError(err) return rune(u16) } testNextNonWhiteSpaceChar := func() byte { save := index c := scan() index = save return c } decorateString := func(rawUTF8 string) string { var quotedString strings.Builder quotedString.WriteByte('"') CoreLoop: for _, c := range []byte(rawUTF8) { // Is this within the JSON standard escapes? for i, esc := range binaryEscapes { if esc == c { quotedString.WriteByte('\\') quotedString.WriteByte(asciiEscapes[i]) continue CoreLoop } } if c < 0x20 { // Other ASCII control characters must be escaped with \uhhhh quotedString.WriteString(fmt.Sprintf("\\u%04x", c)) } else { quotedString.WriteByte(c) } } quotedString.WriteByte('"') return quotedString.String() } parseQuotedString = func() string { var rawString strings.Builder CoreLoop: for globalError == nil { var c byte if index < jsonDataLength { c = jsonData[index] index++ } else { nextChar() break } if (c == '"') { break; } if c < ' ' { setError("Unterminated string literal") } else if c == '\\' { // Escape sequence c = nextChar() if c == 'u' { // The \u escape firstUTF16 := getUEscape() if utf16.IsSurrogate(firstUTF16) { // If the first UTF-16 code unit has a certain value there must be // another succeeding UTF-16 code unit as well if nextChar() != '\\' || nextChar() != 'u' { setError("Missing surrogate") } else { // Output the UTF-32 code point as UTF-8 rawString.WriteRune(utf16.DecodeRune(firstUTF16, getUEscape())) } } else { // Single UTF-16 code identical to UTF-32. Output as UTF-8 rawString.WriteRune(firstUTF16) } } else if c == '/' { // Benign but useless escape rawString.WriteByte('/') } else { // The JSON standard escapes for i, esc := range asciiEscapes { if esc == c { rawString.WriteByte(binaryEscapes[i]) continue CoreLoop } } setError("Unexpected escape: \\" + string(c)) } } else { // Just an ordinary ASCII character alternatively a UTF-8 byte // outside of ASCII. // Note that properly formatted UTF-8 never clashes with ASCII // making byte per byte search for ASCII break characters work // as expected. rawString.WriteByte(c) } } return rawString.String() } parseSimpleType = func() string { var token strings.Builder index-- for globalError == nil { c := testNextNonWhiteSpaceChar() if c == ',' || c == ']' || c == '}' { break; } c = nextChar() if isWhiteSpace(c) { break } token.WriteByte(c) } if token.Len() == 0 { setError("Missing argument") } value := token.String() // Is it a JSON literal? for _, literal := range literals { if literal == value { return literal } } // Apparently not so we assume that it is a I-JSON number ieeeF64, err := strconv.ParseFloat(value, 64) checkError(err) value, err = NumberToJSON(ieeeF64) checkError(err) return value } parseElement = func() string { switch scan() { case '{': return parseObject() case '"': return decorateString(parseQuotedString()) case '[': return parseArray() default: return parseSimpleType() } } parseArray = func() string { var arrayData strings.Builder arrayData.WriteByte('[') var next bool = false for globalError == nil && testNextNonWhiteSpaceChar() != ']' { if next { scanFor(',') arrayData.WriteByte(',') } else { next = true } arrayData.WriteString(parseElement()) } scan() arrayData.WriteByte(']') return arrayData.String() } lexicographicallyPrecedes := func(sortKey []uint16, e *list.Element) bool { // Find the minimum length of the sortKeys oldSortKey := e.Value.(nameValueType).sortKey minLength := len(oldSortKey) if minLength > len(sortKey) { minLength = len(sortKey) } for q := 0; q < minLength; q++ { diff := int(sortKey[q]) - int(oldSortKey[q]) if diff < 0 { // Smaller => Precedes return true } else if diff > 0 { // Bigger => No match return false } // Still equal => Continue } // The sortKeys compared equal up to minLength if len(sortKey) < len(oldSortKey) { // Shorter => Precedes return true } if len(sortKey) == len(oldSortKey) { setError("Duplicate key: " + e.Value.(nameValueType).name) } // Longer => No match return false } parseObject = func() string { nameValueList := list.New() var next bool = false CoreLoop: for globalError == nil && testNextNonWhiteSpaceChar() != '}' { if next { scanFor(',') } next = true scanFor('"') rawUTF8 := parseQuotedString() if globalError != nil { break; } // Sort keys on UTF-16 code units // Since UTF-8 doesn't have endianess this is just a value transformation // In the Go case the transformation is UTF-8 => UTF-32 => UTF-16 sortKey := utf16.Encode([]rune(rawUTF8)) scanFor(':') nameValue := nameValueType{rawUTF8, sortKey, parseElement()} for e := nameValueList.Front(); e != nil; e = e.Next() { // Check if the key is smaller than a previous key if lexicographicallyPrecedes(sortKey, e) { // Precedes => Insert before and exit sorting nameValueList.InsertBefore(nameValue, e) continue CoreLoop } // Continue searching for a possibly succeeding sortKey // (which is straightforward since the list is ordered) } // The sortKey is either the first or is succeeding all previous sortKeys nameValueList.PushBack(nameValue) } // Scan away '}' scan() // Now everything is sorted so we can properly serialize the object var objectData strings.Builder objectData.WriteByte('{') next = false for e := nameValueList.Front(); e != nil; e = e.Next() { if next { objectData.WriteByte(',') } next = true nameValue := e.Value.(nameValueType) objectData.WriteString(decorateString(nameValue.name)) objectData.WriteByte(':') objectData.WriteString(nameValue.value) } objectData.WriteByte('}') return objectData.String() } ///////////////////////////////////////////////// // This is where Transform actually begins... // ///////////////////////////////////////////////// var transformed string if testNextNonWhiteSpaceChar() == '[' { scan() transformed = parseArray() } else { scanFor('{') transformed = parseObject() } for index < jsonDataLength { if !isWhiteSpace(jsonData[index]) { setError("Improperly terminated JSON object") break; } index++ } return []byte(transformed), globalError } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/.golangci.yml ================================================ # SPDX-License-Identifier: MPL-2.0 # Copyright (C) 2025 Aleksa Sarai # Copyright (C) 2025 SUSE LLC # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. version: "2" run: build-tags: - libpathrs linters: enable: - asasalint - asciicheck - containedctx - contextcheck - errcheck - errorlint - exhaustive - forcetypeassert - godot - goprintffuncname - govet - importas - ineffassign - makezero - misspell - musttag - nilerr - nilnesserr - nilnil - noctx - prealloc - revive - staticcheck - testifylint - unconvert - unparam - unused - usetesting settings: govet: enable: - nilness testifylint: enable-all: true formatters: enable: - gofumpt - goimports settings: goimports: local-prefixes: - github.com/cyphar/filepath-securejoin ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/CHANGELOG.md ================================================ # Changelog # All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ## ## [0.6.1] - 2025-11-19 ## > At last up jumped the cunning spider, and fiercely held her fast. ### Fixed ### - Our logic for deciding whether to use `openat2(2)` or fallback to an `O_PATH` resolver would cache the result to avoid doing needless test runs of `openat2(2)`. However, this causes issues when `pathrs-lite` is being used by a program that applies new seccomp-bpf filters onto itself -- if the filter denies `openat2(2)` then we would return that error rather than falling back to the `O_PATH` resolver. To resolve this issue, we no longer cache the result if `openat2(2)` was successful, only if there was an error. - A file descriptor leak in our `openat2` wrapper (when doing the necessary `dup` for `RESOLVE_IN_ROOT`) has been removed. ## [0.5.2] - 2025-11-19 ## > "Will you walk into my parlour?" said a spider to a fly. ### Fixed ### - Our logic for deciding whether to use `openat2(2)` or fallback to an `O_PATH` resolver would cache the result to avoid doing needless test runs of `openat2(2)`. However, this causes issues when `pathrs-lite` is being used by a program that applies new seccomp-bpf filters onto itself -- if the filter denies `openat2(2)` then we would return that error rather than falling back to the `O_PATH` resolver. To resolve this issue, we no longer cache the result if `openat2(2)` was successful, only if there was an error. - A file descriptor leak in our `openat2` wrapper (when doing the necessary `dup` for `RESOLVE_IN_ROOT`) has been removed. ## [0.6.0] - 2025-11-03 ## > By the Power of Greyskull! ### Breaking ### - The deprecated `MkdirAll`, `MkdirAllHandle`, `OpenInRoot`, `OpenatInRoot` and `Reopen` wrappers have been removed. Please switch to using `pathrs-lite` directly. ### Added ### - `pathrs-lite` now has support for using libpathrs as a backend. This is opt-in and can be enabled at build time with the `libpathrs` build tag. The intention is to allow for downstream libraries and other projects to make use of the pure-Go `github.com/cyphar/filepath-securejoin/pathrs-lite` package and distributors can then opt-in to using `libpathrs` for the entire binary if they wish. ## [0.5.1] - 2025-10-31 ## > Spooky scary skeletons send shivers down your spine! ### Changed ### - `openat2` can return `-EAGAIN` if it detects a possible attack in certain scenarios (namely if there was a rename or mount while walking a path with a `..` component). While this is necessary to avoid a denial-of-service in the kernel, it does require retry loops in userspace. In previous versions, `pathrs-lite` would retry `openat2` 32 times before returning an error, but we've received user reports that this limit can be hit on systems with very heavy load. In some synthetic benchmarks (testing the worst-case of an attacker doing renames in a tight loop on every core of a 16-core machine) we managed to get a ~3% failure rate in runc. We have improved this situation in two ways: * We have now increased this limit to 128, which should be good enough for most use-cases without becoming a denial-of-service vector (the number of syscalls called by the `O_PATH` resolver in a typical case is within the same ballpark). The same benchmarks show a failure rate of ~0.12% which (while not zero) is probably sufficient for most users. * In addition, we now return a `unix.EAGAIN` error that is bubbled up and can be detected by callers. This means that callers with stricter requirements to avoid spurious errors can choose to do their own infinite `EAGAIN` retry loop (though we would strongly recommend users use time-based deadlines in such retry loops to avoid potentially unbounded denials-of-service). ## [0.5.0] - 2025-09-26 ## > Let the past die. Kill it if you have to. > **NOTE**: With this release, some parts of > `github.com/cyphar/filepath-securejoin` are now licensed under the Mozilla > Public License (version 2). Please see [COPYING.md][] as well as the the > license header in each file for more details. [COPYING.md]: ./COPYING.md ### Breaking ### - The new API introduced in the [0.3.0][] release has been moved to a new subpackage called `pathrs-lite`. This was primarily done to better indicate the split between the new and old APIs, as well as indicate to users the purpose of this subpackage (it is a less complete version of [libpathrs][]). We have added some wrappers to the top-level package to ease the transition, but those are deprecated and will be removed in the next minor release of filepath-securejoin. Users should update their import paths. This new subpackage has also been relicensed under the Mozilla Public License (version 2), please see [COPYING.md][] for more details. ### Added ### - Most of the key bits the safe `procfs` API have now been exported and are available in `github.com/cyphar/filepath-securejoin/pathrs-lite/procfs`. At the moment this primarily consists of a new `procfs.Handle` API: * `OpenProcRoot` returns a new handle to `/proc`, endeavouring to make it safe if possible (`subset=pid` to protect against mistaken write attacks and leaks, as well as using `fsopen(2)` to avoid racing mount attacks). `OpenUnsafeProcRoot` returns a handle without attempting to create one with `subset=pid`, which makes it more dangerous to leak. Most users should use `OpenProcRoot` (even if you need to use `ProcRoot` as the base of an operation, as filepath-securejoin will internally open a handle when necessary). * The `(*procfs.Handle).Open*` family of methods lets you get a safe `O_PATH` handle to subpaths within `/proc` for certain subpaths. For `OpenThreadSelf`, the returned `ProcThreadSelfCloser` needs to be called after you completely finish using the handle (this is necessary because Go is multi-threaded and `ProcThreadSelf` references `/proc/thread-self` which may disappear if we do not `runtime.LockOSThread` -- `ProcThreadSelfCloser` is currently equivalent to `runtime.UnlockOSThread`). Note that you cannot open any `procfs` symlinks (most notably magic-links) using this API. At the moment, filepath-securejoin does not support this feature (but [libpathrs][] does). * `ProcSelfFdReadlink` lets you get the in-kernel path representation of a file descriptor (think `readlink("/proc/self/fd/...")`), except that we verify that there aren't any tricky overmounts that could fool the process. Please be aware that the returned string is simply a snapshot at that particular moment, and an attacker could move the file being pointed to. In addition, complex namespace configurations could result in non-sensical or confusing paths to be returned. The value received from this function should only be used as secondary verification of some security property, not as proof that a particular handle has a particular path. The procfs handle used internally by the API is the same as the rest of `filepath-securejoin` (for privileged programs this is usually a private in-process `procfs` instance created with `fsopen(2)`). As before, this is intended as a stop-gap before users migrate to [libpathrs][], which provides a far more extensive safe `procfs` API and is generally more robust. - Previously, the hardened procfs implementation (used internally within `Reopen` and `Open(at)InRoot`) only protected against overmount attacks on systems with `openat2(2)` (Linux 5.6) or systems with `fsopen(2)` or `open_tree(2)` (Linux 5.2) and programs with privileges to use them (with some caveats about locked mounts that probably affect very few users). For other users, an attacker with the ability to create malicious mounts (on most systems, a sysadmin) could trick you into operating on files you didn't expect. This attack only really makes sense in the context of container runtime implementations. This was considered a reasonable trade-off, as the long-term intention was to get all users to just switch to [libpathrs][] if they wanted to use the safe `procfs` API (which had more extensive protections, and is what these new protections in `filepath-securejoin` are based on). However, as the API is now being exported it seems unwise to advertise the API as "safe" if we do not protect against known attacks. The procfs API is now more protected against attackers on systems lacking the aforementioned protections. However, the most comprehensive of these protections effectively rely on [`statx(STATX_MNT_ID)`][statx.2] (Linux 5.8). On older kernel versions, there is no effective protection (there is some minimal protection against non-`procfs` filesystem components but a sufficiently clever attacker can work around those). In addition, `STATX_MNT_ID` is vulnerable to mount ID reuse attacks by sufficiently motivated and privileged attackers -- this problem is mitigated with `STATX_MNT_ID_UNIQUE` (Linux 6.8) but that raises the minimum kernel version for more protection. The fact that these protections are quite limited despite needing a fair bit of extra code to handle was one of the primary reasons we did not initially implement this in `filepath-securejoin` ([libpathrs][] supports all of this, of course). ### Fixed ### - RHEL 8 kernels have backports of `fsopen(2)` but in some testing we've found that it has very bad (and very difficult to debug) performance issues, and so we will explicitly refuse to use `fsopen(2)` if the running kernel version is pre-5.2 and will instead fallback to `open("/proc")`. [CVE-2024-21626]: https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv [libpathrs]: https://github.com/cyphar/libpathrs [statx.2]: https://www.man7.org/linux/man-pages/man2/statx.2.html ## [0.4.1] - 2025-01-28 ## ### Fixed ### - The restrictions added for `root` paths passed to `SecureJoin` in 0.4.0 was found to be too strict and caused some regressions when folks tried to update, so this restriction has been relaxed to only return an error if the path contains a `..` component. We still recommend users use `filepath.Clean` (and even `filepath.EvalSymlinks`) on the `root` path they are using, but at least you will no longer be punished for "trivial" unclean paths. ## [0.4.0] - 2025-01-13 ## ### Breaking #### - `SecureJoin(VFS)` will now return an error if the provided `root` is not a `filepath.Clean`'d path. While it is ultimately the responsibility of the caller to ensure the root is a safe path to use, passing a path like `/symlink/..` as a root would result in the `SecureJoin`'d path being placed in `/` even though `/symlink/..` might be a different directory, and so we should more strongly discourage such usage. All major users of `securejoin.SecureJoin` already ensure that the paths they provide are safe (and this is ultimately a question of user error), but removing this foot-gun is probably a good idea. Of course, this is necessarily a breaking API change (though we expect no real users to be affected by it). Thanks to [Erik Sjölund](https://github.com/eriksjolund), who initially reported this issue as a possible security issue. - `MkdirAll` and `MkdirHandle` now take an `os.FileMode`-style mode argument instead of a raw `unix.S_*`-style mode argument, which may cause compile-time type errors depending on how you use `filepath-securejoin`. For most users, there will be no change in behaviour aside from the type change (as the bottom `0o777` bits are the same in both formats, and most users are probably only using those bits). However, if you were using `unix.S_ISVTX` to set the sticky bit with `MkdirAll(Handle)` you will need to switch to `os.ModeSticky` otherwise you will get a runtime error with this update. In addition, the error message you will get from passing `unix.S_ISUID` and `unix.S_ISGID` will be different as they are treated as invalid bits now (note that previously passing said bits was also an error). ## [0.3.6] - 2024-12-17 ## ### Compatibility ### - The minimum Go version requirement for `filepath-securejoin` is now Go 1.18 (we use generics internally). For reference, `filepath-securejoin@v0.3.0` somewhat-arbitrarily bumped the Go version requirement to 1.21. While we did make some use of Go 1.21 stdlib features (and in principle Go versions <= 1.21 are no longer even supported by upstream anymore), some downstreams have complained that the version bump has meant that they have to do workarounds when backporting fixes that use the new `filepath-securejoin` API onto old branches. This is not an ideal situation, but since using this library is probably better for most downstreams than a hand-rolled workaround, we now have compatibility shims that allow us to build on older Go versions. - Lower minimum version requirement for `golang.org/x/sys` to `v0.18.0` (we need the wrappers for `fsconfig(2)`), which should also make backporting patches to older branches easier. ## [0.3.5] - 2024-12-06 ## ### Fixed ### - `MkdirAll` will now no longer return an `EEXIST` error if two racing processes are creating the same directory. We will still verify that the path is a directory, but this will avoid spurious errors when multiple threads or programs are trying to `MkdirAll` the same path. opencontainers/runc#4543 ## [0.3.4] - 2024-10-09 ## ### Fixed ### - Previously, some testing mocks we had resulted in us doing `import "testing"` in non-`_test.go` code, which made some downstreams like Kubernetes unhappy. This has been fixed. (#32) ## [0.3.3] - 2024-09-30 ## ### Fixed ### - The mode and owner verification logic in `MkdirAll` has been removed. This was originally intended to protect against some theoretical attacks but upon further consideration these protections don't actually buy us anything and they were causing spurious errors with more complicated filesystem setups. - The "is the created directory empty" logic in `MkdirAll` has also been removed. This was not causing us issues yet, but some pseudofilesystems (such as `cgroup`) create non-empty directories and so this logic would've been wrong for such cases. ## [0.3.2] - 2024-09-13 ## ### Changed ### - Passing the `S_ISUID` or `S_ISGID` modes to `MkdirAllInRoot` will now return an explicit error saying that those bits are ignored by `mkdirat(2)`. In the past a different error was returned, but since the silent ignoring behaviour is codified in the man pages a more explicit error seems apt. While silently ignoring these bits would be the most compatible option, it could lead to users thinking their code sets these bits when it doesn't. Programs that need to deal with compatibility can mask the bits themselves. (#23, #25) ### Fixed ### - If a directory has `S_ISGID` set, then all child directories will have `S_ISGID` set when created and a different gid will be used for any inode created under the directory. Previously, the "expected owner and mode" validation in `securejoin.MkdirAll` did not correctly handle this. We now correctly handle this case. (#24, #25) ## [0.3.1] - 2024-07-23 ## ### Changed ### - By allowing `Open(at)InRoot` to opt-out of the extra work done by `MkdirAll` to do the necessary "partial lookups", `Open(at)InRoot` now does less work for both implementations (resulting in a many-fold decrease in the number of operations for `openat2`, and a modest improvement for non-`openat2`) and is far more guaranteed to match the correct `openat2(RESOLVE_IN_ROOT)` behaviour. - We now use `readlinkat(fd, "")` where possible. For `Open(at)InRoot` this effectively just means that we no longer risk getting spurious errors during rename races. However, for our hardened procfs handler, this in theory should prevent mount attacks from tricking us when doing magic-link readlinks (even when using the unsafe host `/proc` handle). Unfortunately `Reopen` is still potentially vulnerable to those kinds of somewhat-esoteric attacks. Technically this [will only work on post-2.6.39 kernels][linux-readlinkat-emptypath] but it seems incredibly unlikely anyone is using `filepath-securejoin` on a pre-2011 kernel. ### Fixed ### - Several improvements were made to the errors returned by `Open(at)InRoot` and `MkdirAll` when dealing with invalid paths under the emulated (ie. non-`openat2`) implementation. Previously, some paths would return the wrong error (`ENOENT` when the last component was a non-directory), and other paths would be returned as though they were acceptable (trailing-slash components after a non-directory would be ignored by `Open(at)InRoot`). These changes were done to match `openat2`'s behaviour and purely is a consistency fix (most users are going to be using `openat2` anyway). [linux-readlinkat-emptypath]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=65cfc6722361570bfe255698d9cd4dccaf47570d ## [0.3.0] - 2024-07-11 ## ### Added ### - A new set of `*os.File`-based APIs have been added. These are adapted from [libpathrs][] and we strongly suggest using them if possible (as they provide far more protection against attacks than `SecureJoin`): - `Open(at)InRoot` resolves a path inside a rootfs and returns an `*os.File` handle to the path. Note that the handle returned is an `O_PATH` handle, which cannot be used for reading or writing (as well as some other operations -- [see open(2) for more details][open.2]) - `Reopen` takes an `O_PATH` file handle and safely re-opens it to upgrade it to a regular handle. This can also be used with non-`O_PATH` handles, but `O_PATH` is the most obvious application. - `MkdirAll` is an implementation of `os.MkdirAll` that is safe to use to create a directory tree within a rootfs. As these are new APIs, they may change in the future. However, they should be safe to start migrating to as we have extensive tests ensuring they behave correctly and are safe against various races and other attacks. [libpathrs]: https://github.com/cyphar/libpathrs [open.2]: https://www.man7.org/linux/man-pages/man2/open.2.html ## [0.2.5] - 2024-05-03 ## ### Changed ### - Some minor changes were made to how lexical components (like `..` and `.`) are handled during path generation in `SecureJoin`. There is no behaviour change as a result of this fix (the resulting paths are the same). ### Fixed ### - The error returned when we hit a symlink loop now references the correct path. (#10) ## [0.2.4] - 2023-09-06 ## ### Security ### - This release fixes a potential security issue in filepath-securejoin when used on Windows ([GHSA-6xv5-86q9-7xr8][], which could be used to generate paths outside of the provided rootfs in certain cases), as well as improving the overall behaviour of filepath-securejoin when dealing with Windows paths that contain volume names. Thanks to Paulo Gomes for discovering and fixing these issues. ### Fixed ### - Switch to GitHub Actions for CI so we can test on Windows as well as Linux and MacOS. [GHSA-6xv5-86q9-7xr8]: https://github.com/advisories/GHSA-6xv5-86q9-7xr8 ## [0.2.3] - 2021-06-04 ## ### Changed ### - Switch to Go 1.13-style `%w` error wrapping, letting us drop the dependency on `github.com/pkg/errors`. ## [0.2.2] - 2018-09-05 ## ### Changed ### - Use `syscall.ELOOP` as the base error for symlink loops, rather than our own (internal) error. This allows callers to more easily use `errors.Is` to check for this case. ## [0.2.1] - 2018-09-05 ## ### Fixed ### - Use our own `IsNotExist` implementation, which lets us handle `ENOTDIR` properly within `SecureJoin`. ## [0.2.0] - 2017-07-19 ## We now have 100% test coverage! ### Added ### - Add a `SecureJoinVFS` API that can be used for mocking (as we do in our new tests) or for implementing custom handling of lookup operations (such as for rootless containers, where work is necessary to access directories with weird modes because we don't have `CAP_DAC_READ_SEARCH` or `CAP_DAC_OVERRIDE`). ## 0.1.0 - 2017-07-19 This is our first release of `github.com/cyphar/filepath-securejoin`, containing a full implementation with a coverage of 93.5% (the only missing cases are the error cases, which are hard to mocktest at the moment). [Unreleased]: https://github.com/cyphar/filepath-securejoin/compare/v0.6.1...HEAD [0.6.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.6.0...v0.6.1 [0.6.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.5.0...v0.6.0 [0.5.2]: https://github.com/cyphar/filepath-securejoin/compare/v0.5.1...v0.5.2 [0.5.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.5.0...v0.5.1 [0.5.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.4.1...v0.5.0 [0.4.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.4.0...v0.4.1 [0.4.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.6...v0.4.0 [0.3.6]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.5...v0.3.6 [0.3.5]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.4...v0.3.5 [0.3.4]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.3...v0.3.4 [0.3.3]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.2...v0.3.3 [0.3.2]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.1...v0.3.2 [0.3.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.0...v0.3.1 [0.3.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.5...v0.3.0 [0.2.5]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.4...v0.2.5 [0.2.4]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.3...v0.2.4 [0.2.3]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.2...v0.2.3 [0.2.2]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.1...v0.2.2 [0.2.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.0...v0.2.1 [0.2.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.1.0...v0.2.0 ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/COPYING.md ================================================ ## COPYING ## `SPDX-License-Identifier: BSD-3-Clause AND MPL-2.0` This project is made up of code licensed under different licenses. Which code you use will have an impact on whether only one or both licenses apply to your usage of this library. Note that **each file** in this project individually has a code comment at the start describing the license of that particular file -- this is the most accurate license information of this project; in case there is any conflict between this document and the comment at the start of a file, the comment shall take precedence. The only purpose of this document is to work around [a known technical limitation of pkg.go.dev's license checking tool when dealing with non-trivial project licenses][go75067]. [go75067]: https://go.dev/issue/75067 ### `BSD-3-Clause` ### At time of writing, the following files and directories are licensed under the BSD-3-Clause license: * `doc.go` * `join*.go` * `vfs.go` * `internal/consts/*.go` * `pathrs-lite/internal/gocompat/*.go` * `pathrs-lite/internal/kernelversion/*.go` The text of the BSD-3-Clause license used by this project is the following (the text is also available from the [`LICENSE.BSD`](./LICENSE.BSD) file): ``` Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved. Copyright (C) 2017-2024 SUSE LLC. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ``` ### `MPL-2.0` ### All other files (unless otherwise marked) are licensed under the Mozilla Public License (version 2.0). The text of the Mozilla Public License (version 2.0) is the following (the text is also available from the [`LICENSE.MPL-2.0`](./LICENSE.MPL-2.0) file): ``` Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. ``` ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/LICENSE.BSD ================================================ Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved. Copyright (C) 2017-2024 SUSE LLC. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/LICENSE.MPL-2.0 ================================================ Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/README.md ================================================ ## `filepath-securejoin` ## [![Go Documentation](https://pkg.go.dev/badge/github.com/cyphar/filepath-securejoin.svg)](https://pkg.go.dev/github.com/cyphar/filepath-securejoin) [![Build Status](https://github.com/cyphar/filepath-securejoin/actions/workflows/ci.yml/badge.svg)](https://github.com/cyphar/filepath-securejoin/actions/workflows/ci.yml) ### Old API ### This library was originally just an implementation of `SecureJoin` which was [intended to be included in the Go standard library][go#20126] as a safer `filepath.Join` that would restrict the path lookup to be inside a root directory. The implementation was based on code that existed in several container runtimes. Unfortunately, this API is **fundamentally unsafe** against attackers that can modify path components after `SecureJoin` returns and before the caller uses the path, allowing for some fairly trivial TOCTOU attacks. `SecureJoin` (and `SecureJoinVFS`) are still provided by this library to support legacy users, but new users are strongly suggested to avoid using `SecureJoin` and instead use the [new api](#new-api) or switch to [libpathrs][libpathrs]. With the above limitations in mind, this library guarantees the following: * If no error is set, the resulting string **must** be a child path of `root` and will not contain any symlink path components (they will all be expanded). * When expanding symlinks, all symlink path components **must** be resolved relative to the provided root. In particular, this can be considered a userspace implementation of how `chroot(2)` operates on file paths. Note that these symlinks will **not** be expanded lexically (`filepath.Clean` is not called on the input before processing). * Non-existent path components are unaffected by `SecureJoin` (similar to `filepath.EvalSymlinks`'s semantics). * The returned path will always be `filepath.Clean`ed and thus not contain any `..` components. A (trivial) implementation of this function on GNU/Linux systems could be done with the following (note that this requires root privileges and is far more opaque than the implementation in this library, and also requires that `readlink` is inside the `root` path and is trustworthy): ```go package securejoin import ( "os/exec" "path/filepath" ) func SecureJoin(root, unsafePath string) (string, error) { unsafePath = string(filepath.Separator) + unsafePath cmd := exec.Command("chroot", root, "readlink", "--canonicalize-missing", "--no-newline", unsafePath) output, err := cmd.CombinedOutput() if err != nil { return "", err } expanded := string(output) return filepath.Join(root, expanded), nil } ``` [libpathrs]: https://github.com/openSUSE/libpathrs [go#20126]: https://github.com/golang/go/issues/20126 ### New API ### [#new-api]: #new-api While we recommend users switch to [libpathrs][libpathrs] as soon as it has a stable release, some methods implemented by libpathrs have been ported to this library to ease the transition. These APIs are only supported on Linux. These APIs are implemented such that `filepath-securejoin` will opportunistically use certain newer kernel APIs that make these operations far more secure. In particular: * All of the lookup operations will use [`openat2`][openat2.2] on new enough kernels (Linux 5.6 or later) to restrict lookups through magic-links and bind-mounts (for certain operations) and to make use of `RESOLVE_IN_ROOT` to efficiently resolve symlinks within a rootfs. * The APIs provide hardening against a malicious `/proc` mount to either detect or avoid being tricked by a `/proc` that is not legitimate. This is done using [`openat2`][openat2.2] for all users, and privileged users will also be further protected by using [`fsopen`][fsopen.2] and [`open_tree`][open_tree.2] (Linux 5.2 or later). [openat2.2]: https://www.man7.org/linux/man-pages/man2/openat2.2.html [fsopen.2]: https://github.com/brauner/man-pages-md/blob/main/fsopen.md [open_tree.2]: https://github.com/brauner/man-pages-md/blob/main/open_tree.md #### `OpenInRoot` #### ```go func OpenInRoot(root, unsafePath string) (*os.File, error) func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error) func Reopen(handle *os.File, flags int) (*os.File, error) ``` `OpenInRoot` is a much safer version of ```go path, err := securejoin.SecureJoin(root, unsafePath) file, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC) ``` that protects against various race attacks that could lead to serious security issues, depending on the application. Note that the returned `*os.File` is an `O_PATH` file descriptor, which is quite restricted. Callers will probably need to use `Reopen` to get a more usable handle (this split is done to provide useful features like PTY spawning and to avoid users accidentally opening bad inodes that could cause a DoS). Callers need to be careful in how they use the returned `*os.File`. Usually it is only safe to operate on the handle directly, and it is very easy to create a security issue. [libpathrs][libpathrs] provides far more helpers to make using these handles safer -- there is currently no plan to port them to `filepath-securejoin`. `OpenatInRoot` is like `OpenInRoot` except that the root is provided using an `*os.File`. This allows you to ensure that multiple `OpenatInRoot` (or `MkdirAllHandle`) calls are operating on the same rootfs. > **NOTE**: Unlike `SecureJoin`, `OpenInRoot` will error out as soon as it hits > a dangling symlink or non-existent path. This is in contrast to `SecureJoin` > which treated non-existent components as though they were real directories, > and would allow for partial resolution of dangling symlinks. These behaviours > are at odds with how Linux treats non-existent paths and dangling symlinks, > and so these are no longer allowed. #### `MkdirAll` #### ```go func MkdirAll(root, unsafePath string, mode int) error func MkdirAllHandle(root *os.File, unsafePath string, mode int) (*os.File, error) ``` `MkdirAll` is a much safer version of ```go path, err := securejoin.SecureJoin(root, unsafePath) err = os.MkdirAll(path, mode) ``` that protects against the same kinds of races that `OpenInRoot` protects against. `MkdirAllHandle` is like `MkdirAll` except that the root is provided using an `*os.File` (the reason for this is the same as with `OpenatInRoot`) and an `*os.File` of the final created directory is returned (this directory is guaranteed to be effectively identical to the directory created by `MkdirAllHandle`, which is not possible to ensure by just using `OpenatInRoot` after `MkdirAll`). > **NOTE**: Unlike `SecureJoin`, `MkdirAll` will error out as soon as it hits > a dangling symlink or non-existent path. This is in contrast to `SecureJoin` > which treated non-existent components as though they were real directories, > and would allow for partial resolution of dangling symlinks. These behaviours > are at odds with how Linux treats non-existent paths and dangling symlinks, > and so these are no longer allowed. This means that `MkdirAll` will not > create non-existent directories referenced by a dangling symlink. ### License ### `SPDX-License-Identifier: BSD-3-Clause AND MPL-2.0` Some of the code in this project is derived from Go, and is licensed under a BSD 3-clause license (available in `LICENSE.BSD`). Other files (many of which are derived from [libpathrs][libpathrs]) are licensed under the Mozilla Public License version 2.0 (available in `LICENSE.MPL-2.0`). If you are using the ["New API" described above][#new-api], you are probably using code from files released under this license. Every source file in this project has a copyright header describing its license. Please check the license headers of each file to see what license applies to it. See [COPYING.md](./COPYING.md) for some more details. [umoci]: https://github.com/opencontainers/umoci ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/VERSION ================================================ 0.6.1 ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/codecov.yml ================================================ # SPDX-License-Identifier: MPL-2.0 # Copyright (C) 2025 Aleksa Sarai # Copyright (C) 2025 SUSE LLC # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. comment: layout: "condensed_header, reach, diff, components, condensed_files, condensed_footer" require_changes: true branches: - main coverage: range: 60..100 status: project: default: target: 85% threshold: 0% patch: default: target: auto informational: true github_checks: annotations: false ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/doc.go ================================================ // SPDX-License-Identifier: BSD-3-Clause // Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved. // Copyright (C) 2017-2024 SUSE LLC. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package securejoin implements a set of helpers to make it easier to write Go // code that is safe against symlink-related escape attacks. The primary idea // is to let you resolve a path within a rootfs directory as if the rootfs was // a chroot. // // securejoin has two APIs, a "legacy" API and a "modern" API. // // The legacy API is [SecureJoin] and [SecureJoinVFS]. These methods are // **not** safe against race conditions where an attacker changes the // filesystem after (or during) the [SecureJoin] operation. // // The new API is available in the [pathrs-lite] subpackage, and provide // protections against racing attackers as well as several other key // protections against attacks often seen by container runtimes. As the name // suggests, [pathrs-lite] is a stripped down (pure Go) reimplementation of // [libpathrs]. The main APIs provided are [OpenInRoot], [MkdirAll], and // [procfs.Handle] -- other APIs are not planned to be ported. The long-term // goal is for users to migrate to [libpathrs] which is more fully-featured. // // securejoin has been used by several container runtimes (Docker, runc, // Kubernetes, etc) for quite a few years as a de-facto standard for operating // on container filesystem paths "safely". However, most users still use the // legacy API which is unsafe against various attacks (there is a fairly long // history of CVEs in dependent as a result). Users should switch to the modern // API as soon as possible (or even better, switch to libpathrs). // // This project was initially intended to be included in the Go standard // library, but it was rejected (see https://go.dev/issue/20126). Much later, // [os.Root] was added to the Go stdlib that shares some of the goals of // filepath-securejoin. However, its design is intended to work like // openat2(RESOLVE_BENEATH) which does not fit the usecase of container // runtimes and most system tools. // // [pathrs-lite]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite // [libpathrs]: https://github.com/openSUSE/libpathrs // [OpenInRoot]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite#OpenInRoot // [MkdirAll]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite#MkdirAll // [procfs.Handle]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite/procfs#Handle // [os.Root]: https:///pkg.go.dev/os#Root package securejoin ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/internal/consts/consts.go ================================================ // SPDX-License-Identifier: BSD-3-Clause // Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved. // Copyright (C) 2017-2025 SUSE LLC. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package consts contains the definitions of internal constants used // throughout filepath-securejoin. package consts // MaxSymlinkLimit is the maximum number of symlinks that can be encountered // during a single lookup before returning -ELOOP. At time of writing, Linux // has an internal limit of 40. const MaxSymlinkLimit = 255 ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/join.go ================================================ // SPDX-License-Identifier: BSD-3-Clause // Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved. // Copyright (C) 2017-2025 SUSE LLC. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package securejoin import ( "errors" "os" "path/filepath" "strings" "syscall" "github.com/cyphar/filepath-securejoin/internal/consts" ) // IsNotExist tells you if err is an error that implies that either the path // accessed does not exist (or path components don't exist). This is // effectively a more broad version of [os.IsNotExist]. func IsNotExist(err error) bool { // Check that it's not actually an ENOTDIR, which in some cases is a more // convoluted case of ENOENT (usually involving weird paths). return errors.Is(err, os.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) || errors.Is(err, syscall.ENOENT) } // errUnsafeRoot is returned if the user provides SecureJoinVFS with a path // that contains ".." components. var errUnsafeRoot = errors.New("root path provided to SecureJoin contains '..' components") // stripVolume just gets rid of the Windows volume included in a path. Based on // some godbolt tests, the Go compiler is smart enough to make this a no-op on // Linux. func stripVolume(path string) string { return path[len(filepath.VolumeName(path)):] } // hasDotDot checks if the path contains ".." components in a platform-agnostic // way. func hasDotDot(path string) bool { // If we are on Windows, strip any volume letters. It turns out that // C:..\foo may (or may not) be a valid pathname and we need to handle that // leading "..". path = stripVolume(path) // Look for "/../" in the path, but we need to handle leading and trailing // ".."s by adding separators. Doing this with filepath.Separator is ugly // so just convert to Unix-style "/" first. path = filepath.ToSlash(path) return strings.Contains("/"+path+"/", "/../") } // SecureJoinVFS joins the two given path components (similar to // [filepath.Join]) except that the returned path is guaranteed to be scoped // inside the provided root path (when evaluated). Any symbolic links in the // path are evaluated with the given root treated as the root of the // filesystem, similar to a chroot. The filesystem state is evaluated through // the given [VFS] interface (if nil, the standard [os].* family of functions // are used). // // Note that the guarantees provided by this function only apply if the path // components in the returned string are not modified (in other words are not // replaced with symlinks on the filesystem) after this function has returned. // Such a symlink race is necessarily out-of-scope of SecureJoinVFS. // // NOTE: Due to the above limitation, Linux users are strongly encouraged to // use [OpenInRoot] instead, which does safely protect against these kinds of // attacks. There is no way to solve this problem with SecureJoinVFS because // the API is fundamentally wrong (you cannot return a "safe" path string and // guarantee it won't be modified afterwards). // // Volume names in unsafePath are always discarded, regardless if they are // provided via direct input or when evaluating symlinks. Therefore: // // "C:\Temp" + "D:\path\to\file.txt" results in "C:\Temp\path\to\file.txt" // // If the provided root is not [filepath.Clean] then an error will be returned, // as such root paths are bordering on somewhat unsafe and using such paths is // not best practice. We also strongly suggest that any root path is first // fully resolved using [filepath.EvalSymlinks] or otherwise constructed to // avoid containing symlink components. Of course, the root also *must not* be // attacker-controlled. func SecureJoinVFS(root, unsafePath string, vfs VFS) (string, error) { //nolint:revive // name is part of public API // The root path must not contain ".." components, otherwise when we join // the subpath we will end up with a weird path. We could work around this // in other ways but users shouldn't be giving us non-lexical root paths in // the first place. if hasDotDot(root) { return "", errUnsafeRoot } // Use the os.* VFS implementation if none was specified. if vfs == nil { vfs = osVFS{} } unsafePath = filepath.FromSlash(unsafePath) var ( currentPath string remainingPath = unsafePath linksWalked int ) for remainingPath != "" { // On Windows, if we managed to end up at a path referencing a volume, // drop the volume to make sure we don't end up with broken paths or // escaping the root volume. remainingPath = stripVolume(remainingPath) // Get the next path component. var part string if i := strings.IndexRune(remainingPath, filepath.Separator); i == -1 { part, remainingPath = remainingPath, "" } else { part, remainingPath = remainingPath[:i], remainingPath[i+1:] } // Apply the component lexically to the path we are building. // currentPath does not contain any symlinks, and we are lexically // dealing with a single component, so it's okay to do a filepath.Clean // here. nextPath := filepath.Join(string(filepath.Separator), currentPath, part) if nextPath == string(filepath.Separator) { currentPath = "" continue } fullPath := root + string(filepath.Separator) + nextPath // Figure out whether the path is a symlink. fi, err := vfs.Lstat(fullPath) if err != nil && !IsNotExist(err) { return "", err } // Treat non-existent path components the same as non-symlinks (we // can't do any better here). if IsNotExist(err) || fi.Mode()&os.ModeSymlink == 0 { currentPath = nextPath continue } // It's a symlink, so get its contents and expand it by prepending it // to the yet-unparsed path. linksWalked++ if linksWalked > consts.MaxSymlinkLimit { return "", &os.PathError{Op: "SecureJoin", Path: root + string(filepath.Separator) + unsafePath, Err: syscall.ELOOP} } dest, err := vfs.Readlink(fullPath) if err != nil { return "", err } remainingPath = dest + string(filepath.Separator) + remainingPath // Absolute symlinks reset any work we've already done. if filepath.IsAbs(dest) { currentPath = "" } } // There should be no lexical components like ".." left in the path here, // but for safety clean up the path before joining it to the root. finalPath := filepath.Join(string(filepath.Separator), currentPath) return filepath.Join(root, finalPath), nil } // SecureJoin is a wrapper around [SecureJoinVFS] that just uses the [os].* library // of functions as the [VFS]. If in doubt, use this function over [SecureJoinVFS]. func SecureJoin(root, unsafePath string) (string, error) { return SecureJoinVFS(root, unsafePath, nil) } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/README.md ================================================ ## `pathrs-lite` ## `github.com/cyphar/filepath-securejoin/pathrs-lite` provides a minimal **pure Go** implementation of the core bits of [libpathrs][]. This is not intended to be a complete replacement for libpathrs, instead it is mainly intended to be useful as a transition tool for existing Go projects. `pathrs-lite` also provides a very easy way to switch to `libpathrs` (even for downstreams where `pathrs-lite` is being used in a third-party package and is not interested in using CGo). At build time, if you use the `libpathrs` build tag then `pathrs-lite` will use `libpathrs` directly instead of the pure Go implementation. The two backends are functionally equivalent (and we have integration tests to verify this), so this migration should be very easy with no user-visible impact. [libpathrs]: https://github.com/cyphar/libpathrs ### License ### Most of this subpackage is licensed under the Mozilla Public License (version 2.0). For more information, see the top-level [COPYING.md][] and [LICENSE.MPL-2.0][] files, as well as the individual license headers for each file. ``` Copyright (C) 2024-2025 Aleksa Sarai Copyright (C) 2024-2025 SUSE LLC This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. ``` [COPYING.md]: ../COPYING.md [LICENSE.MPL-2.0]: ../LICENSE.MPL-2.0 ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/doc.go ================================================ // SPDX-License-Identifier: MPL-2.0 //go:build linux // Copyright (C) 2024-2025 Aleksa Sarai // Copyright (C) 2024-2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // Package pathrs (pathrs-lite) is a less complete pure Go implementation of // some of the APIs provided by [libpathrs]. // // [libpathrs]: https://github.com/cyphar/libpathrs package pathrs ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/assert/assert.go ================================================ // SPDX-License-Identifier: MPL-2.0 // Copyright (C) 2025 Aleksa Sarai // Copyright (C) 2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // Package assert provides some basic assertion helpers for Go. package assert import ( "fmt" ) // Assert panics if the predicate is false with the provided argument. func Assert(predicate bool, msg any) { if !predicate { panic(msg) } } // Assertf panics if the predicate is false and formats the message using the // same formatting as [fmt.Printf]. // // [fmt.Printf]: https://pkg.go.dev/fmt#Printf func Assertf(predicate bool, fmtMsg string, args ...any) { Assert(predicate, fmt.Sprintf(fmtMsg, args...)) } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/errors_linux.go ================================================ // SPDX-License-Identifier: MPL-2.0 //go:build linux // Copyright (C) 2024-2025 Aleksa Sarai // Copyright (C) 2024-2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // Package internal contains unexported common code for filepath-securejoin. package internal import ( "errors" "golang.org/x/sys/unix" ) type xdevErrorish struct { description string } func (err xdevErrorish) Error() string { return err.description } func (err xdevErrorish) Is(target error) bool { return target == unix.EXDEV } var ( // ErrPossibleAttack indicates that some attack was detected. ErrPossibleAttack error = xdevErrorish{"possible attack detected"} // ErrPossibleBreakout indicates that during an operation we ended up in a // state that could be a breakout but we detected it. ErrPossibleBreakout error = xdevErrorish{"possible breakout detected"} // ErrInvalidDirectory indicates an unlinked directory. ErrInvalidDirectory = errors.New("wandered into deleted directory") // ErrDeletedInode indicates an unlinked file (non-directory). ErrDeletedInode = errors.New("cannot verify path of deleted inode") ) ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/at_linux.go ================================================ // SPDX-License-Identifier: MPL-2.0 //go:build linux // Copyright (C) 2024-2025 Aleksa Sarai // Copyright (C) 2024-2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package fd import ( "fmt" "os" "path/filepath" "runtime" "golang.org/x/sys/unix" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat" ) // prepareAtWith returns -EBADF (an invalid fd) if dir is nil, otherwise using // the dir.Fd(). We use -EBADF because in filepath-securejoin we generally // don't want to allow relative-to-cwd paths. The returned path is an // *informational* string that describes a reasonable pathname for the given // *at(2) arguments. You must not use the full path for any actual filesystem // operations. func prepareAt(dir Fd, path string) (dirFd int, unsafeUnmaskedPath string) { dirFd, dirPath := -int(unix.EBADF), "." if dir != nil { dirFd, dirPath = int(dir.Fd()), dir.Name() } if !filepath.IsAbs(path) { // only prepend the dirfd path for relative paths path = dirPath + "/" + path } // NOTE: If path is "." or "", the returned path won't be filepath.Clean, // but that's okay since this path is either used for errors (in which case // a trailing "/" or "/." is important information) or will be // filepath.Clean'd later (in the case of fd.Openat). return dirFd, path } // Openat is an [Fd]-based wrapper around unix.Openat. func Openat(dir Fd, path string, flags int, mode int) (*os.File, error) { //nolint:unparam // wrapper func dirFd, fullPath := prepareAt(dir, path) // Make sure we always set O_CLOEXEC. flags |= unix.O_CLOEXEC fd, err := unix.Openat(dirFd, path, flags, uint32(mode)) if err != nil { return nil, &os.PathError{Op: "openat", Path: fullPath, Err: err} } runtime.KeepAlive(dir) // openat is only used with lexically-safe paths so we can use // filepath.Clean here, and also the path itself is not going to be used // for actual path operations. fullPath = filepath.Clean(fullPath) return os.NewFile(uintptr(fd), fullPath), nil } // Fstatat is an [Fd]-based wrapper around unix.Fstatat. func Fstatat(dir Fd, path string, flags int) (unix.Stat_t, error) { dirFd, fullPath := prepareAt(dir, path) var stat unix.Stat_t if err := unix.Fstatat(dirFd, path, &stat, flags); err != nil { return stat, &os.PathError{Op: "fstatat", Path: fullPath, Err: err} } runtime.KeepAlive(dir) return stat, nil } // Faccessat is an [Fd]-based wrapper around unix.Faccessat. func Faccessat(dir Fd, path string, mode uint32, flags int) error { dirFd, fullPath := prepareAt(dir, path) err := unix.Faccessat(dirFd, path, mode, flags) if err != nil { err = &os.PathError{Op: "faccessat", Path: fullPath, Err: err} } runtime.KeepAlive(dir) return err } // Readlinkat is an [Fd]-based wrapper around unix.Readlinkat. func Readlinkat(dir Fd, path string) (string, error) { dirFd, fullPath := prepareAt(dir, path) size := 4096 for { linkBuf := make([]byte, size) n, err := unix.Readlinkat(dirFd, path, linkBuf) if err != nil { return "", &os.PathError{Op: "readlinkat", Path: fullPath, Err: err} } runtime.KeepAlive(dir) if n != size { return string(linkBuf[:n]), nil } // Possible truncation, resize the buffer. size *= 2 } } const ( // STATX_MNT_ID_UNIQUE is provided in golang.org/x/sys@v0.20.0, but in order to // avoid bumping the requirement for a single constant we can just define it // ourselves. _STATX_MNT_ID_UNIQUE = 0x4000 //nolint:revive // unix.* name // We don't care which mount ID we get. The kernel will give us the unique // one if it is supported. If the kernel doesn't support // STATX_MNT_ID_UNIQUE, the bit is ignored and the returned request mask // will only contain STATX_MNT_ID (if supported). wantStatxMntMask = _STATX_MNT_ID_UNIQUE | unix.STATX_MNT_ID ) var hasStatxMountID = gocompat.SyncOnceValue(func() bool { var stx unix.Statx_t err := unix.Statx(-int(unix.EBADF), "/", 0, wantStatxMntMask, &stx) return err == nil && stx.Mask&wantStatxMntMask != 0 }) // GetMountID gets the mount identifier associated with the fd and path // combination. It is effectively a wrapper around fetching // STATX_MNT_ID{,_UNIQUE} with unix.Statx, but with a fallback to 0 if the // kernel doesn't support the feature. func GetMountID(dir Fd, path string) (uint64, error) { // If we don't have statx(STATX_MNT_ID*) support, we can't do anything. if !hasStatxMountID() { return 0, nil } dirFd, fullPath := prepareAt(dir, path) var stx unix.Statx_t err := unix.Statx(dirFd, path, unix.AT_EMPTY_PATH|unix.AT_SYMLINK_NOFOLLOW, wantStatxMntMask, &stx) if stx.Mask&wantStatxMntMask == 0 { // It's not a kernel limitation, for some reason we couldn't get a // mount ID. Assume it's some kind of attack. err = fmt.Errorf("could not get mount id: %w", err) } if err != nil { return 0, &os.PathError{Op: "statx(STATX_MNT_ID_...)", Path: fullPath, Err: err} } runtime.KeepAlive(dir) return stx.Mnt_id, nil } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/fd.go ================================================ // SPDX-License-Identifier: MPL-2.0 // Copyright (C) 2025 Aleksa Sarai // Copyright (C) 2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // Package fd provides a drop-in interface-based replacement of [*os.File] that // allows for things like noop-Close wrappers to be used. // // [*os.File]: https://pkg.go.dev/os#File package fd import ( "io" "os" ) // Fd is an interface that mirrors most of the API of [*os.File], allowing you // to create wrappers that can be used in place of [*os.File]. // // [*os.File]: https://pkg.go.dev/os#File type Fd interface { io.Closer Name() string Fd() uintptr } // Compile-time interface checks. var ( _ Fd = (*os.File)(nil) _ Fd = noClose{} ) type noClose struct{ inner Fd } func (f noClose) Name() string { return f.inner.Name() } func (f noClose) Fd() uintptr { return f.inner.Fd() } func (f noClose) Close() error { return nil } // NopCloser returns an [*os.File]-like object where the [Close] method is now // a no-op. // // Note that for [*os.File] and similar objects, the Go garbage collector will // still call [Close] on the underlying file unless you use // [runtime.SetFinalizer] to disable this behaviour. This is up to the caller // to do (if necessary). // // [*os.File]: https://pkg.go.dev/os#File // [Close]: https://pkg.go.dev/io#Closer // [runtime.SetFinalizer]: https://pkg.go.dev/runtime#SetFinalizer func NopCloser(f Fd) Fd { return noClose{inner: f} } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/fd_linux.go ================================================ // SPDX-License-Identifier: MPL-2.0 //go:build linux // Copyright (C) 2024-2025 Aleksa Sarai // Copyright (C) 2024-2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package fd import ( "fmt" "os" "runtime" "golang.org/x/sys/unix" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal" ) // DupWithName creates a new file descriptor referencing the same underlying // file, but with the provided name instead of fd.Name(). func DupWithName(fd Fd, name string) (*os.File, error) { fd2, err := unix.FcntlInt(fd.Fd(), unix.F_DUPFD_CLOEXEC, 0) if err != nil { return nil, os.NewSyscallError("fcntl(F_DUPFD_CLOEXEC)", err) } runtime.KeepAlive(fd) return os.NewFile(uintptr(fd2), name), nil } // Dup creates a new file description referencing the same underlying file. func Dup(fd Fd) (*os.File, error) { return DupWithName(fd, fd.Name()) } // Fstat is an [Fd]-based wrapper around unix.Fstat. func Fstat(fd Fd) (unix.Stat_t, error) { var stat unix.Stat_t if err := unix.Fstat(int(fd.Fd()), &stat); err != nil { return stat, &os.PathError{Op: "fstat", Path: fd.Name(), Err: err} } runtime.KeepAlive(fd) return stat, nil } // Fstatfs is an [Fd]-based wrapper around unix.Fstatfs. func Fstatfs(fd Fd) (unix.Statfs_t, error) { var statfs unix.Statfs_t if err := unix.Fstatfs(int(fd.Fd()), &statfs); err != nil { return statfs, &os.PathError{Op: "fstatfs", Path: fd.Name(), Err: err} } runtime.KeepAlive(fd) return statfs, nil } // IsDeadInode detects whether the file has been unlinked from a filesystem and // is thus a "dead inode" from the kernel's perspective. func IsDeadInode(file Fd) error { // If the nlink of a file drops to 0, there is an attacker deleting // directories during our walk, which could result in weird /proc values. // It's better to error out in this case. stat, err := Fstat(file) if err != nil { return fmt.Errorf("check for dead inode: %w", err) } if stat.Nlink == 0 { err := internal.ErrDeletedInode if stat.Mode&unix.S_IFMT == unix.S_IFDIR { err = internal.ErrInvalidDirectory } return fmt.Errorf("%w %q", err, file.Name()) } return nil } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/mount_linux.go ================================================ // SPDX-License-Identifier: MPL-2.0 //go:build linux // Copyright (C) 2024-2025 Aleksa Sarai // Copyright (C) 2024-2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package fd import ( "os" "runtime" "golang.org/x/sys/unix" ) // Fsopen is an [Fd]-based wrapper around unix.Fsopen. func Fsopen(fsName string, flags int) (*os.File, error) { // Make sure we always set O_CLOEXEC. flags |= unix.FSOPEN_CLOEXEC fd, err := unix.Fsopen(fsName, flags) if err != nil { return nil, os.NewSyscallError("fsopen "+fsName, err) } return os.NewFile(uintptr(fd), "fscontext:"+fsName), nil } // Fsmount is an [Fd]-based wrapper around unix.Fsmount. func Fsmount(ctx Fd, flags, mountAttrs int) (*os.File, error) { // Make sure we always set O_CLOEXEC. flags |= unix.FSMOUNT_CLOEXEC fd, err := unix.Fsmount(int(ctx.Fd()), flags, mountAttrs) if err != nil { return nil, os.NewSyscallError("fsmount "+ctx.Name(), err) } return os.NewFile(uintptr(fd), "fsmount:"+ctx.Name()), nil } // OpenTree is an [Fd]-based wrapper around unix.OpenTree. func OpenTree(dir Fd, path string, flags uint) (*os.File, error) { dirFd, fullPath := prepareAt(dir, path) // Make sure we always set O_CLOEXEC. flags |= unix.OPEN_TREE_CLOEXEC fd, err := unix.OpenTree(dirFd, path, flags) if err != nil { return nil, &os.PathError{Op: "open_tree", Path: fullPath, Err: err} } runtime.KeepAlive(dir) return os.NewFile(uintptr(fd), fullPath), nil } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/openat2_linux.go ================================================ // SPDX-License-Identifier: MPL-2.0 //go:build linux // Copyright (C) 2024-2025 Aleksa Sarai // Copyright (C) 2024-2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package fd import ( "errors" "os" "runtime" "golang.org/x/sys/unix" ) func scopedLookupShouldRetry(how *unix.OpenHow, err error) bool { // RESOLVE_IN_ROOT (and RESOLVE_BENEATH) can return -EAGAIN if we resolve // ".." while a mount or rename occurs anywhere on the system. This could // happen spuriously, or as the result of an attacker trying to mess with // us during lookup. // // In addition, scoped lookups have a "safety check" at the end of // complete_walk which will return -EXDEV if the final path is not in the // root. return how.Resolve&(unix.RESOLVE_IN_ROOT|unix.RESOLVE_BENEATH) != 0 && (errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EXDEV)) } // This is a fairly arbitrary limit we have just to avoid an attacker being // able to make us spin in an infinite retry loop -- callers can choose to // retry on EAGAIN if they prefer. const scopedLookupMaxRetries = 128 // Openat2 is an [Fd]-based wrapper around unix.Openat2, but with some retry // logic in case of EAGAIN errors. // // NOTE: This is a variable so that the lookup tests can force openat2 to fail. var Openat2 = func(dir Fd, path string, how *unix.OpenHow) (*os.File, error) { dirFd, fullPath := prepareAt(dir, path) // Make sure we always set O_CLOEXEC. how.Flags |= unix.O_CLOEXEC var tries int for { fd, err := unix.Openat2(dirFd, path, how) if err != nil { if scopedLookupShouldRetry(how, err) && tries < scopedLookupMaxRetries { // We retry a couple of times to avoid the spurious errors, and // if we are being attacked then returning -EAGAIN is the best // we can do. tries++ continue } return nil, &os.PathError{Op: "openat2", Path: fullPath, Err: err} } runtime.KeepAlive(dir) return os.NewFile(uintptr(fd), fullPath), nil } } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/README.md ================================================ ## gocompat ## This directory contains backports of stdlib functions from later Go versions so the filepath-securejoin can continue to be used by projects that are stuck with Go 1.18 support. Note that often filepath-securejoin is added in security patches for old releases, so avoiding the need to bump Go compiler requirements is a huge plus to downstreams. The source code is licensed under the same license as the Go stdlib. See the source files for the precise license information. ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/doc.go ================================================ // SPDX-License-Identifier: BSD-3-Clause //go:build linux && go1.20 // Copyright (C) 2025 SUSE LLC. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package gocompat includes compatibility shims (backported from future Go // stdlib versions) to permit filepath-securejoin to be used with older Go // versions (often filepath-securejoin is added in security patches for old // releases, so avoiding the need to bump Go compiler requirements is a huge // plus to downstreams). package gocompat ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/gocompat_atomic_go119.go ================================================ // SPDX-License-Identifier: BSD-3-Clause //go:build linux && go1.19 // Copyright 2022 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package gocompat import ( "sync/atomic" ) // A Bool is an atomic boolean value. // The zero value is false. // // Bool must not be copied after first use. type Bool = atomic.Bool ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/gocompat_atomic_unsupported.go ================================================ // SPDX-License-Identifier: BSD-3-Clause //go:build linux && !go1.19 // Copyright (C) 2024-2025 SUSE LLC. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package gocompat import ( "sync/atomic" ) // noCopy may be added to structs which must not be copied // after the first use. // // See https://golang.org/issues/8005#issuecomment-190753527 // for details. // // Note that it must not be embedded, due to the Lock and Unlock methods. type noCopy struct{} // Lock is a no-op used by -copylocks checker from `go vet`. func (*noCopy) Lock() {} // b32 returns a uint32 0 or 1 representing b. func b32(b bool) uint32 { if b { return 1 } return 0 } // A Bool is an atomic boolean value. // The zero value is false. // // Bool must not be copied after first use. type Bool struct { _ noCopy v uint32 } // Load atomically loads and returns the value stored in x. func (x *Bool) Load() bool { return atomic.LoadUint32(&x.v) != 0 } // Store atomically stores val into x. func (x *Bool) Store(val bool) { atomic.StoreUint32(&x.v, b32(val)) } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/gocompat_errors_go120.go ================================================ // SPDX-License-Identifier: BSD-3-Clause //go:build linux && go1.20 // Copyright (C) 2024 SUSE LLC. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package gocompat import ( "fmt" ) // WrapBaseError is a helper that is equivalent to fmt.Errorf("%w: %w"), except // that on pre-1.20 Go versions only errors.Is() works properly (errors.Unwrap) // is only guaranteed to give you baseErr. func WrapBaseError(baseErr, extraErr error) error { return fmt.Errorf("%w: %w", extraErr, baseErr) } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/gocompat_errors_unsupported.go ================================================ // SPDX-License-Identifier: BSD-3-Clause //go:build linux && !go1.20 // Copyright (C) 2024 SUSE LLC. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package gocompat import ( "fmt" ) type wrappedError struct { inner error isError error } func (err wrappedError) Is(target error) bool { return err.isError == target } func (err wrappedError) Unwrap() error { return err.inner } func (err wrappedError) Error() string { return fmt.Sprintf("%v: %v", err.isError, err.inner) } // WrapBaseError is a helper that is equivalent to fmt.Errorf("%w: %w"), except // that on pre-1.20 Go versions only errors.Is() works properly (errors.Unwrap) // is only guaranteed to give you baseErr. func WrapBaseError(baseErr, extraErr error) error { return wrappedError{ inner: baseErr, isError: extraErr, } } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/gocompat_generics_go121.go ================================================ // SPDX-License-Identifier: BSD-3-Clause //go:build linux && go1.21 // Copyright (C) 2024-2025 SUSE LLC. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package gocompat import ( "cmp" "slices" "sync" ) // SlicesDeleteFunc is equivalent to Go 1.21's slices.DeleteFunc. func SlicesDeleteFunc[S ~[]E, E any](slice S, delFn func(E) bool) S { return slices.DeleteFunc(slice, delFn) } // SlicesContains is equivalent to Go 1.21's slices.Contains. func SlicesContains[S ~[]E, E comparable](slice S, val E) bool { return slices.Contains(slice, val) } // SlicesClone is equivalent to Go 1.21's slices.Clone. func SlicesClone[S ~[]E, E any](slice S) S { return slices.Clone(slice) } // SyncOnceValue is equivalent to Go 1.21's sync.OnceValue. func SyncOnceValue[T any](f func() T) func() T { return sync.OnceValue(f) } // SyncOnceValues is equivalent to Go 1.21's sync.OnceValues. func SyncOnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) { return sync.OnceValues(f) } // CmpOrdered is equivalent to Go 1.21's cmp.Ordered generic type definition. type CmpOrdered = cmp.Ordered // CmpCompare is equivalent to Go 1.21's cmp.Compare. func CmpCompare[T CmpOrdered](x, y T) int { return cmp.Compare(x, y) } // Max2 is equivalent to Go 1.21's max builtin (but only for two parameters). func Max2[T CmpOrdered](x, y T) T { return max(x, y) } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/gocompat_generics_unsupported.go ================================================ // SPDX-License-Identifier: BSD-3-Clause //go:build linux && !go1.21 // Copyright (C) 2021, 2022 The Go Authors. All rights reserved. // Copyright (C) 2024-2025 SUSE LLC. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE.BSD file. package gocompat import ( "sync" ) // These are very minimal implementations of functions that appear in Go 1.21's // stdlib, included so that we can build on older Go versions. Most are // borrowed directly from the stdlib, and a few are modified to be "obviously // correct" without needing to copy too many other helpers. // clearSlice is equivalent to Go 1.21's builtin clear. // Copied from the Go 1.24 stdlib implementation. func clearSlice[S ~[]E, E any](slice S) { var zero E for i := range slice { slice[i] = zero } } // slicesIndexFunc is equivalent to Go 1.21's slices.IndexFunc. // Copied from the Go 1.24 stdlib implementation. func slicesIndexFunc[S ~[]E, E any](s S, f func(E) bool) int { for i := range s { if f(s[i]) { return i } } return -1 } // SlicesDeleteFunc is equivalent to Go 1.21's slices.DeleteFunc. // Copied from the Go 1.24 stdlib implementation. func SlicesDeleteFunc[S ~[]E, E any](s S, del func(E) bool) S { i := slicesIndexFunc(s, del) if i == -1 { return s } // Don't start copying elements until we find one to delete. for j := i + 1; j < len(s); j++ { if v := s[j]; !del(v) { s[i] = v i++ } } clearSlice(s[i:]) // zero/nil out the obsolete elements, for GC return s[:i] } // SlicesContains is equivalent to Go 1.21's slices.Contains. // Similar to the stdlib slices.Contains, except that we don't have // slices.Index so we need to use slices.IndexFunc for this non-Func helper. func SlicesContains[S ~[]E, E comparable](s S, v E) bool { return slicesIndexFunc(s, func(e E) bool { return e == v }) >= 0 } // SlicesClone is equivalent to Go 1.21's slices.Clone. // Copied from the Go 1.24 stdlib implementation. func SlicesClone[S ~[]E, E any](s S) S { // Preserve nil in case it matters. if s == nil { return nil } return append(S([]E{}), s...) } // SyncOnceValue is equivalent to Go 1.21's sync.OnceValue. // Copied from the Go 1.25 stdlib implementation. func SyncOnceValue[T any](f func() T) func() T { // Use a struct so that there's a single heap allocation. d := struct { f func() T once sync.Once valid bool p any result T }{ f: f, } return func() T { d.once.Do(func() { defer func() { d.f = nil d.p = recover() if !d.valid { panic(d.p) } }() d.result = d.f() d.valid = true }) if !d.valid { panic(d.p) } return d.result } } // SyncOnceValues is equivalent to Go 1.21's sync.OnceValues. // Copied from the Go 1.25 stdlib implementation. func SyncOnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) { // Use a struct so that there's a single heap allocation. d := struct { f func() (T1, T2) once sync.Once valid bool p any r1 T1 r2 T2 }{ f: f, } return func() (T1, T2) { d.once.Do(func() { defer func() { d.f = nil d.p = recover() if !d.valid { panic(d.p) } }() d.r1, d.r2 = d.f() d.valid = true }) if !d.valid { panic(d.p) } return d.r1, d.r2 } } // CmpOrdered is equivalent to Go 1.21's cmp.Ordered generic type definition. // Copied from the Go 1.25 stdlib implementation. type CmpOrdered interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string } // isNaN reports whether x is a NaN without requiring the math package. // This will always return false if T is not floating-point. // Copied from the Go 1.25 stdlib implementation. func isNaN[T CmpOrdered](x T) bool { return x != x } // CmpCompare is equivalent to Go 1.21's cmp.Compare. // Copied from the Go 1.25 stdlib implementation. func CmpCompare[T CmpOrdered](x, y T) int { xNaN := isNaN(x) yNaN := isNaN(y) if xNaN { if yNaN { return 0 } return -1 } if yNaN { return +1 } if x < y { return -1 } if x > y { return +1 } return 0 } // Max2 is equivalent to Go 1.21's max builtin for two parameters. func Max2[T CmpOrdered](x, y T) T { m := x if y > m { m = y } return m } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gopathrs/doc.go ================================================ // SPDX-License-Identifier: MPL-2.0 //go:build linux // Copyright (C) 2024-2025 Aleksa Sarai // Copyright (C) 2024-2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // Package gopathrs is a less complete pure Go implementation of some of the // APIs provided by [libpathrs]. // // [libpathrs]: https://github.com/cyphar/libpathrs package gopathrs ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gopathrs/lookup_linux.go ================================================ // SPDX-License-Identifier: MPL-2.0 //go:build linux // Copyright (C) 2024-2025 Aleksa Sarai // Copyright (C) 2024-2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package gopathrs import ( "errors" "fmt" "os" "path" "path/filepath" "strings" "golang.org/x/sys/unix" "github.com/cyphar/filepath-securejoin/internal/consts" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs" ) type symlinkStackEntry struct { // (dir, remainingPath) is what we would've returned if the link didn't // exist. This matches what openat2(RESOLVE_IN_ROOT) would return in // this case. dir *os.File remainingPath string // linkUnwalked is the remaining path components from the original // Readlink which we have yet to walk. When this slice is empty, we // drop the link from the stack. linkUnwalked []string } func (se symlinkStackEntry) String() string { return fmt.Sprintf("<%s>/%s [->%s]", se.dir.Name(), se.remainingPath, strings.Join(se.linkUnwalked, "/")) } func (se symlinkStackEntry) Close() { _ = se.dir.Close() } type symlinkStack []*symlinkStackEntry func (s *symlinkStack) IsEmpty() bool { return s == nil || len(*s) == 0 } func (s *symlinkStack) Close() { if s != nil { for _, link := range *s { link.Close() } // TODO: Switch to clear once we switch to Go 1.21. *s = nil } } var ( errEmptyStack = errors.New("[internal] stack is empty") errBrokenSymlinkStack = errors.New("[internal error] broken symlink stack") ) func (s *symlinkStack) popPart(part string) error { if s == nil || s.IsEmpty() { // If there is nothing in the symlink stack, then the part was from the // real path provided by the user, and this is a no-op. return errEmptyStack } if part == "." { // "." components are no-ops -- we drop them when doing SwapLink. return nil } tailEntry := (*s)[len(*s)-1] // Double-check that we are popping the component we expect. if len(tailEntry.linkUnwalked) == 0 { return fmt.Errorf("%w: trying to pop component %q of empty stack entry %s", errBrokenSymlinkStack, part, tailEntry) } headPart := tailEntry.linkUnwalked[0] if headPart != part { return fmt.Errorf("%w: trying to pop component %q but the last stack entry is %s (%q)", errBrokenSymlinkStack, part, tailEntry, headPart) } // Drop the component, but keep the entry around in case we are dealing // with a "tail-chained" symlink. tailEntry.linkUnwalked = tailEntry.linkUnwalked[1:] return nil } func (s *symlinkStack) PopPart(part string) error { if err := s.popPart(part); err != nil { if errors.Is(err, errEmptyStack) { // Skip empty stacks. err = nil } return err } // Clean up any of the trailing stack entries that are empty. for lastGood := len(*s) - 1; lastGood >= 0; lastGood-- { entry := (*s)[lastGood] if len(entry.linkUnwalked) > 0 { break } entry.Close() (*s) = (*s)[:lastGood] } return nil } func (s *symlinkStack) push(dir *os.File, remainingPath, linkTarget string) error { if s == nil { return nil } // Split the link target and clean up any "" parts. linkTargetParts := gocompat.SlicesDeleteFunc( strings.Split(linkTarget, "/"), func(part string) bool { return part == "" || part == "." }) // Copy the directory so the caller doesn't close our copy. dirCopy, err := fd.Dup(dir) if err != nil { return err } // Add to the stack. *s = append(*s, &symlinkStackEntry{ dir: dirCopy, remainingPath: remainingPath, linkUnwalked: linkTargetParts, }) return nil } func (s *symlinkStack) SwapLink(linkPart string, dir *os.File, remainingPath, linkTarget string) error { // If we are currently inside a symlink resolution, remove the symlink // component from the last symlink entry, but don't remove the entry even // if it's empty. If we are a "tail-chained" symlink (a trailing symlink we // hit during a symlink resolution) we need to keep the old symlink until // we finish the resolution. if err := s.popPart(linkPart); err != nil { if !errors.Is(err, errEmptyStack) { return err } // Push the component regardless of whether the stack was empty. } return s.push(dir, remainingPath, linkTarget) } func (s *symlinkStack) PopTopSymlink() (*os.File, string, bool) { if s == nil || s.IsEmpty() { return nil, "", false } tailEntry := (*s)[0] *s = (*s)[1:] return tailEntry.dir, tailEntry.remainingPath, true } // PartialLookupInRoot tries to lookup as much of the request path as possible // within the provided root (a-la RESOLVE_IN_ROOT) and opens the final existing // component of the requested path, returning a file handle to the final // existing component and a string containing the remaining path components. func PartialLookupInRoot(root fd.Fd, unsafePath string) (*os.File, string, error) { return lookupInRoot(root, unsafePath, true) } func completeLookupInRoot(root fd.Fd, unsafePath string) (*os.File, error) { handle, remainingPath, err := lookupInRoot(root, unsafePath, false) if remainingPath != "" && err == nil { // should never happen err = fmt.Errorf("[bug] non-empty remaining path when doing a non-partial lookup: %q", remainingPath) } // lookupInRoot(partial=false) will always close the handle if an error is // returned, so no need to double-check here. return handle, err } func lookupInRoot(root fd.Fd, unsafePath string, partial bool) (Handle *os.File, _ string, _ error) { unsafePath = filepath.ToSlash(unsafePath) // noop // This is very similar to SecureJoin, except that we operate on the // components using file descriptors. We then return the last component we // managed open, along with the remaining path components not opened. // Try to use openat2 if possible. // // NOTE: If openat2(2) works normally but fails for this lookup, it is // probably not a good idea to fall-back to the O_PATH resolver. An // attacker could find a bug in the O_PATH resolver and uncontionally // falling back to the O_PATH resolver would form a downgrade attack. if handle, remainingPath, err := lookupOpenat2(root, unsafePath, partial); err == nil || linux.HasOpenat2() { return handle, remainingPath, err } // Get the "actual" root path from /proc/self/fd. This is necessary if the // root is some magic-link like /proc/$pid/root, in which case we want to // make sure when we do procfs.CheckProcSelfFdPath that we are using the // correct root path. logicalRootPath, err := procfs.ProcSelfFdReadlink(root) if err != nil { return nil, "", fmt.Errorf("get real root path: %w", err) } currentDir, err := fd.Dup(root) if err != nil { return nil, "", fmt.Errorf("clone root fd: %w", err) } defer func() { // If a handle is not returned, close the internal handle. if Handle == nil { _ = currentDir.Close() } }() // symlinkStack is used to emulate how openat2(RESOLVE_IN_ROOT) treats // dangling symlinks. If we hit a non-existent path while resolving a // symlink, we need to return the (dir, remainingPath) that we had when we // hit the symlink (treating the symlink as though it were a regular file). // The set of (dir, remainingPath) sets is stored within the symlinkStack // and we add and remove parts when we hit symlink and non-symlink // components respectively. We need a stack because of recursive symlinks // (symlinks that contain symlink components in their target). // // Note that the stack is ONLY used for book-keeping. All of the actual // path walking logic is still based on currentPath/remainingPath and // currentDir (as in SecureJoin). var symStack *symlinkStack if partial { symStack = new(symlinkStack) defer symStack.Close() } var ( linksWalked int currentPath string remainingPath = unsafePath ) for remainingPath != "" { // Save the current remaining path so if the part is not real we can // return the path including the component. oldRemainingPath := remainingPath // Get the next path component. var part string if i := strings.IndexByte(remainingPath, '/'); i == -1 { part, remainingPath = remainingPath, "" } else { part, remainingPath = remainingPath[:i], remainingPath[i+1:] } // If we hit an empty component, we need to treat it as though it is // "." so that trailing "/" and "//" components on a non-directory // correctly return the right error code. if part == "" { part = "." } // Apply the component lexically to the path we are building. // currentPath does not contain any symlinks, and we are lexically // dealing with a single component, so it's okay to do a filepath.Clean // here. nextPath := path.Join("/", currentPath, part) // If we logically hit the root, just clone the root rather than // opening the part and doing all of the other checks. if nextPath == "/" { if err := symStack.PopPart(part); err != nil { return nil, "", fmt.Errorf("walking into root with part %q failed: %w", part, err) } // Jump to root. rootClone, err := fd.Dup(root) if err != nil { return nil, "", fmt.Errorf("clone root fd: %w", err) } _ = currentDir.Close() currentDir = rootClone currentPath = nextPath continue } // Try to open the next component. nextDir, err := fd.Openat(currentDir, part, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) switch err { case nil: st, err := nextDir.Stat() if err != nil { _ = nextDir.Close() return nil, "", fmt.Errorf("stat component %q: %w", part, err) } switch st.Mode() & os.ModeType { //nolint:exhaustive // just a glorified if statement case os.ModeSymlink: // readlinkat implies AT_EMPTY_PATH since Linux 2.6.39. See // Linux commit 65cfc6722361 ("readlinkat(), fchownat() and // fstatat() with empty relative pathnames"). linkDest, err := fd.Readlinkat(nextDir, "") // We don't need the handle anymore. _ = nextDir.Close() if err != nil { return nil, "", err } linksWalked++ if linksWalked > consts.MaxSymlinkLimit { return nil, "", &os.PathError{Op: "securejoin.lookupInRoot", Path: logicalRootPath + "/" + unsafePath, Err: unix.ELOOP} } // Swap out the symlink's component for the link entry itself. if err := symStack.SwapLink(part, currentDir, oldRemainingPath, linkDest); err != nil { return nil, "", fmt.Errorf("walking into symlink %q failed: push symlink: %w", part, err) } // Update our logical remaining path. remainingPath = linkDest + "/" + remainingPath // Absolute symlinks reset any work we've already done. if path.IsAbs(linkDest) { // Jump to root. rootClone, err := fd.Dup(root) if err != nil { return nil, "", fmt.Errorf("clone root fd: %w", err) } _ = currentDir.Close() currentDir = rootClone currentPath = "/" } default: // If we are dealing with a directory, simply walk into it. _ = currentDir.Close() currentDir = nextDir currentPath = nextPath // The part was real, so drop it from the symlink stack. if err := symStack.PopPart(part); err != nil { return nil, "", fmt.Errorf("walking into directory %q failed: %w", part, err) } // If we are operating on a .., make sure we haven't escaped. // We only have to check for ".." here because walking down // into a regular component component cannot cause you to // escape. This mirrors the logic in RESOLVE_IN_ROOT, except we // have to check every ".." rather than only checking after a // rename or mount on the system. if part == ".." { // Make sure the root hasn't moved. if err := procfs.CheckProcSelfFdPath(logicalRootPath, root); err != nil { return nil, "", fmt.Errorf("root path moved during lookup: %w", err) } // Make sure the path is what we expect. fullPath := logicalRootPath + nextPath if err := procfs.CheckProcSelfFdPath(fullPath, currentDir); err != nil { return nil, "", fmt.Errorf("walking into %q had unexpected result: %w", part, err) } } } default: if !partial { return nil, "", err } // If there are any remaining components in the symlink stack, we // are still within a symlink resolution and thus we hit a dangling // symlink. So pretend that the first symlink in the stack we hit // was an ENOENT (to match openat2). if oldDir, remainingPath, ok := symStack.PopTopSymlink(); ok { _ = currentDir.Close() return oldDir, remainingPath, err } // We have hit a final component that doesn't exist, so we have our // partial open result. Note that we have to use the OLD remaining // path, since the lookup failed. return currentDir, oldRemainingPath, err } } // If the unsafePath had a trailing slash, we need to make sure we try to // do a relative "." open so that we will correctly return an error when // the final component is a non-directory (to match openat2). In the // context of openat2, a trailing slash and a trailing "/." are completely // equivalent. if strings.HasSuffix(unsafePath, "/") { nextDir, err := fd.Openat(currentDir, ".", unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) if err != nil { if !partial { _ = currentDir.Close() currentDir = nil } return currentDir, "", err } _ = currentDir.Close() currentDir = nextDir } // All of the components existed! return currentDir, "", nil } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gopathrs/mkdir_linux.go ================================================ // SPDX-License-Identifier: MPL-2.0 //go:build linux // Copyright (C) 2024-2025 Aleksa Sarai // Copyright (C) 2024-2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package gopathrs import ( "errors" "fmt" "os" "path/filepath" "strings" "golang.org/x/sys/unix" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs" ) // ErrInvalidMode is returned from [MkdirAll] when the requested mode is // invalid. var ErrInvalidMode = errors.New("invalid permission mode") // modePermExt is like os.ModePerm except that it also includes the set[ug]id // and sticky bits. const modePermExt = os.ModePerm | os.ModeSetuid | os.ModeSetgid | os.ModeSticky //nolint:cyclop // this function needs to handle a lot of cases func toUnixMode(mode os.FileMode) (uint32, error) { sysMode := uint32(mode.Perm()) if mode&os.ModeSetuid != 0 { sysMode |= unix.S_ISUID } if mode&os.ModeSetgid != 0 { sysMode |= unix.S_ISGID } if mode&os.ModeSticky != 0 { sysMode |= unix.S_ISVTX } // We don't allow file type bits. if mode&os.ModeType != 0 { return 0, fmt.Errorf("%w %+.3o (%s): type bits not permitted", ErrInvalidMode, mode, mode) } // We don't allow other unknown modes. if mode&^modePermExt != 0 || sysMode&unix.S_IFMT != 0 { return 0, fmt.Errorf("%w %+.3o (%s): unknown mode bits", ErrInvalidMode, mode, mode) } return sysMode, nil } // MkdirAllHandle is equivalent to [MkdirAll], except that it is safer to use // in two respects: // // - The caller provides the root directory as an *[os.File] (preferably O_PATH) // handle. This means that the caller can be sure which root directory is // being used. Note that this can be emulated by using /proc/self/fd/... as // the root path with [os.MkdirAll]. // // - Once all of the directories have been created, an *[os.File] O_PATH handle // to the directory at unsafePath is returned to the caller. This is done in // an effectively-race-free way (an attacker would only be able to swap the // final directory component), which is not possible to emulate with // [MkdirAll]. // // In addition, the returned handle is obtained far more efficiently than doing // a brand new lookup of unsafePath (such as with [SecureJoin] or openat2) after // doing [MkdirAll]. If you intend to open the directory after creating it, you // should use MkdirAllHandle. // // [SecureJoin]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin#SecureJoin func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.File, Err error) { unixMode, err := toUnixMode(mode) if err != nil { return nil, err } // On Linux, mkdirat(2) (and os.Mkdir) silently ignore the suid and sgid // bits. We could also silently ignore them but since we have very few // users it seems more prudent to return an error so users notice that // these bits will not be set. if unixMode&^0o1777 != 0 { return nil, fmt.Errorf("%w for mkdir %+.3o: suid and sgid are ignored by mkdir", ErrInvalidMode, mode) } // Try to open as much of the path as possible. currentDir, remainingPath, err := PartialLookupInRoot(root, unsafePath) defer func() { if Err != nil { _ = currentDir.Close() } }() if err != nil && !errors.Is(err, unix.ENOENT) { return nil, fmt.Errorf("find existing subpath of %q: %w", unsafePath, err) } // If there is an attacker deleting directories as we walk into them, // detect this proactively. Note this is guaranteed to detect if the // attacker deleted any part of the tree up to currentDir. // // Once we walk into a dead directory, partialLookupInRoot would not be // able to walk further down the tree (directories must be empty before // they are deleted), and if the attacker has removed the entire tree we // can be sure that anything that was originally inside a dead directory // must also be deleted and thus is a dead directory in its own right. // // This is mostly a quality-of-life check, because mkdir will simply fail // later if the attacker deletes the tree after this check. if err := fd.IsDeadInode(currentDir); err != nil { return nil, fmt.Errorf("finding existing subpath of %q: %w", unsafePath, err) } // Re-open the path to match the O_DIRECTORY reopen loop later (so that we // always return a non-O_PATH handle). We also check that we actually got a // directory. if reopenDir, err := procfs.ReopenFd(currentDir, unix.O_DIRECTORY|unix.O_CLOEXEC); errors.Is(err, unix.ENOTDIR) { return nil, fmt.Errorf("cannot create subdirectories in %q: %w", currentDir.Name(), unix.ENOTDIR) } else if err != nil { return nil, fmt.Errorf("re-opening handle to %q: %w", currentDir.Name(), err) } else { //nolint:revive // indent-error-flow lint doesn't make sense here _ = currentDir.Close() currentDir = reopenDir } remainingParts := strings.Split(remainingPath, string(filepath.Separator)) if gocompat.SlicesContains(remainingParts, "..") { // The path contained ".." components after the end of the "real" // components. We could try to safely resolve ".." here but that would // add a bunch of extra logic for something that it's not clear even // needs to be supported. So just return an error. // // If we do filepath.Clean(remainingPath) then we end up with the // problem that ".." can erase a trailing dangling symlink and produce // a path that doesn't quite match what the user asked for. return nil, fmt.Errorf("%w: yet-to-be-created path %q contains '..' components", unix.ENOENT, remainingPath) } // Create the remaining components. for _, part := range remainingParts { switch part { case "", ".": // Skip over no-op paths. continue } // NOTE: mkdir(2) will not follow trailing symlinks, so we can safely // create the final component without worrying about symlink-exchange // attacks. // // If we get -EEXIST, it's possible that another program created the // directory at the same time as us. In that case, just continue on as // if we created it (if the created inode is not a directory, the // following open call will fail). if err := unix.Mkdirat(int(currentDir.Fd()), part, unixMode); err != nil && !errors.Is(err, unix.EEXIST) { err = &os.PathError{Op: "mkdirat", Path: currentDir.Name() + "/" + part, Err: err} // Make the error a bit nicer if the directory is dead. if deadErr := fd.IsDeadInode(currentDir); deadErr != nil { // TODO: Once we bump the minimum Go version to 1.20, we can use // multiple %w verbs for this wrapping. For now we need to use a // compatibility shim for older Go versions. // err = fmt.Errorf("%w (%w)", err, deadErr) err = gocompat.WrapBaseError(err, deadErr) } return nil, err } // Get a handle to the next component. O_DIRECTORY means we don't need // to use O_PATH. var nextDir *os.File if linux.HasOpenat2() { nextDir, err = openat2(currentDir, part, &unix.OpenHow{ Flags: unix.O_NOFOLLOW | unix.O_DIRECTORY | unix.O_CLOEXEC, Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_NO_XDEV, }) } else { nextDir, err = fd.Openat(currentDir, part, unix.O_NOFOLLOW|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) } if err != nil { return nil, err } _ = currentDir.Close() currentDir = nextDir // It's possible that the directory we just opened was swapped by an // attacker. Unfortunately there isn't much we can do to protect // against this, and MkdirAll's behaviour is that we will reuse // existing directories anyway so the need to protect against this is // incredibly limited (and arguably doesn't even deserve mention here). // // Ideally we might want to check that the owner and mode match what we // would've created -- unfortunately, it is non-trivial to verify that // the owner and mode of the created directory match. While plain Unix // DAC rules seem simple enough to emulate, there are a bunch of other // factors that can change the mode or owner of created directories // (default POSIX ACLs, mount options like uid=1,gid=2,umask=0 on // filesystems like vfat, etc etc). We used to try to verify this but // it just lead to a series of spurious errors. // // We could also check that the directory is non-empty, but // unfortunately some pseduofilesystems (like cgroupfs) create // non-empty directories, which would result in different spurious // errors. } return currentDir, nil } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gopathrs/open_linux.go ================================================ // SPDX-License-Identifier: MPL-2.0 //go:build linux // Copyright (C) 2024-2025 Aleksa Sarai // Copyright (C) 2024-2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package gopathrs import ( "os" ) // OpenatInRoot is equivalent to [OpenInRoot], except that the root is provided // using an *[os.File] handle, to ensure that the correct root directory is used. func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error) { handle, err := completeLookupInRoot(root, unsafePath) if err != nil { return nil, &os.PathError{Op: "securejoin.OpenInRoot", Path: unsafePath, Err: err} } return handle, nil } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gopathrs/openat2_linux.go ================================================ // SPDX-License-Identifier: MPL-2.0 //go:build linux // Copyright (C) 2024-2025 Aleksa Sarai // Copyright (C) 2024-2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package gopathrs import ( "errors" "fmt" "os" "path/filepath" "strings" "golang.org/x/sys/unix" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd" "github.com/cyphar/filepath-securejoin/pathrs-lite/procfs" ) func openat2(dir fd.Fd, path string, how *unix.OpenHow) (*os.File, error) { file, err := fd.Openat2(dir, path, how) if err != nil { return nil, err } // If we are using RESOLVE_IN_ROOT, the name we generated may be wrong. if how.Resolve&unix.RESOLVE_IN_ROOT == unix.RESOLVE_IN_ROOT { if actualPath, err := procfs.ProcSelfFdReadlink(file); err == nil { // TODO: Ideally we would not need to dup the fd, but you cannot // easily just swap an *os.File with one from the same fd // (the GC will close the old one, and you cannot clear the // finaliser easily because it is associated with an internal // field of *os.File not *os.File itself). newFile, err := fd.DupWithName(file, actualPath) if err != nil { return nil, err } _ = file.Close() file = newFile } } return file, nil } func lookupOpenat2(root fd.Fd, unsafePath string, partial bool) (*os.File, string, error) { if !partial { file, err := openat2(root, unsafePath, &unix.OpenHow{ Flags: unix.O_PATH | unix.O_CLOEXEC, Resolve: unix.RESOLVE_IN_ROOT | unix.RESOLVE_NO_MAGICLINKS, }) return file, "", err } return partialLookupOpenat2(root, unsafePath) } // partialLookupOpenat2 is an alternative implementation of // partialLookupInRoot, using openat2(RESOLVE_IN_ROOT) to more safely get a // handle to the deepest existing child of the requested path within the root. func partialLookupOpenat2(root fd.Fd, unsafePath string) (*os.File, string, error) { // TODO: Implement this as a git-bisect-like binary search. unsafePath = filepath.ToSlash(unsafePath) // noop endIdx := len(unsafePath) var lastError error for endIdx > 0 { subpath := unsafePath[:endIdx] handle, err := openat2(root, subpath, &unix.OpenHow{ Flags: unix.O_PATH | unix.O_CLOEXEC, Resolve: unix.RESOLVE_IN_ROOT | unix.RESOLVE_NO_MAGICLINKS, }) if err == nil { // Jump over the slash if we have a non-"" remainingPath. if endIdx < len(unsafePath) { endIdx++ } // We found a subpath! return handle, unsafePath[endIdx:], lastError } if errors.Is(err, unix.ENOENT) || errors.Is(err, unix.ENOTDIR) { // That path doesn't exist, let's try the next directory up. endIdx = strings.LastIndexByte(subpath, '/') lastError = err continue } return nil, "", fmt.Errorf("open subpath: %w", err) } // If we couldn't open anything, the whole subpath is missing. Return a // copy of the root fd so that the caller doesn't close this one by // accident. rootClone, err := fd.Dup(root) if err != nil { return nil, "", err } return rootClone, unsafePath, lastError } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/kernelversion/kernel_linux.go ================================================ // SPDX-License-Identifier: BSD-3-Clause // Copyright (C) 2022 The Go Authors. All rights reserved. // Copyright (C) 2025 SUSE LLC. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE.BSD file. // The parsing logic is very loosely based on the Go stdlib's // src/internal/syscall/unix/kernel_version_linux.go but with an API that looks // a bit like runc's libcontainer/system/kernelversion. // // TODO(cyphar): This API has been copied around to a lot of different projects // (Docker, containerd, runc, and now filepath-securejoin) -- maybe we should // put it in a separate project? // Package kernelversion provides a simple mechanism for checking whether the // running kernel is at least as new as some baseline kernel version. This is // often useful when checking for features that would be too complicated to // test support for (or in cases where we know that some kernel features in // backport-heavy kernels are broken and need to be avoided). package kernelversion import ( "bytes" "errors" "fmt" "strconv" "strings" "golang.org/x/sys/unix" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat" ) // KernelVersion is a numeric representation of the key numerical elements of a // kernel version (for instance, "4.1.2-default-1" would be represented as // KernelVersion{4, 1, 2}). type KernelVersion []uint64 func (kver KernelVersion) String() string { var str strings.Builder for idx, elem := range kver { if idx != 0 { _, _ = str.WriteRune('.') } _, _ = str.WriteString(strconv.FormatUint(elem, 10)) } return str.String() } var errInvalidKernelVersion = errors.New("invalid kernel version") // parseKernelVersion parses a string and creates a KernelVersion based on it. func parseKernelVersion(kverStr string) (KernelVersion, error) { kver := make(KernelVersion, 1, 3) for idx, ch := range kverStr { if '0' <= ch && ch <= '9' { v := &kver[len(kver)-1] *v = (*v * 10) + uint64(ch-'0') } else { if idx == 0 || kverStr[idx-1] < '0' || '9' < kverStr[idx-1] { // "." must be preceded by a digit while in version section return nil, fmt.Errorf("%w %q: kernel version has dot(s) followed by non-digit in version section", errInvalidKernelVersion, kverStr) } if ch != '.' { break } kver = append(kver, 0) } } if len(kver) < 2 { return nil, fmt.Errorf("%w %q: kernel versions must contain at least two components", errInvalidKernelVersion, kverStr) } return kver, nil } // getKernelVersion gets the current kernel version. var getKernelVersion = gocompat.SyncOnceValues(func() (KernelVersion, error) { var uts unix.Utsname if err := unix.Uname(&uts); err != nil { return nil, err } // Remove the \x00 from the release. release := uts.Release[:] return parseKernelVersion(string(release[:bytes.IndexByte(release, 0)])) }) // GreaterEqualThan returns true if the the host kernel version is greater than // or equal to the provided [KernelVersion]. When doing this comparison, any // non-numerical suffixes of the host kernel version are ignored. // // If the number of components provided is not equal to the number of numerical // components of the host kernel version, any missing components are treated as // 0. This means that GreaterEqualThan(KernelVersion{4}) will be treated the // same as GreaterEqualThan(KernelVersion{4, 0, 0, ..., 0, 0}), and that if the // host kernel version is "4" then GreaterEqualThan(KernelVersion{4, 1}) will // return false (because the host version will be treated as "4.0"). func GreaterEqualThan(wantKver KernelVersion) (bool, error) { hostKver, err := getKernelVersion() if err != nil { return false, err } // Pad out the kernel version lengths to match one another. cmpLen := gocompat.Max2(len(hostKver), len(wantKver)) hostKver = append(hostKver, make(KernelVersion, cmpLen-len(hostKver))...) wantKver = append(wantKver, make(KernelVersion, cmpLen-len(wantKver))...) for i := 0; i < cmpLen; i++ { switch gocompat.CmpCompare(hostKver[i], wantKver[i]) { case -1: // host < want return false, nil case +1: // host > want return true, nil case 0: continue } } // equal version values return true, nil } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux/doc.go ================================================ // SPDX-License-Identifier: MPL-2.0 // Copyright (C) 2024-2025 Aleksa Sarai // Copyright (C) 2024-2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // Package linux returns information about what features are supported on the // running kernel. package linux ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux/mount_linux.go ================================================ // SPDX-License-Identifier: MPL-2.0 //go:build linux // Copyright (C) 2024-2025 Aleksa Sarai // Copyright (C) 2024-2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package linux import ( "golang.org/x/sys/unix" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/kernelversion" ) // HasNewMountAPI returns whether the new fsopen(2) mount API is supported on // the running kernel. var HasNewMountAPI = gocompat.SyncOnceValue(func() bool { // All of the pieces of the new mount API we use (fsopen, fsconfig, // fsmount, open_tree) were added together in Linux 5.2[1,2], so we can // just check for one of the syscalls and the others should also be // available. // // Just try to use open_tree(2) to open a file without OPEN_TREE_CLONE. // This is equivalent to openat(2), but tells us if open_tree is // available (and thus all of the other basic new mount API syscalls). // open_tree(2) is most light-weight syscall to test here. // // [1]: merge commit 400913252d09 // [2]: fd, err := unix.OpenTree(-int(unix.EBADF), "/", unix.OPEN_TREE_CLOEXEC) if err != nil { return false } _ = unix.Close(fd) // RHEL 8 has a backport of fsopen(2) that appears to have some very // difficult to debug performance pathology. As such, it seems prudent to // simply reject pre-5.2 kernels. isNotBackport, _ := kernelversion.GreaterEqualThan(kernelversion.KernelVersion{5, 2}) return isNotBackport }) ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux/openat2_linux.go ================================================ // SPDX-License-Identifier: MPL-2.0 //go:build linux // Copyright (C) 2024-2025 Aleksa Sarai // Copyright (C) 2024-2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package linux import ( "golang.org/x/sys/unix" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat" ) // sawOpenat2Error stores whether we have seen an error from HasOpenat2. This // is a one-way toggle, so as soon as we see an error we "lock" into that mode. // We cannot use sync.OnceValue to store the success/fail state once because it // is possible for the program we are running in to apply a seccomp-bpf filter // and thus disable openat2 during execution. var sawOpenat2Error gocompat.Bool // HasOpenat2 returns whether openat2(2) is supported on the running kernel. var HasOpenat2 = func() bool { if sawOpenat2Error.Load() { return false } fd, err := unix.Openat2(unix.AT_FDCWD, ".", &unix.OpenHow{ Flags: unix.O_PATH | unix.O_CLOEXEC, Resolve: unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_IN_ROOT, }) if err != nil { sawOpenat2Error.Store(true) // doesn't matter if we race here return false } _ = unix.Close(fd) return true } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs/procfs_linux.go ================================================ // SPDX-License-Identifier: MPL-2.0 //go:build linux // Copyright (C) 2024-2025 Aleksa Sarai // Copyright (C) 2024-2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // Package procfs provides a safe API for operating on /proc on Linux. Note // that this is the *internal* procfs API, mainy needed due to Go's // restrictions on cyclic dependencies and its incredibly minimal visibility // system without making a separate internal/ package. package procfs import ( "errors" "fmt" "io" "os" "runtime" "strconv" "golang.org/x/sys/unix" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/assert" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux" ) // The kernel guarantees that the root inode of a procfs mount has an // f_type of PROC_SUPER_MAGIC and st_ino of PROC_ROOT_INO. const ( procSuperMagic = 0x9fa0 // PROC_SUPER_MAGIC procRootIno = 1 // PROC_ROOT_INO ) // verifyProcHandle checks that the handle is from a procfs filesystem. // Contrast this to [verifyProcRoot], which also verifies that the handle is // the root of a procfs mount. func verifyProcHandle(procHandle fd.Fd) error { if statfs, err := fd.Fstatfs(procHandle); err != nil { return err } else if statfs.Type != procSuperMagic { return fmt.Errorf("%w: incorrect procfs root filesystem type 0x%x", errUnsafeProcfs, statfs.Type) } return nil } // verifyProcRoot verifies that the handle is the root of a procfs filesystem. // Contrast this to [verifyProcHandle], which only verifies if the handle is // some file on procfs (regardless of what file it is). func verifyProcRoot(procRoot fd.Fd) error { if err := verifyProcHandle(procRoot); err != nil { return err } if stat, err := fd.Fstat(procRoot); err != nil { return err } else if stat.Ino != procRootIno { return fmt.Errorf("%w: incorrect procfs root inode number %d", errUnsafeProcfs, stat.Ino) } return nil } type procfsFeatures struct { // hasSubsetPid was added in Linux 5.8, along with hidepid=ptraceable (and // string-based hidepid= values). Before this patchset, it was not really // safe to try to modify procfs superblock flags because the superblock was // shared -- so if this feature is not available, **you should not set any // superblock flags**. // // 6814ef2d992a ("proc: add option to mount only a pids subset") // fa10fed30f25 ("proc: allow to mount many instances of proc in one pid namespace") // 24a71ce5c47f ("proc: instantiate only pids that we can ptrace on 'hidepid=4' mount option") // 1c6c4d112e81 ("proc: use human-readable values for hidepid") // 9ff7258575d5 ("Merge branch 'proc-linus' of git://git.kernel.org/pub/scm/linux/kernel/git/ebiederm/user-namespace") hasSubsetPid bool } var getProcfsFeatures = gocompat.SyncOnceValue(func() procfsFeatures { if !linux.HasNewMountAPI() { return procfsFeatures{} } procfsCtx, err := fd.Fsopen("proc", unix.FSOPEN_CLOEXEC) if err != nil { return procfsFeatures{} } defer procfsCtx.Close() //nolint:errcheck // close failures aren't critical here return procfsFeatures{ hasSubsetPid: unix.FsconfigSetString(int(procfsCtx.Fd()), "subset", "pid") == nil, } }) func newPrivateProcMount(subset bool) (_ *Handle, Err error) { procfsCtx, err := fd.Fsopen("proc", unix.FSOPEN_CLOEXEC) if err != nil { return nil, err } defer procfsCtx.Close() //nolint:errcheck // close failures aren't critical here if subset && getProcfsFeatures().hasSubsetPid { // Try to configure hidepid=ptraceable,subset=pid if possible, but // ignore errors. _ = unix.FsconfigSetString(int(procfsCtx.Fd()), "hidepid", "ptraceable") _ = unix.FsconfigSetString(int(procfsCtx.Fd()), "subset", "pid") } // Get an actual handle. if err := unix.FsconfigCreate(int(procfsCtx.Fd())); err != nil { return nil, os.NewSyscallError("fsconfig create procfs", err) } // TODO: Output any information from the fscontext log to debug logs. procRoot, err := fd.Fsmount(procfsCtx, unix.FSMOUNT_CLOEXEC, unix.MS_NODEV|unix.MS_NOEXEC|unix.MS_NOSUID) if err != nil { return nil, err } defer func() { if Err != nil { _ = procRoot.Close() } }() return newHandle(procRoot) } func clonePrivateProcMount() (_ *Handle, Err error) { // Try to make a clone without using AT_RECURSIVE if we can. If this works, // we can be sure there are no over-mounts and so if the root is valid then // we're golden. Otherwise, we have to deal with over-mounts. procRoot, err := fd.OpenTree(nil, "/proc", unix.OPEN_TREE_CLONE) if err != nil || hookForcePrivateProcRootOpenTreeAtRecursive(procRoot) { procRoot, err = fd.OpenTree(nil, "/proc", unix.OPEN_TREE_CLONE|unix.AT_RECURSIVE) } if err != nil { return nil, fmt.Errorf("creating a detached procfs clone: %w", err) } defer func() { if Err != nil { _ = procRoot.Close() } }() return newHandle(procRoot) } func privateProcRoot(subset bool) (*Handle, error) { if !linux.HasNewMountAPI() || hookForceGetProcRootUnsafe() { return nil, fmt.Errorf("new mount api: %w", unix.ENOTSUP) } // Try to create a new procfs mount from scratch if we can. This ensures we // can get a procfs mount even if /proc is fake (for whatever reason). procRoot, err := newPrivateProcMount(subset) if err != nil || hookForcePrivateProcRootOpenTree(procRoot) { // Try to clone /proc then... procRoot, err = clonePrivateProcMount() } return procRoot, err } func unsafeHostProcRoot() (_ *Handle, Err error) { procRoot, err := os.OpenFile("/proc", unix.O_PATH|unix.O_NOFOLLOW|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) if err != nil { return nil, err } defer func() { if Err != nil { _ = procRoot.Close() } }() return newHandle(procRoot) } // Handle is a wrapper around an *os.File handle to "/proc", which can be used // to do further procfs-related operations in a safe way. type Handle struct { Inner fd.Fd // Does this handle have subset=pid set? isSubset bool } func newHandle(procRoot fd.Fd) (*Handle, error) { if err := verifyProcRoot(procRoot); err != nil { // This is only used in methods that _ = procRoot.Close() return nil, err } proc := &Handle{Inner: procRoot} // With subset=pid we can be sure that /proc/uptime will not exist. if err := fd.Faccessat(proc.Inner, "uptime", unix.F_OK, unix.AT_SYMLINK_NOFOLLOW); err != nil { proc.isSubset = errors.Is(err, os.ErrNotExist) } return proc, nil } // Close closes the underlying file for the Handle. func (proc *Handle) Close() error { return proc.Inner.Close() } var getCachedProcRoot = gocompat.SyncOnceValue(func() *Handle { procRoot, err := getProcRoot(true) if err != nil { return nil // just don't cache if we see an error } if !procRoot.isSubset { return nil // we only cache verified subset=pid handles } // Disarm (*Handle).Close() to stop someone from accidentally closing // the global handle. procRoot.Inner = fd.NopCloser(procRoot.Inner) return procRoot }) // OpenProcRoot tries to open a "safer" handle to "/proc". func OpenProcRoot() (*Handle, error) { if proc := getCachedProcRoot(); proc != nil { return proc, nil } return getProcRoot(true) } // OpenUnsafeProcRoot opens a handle to "/proc" without any overmounts or // masked paths (but also without "subset=pid"). func OpenUnsafeProcRoot() (*Handle, error) { return getProcRoot(false) } func getProcRoot(subset bool) (*Handle, error) { proc, err := privateProcRoot(subset) if err != nil { // Fall back to using a /proc handle if making a private mount failed. // If we have openat2, at least we can avoid some kinds of over-mount // attacks, but without openat2 there's not much we can do. proc, err = unsafeHostProcRoot() } return proc, err } var hasProcThreadSelf = gocompat.SyncOnceValue(func() bool { return unix.Access("/proc/thread-self/", unix.F_OK) == nil }) var errUnsafeProcfs = errors.New("unsafe procfs detected") // lookup is a very minimal wrapper around [procfsLookupInRoot] which is // intended to be called from the external API. func (proc *Handle) lookup(subpath string) (*os.File, error) { handle, err := procfsLookupInRoot(proc.Inner, subpath) if err != nil { return nil, err } return handle, nil } // procfsBase is an enum indicating the prefix of a subpath in operations // involving [Handle]s. type procfsBase string const ( // ProcRoot refers to the root of the procfs (i.e., "/proc/"). ProcRoot procfsBase = "/proc" // ProcSelf refers to the current process' subdirectory (i.e., // "/proc/self/"). ProcSelf procfsBase = "/proc/self" // ProcThreadSelf refers to the current thread's subdirectory (i.e., // "/proc/thread-self/"). In multi-threaded programs (i.e., all Go // programs) where one thread has a different CLONE_FS, it is possible for // "/proc/self" to point the wrong thread and so "/proc/thread-self" may be // necessary. Note that on pre-3.17 kernels, "/proc/thread-self" doesn't // exist and so a fallback will be used in that case. ProcThreadSelf procfsBase = "/proc/thread-self" // TODO: Switch to an interface setup so we can have a more type-safe // version of ProcPid and remove the need to worry about invalid string // values. ) // prefix returns a prefix that can be used with the given [Handle]. func (base procfsBase) prefix(proc *Handle) (string, error) { switch base { case ProcRoot: return ".", nil case ProcSelf: return "self", nil case ProcThreadSelf: threadSelf := "thread-self" if !hasProcThreadSelf() || hookForceProcSelfTask() { // Pre-3.17 kernels don't have /proc/thread-self, so do it // manually. threadSelf = "self/task/" + strconv.Itoa(unix.Gettid()) if err := fd.Faccessat(proc.Inner, threadSelf, unix.F_OK, unix.AT_SYMLINK_NOFOLLOW); err != nil || hookForceProcSelf() { // In this case, we running in a pid namespace that doesn't // match the /proc mount we have. This can happen inside runc. // // Unfortunately, there is no nice way to get the correct TID // to use here because of the age of the kernel, so we have to // just use /proc/self and hope that it works. threadSelf = "self" } } return threadSelf, nil } return "", fmt.Errorf("invalid procfs base %q", base) } // ProcThreadSelfCloser is a callback that needs to be called when you are done // operating on an [os.File] fetched using [ProcThreadSelf]. // // [os.File]: https://pkg.go.dev/os#File type ProcThreadSelfCloser func() // open is the core lookup operation for [Handle]. It returns a handle to // "/proc//". If the returned [ProcThreadSelfCloser] is non-nil, // you should call it after you are done interacting with the returned handle. // // In general you should use prefer to use the other helpers, as they remove // the need to interact with [procfsBase] and do not return a nil // [ProcThreadSelfCloser] for [procfsBase] values other than [ProcThreadSelf] // where it is necessary. func (proc *Handle) open(base procfsBase, subpath string) (_ *os.File, closer ProcThreadSelfCloser, Err error) { prefix, err := base.prefix(proc) if err != nil { return nil, nil, err } subpath = prefix + "/" + subpath switch base { case ProcRoot: file, err := proc.lookup(subpath) if errors.Is(err, os.ErrNotExist) { // The Handle handle in use might be a subset=pid one, which will // result in spurious errors. In this case, just open a temporary // unmasked procfs handle for this operation. proc, err2 := OpenUnsafeProcRoot() // !subset=pid if err2 != nil { return nil, nil, err } defer proc.Close() //nolint:errcheck // close failures aren't critical here file, err = proc.lookup(subpath) } return file, nil, err case ProcSelf: file, err := proc.lookup(subpath) return file, nil, err case ProcThreadSelf: // We need to lock our thread until the caller is done with the handle // because between getting the handle and using it we could get // interrupted by the Go runtime and hit the case where the underlying // thread is swapped out and the original thread is killed, resulting // in pull-your-hair-out-hard-to-debug issues in the caller. runtime.LockOSThread() defer func() { if Err != nil { runtime.UnlockOSThread() closer = nil } }() file, err := proc.lookup(subpath) return file, runtime.UnlockOSThread, err } // should never be reached return nil, nil, fmt.Errorf("[internal error] invalid procfs base %q", base) } // OpenThreadSelf returns a handle to "/proc/thread-self/" (or an // equivalent handle on older kernels where "/proc/thread-self" doesn't exist). // Once finished with the handle, you must call the returned closer function // (runtime.UnlockOSThread). You must not pass the returned *os.File to other // Go threads or use the handle after calling the closer. func (proc *Handle) OpenThreadSelf(subpath string) (_ *os.File, _ ProcThreadSelfCloser, Err error) { return proc.open(ProcThreadSelf, subpath) } // OpenSelf returns a handle to /proc/self/. func (proc *Handle) OpenSelf(subpath string) (*os.File, error) { file, closer, err := proc.open(ProcSelf, subpath) assert.Assert(closer == nil, "closer for ProcSelf must be nil") return file, err } // OpenRoot returns a handle to /proc/. func (proc *Handle) OpenRoot(subpath string) (*os.File, error) { file, closer, err := proc.open(ProcRoot, subpath) assert.Assert(closer == nil, "closer for ProcRoot must be nil") return file, err } // OpenPid returns a handle to /proc/$pid/ (pid can be a pid or tid). // This is mainly intended for usage when operating on other processes. func (proc *Handle) OpenPid(pid int, subpath string) (*os.File, error) { return proc.OpenRoot(strconv.Itoa(pid) + "/" + subpath) } // checkSubpathOvermount checks if the dirfd and path combination is on the // same mount as the given root. func checkSubpathOvermount(root, dir fd.Fd, path string) error { // Get the mntID of our procfs handle. expectedMountID, err := fd.GetMountID(root, "") if err != nil { return fmt.Errorf("get root mount id: %w", err) } // Get the mntID of the target magic-link. gotMountID, err := fd.GetMountID(dir, path) if err != nil { return fmt.Errorf("get subpath mount id: %w", err) } // As long as the directory mount is alive, even with wrapping mount IDs, // we would expect to see a different mount ID here. (Of course, if we're // using unsafeHostProcRoot() then an attaker could change this after we // did this check.) if expectedMountID != gotMountID { return fmt.Errorf("%w: subpath %s/%s has an overmount obscuring the real path (mount ids do not match %d != %d)", errUnsafeProcfs, dir.Name(), path, expectedMountID, gotMountID) } return nil } // Readlink performs a readlink operation on "/proc//" in a way // that should be free from race attacks. This is most commonly used to get the // real path of a file by looking at "/proc/self/fd/$n", with the same safety // protections as [Open] (as well as some additional checks against // overmounts). func (proc *Handle) Readlink(base procfsBase, subpath string) (string, error) { link, closer, err := proc.open(base, subpath) if closer != nil { defer closer() } if err != nil { return "", fmt.Errorf("get safe %s/%s handle: %w", base, subpath, err) } defer link.Close() //nolint:errcheck // close failures aren't critical here // Try to detect if there is a mount on top of the magic-link. This should // be safe in general (a mount on top of the path afterwards would not // affect the handle itself) and will definitely be safe if we are using // privateProcRoot() (at least since Linux 5.12[1], when anonymous mount // namespaces were completely isolated from external mounts including mount // propagation events). // // [1]: Linux commit ee2e3f50629f ("mount: fix mounting of detached mounts // onto targets that reside on shared mounts"). if err := checkSubpathOvermount(proc.Inner, link, ""); err != nil { return "", fmt.Errorf("check safety of %s/%s magiclink: %w", base, subpath, err) } // readlinkat implies AT_EMPTY_PATH since Linux 2.6.39. See Linux commit // 65cfc6722361 ("readlinkat(), fchownat() and fstatat() with empty // relative pathnames"). return fd.Readlinkat(link, "") } // ProcSelfFdReadlink gets the real path of the given file by looking at // readlink(/proc/thread-self/fd/$n). // // This is just a wrapper around [Handle.Readlink]. func ProcSelfFdReadlink(fd fd.Fd) (string, error) { procRoot, err := OpenProcRoot() // subset=pid if err != nil { return "", err } defer procRoot.Close() //nolint:errcheck // close failures aren't critical here fdPath := "fd/" + strconv.Itoa(int(fd.Fd())) return procRoot.Readlink(ProcThreadSelf, fdPath) } // CheckProcSelfFdPath returns whether the given file handle matches the // expected path. (This is inherently racy.) func CheckProcSelfFdPath(path string, file fd.Fd) error { if err := fd.IsDeadInode(file); err != nil { return err } actualPath, err := ProcSelfFdReadlink(file) if err != nil { return fmt.Errorf("get path of handle: %w", err) } if actualPath != path { return fmt.Errorf("%w: handle path %q doesn't match expected path %q", internal.ErrPossibleBreakout, actualPath, path) } return nil } // ReopenFd takes an existing file descriptor and "re-opens" it through // /proc/thread-self/fd/. This allows for O_PATH file descriptors to be // upgraded to regular file descriptors, as well as changing the open mode of a // regular file descriptor. Some filesystems have unique handling of open(2) // which make this incredibly useful (such as /dev/ptmx). func ReopenFd(handle fd.Fd, flags int) (*os.File, error) { procRoot, err := OpenProcRoot() // subset=pid if err != nil { return nil, err } defer procRoot.Close() //nolint:errcheck // close failures aren't critical here // We can't operate on /proc/thread-self/fd/$n directly when doing a // re-open, so we need to open /proc/thread-self/fd and then open a single // final component. procFdDir, closer, err := procRoot.OpenThreadSelf("fd/") if err != nil { return nil, fmt.Errorf("get safe /proc/thread-self/fd handle: %w", err) } defer procFdDir.Close() //nolint:errcheck // close failures aren't critical here defer closer() // Try to detect if there is a mount on top of the magic-link we are about // to open. If we are using unsafeHostProcRoot(), this could change after // we check it (and there's nothing we can do about that) but for // privateProcRoot() this should be guaranteed to be safe (at least since // Linux 5.12[1], when anonymous mount namespaces were completely isolated // from external mounts including mount propagation events). // // [1]: Linux commit ee2e3f50629f ("mount: fix mounting of detached mounts // onto targets that reside on shared mounts"). fdStr := strconv.Itoa(int(handle.Fd())) if err := checkSubpathOvermount(procRoot.Inner, procFdDir, fdStr); err != nil { return nil, fmt.Errorf("check safety of /proc/thread-self/fd/%s magiclink: %w", fdStr, err) } flags |= unix.O_CLOEXEC // Rather than just wrapping fd.Openat, open-code it so we can copy // handle.Name(). reopenFd, err := unix.Openat(int(procFdDir.Fd()), fdStr, flags, 0) if err != nil { return nil, fmt.Errorf("reopen fd %d: %w", handle.Fd(), err) } return os.NewFile(uintptr(reopenFd), handle.Name()), nil } // Test hooks used in the procfs tests to verify that the fallback logic works. // See testing_mocks_linux_test.go and procfs_linux_test.go for more details. var ( hookForcePrivateProcRootOpenTree = hookDummyFile hookForcePrivateProcRootOpenTreeAtRecursive = hookDummyFile hookForceGetProcRootUnsafe = hookDummy hookForceProcSelfTask = hookDummy hookForceProcSelf = hookDummy ) func hookDummy() bool { return false } func hookDummyFile(_ io.Closer) bool { return false } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs/procfs_lookup_linux.go ================================================ // SPDX-License-Identifier: MPL-2.0 //go:build linux // Copyright (C) 2024-2025 Aleksa Sarai // Copyright (C) 2024-2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // This code is adapted to be a minimal version of the libpathrs proc resolver // . // As we only need O_PATH|O_NOFOLLOW support, this is not too much to port. package procfs import ( "fmt" "os" "path" "path/filepath" "strings" "golang.org/x/sys/unix" "github.com/cyphar/filepath-securejoin/internal/consts" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux" ) // procfsLookupInRoot is a stripped down version of completeLookupInRoot, // entirely designed to support the very small set of features necessary to // make procfs handling work. Unlike completeLookupInRoot, we always have // O_PATH|O_NOFOLLOW behaviour for trailing symlinks. // // The main restrictions are: // // - ".." is not supported (as it requires either os.Root-style replays, // which is more bug-prone; or procfs verification, which is not possible // due to re-entrancy issues). // - Absolute symlinks for the same reason (and all absolute symlinks in // procfs are magic-links, which we want to skip anyway). // - If statx is supported (checkSymlinkOvermount), any mount-point crossings // (which is the main attack of concern against /proc). // - Partial lookups are not supported, so the symlink stack is not needed. // - Trailing slash special handling is not necessary in most cases (if we // operating on procfs, it's usually with programmer-controlled strings // that will then be re-opened), so we skip it since whatever re-opens it // can deal with it. It's a creature comfort anyway. // // If the system supports openat2(), this is implemented using equivalent flags // (RESOLVE_BENEATH | RESOLVE_NO_XDEV | RESOLVE_NO_MAGICLINKS). func procfsLookupInRoot(procRoot fd.Fd, unsafePath string) (Handle *os.File, _ error) { unsafePath = filepath.ToSlash(unsafePath) // noop // Make sure that an empty unsafe path still returns something sane, even // with openat2 (which doesn't have AT_EMPTY_PATH semantics yet). if unsafePath == "" { unsafePath = "." } // This is already checked by getProcRoot, but make sure here since the // core security of this lookup is based on this assumption. if err := verifyProcRoot(procRoot); err != nil { return nil, err } if linux.HasOpenat2() { // We prefer being able to use RESOLVE_NO_XDEV if we can, to be // absolutely sure we are operating on a clean /proc handle that // doesn't have any cheeky overmounts that could trick us (including // symlink mounts on top of /proc/thread-self). RESOLVE_BENEATH isn't // strictly needed, but just use it since we have it. // // NOTE: /proc/self is technically a magic-link (the contents of the // symlink are generated dynamically), but it doesn't use // nd_jump_link() so RESOLVE_NO_MAGICLINKS allows it. // // TODO: It would be nice to have RESOLVE_NO_DOTDOT, purely for // self-consistency with the backup O_PATH resolver. handle, err := fd.Openat2(procRoot, unsafePath, &unix.OpenHow{ Flags: unix.O_PATH | unix.O_NOFOLLOW | unix.O_CLOEXEC, Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_XDEV | unix.RESOLVE_NO_MAGICLINKS, }) if err != nil { // TODO: Once we bump the minimum Go version to 1.20, we can use // multiple %w verbs for this wrapping. For now we need to use a // compatibility shim for older Go versions. // err = fmt.Errorf("%w: %w", errUnsafeProcfs, err) return nil, gocompat.WrapBaseError(err, errUnsafeProcfs) } return handle, nil } // To mirror openat2(RESOLVE_BENEATH), we need to return an error if the // path is absolute. if path.IsAbs(unsafePath) { return nil, fmt.Errorf("%w: cannot resolve absolute paths in procfs resolver", internal.ErrPossibleBreakout) } currentDir, err := fd.Dup(procRoot) if err != nil { return nil, fmt.Errorf("clone root fd: %w", err) } defer func() { // If a handle is not returned, close the internal handle. if Handle == nil { _ = currentDir.Close() } }() var ( linksWalked int currentPath string remainingPath = unsafePath ) for remainingPath != "" { // Get the next path component. var part string if i := strings.IndexByte(remainingPath, '/'); i == -1 { part, remainingPath = remainingPath, "" } else { part, remainingPath = remainingPath[:i], remainingPath[i+1:] } if part == "" { // no-op component, but treat it the same as "." part = "." } if part == ".." { // not permitted return nil, fmt.Errorf("%w: cannot walk into '..' in procfs resolver", internal.ErrPossibleBreakout) } // Apply the component lexically to the path we are building. // currentPath does not contain any symlinks, and we are lexically // dealing with a single component, so it's okay to do a filepath.Clean // here. (Not to mention that ".." isn't allowed.) nextPath := path.Join("/", currentPath, part) // If we logically hit the root, just clone the root rather than // opening the part and doing all of the other checks. if nextPath == "/" { // Jump to root. rootClone, err := fd.Dup(procRoot) if err != nil { return nil, fmt.Errorf("clone root fd: %w", err) } _ = currentDir.Close() currentDir = rootClone currentPath = nextPath continue } // Try to open the next component. nextDir, err := fd.Openat(currentDir, part, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) if err != nil { return nil, err } // Make sure we are still on procfs and haven't crossed mounts. if err := verifyProcHandle(nextDir); err != nil { _ = nextDir.Close() return nil, fmt.Errorf("check %q component is on procfs: %w", part, err) } if err := checkSubpathOvermount(procRoot, nextDir, ""); err != nil { _ = nextDir.Close() return nil, fmt.Errorf("check %q component is not overmounted: %w", part, err) } // We are emulating O_PATH|O_NOFOLLOW, so we only need to traverse into // trailing symlinks if we are not the final component. Otherwise we // can just return the currentDir. if remainingPath != "" { st, err := nextDir.Stat() if err != nil { _ = nextDir.Close() return nil, fmt.Errorf("stat component %q: %w", part, err) } if st.Mode()&os.ModeType == os.ModeSymlink { // readlinkat implies AT_EMPTY_PATH since Linux 2.6.39. See // Linux commit 65cfc6722361 ("readlinkat(), fchownat() and // fstatat() with empty relative pathnames"). linkDest, err := fd.Readlinkat(nextDir, "") // We don't need the handle anymore. _ = nextDir.Close() if err != nil { return nil, err } linksWalked++ if linksWalked > consts.MaxSymlinkLimit { return nil, &os.PathError{Op: "securejoin.procfsLookupInRoot", Path: "/proc/" + unsafePath, Err: unix.ELOOP} } // Update our logical remaining path. remainingPath = linkDest + "/" + remainingPath // Absolute symlinks are probably magiclinks, we reject them. if path.IsAbs(linkDest) { return nil, fmt.Errorf("%w: cannot jump to / in procfs resolver -- possible magiclink", internal.ErrPossibleBreakout) } continue } } // Walk into the next component. _ = currentDir.Close() currentDir = nextDir currentPath = nextPath } // One final sanity-check. if err := verifyProcHandle(currentDir); err != nil { return nil, fmt.Errorf("check final handle is on procfs: %w", err) } if err := checkSubpathOvermount(procRoot, currentDir, ""); err != nil { return nil, fmt.Errorf("check final handle is not overmounted: %w", err) } return currentDir, nil } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/mkdir.go ================================================ // SPDX-License-Identifier: MPL-2.0 //go:build linux // Copyright (C) 2024-2025 Aleksa Sarai // Copyright (C) 2024-2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package pathrs import ( "os" "golang.org/x/sys/unix" ) // MkdirAll is a race-safe alternative to the [os.MkdirAll] function, // where the new directory is guaranteed to be within the root directory (if an // attacker can move directories from inside the root to outside the root, the // created directory tree might be outside of the root but the key constraint // is that at no point will we walk outside of the directory tree we are // creating). // // Effectively, MkdirAll(root, unsafePath, mode) is equivalent to // // path, _ := securejoin.SecureJoin(root, unsafePath) // err := os.MkdirAll(path, mode) // // But is much safer. The above implementation is unsafe because if an attacker // can modify the filesystem tree between [SecureJoin] and [os.MkdirAll], it is // possible for MkdirAll to resolve unsafe symlink components and create // directories outside of the root. // // If you plan to open the directory after you have created it or want to use // an open directory handle as the root, you should use [MkdirAllHandle] instead. // This function is a wrapper around [MkdirAllHandle]. // // [SecureJoin]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin#SecureJoin func MkdirAll(root, unsafePath string, mode os.FileMode) error { rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) if err != nil { return err } defer rootDir.Close() //nolint:errcheck // close failures aren't critical here f, err := MkdirAllHandle(rootDir, unsafePath, mode) if err != nil { return err } _ = f.Close() return nil } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/mkdir_libpathrs.go ================================================ // SPDX-License-Identifier: MPL-2.0 //go:build libpathrs // Copyright (C) 2024-2025 Aleksa Sarai // Copyright (C) 2024-2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package pathrs import ( "os" "cyphar.com/go-pathrs" ) // MkdirAllHandle is equivalent to [MkdirAll], except that it is safer to use // in two respects: // // - The caller provides the root directory as an *[os.File] (preferably O_PATH) // handle. This means that the caller can be sure which root directory is // being used. Note that this can be emulated by using /proc/self/fd/... as // the root path with [os.MkdirAll]. // // - Once all of the directories have been created, an *[os.File] O_PATH handle // to the directory at unsafePath is returned to the caller. This is done in // an effectively-race-free way (an attacker would only be able to swap the // final directory component), which is not possible to emulate with // [MkdirAll]. // // In addition, the returned handle is obtained far more efficiently than doing // a brand new lookup of unsafePath (such as with [SecureJoin] or openat2) after // doing [MkdirAll]. If you intend to open the directory after creating it, you // should use MkdirAllHandle. // // [SecureJoin]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin#SecureJoin func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (*os.File, error) { rootRef, err := pathrs.RootFromFile(root) if err != nil { return nil, err } defer rootRef.Close() //nolint:errcheck // close failures aren't critical here handle, err := rootRef.MkdirAll(unsafePath, mode) if err != nil { return nil, err } return handle.IntoFile(), nil } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/mkdir_purego.go ================================================ // SPDX-License-Identifier: MPL-2.0 //go:build linux && !libpathrs // Copyright (C) 2024-2025 Aleksa Sarai // Copyright (C) 2024-2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package pathrs import ( "os" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gopathrs" ) // MkdirAllHandle is equivalent to [MkdirAll], except that it is safer to use // in two respects: // // - The caller provides the root directory as an *[os.File] (preferably O_PATH) // handle. This means that the caller can be sure which root directory is // being used. Note that this can be emulated by using /proc/self/fd/... as // the root path with [os.MkdirAll]. // // - Once all of the directories have been created, an *[os.File] O_PATH handle // to the directory at unsafePath is returned to the caller. This is done in // an effectively-race-free way (an attacker would only be able to swap the // final directory component), which is not possible to emulate with // [MkdirAll]. // // In addition, the returned handle is obtained far more efficiently than doing // a brand new lookup of unsafePath (such as with [SecureJoin] or openat2) after // doing [MkdirAll]. If you intend to open the directory after creating it, you // should use MkdirAllHandle. // // [SecureJoin]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin#SecureJoin func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (*os.File, error) { return gopathrs.MkdirAllHandle(root, unsafePath, mode) } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/open.go ================================================ // SPDX-License-Identifier: MPL-2.0 //go:build linux // Copyright (C) 2024-2025 Aleksa Sarai // Copyright (C) 2024-2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package pathrs import ( "os" "golang.org/x/sys/unix" ) // OpenInRoot safely opens the provided unsafePath within the root. // Effectively, OpenInRoot(root, unsafePath) is equivalent to // // path, _ := securejoin.SecureJoin(root, unsafePath) // handle, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC) // // But is much safer. The above implementation is unsafe because if an attacker // can modify the filesystem tree between [SecureJoin] and [os.OpenFile], it is // possible for the returned file to be outside of the root. // // Note that the returned handle is an O_PATH handle, meaning that only a very // limited set of operations will work on the handle. This is done to avoid // accidentally opening an untrusted file that could cause issues (such as a // disconnected TTY that could cause a DoS, or some other issue). In order to // use the returned handle, you can "upgrade" it to a proper handle using // [Reopen]. // // [SecureJoin]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin#SecureJoin func OpenInRoot(root, unsafePath string) (*os.File, error) { rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) if err != nil { return nil, err } defer rootDir.Close() //nolint:errcheck // close failures aren't critical here return OpenatInRoot(rootDir, unsafePath) } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/open_libpathrs.go ================================================ // SPDX-License-Identifier: MPL-2.0 //go:build libpathrs // Copyright (C) 2024-2025 Aleksa Sarai // Copyright (C) 2024-2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package pathrs import ( "os" "cyphar.com/go-pathrs" ) // OpenatInRoot is equivalent to [OpenInRoot], except that the root is provided // using an *[os.File] handle, to ensure that the correct root directory is used. func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error) { rootRef, err := pathrs.RootFromFile(root) if err != nil { return nil, err } defer rootRef.Close() //nolint:errcheck // close failures aren't critical here handle, err := rootRef.Resolve(unsafePath) if err != nil { return nil, err } return handle.IntoFile(), nil } // Reopen takes an *[os.File] handle and re-opens it through /proc/self/fd. // Reopen(file, flags) is effectively equivalent to // // fdPath := fmt.Sprintf("/proc/self/fd/%d", file.Fd()) // os.OpenFile(fdPath, flags|unix.O_CLOEXEC) // // But with some extra hardenings to ensure that we are not tricked by a // maliciously-configured /proc mount. While this attack scenario is not // common, in container runtimes it is possible for higher-level runtimes to be // tricked into configuring an unsafe /proc that can be used to attack file // operations. See [CVE-2019-19921] for more details. // // [CVE-2019-19921]: https://github.com/advisories/GHSA-fh74-hm69-rqjw func Reopen(file *os.File, flags int) (*os.File, error) { handle, err := pathrs.HandleFromFile(file) if err != nil { return nil, err } defer handle.Close() //nolint:errcheck // close failures aren't critical here return handle.OpenFile(flags) } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/open_purego.go ================================================ // SPDX-License-Identifier: MPL-2.0 //go:build linux && !libpathrs // Copyright (C) 2024-2025 Aleksa Sarai // Copyright (C) 2024-2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package pathrs import ( "os" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gopathrs" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs" ) // OpenatInRoot is equivalent to [OpenInRoot], except that the root is provided // using an *[os.File] handle, to ensure that the correct root directory is used. func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error) { return gopathrs.OpenatInRoot(root, unsafePath) } // Reopen takes an *[os.File] handle and re-opens it through /proc/self/fd. // Reopen(file, flags) is effectively equivalent to // // fdPath := fmt.Sprintf("/proc/self/fd/%d", file.Fd()) // os.OpenFile(fdPath, flags|unix.O_CLOEXEC) // // But with some extra hardenings to ensure that we are not tricked by a // maliciously-configured /proc mount. While this attack scenario is not // common, in container runtimes it is possible for higher-level runtimes to be // tricked into configuring an unsafe /proc that can be used to attack file // operations. See [CVE-2019-19921] for more details. // // [CVE-2019-19921]: https://github.com/advisories/GHSA-fh74-hm69-rqjw func Reopen(handle *os.File, flags int) (*os.File, error) { return procfs.ReopenFd(handle, flags) } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/procfs/procfs_libpathrs.go ================================================ // SPDX-License-Identifier: MPL-2.0 //go:build libpathrs // Copyright (C) 2024-2025 Aleksa Sarai // Copyright (C) 2024-2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // Package procfs provides a safe API for operating on /proc on Linux. package procfs import ( "os" "strconv" "cyphar.com/go-pathrs/procfs" "golang.org/x/sys/unix" ) // ProcThreadSelfCloser is a callback that needs to be called when you are done // operating on an [os.File] fetched using [Handle.OpenThreadSelf]. // // [os.File]: https://pkg.go.dev/os#File type ProcThreadSelfCloser = procfs.ThreadCloser // Handle is a wrapper around an *os.File handle to "/proc", which can be used // to do further procfs-related operations in a safe way. type Handle struct { inner *procfs.Handle } // Close close the resources associated with this [Handle]. Note that if this // [Handle] was created with [OpenProcRoot], on some kernels the underlying // procfs handle is cached and so this Close operation may be a no-op. However, // you should always call Close on [Handle]s once you are done with them. func (proc *Handle) Close() error { return proc.inner.Close() } // OpenProcRoot tries to open a "safer" handle to "/proc" (i.e., one with the // "subset=pid" mount option applied, available from Linux 5.8). Unless you // plan to do many [Handle.OpenRoot] operations, users should prefer to use // this over [OpenUnsafeProcRoot] which is far more dangerous to keep open. // // If a safe handle cannot be opened, OpenProcRoot will fall back to opening a // regular "/proc" handle. // // Note that using [Handle.OpenRoot] will still work with handles returned by // this function. If a subpath cannot be operated on with a safe "/proc" // handle, then [OpenUnsafeProcRoot] will be called internally and a temporary // unsafe handle will be used. func OpenProcRoot() (*Handle, error) { proc, err := procfs.Open() if err != nil { return nil, err } return &Handle{inner: proc}, nil } // OpenUnsafeProcRoot opens a handle to "/proc" without any overmounts or // masked paths. You must be extremely careful to make sure this handle is // never leaked to a container and that you program cannot be tricked into // writing to arbitrary paths within it. // // This is not necessary if you just wish to use [Handle.OpenRoot], as handles // returned by [OpenProcRoot] will fall back to using a *temporary* unsafe // handle in that case. You should only really use this if you need to do many // operations with [Handle.OpenRoot] and the performance overhead of making // many procfs handles is an issue. If you do use OpenUnsafeProcRoot, you // should make sure to close the handle as soon as possible to avoid // known-fd-number attacks. func OpenUnsafeProcRoot() (*Handle, error) { proc, err := procfs.Open(procfs.UnmaskedProcRoot) if err != nil { return nil, err } return &Handle{inner: proc}, nil } // OpenThreadSelf returns a handle to "/proc/thread-self/" (or an // equivalent handle on older kernels where "/proc/thread-self" doesn't exist). // Once finished with the handle, you must call the returned closer function // ([runtime.UnlockOSThread]). You must not pass the returned *os.File to other // Go threads or use the handle after calling the closer. // // [runtime.UnlockOSThread]: https://pkg.go.dev/runtime#UnlockOSThread func (proc *Handle) OpenThreadSelf(subpath string) (*os.File, ProcThreadSelfCloser, error) { return proc.inner.OpenThreadSelf(subpath, unix.O_PATH|unix.O_NOFOLLOW) } // OpenSelf returns a handle to /proc/self/. // // Note that in Go programs with non-homogenous threads, this may result in // spurious errors. If you are monkeying around with APIs that are // thread-specific, you probably want to use [Handle.OpenThreadSelf] instead // which will guarantee that the handle refers to the same thread as the caller // is executing on. func (proc *Handle) OpenSelf(subpath string) (*os.File, error) { return proc.inner.OpenSelf(subpath, unix.O_PATH|unix.O_NOFOLLOW) } // OpenRoot returns a handle to /proc/. // // You should only use this when you need to operate on global procfs files // (such as sysctls in /proc/sys). Unlike [Handle.OpenThreadSelf], // [Handle.OpenSelf], and [Handle.OpenPid], the procfs handle used internally // for this operation will never use "subset=pid", which makes it a more juicy // target for [CVE-2024-21626]-style attacks (and doing something like opening // a directory with OpenRoot effectively leaks [OpenUnsafeProcRoot] as long as // the file descriptor is open). // // [CVE-2024-21626]: https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv func (proc *Handle) OpenRoot(subpath string) (*os.File, error) { return proc.inner.OpenRoot(subpath, unix.O_PATH|unix.O_NOFOLLOW) } // OpenPid returns a handle to /proc/$pid/ (pid can be a pid or tid). // This is mainly intended for usage when operating on other processes. // // You should not use this for the current thread, as special handling is // needed for /proc/thread-self (or /proc/self/task/) when dealing with // goroutine scheduling -- use [Handle.OpenThreadSelf] instead. // // To refer to the current thread-group, you should use prefer // [Handle.OpenSelf] to passing os.Getpid as the pid argument. func (proc *Handle) OpenPid(pid int, subpath string) (*os.File, error) { return proc.inner.OpenPid(pid, subpath, unix.O_PATH|unix.O_NOFOLLOW) } // ProcSelfFdReadlink gets the real path of the given file by looking at // /proc/self/fd/ with [readlink]. It is effectively just shorthand for // something along the lines of: // // proc, err := procfs.OpenProcRoot() // if err != nil { // return err // } // link, err := proc.OpenThreadSelf(fmt.Sprintf("fd/%d", f.Fd())) // if err != nil { // return err // } // defer link.Close() // var buf [4096]byte // n, err := unix.Readlinkat(int(link.Fd()), "", buf[:]) // if err != nil { // return err // } // pathname := buf[:n] // // [readlink]: https://pkg.go.dev/golang.org/x/sys/unix#Readlinkat func ProcSelfFdReadlink(f *os.File) (string, error) { proc, err := procfs.Open() if err != nil { return "", err } defer proc.Close() //nolint:errcheck // close failures aren't critical here fdPath := "fd/" + strconv.Itoa(int(f.Fd())) return proc.Readlink(procfs.ProcThreadSelf, fdPath) } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/procfs/procfs_purego.go ================================================ // SPDX-License-Identifier: MPL-2.0 //go:build linux && !libpathrs // Copyright (C) 2024-2025 Aleksa Sarai // Copyright (C) 2024-2025 SUSE LLC // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // Package procfs provides a safe API for operating on /proc on Linux. package procfs import ( "os" "github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs" ) // This package mostly just wraps internal/procfs APIs. This is necessary // because we are forced to export some things from internal/procfs in order to // avoid some dependency cycle issues, but we don't want users to see or use // them. // ProcThreadSelfCloser is a callback that needs to be called when you are done // operating on an [os.File] fetched using [Handle.OpenThreadSelf]. // // [os.File]: https://pkg.go.dev/os#File type ProcThreadSelfCloser = procfs.ProcThreadSelfCloser // Handle is a wrapper around an *os.File handle to "/proc", which can be used // to do further procfs-related operations in a safe way. type Handle struct { inner *procfs.Handle } // Close close the resources associated with this [Handle]. Note that if this // [Handle] was created with [OpenProcRoot], on some kernels the underlying // procfs handle is cached and so this Close operation may be a no-op. However, // you should always call Close on [Handle]s once you are done with them. func (proc *Handle) Close() error { return proc.inner.Close() } // OpenProcRoot tries to open a "safer" handle to "/proc" (i.e., one with the // "subset=pid" mount option applied, available from Linux 5.8). Unless you // plan to do many [Handle.OpenRoot] operations, users should prefer to use // this over [OpenUnsafeProcRoot] which is far more dangerous to keep open. // // If a safe handle cannot be opened, OpenProcRoot will fall back to opening a // regular "/proc" handle. // // Note that using [Handle.OpenRoot] will still work with handles returned by // this function. If a subpath cannot be operated on with a safe "/proc" // handle, then [OpenUnsafeProcRoot] will be called internally and a temporary // unsafe handle will be used. func OpenProcRoot() (*Handle, error) { proc, err := procfs.OpenProcRoot() if err != nil { return nil, err } return &Handle{inner: proc}, nil } // OpenUnsafeProcRoot opens a handle to "/proc" without any overmounts or // masked paths. You must be extremely careful to make sure this handle is // never leaked to a container and that you program cannot be tricked into // writing to arbitrary paths within it. // // This is not necessary if you just wish to use [Handle.OpenRoot], as handles // returned by [OpenProcRoot] will fall back to using a *temporary* unsafe // handle in that case. You should only really use this if you need to do many // operations with [Handle.OpenRoot] and the performance overhead of making // many procfs handles is an issue. If you do use OpenUnsafeProcRoot, you // should make sure to close the handle as soon as possible to avoid // known-fd-number attacks. func OpenUnsafeProcRoot() (*Handle, error) { proc, err := procfs.OpenUnsafeProcRoot() if err != nil { return nil, err } return &Handle{inner: proc}, nil } // OpenThreadSelf returns a handle to "/proc/thread-self/" (or an // equivalent handle on older kernels where "/proc/thread-self" doesn't exist). // Once finished with the handle, you must call the returned closer function // ([runtime.UnlockOSThread]). You must not pass the returned *os.File to other // Go threads or use the handle after calling the closer. // // [runtime.UnlockOSThread]: https://pkg.go.dev/runtime#UnlockOSThread func (proc *Handle) OpenThreadSelf(subpath string) (*os.File, ProcThreadSelfCloser, error) { return proc.inner.OpenThreadSelf(subpath) } // OpenSelf returns a handle to /proc/self/. // // Note that in Go programs with non-homogenous threads, this may result in // spurious errors. If you are monkeying around with APIs that are // thread-specific, you probably want to use [Handle.OpenThreadSelf] instead // which will guarantee that the handle refers to the same thread as the caller // is executing on. func (proc *Handle) OpenSelf(subpath string) (*os.File, error) { return proc.inner.OpenSelf(subpath) } // OpenRoot returns a handle to /proc/. // // You should only use this when you need to operate on global procfs files // (such as sysctls in /proc/sys). Unlike [Handle.OpenThreadSelf], // [Handle.OpenSelf], and [Handle.OpenPid], the procfs handle used internally // for this operation will never use "subset=pid", which makes it a more juicy // target for [CVE-2024-21626]-style attacks (and doing something like opening // a directory with OpenRoot effectively leaks [OpenUnsafeProcRoot] as long as // the file descriptor is open). // // [CVE-2024-21626]: https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv func (proc *Handle) OpenRoot(subpath string) (*os.File, error) { return proc.inner.OpenRoot(subpath) } // OpenPid returns a handle to /proc/$pid/ (pid can be a pid or tid). // This is mainly intended for usage when operating on other processes. // // You should not use this for the current thread, as special handling is // needed for /proc/thread-self (or /proc/self/task/) when dealing with // goroutine scheduling -- use [Handle.OpenThreadSelf] instead. // // To refer to the current thread-group, you should use prefer // [Handle.OpenSelf] to passing os.Getpid as the pid argument. func (proc *Handle) OpenPid(pid int, subpath string) (*os.File, error) { return proc.inner.OpenPid(pid, subpath) } // ProcSelfFdReadlink gets the real path of the given file by looking at // /proc/self/fd/ with [readlink]. It is effectively just shorthand for // something along the lines of: // // proc, err := procfs.OpenProcRoot() // if err != nil { // return err // } // link, err := proc.OpenThreadSelf(fmt.Sprintf("fd/%d", f.Fd())) // if err != nil { // return err // } // defer link.Close() // var buf [4096]byte // n, err := unix.Readlinkat(int(link.Fd()), "", buf[:]) // if err != nil { // return err // } // pathname := buf[:n] // // [readlink]: https://pkg.go.dev/golang.org/x/sys/unix#Readlinkat func ProcSelfFdReadlink(f *os.File) (string, error) { return procfs.ProcSelfFdReadlink(f) } ================================================ FILE: vendor/github.com/cyphar/filepath-securejoin/vfs.go ================================================ // SPDX-License-Identifier: BSD-3-Clause // Copyright (C) 2017-2024 SUSE LLC. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package securejoin import "os" // In future this should be moved into a separate package, because now there // are several projects (umoci and go-mtree) that are using this sort of // interface. // VFS is the minimal interface necessary to use [SecureJoinVFS]. A nil VFS is // equivalent to using the standard [os].* family of functions. This is mainly // used for the purposes of mock testing, but also can be used to otherwise use // [SecureJoinVFS] with VFS-like system. type VFS interface { // Lstat returns an [os.FileInfo] describing the named file. If the // file is a symbolic link, the returned [os.FileInfo] describes the // symbolic link. Lstat makes no attempt to follow the link. // The semantics are identical to [os.Lstat]. Lstat(name string) (os.FileInfo, error) // Readlink returns the destination of the named symbolic link. // The semantics are identical to [os.Readlink]. Readlink(name string) (string, error) } // osVFS is the "nil" VFS, in that it just passes everything through to the os // module. type osVFS struct{} func (o osVFS) Lstat(name string) (os.FileInfo, error) { return os.Lstat(name) } func (o osVFS) Readlink(name string) (string, error) { return os.Readlink(name) } ================================================ FILE: vendor/github.com/davecgh/go-spew/LICENSE ================================================ ISC License Copyright (c) 2012-2016 Dave Collins Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ================================================ FILE: vendor/github.com/davecgh/go-spew/spew/bypass.go ================================================ // Copyright (c) 2015-2016 Dave Collins // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // NOTE: Due to the following build constraints, this file will only be compiled // when the code is not running on Google App Engine, compiled by GopherJS, and // "-tags safe" is not added to the go build command line. The "disableunsafe" // tag is deprecated and thus should not be used. // Go versions prior to 1.4 are disabled because they use a different layout // for interfaces which make the implementation of unsafeReflectValue more complex. // +build !js,!appengine,!safe,!disableunsafe,go1.4 package spew import ( "reflect" "unsafe" ) const ( // UnsafeDisabled is a build-time constant which specifies whether or // not access to the unsafe package is available. UnsafeDisabled = false // ptrSize is the size of a pointer on the current arch. ptrSize = unsafe.Sizeof((*byte)(nil)) ) type flag uintptr var ( // flagRO indicates whether the value field of a reflect.Value // is read-only. flagRO flag // flagAddr indicates whether the address of the reflect.Value's // value may be taken. flagAddr flag ) // flagKindMask holds the bits that make up the kind // part of the flags field. In all the supported versions, // it is in the lower 5 bits. const flagKindMask = flag(0x1f) // Different versions of Go have used different // bit layouts for the flags type. This table // records the known combinations. var okFlags = []struct { ro, addr flag }{{ // From Go 1.4 to 1.5 ro: 1 << 5, addr: 1 << 7, }, { // Up to Go tip. ro: 1<<5 | 1<<6, addr: 1 << 8, }} var flagValOffset = func() uintptr { field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag") if !ok { panic("reflect.Value has no flag field") } return field.Offset }() // flagField returns a pointer to the flag field of a reflect.Value. func flagField(v *reflect.Value) *flag { return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset)) } // unsafeReflectValue converts the passed reflect.Value into a one that bypasses // the typical safety restrictions preventing access to unaddressable and // unexported data. It works by digging the raw pointer to the underlying // value out of the protected value and generating a new unprotected (unsafe) // reflect.Value to it. // // This allows us to check for implementations of the Stringer and error // interfaces to be used for pretty printing ordinarily unaddressable and // inaccessible values such as unexported struct fields. func unsafeReflectValue(v reflect.Value) reflect.Value { if !v.IsValid() || (v.CanInterface() && v.CanAddr()) { return v } flagFieldPtr := flagField(&v) *flagFieldPtr &^= flagRO *flagFieldPtr |= flagAddr return v } // Sanity checks against future reflect package changes // to the type or semantics of the Value.flag field. func init() { field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag") if !ok { panic("reflect.Value has no flag field") } if field.Type.Kind() != reflect.TypeOf(flag(0)).Kind() { panic("reflect.Value flag field has changed kind") } type t0 int var t struct { A t0 // t0 will have flagEmbedRO set. t0 // a will have flagStickyRO set a t0 } vA := reflect.ValueOf(t).FieldByName("A") va := reflect.ValueOf(t).FieldByName("a") vt0 := reflect.ValueOf(t).FieldByName("t0") // Infer flagRO from the difference between the flags // for the (otherwise identical) fields in t. flagPublic := *flagField(&vA) flagWithRO := *flagField(&va) | *flagField(&vt0) flagRO = flagPublic ^ flagWithRO // Infer flagAddr from the difference between a value // taken from a pointer and not. vPtrA := reflect.ValueOf(&t).Elem().FieldByName("A") flagNoPtr := *flagField(&vA) flagPtr := *flagField(&vPtrA) flagAddr = flagNoPtr ^ flagPtr // Check that the inferred flags tally with one of the known versions. for _, f := range okFlags { if flagRO == f.ro && flagAddr == f.addr { return } } panic("reflect.Value read-only flag has changed semantics") } ================================================ FILE: vendor/github.com/davecgh/go-spew/spew/bypasssafe.go ================================================ // Copyright (c) 2015-2016 Dave Collins // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // NOTE: Due to the following build constraints, this file will only be compiled // when the code is running on Google App Engine, compiled by GopherJS, or // "-tags safe" is added to the go build command line. The "disableunsafe" // tag is deprecated and thus should not be used. // +build js appengine safe disableunsafe !go1.4 package spew import "reflect" const ( // UnsafeDisabled is a build-time constant which specifies whether or // not access to the unsafe package is available. UnsafeDisabled = true ) // unsafeReflectValue typically converts the passed reflect.Value into a one // that bypasses the typical safety restrictions preventing access to // unaddressable and unexported data. However, doing this relies on access to // the unsafe package. This is a stub version which simply returns the passed // reflect.Value when the unsafe package is not available. func unsafeReflectValue(v reflect.Value) reflect.Value { return v } ================================================ FILE: vendor/github.com/davecgh/go-spew/spew/common.go ================================================ /* * Copyright (c) 2013-2016 Dave Collins * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ package spew import ( "bytes" "fmt" "io" "reflect" "sort" "strconv" ) // Some constants in the form of bytes to avoid string overhead. This mirrors // the technique used in the fmt package. var ( panicBytes = []byte("(PANIC=") plusBytes = []byte("+") iBytes = []byte("i") trueBytes = []byte("true") falseBytes = []byte("false") interfaceBytes = []byte("(interface {})") commaNewlineBytes = []byte(",\n") newlineBytes = []byte("\n") openBraceBytes = []byte("{") openBraceNewlineBytes = []byte("{\n") closeBraceBytes = []byte("}") asteriskBytes = []byte("*") colonBytes = []byte(":") colonSpaceBytes = []byte(": ") openParenBytes = []byte("(") closeParenBytes = []byte(")") spaceBytes = []byte(" ") pointerChainBytes = []byte("->") nilAngleBytes = []byte("") maxNewlineBytes = []byte("\n") maxShortBytes = []byte("") circularBytes = []byte("") circularShortBytes = []byte("") invalidAngleBytes = []byte("") openBracketBytes = []byte("[") closeBracketBytes = []byte("]") percentBytes = []byte("%") precisionBytes = []byte(".") openAngleBytes = []byte("<") closeAngleBytes = []byte(">") openMapBytes = []byte("map[") closeMapBytes = []byte("]") lenEqualsBytes = []byte("len=") capEqualsBytes = []byte("cap=") ) // hexDigits is used to map a decimal value to a hex digit. var hexDigits = "0123456789abcdef" // catchPanic handles any panics that might occur during the handleMethods // calls. func catchPanic(w io.Writer, v reflect.Value) { if err := recover(); err != nil { w.Write(panicBytes) fmt.Fprintf(w, "%v", err) w.Write(closeParenBytes) } } // handleMethods attempts to call the Error and String methods on the underlying // type the passed reflect.Value represents and outputes the result to Writer w. // // It handles panics in any called methods by catching and displaying the error // as the formatted value. func handleMethods(cs *ConfigState, w io.Writer, v reflect.Value) (handled bool) { // We need an interface to check if the type implements the error or // Stringer interface. However, the reflect package won't give us an // interface on certain things like unexported struct fields in order // to enforce visibility rules. We use unsafe, when it's available, // to bypass these restrictions since this package does not mutate the // values. if !v.CanInterface() { if UnsafeDisabled { return false } v = unsafeReflectValue(v) } // Choose whether or not to do error and Stringer interface lookups against // the base type or a pointer to the base type depending on settings. // Technically calling one of these methods with a pointer receiver can // mutate the value, however, types which choose to satisify an error or // Stringer interface with a pointer receiver should not be mutating their // state inside these interface methods. if !cs.DisablePointerMethods && !UnsafeDisabled && !v.CanAddr() { v = unsafeReflectValue(v) } if v.CanAddr() { v = v.Addr() } // Is it an error or Stringer? switch iface := v.Interface().(type) { case error: defer catchPanic(w, v) if cs.ContinueOnMethod { w.Write(openParenBytes) w.Write([]byte(iface.Error())) w.Write(closeParenBytes) w.Write(spaceBytes) return false } w.Write([]byte(iface.Error())) return true case fmt.Stringer: defer catchPanic(w, v) if cs.ContinueOnMethod { w.Write(openParenBytes) w.Write([]byte(iface.String())) w.Write(closeParenBytes) w.Write(spaceBytes) return false } w.Write([]byte(iface.String())) return true } return false } // printBool outputs a boolean value as true or false to Writer w. func printBool(w io.Writer, val bool) { if val { w.Write(trueBytes) } else { w.Write(falseBytes) } } // printInt outputs a signed integer value to Writer w. func printInt(w io.Writer, val int64, base int) { w.Write([]byte(strconv.FormatInt(val, base))) } // printUint outputs an unsigned integer value to Writer w. func printUint(w io.Writer, val uint64, base int) { w.Write([]byte(strconv.FormatUint(val, base))) } // printFloat outputs a floating point value using the specified precision, // which is expected to be 32 or 64bit, to Writer w. func printFloat(w io.Writer, val float64, precision int) { w.Write([]byte(strconv.FormatFloat(val, 'g', -1, precision))) } // printComplex outputs a complex value using the specified float precision // for the real and imaginary parts to Writer w. func printComplex(w io.Writer, c complex128, floatPrecision int) { r := real(c) w.Write(openParenBytes) w.Write([]byte(strconv.FormatFloat(r, 'g', -1, floatPrecision))) i := imag(c) if i >= 0 { w.Write(plusBytes) } w.Write([]byte(strconv.FormatFloat(i, 'g', -1, floatPrecision))) w.Write(iBytes) w.Write(closeParenBytes) } // printHexPtr outputs a uintptr formatted as hexadecimal with a leading '0x' // prefix to Writer w. func printHexPtr(w io.Writer, p uintptr) { // Null pointer. num := uint64(p) if num == 0 { w.Write(nilAngleBytes) return } // Max uint64 is 16 bytes in hex + 2 bytes for '0x' prefix buf := make([]byte, 18) // It's simpler to construct the hex string right to left. base := uint64(16) i := len(buf) - 1 for num >= base { buf[i] = hexDigits[num%base] num /= base i-- } buf[i] = hexDigits[num] // Add '0x' prefix. i-- buf[i] = 'x' i-- buf[i] = '0' // Strip unused leading bytes. buf = buf[i:] w.Write(buf) } // valuesSorter implements sort.Interface to allow a slice of reflect.Value // elements to be sorted. type valuesSorter struct { values []reflect.Value strings []string // either nil or same len and values cs *ConfigState } // newValuesSorter initializes a valuesSorter instance, which holds a set of // surrogate keys on which the data should be sorted. It uses flags in // ConfigState to decide if and how to populate those surrogate keys. func newValuesSorter(values []reflect.Value, cs *ConfigState) sort.Interface { vs := &valuesSorter{values: values, cs: cs} if canSortSimply(vs.values[0].Kind()) { return vs } if !cs.DisableMethods { vs.strings = make([]string, len(values)) for i := range vs.values { b := bytes.Buffer{} if !handleMethods(cs, &b, vs.values[i]) { vs.strings = nil break } vs.strings[i] = b.String() } } if vs.strings == nil && cs.SpewKeys { vs.strings = make([]string, len(values)) for i := range vs.values { vs.strings[i] = Sprintf("%#v", vs.values[i].Interface()) } } return vs } // canSortSimply tests whether a reflect.Kind is a primitive that can be sorted // directly, or whether it should be considered for sorting by surrogate keys // (if the ConfigState allows it). func canSortSimply(kind reflect.Kind) bool { // This switch parallels valueSortLess, except for the default case. switch kind { case reflect.Bool: return true case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: return true case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: return true case reflect.Float32, reflect.Float64: return true case reflect.String: return true case reflect.Uintptr: return true case reflect.Array: return true } return false } // Len returns the number of values in the slice. It is part of the // sort.Interface implementation. func (s *valuesSorter) Len() int { return len(s.values) } // Swap swaps the values at the passed indices. It is part of the // sort.Interface implementation. func (s *valuesSorter) Swap(i, j int) { s.values[i], s.values[j] = s.values[j], s.values[i] if s.strings != nil { s.strings[i], s.strings[j] = s.strings[j], s.strings[i] } } // valueSortLess returns whether the first value should sort before the second // value. It is used by valueSorter.Less as part of the sort.Interface // implementation. func valueSortLess(a, b reflect.Value) bool { switch a.Kind() { case reflect.Bool: return !a.Bool() && b.Bool() case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: return a.Int() < b.Int() case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: return a.Uint() < b.Uint() case reflect.Float32, reflect.Float64: return a.Float() < b.Float() case reflect.String: return a.String() < b.String() case reflect.Uintptr: return a.Uint() < b.Uint() case reflect.Array: // Compare the contents of both arrays. l := a.Len() for i := 0; i < l; i++ { av := a.Index(i) bv := b.Index(i) if av.Interface() == bv.Interface() { continue } return valueSortLess(av, bv) } } return a.String() < b.String() } // Less returns whether the value at index i should sort before the // value at index j. It is part of the sort.Interface implementation. func (s *valuesSorter) Less(i, j int) bool { if s.strings == nil { return valueSortLess(s.values[i], s.values[j]) } return s.strings[i] < s.strings[j] } // sortValues is a sort function that handles both native types and any type that // can be converted to error or Stringer. Other inputs are sorted according to // their Value.String() value to ensure display stability. func sortValues(values []reflect.Value, cs *ConfigState) { if len(values) == 0 { return } sort.Sort(newValuesSorter(values, cs)) } ================================================ FILE: vendor/github.com/davecgh/go-spew/spew/config.go ================================================ /* * Copyright (c) 2013-2016 Dave Collins * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ package spew import ( "bytes" "fmt" "io" "os" ) // ConfigState houses the configuration options used by spew to format and // display values. There is a global instance, Config, that is used to control // all top-level Formatter and Dump functionality. Each ConfigState instance // provides methods equivalent to the top-level functions. // // The zero value for ConfigState provides no indentation. You would typically // want to set it to a space or a tab. // // Alternatively, you can use NewDefaultConfig to get a ConfigState instance // with default settings. See the documentation of NewDefaultConfig for default // values. type ConfigState struct { // Indent specifies the string to use for each indentation level. The // global config instance that all top-level functions use set this to a // single space by default. If you would like more indentation, you might // set this to a tab with "\t" or perhaps two spaces with " ". Indent string // MaxDepth controls the maximum number of levels to descend into nested // data structures. The default, 0, means there is no limit. // // NOTE: Circular data structures are properly detected, so it is not // necessary to set this value unless you specifically want to limit deeply // nested data structures. MaxDepth int // DisableMethods specifies whether or not error and Stringer interfaces are // invoked for types that implement them. DisableMethods bool // DisablePointerMethods specifies whether or not to check for and invoke // error and Stringer interfaces on types which only accept a pointer // receiver when the current type is not a pointer. // // NOTE: This might be an unsafe action since calling one of these methods // with a pointer receiver could technically mutate the value, however, // in practice, types which choose to satisify an error or Stringer // interface with a pointer receiver should not be mutating their state // inside these interface methods. As a result, this option relies on // access to the unsafe package, so it will not have any effect when // running in environments without access to the unsafe package such as // Google App Engine or with the "safe" build tag specified. DisablePointerMethods bool // DisablePointerAddresses specifies whether to disable the printing of // pointer addresses. This is useful when diffing data structures in tests. DisablePointerAddresses bool // DisableCapacities specifies whether to disable the printing of capacities // for arrays, slices, maps and channels. This is useful when diffing // data structures in tests. DisableCapacities bool // ContinueOnMethod specifies whether or not recursion should continue once // a custom error or Stringer interface is invoked. The default, false, // means it will print the results of invoking the custom error or Stringer // interface and return immediately instead of continuing to recurse into // the internals of the data type. // // NOTE: This flag does not have any effect if method invocation is disabled // via the DisableMethods or DisablePointerMethods options. ContinueOnMethod bool // SortKeys specifies map keys should be sorted before being printed. Use // this to have a more deterministic, diffable output. Note that only // native types (bool, int, uint, floats, uintptr and string) and types // that support the error or Stringer interfaces (if methods are // enabled) are supported, with other types sorted according to the // reflect.Value.String() output which guarantees display stability. SortKeys bool // SpewKeys specifies that, as a last resort attempt, map keys should // be spewed to strings and sorted by those strings. This is only // considered if SortKeys is true. SpewKeys bool } // Config is the active configuration of the top-level functions. // The configuration can be changed by modifying the contents of spew.Config. var Config = ConfigState{Indent: " "} // Errorf is a wrapper for fmt.Errorf that treats each argument as if it were // passed with a Formatter interface returned by c.NewFormatter. It returns // the formatted string as a value that satisfies error. See NewFormatter // for formatting details. // // This function is shorthand for the following syntax: // // fmt.Errorf(format, c.NewFormatter(a), c.NewFormatter(b)) func (c *ConfigState) Errorf(format string, a ...interface{}) (err error) { return fmt.Errorf(format, c.convertArgs(a)...) } // Fprint is a wrapper for fmt.Fprint that treats each argument as if it were // passed with a Formatter interface returned by c.NewFormatter. It returns // the number of bytes written and any write error encountered. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Fprint(w, c.NewFormatter(a), c.NewFormatter(b)) func (c *ConfigState) Fprint(w io.Writer, a ...interface{}) (n int, err error) { return fmt.Fprint(w, c.convertArgs(a)...) } // Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were // passed with a Formatter interface returned by c.NewFormatter. It returns // the number of bytes written and any write error encountered. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Fprintf(w, format, c.NewFormatter(a), c.NewFormatter(b)) func (c *ConfigState) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { return fmt.Fprintf(w, format, c.convertArgs(a)...) } // Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it // passed with a Formatter interface returned by c.NewFormatter. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Fprintln(w, c.NewFormatter(a), c.NewFormatter(b)) func (c *ConfigState) Fprintln(w io.Writer, a ...interface{}) (n int, err error) { return fmt.Fprintln(w, c.convertArgs(a)...) } // Print is a wrapper for fmt.Print that treats each argument as if it were // passed with a Formatter interface returned by c.NewFormatter. It returns // the number of bytes written and any write error encountered. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Print(c.NewFormatter(a), c.NewFormatter(b)) func (c *ConfigState) Print(a ...interface{}) (n int, err error) { return fmt.Print(c.convertArgs(a)...) } // Printf is a wrapper for fmt.Printf that treats each argument as if it were // passed with a Formatter interface returned by c.NewFormatter. It returns // the number of bytes written and any write error encountered. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Printf(format, c.NewFormatter(a), c.NewFormatter(b)) func (c *ConfigState) Printf(format string, a ...interface{}) (n int, err error) { return fmt.Printf(format, c.convertArgs(a)...) } // Println is a wrapper for fmt.Println that treats each argument as if it were // passed with a Formatter interface returned by c.NewFormatter. It returns // the number of bytes written and any write error encountered. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Println(c.NewFormatter(a), c.NewFormatter(b)) func (c *ConfigState) Println(a ...interface{}) (n int, err error) { return fmt.Println(c.convertArgs(a)...) } // Sprint is a wrapper for fmt.Sprint that treats each argument as if it were // passed with a Formatter interface returned by c.NewFormatter. It returns // the resulting string. See NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Sprint(c.NewFormatter(a), c.NewFormatter(b)) func (c *ConfigState) Sprint(a ...interface{}) string { return fmt.Sprint(c.convertArgs(a)...) } // Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were // passed with a Formatter interface returned by c.NewFormatter. It returns // the resulting string. See NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Sprintf(format, c.NewFormatter(a), c.NewFormatter(b)) func (c *ConfigState) Sprintf(format string, a ...interface{}) string { return fmt.Sprintf(format, c.convertArgs(a)...) } // Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it // were passed with a Formatter interface returned by c.NewFormatter. It // returns the resulting string. See NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Sprintln(c.NewFormatter(a), c.NewFormatter(b)) func (c *ConfigState) Sprintln(a ...interface{}) string { return fmt.Sprintln(c.convertArgs(a)...) } /* NewFormatter returns a custom formatter that satisfies the fmt.Formatter interface. As a result, it integrates cleanly with standard fmt package printing functions. The formatter is useful for inline printing of smaller data types similar to the standard %v format specifier. The custom formatter only responds to the %v (most compact), %+v (adds pointer addresses), %#v (adds types), and %#+v (adds types and pointer addresses) verb combinations. Any other verbs such as %x and %q will be sent to the the standard fmt package for formatting. In addition, the custom formatter ignores the width and precision arguments (however they will still work on the format specifiers not handled by the custom formatter). Typically this function shouldn't be called directly. It is much easier to make use of the custom formatter by calling one of the convenience functions such as c.Printf, c.Println, or c.Printf. */ func (c *ConfigState) NewFormatter(v interface{}) fmt.Formatter { return newFormatter(c, v) } // Fdump formats and displays the passed arguments to io.Writer w. It formats // exactly the same as Dump. func (c *ConfigState) Fdump(w io.Writer, a ...interface{}) { fdump(c, w, a...) } /* Dump displays the passed parameters to standard out with newlines, customizable indentation, and additional debug information such as complete types and all pointer addresses used to indirect to the final value. It provides the following features over the built-in printing facilities provided by the fmt package: * Pointers are dereferenced and followed * Circular data structures are detected and handled properly * Custom Stringer/error interfaces are optionally invoked, including on unexported types * Custom types which only implement the Stringer/error interfaces via a pointer receiver are optionally invoked when passing non-pointer variables * Byte arrays and slices are dumped like the hexdump -C command which includes offsets, byte values in hex, and ASCII output The configuration options are controlled by modifying the public members of c. See ConfigState for options documentation. See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to get the formatted result as a string. */ func (c *ConfigState) Dump(a ...interface{}) { fdump(c, os.Stdout, a...) } // Sdump returns a string with the passed arguments formatted exactly the same // as Dump. func (c *ConfigState) Sdump(a ...interface{}) string { var buf bytes.Buffer fdump(c, &buf, a...) return buf.String() } // convertArgs accepts a slice of arguments and returns a slice of the same // length with each argument converted to a spew Formatter interface using // the ConfigState associated with s. func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{}) { formatters = make([]interface{}, len(args)) for index, arg := range args { formatters[index] = newFormatter(c, arg) } return formatters } // NewDefaultConfig returns a ConfigState with the following default settings. // // Indent: " " // MaxDepth: 0 // DisableMethods: false // DisablePointerMethods: false // ContinueOnMethod: false // SortKeys: false func NewDefaultConfig() *ConfigState { return &ConfigState{Indent: " "} } ================================================ FILE: vendor/github.com/davecgh/go-spew/spew/doc.go ================================================ /* * Copyright (c) 2013-2016 Dave Collins * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ /* Package spew implements a deep pretty printer for Go data structures to aid in debugging. A quick overview of the additional features spew provides over the built-in printing facilities for Go data types are as follows: * Pointers are dereferenced and followed * Circular data structures are detected and handled properly * Custom Stringer/error interfaces are optionally invoked, including on unexported types * Custom types which only implement the Stringer/error interfaces via a pointer receiver are optionally invoked when passing non-pointer variables * Byte arrays and slices are dumped like the hexdump -C command which includes offsets, byte values in hex, and ASCII output (only when using Dump style) There are two different approaches spew allows for dumping Go data structures: * Dump style which prints with newlines, customizable indentation, and additional debug information such as types and all pointer addresses used to indirect to the final value * A custom Formatter interface that integrates cleanly with the standard fmt package and replaces %v, %+v, %#v, and %#+v to provide inline printing similar to the default %v while providing the additional functionality outlined above and passing unsupported format verbs such as %x and %q along to fmt Quick Start This section demonstrates how to quickly get started with spew. See the sections below for further details on formatting and configuration options. To dump a variable with full newlines, indentation, type, and pointer information use Dump, Fdump, or Sdump: spew.Dump(myVar1, myVar2, ...) spew.Fdump(someWriter, myVar1, myVar2, ...) str := spew.Sdump(myVar1, myVar2, ...) Alternatively, if you would prefer to use format strings with a compacted inline printing style, use the convenience wrappers Printf, Fprintf, etc with %v (most compact), %+v (adds pointer addresses), %#v (adds types), or %#+v (adds types and pointer addresses): spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2) spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2) spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) Configuration Options Configuration of spew is handled by fields in the ConfigState type. For convenience, all of the top-level functions use a global state available via the spew.Config global. It is also possible to create a ConfigState instance that provides methods equivalent to the top-level functions. This allows concurrent configuration options. See the ConfigState documentation for more details. The following configuration options are available: * Indent String to use for each indentation level for Dump functions. It is a single space by default. A popular alternative is "\t". * MaxDepth Maximum number of levels to descend into nested data structures. There is no limit by default. * DisableMethods Disables invocation of error and Stringer interface methods. Method invocation is enabled by default. * DisablePointerMethods Disables invocation of error and Stringer interface methods on types which only accept pointer receivers from non-pointer variables. Pointer method invocation is enabled by default. * DisablePointerAddresses DisablePointerAddresses specifies whether to disable the printing of pointer addresses. This is useful when diffing data structures in tests. * DisableCapacities DisableCapacities specifies whether to disable the printing of capacities for arrays, slices, maps and channels. This is useful when diffing data structures in tests. * ContinueOnMethod Enables recursion into types after invoking error and Stringer interface methods. Recursion after method invocation is disabled by default. * SortKeys Specifies map keys should be sorted before being printed. Use this to have a more deterministic, diffable output. Note that only native types (bool, int, uint, floats, uintptr and string) and types which implement error or Stringer interfaces are supported with other types sorted according to the reflect.Value.String() output which guarantees display stability. Natural map order is used by default. * SpewKeys Specifies that, as a last resort attempt, map keys should be spewed to strings and sorted by those strings. This is only considered if SortKeys is true. Dump Usage Simply call spew.Dump with a list of variables you want to dump: spew.Dump(myVar1, myVar2, ...) You may also call spew.Fdump if you would prefer to output to an arbitrary io.Writer. For example, to dump to standard error: spew.Fdump(os.Stderr, myVar1, myVar2, ...) A third option is to call spew.Sdump to get the formatted output as a string: str := spew.Sdump(myVar1, myVar2, ...) Sample Dump Output See the Dump example for details on the setup of the types and variables being shown here. (main.Foo) { unexportedField: (*main.Bar)(0xf84002e210)({ flag: (main.Flag) flagTwo, data: (uintptr) }), ExportedField: (map[interface {}]interface {}) (len=1) { (string) (len=3) "one": (bool) true } } Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C command as shown. ([]uint8) (len=32 cap=32) { 00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... | 00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0| 00000020 31 32 |12| } Custom Formatter Spew provides a custom formatter that implements the fmt.Formatter interface so that it integrates cleanly with standard fmt package printing functions. The formatter is useful for inline printing of smaller data types similar to the standard %v format specifier. The custom formatter only responds to the %v (most compact), %+v (adds pointer addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb combinations. Any other verbs such as %x and %q will be sent to the the standard fmt package for formatting. In addition, the custom formatter ignores the width and precision arguments (however they will still work on the format specifiers not handled by the custom formatter). Custom Formatter Usage The simplest way to make use of the spew custom formatter is to call one of the convenience functions such as spew.Printf, spew.Println, or spew.Printf. The functions have syntax you are most likely already familiar with: spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2) spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) spew.Println(myVar, myVar2) spew.Fprintf(os.Stderr, "myVar1: %v -- myVar2: %+v", myVar1, myVar2) spew.Fprintf(os.Stderr, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) See the Index for the full list convenience functions. Sample Formatter Output Double pointer to a uint8: %v: <**>5 %+v: <**>(0xf8400420d0->0xf8400420c8)5 %#v: (**uint8)5 %#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5 Pointer to circular struct with a uint8 field and a pointer to itself: %v: <*>{1 <*>} %+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)} %#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)} %#+v: (*main.circular)(0xf84003e260){ui8:(uint8)1 c:(*main.circular)(0xf84003e260)} See the Printf example for details on the setup of variables being shown here. Errors Since it is possible for custom Stringer/error interfaces to panic, spew detects them and handles them internally by printing the panic information inline with the output. Since spew is intended to provide deep pretty printing capabilities on structures, it intentionally does not return any errors. */ package spew ================================================ FILE: vendor/github.com/davecgh/go-spew/spew/dump.go ================================================ /* * Copyright (c) 2013-2016 Dave Collins * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ package spew import ( "bytes" "encoding/hex" "fmt" "io" "os" "reflect" "regexp" "strconv" "strings" ) var ( // uint8Type is a reflect.Type representing a uint8. It is used to // convert cgo types to uint8 slices for hexdumping. uint8Type = reflect.TypeOf(uint8(0)) // cCharRE is a regular expression that matches a cgo char. // It is used to detect character arrays to hexdump them. cCharRE = regexp.MustCompile(`^.*\._Ctype_char$`) // cUnsignedCharRE is a regular expression that matches a cgo unsigned // char. It is used to detect unsigned character arrays to hexdump // them. cUnsignedCharRE = regexp.MustCompile(`^.*\._Ctype_unsignedchar$`) // cUint8tCharRE is a regular expression that matches a cgo uint8_t. // It is used to detect uint8_t arrays to hexdump them. cUint8tCharRE = regexp.MustCompile(`^.*\._Ctype_uint8_t$`) ) // dumpState contains information about the state of a dump operation. type dumpState struct { w io.Writer depth int pointers map[uintptr]int ignoreNextType bool ignoreNextIndent bool cs *ConfigState } // indent performs indentation according to the depth level and cs.Indent // option. func (d *dumpState) indent() { if d.ignoreNextIndent { d.ignoreNextIndent = false return } d.w.Write(bytes.Repeat([]byte(d.cs.Indent), d.depth)) } // unpackValue returns values inside of non-nil interfaces when possible. // This is useful for data types like structs, arrays, slices, and maps which // can contain varying types packed inside an interface. func (d *dumpState) unpackValue(v reflect.Value) reflect.Value { if v.Kind() == reflect.Interface && !v.IsNil() { v = v.Elem() } return v } // dumpPtr handles formatting of pointers by indirecting them as necessary. func (d *dumpState) dumpPtr(v reflect.Value) { // Remove pointers at or below the current depth from map used to detect // circular refs. for k, depth := range d.pointers { if depth >= d.depth { delete(d.pointers, k) } } // Keep list of all dereferenced pointers to show later. pointerChain := make([]uintptr, 0) // Figure out how many levels of indirection there are by dereferencing // pointers and unpacking interfaces down the chain while detecting circular // references. nilFound := false cycleFound := false indirects := 0 ve := v for ve.Kind() == reflect.Ptr { if ve.IsNil() { nilFound = true break } indirects++ addr := ve.Pointer() pointerChain = append(pointerChain, addr) if pd, ok := d.pointers[addr]; ok && pd < d.depth { cycleFound = true indirects-- break } d.pointers[addr] = d.depth ve = ve.Elem() if ve.Kind() == reflect.Interface { if ve.IsNil() { nilFound = true break } ve = ve.Elem() } } // Display type information. d.w.Write(openParenBytes) d.w.Write(bytes.Repeat(asteriskBytes, indirects)) d.w.Write([]byte(ve.Type().String())) d.w.Write(closeParenBytes) // Display pointer information. if !d.cs.DisablePointerAddresses && len(pointerChain) > 0 { d.w.Write(openParenBytes) for i, addr := range pointerChain { if i > 0 { d.w.Write(pointerChainBytes) } printHexPtr(d.w, addr) } d.w.Write(closeParenBytes) } // Display dereferenced value. d.w.Write(openParenBytes) switch { case nilFound: d.w.Write(nilAngleBytes) case cycleFound: d.w.Write(circularBytes) default: d.ignoreNextType = true d.dump(ve) } d.w.Write(closeParenBytes) } // dumpSlice handles formatting of arrays and slices. Byte (uint8 under // reflection) arrays and slices are dumped in hexdump -C fashion. func (d *dumpState) dumpSlice(v reflect.Value) { // Determine whether this type should be hex dumped or not. Also, // for types which should be hexdumped, try to use the underlying data // first, then fall back to trying to convert them to a uint8 slice. var buf []uint8 doConvert := false doHexDump := false numEntries := v.Len() if numEntries > 0 { vt := v.Index(0).Type() vts := vt.String() switch { // C types that need to be converted. case cCharRE.MatchString(vts): fallthrough case cUnsignedCharRE.MatchString(vts): fallthrough case cUint8tCharRE.MatchString(vts): doConvert = true // Try to use existing uint8 slices and fall back to converting // and copying if that fails. case vt.Kind() == reflect.Uint8: // We need an addressable interface to convert the type // to a byte slice. However, the reflect package won't // give us an interface on certain things like // unexported struct fields in order to enforce // visibility rules. We use unsafe, when available, to // bypass these restrictions since this package does not // mutate the values. vs := v if !vs.CanInterface() || !vs.CanAddr() { vs = unsafeReflectValue(vs) } if !UnsafeDisabled { vs = vs.Slice(0, numEntries) // Use the existing uint8 slice if it can be // type asserted. iface := vs.Interface() if slice, ok := iface.([]uint8); ok { buf = slice doHexDump = true break } } // The underlying data needs to be converted if it can't // be type asserted to a uint8 slice. doConvert = true } // Copy and convert the underlying type if needed. if doConvert && vt.ConvertibleTo(uint8Type) { // Convert and copy each element into a uint8 byte // slice. buf = make([]uint8, numEntries) for i := 0; i < numEntries; i++ { vv := v.Index(i) buf[i] = uint8(vv.Convert(uint8Type).Uint()) } doHexDump = true } } // Hexdump the entire slice as needed. if doHexDump { indent := strings.Repeat(d.cs.Indent, d.depth) str := indent + hex.Dump(buf) str = strings.Replace(str, "\n", "\n"+indent, -1) str = strings.TrimRight(str, d.cs.Indent) d.w.Write([]byte(str)) return } // Recursively call dump for each item. for i := 0; i < numEntries; i++ { d.dump(d.unpackValue(v.Index(i))) if i < (numEntries - 1) { d.w.Write(commaNewlineBytes) } else { d.w.Write(newlineBytes) } } } // dump is the main workhorse for dumping a value. It uses the passed reflect // value to figure out what kind of object we are dealing with and formats it // appropriately. It is a recursive function, however circular data structures // are detected and handled properly. func (d *dumpState) dump(v reflect.Value) { // Handle invalid reflect values immediately. kind := v.Kind() if kind == reflect.Invalid { d.w.Write(invalidAngleBytes) return } // Handle pointers specially. if kind == reflect.Ptr { d.indent() d.dumpPtr(v) return } // Print type information unless already handled elsewhere. if !d.ignoreNextType { d.indent() d.w.Write(openParenBytes) d.w.Write([]byte(v.Type().String())) d.w.Write(closeParenBytes) d.w.Write(spaceBytes) } d.ignoreNextType = false // Display length and capacity if the built-in len and cap functions // work with the value's kind and the len/cap itself is non-zero. valueLen, valueCap := 0, 0 switch v.Kind() { case reflect.Array, reflect.Slice, reflect.Chan: valueLen, valueCap = v.Len(), v.Cap() case reflect.Map, reflect.String: valueLen = v.Len() } if valueLen != 0 || !d.cs.DisableCapacities && valueCap != 0 { d.w.Write(openParenBytes) if valueLen != 0 { d.w.Write(lenEqualsBytes) printInt(d.w, int64(valueLen), 10) } if !d.cs.DisableCapacities && valueCap != 0 { if valueLen != 0 { d.w.Write(spaceBytes) } d.w.Write(capEqualsBytes) printInt(d.w, int64(valueCap), 10) } d.w.Write(closeParenBytes) d.w.Write(spaceBytes) } // Call Stringer/error interfaces if they exist and the handle methods flag // is enabled if !d.cs.DisableMethods { if (kind != reflect.Invalid) && (kind != reflect.Interface) { if handled := handleMethods(d.cs, d.w, v); handled { return } } } switch kind { case reflect.Invalid: // Do nothing. We should never get here since invalid has already // been handled above. case reflect.Bool: printBool(d.w, v.Bool()) case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: printInt(d.w, v.Int(), 10) case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: printUint(d.w, v.Uint(), 10) case reflect.Float32: printFloat(d.w, v.Float(), 32) case reflect.Float64: printFloat(d.w, v.Float(), 64) case reflect.Complex64: printComplex(d.w, v.Complex(), 32) case reflect.Complex128: printComplex(d.w, v.Complex(), 64) case reflect.Slice: if v.IsNil() { d.w.Write(nilAngleBytes) break } fallthrough case reflect.Array: d.w.Write(openBraceNewlineBytes) d.depth++ if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) { d.indent() d.w.Write(maxNewlineBytes) } else { d.dumpSlice(v) } d.depth-- d.indent() d.w.Write(closeBraceBytes) case reflect.String: d.w.Write([]byte(strconv.Quote(v.String()))) case reflect.Interface: // The only time we should get here is for nil interfaces due to // unpackValue calls. if v.IsNil() { d.w.Write(nilAngleBytes) } case reflect.Ptr: // Do nothing. We should never get here since pointers have already // been handled above. case reflect.Map: // nil maps should be indicated as different than empty maps if v.IsNil() { d.w.Write(nilAngleBytes) break } d.w.Write(openBraceNewlineBytes) d.depth++ if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) { d.indent() d.w.Write(maxNewlineBytes) } else { numEntries := v.Len() keys := v.MapKeys() if d.cs.SortKeys { sortValues(keys, d.cs) } for i, key := range keys { d.dump(d.unpackValue(key)) d.w.Write(colonSpaceBytes) d.ignoreNextIndent = true d.dump(d.unpackValue(v.MapIndex(key))) if i < (numEntries - 1) { d.w.Write(commaNewlineBytes) } else { d.w.Write(newlineBytes) } } } d.depth-- d.indent() d.w.Write(closeBraceBytes) case reflect.Struct: d.w.Write(openBraceNewlineBytes) d.depth++ if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) { d.indent() d.w.Write(maxNewlineBytes) } else { vt := v.Type() numFields := v.NumField() for i := 0; i < numFields; i++ { d.indent() vtf := vt.Field(i) d.w.Write([]byte(vtf.Name)) d.w.Write(colonSpaceBytes) d.ignoreNextIndent = true d.dump(d.unpackValue(v.Field(i))) if i < (numFields - 1) { d.w.Write(commaNewlineBytes) } else { d.w.Write(newlineBytes) } } } d.depth-- d.indent() d.w.Write(closeBraceBytes) case reflect.Uintptr: printHexPtr(d.w, uintptr(v.Uint())) case reflect.UnsafePointer, reflect.Chan, reflect.Func: printHexPtr(d.w, v.Pointer()) // There were not any other types at the time this code was written, but // fall back to letting the default fmt package handle it in case any new // types are added. default: if v.CanInterface() { fmt.Fprintf(d.w, "%v", v.Interface()) } else { fmt.Fprintf(d.w, "%v", v.String()) } } } // fdump is a helper function to consolidate the logic from the various public // methods which take varying writers and config states. func fdump(cs *ConfigState, w io.Writer, a ...interface{}) { for _, arg := range a { if arg == nil { w.Write(interfaceBytes) w.Write(spaceBytes) w.Write(nilAngleBytes) w.Write(newlineBytes) continue } d := dumpState{w: w, cs: cs} d.pointers = make(map[uintptr]int) d.dump(reflect.ValueOf(arg)) d.w.Write(newlineBytes) } } // Fdump formats and displays the passed arguments to io.Writer w. It formats // exactly the same as Dump. func Fdump(w io.Writer, a ...interface{}) { fdump(&Config, w, a...) } // Sdump returns a string with the passed arguments formatted exactly the same // as Dump. func Sdump(a ...interface{}) string { var buf bytes.Buffer fdump(&Config, &buf, a...) return buf.String() } /* Dump displays the passed parameters to standard out with newlines, customizable indentation, and additional debug information such as complete types and all pointer addresses used to indirect to the final value. It provides the following features over the built-in printing facilities provided by the fmt package: * Pointers are dereferenced and followed * Circular data structures are detected and handled properly * Custom Stringer/error interfaces are optionally invoked, including on unexported types * Custom types which only implement the Stringer/error interfaces via a pointer receiver are optionally invoked when passing non-pointer variables * Byte arrays and slices are dumped like the hexdump -C command which includes offsets, byte values in hex, and ASCII output The configuration options are controlled by an exported package global, spew.Config. See ConfigState for options documentation. See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to get the formatted result as a string. */ func Dump(a ...interface{}) { fdump(&Config, os.Stdout, a...) } ================================================ FILE: vendor/github.com/davecgh/go-spew/spew/format.go ================================================ /* * Copyright (c) 2013-2016 Dave Collins * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ package spew import ( "bytes" "fmt" "reflect" "strconv" "strings" ) // supportedFlags is a list of all the character flags supported by fmt package. const supportedFlags = "0-+# " // formatState implements the fmt.Formatter interface and contains information // about the state of a formatting operation. The NewFormatter function can // be used to get a new Formatter which can be used directly as arguments // in standard fmt package printing calls. type formatState struct { value interface{} fs fmt.State depth int pointers map[uintptr]int ignoreNextType bool cs *ConfigState } // buildDefaultFormat recreates the original format string without precision // and width information to pass in to fmt.Sprintf in the case of an // unrecognized type. Unless new types are added to the language, this // function won't ever be called. func (f *formatState) buildDefaultFormat() (format string) { buf := bytes.NewBuffer(percentBytes) for _, flag := range supportedFlags { if f.fs.Flag(int(flag)) { buf.WriteRune(flag) } } buf.WriteRune('v') format = buf.String() return format } // constructOrigFormat recreates the original format string including precision // and width information to pass along to the standard fmt package. This allows // automatic deferral of all format strings this package doesn't support. func (f *formatState) constructOrigFormat(verb rune) (format string) { buf := bytes.NewBuffer(percentBytes) for _, flag := range supportedFlags { if f.fs.Flag(int(flag)) { buf.WriteRune(flag) } } if width, ok := f.fs.Width(); ok { buf.WriteString(strconv.Itoa(width)) } if precision, ok := f.fs.Precision(); ok { buf.Write(precisionBytes) buf.WriteString(strconv.Itoa(precision)) } buf.WriteRune(verb) format = buf.String() return format } // unpackValue returns values inside of non-nil interfaces when possible and // ensures that types for values which have been unpacked from an interface // are displayed when the show types flag is also set. // This is useful for data types like structs, arrays, slices, and maps which // can contain varying types packed inside an interface. func (f *formatState) unpackValue(v reflect.Value) reflect.Value { if v.Kind() == reflect.Interface { f.ignoreNextType = false if !v.IsNil() { v = v.Elem() } } return v } // formatPtr handles formatting of pointers by indirecting them as necessary. func (f *formatState) formatPtr(v reflect.Value) { // Display nil if top level pointer is nil. showTypes := f.fs.Flag('#') if v.IsNil() && (!showTypes || f.ignoreNextType) { f.fs.Write(nilAngleBytes) return } // Remove pointers at or below the current depth from map used to detect // circular refs. for k, depth := range f.pointers { if depth >= f.depth { delete(f.pointers, k) } } // Keep list of all dereferenced pointers to possibly show later. pointerChain := make([]uintptr, 0) // Figure out how many levels of indirection there are by derferencing // pointers and unpacking interfaces down the chain while detecting circular // references. nilFound := false cycleFound := false indirects := 0 ve := v for ve.Kind() == reflect.Ptr { if ve.IsNil() { nilFound = true break } indirects++ addr := ve.Pointer() pointerChain = append(pointerChain, addr) if pd, ok := f.pointers[addr]; ok && pd < f.depth { cycleFound = true indirects-- break } f.pointers[addr] = f.depth ve = ve.Elem() if ve.Kind() == reflect.Interface { if ve.IsNil() { nilFound = true break } ve = ve.Elem() } } // Display type or indirection level depending on flags. if showTypes && !f.ignoreNextType { f.fs.Write(openParenBytes) f.fs.Write(bytes.Repeat(asteriskBytes, indirects)) f.fs.Write([]byte(ve.Type().String())) f.fs.Write(closeParenBytes) } else { if nilFound || cycleFound { indirects += strings.Count(ve.Type().String(), "*") } f.fs.Write(openAngleBytes) f.fs.Write([]byte(strings.Repeat("*", indirects))) f.fs.Write(closeAngleBytes) } // Display pointer information depending on flags. if f.fs.Flag('+') && (len(pointerChain) > 0) { f.fs.Write(openParenBytes) for i, addr := range pointerChain { if i > 0 { f.fs.Write(pointerChainBytes) } printHexPtr(f.fs, addr) } f.fs.Write(closeParenBytes) } // Display dereferenced value. switch { case nilFound: f.fs.Write(nilAngleBytes) case cycleFound: f.fs.Write(circularShortBytes) default: f.ignoreNextType = true f.format(ve) } } // format is the main workhorse for providing the Formatter interface. It // uses the passed reflect value to figure out what kind of object we are // dealing with and formats it appropriately. It is a recursive function, // however circular data structures are detected and handled properly. func (f *formatState) format(v reflect.Value) { // Handle invalid reflect values immediately. kind := v.Kind() if kind == reflect.Invalid { f.fs.Write(invalidAngleBytes) return } // Handle pointers specially. if kind == reflect.Ptr { f.formatPtr(v) return } // Print type information unless already handled elsewhere. if !f.ignoreNextType && f.fs.Flag('#') { f.fs.Write(openParenBytes) f.fs.Write([]byte(v.Type().String())) f.fs.Write(closeParenBytes) } f.ignoreNextType = false // Call Stringer/error interfaces if they exist and the handle methods // flag is enabled. if !f.cs.DisableMethods { if (kind != reflect.Invalid) && (kind != reflect.Interface) { if handled := handleMethods(f.cs, f.fs, v); handled { return } } } switch kind { case reflect.Invalid: // Do nothing. We should never get here since invalid has already // been handled above. case reflect.Bool: printBool(f.fs, v.Bool()) case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: printInt(f.fs, v.Int(), 10) case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: printUint(f.fs, v.Uint(), 10) case reflect.Float32: printFloat(f.fs, v.Float(), 32) case reflect.Float64: printFloat(f.fs, v.Float(), 64) case reflect.Complex64: printComplex(f.fs, v.Complex(), 32) case reflect.Complex128: printComplex(f.fs, v.Complex(), 64) case reflect.Slice: if v.IsNil() { f.fs.Write(nilAngleBytes) break } fallthrough case reflect.Array: f.fs.Write(openBracketBytes) f.depth++ if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) { f.fs.Write(maxShortBytes) } else { numEntries := v.Len() for i := 0; i < numEntries; i++ { if i > 0 { f.fs.Write(spaceBytes) } f.ignoreNextType = true f.format(f.unpackValue(v.Index(i))) } } f.depth-- f.fs.Write(closeBracketBytes) case reflect.String: f.fs.Write([]byte(v.String())) case reflect.Interface: // The only time we should get here is for nil interfaces due to // unpackValue calls. if v.IsNil() { f.fs.Write(nilAngleBytes) } case reflect.Ptr: // Do nothing. We should never get here since pointers have already // been handled above. case reflect.Map: // nil maps should be indicated as different than empty maps if v.IsNil() { f.fs.Write(nilAngleBytes) break } f.fs.Write(openMapBytes) f.depth++ if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) { f.fs.Write(maxShortBytes) } else { keys := v.MapKeys() if f.cs.SortKeys { sortValues(keys, f.cs) } for i, key := range keys { if i > 0 { f.fs.Write(spaceBytes) } f.ignoreNextType = true f.format(f.unpackValue(key)) f.fs.Write(colonBytes) f.ignoreNextType = true f.format(f.unpackValue(v.MapIndex(key))) } } f.depth-- f.fs.Write(closeMapBytes) case reflect.Struct: numFields := v.NumField() f.fs.Write(openBraceBytes) f.depth++ if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) { f.fs.Write(maxShortBytes) } else { vt := v.Type() for i := 0; i < numFields; i++ { if i > 0 { f.fs.Write(spaceBytes) } vtf := vt.Field(i) if f.fs.Flag('+') || f.fs.Flag('#') { f.fs.Write([]byte(vtf.Name)) f.fs.Write(colonBytes) } f.format(f.unpackValue(v.Field(i))) } } f.depth-- f.fs.Write(closeBraceBytes) case reflect.Uintptr: printHexPtr(f.fs, uintptr(v.Uint())) case reflect.UnsafePointer, reflect.Chan, reflect.Func: printHexPtr(f.fs, v.Pointer()) // There were not any other types at the time this code was written, but // fall back to letting the default fmt package handle it if any get added. default: format := f.buildDefaultFormat() if v.CanInterface() { fmt.Fprintf(f.fs, format, v.Interface()) } else { fmt.Fprintf(f.fs, format, v.String()) } } } // Format satisfies the fmt.Formatter interface. See NewFormatter for usage // details. func (f *formatState) Format(fs fmt.State, verb rune) { f.fs = fs // Use standard formatting for verbs that are not v. if verb != 'v' { format := f.constructOrigFormat(verb) fmt.Fprintf(fs, format, f.value) return } if f.value == nil { if fs.Flag('#') { fs.Write(interfaceBytes) } fs.Write(nilAngleBytes) return } f.format(reflect.ValueOf(f.value)) } // newFormatter is a helper function to consolidate the logic from the various // public methods which take varying config states. func newFormatter(cs *ConfigState, v interface{}) fmt.Formatter { fs := &formatState{value: v, cs: cs} fs.pointers = make(map[uintptr]int) return fs } /* NewFormatter returns a custom formatter that satisfies the fmt.Formatter interface. As a result, it integrates cleanly with standard fmt package printing functions. The formatter is useful for inline printing of smaller data types similar to the standard %v format specifier. The custom formatter only responds to the %v (most compact), %+v (adds pointer addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb combinations. Any other verbs such as %x and %q will be sent to the the standard fmt package for formatting. In addition, the custom formatter ignores the width and precision arguments (however they will still work on the format specifiers not handled by the custom formatter). Typically this function shouldn't be called directly. It is much easier to make use of the custom formatter by calling one of the convenience functions such as Printf, Println, or Fprintf. */ func NewFormatter(v interface{}) fmt.Formatter { return newFormatter(&Config, v) } ================================================ FILE: vendor/github.com/davecgh/go-spew/spew/spew.go ================================================ /* * Copyright (c) 2013-2016 Dave Collins * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ package spew import ( "fmt" "io" ) // Errorf is a wrapper for fmt.Errorf that treats each argument as if it were // passed with a default Formatter interface returned by NewFormatter. It // returns the formatted string as a value that satisfies error. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Errorf(format, spew.NewFormatter(a), spew.NewFormatter(b)) func Errorf(format string, a ...interface{}) (err error) { return fmt.Errorf(format, convertArgs(a)...) } // Fprint is a wrapper for fmt.Fprint that treats each argument as if it were // passed with a default Formatter interface returned by NewFormatter. It // returns the number of bytes written and any write error encountered. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Fprint(w, spew.NewFormatter(a), spew.NewFormatter(b)) func Fprint(w io.Writer, a ...interface{}) (n int, err error) { return fmt.Fprint(w, convertArgs(a)...) } // Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were // passed with a default Formatter interface returned by NewFormatter. It // returns the number of bytes written and any write error encountered. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Fprintf(w, format, spew.NewFormatter(a), spew.NewFormatter(b)) func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { return fmt.Fprintf(w, format, convertArgs(a)...) } // Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it // passed with a default Formatter interface returned by NewFormatter. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Fprintln(w, spew.NewFormatter(a), spew.NewFormatter(b)) func Fprintln(w io.Writer, a ...interface{}) (n int, err error) { return fmt.Fprintln(w, convertArgs(a)...) } // Print is a wrapper for fmt.Print that treats each argument as if it were // passed with a default Formatter interface returned by NewFormatter. It // returns the number of bytes written and any write error encountered. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Print(spew.NewFormatter(a), spew.NewFormatter(b)) func Print(a ...interface{}) (n int, err error) { return fmt.Print(convertArgs(a)...) } // Printf is a wrapper for fmt.Printf that treats each argument as if it were // passed with a default Formatter interface returned by NewFormatter. It // returns the number of bytes written and any write error encountered. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Printf(format, spew.NewFormatter(a), spew.NewFormatter(b)) func Printf(format string, a ...interface{}) (n int, err error) { return fmt.Printf(format, convertArgs(a)...) } // Println is a wrapper for fmt.Println that treats each argument as if it were // passed with a default Formatter interface returned by NewFormatter. It // returns the number of bytes written and any write error encountered. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Println(spew.NewFormatter(a), spew.NewFormatter(b)) func Println(a ...interface{}) (n int, err error) { return fmt.Println(convertArgs(a)...) } // Sprint is a wrapper for fmt.Sprint that treats each argument as if it were // passed with a default Formatter interface returned by NewFormatter. It // returns the resulting string. See NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Sprint(spew.NewFormatter(a), spew.NewFormatter(b)) func Sprint(a ...interface{}) string { return fmt.Sprint(convertArgs(a)...) } // Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were // passed with a default Formatter interface returned by NewFormatter. It // returns the resulting string. See NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Sprintf(format, spew.NewFormatter(a), spew.NewFormatter(b)) func Sprintf(format string, a ...interface{}) string { return fmt.Sprintf(format, convertArgs(a)...) } // Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it // were passed with a default Formatter interface returned by NewFormatter. It // returns the resulting string. See NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Sprintln(spew.NewFormatter(a), spew.NewFormatter(b)) func Sprintln(a ...interface{}) string { return fmt.Sprintln(convertArgs(a)...) } // convertArgs accepts a slice of arguments and returns a slice of the same // length with each argument converted to a default spew Formatter interface. func convertArgs(args []interface{}) (formatters []interface{}) { formatters = make([]interface{}, len(args)) for index, arg := range args { formatters[index] = NewFormatter(arg) } return formatters } ================================================ FILE: vendor/github.com/disiqueira/gotree/v3/.gitignore ================================================ # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof .idea/ GoTree.iml ### Linux template *~ # temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden* # KDE directory preferences .directory # Linux trash folder which might appear on any partition or disk .Trash-* ### Windows template # Windows image file caches Thumbs.db ehthumbs.db # Folder config file Desktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msm *.msp # Windows shortcuts *.lnk ### JetBrains template # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff: .idea/workspace.xml .idea/tasks.xml .idea/dictionaries .idea/vcs.xml .idea/jsLibraryMappings.xml # Sensitive or high-churn files: .idea/dataSources.ids .idea/dataSources.xml .idea/dataSources.local.xml .idea/sqlDataSources.xml .idea/dynamic.xml .idea/uiDesigner.xml # Gradle: .idea/gradle.xml .idea/libraries # Mongo Explorer plugin: .idea/mongoSettings.xml ## File-based project format: *.iws ## Plugin-specific files: # IntelliJ /out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties ### Go template # Compiled Object files, Static and Dynamic libs (Shared Objects) # Folders # Architecture specific extensions/prefixes ### OSX template *.DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ================================================ FILE: vendor/github.com/disiqueira/gotree/v3/.travis.yml ================================================ language: go go_import_path: github.com/disiqueira/gotree git: depth: 1 env: - GO111MODULE=on - GO111MODULE=off go: [ 1.11.x, 1.12.x, 1.13.x ] os: [ linux, osx ] script: - go test -race -v ./... ================================================ FILE: vendor/github.com/disiqueira/gotree/v3/LICENSE ================================================ MIT License Copyright (c) 2017 Diego Siqueira Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: vendor/github.com/disiqueira/gotree/v3/README.md ================================================ # ![GoTree](https://rawgit.com/DiSiqueira/GoTree/master/gotree-logo.png) # GoTree ![Language Badge](https://img.shields.io/badge/Language-Go-blue.svg) ![Go Report](https://goreportcard.com/badge/github.com/DiSiqueira/GoTree) ![License Badge](https://img.shields.io/badge/License-MIT-blue.svg) ![Status Badge](https://img.shields.io/badge/Status-Beta-brightgreen.svg) [![GoDoc](https://godoc.org/github.com/DiSiqueira/GoTree?status.svg)](https://godoc.org/github.com/DiSiqueira/GoTree) [![Build Status](https://travis-ci.org/DiSiqueira/GoTree.svg?branch=master)](https://travis-ci.org/DiSiqueira/GoTree) Simple Go module to print tree structures in terminal. Heavily inpired by [The Tree Command for Linux][treecommand] The GoTree's goal is to be a simple tool providing a stupidly easy-to-use and fast way to print recursive structures. [treecommand]: http://mama.indstate.edu/users/ice/tree/ ## Project Status GoTree is on beta. Pull Requests [are welcome](https://github.com/DiSiqueira/GoTree#social-coding) ![](http://image.prntscr.com/image/2a0dbf0777454446b8083fb6a0dc51fe.png) ## Features - Very simple and fast code - Intuitive names - Easy to extend - Uses only native libs - STUPIDLY [EASY TO USE](https://github.com/DiSiqueira/GoTree#usage) ## Installation ### Go Get ```bash $ go get github.com/disiqueira/gotree ``` ## Usage ### Simple create, populate and print example ![](http://image.prntscr.com/image/dd2fe3737e6543f7b21941a6953598c2.png) ```golang package main import ( "fmt" "github.com/disiqueira/gotree" ) func main() { artist := gotree.New("Pantera") album := artist.Add("Far Beyond Driven") album.Add("5 minutes Alone") fmt.Println(artist.Print()) } ``` ## Contributing ### Bug Reports & Feature Requests Please use the [issue tracker](https://github.com/DiSiqueira/GoTree/issues) to report any bugs or file feature requests. ### Developing PRs are welcome. To begin developing, do this: ```bash $ git clone --recursive git@github.com:DiSiqueira/GoTree.git $ cd GoTree/ ``` ## Social Coding 1. Create an issue to discuss about your idea 2. [Fork it] (https://github.com/DiSiqueira/GoTree/fork) 3. Create your feature branch (`git checkout -b my-new-feature`) 4. Commit your changes (`git commit -am 'Add some feature'`) 5. Push to the branch (`git push origin my-new-feature`) 6. Create a new Pull Request 7. Profit! :white_check_mark: ## License The MIT License (MIT) Copyright (c) 2013-2018 Diego Siqueira Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: vendor/github.com/disiqueira/gotree/v3/_config.yml ================================================ theme: jekyll-theme-slate ================================================ FILE: vendor/github.com/disiqueira/gotree/v3/gotree.go ================================================ // Package gotree create and print tree. package gotree import ( "strings" ) const ( newLine = "\n" emptySpace = " " middleItem = "├── " continueItem = "│ " lastItem = "└── " ) type ( tree struct { text string items []Tree } // Tree is tree interface Tree interface { Add(text string) Tree AddTree(tree Tree) Items() []Tree Text() string Print() string } printer struct { } // Printer is printer interface Printer interface { Print(Tree) string } ) //New returns a new GoTree.Tree func New(text string) Tree { return &tree{ text: text, items: []Tree{}, } } //Add adds a node to the tree func (t *tree) Add(text string) Tree { n := New(text) t.items = append(t.items, n) return n } //AddTree adds a tree as an item func (t *tree) AddTree(tree Tree) { t.items = append(t.items, tree) } //Text returns the node's value func (t *tree) Text() string { return t.text } //Items returns all items in the tree func (t *tree) Items() []Tree { return t.items } //Print returns an visual representation of the tree func (t *tree) Print() string { return newPrinter().Print(t) } func newPrinter() Printer { return &printer{} } //Print prints a tree to a string func (p *printer) Print(t Tree) string { return t.Text() + newLine + p.printItems(t.Items(), []bool{}) } func (p *printer) printText(text string, spaces []bool, last bool) string { var result string for _, space := range spaces { if space { result += emptySpace } else { result += continueItem } } indicator := middleItem if last { indicator = lastItem } var out string lines := strings.Split(text, "\n") for i := range lines { text := lines[i] if i == 0 { out += result + indicator + text + newLine continue } if last { indicator = emptySpace } else { indicator = continueItem } out += result + indicator + text + newLine } return out } func (p *printer) printItems(t []Tree, spaces []bool) string { var result string for i, f := range t { last := i == len(t)-1 result += p.printText(f.Text(), spaces, last) if len(f.Items()) > 0 { spacesChild := append(spaces, last) result += p.printItems(f.Items(), spacesChild) } } return result } ================================================ FILE: vendor/github.com/distribution/reference/.gitattributes ================================================ *.go text eol=lf ================================================ FILE: vendor/github.com/distribution/reference/.gitignore ================================================ # Cover profiles *.out ================================================ FILE: vendor/github.com/distribution/reference/.golangci.yml ================================================ linters: enable: - bodyclose - dupword # Checks for duplicate words in the source code - gofmt - goimports - ineffassign - misspell - revive - staticcheck - unconvert - unused - vet disable: - errcheck run: deadline: 2m ================================================ FILE: vendor/github.com/distribution/reference/CODE-OF-CONDUCT.md ================================================ # Code of Conduct We follow the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). Please contact the [CNCF Code of Conduct Committee](mailto:conduct@cncf.io) in order to report violations of the Code of Conduct. ================================================ FILE: vendor/github.com/distribution/reference/CONTRIBUTING.md ================================================ # Contributing to the reference library ## Community help If you need help, please ask in the [#distribution](https://cloud-native.slack.com/archives/C01GVR8SY4R) channel on CNCF community slack. [Click here for an invite to the CNCF community slack](https://slack.cncf.io/) ## Reporting security issues The maintainers take security seriously. If you discover a security issue, please bring it to their attention right away! Please **DO NOT** file a public issue, instead send your report privately to [cncf-distribution-security@lists.cncf.io](mailto:cncf-distribution-security@lists.cncf.io). ## Reporting an issue properly By following these simple rules you will get better and faster feedback on your issue. - search the bugtracker for an already reported issue ### If you found an issue that describes your problem: - please read other user comments first, and confirm this is the same issue: a given error condition might be indicative of different problems - you may also find a workaround in the comments - please refrain from adding "same thing here" or "+1" comments - you don't need to comment on an issue to get notified of updates: just hit the "subscribe" button - comment if you have some new, technical and relevant information to add to the case - __DO NOT__ comment on closed issues or merged PRs. If you think you have a related problem, open up a new issue and reference the PR or issue. ### If you have not found an existing issue that describes your problem: 1. create a new issue, with a succinct title that describes your issue: - bad title: "It doesn't work with my docker" - good title: "Private registry push fail: 400 error with E_INVALID_DIGEST" 2. copy the output of (or similar for other container tools): - `docker version` - `docker info` - `docker exec registry --version` 3. copy the command line you used to launch your Registry 4. restart your docker daemon in debug mode (add `-D` to the daemon launch arguments) 5. reproduce your problem and get your docker daemon logs showing the error 6. if relevant, copy your registry logs that show the error 7. provide any relevant detail about your specific Registry configuration (e.g., storage backend used) 8. indicate if you are using an enterprise proxy, Nginx, or anything else between you and your Registry ## Contributing Code Contributions should be made via pull requests. Pull requests will be reviewed by one or more maintainers or reviewers and merged when acceptable. You should follow the basic GitHub workflow: 1. Use your own [fork](https://help.github.com/en/articles/about-forks) 2. Create your [change](https://github.com/containerd/project/blob/master/CONTRIBUTING.md#successful-changes) 3. Test your code 4. [Commit](https://github.com/containerd/project/blob/master/CONTRIBUTING.md#commit-messages) your work, always [sign your commits](https://github.com/containerd/project/blob/master/CONTRIBUTING.md#commit-messages) 5. Push your change to your fork and create a [Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork) Refer to [containerd's contribution guide](https://github.com/containerd/project/blob/master/CONTRIBUTING.md#successful-changes) for tips on creating a successful contribution. ## Sign your work The sign-off is a simple line at the end of the explanation for the patch. Your signature certifies that you wrote the patch or otherwise have the right to pass it on as an open-source patch. The rules are pretty simple: if you can certify the below (from [developercertificate.org](http://developercertificate.org/)): ``` Developer Certificate of Origin Version 1.1 Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 660 York Street, Suite 102, San Francisco, CA 94110 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. ``` Then you just add a line to every git commit message: Signed-off-by: Joe Smith Use your real name (sorry, no pseudonyms or anonymous contributions.) If you set your `user.name` and `user.email` git configs, you can sign your commit automatically with `git commit -s`. ================================================ FILE: vendor/github.com/distribution/reference/GOVERNANCE.md ================================================ # distribution/reference Project Governance Distribution [Code of Conduct](./CODE-OF-CONDUCT.md) can be found here. For specific guidance on practical contribution steps please see our [CONTRIBUTING.md](./CONTRIBUTING.md) guide. ## Maintainership There are different types of maintainers, with different responsibilities, but all maintainers have 3 things in common: 1) They share responsibility in the project's success. 2) They have made a long-term, recurring time investment to improve the project. 3) They spend that time doing whatever needs to be done, not necessarily what is the most interesting or fun. Maintainers are often under-appreciated, because their work is harder to appreciate. It's easy to appreciate a really cool and technically advanced feature. It's harder to appreciate the absence of bugs, the slow but steady improvement in stability, or the reliability of a release process. But those things distinguish a good project from a great one. ## Reviewers A reviewer is a core role within the project. They share in reviewing issues and pull requests and their LGTM counts towards the required LGTM count to merge a code change into the project. Reviewers are part of the organization but do not have write access. Becoming a reviewer is a core aspect in the journey to becoming a maintainer. ## Adding maintainers Maintainers are first and foremost contributors that have shown they are committed to the long term success of a project. Contributors wanting to become maintainers are expected to be deeply involved in contributing code, pull request review, and triage of issues in the project for more than three months. Just contributing does not make you a maintainer, it is about building trust with the current maintainers of the project and being a person that they can depend on and trust to make decisions in the best interest of the project. Periodically, the existing maintainers curate a list of contributors that have shown regular activity on the project over the prior months. From this list, maintainer candidates are selected and proposed in a pull request or a maintainers communication channel. After a candidate has been announced to the maintainers, the existing maintainers are given five business days to discuss the candidate, raise objections and cast their vote. Votes may take place on the communication channel or via pull request comment. Candidates must be approved by at least 66% of the current maintainers by adding their vote on the mailing list. The reviewer role has the same process but only requires 33% of current maintainers. Only maintainers of the repository that the candidate is proposed for are allowed to vote. If a candidate is approved, a maintainer will contact the candidate to invite the candidate to open a pull request that adds the contributor to the MAINTAINERS file. The voting process may take place inside a pull request if a maintainer has already discussed the candidacy with the candidate and a maintainer is willing to be a sponsor by opening the pull request. The candidate becomes a maintainer once the pull request is merged. ## Stepping down policy Life priorities, interests, and passions can change. If you're a maintainer but feel you must remove yourself from the list, inform other maintainers that you intend to step down, and if possible, help find someone to pick up your work. At the very least, ensure your work can be continued where you left off. After you've informed other maintainers, create a pull request to remove yourself from the MAINTAINERS file. ## Removal of inactive maintainers Similar to the procedure for adding new maintainers, existing maintainers can be removed from the list if they do not show significant activity on the project. Periodically, the maintainers review the list of maintainers and their activity over the last three months. If a maintainer has shown insufficient activity over this period, a neutral person will contact the maintainer to ask if they want to continue being a maintainer. If the maintainer decides to step down as a maintainer, they open a pull request to be removed from the MAINTAINERS file. If the maintainer wants to remain a maintainer, but is unable to perform the required duties they can be removed with a vote of at least 66% of the current maintainers. In this case, maintainers should first propose the change to maintainers via the maintainers communication channel, then open a pull request for voting. The voting period is five business days. The voting pull request should not come as a surpise to any maintainer and any discussion related to performance must not be discussed on the pull request. ## How are decisions made? Docker distribution is an open-source project with an open design philosophy. This means that the repository is the source of truth for EVERY aspect of the project, including its philosophy, design, road map, and APIs. *If it's part of the project, it's in the repo. If it's in the repo, it's part of the project.* As a result, all decisions can be expressed as changes to the repository. An implementation change is a change to the source code. An API change is a change to the API specification. A philosophy change is a change to the philosophy manifesto, and so on. All decisions affecting distribution, big and small, follow the same 3 steps: * Step 1: Open a pull request. Anyone can do this. * Step 2: Discuss the pull request. Anyone can do this. * Step 3: Merge or refuse the pull request. Who does this depends on the nature of the pull request and which areas of the project it affects. ## Helping contributors with the DCO The [DCO or `Sign your work`](./CONTRIBUTING.md#sign-your-work) requirement is not intended as a roadblock or speed bump. Some contributors are not as familiar with `git`, or have used a web based editor, and thus asking them to `git commit --amend -s` is not the best way forward. In this case, maintainers can update the commits based on clause (c) of the DCO. The most trivial way for a contributor to allow the maintainer to do this, is to add a DCO signature in a pull requests's comment, or a maintainer can simply note that the change is sufficiently trivial that it does not substantially change the existing contribution - i.e., a spelling change. When you add someone's DCO, please also add your own to keep a log. ## I'm a maintainer. Should I make pull requests too? Yes. Nobody should ever push to master directly. All changes should be made through a pull request. ## Conflict Resolution If you have a technical dispute that you feel has reached an impasse with a subset of the community, any contributor may open an issue, specifically calling for a resolution vote of the current core maintainers to resolve the dispute. The same voting quorums required (2/3) for adding and removing maintainers will apply to conflict resolution. ================================================ FILE: vendor/github.com/distribution/reference/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: vendor/github.com/distribution/reference/MAINTAINERS ================================================ # Distribution project maintainers & reviewers # # See GOVERNANCE.md for maintainer versus reviewer roles # # MAINTAINERS (cncf-distribution-maintainers@lists.cncf.io) # GitHub ID, Name, Email address "chrispat","Chris Patterson","chrispat@github.com" "clarkbw","Bryan Clark","clarkbw@github.com" "corhere","Cory Snider","csnider@mirantis.com" "deleteriousEffect","Hayley Swimelar","hswimelar@gitlab.com" "heww","He Weiwei","hweiwei@vmware.com" "joaodrp","João Pereira","jpereira@gitlab.com" "justincormack","Justin Cormack","justin.cormack@docker.com" "squizzi","Kyle Squizzato","ksquizzato@mirantis.com" "milosgajdos","Milos Gajdos","milosthegajdos@gmail.com" "sargun","Sargun Dhillon","sargun@sargun.me" "wy65701436","Wang Yan","wangyan@vmware.com" "stevelasker","Steve Lasker","steve.lasker@microsoft.com" # # REVIEWERS # GitHub ID, Name, Email address "dmcgowan","Derek McGowan","derek@mcgstyle.net" "stevvooe","Stephen Day","stevvooe@gmail.com" "thajeztah","Sebastiaan van Stijn","github@gone.nl" "DavidSpek", "David van der Spek", "vanderspek.david@gmail.com" "Jamstah", "James Hewitt", "james.hewitt@gmail.com" ================================================ FILE: vendor/github.com/distribution/reference/Makefile ================================================ # Project packages. PACKAGES=$(shell go list ./...) # Flags passed to `go test` BUILDFLAGS ?= TESTFLAGS ?= .PHONY: all build test coverage .DEFAULT: all all: build build: ## no binaries to build, so just check compilation suceeds go build ${BUILDFLAGS} ./... test: ## run tests go test ${TESTFLAGS} ./... coverage: ## generate coverprofiles from the unit tests rm -f coverage.txt go test ${TESTFLAGS} -cover -coverprofile=cover.out ./... .PHONY: help help: @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_\/%-]+:.*?##/ { printf " \033[36m%-27s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) ================================================ FILE: vendor/github.com/distribution/reference/README.md ================================================ # Distribution reference Go library to handle references to container images. [![Build Status](https://github.com/distribution/reference/actions/workflows/test.yml/badge.svg?branch=main&event=push)](https://github.com/distribution/reference/actions?query=workflow%3ACI) [![GoDoc](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/distribution/reference) [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE) [![codecov](https://codecov.io/gh/distribution/reference/branch/main/graph/badge.svg)](https://codecov.io/gh/distribution/reference) [![FOSSA Status](https://app.fossa.com/api/projects/custom%2B162%2Fgithub.com%2Fdistribution%2Freference.svg?type=shield)](https://app.fossa.com/projects/custom%2B162%2Fgithub.com%2Fdistribution%2Freference?ref=badge_shield) This repository contains a library for handling references to container images held in container registries. Please see [godoc](https://pkg.go.dev/github.com/distribution/reference) for details. ## Contribution Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute issues, fixes, and patches to this project. ## Communication For async communication and long running discussions please use issues and pull requests on the github repo. This will be the best place to discuss design and implementation. For sync communication we have a #distribution channel in the [CNCF Slack](https://slack.cncf.io/) that everyone is welcome to join and chat about development. ## Licenses The distribution codebase is released under the [Apache 2.0 license](LICENSE). ================================================ FILE: vendor/github.com/distribution/reference/SECURITY.md ================================================ # Security Policy ## Reporting a Vulnerability The maintainers take security seriously. If you discover a security issue, please bring it to their attention right away! Please DO NOT file a public issue, instead send your report privately to cncf-distribution-security@lists.cncf.io. ================================================ FILE: vendor/github.com/distribution/reference/helpers.go ================================================ package reference import "path" // IsNameOnly returns true if reference only contains a repo name. func IsNameOnly(ref Named) bool { if _, ok := ref.(NamedTagged); ok { return false } if _, ok := ref.(Canonical); ok { return false } return true } // FamiliarName returns the familiar name string // for the given named, familiarizing if needed. func FamiliarName(ref Named) string { if nn, ok := ref.(normalizedNamed); ok { return nn.Familiar().Name() } return ref.Name() } // FamiliarString returns the familiar string representation // for the given reference, familiarizing if needed. func FamiliarString(ref Reference) string { if nn, ok := ref.(normalizedNamed); ok { return nn.Familiar().String() } return ref.String() } // FamiliarMatch reports whether ref matches the specified pattern. // See [path.Match] for supported patterns. func FamiliarMatch(pattern string, ref Reference) (bool, error) { matched, err := path.Match(pattern, FamiliarString(ref)) if namedRef, isNamed := ref.(Named); isNamed && !matched { matched, _ = path.Match(pattern, FamiliarName(namedRef)) } return matched, err } ================================================ FILE: vendor/github.com/distribution/reference/normalize.go ================================================ package reference import ( "fmt" "strings" "github.com/opencontainers/go-digest" ) const ( // legacyDefaultDomain is the legacy domain for Docker Hub (which was // originally named "the Docker Index"). This domain is still used for // authentication and image search, which were part of the "v1" Docker // registry specification. // // This domain will continue to be supported, but there are plans to consolidate // legacy domains to new "canonical" domains. Once those domains are decided // on, we must update the normalization functions, but preserve compatibility // with existing installs, clients, and user configuration. legacyDefaultDomain = "index.docker.io" // defaultDomain is the default domain used for images on Docker Hub. // It is used to normalize "familiar" names to canonical names, for example, // to convert "ubuntu" to "docker.io/library/ubuntu:latest". // // Note that actual domain of Docker Hub's registry is registry-1.docker.io. // This domain will continue to be supported, but there are plans to consolidate // legacy domains to new "canonical" domains. Once those domains are decided // on, we must update the normalization functions, but preserve compatibility // with existing installs, clients, and user configuration. defaultDomain = "docker.io" // officialRepoPrefix is the namespace used for official images on Docker Hub. // It is used to normalize "familiar" names to canonical names, for example, // to convert "ubuntu" to "docker.io/library/ubuntu:latest". officialRepoPrefix = "library/" // defaultTag is the default tag if no tag is provided. defaultTag = "latest" ) // normalizedNamed represents a name which has been // normalized and has a familiar form. A familiar name // is what is used in Docker UI. An example normalized // name is "docker.io/library/ubuntu" and corresponding // familiar name of "ubuntu". type normalizedNamed interface { Named Familiar() Named } // ParseNormalizedNamed parses a string into a named reference // transforming a familiar name from Docker UI to a fully // qualified reference. If the value may be an identifier // use ParseAnyReference. func ParseNormalizedNamed(s string) (Named, error) { if ok := anchoredIdentifierRegexp.MatchString(s); ok { return nil, fmt.Errorf("invalid repository name (%s), cannot specify 64-byte hexadecimal strings", s) } domain, remainder := splitDockerDomain(s) var remote string if tagSep := strings.IndexRune(remainder, ':'); tagSep > -1 { remote = remainder[:tagSep] } else { remote = remainder } if strings.ToLower(remote) != remote { return nil, fmt.Errorf("invalid reference format: repository name (%s) must be lowercase", remote) } ref, err := Parse(domain + "/" + remainder) if err != nil { return nil, err } named, isNamed := ref.(Named) if !isNamed { return nil, fmt.Errorf("reference %s has no name", ref.String()) } return named, nil } // namedTaggedDigested is a reference that has both a tag and a digest. type namedTaggedDigested interface { NamedTagged Digested } // ParseDockerRef normalizes the image reference following the docker convention, // which allows for references to contain both a tag and a digest. It returns a // reference that is either tagged or digested. For references containing both // a tag and a digest, it returns a digested reference. For example, the following // reference: // // docker.io/library/busybox:latest@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa // // Is returned as a digested reference (with the ":latest" tag removed): // // docker.io/library/busybox@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa // // References that are already "tagged" or "digested" are returned unmodified: // // // Already a digested reference // docker.io/library/busybox@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa // // // Already a named reference // docker.io/library/busybox:latest func ParseDockerRef(ref string) (Named, error) { named, err := ParseNormalizedNamed(ref) if err != nil { return nil, err } if canonical, ok := named.(namedTaggedDigested); ok { // The reference is both tagged and digested; only return digested. newNamed, err := WithName(canonical.Name()) if err != nil { return nil, err } return WithDigest(newNamed, canonical.Digest()) } return TagNameOnly(named), nil } // splitDockerDomain splits a repository name to domain and remote-name. // If no valid domain is found, the default domain is used. Repository name // needs to be already validated before. func splitDockerDomain(name string) (domain, remoteName string) { maybeDomain, maybeRemoteName, ok := strings.Cut(name, "/") if !ok { // Fast-path for single element ("familiar" names), such as "ubuntu" // or "ubuntu:latest". Familiar names must be handled separately, to // prevent them from being handled as "hostname:port". // // Canonicalize them as "docker.io/library/name[:tag]" // FIXME(thaJeztah): account for bare "localhost" or "example.com" names, which SHOULD be considered a domain. return defaultDomain, officialRepoPrefix + name } switch { case maybeDomain == localhost: // localhost is a reserved namespace and always considered a domain. domain, remoteName = maybeDomain, maybeRemoteName case maybeDomain == legacyDefaultDomain: // canonicalize the Docker Hub and legacy "Docker Index" domains. domain, remoteName = defaultDomain, maybeRemoteName case strings.ContainsAny(maybeDomain, ".:"): // Likely a domain or IP-address: // // - contains a "." (e.g., "example.com" or "127.0.0.1") // - contains a ":" (e.g., "example:5000", "::1", or "[::1]:5000") domain, remoteName = maybeDomain, maybeRemoteName case strings.ToLower(maybeDomain) != maybeDomain: // Uppercase namespaces are not allowed, so if the first element // is not lowercase, we assume it to be a domain-name. domain, remoteName = maybeDomain, maybeRemoteName default: // None of the above: it's not a domain, so use the default, and // use the name input the remote-name. domain, remoteName = defaultDomain, name } if domain == defaultDomain && !strings.ContainsRune(remoteName, '/') { // Canonicalize "familiar" names, but only on Docker Hub, not // on other domains: // // "docker.io/ubuntu[:tag]" => "docker.io/library/ubuntu[:tag]" remoteName = officialRepoPrefix + remoteName } return domain, remoteName } // familiarizeName returns a shortened version of the name familiar // to the Docker UI. Familiar names have the default domain // "docker.io" and "library/" repository prefix removed. // For example, "docker.io/library/redis" will have the familiar // name "redis" and "docker.io/dmcgowan/myapp" will be "dmcgowan/myapp". // Returns a familiarized named only reference. func familiarizeName(named namedRepository) repository { repo := repository{ domain: named.Domain(), path: named.Path(), } if repo.domain == defaultDomain { repo.domain = "" // Handle official repositories which have the pattern "library/" if strings.HasPrefix(repo.path, officialRepoPrefix) { // TODO(thaJeztah): this check may be too strict, as it assumes the // "library/" namespace does not have nested namespaces. While this // is true (currently), technically it would be possible for Docker // Hub to use those (e.g. "library/distros/ubuntu:latest"). // See https://github.com/distribution/distribution/pull/3769#issuecomment-1302031785. if remainder := strings.TrimPrefix(repo.path, officialRepoPrefix); !strings.ContainsRune(remainder, '/') { repo.path = remainder } } } return repo } func (r reference) Familiar() Named { return reference{ namedRepository: familiarizeName(r.namedRepository), tag: r.tag, digest: r.digest, } } func (r repository) Familiar() Named { return familiarizeName(r) } func (t taggedReference) Familiar() Named { return taggedReference{ namedRepository: familiarizeName(t.namedRepository), tag: t.tag, } } func (c canonicalReference) Familiar() Named { return canonicalReference{ namedRepository: familiarizeName(c.namedRepository), digest: c.digest, } } // TagNameOnly adds the default tag "latest" to a reference if it only has // a repo name. func TagNameOnly(ref Named) Named { if IsNameOnly(ref) { namedTagged, err := WithTag(ref, defaultTag) if err != nil { // Default tag must be valid, to create a NamedTagged // type with non-validated input the WithTag function // should be used instead panic(err) } return namedTagged } return ref } // ParseAnyReference parses a reference string as a possible identifier, // full digest, or familiar name. func ParseAnyReference(ref string) (Reference, error) { if ok := anchoredIdentifierRegexp.MatchString(ref); ok { return digestReference("sha256:" + ref), nil } if dgst, err := digest.Parse(ref); err == nil { return digestReference(dgst), nil } return ParseNormalizedNamed(ref) } ================================================ FILE: vendor/github.com/distribution/reference/reference.go ================================================ // Package reference provides a general type to represent any way of referencing images within the registry. // Its main purpose is to abstract tags and digests (content-addressable hash). // // Grammar // // reference := name [ ":" tag ] [ "@" digest ] // name := [domain '/'] remote-name // domain := host [':' port-number] // host := domain-name | IPv4address | \[ IPv6address \] ; rfc3986 appendix-A // domain-name := domain-component ['.' domain-component]* // domain-component := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/ // port-number := /[0-9]+/ // path-component := alpha-numeric [separator alpha-numeric]* // path (or "remote-name") := path-component ['/' path-component]* // alpha-numeric := /[a-z0-9]+/ // separator := /[_.]|__|[-]*/ // // tag := /[\w][\w.-]{0,127}/ // // digest := digest-algorithm ":" digest-hex // digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ]* // digest-algorithm-separator := /[+.-_]/ // digest-algorithm-component := /[A-Za-z][A-Za-z0-9]*/ // digest-hex := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value // // identifier := /[a-f0-9]{64}/ package reference import ( "errors" "fmt" "strings" "github.com/opencontainers/go-digest" ) const ( // RepositoryNameTotalLengthMax is the maximum total number of characters in a repository name. RepositoryNameTotalLengthMax = 255 // NameTotalLengthMax is the maximum total number of characters in a repository name. // // Deprecated: use [RepositoryNameTotalLengthMax] instead. NameTotalLengthMax = RepositoryNameTotalLengthMax ) var ( // ErrReferenceInvalidFormat represents an error while trying to parse a string as a reference. ErrReferenceInvalidFormat = errors.New("invalid reference format") // ErrTagInvalidFormat represents an error while trying to parse a string as a tag. ErrTagInvalidFormat = errors.New("invalid tag format") // ErrDigestInvalidFormat represents an error while trying to parse a string as a tag. ErrDigestInvalidFormat = errors.New("invalid digest format") // ErrNameContainsUppercase is returned for invalid repository names that contain uppercase characters. ErrNameContainsUppercase = errors.New("repository name must be lowercase") // ErrNameEmpty is returned for empty, invalid repository names. ErrNameEmpty = errors.New("repository name must have at least one component") // ErrNameTooLong is returned when a repository name is longer than RepositoryNameTotalLengthMax. ErrNameTooLong = fmt.Errorf("repository name must not be more than %v characters", RepositoryNameTotalLengthMax) // ErrNameNotCanonical is returned when a name is not canonical. ErrNameNotCanonical = errors.New("repository name must be canonical") ) // Reference is an opaque object reference identifier that may include // modifiers such as a hostname, name, tag, and digest. type Reference interface { // String returns the full reference String() string } // Field provides a wrapper type for resolving correct reference types when // working with encoding. type Field struct { reference Reference } // AsField wraps a reference in a Field for encoding. func AsField(reference Reference) Field { return Field{reference} } // Reference unwraps the reference type from the field to // return the Reference object. This object should be // of the appropriate type to further check for different // reference types. func (f Field) Reference() Reference { return f.reference } // MarshalText serializes the field to byte text which // is the string of the reference. func (f Field) MarshalText() (p []byte, err error) { return []byte(f.reference.String()), nil } // UnmarshalText parses text bytes by invoking the // reference parser to ensure the appropriately // typed reference object is wrapped by field. func (f *Field) UnmarshalText(p []byte) error { r, err := Parse(string(p)) if err != nil { return err } f.reference = r return nil } // Named is an object with a full name type Named interface { Reference Name() string } // Tagged is an object which has a tag type Tagged interface { Reference Tag() string } // NamedTagged is an object including a name and tag. type NamedTagged interface { Named Tag() string } // Digested is an object which has a digest // in which it can be referenced by type Digested interface { Reference Digest() digest.Digest } // Canonical reference is an object with a fully unique // name including a name with domain and digest type Canonical interface { Named Digest() digest.Digest } // namedRepository is a reference to a repository with a name. // A namedRepository has both domain and path components. type namedRepository interface { Named Domain() string Path() string } // Domain returns the domain part of the [Named] reference. func Domain(named Named) string { if r, ok := named.(namedRepository); ok { return r.Domain() } domain, _ := splitDomain(named.Name()) return domain } // Path returns the name without the domain part of the [Named] reference. func Path(named Named) (name string) { if r, ok := named.(namedRepository); ok { return r.Path() } _, path := splitDomain(named.Name()) return path } // splitDomain splits a named reference into a hostname and path string. // If no valid hostname is found, the hostname is empty and the full value // is returned as name func splitDomain(name string) (string, string) { match := anchoredNameRegexp.FindStringSubmatch(name) if len(match) != 3 { return "", name } return match[1], match[2] } // Parse parses s and returns a syntactically valid Reference. // If an error was encountered it is returned, along with a nil Reference. func Parse(s string) (Reference, error) { matches := ReferenceRegexp.FindStringSubmatch(s) if matches == nil { if s == "" { return nil, ErrNameEmpty } if ReferenceRegexp.FindStringSubmatch(strings.ToLower(s)) != nil { return nil, ErrNameContainsUppercase } return nil, ErrReferenceInvalidFormat } var repo repository nameMatch := anchoredNameRegexp.FindStringSubmatch(matches[1]) if len(nameMatch) == 3 { repo.domain = nameMatch[1] repo.path = nameMatch[2] } else { repo.domain = "" repo.path = matches[1] } if len(repo.path) > RepositoryNameTotalLengthMax { return nil, ErrNameTooLong } ref := reference{ namedRepository: repo, tag: matches[2], } if matches[3] != "" { var err error ref.digest, err = digest.Parse(matches[3]) if err != nil { return nil, err } } r := getBestReferenceType(ref) if r == nil { return nil, ErrNameEmpty } return r, nil } // ParseNamed parses s and returns a syntactically valid reference implementing // the Named interface. The reference must have a name and be in the canonical // form, otherwise an error is returned. // If an error was encountered it is returned, along with a nil Reference. func ParseNamed(s string) (Named, error) { named, err := ParseNormalizedNamed(s) if err != nil { return nil, err } if named.String() != s { return nil, ErrNameNotCanonical } return named, nil } // WithName returns a named object representing the given string. If the input // is invalid ErrReferenceInvalidFormat will be returned. func WithName(name string) (Named, error) { match := anchoredNameRegexp.FindStringSubmatch(name) if match == nil || len(match) != 3 { return nil, ErrReferenceInvalidFormat } if len(match[2]) > RepositoryNameTotalLengthMax { return nil, ErrNameTooLong } return repository{ domain: match[1], path: match[2], }, nil } // WithTag combines the name from "name" and the tag from "tag" to form a // reference incorporating both the name and the tag. func WithTag(name Named, tag string) (NamedTagged, error) { if !anchoredTagRegexp.MatchString(tag) { return nil, ErrTagInvalidFormat } var repo repository if r, ok := name.(namedRepository); ok { repo.domain = r.Domain() repo.path = r.Path() } else { repo.path = name.Name() } if canonical, ok := name.(Canonical); ok { return reference{ namedRepository: repo, tag: tag, digest: canonical.Digest(), }, nil } return taggedReference{ namedRepository: repo, tag: tag, }, nil } // WithDigest combines the name from "name" and the digest from "digest" to form // a reference incorporating both the name and the digest. func WithDigest(name Named, digest digest.Digest) (Canonical, error) { if !anchoredDigestRegexp.MatchString(digest.String()) { return nil, ErrDigestInvalidFormat } var repo repository if r, ok := name.(namedRepository); ok { repo.domain = r.Domain() repo.path = r.Path() } else { repo.path = name.Name() } if tagged, ok := name.(Tagged); ok { return reference{ namedRepository: repo, tag: tagged.Tag(), digest: digest, }, nil } return canonicalReference{ namedRepository: repo, digest: digest, }, nil } // TrimNamed removes any tag or digest from the named reference. func TrimNamed(ref Named) Named { repo := repository{} if r, ok := ref.(namedRepository); ok { repo.domain, repo.path = r.Domain(), r.Path() } else { repo.domain, repo.path = splitDomain(ref.Name()) } return repo } func getBestReferenceType(ref reference) Reference { if ref.Name() == "" { // Allow digest only references if ref.digest != "" { return digestReference(ref.digest) } return nil } if ref.tag == "" { if ref.digest != "" { return canonicalReference{ namedRepository: ref.namedRepository, digest: ref.digest, } } return ref.namedRepository } if ref.digest == "" { return taggedReference{ namedRepository: ref.namedRepository, tag: ref.tag, } } return ref } type reference struct { namedRepository tag string digest digest.Digest } func (r reference) String() string { return r.Name() + ":" + r.tag + "@" + r.digest.String() } func (r reference) Tag() string { return r.tag } func (r reference) Digest() digest.Digest { return r.digest } type repository struct { domain string path string } func (r repository) String() string { return r.Name() } func (r repository) Name() string { if r.domain == "" { return r.path } return r.domain + "/" + r.path } func (r repository) Domain() string { return r.domain } func (r repository) Path() string { return r.path } type digestReference digest.Digest func (d digestReference) String() string { return digest.Digest(d).String() } func (d digestReference) Digest() digest.Digest { return digest.Digest(d) } type taggedReference struct { namedRepository tag string } func (t taggedReference) String() string { return t.Name() + ":" + t.tag } func (t taggedReference) Tag() string { return t.tag } type canonicalReference struct { namedRepository digest digest.Digest } func (c canonicalReference) String() string { return c.Name() + "@" + c.digest.String() } func (c canonicalReference) Digest() digest.Digest { return c.digest } ================================================ FILE: vendor/github.com/distribution/reference/regexp.go ================================================ package reference import ( "regexp" "strings" ) // DigestRegexp matches well-formed digests, including algorithm (e.g. "sha256:"). var DigestRegexp = regexp.MustCompile(digestPat) // DomainRegexp matches hostname or IP-addresses, optionally including a port // number. It defines the structure of potential domain components that may be // part of image names. This is purposely a subset of what is allowed by DNS to // ensure backwards compatibility with Docker image names. It may be a subset of // DNS domain name, an IPv4 address in decimal format, or an IPv6 address between // square brackets (excluding zone identifiers as defined by [RFC 6874] or special // addresses such as IPv4-Mapped). // // [RFC 6874]: https://www.rfc-editor.org/rfc/rfc6874. var DomainRegexp = regexp.MustCompile(domainAndPort) // IdentifierRegexp is the format for string identifier used as a // content addressable identifier using sha256. These identifiers // are like digests without the algorithm, since sha256 is used. var IdentifierRegexp = regexp.MustCompile(identifier) // NameRegexp is the format for the name component of references, including // an optional domain and port, but without tag or digest suffix. var NameRegexp = regexp.MustCompile(namePat) // ReferenceRegexp is the full supported format of a reference. The regexp // is anchored and has capturing groups for name, tag, and digest // components. var ReferenceRegexp = regexp.MustCompile(referencePat) // TagRegexp matches valid tag names. From [docker/docker:graph/tags.go]. // // [docker/docker:graph/tags.go]: https://github.com/moby/moby/blob/v1.6.0/graph/tags.go#L26-L28 var TagRegexp = regexp.MustCompile(tag) const ( // alphanumeric defines the alphanumeric atom, typically a // component of names. This only allows lower case characters and digits. alphanumeric = `[a-z0-9]+` // separator defines the separators allowed to be embedded in name // components. This allows one period, one or two underscore and multiple // dashes. Repeated dashes and underscores are intentionally treated // differently. In order to support valid hostnames as name components, // supporting repeated dash was added. Additionally double underscore is // now allowed as a separator to loosen the restriction for previously // supported names. separator = `(?:[._]|__|[-]+)` // localhost is treated as a special value for domain-name. Any other // domain-name without a "." or a ":port" are considered a path component. localhost = `localhost` // domainNameComponent restricts the registry domain component of a // repository name to start with a component as defined by DomainRegexp. domainNameComponent = `(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])` // optionalPort matches an optional port-number including the port separator // (e.g. ":80"). optionalPort = `(?::[0-9]+)?` // tag matches valid tag names. From docker/docker:graph/tags.go. tag = `[\w][\w.-]{0,127}` // digestPat matches well-formed digests, including algorithm (e.g. "sha256:"). // // TODO(thaJeztah): this should follow the same rules as https://pkg.go.dev/github.com/opencontainers/go-digest@v1.0.0#DigestRegexp // so that go-digest defines the canonical format. Note that the go-digest is // more relaxed: // - it allows multiple algorithms (e.g. "sha256+b64:") to allow // future expansion of supported algorithms. // - it allows the "" value to use urlsafe base64 encoding as defined // in [rfc4648, section 5]. // // [rfc4648, section 5]: https://www.rfc-editor.org/rfc/rfc4648#section-5. digestPat = `[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}` // identifier is the format for a content addressable identifier using sha256. // These identifiers are like digests without the algorithm, since sha256 is used. identifier = `([a-f0-9]{64})` // ipv6address are enclosed between square brackets and may be represented // in many ways, see rfc5952. Only IPv6 in compressed or uncompressed format // are allowed, IPv6 zone identifiers (rfc6874) or Special addresses such as // IPv4-Mapped are deliberately excluded. ipv6address = `\[(?:[a-fA-F0-9:]+)\]` ) var ( // domainName defines the structure of potential domain components // that may be part of image names. This is purposely a subset of what is // allowed by DNS to ensure backwards compatibility with Docker image // names. This includes IPv4 addresses on decimal format. domainName = domainNameComponent + anyTimes(`\.`+domainNameComponent) // host defines the structure of potential domains based on the URI // Host subcomponent on rfc3986. It may be a subset of DNS domain name, // or an IPv4 address in decimal format, or an IPv6 address between square // brackets (excluding zone identifiers as defined by rfc6874 or special // addresses such as IPv4-Mapped). host = `(?:` + domainName + `|` + ipv6address + `)` // allowed by the URI Host subcomponent on rfc3986 to ensure backwards // compatibility with Docker image names. domainAndPort = host + optionalPort // anchoredTagRegexp matches valid tag names, anchored at the start and // end of the matched string. anchoredTagRegexp = regexp.MustCompile(anchored(tag)) // anchoredDigestRegexp matches valid digests, anchored at the start and // end of the matched string. anchoredDigestRegexp = regexp.MustCompile(anchored(digestPat)) // pathComponent restricts path-components to start with an alphanumeric // character, with following parts able to be separated by a separator // (one period, one or two underscore and multiple dashes). pathComponent = alphanumeric + anyTimes(separator+alphanumeric) // remoteName matches the remote-name of a repository. It consists of one // or more forward slash (/) delimited path-components: // // pathComponent[[/pathComponent] ...] // e.g., "library/ubuntu" remoteName = pathComponent + anyTimes(`/`+pathComponent) namePat = optional(domainAndPort+`/`) + remoteName // anchoredNameRegexp is used to parse a name value, capturing the // domain and trailing components. anchoredNameRegexp = regexp.MustCompile(anchored(optional(capture(domainAndPort), `/`), capture(remoteName))) referencePat = anchored(capture(namePat), optional(`:`, capture(tag)), optional(`@`, capture(digestPat))) // anchoredIdentifierRegexp is used to check or match an // identifier value, anchored at start and end of string. anchoredIdentifierRegexp = regexp.MustCompile(anchored(identifier)) ) // optional wraps the expression in a non-capturing group and makes the // production optional. func optional(res ...string) string { return `(?:` + strings.Join(res, "") + `)?` } // anyTimes wraps the expression in a non-capturing group that can occur // any number of times. func anyTimes(res ...string) string { return `(?:` + strings.Join(res, "") + `)*` } // capture wraps the expression in a capturing group. func capture(res ...string) string { return `(` + strings.Join(res, "") + `)` } // anchored anchors the regular expression by adding start and end delimiters. func anchored(res ...string) string { return `^` + strings.Join(res, "") + `$` } ================================================ FILE: vendor/github.com/distribution/reference/sort.go ================================================ /* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package reference import ( "sort" ) // Sort sorts string references preferring higher information references. // // The precedence is as follows: // // 1. [Named] + [Tagged] + [Digested] (e.g., "docker.io/library/busybox:latest@sha256:") // 2. [Named] + [Tagged] (e.g., "docker.io/library/busybox:latest") // 3. [Named] + [Digested] (e.g., "docker.io/library/busybo@sha256:") // 4. [Named] (e.g., "docker.io/library/busybox") // 5. [Digested] (e.g., "docker.io@sha256:") // 6. Parse error func Sort(references []string) []string { var prefs []Reference var bad []string for _, ref := range references { pref, err := ParseAnyReference(ref) if err != nil { bad = append(bad, ref) } else { prefs = append(prefs, pref) } } sort.Slice(prefs, func(a, b int) bool { ar := refRank(prefs[a]) br := refRank(prefs[b]) if ar == br { return prefs[a].String() < prefs[b].String() } return ar < br }) sort.Strings(bad) var refs []string for _, pref := range prefs { refs = append(refs, pref.String()) } return append(refs, bad...) } func refRank(ref Reference) uint8 { if _, ok := ref.(Named); ok { if _, ok = ref.(Tagged); ok { if _, ok = ref.(Digested); ok { return 1 } return 2 } if _, ok = ref.(Digested); ok { return 3 } return 4 } return 5 } ================================================ FILE: vendor/github.com/docker/distribution/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: vendor/github.com/docker/distribution/registry/api/errcode/errors.go ================================================ package errcode import ( "encoding/json" "fmt" "strings" ) // ErrorCoder is the base interface for ErrorCode and Error allowing // users of each to just call ErrorCode to get the real ID of each type ErrorCoder interface { ErrorCode() ErrorCode } // ErrorCode represents the error type. The errors are serialized via strings // and the integer format may change and should *never* be exported. type ErrorCode int var _ error = ErrorCode(0) // ErrorCode just returns itself func (ec ErrorCode) ErrorCode() ErrorCode { return ec } // Error returns the ID/Value func (ec ErrorCode) Error() string { // NOTE(stevvooe): Cannot use message here since it may have unpopulated args. return strings.ToLower(strings.Replace(ec.String(), "_", " ", -1)) } // Descriptor returns the descriptor for the error code. func (ec ErrorCode) Descriptor() ErrorDescriptor { d, ok := errorCodeToDescriptors[ec] if !ok { return ErrorCodeUnknown.Descriptor() } return d } // String returns the canonical identifier for this error code. func (ec ErrorCode) String() string { return ec.Descriptor().Value } // Message returned the human-readable error message for this error code. func (ec ErrorCode) Message() string { return ec.Descriptor().Message } // MarshalText encodes the receiver into UTF-8-encoded text and returns the // result. func (ec ErrorCode) MarshalText() (text []byte, err error) { return []byte(ec.String()), nil } // UnmarshalText decodes the form generated by MarshalText. func (ec *ErrorCode) UnmarshalText(text []byte) error { desc, ok := idToDescriptors[string(text)] if !ok { desc = ErrorCodeUnknown.Descriptor() } *ec = desc.Code return nil } // WithMessage creates a new Error struct based on the passed-in info and // overrides the Message property. func (ec ErrorCode) WithMessage(message string) Error { return Error{ Code: ec, Message: message, } } // WithDetail creates a new Error struct based on the passed-in info and // set the Detail property appropriately func (ec ErrorCode) WithDetail(detail interface{}) Error { return Error{ Code: ec, Message: ec.Message(), }.WithDetail(detail) } // WithArgs creates a new Error struct and sets the Args slice func (ec ErrorCode) WithArgs(args ...interface{}) Error { return Error{ Code: ec, Message: ec.Message(), }.WithArgs(args...) } // Error provides a wrapper around ErrorCode with extra Details provided. type Error struct { Code ErrorCode `json:"code"` Message string `json:"message"` Detail interface{} `json:"detail,omitempty"` // TODO(duglin): See if we need an "args" property so we can do the // variable substitution right before showing the message to the user } var _ error = Error{} // ErrorCode returns the ID/Value of this Error func (e Error) ErrorCode() ErrorCode { return e.Code } // Error returns a human readable representation of the error. func (e Error) Error() string { return fmt.Sprintf("%s: %s", e.Code.Error(), e.Message) } // WithDetail will return a new Error, based on the current one, but with // some Detail info added func (e Error) WithDetail(detail interface{}) Error { return Error{ Code: e.Code, Message: e.Message, Detail: detail, } } // WithArgs uses the passed-in list of interface{} as the substitution // variables in the Error's Message string, but returns a new Error func (e Error) WithArgs(args ...interface{}) Error { return Error{ Code: e.Code, Message: fmt.Sprintf(e.Code.Message(), args...), Detail: e.Detail, } } // ErrorDescriptor provides relevant information about a given error code. type ErrorDescriptor struct { // Code is the error code that this descriptor describes. Code ErrorCode // Value provides a unique, string key, often captilized with // underscores, to identify the error code. This value is used as the // keyed value when serializing api errors. Value string // Message is a short, human readable decription of the error condition // included in API responses. Message string // Description provides a complete account of the errors purpose, suitable // for use in documentation. Description string // HTTPStatusCode provides the http status code that is associated with // this error condition. HTTPStatusCode int } // ParseErrorCode returns the value by the string error code. // `ErrorCodeUnknown` will be returned if the error is not known. func ParseErrorCode(value string) ErrorCode { ed, ok := idToDescriptors[value] if ok { return ed.Code } return ErrorCodeUnknown } // Errors provides the envelope for multiple errors and a few sugar methods // for use within the application. type Errors []error var _ error = Errors{} func (errs Errors) Error() string { switch len(errs) { case 0: return "" case 1: return errs[0].Error() default: msg := "errors:\n" for _, err := range errs { msg += err.Error() + "\n" } return msg } } // Len returns the current number of errors. func (errs Errors) Len() int { return len(errs) } // MarshalJSON converts slice of error, ErrorCode or Error into a // slice of Error - then serializes func (errs Errors) MarshalJSON() ([]byte, error) { var tmpErrs struct { Errors []Error `json:"errors,omitempty"` } for _, daErr := range errs { var err Error switch daErr := daErr.(type) { case ErrorCode: err = daErr.WithDetail(nil) case Error: err = daErr default: err = ErrorCodeUnknown.WithDetail(daErr) } // If the Error struct was setup and they forgot to set the // Message field (meaning its "") then grab it from the ErrCode msg := err.Message if msg == "" { msg = err.Code.Message() } tmpErrs.Errors = append(tmpErrs.Errors, Error{ Code: err.Code, Message: msg, Detail: err.Detail, }) } return json.Marshal(tmpErrs) } // UnmarshalJSON deserializes []Error and then converts it into slice of // Error or ErrorCode func (errs *Errors) UnmarshalJSON(data []byte) error { var tmpErrs struct { Errors []Error } if err := json.Unmarshal(data, &tmpErrs); err != nil { return err } var newErrs Errors for _, daErr := range tmpErrs.Errors { // If Message is empty or exactly matches the Code's message string // then just use the Code, no need for a full Error struct if daErr.Detail == nil && (daErr.Message == "" || daErr.Message == daErr.Code.Message()) { // Error's w/o details get converted to ErrorCode newErrs = append(newErrs, daErr.Code) } else { // Error's w/ details are untouched newErrs = append(newErrs, Error{ Code: daErr.Code, Message: daErr.Message, Detail: daErr.Detail, }) } } *errs = newErrs return nil } ================================================ FILE: vendor/github.com/docker/distribution/registry/api/errcode/handler.go ================================================ package errcode import ( "encoding/json" "net/http" ) // ServeJSON attempts to serve the errcode in a JSON envelope. It marshals err // and sets the content-type header to 'application/json'. It will handle // ErrorCoder and Errors, and if necessary will create an envelope. func ServeJSON(w http.ResponseWriter, err error) error { w.Header().Set("Content-Type", "application/json; charset=utf-8") var sc int switch errs := err.(type) { case Errors: if len(errs) < 1 { break } if err, ok := errs[0].(ErrorCoder); ok { sc = err.ErrorCode().Descriptor().HTTPStatusCode } case ErrorCoder: sc = errs.ErrorCode().Descriptor().HTTPStatusCode err = Errors{err} // create an envelope. default: // We just have an unhandled error type, so just place in an envelope // and move along. err = Errors{err} } if sc == 0 { sc = http.StatusInternalServerError } w.WriteHeader(sc) return json.NewEncoder(w).Encode(err) } ================================================ FILE: vendor/github.com/docker/distribution/registry/api/errcode/register.go ================================================ package errcode import ( "fmt" "net/http" "sort" "sync" ) var ( errorCodeToDescriptors = map[ErrorCode]ErrorDescriptor{} idToDescriptors = map[string]ErrorDescriptor{} groupToDescriptors = map[string][]ErrorDescriptor{} ) var ( // ErrorCodeUnknown is a generic error that can be used as a last // resort if there is no situation-specific error message that can be used ErrorCodeUnknown = Register("errcode", ErrorDescriptor{ Value: "UNKNOWN", Message: "unknown error", Description: `Generic error returned when the error does not have an API classification.`, HTTPStatusCode: http.StatusInternalServerError, }) // ErrorCodeUnsupported is returned when an operation is not supported. ErrorCodeUnsupported = Register("errcode", ErrorDescriptor{ Value: "UNSUPPORTED", Message: "The operation is unsupported.", Description: `The operation was unsupported due to a missing implementation or invalid set of parameters.`, HTTPStatusCode: http.StatusMethodNotAllowed, }) // ErrorCodeUnauthorized is returned if a request requires // authentication. ErrorCodeUnauthorized = Register("errcode", ErrorDescriptor{ Value: "UNAUTHORIZED", Message: "authentication required", Description: `The access controller was unable to authenticate the client. Often this will be accompanied by a Www-Authenticate HTTP response header indicating how to authenticate.`, HTTPStatusCode: http.StatusUnauthorized, }) // ErrorCodeDenied is returned if a client does not have sufficient // permission to perform an action. ErrorCodeDenied = Register("errcode", ErrorDescriptor{ Value: "DENIED", Message: "requested access to the resource is denied", Description: `The access controller denied access for the operation on a resource.`, HTTPStatusCode: http.StatusForbidden, }) // ErrorCodeUnavailable provides a common error to report unavailability // of a service or endpoint. ErrorCodeUnavailable = Register("errcode", ErrorDescriptor{ Value: "UNAVAILABLE", Message: "service unavailable", Description: "Returned when a service is not available", HTTPStatusCode: http.StatusServiceUnavailable, }) // ErrorCodeTooManyRequests is returned if a client attempts too many // times to contact a service endpoint. ErrorCodeTooManyRequests = Register("errcode", ErrorDescriptor{ Value: "TOOMANYREQUESTS", Message: "too many requests", Description: `Returned when a client attempts to contact a service too many times`, HTTPStatusCode: http.StatusTooManyRequests, }) ) var nextCode = 1000 var registerLock sync.Mutex // Register will make the passed-in error known to the environment and // return a new ErrorCode func Register(group string, descriptor ErrorDescriptor) ErrorCode { registerLock.Lock() defer registerLock.Unlock() descriptor.Code = ErrorCode(nextCode) if _, ok := idToDescriptors[descriptor.Value]; ok { panic(fmt.Sprintf("ErrorValue %q is already registered", descriptor.Value)) } if _, ok := errorCodeToDescriptors[descriptor.Code]; ok { panic(fmt.Sprintf("ErrorCode %v is already registered", descriptor.Code)) } groupToDescriptors[group] = append(groupToDescriptors[group], descriptor) errorCodeToDescriptors[descriptor.Code] = descriptor idToDescriptors[descriptor.Value] = descriptor nextCode++ return descriptor.Code } type byValue []ErrorDescriptor func (a byValue) Len() int { return len(a) } func (a byValue) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a byValue) Less(i, j int) bool { return a[i].Value < a[j].Value } // GetGroupNames returns the list of Error group names that are registered func GetGroupNames() []string { keys := []string{} for k := range groupToDescriptors { keys = append(keys, k) } sort.Strings(keys) return keys } // GetErrorCodeGroup returns the named group of error descriptors func GetErrorCodeGroup(name string) []ErrorDescriptor { desc := groupToDescriptors[name] sort.Sort(byValue(desc)) return desc } // GetErrorAllDescriptors returns a slice of all ErrorDescriptors that are // registered, irrespective of what group they're in func GetErrorAllDescriptors() []ErrorDescriptor { result := []ErrorDescriptor{} for _, group := range GetGroupNames() { result = append(result, GetErrorCodeGroup(group)...) } sort.Sort(byValue(result)) return result } ================================================ FILE: vendor/github.com/docker/distribution/registry/api/v2/descriptors.go ================================================ package v2 import ( "net/http" "regexp" "github.com/distribution/reference" "github.com/docker/distribution/registry/api/errcode" "github.com/opencontainers/go-digest" ) var ( nameParameterDescriptor = ParameterDescriptor{ Name: "name", Type: "string", Format: reference.NameRegexp.String(), Required: true, Description: `Name of the target repository.`, } referenceParameterDescriptor = ParameterDescriptor{ Name: "reference", Type: "string", Format: reference.TagRegexp.String(), Required: true, Description: `Tag or digest of the target manifest.`, } uuidParameterDescriptor = ParameterDescriptor{ Name: "uuid", Type: "opaque", Required: true, Description: "A uuid identifying the upload. This field can accept characters that match `[a-zA-Z0-9-_.=]+`.", } digestPathParameter = ParameterDescriptor{ Name: "digest", Type: "path", Required: true, Format: digest.DigestRegexp.String(), Description: `Digest of desired blob.`, } hostHeader = ParameterDescriptor{ Name: "Host", Type: "string", Description: "Standard HTTP Host Header. Should be set to the registry host.", Format: "", Examples: []string{"registry-1.docker.io"}, } authHeader = ParameterDescriptor{ Name: "Authorization", Type: "string", Description: "An RFC7235 compliant authorization header.", Format: " ", Examples: []string{"Bearer dGhpcyBpcyBhIGZha2UgYmVhcmVyIHRva2VuIQ=="}, } authChallengeHeader = ParameterDescriptor{ Name: "WWW-Authenticate", Type: "string", Description: "An RFC7235 compliant authentication challenge header.", Format: ` realm="", ..."`, Examples: []string{ `Bearer realm="https://auth.docker.com/", service="registry.docker.com", scopes="repository:library/ubuntu:pull"`, }, } contentLengthZeroHeader = ParameterDescriptor{ Name: "Content-Length", Description: "The `Content-Length` header must be zero and the body must be empty.", Type: "integer", Format: "0", } dockerUploadUUIDHeader = ParameterDescriptor{ Name: "Docker-Upload-UUID", Description: "Identifies the docker upload uuid for the current request.", Type: "uuid", Format: "", } digestHeader = ParameterDescriptor{ Name: "Docker-Content-Digest", Description: "Digest of the targeted content for the request.", Type: "digest", Format: "", } linkHeader = ParameterDescriptor{ Name: "Link", Type: "link", Description: "RFC5988 compliant rel='next' with URL to next result set, if available", Format: `<?n=&last=>; rel="next"`, } paginationParameters = []ParameterDescriptor{ { Name: "n", Type: "integer", Description: "Limit the number of entries in each response. It not present, all entries will be returned.", Format: "", Required: false, }, { Name: "last", Type: "string", Description: "Result set will include values lexically after last.", Format: "", Required: false, }, } unauthorizedResponseDescriptor = ResponseDescriptor{ Name: "Authentication Required", StatusCode: http.StatusUnauthorized, Description: "The client is not authenticated.", Headers: []ParameterDescriptor{ authChallengeHeader, { Name: "Content-Length", Type: "integer", Description: "Length of the JSON response body.", Format: "", }, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, ErrorCodes: []errcode.ErrorCode{ errcode.ErrorCodeUnauthorized, }, } invalidPaginationResponseDescriptor = ResponseDescriptor{ Name: "Invalid pagination number", Description: "The received parameter n was invalid in some way, as described by the error code. The client should resolve the issue and retry the request.", StatusCode: http.StatusBadRequest, Body: BodyDescriptor{ ContentType: "application/json", Format: errorsBody, }, ErrorCodes: []errcode.ErrorCode{ ErrorCodePaginationNumberInvalid, }, } repositoryNotFoundResponseDescriptor = ResponseDescriptor{ Name: "No Such Repository Error", StatusCode: http.StatusNotFound, Description: "The repository is not known to the registry.", Headers: []ParameterDescriptor{ { Name: "Content-Length", Type: "integer", Description: "Length of the JSON response body.", Format: "", }, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, ErrorCodes: []errcode.ErrorCode{ ErrorCodeNameUnknown, }, } deniedResponseDescriptor = ResponseDescriptor{ Name: "Access Denied", StatusCode: http.StatusForbidden, Description: "The client does not have required access to the repository.", Headers: []ParameterDescriptor{ { Name: "Content-Length", Type: "integer", Description: "Length of the JSON response body.", Format: "", }, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, ErrorCodes: []errcode.ErrorCode{ errcode.ErrorCodeDenied, }, } tooManyRequestsDescriptor = ResponseDescriptor{ Name: "Too Many Requests", StatusCode: http.StatusTooManyRequests, Description: "The client made too many requests within a time interval.", Headers: []ParameterDescriptor{ { Name: "Content-Length", Type: "integer", Description: "Length of the JSON response body.", Format: "", }, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, ErrorCodes: []errcode.ErrorCode{ errcode.ErrorCodeTooManyRequests, }, } ) const ( manifestBody = `{ "name": , "tag": , "fsLayers": [ { "blobSum": "" }, ... ] ], "history": , "signature": }` errorsBody = `{ "errors:" [ { "code": , "message": "", "detail": ... }, ... ] }` ) // APIDescriptor exports descriptions of the layout of the v2 registry API. var APIDescriptor = struct { // RouteDescriptors provides a list of the routes available in the API. RouteDescriptors []RouteDescriptor }{ RouteDescriptors: routeDescriptors, } // RouteDescriptor describes a route specified by name. type RouteDescriptor struct { // Name is the name of the route, as specified in RouteNameXXX exports. // These names a should be considered a unique reference for a route. If // the route is registered with gorilla, this is the name that will be // used. Name string // Path is a gorilla/mux-compatible regexp that can be used to match the // route. For any incoming method and path, only one route descriptor // should match. Path string // Entity should be a short, human-readalbe description of the object // targeted by the endpoint. Entity string // Description should provide an accurate overview of the functionality // provided by the route. Description string // Methods should describe the various HTTP methods that may be used on // this route, including request and response formats. Methods []MethodDescriptor } // MethodDescriptor provides a description of the requests that may be // conducted with the target method. type MethodDescriptor struct { // Method is an HTTP method, such as GET, PUT or POST. Method string // Description should provide an overview of the functionality provided by // the covered method, suitable for use in documentation. Use of markdown // here is encouraged. Description string // Requests is a slice of request descriptors enumerating how this // endpoint may be used. Requests []RequestDescriptor } // RequestDescriptor covers a particular set of headers and parameters that // can be carried out with the parent method. Its most helpful to have one // RequestDescriptor per API use case. type RequestDescriptor struct { // Name provides a short identifier for the request, usable as a title or // to provide quick context for the particular request. Name string // Description should cover the requests purpose, covering any details for // this particular use case. Description string // Headers describes headers that must be used with the HTTP request. Headers []ParameterDescriptor // PathParameters enumerate the parameterized path components for the // given request, as defined in the route's regular expression. PathParameters []ParameterDescriptor // QueryParameters provides a list of query parameters for the given // request. QueryParameters []ParameterDescriptor // Body describes the format of the request body. Body BodyDescriptor // Successes enumerates the possible responses that are considered to be // the result of a successful request. Successes []ResponseDescriptor // Failures covers the possible failures from this particular request. Failures []ResponseDescriptor } // ResponseDescriptor describes the components of an API response. type ResponseDescriptor struct { // Name provides a short identifier for the response, usable as a title or // to provide quick context for the particular response. Name string // Description should provide a brief overview of the role of the // response. Description string // StatusCode specifies the status received by this particular response. StatusCode int // Headers covers any headers that may be returned from the response. Headers []ParameterDescriptor // Fields describes any fields that may be present in the response. Fields []ParameterDescriptor // ErrorCodes enumerates the error codes that may be returned along with // the response. ErrorCodes []errcode.ErrorCode // Body describes the body of the response, if any. Body BodyDescriptor } // BodyDescriptor describes a request body and its expected content type. For // the most part, it should be example json or some placeholder for body // data in documentation. type BodyDescriptor struct { ContentType string Format string } // ParameterDescriptor describes the format of a request parameter, which may // be a header, path parameter or query parameter. type ParameterDescriptor struct { // Name is the name of the parameter, either of the path component or // query parameter. Name string // Type specifies the type of the parameter, such as string, integer, etc. Type string // Description provides a human-readable description of the parameter. Description string // Required means the field is required when set. Required bool // Format is a specifying the string format accepted by this parameter. Format string // Regexp is a compiled regular expression that can be used to validate // the contents of the parameter. Regexp *regexp.Regexp // Examples provides multiple examples for the values that might be valid // for this parameter. Examples []string } var routeDescriptors = []RouteDescriptor{ { Name: RouteNameBase, Path: "/v2/", Entity: "Base", Description: `Base V2 API route. Typically, this can be used for lightweight version checks and to validate registry authentication.`, Methods: []MethodDescriptor{ { Method: "GET", Description: "Check that the endpoint implements Docker Registry API V2.", Requests: []RequestDescriptor{ { Headers: []ParameterDescriptor{ hostHeader, authHeader, }, Successes: []ResponseDescriptor{ { Description: "The API implements V2 protocol and is accessible.", StatusCode: http.StatusOK, }, }, Failures: []ResponseDescriptor{ { Description: "The registry does not implement the V2 API.", StatusCode: http.StatusNotFound, }, unauthorizedResponseDescriptor, tooManyRequestsDescriptor, }, }, }, }, }, }, { Name: RouteNameTags, Path: "/v2/{name:" + reference.NameRegexp.String() + "}/tags/list", Entity: "Tags", Description: "Retrieve information about tags.", Methods: []MethodDescriptor{ { Method: "GET", Description: "Fetch the tags under the repository identified by `name`.", Requests: []RequestDescriptor{ { Name: "Tags", Description: "Return all tags for the repository", Headers: []ParameterDescriptor{ hostHeader, authHeader, }, PathParameters: []ParameterDescriptor{ nameParameterDescriptor, }, Successes: []ResponseDescriptor{ { StatusCode: http.StatusOK, Description: "A list of tags for the named repository.", Headers: []ParameterDescriptor{ { Name: "Content-Length", Type: "integer", Description: "Length of the JSON response body.", Format: "", }, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: `{ "name": , "tags": [ , ... ] }`, }, }, }, Failures: []ResponseDescriptor{ unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, { Name: "Tags Paginated", Description: "Return a portion of the tags for the specified repository.", PathParameters: []ParameterDescriptor{nameParameterDescriptor}, QueryParameters: paginationParameters, Successes: []ResponseDescriptor{ { StatusCode: http.StatusOK, Description: "A list of tags for the named repository.", Headers: []ParameterDescriptor{ { Name: "Content-Length", Type: "integer", Description: "Length of the JSON response body.", Format: "", }, linkHeader, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: `{ "name": , "tags": [ , ... ], }`, }, }, }, Failures: []ResponseDescriptor{ invalidPaginationResponseDescriptor, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, }, }, }, }, { Name: RouteNameManifest, Path: "/v2/{name:" + reference.NameRegexp.String() + "}/manifests/{reference:" + reference.TagRegexp.String() + "|" + digest.DigestRegexp.String() + "}", Entity: "Manifest", Description: "Create, update, delete and retrieve manifests.", Methods: []MethodDescriptor{ { Method: "GET", Description: "Fetch the manifest identified by `name` and `reference` where `reference` can be a tag or digest. A `HEAD` request can also be issued to this endpoint to obtain resource information without receiving all data.", Requests: []RequestDescriptor{ { Headers: []ParameterDescriptor{ hostHeader, authHeader, }, PathParameters: []ParameterDescriptor{ nameParameterDescriptor, referenceParameterDescriptor, }, Successes: []ResponseDescriptor{ { Description: "The manifest identified by `name` and `reference`. The contents can be used to identify and resolve resources required to run the specified image.", StatusCode: http.StatusOK, Headers: []ParameterDescriptor{ digestHeader, }, Body: BodyDescriptor{ ContentType: "", Format: manifestBody, }, }, }, Failures: []ResponseDescriptor{ { Description: "The name or reference was invalid.", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeNameInvalid, ErrorCodeTagInvalid, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, }, }, { Method: "PUT", Description: "Put the manifest identified by `name` and `reference` where `reference` can be a tag or digest.", Requests: []RequestDescriptor{ { Headers: []ParameterDescriptor{ hostHeader, authHeader, }, PathParameters: []ParameterDescriptor{ nameParameterDescriptor, referenceParameterDescriptor, }, Body: BodyDescriptor{ ContentType: "", Format: manifestBody, }, Successes: []ResponseDescriptor{ { Description: "The manifest has been accepted by the registry and is stored under the specified `name` and `tag`.", StatusCode: http.StatusCreated, Headers: []ParameterDescriptor{ { Name: "Location", Type: "url", Description: "The canonical location url of the uploaded manifest.", Format: "", }, contentLengthZeroHeader, digestHeader, }, }, }, Failures: []ResponseDescriptor{ { Name: "Invalid Manifest", Description: "The received manifest was invalid in some way, as described by the error codes. The client should resolve the issue and retry the request.", StatusCode: http.StatusBadRequest, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, ErrorCodes: []errcode.ErrorCode{ ErrorCodeNameInvalid, ErrorCodeTagInvalid, ErrorCodeManifestInvalid, ErrorCodeManifestUnverified, ErrorCodeBlobUnknown, }, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, { Name: "Missing Layer(s)", Description: "One or more layers may be missing during a manifest upload. If so, the missing layers will be enumerated in the error response.", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeBlobUnknown, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: `{ "errors:" [{ "code": "BLOB_UNKNOWN", "message": "blob unknown to registry", "detail": { "digest": "" } }, ... ] }`, }, }, { Name: "Not allowed", Description: "Manifest put is not allowed because the registry is configured as a pull-through cache or for some other reason", StatusCode: http.StatusMethodNotAllowed, ErrorCodes: []errcode.ErrorCode{ errcode.ErrorCodeUnsupported, }, }, }, }, }, }, { Method: "DELETE", Description: "Delete the manifest identified by `name` and `reference`. Note that a manifest can _only_ be deleted by `digest`.", Requests: []RequestDescriptor{ { Headers: []ParameterDescriptor{ hostHeader, authHeader, }, PathParameters: []ParameterDescriptor{ nameParameterDescriptor, referenceParameterDescriptor, }, Successes: []ResponseDescriptor{ { StatusCode: http.StatusAccepted, }, }, Failures: []ResponseDescriptor{ { Name: "Invalid Name or Reference", Description: "The specified `name` or `reference` were invalid and the delete was unable to proceed.", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeNameInvalid, ErrorCodeTagInvalid, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, { Name: "Unknown Manifest", Description: "The specified `name` or `reference` are unknown to the registry and the delete was unable to proceed. Clients can assume the manifest was already deleted if this response is returned.", StatusCode: http.StatusNotFound, ErrorCodes: []errcode.ErrorCode{ ErrorCodeNameUnknown, ErrorCodeManifestUnknown, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, { Name: "Not allowed", Description: "Manifest delete is not allowed because the registry is configured as a pull-through cache or `delete` has been disabled.", StatusCode: http.StatusMethodNotAllowed, ErrorCodes: []errcode.ErrorCode{ errcode.ErrorCodeUnsupported, }, }, }, }, }, }, }, }, { Name: RouteNameBlob, Path: "/v2/{name:" + reference.NameRegexp.String() + "}/blobs/{digest:" + digest.DigestRegexp.String() + "}", Entity: "Blob", Description: "Operations on blobs identified by `name` and `digest`. Used to fetch or delete layers by digest.", Methods: []MethodDescriptor{ { Method: "GET", Description: "Retrieve the blob from the registry identified by `digest`. A `HEAD` request can also be issued to this endpoint to obtain resource information without receiving all data.", Requests: []RequestDescriptor{ { Name: "Fetch Blob", Headers: []ParameterDescriptor{ hostHeader, authHeader, }, PathParameters: []ParameterDescriptor{ nameParameterDescriptor, digestPathParameter, }, Successes: []ResponseDescriptor{ { Description: "The blob identified by `digest` is available. The blob content will be present in the body of the request.", StatusCode: http.StatusOK, Headers: []ParameterDescriptor{ { Name: "Content-Length", Type: "integer", Description: "The length of the requested blob content.", Format: "", }, digestHeader, }, Body: BodyDescriptor{ ContentType: "application/octet-stream", Format: "", }, }, { Description: "The blob identified by `digest` is available at the provided location.", StatusCode: http.StatusTemporaryRedirect, Headers: []ParameterDescriptor{ { Name: "Location", Type: "url", Description: "The location where the layer should be accessible.", Format: "", }, digestHeader, }, }, }, Failures: []ResponseDescriptor{ { Description: "There was a problem with the request that needs to be addressed by the client, such as an invalid `name` or `tag`.", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeNameInvalid, ErrorCodeDigestInvalid, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, { Description: "The blob, identified by `name` and `digest`, is unknown to the registry.", StatusCode: http.StatusNotFound, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, ErrorCodes: []errcode.ErrorCode{ ErrorCodeNameUnknown, ErrorCodeBlobUnknown, }, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, { Name: "Fetch Blob Part", Description: "This endpoint may also support RFC7233 compliant range requests. Support can be detected by issuing a HEAD request. If the header `Accept-Range: bytes` is returned, range requests can be used to fetch partial content.", Headers: []ParameterDescriptor{ hostHeader, authHeader, { Name: "Range", Type: "string", Description: "HTTP Range header specifying blob chunk.", Format: "bytes=-", }, }, PathParameters: []ParameterDescriptor{ nameParameterDescriptor, digestPathParameter, }, Successes: []ResponseDescriptor{ { Description: "The blob identified by `digest` is available. The specified chunk of blob content will be present in the body of the request.", StatusCode: http.StatusPartialContent, Headers: []ParameterDescriptor{ { Name: "Content-Length", Type: "integer", Description: "The length of the requested blob chunk.", Format: "", }, { Name: "Content-Range", Type: "byte range", Description: "Content range of blob chunk.", Format: "bytes -/", }, }, Body: BodyDescriptor{ ContentType: "application/octet-stream", Format: "", }, }, }, Failures: []ResponseDescriptor{ { Description: "There was a problem with the request that needs to be addressed by the client, such as an invalid `name` or `tag`.", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeNameInvalid, ErrorCodeDigestInvalid, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, { StatusCode: http.StatusNotFound, ErrorCodes: []errcode.ErrorCode{ ErrorCodeNameUnknown, ErrorCodeBlobUnknown, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, { Description: "The range specification cannot be satisfied for the requested content. This can happen when the range is not formatted correctly or if the range is outside of the valid size of the content.", StatusCode: http.StatusRequestedRangeNotSatisfiable, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, }, }, { Method: "DELETE", Description: "Delete the blob identified by `name` and `digest`", Requests: []RequestDescriptor{ { Headers: []ParameterDescriptor{ hostHeader, authHeader, }, PathParameters: []ParameterDescriptor{ nameParameterDescriptor, digestPathParameter, }, Successes: []ResponseDescriptor{ { StatusCode: http.StatusAccepted, Headers: []ParameterDescriptor{ { Name: "Content-Length", Type: "integer", Description: "0", Format: "0", }, digestHeader, }, }, }, Failures: []ResponseDescriptor{ { Name: "Invalid Name or Digest", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeDigestInvalid, ErrorCodeNameInvalid, }, }, { Description: "The blob, identified by `name` and `digest`, is unknown to the registry.", StatusCode: http.StatusNotFound, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, ErrorCodes: []errcode.ErrorCode{ ErrorCodeNameUnknown, ErrorCodeBlobUnknown, }, }, { Description: "Blob delete is not allowed because the registry is configured as a pull-through cache or `delete` has been disabled", StatusCode: http.StatusMethodNotAllowed, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, ErrorCodes: []errcode.ErrorCode{ errcode.ErrorCodeUnsupported, }, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, }, }, // TODO(stevvooe): We may want to add a PUT request here to // kickoff an upload of a blob, integrated with the blob upload // API. }, }, { Name: RouteNameBlobUpload, Path: "/v2/{name:" + reference.NameRegexp.String() + "}/blobs/uploads/", Entity: "Initiate Blob Upload", Description: "Initiate a blob upload. This endpoint can be used to create resumable uploads or monolithic uploads.", Methods: []MethodDescriptor{ { Method: "POST", Description: "Initiate a resumable blob upload. If successful, an upload location will be provided to complete the upload. Optionally, if the `digest` parameter is present, the request body will be used to complete the upload in a single request.", Requests: []RequestDescriptor{ { Name: "Initiate Monolithic Blob Upload", Description: "Upload a blob identified by the `digest` parameter in single request. This upload will not be resumable unless a recoverable error is returned.", Headers: []ParameterDescriptor{ hostHeader, authHeader, { Name: "Content-Length", Type: "integer", Format: "", }, }, PathParameters: []ParameterDescriptor{ nameParameterDescriptor, }, QueryParameters: []ParameterDescriptor{ { Name: "digest", Type: "query", Format: "", Regexp: digest.DigestRegexp, Description: `Digest of uploaded blob. If present, the upload will be completed, in a single request, with contents of the request body as the resulting blob.`, }, }, Body: BodyDescriptor{ ContentType: "application/octect-stream", Format: "", }, Successes: []ResponseDescriptor{ { Description: "The blob has been created in the registry and is available at the provided location.", StatusCode: http.StatusCreated, Headers: []ParameterDescriptor{ { Name: "Location", Type: "url", Format: "", }, contentLengthZeroHeader, dockerUploadUUIDHeader, }, }, }, Failures: []ResponseDescriptor{ { Name: "Invalid Name or Digest", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeDigestInvalid, ErrorCodeNameInvalid, }, }, { Name: "Not allowed", Description: "Blob upload is not allowed because the registry is configured as a pull-through cache or for some other reason", StatusCode: http.StatusMethodNotAllowed, ErrorCodes: []errcode.ErrorCode{ errcode.ErrorCodeUnsupported, }, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, { Name: "Initiate Resumable Blob Upload", Description: "Initiate a resumable blob upload with an empty request body.", Headers: []ParameterDescriptor{ hostHeader, authHeader, contentLengthZeroHeader, }, PathParameters: []ParameterDescriptor{ nameParameterDescriptor, }, Successes: []ResponseDescriptor{ { Description: "The upload has been created. The `Location` header must be used to complete the upload. The response should be identical to a `GET` request on the contents of the returned `Location` header.", StatusCode: http.StatusAccepted, Headers: []ParameterDescriptor{ contentLengthZeroHeader, { Name: "Location", Type: "url", Format: "/v2//blobs/uploads/", Description: "The location of the created upload. Clients should use the contents verbatim to complete the upload, adding parameters where required.", }, { Name: "Range", Format: "0-0", Description: "Range header indicating the progress of the upload. When starting an upload, it will return an empty range, since no content has been received.", }, dockerUploadUUIDHeader, }, }, }, Failures: []ResponseDescriptor{ { Name: "Invalid Name or Digest", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeDigestInvalid, ErrorCodeNameInvalid, }, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, { Name: "Mount Blob", Description: "Mount a blob identified by the `mount` parameter from another repository.", Headers: []ParameterDescriptor{ hostHeader, authHeader, contentLengthZeroHeader, }, PathParameters: []ParameterDescriptor{ nameParameterDescriptor, }, QueryParameters: []ParameterDescriptor{ { Name: "mount", Type: "query", Format: "", Regexp: digest.DigestRegexp, Description: `Digest of blob to mount from the source repository.`, }, { Name: "from", Type: "query", Format: "", Regexp: reference.NameRegexp, Description: `Name of the source repository.`, }, }, Successes: []ResponseDescriptor{ { Description: "The blob has been mounted in the repository and is available at the provided location.", StatusCode: http.StatusCreated, Headers: []ParameterDescriptor{ { Name: "Location", Type: "url", Format: "", }, contentLengthZeroHeader, dockerUploadUUIDHeader, }, }, }, Failures: []ResponseDescriptor{ { Name: "Invalid Name or Digest", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeDigestInvalid, ErrorCodeNameInvalid, }, }, { Name: "Not allowed", Description: "Blob mount is not allowed because the registry is configured as a pull-through cache or for some other reason", StatusCode: http.StatusMethodNotAllowed, ErrorCodes: []errcode.ErrorCode{ errcode.ErrorCodeUnsupported, }, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, }, }, }, }, { Name: RouteNameBlobUploadChunk, Path: "/v2/{name:" + reference.NameRegexp.String() + "}/blobs/uploads/{uuid:[a-zA-Z0-9-_.=]+}", Entity: "Blob Upload", Description: "Interact with blob uploads. Clients should never assemble URLs for this endpoint and should only take it through the `Location` header on related API requests. The `Location` header and its parameters should be preserved by clients, using the latest value returned via upload related API calls.", Methods: []MethodDescriptor{ { Method: "GET", Description: "Retrieve status of upload identified by `uuid`. The primary purpose of this endpoint is to resolve the current status of a resumable upload.", Requests: []RequestDescriptor{ { Description: "Retrieve the progress of the current upload, as reported by the `Range` header.", Headers: []ParameterDescriptor{ hostHeader, authHeader, }, PathParameters: []ParameterDescriptor{ nameParameterDescriptor, uuidParameterDescriptor, }, Successes: []ResponseDescriptor{ { Name: "Upload Progress", Description: "The upload is known and in progress. The last received offset is available in the `Range` header.", StatusCode: http.StatusNoContent, Headers: []ParameterDescriptor{ { Name: "Range", Type: "header", Format: "0-", Description: "Range indicating the current progress of the upload.", }, contentLengthZeroHeader, dockerUploadUUIDHeader, }, }, }, Failures: []ResponseDescriptor{ { Description: "There was an error processing the upload and it must be restarted.", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeDigestInvalid, ErrorCodeNameInvalid, ErrorCodeBlobUploadInvalid, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, { Description: "The upload is unknown to the registry. The upload must be restarted.", StatusCode: http.StatusNotFound, ErrorCodes: []errcode.ErrorCode{ ErrorCodeBlobUploadUnknown, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, }, }, { Method: "PATCH", Description: "Upload a chunk of data for the specified upload.", Requests: []RequestDescriptor{ { Name: "Stream upload", Description: "Upload a stream of data to upload without completing the upload.", PathParameters: []ParameterDescriptor{ nameParameterDescriptor, uuidParameterDescriptor, }, Headers: []ParameterDescriptor{ hostHeader, authHeader, }, Body: BodyDescriptor{ ContentType: "application/octet-stream", Format: "", }, Successes: []ResponseDescriptor{ { Name: "Data Accepted", Description: "The stream of data has been accepted and the current progress is available in the range header. The updated upload location is available in the `Location` header.", StatusCode: http.StatusNoContent, Headers: []ParameterDescriptor{ { Name: "Location", Type: "url", Format: "/v2//blobs/uploads/", Description: "The location of the upload. Clients should assume this changes after each request. Clients should use the contents verbatim to complete the upload, adding parameters where required.", }, { Name: "Range", Type: "header", Format: "0-", Description: "Range indicating the current progress of the upload.", }, contentLengthZeroHeader, dockerUploadUUIDHeader, }, }, }, Failures: []ResponseDescriptor{ { Description: "There was an error processing the upload and it must be restarted.", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeDigestInvalid, ErrorCodeNameInvalid, ErrorCodeBlobUploadInvalid, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, { Description: "The upload is unknown to the registry. The upload must be restarted.", StatusCode: http.StatusNotFound, ErrorCodes: []errcode.ErrorCode{ ErrorCodeBlobUploadUnknown, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, { Name: "Chunked upload", Description: "Upload a chunk of data to specified upload without completing the upload. The data will be uploaded to the specified Content Range.", PathParameters: []ParameterDescriptor{ nameParameterDescriptor, uuidParameterDescriptor, }, Headers: []ParameterDescriptor{ hostHeader, authHeader, { Name: "Content-Range", Type: "header", Format: "-", Required: true, Description: "Range of bytes identifying the desired block of content represented by the body. Start must the end offset retrieved via status check plus one. Note that this is a non-standard use of the `Content-Range` header.", }, { Name: "Content-Length", Type: "integer", Format: "", Description: "Length of the chunk being uploaded, corresponding the length of the request body.", }, }, Body: BodyDescriptor{ ContentType: "application/octet-stream", Format: "", }, Successes: []ResponseDescriptor{ { Name: "Chunk Accepted", Description: "The chunk of data has been accepted and the current progress is available in the range header. The updated upload location is available in the `Location` header.", StatusCode: http.StatusNoContent, Headers: []ParameterDescriptor{ { Name: "Location", Type: "url", Format: "/v2//blobs/uploads/", Description: "The location of the upload. Clients should assume this changes after each request. Clients should use the contents verbatim to complete the upload, adding parameters where required.", }, { Name: "Range", Type: "header", Format: "0-", Description: "Range indicating the current progress of the upload.", }, contentLengthZeroHeader, dockerUploadUUIDHeader, }, }, }, Failures: []ResponseDescriptor{ { Description: "There was an error processing the upload and it must be restarted.", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeDigestInvalid, ErrorCodeNameInvalid, ErrorCodeBlobUploadInvalid, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, { Description: "The upload is unknown to the registry. The upload must be restarted.", StatusCode: http.StatusNotFound, ErrorCodes: []errcode.ErrorCode{ ErrorCodeBlobUploadUnknown, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, { Description: "The `Content-Range` specification cannot be accepted, either because it does not overlap with the current progress or it is invalid.", StatusCode: http.StatusRequestedRangeNotSatisfiable, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, }, }, { Method: "PUT", Description: "Complete the upload specified by `uuid`, optionally appending the body as the final chunk.", Requests: []RequestDescriptor{ { Description: "Complete the upload, providing all the data in the body, if necessary. A request without a body will just complete the upload with previously uploaded content.", Headers: []ParameterDescriptor{ hostHeader, authHeader, { Name: "Content-Length", Type: "integer", Format: "", Description: "Length of the data being uploaded, corresponding to the length of the request body. May be zero if no data is provided.", }, }, PathParameters: []ParameterDescriptor{ nameParameterDescriptor, uuidParameterDescriptor, }, QueryParameters: []ParameterDescriptor{ { Name: "digest", Type: "string", Format: "", Regexp: digest.DigestRegexp, Required: true, Description: `Digest of uploaded blob.`, }, }, Body: BodyDescriptor{ ContentType: "application/octet-stream", Format: "", }, Successes: []ResponseDescriptor{ { Name: "Upload Complete", Description: "The upload has been completed and accepted by the registry. The canonical location will be available in the `Location` header.", StatusCode: http.StatusNoContent, Headers: []ParameterDescriptor{ { Name: "Location", Type: "url", Format: "", Description: "The canonical location of the blob for retrieval", }, { Name: "Content-Range", Type: "header", Format: "-", Description: "Range of bytes identifying the desired block of content represented by the body. Start must match the end of offset retrieved via status check. Note that this is a non-standard use of the `Content-Range` header.", }, contentLengthZeroHeader, digestHeader, }, }, }, Failures: []ResponseDescriptor{ { Description: "There was an error processing the upload and it must be restarted.", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeDigestInvalid, ErrorCodeNameInvalid, ErrorCodeBlobUploadInvalid, errcode.ErrorCodeUnsupported, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, { Description: "The upload is unknown to the registry. The upload must be restarted.", StatusCode: http.StatusNotFound, ErrorCodes: []errcode.ErrorCode{ ErrorCodeBlobUploadUnknown, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, }, }, { Method: "DELETE", Description: "Cancel outstanding upload processes, releasing associated resources. If this is not called, the unfinished uploads will eventually timeout.", Requests: []RequestDescriptor{ { Description: "Cancel the upload specified by `uuid`.", PathParameters: []ParameterDescriptor{ nameParameterDescriptor, uuidParameterDescriptor, }, Headers: []ParameterDescriptor{ hostHeader, authHeader, contentLengthZeroHeader, }, Successes: []ResponseDescriptor{ { Name: "Upload Deleted", Description: "The upload has been successfully deleted.", StatusCode: http.StatusNoContent, Headers: []ParameterDescriptor{ contentLengthZeroHeader, }, }, }, Failures: []ResponseDescriptor{ { Description: "An error was encountered processing the delete. The client may ignore this error.", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeNameInvalid, ErrorCodeBlobUploadInvalid, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, { Description: "The upload is unknown to the registry. The client may ignore this error and assume the upload has been deleted.", StatusCode: http.StatusNotFound, ErrorCodes: []errcode.ErrorCode{ ErrorCodeBlobUploadUnknown, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, }, }, }, }, { Name: RouteNameCatalog, Path: "/v2/_catalog", Entity: "Catalog", Description: "List a set of available repositories in the local registry cluster. Does not provide any indication of what may be available upstream. Applications can only determine if a repository is available but not if it is not available.", Methods: []MethodDescriptor{ { Method: "GET", Description: "Retrieve a sorted, json list of repositories available in the registry.", Requests: []RequestDescriptor{ { Name: "Catalog Fetch", Description: "Request an unabridged list of repositories available. The implementation may impose a maximum limit and return a partial set with pagination links.", Successes: []ResponseDescriptor{ { Description: "Returns the unabridged list of repositories as a json response.", StatusCode: http.StatusOK, Headers: []ParameterDescriptor{ { Name: "Content-Length", Type: "integer", Description: "Length of the JSON response body.", Format: "", }, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: `{ "repositories": [ , ... ] }`, }, }, }, }, { Name: "Catalog Fetch Paginated", Description: "Return the specified portion of repositories.", QueryParameters: paginationParameters, Successes: []ResponseDescriptor{ { StatusCode: http.StatusOK, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: `{ "repositories": [ , ... ] "next": "?last=&n=" }`, }, Headers: []ParameterDescriptor{ { Name: "Content-Length", Type: "integer", Description: "Length of the JSON response body.", Format: "", }, linkHeader, }, }, }, Failures: []ResponseDescriptor{ invalidPaginationResponseDescriptor, }, }, }, }, }, }, } var routeDescriptorsMap map[string]RouteDescriptor func init() { routeDescriptorsMap = make(map[string]RouteDescriptor, len(routeDescriptors)) for _, descriptor := range routeDescriptors { routeDescriptorsMap[descriptor.Name] = descriptor } } ================================================ FILE: vendor/github.com/docker/distribution/registry/api/v2/doc.go ================================================ // Package v2 describes routes, urls and the error codes used in the Docker // Registry JSON HTTP API V2. In addition to declarations, descriptors are // provided for routes and error codes that can be used for implementation and // automatically generating documentation. // // Definitions here are considered to be locked down for the V2 registry api. // Any changes must be considered carefully and should not proceed without a // change proposal in docker core. package v2 ================================================ FILE: vendor/github.com/docker/distribution/registry/api/v2/errors.go ================================================ package v2 import ( "net/http" "github.com/docker/distribution/registry/api/errcode" ) const errGroup = "registry.api.v2" var ( // ErrorCodeDigestInvalid is returned when uploading a blob if the // provided digest does not match the blob contents. ErrorCodeDigestInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "DIGEST_INVALID", Message: "provided digest did not match uploaded content", Description: `When a blob is uploaded, the registry will check that the content matches the digest provided by the client. The error may include a detail structure with the key "digest", including the invalid digest string. This error may also be returned when a manifest includes an invalid layer digest.`, HTTPStatusCode: http.StatusBadRequest, }) // ErrorCodeSizeInvalid is returned when uploading a blob if the provided ErrorCodeSizeInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "SIZE_INVALID", Message: "provided length did not match content length", Description: `When a layer is uploaded, the provided size will be checked against the uploaded content. If they do not match, this error will be returned.`, HTTPStatusCode: http.StatusBadRequest, }) // ErrorCodeNameInvalid is returned when the name in the manifest does not // match the provided name. ErrorCodeNameInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "NAME_INVALID", Message: "invalid repository name", Description: `Invalid repository name encountered either during manifest validation or any API operation.`, HTTPStatusCode: http.StatusBadRequest, }) // ErrorCodeTagInvalid is returned when the tag in the manifest does not // match the provided tag. ErrorCodeTagInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "TAG_INVALID", Message: "manifest tag did not match URI", Description: `During a manifest upload, if the tag in the manifest does not match the uri tag, this error will be returned.`, HTTPStatusCode: http.StatusBadRequest, }) // ErrorCodeNameUnknown when the repository name is not known. ErrorCodeNameUnknown = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "NAME_UNKNOWN", Message: "repository name not known to registry", Description: `This is returned if the name used during an operation is unknown to the registry.`, HTTPStatusCode: http.StatusNotFound, }) // ErrorCodeManifestUnknown returned when image manifest is unknown. ErrorCodeManifestUnknown = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "MANIFEST_UNKNOWN", Message: "manifest unknown", Description: `This error is returned when the manifest, identified by name and tag is unknown to the repository.`, HTTPStatusCode: http.StatusNotFound, }) // ErrorCodeManifestInvalid returned when an image manifest is invalid, // typically during a PUT operation. This error encompasses all errors // encountered during manifest validation that aren't signature errors. ErrorCodeManifestInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "MANIFEST_INVALID", Message: "manifest invalid", Description: `During upload, manifests undergo several checks ensuring validity. If those checks fail, this error may be returned, unless a more specific error is included. The detail will contain information the failed validation.`, HTTPStatusCode: http.StatusBadRequest, }) // ErrorCodeManifestUnverified is returned when the manifest fails // signature verification. ErrorCodeManifestUnverified = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "MANIFEST_UNVERIFIED", Message: "manifest failed signature verification", Description: `During manifest upload, if the manifest fails signature verification, this error will be returned.`, HTTPStatusCode: http.StatusBadRequest, }) // ErrorCodeManifestBlobUnknown is returned when a manifest blob is // unknown to the registry. ErrorCodeManifestBlobUnknown = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "MANIFEST_BLOB_UNKNOWN", Message: "blob unknown to registry", Description: `This error may be returned when a manifest blob is unknown to the registry.`, HTTPStatusCode: http.StatusBadRequest, }) // ErrorCodeBlobUnknown is returned when a blob is unknown to the // registry. This can happen when the manifest references a nonexistent // layer or the result is not found by a blob fetch. ErrorCodeBlobUnknown = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "BLOB_UNKNOWN", Message: "blob unknown to registry", Description: `This error may be returned when a blob is unknown to the registry in a specified repository. This can be returned with a standard get or if a manifest references an unknown layer during upload.`, HTTPStatusCode: http.StatusNotFound, }) // ErrorCodeBlobUploadUnknown is returned when an upload is unknown. ErrorCodeBlobUploadUnknown = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "BLOB_UPLOAD_UNKNOWN", Message: "blob upload unknown to registry", Description: `If a blob upload has been cancelled or was never started, this error code may be returned.`, HTTPStatusCode: http.StatusNotFound, }) // ErrorCodeBlobUploadInvalid is returned when an upload is invalid. ErrorCodeBlobUploadInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "BLOB_UPLOAD_INVALID", Message: "blob upload invalid", Description: `The blob upload encountered an error and can no longer proceed.`, HTTPStatusCode: http.StatusNotFound, }) ErrorCodePaginationNumberInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "PAGINATION_NUMBER_INVALID", Message: "invalid number of results requested", Description: `Returned when the "n" parameter (number of results to return) is not an integer, "n" is negative or "n" is bigger than the maximum allowed.`, HTTPStatusCode: http.StatusBadRequest, }) ) ================================================ FILE: vendor/github.com/docker/distribution/registry/api/v2/headerparser.go ================================================ package v2 import ( "fmt" "regexp" "strings" "unicode" ) var ( // according to rfc7230 reToken = regexp.MustCompile(`^[^"(),/:;<=>?@[\]{}[:space:][:cntrl:]]+`) reQuotedValue = regexp.MustCompile(`^[^\\"]+`) reEscapedCharacter = regexp.MustCompile(`^[[:blank:][:graph:]]`) ) // parseForwardedHeader is a benevolent parser of Forwarded header defined in rfc7239. The header contains // a comma-separated list of forwarding key-value pairs. Each list element is set by single proxy. The // function parses only the first element of the list, which is set by the very first proxy. It returns a map // of corresponding key-value pairs and an unparsed slice of the input string. // // Examples of Forwarded header values: // // 1. Forwarded: For=192.0.2.43; Proto=https,For="[2001:db8:cafe::17]",For=unknown // 2. Forwarded: for="192.0.2.43:443"; host="registry.example.org", for="10.10.05.40:80" // // The first will be parsed into {"for": "192.0.2.43", "proto": "https"} while the second into // {"for": "192.0.2.43:443", "host": "registry.example.org"}. func parseForwardedHeader(forwarded string) (map[string]string, string, error) { // Following are states of forwarded header parser. Any state could transition to a failure. const ( // terminating state; can transition to Parameter stateElement = iota // terminating state; can transition to KeyValueDelimiter stateParameter // can transition to Value stateKeyValueDelimiter // can transition to one of { QuotedValue, PairEnd } stateValue // can transition to one of { EscapedCharacter, PairEnd } stateQuotedValue // can transition to one of { QuotedValue } stateEscapedCharacter // terminating state; can transition to one of { Parameter, Element } statePairEnd ) var ( parameter string value string parse = forwarded[:] res = map[string]string{} state = stateElement ) Loop: for { // skip spaces unless in quoted value if state != stateQuotedValue && state != stateEscapedCharacter { parse = strings.TrimLeftFunc(parse, unicode.IsSpace) } if len(parse) == 0 { if state != stateElement && state != statePairEnd && state != stateParameter { return nil, parse, fmt.Errorf("unexpected end of input") } // terminating break } switch state { // terminate at list element delimiter case stateElement: if parse[0] == ',' { parse = parse[1:] break Loop } state = stateParameter // parse parameter (the key of key-value pair) case stateParameter: match := reToken.FindString(parse) if len(match) == 0 { return nil, parse, fmt.Errorf("failed to parse token at position %d", len(forwarded)-len(parse)) } parameter = strings.ToLower(match) parse = parse[len(match):] state = stateKeyValueDelimiter // parse '=' case stateKeyValueDelimiter: if parse[0] != '=' { return nil, parse, fmt.Errorf("expected '=', not '%c' at position %d", parse[0], len(forwarded)-len(parse)) } parse = parse[1:] state = stateValue // parse value or quoted value case stateValue: if parse[0] == '"' { parse = parse[1:] state = stateQuotedValue } else { value = reToken.FindString(parse) if len(value) == 0 { return nil, parse, fmt.Errorf("failed to parse value at position %d", len(forwarded)-len(parse)) } if _, exists := res[parameter]; exists { return nil, parse, fmt.Errorf("duplicate parameter %q at position %d", parameter, len(forwarded)-len(parse)) } res[parameter] = value parse = parse[len(value):] value = "" state = statePairEnd } // parse a part of quoted value until the first backslash case stateQuotedValue: match := reQuotedValue.FindString(parse) value += match parse = parse[len(match):] switch { case len(parse) == 0: return nil, parse, fmt.Errorf("unterminated quoted string") case parse[0] == '"': res[parameter] = value value = "" parse = parse[1:] state = statePairEnd case parse[0] == '\\': parse = parse[1:] state = stateEscapedCharacter } // parse escaped character in a quoted string, ignore the backslash // transition back to QuotedValue state case stateEscapedCharacter: c := reEscapedCharacter.FindString(parse) if len(c) == 0 { return nil, parse, fmt.Errorf("invalid escape sequence at position %d", len(forwarded)-len(parse)-1) } value += c parse = parse[1:] state = stateQuotedValue // expect either a new key-value pair, new list or end of input case statePairEnd: switch parse[0] { case ';': parse = parse[1:] state = stateParameter case ',': state = stateElement default: return nil, parse, fmt.Errorf("expected ',' or ';', not %c at position %d", parse[0], len(forwarded)-len(parse)) } } } return res, parse, nil } ================================================ FILE: vendor/github.com/docker/distribution/registry/api/v2/routes.go ================================================ package v2 import "github.com/gorilla/mux" // The following are definitions of the name under which all V2 routes are // registered. These symbols can be used to look up a route based on the name. const ( RouteNameBase = "base" RouteNameManifest = "manifest" RouteNameTags = "tags" RouteNameBlob = "blob" RouteNameBlobUpload = "blob-upload" RouteNameBlobUploadChunk = "blob-upload-chunk" RouteNameCatalog = "catalog" ) // Router builds a gorilla router with named routes for the various API // methods. This can be used directly by both server implementations and // clients. func Router() *mux.Router { return RouterWithPrefix("") } // RouterWithPrefix builds a gorilla router with a configured prefix // on all routes. func RouterWithPrefix(prefix string) *mux.Router { rootRouter := mux.NewRouter() router := rootRouter if prefix != "" { router = router.PathPrefix(prefix).Subrouter() } router.StrictSlash(true) for _, descriptor := range routeDescriptors { router.Path(descriptor.Path).Name(descriptor.Name) } return rootRouter } ================================================ FILE: vendor/github.com/docker/distribution/registry/api/v2/urls.go ================================================ package v2 import ( "fmt" "net/http" "net/url" "strings" "github.com/distribution/reference" "github.com/gorilla/mux" ) // URLBuilder creates registry API urls from a single base endpoint. It can be // used to create urls for use in a registry client or server. // // All urls will be created from the given base, including the api version. // For example, if a root of "/foo/" is provided, urls generated will be fall // under "/foo/v2/...". Most application will only provide a schema, host and // port, such as "https://localhost:5000/". type URLBuilder struct { root *url.URL // url root (ie http://localhost/) router *mux.Router relative bool } // NewURLBuilder creates a URLBuilder with provided root url object. func NewURLBuilder(root *url.URL, relative bool) *URLBuilder { return &URLBuilder{ root: root, router: Router(), relative: relative, } } // NewURLBuilderFromString workes identically to NewURLBuilder except it takes // a string argument for the root, returning an error if it is not a valid // url. func NewURLBuilderFromString(root string, relative bool) (*URLBuilder, error) { u, err := url.Parse(root) if err != nil { return nil, err } return NewURLBuilder(u, relative), nil } // NewURLBuilderFromRequest uses information from an *http.Request to // construct the root url. func NewURLBuilderFromRequest(r *http.Request, relative bool) *URLBuilder { var ( scheme = "http" host = r.Host ) if r.TLS != nil { scheme = "https" } else if len(r.URL.Scheme) > 0 { scheme = r.URL.Scheme } // Handle fowarded headers // Prefer "Forwarded" header as defined by rfc7239 if given // see https://tools.ietf.org/html/rfc7239 if forwarded := r.Header.Get("Forwarded"); len(forwarded) > 0 { forwardedHeader, _, err := parseForwardedHeader(forwarded) if err == nil { if fproto := forwardedHeader["proto"]; len(fproto) > 0 { scheme = fproto } if fhost := forwardedHeader["host"]; len(fhost) > 0 { host = fhost } } } else { if forwardedProto := r.Header.Get("X-Forwarded-Proto"); len(forwardedProto) > 0 { scheme = forwardedProto } if forwardedHost := r.Header.Get("X-Forwarded-Host"); len(forwardedHost) > 0 { // According to the Apache mod_proxy docs, X-Forwarded-Host can be a // comma-separated list of hosts, to which each proxy appends the // requested host. We want to grab the first from this comma-separated // list. hosts := strings.SplitN(forwardedHost, ",", 2) host = strings.TrimSpace(hosts[0]) } } basePath := routeDescriptorsMap[RouteNameBase].Path requestPath := r.URL.Path index := strings.Index(requestPath, basePath) u := &url.URL{ Scheme: scheme, Host: host, } if index > 0 { // N.B. index+1 is important because we want to include the trailing / u.Path = requestPath[0 : index+1] } return NewURLBuilder(u, relative) } // BuildBaseURL constructs a base url for the API, typically just "/v2/". func (ub *URLBuilder) BuildBaseURL() (string, error) { route := ub.cloneRoute(RouteNameBase) baseURL, err := route.URL() if err != nil { return "", err } return baseURL.String(), nil } // BuildCatalogURL constructs a url get a catalog of repositories func (ub *URLBuilder) BuildCatalogURL(values ...url.Values) (string, error) { route := ub.cloneRoute(RouteNameCatalog) catalogURL, err := route.URL() if err != nil { return "", err } return appendValuesURL(catalogURL, values...).String(), nil } // BuildTagsURL constructs a url to list the tags in the named repository. func (ub *URLBuilder) BuildTagsURL(name reference.Named) (string, error) { route := ub.cloneRoute(RouteNameTags) tagsURL, err := route.URL("name", name.Name()) if err != nil { return "", err } return tagsURL.String(), nil } // BuildManifestURL constructs a url for the manifest identified by name and // reference. The argument reference may be either a tag or digest. func (ub *URLBuilder) BuildManifestURL(ref reference.Named) (string, error) { route := ub.cloneRoute(RouteNameManifest) tagOrDigest := "" switch v := ref.(type) { case reference.Tagged: tagOrDigest = v.Tag() case reference.Digested: tagOrDigest = v.Digest().String() default: return "", fmt.Errorf("reference must have a tag or digest") } manifestURL, err := route.URL("name", ref.Name(), "reference", tagOrDigest) if err != nil { return "", err } return manifestURL.String(), nil } // BuildBlobURL constructs the url for the blob identified by name and dgst. func (ub *URLBuilder) BuildBlobURL(ref reference.Canonical) (string, error) { route := ub.cloneRoute(RouteNameBlob) layerURL, err := route.URL("name", ref.Name(), "digest", ref.Digest().String()) if err != nil { return "", err } return layerURL.String(), nil } // BuildBlobUploadURL constructs a url to begin a blob upload in the // repository identified by name. func (ub *URLBuilder) BuildBlobUploadURL(name reference.Named, values ...url.Values) (string, error) { route := ub.cloneRoute(RouteNameBlobUpload) uploadURL, err := route.URL("name", name.Name()) if err != nil { return "", err } return appendValuesURL(uploadURL, values...).String(), nil } // BuildBlobUploadChunkURL constructs a url for the upload identified by uuid, // including any url values. This should generally not be used by clients, as // this url is provided by server implementations during the blob upload // process. func (ub *URLBuilder) BuildBlobUploadChunkURL(name reference.Named, uuid string, values ...url.Values) (string, error) { route := ub.cloneRoute(RouteNameBlobUploadChunk) uploadURL, err := route.URL("name", name.Name(), "uuid", uuid) if err != nil { return "", err } return appendValuesURL(uploadURL, values...).String(), nil } // clondedRoute returns a clone of the named route from the router. Routes // must be cloned to avoid modifying them during url generation. func (ub *URLBuilder) cloneRoute(name string) clonedRoute { route := new(mux.Route) root := new(url.URL) *route = *ub.router.GetRoute(name) // clone the route *root = *ub.root return clonedRoute{Route: route, root: root, relative: ub.relative} } type clonedRoute struct { *mux.Route root *url.URL relative bool } func (cr clonedRoute) URL(pairs ...string) (*url.URL, error) { routeURL, err := cr.Route.URL(pairs...) if err != nil { return nil, err } if cr.relative { return routeURL, nil } if routeURL.Scheme == "" && routeURL.User == nil && routeURL.Host == "" { routeURL.Path = routeURL.Path[1:] } url := cr.root.ResolveReference(routeURL) url.Scheme = cr.root.Scheme return url, nil } // appendValuesURL appends the parameters to the url. func appendValuesURL(u *url.URL, values ...url.Values) *url.URL { merged := u.Query() for _, v := range values { for k, vv := range v { merged[k] = append(merged[k], vv...) } } u.RawQuery = merged.Encode() return u } ================================================ FILE: vendor/github.com/docker/docker/AUTHORS ================================================ # File @generated by hack/generate-authors.sh. DO NOT EDIT. # This file lists all contributors to the repository. # See hack/generate-authors.sh to make modifications. 17neverends 7sunarni <710720732@qq.com> Aanand Prasad Aarni Koskela Aaron Davidson Aaron Feng Aaron Hnatiw Aaron Huslage Aaron L. Xu Aaron Lehmann Aaron Welch Aaron Yoshitake Abdur Rehman Abel Muiño Abhijeet Kasurde Abhinandan Prativadi Abhinav Ajgaonkar Abhishek Chanda Abhishek Sharma Abin Shahab Abirdcfly Ada Mancini Adam Avilla Adam Dobrawy Adam Eijdenberg Adam Kunk Adam Lamers Adam Miller Adam Mills Adam Pointer Adam Simon Adam Singer Adam Thornton Adam Walz Adam Williams AdamKorcz Addam Hardy Aditi Rajagopal Aditya Adnan Khan Adolfo Ochagavía Adria Casas Adrian Moisey Adrian Mouat Adrian Oprea Adrien Folie Adrien Gallouët Ahmed Kamal Ahmet Alp Balkan Aidan Feldman Aidan Hobson Sayers AJ Bowen Ajey Charantimath ajneu Akash Gupta Akhil Mohan Akihiro Matsushima Akihiro Suda Akim Demaille Akira Koyasu Akshay Karle Akshay Moghe Al Tobey alambike Alan Hoyle Alan Scherger Alan Thompson Alano Terblanche Albert Callarisa Albert Zhang Albin Kerouanton Alec Benson Alejandro González Hevia Aleksa Sarai Aleksandr Chebotov Aleksandrs Fadins Alena Prokharchyk Alessandro Boch Alessio Biancalana Alex Chan Alex Chen Alex Coventry Alex Crawford Alex Ellis Alex Gaynor Alex Goodman Alex Nordlund Alex Olshansky Alex Samorukov Alex Stockinger Alex Warhawk Alexander Artemenko Alexander Boyd Alexander Larsson Alexander Midlash Alexander Morozov Alexander Polakov Alexander Shopov Alexandre Beslic Alexandre Garnier Alexandre González Alexandre Jomin Alexandru Sfirlogea Alexei Margasov Alexey Guskov Alexey Kotlyarov Alexey Shamrin Alexis Ries Alexis Thomas Alfred Landrum Ali Dehghani Alicia Lauerman Alihan Demir Allen Madsen Allen Sun almoehi Alvaro Saurin Alvin Deng Alvin Richards amangoel Amen Belayneh Ameya Gawde Amir Goldstein AmirBuddy Amit Bakshi Amit Krishnan Amit Shukla Amr Gawish Amy Lindburg Anand Patil AnandkumarPatel Anatoly Borodin Anca Iordache Anchal Agrawal Anda Xu Anders Janmyr Andre Dublin <81dublin@gmail.com> Andre Granovsky Andrea Denisse Gómez Andrea Luzzardi Andrea Turli Andreas Elvers Andreas Köhler Andreas Savvides Andreas Tiefenthaler Andrei Gherzan Andrei Ushakov Andrei Vagin Andrew Baxter <423qpsxzhh8k3h@s.rendaw.me> Andrew C. Bodine Andrew Clay Shafer Andrew Duckworth Andrew France Andrew Gerrand Andrew Guenther Andrew He Andrew Hsu Andrew Kim Andrew Kuklewicz Andrew Macgregor Andrew Macpherson Andrew Martin Andrew McDonnell Andrew Munsell Andrew Pennebaker Andrew Po Andrew Weiss Andrew Williams Andrews Medina Andrey Kolomentsev Andrey Petrov Andrey Stolbovsky André Martins Andrés Maldonado Andy Chambers andy diller Andy Goldstein Andy Kipp Andy Lindeman Andy Rothfusz Andy Smith Andy Wilson Andy Zhang Aneesh Kulkarni Anes Hasicic Angel Velazquez Anil Belur Anil Madhavapeddy Anirudh Aithal Ankit Jain Ankush Agarwal Anonmily Anran Qiao Anshul Pundir Anthon van der Neut Anthony Baire Anthony Bishopric Anthony Dahanne Anthony Sottile Anton Löfgren Anton Nikitin Anton Polonskiy Anton Tiurin Antonio Aguilar Antonio Murdaca Antonis Kalipetis Antony Messerli Anuj Bahuguna Anuj Varma Anusha Ragunathan Anyu Wang apocas Arash Deshmeh arcosx ArikaChen Arko Dasgupta Arnaud Lefebvre Arnaud Porterie Arnaud Rebillout Artem Khramov Arthur Barr Arthur Gautier Artur Meyster Arun Gupta Asad Saeeduddin Asbjørn Enge Ashly Mathew Austin Vazquez averagehuman Avi Das Avi Kivity Avi Miller Avi Vaid Azat Khuyiyakhmetov Bao Yonglei Bardia Keyoumarsi Barnaby Gray Barry Allard Bartłomiej Piotrowski Bastiaan Bakker Bastien Pascard bdevloed Bearice Ren Ben Bonnefoy Ben Firshman Ben Golub Ben Gould Ben Hall Ben Langfeld Ben Lovy Ben Sargent Ben Severson Ben Toews Ben Wiklund Benjamin Atkin Benjamin Baker Benjamin Boudreau Benjamin Böhmke Benjamin Wang Benjamin Yolken Benny Ng Benoit Chesneau Bernerd Schaefer Bernhard M. Wiedemann Bert Goethals Bertrand Roussel Bevisy Zhang Bharath Thiruveedula Bhiraj Butala Bhumika Bayani Bilal Amarni Bill Wang Billy Ridgway Bily Zhang Bin Liu Bingshen Wang Bjorn Neergaard Blake Geno Boaz Shuster bobby abbott Bojun Zhu Boqin Qin Boris Pruessmann Boshi Lian Bouke Haarsma Boyd Hemphill boynux Bradley Cicenas Bradley Wright Brandon Liu Brandon Philips Brandon Rhodes Brendan Dixon Brendon Smith Brennan Kinney <5098581+polarathene@users.noreply.github.com> Brent Salisbury Brett Higgins Brett Kochendorfer Brett Milford Brett Randall Brian (bex) Exelbierd Brian Bland Brian DeHamer Brian Dorsey Brian Flad Brian Goff Brian McCallister Brian Olsen Brian Schwind Brian Shumate Brian Torres-Gil Brian Trump Brice Jaglin Briehan Lombaard Brielle Broder Bruno Bigras Bruno Binet Bruno Gazzera Bruno Renié Bruno Tavares Bryan Bess Bryan Boreham Bryan Matsuo Bryan Murphy Burke Libbey Byung Kang Caleb Spare Calen Pennington Calvin Liu Cameron Boehmer Cameron Sparr Cameron Spear Campbell Allen Candid Dauth Cao Weiwei Carl Henrik Lunde Carl Loa Odin Carl X. Su Carlo Mion Carlos Alexandro Becker Carlos de Paula Carlos Sanchez Carol Fager-Higgins Cary Casey Bisson Catalin Pirvu Ce Gao Cedric Davies Cesar Talledo Cezar Sa Espinola Chad Swenson Chance Zibolski Chander Govindarajan Chanhun Jeong Chao Wang Charity Kathure Charles Chan Charles Hooper Charles Law Charles Lindsay Charles Merriam Charles Sarrazin Charles Smith Charlie Drage Charlie Lewis Chase Bolt ChaYoung You Chee Hau Lim Chen Chao Chen Chuanliang Chen Hanxiao Chen Min Chen Mingjie Chen Qiu Cheng-mean Liu Chengfei Shang Chengguang Xu Chengyu Zhu Chentianze Chenyang Yan chenyuzhu Chetan Birajdar Chewey Chia-liang Kao Chiranjeevi Tirunagari chli Cholerae Hu Chris Alfonso Chris Armstrong Chris Dias Chris Dituri Chris Fordham Chris Gavin Chris Gibson Chris Khoo Chris Kreussling (Flatbush Gardener) Chris McKinnel Chris McKinnel Chris Price Chris Seto Chris Snow Chris St. Pierre Chris Stivers Chris Swan Chris Telfer Chris Wahl Chris Weyl Chris White Christian Becker Christian Berendt Christian Brauner Christian Böhme Christian Muehlhaeuser Christian Persson Christian Rotzoll Christian Simon Christian Stefanescu Christoph Ziebuhr Christophe Mehay Christophe Troestler Christophe Vidal Christopher Biscardi Christopher Crone Christopher Currie Christopher Jones Christopher Latham Christopher Petito Christopher Rigor Christy Norman Chun Chen Ciro S. Costa Clayton Coleman Clint Armstrong Clinton Kitson clubby789 Cody Roseborough Coenraad Loubser Colin Dunklau Colin Hebert Colin Panisset Colin Rice Colin Walters Collin Guarino Colm Hally companycy Conor Evans Corbin Coleman Corey Farrell Cory Forsyth Cory Snider cressie176 Cristian Ariza Cristian Staretu cristiano balducci Cristina Yenyxe Gonzalez Garcia Cruceru Calin-Cristian cui fliter CUI Wei Cuong Manh Le Cyprian Gracz Cyril F Da McGrady Daan van Berkel Daehyeok Mun Dafydd Crosby dalanlan Damian Smyth Damien Nadé Damien Nozay Damjan Georgievski Dan Anolik Dan Buch Dan Cotora Dan Feldman Dan Griffin Dan Hirsch Dan Keder Dan Levy Dan McPherson Dan Plamadeala Dan Stine Dan Williams Dani Hodovic Dani Louca Daniel Antlinger Daniel Black Daniel Dao Daniel Exner Daniel Farrell Daniel Garcia Daniel Gasienica Daniel Grunwell Daniel Guns Daniel Helfand Daniel Hiltgen Daniel J Walsh Daniel Menet Daniel Mizyrycki Daniel Nephin Daniel Norberg Daniel Nordberg Daniel P. Berrangé Daniel Robinson Daniel S Daniel Sweet Daniel Von Fange Daniel Watkins Daniel X Moore Daniel YC Lin Daniel Zhang Daniele Rondina Danny Berger Danny Milosavljevic Danny Yates Danyal Khaliq Darren Coxall Darren Shepherd Darren Stahl Dattatraya Kumbhar Davanum Srinivas Dave Barboza Dave Goodchild Dave Henderson Dave MacDonald Dave Tucker David Anderson David Bellotti David Calavera David Chung David Corking David Cramer David Currie David Davis David Dooling David Gageot David Gebler David Glasser David Karlsson <35727626+dvdksn@users.noreply.github.com> David Lawrence David Lechner David M. Karr David Mackey David Manouchehri David Mat David Mcanulty David McKay David O'Rourke David P Hilton David Pelaez David R. Jenni David Röthlisberger David Sheets David Sissitka David Trott David Wang <00107082@163.com> David Williamson David Xia David Young Davide Ceretti Dawn Chen dbdd dcylabs Debayan De Deborah Gertrude Digges deed02392 Deep Debroy Deng Guangxing Deni Bertovic Denis Defreyne Denis Gladkikh Denis Ollier Dennis Chen Dennis Chen Dennis Docter Derek Derek Derek Ch Derek McGowan Deric Crago Deshi Xiao Devon Estes Devvyn Murphy Dharmit Shah Dhawal Yogesh Bhanushali Dhilip Kumars Diego Romero Diego Siqueira Dieter Reuter Dillon Dixon Dima Stopel Dimitri John Ledkov Dimitris Mandalidis Dimitris Rozakis Dimitry Andric Dinesh Subhraveti Ding Fei dingwei Diogo Monica DiuDiugirl Djibril Koné Djordje Lukic dkumor Dmitri Logvinenko Dmitri Shuralyov Dmitry Demeshchuk Dmitry Gusev Dmitry Kononenko Dmitry Sharshakov Dmitry Shyshkin Dmitry Smirnov Dmitry V. Krivenok Dmitry Vorobev Dmytro Iakovliev docker-unir[bot] Dolph Mathews Dominic Tubach Dominic Yin Dominik Dingel Dominik Finkbeiner Dominik Honnef Don Kirkby Don Kjer Don Spaulding Donald Huang Dong Chen Donghwa Kim Donovan Jones Dorin Geman Doron Podoleanu Doug Davis Doug MacEachern Doug Tangren Douglas Curtis Dr Nic Williams dragon788 Dražen Lučanin Drew Erny Drew Hubl Dustin Sallings Ed Costello Edmund Wagner Eiichi Tsukata Eike Herzbach Eivin Giske Skaaren Eivind Uggedal Elan Ruusamäe Elango Sivanandam Elena Morozova Eli Uriegas Elias Faxö Elias Koromilas Elias Probst Elijah Zupancic eluck Elvir Kuric Emil Davtyan Emil Hernvall Emily Maier Emily Rose Emir Ozer Eng Zer Jun Enguerran Enrico Weigelt, metux IT consult Eohyung Lee epeterso er0k Eric Barch Eric Curtin Eric G. Noriega Eric Hanchrow Eric Lee Eric Mountain Eric Myhre Eric Paris Eric Rafaloff Eric Rosenberg Eric Sage Eric Soderstrom Eric Yang Eric-Olivier Lamey Erica Windisch Erich Cordoba Erik Bray Erik Dubbelboer Erik Hollensbe Erik Inge Bolsø Erik Kristensen Erik Sipsma Erik Sjölund Erik St. Martin Erik Weathers Erno Hopearuoho Erwin van der Koogh Espen Suenson Ethan Bell Ethan Mosbaugh Euan Harris Euan Kemp Eugen Krizo Eugene Yakubovich Evan Allrich Evan Carmi Evan Hazlett Evan Krall Evan Lezar Evan Phoenix Evan Wies Evelyn Xu Everett Toews Evgeniy Makhrov Evgeny Shmarnev Evgeny Vereshchagin Ewa Czechowska Eystein Måløy Stenberg ezbercih Ezra Silvera Fabian Kramm Fabian Lauer Fabian Raetz Fabiano Rosas Fabio Falci Fabio Kung Fabio Rapposelli Fabio Rehm Fabrizio Regini Fabrizio Soppelsa Faiz Khan falmp Fangming Fang Fangyuan Gao <21551127@zju.edu.cn> fanjiyun Fareed Dudhia Fathi Boudra Federico Gimenez Felipe Oliveira Felipe Ruhland Felix Abecassis Felix Geisendörfer Felix Hupfeld Felix Rabe Felix Ruess Felix Schindler Feng Yan Fengtu Wang Ferenc Szabo Fernando Fero Volar Feroz Salam Ferran Rodenas Filipe Brandenburger Filipe Oliveira Filipe Pina Flavio Castelli Flavio Crisciani Florian Florian Klein Florian Maier Florian Noeding Florian Schmaus Florian Weingarten Florin Asavoaie Florin Patan fonglh Foysal Iqbal Francesc Campoy Francesco Degrassi Francesco Mari Francis Chuang Francisco Carriedo Francisco Souza Frank Groeneveld Frank Herrmann Frank Macreery Frank Rosquin Frank Villaro-Dixon Frank Yang François Scala Fred Lifton Frederick F. Kautz IV Frederico F. de Oliveira Frederik Loeffert Frederik Nordahl Jul Sabroe Freek Kalter Frieder Bluemle frobnicaty <92033765+frobnicaty@users.noreply.github.com> Frédéric Dalleau Fu JinLin Félix Baylac-Jacqué Félix Cantournet Gabe Rosenhouse Gabor Nagy Gabriel Adrian Samfira Gabriel Goller Gabriel L. Somlo Gabriel Linder Gabriel Monroy Gabriel Nicolas Avellaneda Gabriel Tomitsuka Gaetan de Villele Galen Sampson Gang Qiao Gareth Rushgrove Garrett Barboza Gary Schaetz Gaurav Gaurav Singh Gaël PORTAY Genki Takiuchi GennadySpb Geoff Levand Geoffrey Bachelet Geon Kim George Adams George Kontridze George Ma George MacRorie George Xie Georgi Hristozov Georgy Yakovlev Gereon Frey German DZ Gert van Valkenhoef Gerwim Feiken Ghislain Bourgeois Giampaolo Mancini Gianluca Borello Gildas Cuisinier Giovan Isa Musthofa gissehel Giuseppe Mazzotta Giuseppe Scrivano Gleb Fotengauer-Malinovskiy Gleb M Borisov Glyn Normington GoBella Goffert van Gool Goldwyn Rodrigues Gopikannan Venugopalsamy Gosuke Miyashita Gou Rao Govinda Fichtner Grace Choi Grant Millar Grant Reaber Graydon Hoare Greg Fausak Greg Pflaum Greg Stephens Greg Thornton Grzegorz Jaśkiewicz Guilhem Lettron Guilherme Salgado Guillaume Dufour Guillaume J. Charmes Gunadhya S. <6939749+gunadhya@users.noreply.github.com> Guoqiang QI guoxiuyan Guri Gurjeet Singh Guruprasad Gustav Sinder gwx296173 Günter Zöchbauer Haichao Yang haikuoliu haining.cao Hakan Özler Hamish Hutchings Hannes Ljungberg Hans Kristian Flaatten Hans Rødtang Hao Shu Wei Hao Zhang <21521210@zju.edu.cn> Harald Albers Harald Niesche Harley Laue Harold Cooper Harrison Turton Harry Zhang Harshal Patil Harshal Patil He Simei He Xiaoxi He Xin heartlock <21521209@zju.edu.cn> Hector Castro Helen Xie Henning Sprang Hiroshi Hatake Hiroyuki Sasagawa Hobofan Hollie Teal Hong Xu Hongbin Lu Hongxu Jia Honza Pokorny Hsing-Hui Hsu Hsing-Yu (David) Chen hsinko <21551195@zju.edu.cn> Hu Keping Hu Tao Huajin Tong huang-jl <1046678590@qq.com> HuanHuan Ye Huanzhong Zhang Huayi Zhang Hugo Barrera Hugo Duncan Hugo Marisco <0x6875676f@gmail.com> Hui Kang Hunter Blanks huqun Huu Nguyen Hyeongkyu Lee Hyzhou Zhy Iago López Galeiras Ian Bishop Ian Bull Ian Calvert Ian Campbell Ian Chen Ian Lee Ian Main Ian Philpot Ian Truslove Iavael Icaro Seara Ignacio Capurro Igor Dolzhikov Igor Karpovich Iliana Weller Ilkka Laukkanen Illia Antypenko Illo Abdulrahim Ilya Dmitrichenko Ilya Gusev Ilya Khlopotov imalasong <2879499479@qq.com> imre Fitos inglesp Ingo Gottwald Innovimax Isaac Dupree Isabel Jimenez Isaiah Grace Isao Jonas Iskander Sharipov Ivan Babrou Ivan Fraixedes Ivan Grcic Ivan Markin J Bruni J. Nunn Jack Danger Canty Jack Laxson Jack Walker <90711509+j2walker@users.noreply.github.com> Jacob Atzen Jacob Edelman Jacob Tomlinson Jacob Vallejo Jacob Wen Jaime Cepeda Jaivish Kothari Jake Champlin Jake Moshenko Jake Sanders Jakub Drahos Jakub Guzik James Allen James Carey James Carr James DeFelice James Harrison Fisher James Kyburz James Kyle James Lal James Mills James Nesbitt James Nugent James Sanders James Turnbull James Watkins-Harvey Jameson Hyde Jamie Hannaford Jamshid Afshar Jan Breig Jan Chren Jan Garcia Jan Götte Jan Keromnes Jan Koprowski Jan Pazdziora Jan Toebes Jan-Gerd Tenberge Jan-Jaap Driessen Jana Radhakrishnan Jannick Fahlbusch Januar Wayong Jared Biel Jared Hocutt Jaroslav Jindrak Jaroslaw Zabiello Jasmine Hegman Jason A. Donenfeld Jason Divock Jason Giedymin Jason Green Jason Hall Jason Heiss Jason Livesay Jason McVetta Jason Plum Jason Shepherd Jason Smith Jason Sommer Jason Stangroome Jasper Siepkes Javier Bassi jaxgeller Jay Jay Kamat Jay Lim Jean Rouge Jean-Baptiste Barth Jean-Baptiste Dalido Jean-Christophe Berthon Jean-Michel Rouet Jean-Paul Calderone Jean-Pierre Huynh Jean-Tiare Le Bigot Jeeva S. Chelladhurai Jeff Anderson Jeff Hajewski Jeff Johnston Jeff Lindsay Jeff Mickey Jeff Minard Jeff Nickoloff Jeff Silberman Jeff Welch Jeff Zvier Jeffrey Bolle Jeffrey Morgan Jeffrey van Gogh Jenny Gebske Jeongseok Kang Jeremy Chambers Jeremy Grosser Jeremy Huntwork Jeremy Price Jeremy Qian Jeremy Unruh Jeremy Yallop Jeroen Franse Jeroen Jacobs Jesse Dearing Jesse Dubay Jessica Frazelle Jeyanthinath Muthuram Jezeniel Zapanta Jhon Honce Ji.Zhilong Jian Liao Jian Zeng Jian Zhang Jiang Jinyang Jianyong Wu Jie Luo Jie Ma Jihyun Hwang Jilles Oldenbeuving Jim Alateras Jim Carroll Jim Ehrismann Jim Galasyn Jim Lin Jim Minter Jim Perrin Jimmy Cuadra Jimmy Puckett Jimmy Song jinjiadu Jinsoo Park Jintao Zhang Jiri Appl Jiri Popelka Jiuyue Ma Jiří Župka jjimbo137 <115816493+jjimbo137@users.noreply.github.com> Joakim Roubert Joan Grau Joao Fernandes Joao Trindade Joe Beda Joe Doliner Joe Ferguson Joe Gordon Joe Shaw Joe Van Dyk Joel Friedly Joel Handwell Joel Hansson Joel Wurtz Joey Geiger Joey Geiger Joey Gibson Joffrey F Johan Euphrosine Johan Rydberg Johanan Lieberman Johannes 'fish' Ziemke John Costa John Feminella John Gardiner Myers John Gossman John Harris John Howard John Laswell John Maguire John Mulhausen John OBrien III John Starks John Stephens John Tims John V. Martinez John Warwick John Willis Jon Johnson Jon Surrell Jon Wedaman Jonas Dohse Jonas Geiler Jonas Heinrich Jonas Pfenniger Jonathan A. Schweder Jonathan A. Sternberg Jonathan Boulle Jonathan Camp Jonathan Choy Jonathan Dowland Jonathan Lebon Jonathan Lomas Jonathan McCrohan Jonathan Mueller Jonathan Pares Jonathan Rudenberg Jonathan Stoppani Jonh Wendell Joni Sar Joost Cassee Jordan Arentsen Jordan Jennings Jordan Sissel Jordi Massaguer Pla Jorge Marin Jorit Kleine-Möllhoff Jose Diaz-Gonzalez Joseph Anthony Pasquale Holsten Joseph Hager Joseph Kern Joseph Rothrock Josh Josh Bodah Josh Bonczkowski Josh Chorlton Josh Eveleth Josh Hawn Josh Horwitz Josh Poimboeuf Josh Soref Josh Wilson Josiah Kiehl José Tomás Albornoz Joyce Jang JP JSchltggr Julian Taylor Julien Barbier Julien Bisconti Julien Bordellier Julien Dubois Julien Kassar Julien Maitrehenry Julien Pervillé Julien Pivotto Julio Guerra Julio Montes Jun Du Jun-Ru Chang junxu Jussi Nummelin Justas Brazauskas Justen Martin Justin Chadwell Justin Cormack Justin Force Justin Keller <85903732+jk-vb@users.noreply.github.com> Justin Menga Justin Plock Justin Simonelis Justin Terry Justyn Temme Jyrki Puttonen Jérémy Leherpeur Jérôme Petazzoni Jörg Thalheim K. Heller Kai Blin Kai Qiang Wu (Kennan) Kaijie Chen Kaita Nakamura Kamil Domański Kamjar Gerami Kanstantsin Shautsou Kara Alexandra Karan Lyons Kareem Khazem kargakis Karl Grzeszczak Karol Duleba Karthik Karanth Karthik Nayak Kasper Fabæch Brandt Kate Heddleston Katie McLaughlin Kato Kazuyoshi Katrina Owen Kawsar Saiyeed Kay Yan kayrus Kazuhiro Sera Kazuyoshi Kato Ke Li Ke Xu Kei Ohmura Keith Hudgins Keli Hu Ken Bannister Ken Cochrane Ken Herner Ken ICHIKAWA Ken Reese Kenfe-Mickaël Laventure Kenjiro Nakayama Kent Johnson Kenta Tada Kevin "qwazerty" Houdebert Kevin Alvarez Kevin Burke Kevin Clark Kevin Feyrer Kevin J. Lynagh Kevin Jing Qiu Kevin Kern Kevin Menard Kevin Meredith Kevin P. Kucharczyk Kevin Parsons Kevin Richardson Kevin Shi Kevin Wallace Kevin Yap Keyvan Fatehi kies Kim BKC Carlbacker Kim Eik Kimbro Staken Kir Kolyshkin Kiran Gangadharan Kirill SIbirev Kirk Easterson knappe Kohei Tsuruta Koichi Shiraishi Konrad Kleine Konrad Ponichtera Konstantin Gribov Konstantin L Konstantin Pelykh Kostadin Plachkov kpcyrd Krasi Georgiev Krasimir Georgiev Kris-Mikael Krister Kristian Haugene Kristian Heljas Kristina Zabunova Krystian Wojcicki Kunal Kushwaha Kunal Tyagi Kyle Conroy Kyle Linden Kyle Squizzato Kyle Wuolle kyu Lachlan Coote Lai Jiangshan Lajos Papp Lakshan Perera Lalatendu Mohanty Lance Chen Lance Kinley Lars Andringa Lars Butler Lars Kellogg-Stedman Lars R. Damerow Lars-Magnus Skog Laszlo Meszaros Laura Brehm Laura Frank Laurent Bernaille Laurent Erignoux Laurent Goderre Laurie Voss Leandro Motta Barros Leandro Siqueira Lee Calcote Lee Chao <932819864@qq.com> Lee, Meng-Han Lei Gong Lei Jitang Leiiwang Len Weincier Lennie Leo Gallucci Leonardo Nodari Leonardo Taccari Leszek Kowalski Levi Blackstone Levi Gross Levi Harrison Lewis Daly Lewis Marshall Lewis Peckover Li Yi Liam Macgillavry Liana Lo Liang Mingqiang Liang-Chi Hsieh liangwei Liao Qingwei Lifubang Lihua Tang Lily Guo limeidan Lin Lu LingFaKe Linus Heckemann Liran Tal Liron Levin Liu Bo Liu Hua liwenqi lixiaobing10051267 Liz Zhang LIZAO LI Lizzie Dixon <_@lizzie.io> Lloyd Dewolf Lokesh Mandvekar longliqiang88 <394564827@qq.com> Lorenz Leutgeb Lorenzo Fontana Lotus Fenn Louis Delossantos Louis Opter Luboslav Pivarc Luca Favatella Luca Marturana Luca Orlandi Luca-Bogdan Grigorescu Lucas Chan Lucas Chi Lucas Molas Lucas Silvestre Luciano Mores Luis Henrique Mulinari Luis Martínez de Bartolomé Izquierdo Luiz Svoboda Lukas Heeren Lukas Waslowski lukaspustina Lukasz Zajaczkowski Luke Marsden Lyn Lynda O'Leary Lénaïc Huard Ma Müller Ma Shimiao Mabin Madhan Raj Mookkandy Madhav Puri Madhu Venugopal Mageee maggie44 <64841595+maggie44@users.noreply.github.com> Mahesh Tiyyagura malnick Malte Janduda Manfred Touron Manfred Zabarauskas Manjunath A Kumatagi Mansi Nahar Manuel Meurer Manuel Rüger Manuel Woelker mapk0y Marat Radchenko Marc Abramowitz Marc Kuo Marc Tamsky Marcel Edmund Franke Marcelo Horacio Fortino Marcelo Salazar Marco Hennings Marcus Cobden Marcus Farkas Marcus Linke Marcus Martins Marcus Ramberg Marek Goldmann Marian Marinov Marianna Tessel Mario Loriedo Marius Gundersen Marius Sturm Marius Voila Mark Allen Mark Feit Mark Jeromin Mark McGranaghan Mark McKinstry Mark Milstein Mark Oates Mark Parker Mark Vainomaa Mark West Markan Patel Marko Mikulicic Marko Tibold Markus Fix Markus Kortlang Martijn Dwars Martijn van Oosterhout Martin Braun Martin Dojcak Martin Honermeyer Martin Jirku Martin Kelly Martin Mosegaard Amdisen Martin Muzatko Martin Redmond Maru Newby Mary Anthony Masahito Zembutsu Masato Ohba Masayuki Morita Mason Malone Mateusz Sulima Mathias Monnerville Mathieu Champlon Mathieu Le Marec - Pasquet Mathieu Parent Mathieu Paturel Matt Apperson Matt Bachmann Matt Bajor Matt Bentley Matt Haggard Matt Hoyle Matt McCormick Matt Moore Matt Morrison <3maven@gmail.com> Matt Richardson Matt Rickard Matt Robenolt Matt Schurenko Matt Williams Matthew Heon Matthew Lapworth Matthew Mayer Matthew Mosesohn Matthew Mueller Matthew Riley Matthias Klumpp Matthias Kühnle Matthias Rampke Matthieu Fronton Matthieu Hauglustaine Matthieu MOREL Mattias Jernberg Mauricio Garavaglia mauriyouth Max Harmathy Max Shytikov Max Timchenko Maxim Fedchyshyn Maxim Ivanov Maxim Kulkin Maxim Treskin Maxime Petazzoni Maximiliano Maccanti Maxwell Meaglith Ma meejah Megan Kostick Mehul Kar Mei ChunTao Mengdi Gao Menghui Chen Mert Yazıcıoğlu mgniu Micah Zoltu Michael A. Smith Michael Beskin Michael Bridgen Michael Brown Michael Chiang Michael Crosby Michael Currie Michael Friis Michael Gorsuch Michael Grauer Michael Holzheu Michael Hudson-Doyle Michael Huettermann Michael Irwin Michael Kebe Michael Kuehn Michael Käufl Michael Neale Michael Nussbaum Michael Prokop Michael Scharf Michael Spetsiotis Michael Stapelberg Michael Steinert Michael Thies Michael Weidmann Michael West Michael Zhao Michal Fojtik Michal Gebauer Michal Jemala Michal Kostrzewa Michal Minář Michal Rostecki Michal Wieczorek Michaël Pailloncy Michał Czeraszkiewicz Michał Gryko Michał Kosek Michiel de Jong Mickaël Fortunato Mickaël Remars Miguel Angel Fernández Miguel Morales Miguel Perez Mihai Borobocea Mihuleacc Sergiu Mikael Davranche Mike Brown Mike Bush Mike Casas Mike Chelen Mike Danese Mike Dillon Mike Dougherty Mike Estes Mike Gaffney Mike Goelzer Mike Leone Mike Lundy Mike MacCana Mike Naberezny Mike Snitzer Mike Sul mikelinjie <294893458@qq.com> Mikhail Sobolev Miklos Szegedi Milas Bowman Milind Chawre Miloslav Trmač mingqing Mingzhen Feng Misty Stanley-Jones Mitch Capper Mizuki Urushida mlarcher Mohammad Banikazemi Mohammad Nasirifar Mohammed Aaqib Ansari Mohd Sadiq Mohit Soni Moorthy RS Morgan Bauer Morgante Pell Morgy93 Morten Siebuhr Morton Fox Moysés Borges mrfly Mrunal Patel Muayyad Alsadi Muhammad Zohaib Aslam Mustafa Akın Muthukumar R Myeongjoon Kim Máximo Cuadros Médi-Rémi Hashim Nace Oroz Nahum Shalman Nakul Pathak Nalin Dahyabhai Nan Monnand Deng Naoki Orii Natalie Parker Natanael Copa Natasha Jarus Nate Brennand Nate Eagleson Nate Jones Nathan Baulch Nathan Carlson Nathan Herald Nathan Hsieh Nathan Kleyn Nathan LeClaire Nathan McCauley Nathan Williams Naveed Jamil Neal McBurnett Neil Horman Neil Peterson Nelson Chen Neyazul Haque Nghia Tran Niall O'Higgins Nicholas E. Rabenau Nick Adcock Nick DeCoursin Nick Irvine Nick Neisen Nick Parker Nick Payne Nick Russo Nick Santos Nick Stenning Nick Stinemates Nick Wood NickrenREN Nicola Kabar Nicolas Borboën Nicolas De Loof Nicolas Dudebout Nicolas Goy Nicolas Kaiser Nicolas Sterchele Nicolas V Castet Nicolás Hock Isaza Niel Drummond Nigel Poulton Nik Nyby Nikhil Chawla NikolaMandic Nikolas Garofil Nikolay Edigaryev Nikolay Milovanov ningmingxiao Nirmal Mehta Nishant Totla NIWA Hideyuki Noah Meyerhans Noah Treuhaft NobodyOnSE noducks Nolan Darilek Nolan Miles Noriki Nakamura nponeccop Nurahmadie Nuutti Kotivuori nzwsch O.S. Tezer objectified Octol1ttle Odin Ugedal Oguz Bilgic Oh Jinkyun Ohad Schneider ohmystack Ole Reifschneider Oliver Neal Oliver Reason Olivier Gambier Olle Jonsson Olli Janatuinen Olly Pomeroy Omri Shiv Onur Filiz Oriol Francès Oscar Bonilla <6f6231@gmail.com> oscar.chen <2972789494@qq.com> Oskar Niburski Otto Kekäläinen Ouyang Liduo Ovidio Mallo Panagiotis Moustafellos Paolo G. Giarrusso Pascal Pascal Bach Pascal Borreli Pascal Hartig Patrick Böänziger Patrick Devine Patrick Haas Patrick Hemmer Patrick St. laurent Patrick Stapleton Patrik Cyvoct Patrik Leifert pattichen Paul "TBBle" Hampson Paul paul Paul Annesley Paul Bellamy Paul Bowsher Paul Furtado Paul Hammond Paul Jimenez Paul Kehrer Paul Lietar Paul Liljenberg Paul Morie Paul Nasrat Paul Seiffert Paul Weaver Paulo Gomes Paulo Ribeiro Pavel Lobashov Pavel Matěja Pavel Pletenev Pavel Pospisil Pavel Sutyrin Pavel Tikhomirov Pavlos Ratis Pavol Vargovcik Pawel Konczalski Paweł Gronowski payall4u Peeyush Gupta Peggy Li Pei Su Peng Tao Penghan Wang Per Weijnitz perhapszzy@sina.com Pete Woods Peter Bourgon Peter Braden Peter Bücker Peter Choi Peter Dave Hello Peter Edge Peter Ericson Peter Esbensen Peter Jaffe Peter Kang Peter Malmgren Peter Salvatore Peter Volpe Peter Waller Petr Švihlík Petros Angelatos Phil Phil Estes Phil Sphicas Phil Spitler Philip Alexander Etling Philip K. Warren Philip Monroe Philipp Fruck Philipp Gillé Philipp Wahala Philipp Weissensteiner Phillip Alexander phineas pidster Piergiuliano Bossi Pierre Pierre Carrier Pierre Dal-Pra Pierre Wacrenier Pierre-Alain RIVIERE pinglanlu Piotr Bogdan Piotr Karbowski Porjo Poul Kjeldager Sørensen Pradeep Chhetri Pradip Dhara Pradipta Kr. Banerjee Prasanna Gautam Pratik Karki Prayag Verma Priya Wadhwa Projjol Banerji Przemek Hejman Puneet Pruthi Pure White pysqz Qiang Huang Qin TianHuan Qinglan Peng Quan Tian qudongfang Quentin Brossard Quentin Perez Quentin Tayssier r0n22 Rachit Sharma Radostin Stoyanov Rafael Fernández López Rafal Jeczalik Rafe Colton Raghavendra K T Raghuram Devarakonda Raja Sami Rajat Pandit Rajdeep Dua Ralf Sippl Ralle Ralph Bean Ramkumar Ramachandra Ramon Brooker Ramon van Alteren RaviTeja Pothana Ray Tsang ReadmeCritic realityone Recursive Madman Reficul Regan McCooey Remi Rampin Remy Suen Renato Riccieri Santos Zannon Renaud Gaubert Rhys Hiltner Ri Xu Ricardo N Feliciano Rich Horwood Rich Moyse Rich Seymour Richard Burnison Richard Hansen Richard Harvey Richard Mathie Richard Metzler Richard Scothern Richo Healey Rick Bradley Rick van de Loo Rick Wieman Rik Nijessen Riku Voipio Riley Guerin Ritesh H Shukla Riyaz Faizullabhoy Rob Cowsill <42620235+rcowsill@users.noreply.github.com> Rob Gulewich Rob Murray Rob Vesse Robert Bachmann Robert Bittle Robert Obryk Robert Schneider Robert Shade Robert Stern Robert Sturla Robert Terhaar Robert Wallis Robert Wang Roberto G. Hashioka Roberto Muñoz Fernández Robin Naundorf Robin Schneider Robin Speekenbrink Robin Thoni robpc Rodolfo Carvalho Rodrigo Campos Rodrigo Vaz Roel Van Nyen Roger Peppe Rohit Jnagal Rohit Kadam Rohit Kapur Rojin George Roland Huß Roland Kammerer Roland Moriz Roma Sokolov Roman Dudin Roman Mazur Roman Strashkin Roman Volosatovs Roman Zabaluev Ron Smits Ron Williams Rong Gao Rong Zhang Rongxiang Song Rony Weng root root root root Rory Hunter Rory McCune Ross Boucher Rovanion Luckey Roy Reznik Royce Remer Rozhnov Alexandr Rudolph Gottesheim Rui Cao Rui JingAn Rui Lopes Ruilin Li Runshen Zhu Russ Magee Ryan Abrams Ryan Anderson Ryan Aslett Ryan Barry Ryan Belgrave Ryan Campbell Ryan Detzel Ryan Fowler Ryan Liu Ryan McLaughlin Ryan O'Donnell Ryan Seto Ryan Shea Ryan Simmen Ryan Stelly Ryan Thomas Ryan Trauntvein Ryan Wallner Ryan Zhang ryancooper7 RyanDeng Ryo Nakao Ryoga Saito Régis Behmo Rémy Greinhofer s. rannou Sabin Basyal Sachin Joshi Sagar Hani Sainath Grandhi Sakeven Jiang Salahuddin Khan Sally O'Malley Sam Abed Sam Alba Sam Bailey Sam J Sharpe Sam Neirinck Sam Reis Sam Rijs Sam Thibault Sam Whited Sambuddha Basu Sami Wagiaalla Samuel Andaya Samuel Dion-Girardeau Samuel Karp Samuel PHAN sanchayanghosh Sandeep Bansal Sankar சங்கர் Sanket Saurav Santhosh Manohar sapphiredev Sargun Dhillon Sascha Andres Sascha Grunert SataQiu Satnam Singh Satoshi Amemiya Satoshi Tagomori Scott Bessler Scott Collier Scott Johnston Scott Moser Scott Percival Scott Stamp Scott Walls sdreyesg Sean Christopherson Sean Cronin Sean Lee Sean McIntyre Sean OMeara Sean P. Kane Sean Rodman Sebastiaan van Steenis Sebastiaan van Stijn Sebastian Höffner Sebastian Radloff Sebastian Thomschke Sebastien Goasguen Senthil Kumar Selvaraj Senthil Kumaran SeongJae Park Seongyeol Lim Serge Hallyn Sergey Alekseev Sergey Evstifeev Sergii Kabashniuk Sergio Lopez Serhat Gülçiçek Serhii Nakon SeungUkLee Sevki Hasirci Shane Canon Shane da Silva Shaun Kaasten Shaun Thompson shaunol Shawn Landden Shawn Siefkas shawnhe Shayan Pooya Shayne Wang Shekhar Gulati Sheng Yang Shengbo Song Shengjing Zhu Shev Yan Shih-Yuan Lee Shihao Xia Shijiang Wei Shijun Qin Shishir Mahajan Shoubhik Bose Shourya Sarcar Shreenidhi Shedi Shu-Wai Chow shuai-z Shukui Yang Sian Lerk Lau Siarhei Rasiukevich Sidhartha Mani sidharthamani Silas Sewell Silvan Jegen Simão Reis Simon Barendse Simon Eskildsen Simon Ferquel Simon Leinen Simon Menke Simon Taranto Simon Vikstrom Sindhu S Sjoerd Langkemper skanehira Smark Meng Solganik Alexander Solomon Hykes Song Gao Soshi Katsuta Sotiris Salloumis Soulou Spencer Brown Spencer Smith Spike Curtis Sridatta Thatipamala Sridhar Ratnakumar Srini Brahmaroutu Srinivasan Srivatsan Staf Wagemakers Stanislav Bondarenko Stanislav Levin Steeve Morin Stefan Berger Stefan Gehrig Stefan J. Wernli Stefan Praszalowicz Stefan S. Stefan Scherer Stefan Staudenmeyer Stefan Weil Steffen Butzer Stephan Henningsen Stephan Spindler Stephen Benjamin Stephen Crosby Stephen Day Stephen Drake Stephen Rust Steve Desmond Steve Dougherty Steve Durrheimer Steve Francia Steve Koch Steven Burgess Steven Erenst Steven Hartland Steven Iveson Steven Merrill Steven Richards Steven Taylor Stéphane Este-Gracias Stig Larsson Su Wang Subhajit Ghosh Sujith Haridasan Sun Gengze <690388648@qq.com> Sun Jianbo Sune Keller Sunny Gogoi Suryakumar Sudar Sven Dowideit Swapnil Daingade Sylvain Baubeau Sylvain Bellemare Sébastien Sébastien HOUZÉ Sébastien Luttringer Sébastien Stormacq Sören Tempel Tabakhase Tadej Janež Tadeusz Dudkiewicz Takuto Sato tang0th Tangi Colin Tatsuki Sugiura Tatsushi Inagaki Taylan Isikdemir Taylor Jones tcpdumppy <847462026@qq.com> Ted M. Young Tehmasp Chaudhri Tejaswini Duggaraju Tejesh Mehta Terry Chu terryding77 <550147740@qq.com> Thatcher Peskens theadactyl Thell 'Bo' Fowler Thermionix Thiago Alves Silva Thijs Terlouw Thomas Bikeev Thomas Frössman Thomas Gazagnaire Thomas Graf Thomas Grainger Thomas Hansen Thomas Ledos Thomas Leonard Thomas Léveil Thomas Orozco Thomas Riccardi Thomas Schroeter Thomas Sjögren Thomas Swift Thomas Tanaka Thomas Texier Ti Zhou Tiago Seabra Tianon Gravi Tianyi Wang Tibor Vass Tiffany Jernigan Tiffany Low Till Claassen Till Wegmüller Tim Tim Bart Tim Bosse Tim Dettrick Tim Düsterhus Tim Hockin Tim Potter Tim Ruffles Tim Smith Tim Terhorst Tim Wagner Tim Wang Tim Waugh Tim Wraight Tim Zju <21651152@zju.edu.cn> timchenxiaoyu <837829664@qq.com> timfeirg Timo Rothenpieler Timothy Hobbs tjwebb123 tobe Tobias Bieniek Tobias Bradtke Tobias Gesellchen Tobias Klauser Tobias Munk Tobias Pfandzelter Tobias Schmidt Tobias Schwab Todd Crane Todd Lunter Todd Whiteman Toli Kuznets Tom Barlow Tom Booth Tom Denham Tom Fotherby Tom Howe Tom Hulihan Tom Maaswinkel Tom Parker Tom Sweeney Tom Wilkie Tom X. Tobin Tom Zhao Tomas Janousek Tomas Kral Tomas Tomecek Tomasz Kopczynski Tomasz Lipinski Tomasz Nurkiewicz Tomek Mańko Tommaso Visconti Tomoya Tabuchi Tomáš Hrčka Tomáš Virtus tonic Tonny Xu Tony Abboud Tony Daws Tony Miller toogley Torstein Husebø Toshiaki Makita Tõnis Tiigi Trace Andreason tracylihui <793912329@qq.com> Trapier Marshall Travis Cline Travis Thieman Trent Ogren Trevor Trevor Pounds Trevor Sullivan Trishna Guha Tristan Carel Troy Denton Tudor Brindus Ty Alexander Tycho Andersen Tyler Brock Tyler Brown Tzu-Jung Lee uhayate Ulysse Carion Umesh Yadav Utz Bacher vagrant Vaidas Jablonskis Valentin Kulesh vanderliang Velko Ivanov Veres Lajos Victor Algaze Victor Coisne Victor Costan Victor I. Wood Victor Lyuboslavsky Victor Marmol Victor Palma Victor Toni Victor Vieux Victoria Bialas Vijaya Kumar K Vikas Choudhary Vikram bir Singh Viktor Stanchev Viktor Vojnovski VinayRaghavanKS Vincent Batts Vincent Bernat Vincent Boulineau Vincent Demeester Vincent Giersch Vincent Mayers Vincent Woo Vinod Kulkarni Vishal Doshi Vishnu Kannan Vitaly Ostrosablin Vitor Anjos Vitor Monteiro Vivek Agarwal Vivek Dasgupta Vivek Goyal Vladimir Bulyga Vladimir Kirillov Vladimir Pouzanov Vladimir Rutsky Vladimir Varankin VladimirAus Vladislav Kolesnikov Vlastimil Zeman Vojtech Vitek (V-Teq) voloder <110066198+voloder@users.noreply.github.com> Walter Leibbrandt Walter Stanish Wang Chao Wang Guoliang Wang Jie Wang Long Wang Ping Wang Xing Wang Yuexiao Wang Yumu <37442693@qq.com> wanghuaiqing Ward Vandewege WarheadsSE Wassim Dhif Wataru Ishida Wayne Chang Wayne Song weebney Weerasak Chongnguluam Wei Fu Wei Wu Wei-Ting Kuo weipeng weiyan Weiyang Zhu Wen Cheng Ma Wendel Fleming Wenjun Tang Wenkai Yin wenlxie Wenxuan Zhao Wenyu You <21551128@zju.edu.cn> Wenzhi Liang Wes Morgan Wesley Pettit Wewang Xiaorenfine Wiktor Kwapisiewicz Will Dietz Will Rouesnel Will Weaver willhf William Delanoue William Henry William Hubbs William Martin William Riancho William Thurston Wilson Júnior Wing-Kam Wong WiseTrem Wolfgang Nagele Wolfgang Powisch Wonjun Kim WuLonghui xamyzhao Xia Wu Xian Chaobo Xianglin Gao Xianjie Xianlu Bird Xiao YongBiao Xiao Zhang XiaoBing Jiang Xiaodong Liu Xiaodong Zhang Xiaohua Ding Xiaoxi He Xiaoxu Chen Xiaoyu Zhang xichengliudui <1693291525@qq.com> xiekeyang Ximo Guanter Gonzálbez xin.li Xinbo Weng Xinfeng Liu Xinzi Zhou Xiuming Chen Xuecong Liao xuzhaokui Yadnyawalkya Tale Yahya yalpul YAMADA Tsuyoshi Yamasaki Masahide Yamazaki Masashi Yan Feng Yan Zhu Yang Bai Yang Li Yang Pengfei yangchenliang Yann Autissier Yanqiang Miao Yao Zaiyong Yash Murty Yassine Tijani Yasunori Mahata Yazhong Liu Yestin Sun Yi EungJun Yibai Zhang Yihang Ho Ying Li Yohei Ueda Yong Tang Yongxin Li Yongzhi Pan Yosef Fertel You-Sheng Yang (楊有勝) youcai Youcef YEKHLEF Youfu Zhang YR Chen Yu Changchun Yu Chengxia Yu Peng Yu-Ju Hong Yuan Sun Yuanhong Peng Yue Zhang Yufei Xiong Yuhao Fang Yuichiro Kaneko YujiOshima Yunxiang Huang Yurii Rashkovskii Yusuf Tarık Günaydın Yves Blusseau <90z7oey02@sneakemail.com> Yves Junqueira Zac Dover Zach Borboa Zach Gershman Zachary Jaffee Zain Memon Zaiste! Zane DeGraffenried Zefan Li Zen Lin(Zhinan Lin) Zhang Kun Zhang Wei Zhang Wentao zhangguanzhang ZhangHang zhangxianwei Zhenan Ye <21551168@zju.edu.cn> zhenghenghuo Zhenhai Gao Zhenkun Bi ZhiPeng Lu zhipengzuo Zhou Hao Zhoulin Xie Zhu Guihua Zhu Kunjia Zhuoyun Wei Ziheng Liu Zilin Du zimbatm Ziming Dong ZJUshuaizhou <21551191@zju.edu.cn> zmarouf Zoltan Tombol Zou Yu zqh Zuhayr Elahi Zunayed Ali Álvaro Lázaro Átila Camurça Alves 吴小白 <296015668@qq.com> 尹吉峰 屈骏 徐俊杰 慕陶 搏通 黄艳红00139573 정재영 ================================================ FILE: vendor/github.com/docker/docker/LICENSE ================================================ Apache License Version 2.0, January 2004 https://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 Copyright 2013-2018 Docker, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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: vendor/github.com/docker/docker/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. ================================================ FILE: vendor/github.com/docker/docker/api/types/filters/errors.go ================================================ package filters import "fmt" // invalidFilter indicates that the provided filter or its value is invalid type invalidFilter struct { Filter string Value []string } func (e invalidFilter) Error() string { msg := "invalid filter" if e.Filter != "" { msg += " '" + e.Filter if e.Value != nil { msg = fmt.Sprintf("%s=%s", msg, e.Value) } msg += "'" } return msg } // InvalidParameter marks this error as ErrInvalidParameter func (e invalidFilter) InvalidParameter() {} ================================================ FILE: vendor/github.com/docker/docker/api/types/filters/filters_deprecated.go ================================================ package filters import ( "encoding/json" "github.com/docker/docker/api/types/versions" ) // ToParamWithVersion encodes Args as a JSON string. If version is less than 1.22 // then the encoded format will use an older legacy format where the values are a // list of strings, instead of a set. // // Deprecated: do not use in any new code; use ToJSON instead func ToParamWithVersion(version string, a Args) (string, error) { out, err := ToJSON(a) if out == "" || err != nil { return "", nil } if version != "" && versions.LessThan(version, "1.22") { return encodeLegacyFilters(out) } return out, nil } // encodeLegacyFilters encodes Args in the legacy format as used in API v1.21 and older. // where values are a list of strings, instead of a set. // // Don't use in any new code; use [filters.ToJSON]] instead. func encodeLegacyFilters(currentFormat string) (string, error) { // The Args.fields field is not exported, but used to marshal JSON, // so we'll marshal to the new format, then unmarshal to get the // fields, and marshal again. // // This is far from optimal, but this code is only used for deprecated // API versions, so should not be hit commonly. var argsFields map[string]map[string]bool err := json.Unmarshal([]byte(currentFormat), &argsFields) if err != nil { return "", err } buf, err := json.Marshal(convertArgsToSlice(argsFields)) if err != nil { return "", err } return string(buf), nil } func convertArgsToSlice(f map[string]map[string]bool) map[string][]string { m := map[string][]string{} for k, v := range f { values := []string{} for kk := range v { if v[kk] { values = append(values, kk) } } m[k] = values } return m } ================================================ FILE: vendor/github.com/docker/docker/api/types/filters/parse.go ================================================ /* Package filters provides tools for encoding a mapping of keys to a set of multiple values. */ package filters import ( "encoding/json" "regexp" "strings" ) // Args stores a mapping of keys to a set of multiple values. type Args struct { fields map[string]map[string]bool } // KeyValuePair are used to initialize a new Args type KeyValuePair struct { Key string Value string } // Arg creates a new KeyValuePair for initializing Args func Arg(key, value string) KeyValuePair { return KeyValuePair{Key: key, Value: value} } // NewArgs returns a new Args populated with the initial args func NewArgs(initialArgs ...KeyValuePair) Args { args := Args{fields: map[string]map[string]bool{}} for _, arg := range initialArgs { args.Add(arg.Key, arg.Value) } return args } // Keys returns all the keys in list of Args func (args Args) Keys() []string { keys := make([]string, 0, len(args.fields)) for k := range args.fields { keys = append(keys, k) } return keys } // MarshalJSON returns a JSON byte representation of the Args func (args Args) MarshalJSON() ([]byte, error) { if len(args.fields) == 0 { return []byte("{}"), nil } return json.Marshal(args.fields) } // ToJSON returns the Args as a JSON encoded string func ToJSON(a Args) (string, error) { if a.Len() == 0 { return "", nil } buf, err := json.Marshal(a) return string(buf), err } // FromJSON decodes a JSON encoded string into Args func FromJSON(p string) (Args, error) { args := NewArgs() if p == "" { return args, nil } raw := []byte(p) err := json.Unmarshal(raw, &args) if err == nil { return args, nil } // Fallback to parsing arguments in the legacy slice format deprecated := map[string][]string{} if legacyErr := json.Unmarshal(raw, &deprecated); legacyErr != nil { return args, &invalidFilter{} } args.fields = deprecatedArgs(deprecated) return args, nil } // UnmarshalJSON populates the Args from JSON encode bytes func (args Args) UnmarshalJSON(raw []byte) error { return json.Unmarshal(raw, &args.fields) } // Get returns the list of values associated with the key func (args Args) Get(key string) []string { values := args.fields[key] if values == nil { return make([]string, 0) } slice := make([]string, 0, len(values)) for key := range values { slice = append(slice, key) } return slice } // Add a new value to the set of values func (args Args) Add(key, value string) { if _, ok := args.fields[key]; ok { args.fields[key][value] = true } else { args.fields[key] = map[string]bool{value: true} } } // Del removes a value from the set func (args Args) Del(key, value string) { if _, ok := args.fields[key]; ok { delete(args.fields[key], value) if len(args.fields[key]) == 0 { delete(args.fields, key) } } } // Len returns the number of keys in the mapping func (args Args) Len() int { return len(args.fields) } // MatchKVList returns true if all the pairs in sources exist as key=value // pairs in the mapping at key, or if there are no values at key. func (args Args) MatchKVList(key string, sources map[string]string) bool { fieldValues := args.fields[key] // do not filter if there is no filter set or cannot determine filter if len(fieldValues) == 0 { return true } if len(sources) == 0 { return false } for value := range fieldValues { testK, testV, hasValue := strings.Cut(value, "=") v, ok := sources[testK] if !ok { return false } if hasValue && testV != v { return false } } return true } // Match returns true if any of the values at key match the source string func (args Args) Match(field, source string) bool { if args.ExactMatch(field, source) { return true } fieldValues := args.fields[field] for name2match := range fieldValues { match, err := regexp.MatchString(name2match, source) if err != nil { continue } if match { return true } } return false } // GetBoolOrDefault returns a boolean value of the key if the key is present // and is interpretable as a boolean value. Otherwise the default value is returned. // Error is not nil only if the filter values are not valid boolean or are conflicting. func (args Args) GetBoolOrDefault(key string, defaultValue bool) (bool, error) { fieldValues, ok := args.fields[key] if !ok { return defaultValue, nil } if len(fieldValues) == 0 { return defaultValue, &invalidFilter{key, nil} } isFalse := fieldValues["0"] || fieldValues["false"] isTrue := fieldValues["1"] || fieldValues["true"] if isFalse == isTrue { // Either no or conflicting truthy/falsy value were provided return defaultValue, &invalidFilter{key, args.Get(key)} } return isTrue, nil } // ExactMatch returns true if the source matches exactly one of the values. func (args Args) ExactMatch(key, source string) bool { fieldValues, ok := args.fields[key] // do not filter if there is no filter set or cannot determine filter if !ok || len(fieldValues) == 0 { return true } // try to match full name value to avoid O(N) regular expression matching return fieldValues[source] } // UniqueExactMatch returns true if there is only one value and the source // matches exactly the value. func (args Args) UniqueExactMatch(key, source string) bool { fieldValues := args.fields[key] // do not filter if there is no filter set or cannot determine filter if len(fieldValues) == 0 { return true } if len(args.fields[key]) != 1 { return false } // try to match full name value to avoid O(N) regular expression matching return fieldValues[source] } // FuzzyMatch returns true if the source matches exactly one value, or the // source has one of the values as a prefix. func (args Args) FuzzyMatch(key, source string) bool { if args.ExactMatch(key, source) { return true } fieldValues := args.fields[key] for prefix := range fieldValues { if strings.HasPrefix(source, prefix) { return true } } return false } // Contains returns true if the key exists in the mapping func (args Args) Contains(field string) bool { _, ok := args.fields[field] return ok } // Validate compared the set of accepted keys against the keys in the mapping. // An error is returned if any mapping keys are not in the accepted set. func (args Args) Validate(accepted map[string]bool) error { for name := range args.fields { if !accepted[name] { return &invalidFilter{name, nil} } } return nil } // WalkValues iterates over the list of values for a key in the mapping and calls // op() for each value. If op returns an error the iteration stops and the // error is returned. func (args Args) WalkValues(field string, op func(value string) error) error { if _, ok := args.fields[field]; !ok { return nil } for v := range args.fields[field] { if err := op(v); err != nil { return err } } return nil } // Clone returns a copy of args. func (args Args) Clone() (newArgs Args) { newArgs.fields = make(map[string]map[string]bool, len(args.fields)) for k, m := range args.fields { var mm map[string]bool if m != nil { mm = make(map[string]bool, len(m)) for kk, v := range m { mm[kk] = v } } newArgs.fields[k] = mm } return newArgs } func deprecatedArgs(d map[string][]string) map[string]map[string]bool { m := map[string]map[string]bool{} for k, v := range d { values := map[string]bool{} for _, vv := range v { values[vv] = true } m[k] = values } return m } ================================================ FILE: vendor/github.com/docker/docker/api/types/registry/authconfig.go ================================================ package registry import ( "context" "encoding/base64" "encoding/json" "fmt" "io" "strings" ) // AuthHeader is the name of the header used to send encoded registry // authorization credentials for registry operations (push/pull). const AuthHeader = "X-Registry-Auth" // RequestAuthConfig is a function interface that clients can supply // to retry operations after getting an authorization error. // // The function must return the [AuthHeader] value ([AuthConfig]), encoded // in base64url format ([RFC4648, section 5]), which can be decoded by // [DecodeAuthConfig]. // // It must return an error if the privilege request fails. // // [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5 type RequestAuthConfig func(context.Context) (string, error) // AuthConfig contains authorization information for connecting to a Registry. type AuthConfig struct { Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` Auth string `json:"auth,omitempty"` // Email is an optional value associated with the username. // // Deprecated: This field is deprecated since docker 1.11 (API v1.23) and will be removed in the next release. Email string `json:"email,omitempty"` ServerAddress string `json:"serveraddress,omitempty"` // IdentityToken is used to authenticate the user and get // an access token for the registry. IdentityToken string `json:"identitytoken,omitempty"` // RegistryToken is a bearer token to be sent to a registry RegistryToken string `json:"registrytoken,omitempty"` } // EncodeAuthConfig serializes the auth configuration as a base64url encoded // ([RFC4648, section 5]) JSON string for sending through the X-Registry-Auth header. // // [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5 func EncodeAuthConfig(authConfig AuthConfig) (string, error) { buf, err := json.Marshal(authConfig) if err != nil { return "", errInvalidParameter{err} } return base64.URLEncoding.EncodeToString(buf), nil } // DecodeAuthConfig decodes base64url encoded ([RFC4648, section 5]) JSON // authentication information as sent through the X-Registry-Auth header. // // This function always returns an [AuthConfig], even if an error occurs. It is up // to the caller to decide if authentication is required, and if the error can // be ignored. // // [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5 func DecodeAuthConfig(authEncoded string) (*AuthConfig, error) { if authEncoded == "" { return &AuthConfig{}, nil } authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded)) return decodeAuthConfigFromReader(authJSON) } // DecodeAuthConfigBody decodes authentication information as sent as JSON in the // body of a request. This function is to provide backward compatibility with old // clients and API versions. Current clients and API versions expect authentication // to be provided through the X-Registry-Auth header. // // Like [DecodeAuthConfig], this function always returns an [AuthConfig], even if an // error occurs. It is up to the caller to decide if authentication is required, // and if the error can be ignored. // // Deprecated: this function is no longer used and will be removed in the next release. func DecodeAuthConfigBody(rdr io.ReadCloser) (*AuthConfig, error) { return decodeAuthConfigFromReader(rdr) } func decodeAuthConfigFromReader(rdr io.Reader) (*AuthConfig, error) { authConfig := &AuthConfig{} if err := json.NewDecoder(rdr).Decode(authConfig); err != nil { // always return an (empty) AuthConfig to increase compatibility with // the existing API. return &AuthConfig{}, invalid(err) } return authConfig, nil } func invalid(err error) error { return errInvalidParameter{fmt.Errorf("invalid X-Registry-Auth header: %w", err)} } type errInvalidParameter struct{ error } func (errInvalidParameter) InvalidParameter() {} func (e errInvalidParameter) Cause() error { return e.error } func (e errInvalidParameter) Unwrap() error { return e.error } ================================================ FILE: vendor/github.com/docker/docker/api/types/registry/authenticate.go ================================================ package registry // ---------------------------------------------------------------------------- // DO NOT EDIT THIS FILE // This file was generated by `swagger generate operation` // // See hack/generate-swagger-api.sh // ---------------------------------------------------------------------------- // AuthenticateOKBody authenticate o k body // swagger:model AuthenticateOKBody type AuthenticateOKBody struct { // An opaque token used to authenticate a user after a successful login // Required: true IdentityToken string `json:"IdentityToken"` // The status of the authentication // Required: true Status string `json:"Status"` } ================================================ FILE: vendor/github.com/docker/docker/api/types/registry/registry.go ================================================ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: //go:build go1.23 package registry import ( "encoding/json" "net" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) // ServiceConfig stores daemon registry services configuration. type ServiceConfig struct { AllowNondistributableArtifactsCIDRs []*NetIPNet `json:"AllowNondistributableArtifactsCIDRs,omitempty"` // Deprecated: non-distributable artifacts are deprecated and enabled by default. This field will be removed in the next release. AllowNondistributableArtifactsHostnames []string `json:"AllowNondistributableArtifactsHostnames,omitempty"` // Deprecated: non-distributable artifacts are deprecated and enabled by default. This field will be removed in the next release. InsecureRegistryCIDRs []*NetIPNet `json:"InsecureRegistryCIDRs"` IndexConfigs map[string]*IndexInfo `json:"IndexConfigs"` Mirrors []string // ExtraFields is for internal use to include deprecated fields on older API versions. ExtraFields map[string]any `json:"-"` } // MarshalJSON implements a custom marshaler to include legacy fields // in API responses. func (sc *ServiceConfig) MarshalJSON() ([]byte, error) { type tmp ServiceConfig base, err := json.Marshal((*tmp)(sc)) if err != nil { return nil, err } var merged map[string]any _ = json.Unmarshal(base, &merged) for k, v := range sc.ExtraFields { merged[k] = v } return json.Marshal(merged) } // NetIPNet is the net.IPNet type, which can be marshalled and // unmarshalled to JSON type NetIPNet net.IPNet // String returns the CIDR notation of ipnet func (ipnet *NetIPNet) String() string { return (*net.IPNet)(ipnet).String() } // MarshalJSON returns the JSON representation of the IPNet func (ipnet *NetIPNet) MarshalJSON() ([]byte, error) { return json.Marshal((*net.IPNet)(ipnet).String()) } // UnmarshalJSON sets the IPNet from a byte array of JSON func (ipnet *NetIPNet) UnmarshalJSON(b []byte) error { var ipnetStr string if err := json.Unmarshal(b, &ipnetStr); err != nil { return err } _, cidr, err := net.ParseCIDR(ipnetStr) if err != nil { return err } *ipnet = NetIPNet(*cidr) return nil } // IndexInfo contains information about a registry // // RepositoryInfo Examples: // // { // "Index" : { // "Name" : "docker.io", // "Mirrors" : ["https://registry-2.docker.io/v1/", "https://registry-3.docker.io/v1/"], // "Secure" : true, // "Official" : true, // }, // "RemoteName" : "library/debian", // "LocalName" : "debian", // "CanonicalName" : "docker.io/debian" // "Official" : true, // } // // { // "Index" : { // "Name" : "127.0.0.1:5000", // "Mirrors" : [], // "Secure" : false, // "Official" : false, // }, // "RemoteName" : "user/repo", // "LocalName" : "127.0.0.1:5000/user/repo", // "CanonicalName" : "127.0.0.1:5000/user/repo", // "Official" : false, // } type IndexInfo struct { // Name is the name of the registry, such as "docker.io" Name string // Mirrors is a list of mirrors, expressed as URIs Mirrors []string // Secure is set to false if the registry is part of the list of // insecure registries. Insecure registries accept HTTP and/or accept // HTTPS with certificates from unknown CAs. Secure bool // Official indicates whether this is an official registry Official bool } // DistributionInspect describes the result obtained from contacting the // registry to retrieve image metadata type DistributionInspect struct { // Descriptor contains information about the manifest, including // the content addressable digest Descriptor ocispec.Descriptor // Platforms contains the list of platforms supported by the image, // obtained by parsing the manifest Platforms []ocispec.Platform } ================================================ FILE: vendor/github.com/docker/docker/api/types/registry/search.go ================================================ package registry import ( "context" "github.com/docker/docker/api/types/filters" ) // SearchOptions holds parameters to search images with. type SearchOptions struct { RegistryAuth string // PrivilegeFunc is a function that clients can supply to retry operations // after getting an authorization error. This function returns the registry // authentication header value in base64 encoded format, or an error if the // privilege request fails. // // For details, refer to [github.com/docker/docker/api/types/registry.RequestAuthConfig]. PrivilegeFunc func(context.Context) (string, error) Filters filters.Args Limit int } // SearchResult describes a search result returned from a registry type SearchResult struct { // StarCount indicates the number of stars this repository has StarCount int `json:"star_count"` // IsOfficial is true if the result is from an official repository. IsOfficial bool `json:"is_official"` // Name is the name of the repository Name string `json:"name"` // IsAutomated indicates whether the result is automated. // // Deprecated: the "is_automated" field is deprecated and will always be "false". IsAutomated bool `json:"is_automated"` // Description is a textual description of the repository Description string `json:"description"` } // SearchResults lists a collection search results returned from a registry type SearchResults struct { // Query contains the query string that generated the search results Query string `json:"query"` // NumResults indicates the number of results the query returned NumResults int `json:"num_results"` // Results is a slice containing the actual results for the search Results []SearchResult `json:"results"` } ================================================ FILE: vendor/github.com/docker/docker/api/types/versions/compare.go ================================================ package versions import ( "strconv" "strings" ) // compare compares two version strings // returns -1 if v1 < v2, 1 if v1 > v2, 0 otherwise. func compare(v1, v2 string) int { if v1 == v2 { return 0 } var ( currTab = strings.Split(v1, ".") otherTab = strings.Split(v2, ".") ) maxVer := len(currTab) if len(otherTab) > maxVer { maxVer = len(otherTab) } for i := 0; i < maxVer; i++ { var currInt, otherInt int if len(currTab) > i { currInt, _ = strconv.Atoi(currTab[i]) } if len(otherTab) > i { otherInt, _ = strconv.Atoi(otherTab[i]) } if currInt > otherInt { return 1 } if otherInt > currInt { return -1 } } return 0 } // LessThan checks if a version is less than another func LessThan(v, other string) bool { return compare(v, other) == -1 } // LessThanOrEqualTo checks if a version is less than or equal to another func LessThanOrEqualTo(v, other string) bool { return compare(v, other) <= 0 } // GreaterThan checks if a version is greater than another func GreaterThan(v, other string) bool { return compare(v, other) == 1 } // GreaterThanOrEqualTo checks if a version is greater than or equal to another func GreaterThanOrEqualTo(v, other string) bool { return compare(v, other) >= 0 } // Equal checks if a version is equal to another func Equal(v, other string) bool { return compare(v, other) == 0 } ================================================ FILE: vendor/github.com/docker/docker-credential-helpers/LICENSE ================================================ Copyright (c) 2016 David Calavera Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: vendor/github.com/docker/docker-credential-helpers/client/client.go ================================================ package client import ( "bytes" "encoding/json" "fmt" "strings" "github.com/docker/docker-credential-helpers/credentials" ) // isValidCredsMessage checks if 'msg' contains invalid credentials error message. // It returns whether the logs are free of invalid credentials errors and the error if it isn't. // error values can be errCredentialsMissingServerURL or errCredentialsMissingUsername. func isValidCredsMessage(msg string) error { if credentials.IsCredentialsMissingServerURLMessage(msg) { return credentials.NewErrCredentialsMissingServerURL() } if credentials.IsCredentialsMissingUsernameMessage(msg) { return credentials.NewErrCredentialsMissingUsername() } return nil } // Store uses an external program to save credentials. func Store(program ProgramFunc, creds *credentials.Credentials) error { cmd := program(credentials.ActionStore) buffer := new(bytes.Buffer) if err := json.NewEncoder(buffer).Encode(creds); err != nil { return err } cmd.Input(buffer) out, err := cmd.Output() if err != nil { if isValidErr := isValidCredsMessage(string(out)); isValidErr != nil { err = isValidErr } return fmt.Errorf("error storing credentials - err: %v, out: `%s`", err, strings.TrimSpace(string(out))) } return nil } // Get executes an external program to get the credentials from a native store. func Get(program ProgramFunc, serverURL string) (*credentials.Credentials, error) { cmd := program(credentials.ActionGet) cmd.Input(strings.NewReader(serverURL)) out, err := cmd.Output() if err != nil { if credentials.IsErrCredentialsNotFoundMessage(string(out)) { return nil, credentials.NewErrCredentialsNotFound() } if isValidErr := isValidCredsMessage(string(out)); isValidErr != nil { err = isValidErr } return nil, fmt.Errorf("error getting credentials - err: %v, out: `%s`", err, strings.TrimSpace(string(out))) } resp := &credentials.Credentials{ ServerURL: serverURL, } if err := json.NewDecoder(bytes.NewReader(out)).Decode(resp); err != nil { return nil, err } return resp, nil } // Erase executes a program to remove the server credentials from the native store. func Erase(program ProgramFunc, serverURL string) error { cmd := program(credentials.ActionErase) cmd.Input(strings.NewReader(serverURL)) out, err := cmd.Output() if err != nil { t := strings.TrimSpace(string(out)) if isValidErr := isValidCredsMessage(t); isValidErr != nil { err = isValidErr } return fmt.Errorf("error erasing credentials - err: %v, out: `%s`", err, t) } return nil } // List executes a program to list server credentials in the native store. func List(program ProgramFunc) (map[string]string, error) { cmd := program(credentials.ActionList) cmd.Input(strings.NewReader("unused")) out, err := cmd.Output() if err != nil { t := strings.TrimSpace(string(out)) if isValidErr := isValidCredsMessage(t); isValidErr != nil { err = isValidErr } return nil, fmt.Errorf("error listing credentials - err: %v, out: `%s`", err, t) } var resp map[string]string if err = json.NewDecoder(bytes.NewReader(out)).Decode(&resp); err != nil { return nil, err } return resp, nil } ================================================ FILE: vendor/github.com/docker/docker-credential-helpers/client/command.go ================================================ package client import ( "io" "os" "os/exec" ) // Program is an interface to execute external programs. type Program interface { Output() ([]byte, error) Input(in io.Reader) } // ProgramFunc is a type of function that initializes programs based on arguments. type ProgramFunc func(args ...string) Program // NewShellProgramFunc creates a [ProgramFunc] to run command in a [Shell]. func NewShellProgramFunc(command string) ProgramFunc { return func(args ...string) Program { return createProgramCmdRedirectErr(command, args, nil) } } // NewShellProgramFuncWithEnv creates a [ProgramFunc] tu run command // in a [Shell] with the given environment variables. func NewShellProgramFuncWithEnv(command string, env *map[string]string) ProgramFunc { return func(args ...string) Program { return createProgramCmdRedirectErr(command, args, env) } } func createProgramCmdRedirectErr(command string, args []string, env *map[string]string) *Shell { ec := exec.Command(command, args...) if env != nil { for k, v := range *env { ec.Env = append(ec.Environ(), k+"="+v) } } ec.Stderr = os.Stderr return &Shell{cmd: ec} } // Shell invokes shell commands to talk with a remote credentials-helper. type Shell struct { cmd *exec.Cmd } // Output returns responses from the remote credentials-helper. func (s *Shell) Output() ([]byte, error) { return s.cmd.Output() } // Input sets the input to send to a remote credentials-helper. func (s *Shell) Input(in io.Reader) { s.cmd.Stdin = in } ================================================ FILE: vendor/github.com/docker/docker-credential-helpers/credentials/credentials.go ================================================ package credentials import ( "bufio" "bytes" "encoding/json" "fmt" "io" "os" "strings" ) // Action defines the name of an action (sub-command) supported by a // credential-helper binary. It is an alias for "string", and mostly // for convenience. type Action = string // List of actions (sub-commands) supported by credential-helper binaries. const ( ActionStore Action = "store" ActionGet Action = "get" ActionErase Action = "erase" ActionList Action = "list" ActionVersion Action = "version" ) // Credentials holds the information shared between docker and the credentials store. type Credentials struct { ServerURL string Username string Secret string } // isValid checks the integrity of Credentials object such that no credentials lack // a server URL or a username. // It returns whether the credentials are valid and the error if it isn't. // error values can be errCredentialsMissingServerURL or errCredentialsMissingUsername func (c *Credentials) isValid() (bool, error) { if len(c.ServerURL) == 0 { return false, NewErrCredentialsMissingServerURL() } if len(c.Username) == 0 { return false, NewErrCredentialsMissingUsername() } return true, nil } // CredsLabel holds the way Docker credentials should be labeled as such in credentials stores that allow labelling. // That label allows to filter out non-Docker credentials too at lookup/search in macOS keychain, // Windows credentials manager and Linux libsecret. Default value is "Docker Credentials" var CredsLabel = "Docker Credentials" // SetCredsLabel is a simple setter for CredsLabel func SetCredsLabel(label string) { CredsLabel = label } // Serve initializes the credentials-helper and parses the action argument. // This function is designed to be called from a command line interface. // It uses os.Args[1] as the key for the action. // It uses os.Stdin as input and os.Stdout as output. // This function terminates the program with os.Exit(1) if there is an error. func Serve(helper Helper) { if len(os.Args) != 2 { _, _ = fmt.Fprintln(os.Stdout, usage()) os.Exit(1) } switch os.Args[1] { case "--version", "-v": _ = PrintVersion(os.Stdout) os.Exit(0) case "--help", "-h": _, _ = fmt.Fprintln(os.Stdout, usage()) os.Exit(0) } if err := HandleCommand(helper, os.Args[1], os.Stdin, os.Stdout); err != nil { _, _ = fmt.Fprintln(os.Stdout, err) os.Exit(1) } } func usage() string { return fmt.Sprintf("Usage: %s ", Name) } // HandleCommand runs a helper to execute a credential action. func HandleCommand(helper Helper, action Action, in io.Reader, out io.Writer) error { switch action { case ActionStore: return Store(helper, in) case ActionGet: return Get(helper, in, out) case ActionErase: return Erase(helper, in) case ActionList: return List(helper, out) case ActionVersion: return PrintVersion(out) default: return fmt.Errorf("%s: unknown action: %s", Name, action) } } // Store uses a helper and an input reader to save credentials. // The reader must contain the JSON serialization of a Credentials struct. func Store(helper Helper, reader io.Reader) error { scanner := bufio.NewScanner(reader) buffer := new(bytes.Buffer) for scanner.Scan() { buffer.Write(scanner.Bytes()) } if err := scanner.Err(); err != nil && err != io.EOF { return err } var creds Credentials if err := json.NewDecoder(buffer).Decode(&creds); err != nil { return err } if ok, err := creds.isValid(); !ok { return err } return helper.Add(&creds) } // Get retrieves the credentials for a given server url. // The reader must contain the server URL to search. // The writer is used to write the JSON serialization of the credentials. func Get(helper Helper, reader io.Reader, writer io.Writer) error { scanner := bufio.NewScanner(reader) buffer := new(bytes.Buffer) for scanner.Scan() { buffer.Write(scanner.Bytes()) } if err := scanner.Err(); err != nil && err != io.EOF { return err } serverURL := strings.TrimSpace(buffer.String()) if len(serverURL) == 0 { return NewErrCredentialsMissingServerURL() } username, secret, err := helper.Get(serverURL) if err != nil { return err } buffer.Reset() err = json.NewEncoder(buffer).Encode(Credentials{ ServerURL: serverURL, Username: username, Secret: secret, }) if err != nil { return err } _, _ = fmt.Fprint(writer, buffer.String()) return nil } // Erase removes credentials from the store. // The reader must contain the server URL to remove. func Erase(helper Helper, reader io.Reader) error { scanner := bufio.NewScanner(reader) buffer := new(bytes.Buffer) for scanner.Scan() { buffer.Write(scanner.Bytes()) } if err := scanner.Err(); err != nil && err != io.EOF { return err } serverURL := strings.TrimSpace(buffer.String()) if len(serverURL) == 0 { return NewErrCredentialsMissingServerURL() } return helper.Delete(serverURL) } // List returns all the serverURLs of keys in // the OS store as a list of strings func List(helper Helper, writer io.Writer) error { accts, err := helper.List() if err != nil { return err } return json.NewEncoder(writer).Encode(accts) } // PrintVersion outputs the current version. func PrintVersion(writer io.Writer) error { _, _ = fmt.Fprintf(writer, "%s (%s) %s\n", Name, Package, Version) return nil } ================================================ FILE: vendor/github.com/docker/docker-credential-helpers/credentials/error.go ================================================ package credentials import ( "errors" "strings" ) const ( // ErrCredentialsNotFound standardizes the not found error, so every helper returns // the same message and docker can handle it properly. errCredentialsNotFoundMessage = "credentials not found in native keychain" // ErrCredentialsMissingServerURL and ErrCredentialsMissingUsername standardize // invalid credentials or credentials management operations errCredentialsMissingServerURLMessage = "no credentials server URL" errCredentialsMissingUsernameMessage = "no credentials username" ) // errCredentialsNotFound represents an error // raised when credentials are not in the store. type errCredentialsNotFound struct{} // Error returns the standard error message // for when the credentials are not in the store. func (errCredentialsNotFound) Error() string { return errCredentialsNotFoundMessage } // NotFound implements the [ErrNotFound][errdefs.ErrNotFound] interface. // // [errdefs.ErrNotFound]: https://pkg.go.dev/github.com/docker/docker@v24.0.1+incompatible/errdefs#ErrNotFound func (errCredentialsNotFound) NotFound() {} // NewErrCredentialsNotFound creates a new error // for when the credentials are not in the store. func NewErrCredentialsNotFound() error { return errCredentialsNotFound{} } // IsErrCredentialsNotFound returns true if the error // was caused by not having a set of credentials in a store. func IsErrCredentialsNotFound(err error) bool { var target errCredentialsNotFound return errors.As(err, &target) } // IsErrCredentialsNotFoundMessage returns true if the error // was caused by not having a set of credentials in a store. // // This function helps to check messages returned by an // external program via its standard output. func IsErrCredentialsNotFoundMessage(err string) bool { return strings.TrimSpace(err) == errCredentialsNotFoundMessage } // errCredentialsMissingServerURL represents an error raised // when the credentials object has no server URL or when no // server URL is provided to a credentials operation requiring // one. type errCredentialsMissingServerURL struct{} func (errCredentialsMissingServerURL) Error() string { return errCredentialsMissingServerURLMessage } // InvalidParameter implements the [ErrInvalidParameter][errdefs.ErrInvalidParameter] // interface. // // [errdefs.ErrInvalidParameter]: https://pkg.go.dev/github.com/docker/docker@v24.0.1+incompatible/errdefs#ErrInvalidParameter func (errCredentialsMissingServerURL) InvalidParameter() {} // errCredentialsMissingUsername represents an error raised // when the credentials object has no username or when no // username is provided to a credentials operation requiring // one. type errCredentialsMissingUsername struct{} func (errCredentialsMissingUsername) Error() string { return errCredentialsMissingUsernameMessage } // InvalidParameter implements the [ErrInvalidParameter][errdefs.ErrInvalidParameter] // interface. // // [errdefs.ErrInvalidParameter]: https://pkg.go.dev/github.com/docker/docker@v24.0.1+incompatible/errdefs#ErrInvalidParameter func (errCredentialsMissingUsername) InvalidParameter() {} // NewErrCredentialsMissingServerURL creates a new error for // errCredentialsMissingServerURL. func NewErrCredentialsMissingServerURL() error { return errCredentialsMissingServerURL{} } // NewErrCredentialsMissingUsername creates a new error for // errCredentialsMissingUsername. func NewErrCredentialsMissingUsername() error { return errCredentialsMissingUsername{} } // IsCredentialsMissingServerURL returns true if the error // was an errCredentialsMissingServerURL. func IsCredentialsMissingServerURL(err error) bool { var target errCredentialsMissingServerURL return errors.As(err, &target) } // IsCredentialsMissingServerURLMessage checks for an // errCredentialsMissingServerURL in the error message. func IsCredentialsMissingServerURLMessage(err string) bool { return strings.TrimSpace(err) == errCredentialsMissingServerURLMessage } // IsCredentialsMissingUsername returns true if the error // was an errCredentialsMissingUsername. func IsCredentialsMissingUsername(err error) bool { var target errCredentialsMissingUsername return errors.As(err, &target) } // IsCredentialsMissingUsernameMessage checks for an // errCredentialsMissingUsername in the error message. func IsCredentialsMissingUsernameMessage(err string) bool { return strings.TrimSpace(err) == errCredentialsMissingUsernameMessage } ================================================ FILE: vendor/github.com/docker/docker-credential-helpers/credentials/helper.go ================================================ package credentials // Helper is the interface a credentials store helper must implement. type Helper interface { // Add appends credentials to the store. Add(*Credentials) error // Delete removes credentials from the store. Delete(serverURL string) error // Get retrieves credentials from the store. // It returns username and secret as strings. Get(serverURL string) (string, string, error) // List returns the stored serverURLs and their associated usernames. List() (map[string]string, error) } ================================================ FILE: vendor/github.com/docker/docker-credential-helpers/credentials/version.go ================================================ package credentials var ( // Name is filled at linking time Name = "" // Package is filled at linking time Package = "github.com/docker/docker-credential-helpers" // Version holds the complete version number. Filled in at linking time. Version = "v0.0.0+unknown" // Revision is filled with the VCS (e.g. git) revision being used to build // the program at linking time. Revision = "" ) ================================================ FILE: vendor/github.com/docker/go-connections/LICENSE ================================================ Apache License Version 2.0, January 2004 https://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 Copyright 2015 Docker, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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: vendor/github.com/docker/go-connections/sockets/inmem_socket.go ================================================ package sockets import ( "errors" "net" "sync" ) var errClosed = errors.New("use of closed network connection") // InmemSocket implements net.Listener using in-memory only connections. type InmemSocket struct { chConn chan net.Conn chClose chan struct{} addr string mu sync.Mutex } // dummyAddr is used to satisfy net.Addr for the in-mem socket // it is just stored as a string and returns the string for all calls type dummyAddr string // NewInmemSocket creates an in-memory only net.Listener // The addr argument can be any string, but is used to satisfy the `Addr()` part // of the net.Listener interface func NewInmemSocket(addr string, bufSize int) *InmemSocket { return &InmemSocket{ chConn: make(chan net.Conn, bufSize), chClose: make(chan struct{}), addr: addr, } } // Addr returns the socket's addr string to satisfy net.Listener func (s *InmemSocket) Addr() net.Addr { return dummyAddr(s.addr) } // Accept implements the Accept method in the Listener interface; it waits for the next call and returns a generic Conn. func (s *InmemSocket) Accept() (net.Conn, error) { select { case conn := <-s.chConn: return conn, nil case <-s.chClose: return nil, errClosed } } // Close closes the listener. It will be unavailable for use once closed. func (s *InmemSocket) Close() error { s.mu.Lock() defer s.mu.Unlock() select { case <-s.chClose: default: close(s.chClose) } return nil } // Dial is used to establish a connection with the in-mem server func (s *InmemSocket) Dial(network, addr string) (net.Conn, error) { srvConn, clientConn := net.Pipe() select { case s.chConn <- srvConn: case <-s.chClose: return nil, errClosed } return clientConn, nil } // Network returns the addr string, satisfies net.Addr func (a dummyAddr) Network() string { return string(a) } // String returns the string form func (a dummyAddr) String() string { return string(a) } ================================================ FILE: vendor/github.com/docker/go-connections/sockets/proxy.go ================================================ package sockets import ( "net" "os" "strings" ) // GetProxyEnv allows access to the uppercase and the lowercase forms of // proxy-related variables. See the Go specification for details on these // variables. https://golang.org/pkg/net/http/ // // Deprecated: this function was used as helper for [DialerFromEnvironment] and is no longer used. It will be removed in the next release. func GetProxyEnv(key string) string { proxyValue := os.Getenv(strings.ToUpper(key)) if proxyValue == "" { return os.Getenv(strings.ToLower(key)) } return proxyValue } // DialerFromEnvironment was previously used to configure a net.Dialer to route // connections through a SOCKS proxy. // // Deprecated: SOCKS proxies are now supported by configuring only // http.Transport.Proxy, and no longer require changing http.Transport.Dial. // Therefore, only [sockets.ConfigureTransport] needs to be called, and any // [sockets.DialerFromEnvironment] calls can be dropped. func DialerFromEnvironment(direct *net.Dialer) (*net.Dialer, error) { return direct, nil } ================================================ FILE: vendor/github.com/docker/go-connections/sockets/sockets.go ================================================ // Package sockets provides helper functions to create and configure Unix or TCP sockets. package sockets import ( "context" "errors" "fmt" "net" "net/http" "syscall" "time" ) const ( defaultTimeout = 10 * time.Second maxUnixSocketPathSize = len(syscall.RawSockaddrUnix{}.Path) ) // ErrProtocolNotAvailable is returned when a given transport protocol is not provided by the operating system. var ErrProtocolNotAvailable = errors.New("protocol not available") // ConfigureTransport configures the specified [http.Transport] according to the specified proto // and addr. // // If the proto is unix (using a unix socket to communicate) or npipe the compression is disabled. // For other protos, compression is enabled. If you want to manually enable/disable compression, // make sure you do it _after_ any subsequent calls to ConfigureTransport is made against the same // [http.Transport]. func ConfigureTransport(tr *http.Transport, proto, addr string) error { switch proto { case "unix": return configureUnixTransport(tr, proto, addr) case "npipe": return configureNpipeTransport(tr, proto, addr) default: tr.Proxy = http.ProxyFromEnvironment tr.DisableCompression = false tr.DialContext = (&net.Dialer{ Timeout: defaultTimeout, }).DialContext } return nil } // DialPipe connects to a Windows named pipe. It is not supported on // non-Windows platforms. // // Deprecated: use [github.com/Microsoft/go-winio.DialPipe] or [github.com/Microsoft/go-winio.DialPipeContext]. func DialPipe(addr string, timeout time.Duration) (net.Conn, error) { return dialPipe(addr, timeout) } func configureUnixTransport(tr *http.Transport, proto, addr string) error { if len(addr) > maxUnixSocketPathSize { return fmt.Errorf("unix socket path %q is too long", addr) } // No need for compression in local communications. tr.DisableCompression = true dialer := &net.Dialer{ Timeout: defaultTimeout, } tr.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) { return dialer.DialContext(ctx, proto, addr) } return nil } ================================================ FILE: vendor/github.com/docker/go-connections/sockets/sockets_unix.go ================================================ //go:build !windows package sockets import ( "net" "net/http" "syscall" "time" ) func configureNpipeTransport(tr *http.Transport, proto, addr string) error { return ErrProtocolNotAvailable } func dialPipe(_ string, _ time.Duration) (net.Conn, error) { return nil, syscall.EAFNOSUPPORT } ================================================ FILE: vendor/github.com/docker/go-connections/sockets/sockets_windows.go ================================================ package sockets import ( "context" "net" "net/http" "time" "github.com/Microsoft/go-winio" ) func configureNpipeTransport(tr *http.Transport, proto, addr string) error { // No need for compression in local communications. tr.DisableCompression = true tr.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) { return winio.DialPipeContext(ctx, addr) } return nil } func dialPipe(addr string, timeout time.Duration) (net.Conn, error) { return winio.DialPipe(addr, &timeout) } ================================================ FILE: vendor/github.com/docker/go-connections/sockets/tcp_socket.go ================================================ // Package sockets provides helper functions to create and configure Unix or TCP sockets. package sockets import ( "crypto/tls" "net" ) // NewTCPSocket creates a TCP socket listener with the specified address and // the specified tls configuration. If TLSConfig is set, will encapsulate the // TCP listener inside a TLS one. func NewTCPSocket(addr string, tlsConfig *tls.Config) (net.Listener, error) { l, err := net.Listen("tcp", addr) if err != nil { return nil, err } if tlsConfig != nil { tlsConfig.NextProtos = []string{"http/1.1"} l = tls.NewListener(l, tlsConfig) } return l, nil } ================================================ FILE: vendor/github.com/docker/go-connections/sockets/unix_socket.go ================================================ /* Package sockets is a simple unix domain socket wrapper. # Usage For example: import( "fmt" "net" "os" "github.com/docker/go-connections/sockets" ) func main() { l, err := sockets.NewUnixSocketWithOpts("/path/to/sockets", sockets.WithChown(0,0),sockets.WithChmod(0660)) if err != nil { panic(err) } echoStr := "hello" go func() { for { conn, err := l.Accept() if err != nil { return } conn.Write([]byte(echoStr)) conn.Close() } }() conn, err := net.Dial("unix", path) if err != nil { t.Fatal(err) } buf := make([]byte, 5) if _, err := conn.Read(buf); err != nil { panic(err) } else if string(buf) != echoStr { panic(fmt.Errorf("msg may lost")) } } */ package sockets import ( "net" "os" "syscall" ) // SockOption sets up socket file's creating option type SockOption func(string) error // NewUnixSocketWithOpts creates a unix socket with the specified options. // By default, socket permissions are 0000 (i.e.: no access for anyone); pass // WithChmod() and WithChown() to set the desired ownership and permissions. // // This function temporarily changes the system's "umask" to 0777 to work around // a race condition between creating the socket and setting its permissions. While // this should only be for a short duration, it may affect other processes that // create files/directories during that period. func NewUnixSocketWithOpts(path string, opts ...SockOption) (net.Listener, error) { if err := syscall.Unlink(path); err != nil && !os.IsNotExist(err) { return nil, err } l, err := listenUnix(path) if err != nil { return nil, err } for _, op := range opts { if err := op(path); err != nil { _ = l.Close() return nil, err } } return l, nil } ================================================ FILE: vendor/github.com/docker/go-connections/sockets/unix_socket_unix.go ================================================ //go:build !windows package sockets import ( "net" "os" "syscall" ) // WithChown modifies the socket file's uid and gid func WithChown(uid, gid int) SockOption { return func(path string) error { if err := os.Chown(path, uid, gid); err != nil { return err } return nil } } // WithChmod modifies socket file's access mode. func WithChmod(mask os.FileMode) SockOption { return func(path string) error { if err := os.Chmod(path, mask); err != nil { return err } return nil } } // NewUnixSocket creates a unix socket with the specified path and group. func NewUnixSocket(path string, gid int) (net.Listener, error) { return NewUnixSocketWithOpts(path, WithChown(0, gid), WithChmod(0o660)) } func listenUnix(path string) (net.Listener, error) { // net.Listen does not allow for permissions to be set. As a result, when // specifying custom permissions ("WithChmod()"), there is a short time // between creating the socket and applying the permissions, during which // the socket permissions are Less restrictive than desired. // // To work around this limitation of net.Listen(), we temporarily set the // umask to 0777, which forces the socket to be created with 000 permissions // (i.e.: no access for anyone). After that, WithChmod() must be used to set // the desired permissions. // // We don't use "defer" here, to reset the umask to its original value as soon // as possible. Ideally we'd be able to detect if WithChmod() was passed as // an option, and skip changing umask if default permissions are used. origUmask := syscall.Umask(0o777) l, err := net.Listen("unix", path) syscall.Umask(origUmask) return l, err } ================================================ FILE: vendor/github.com/docker/go-connections/sockets/unix_socket_windows.go ================================================ package sockets import "net" func listenUnix(path string) (net.Listener, error) { return net.Listen("unix", path) } ================================================ FILE: vendor/github.com/docker/go-connections/tlsconfig/certpool.go ================================================ package tlsconfig import ( "crypto/x509" "runtime" ) // SystemCertPool returns a copy of the system cert pool, // returns an error if failed to load or empty pool on windows. func SystemCertPool() (*x509.CertPool, error) { certpool, err := x509.SystemCertPool() if err != nil && runtime.GOOS == "windows" { return x509.NewCertPool(), nil } return certpool, err } ================================================ FILE: vendor/github.com/docker/go-connections/tlsconfig/config.go ================================================ // Package tlsconfig provides primitives to retrieve secure-enough TLS configurations for both clients and servers. // // As a reminder from https://golang.org/pkg/crypto/tls/#Config: // // A Config structure is used to configure a TLS client or server. After one has been passed to a TLS function it must not be modified. // A Config may be reused; the tls package will also not modify it. package tlsconfig import ( "crypto/tls" "crypto/x509" "encoding/pem" "errors" "fmt" "os" ) // Options represents the information needed to create client and server TLS configurations. type Options struct { CAFile string // If either CertFile or KeyFile is empty, Client() will not load them // preventing the client from authenticating to the server. // However, Server() requires them and will error out if they are empty. CertFile string KeyFile string // client-only option InsecureSkipVerify bool // server-only option ClientAuth tls.ClientAuthType // If ExclusiveRootPools is set, then if a CA file is provided, the root pool used for TLS // creds will include exclusively the roots in that CA file. If no CA file is provided, // the system pool will be used. ExclusiveRootPools bool MinVersion uint16 } // DefaultServerAcceptedCiphers should be uses by code which already has a crypto/tls // options struct but wants to use a commonly accepted set of TLS cipher suites, with // known weak algorithms removed. var DefaultServerAcceptedCiphers = defaultCipherSuites // defaultCipherSuites is shared by both client and server as the default set. var defaultCipherSuites = []uint16{ tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, } // ServerDefault returns a secure-enough TLS configuration for the server TLS configuration. func ServerDefault(ops ...func(*tls.Config)) *tls.Config { return defaultConfig(ops...) } // ClientDefault returns a secure-enough TLS configuration for the client TLS configuration. func ClientDefault(ops ...func(*tls.Config)) *tls.Config { return defaultConfig(ops...) } // defaultConfig is the default config used by both client and server TLS configuration. func defaultConfig(ops ...func(*tls.Config)) *tls.Config { tlsConfig := &tls.Config{ // Avoid fallback by default to SSL protocols < TLS1.2 MinVersion: tls.VersionTLS12, CipherSuites: defaultCipherSuites, } for _, op := range ops { op(tlsConfig) } return tlsConfig } // certPool returns an X.509 certificate pool from `caFile`, the certificate file. func certPool(caFile string, exclusivePool bool) (*x509.CertPool, error) { // If we should verify the server, we need to load a trusted ca var ( pool *x509.CertPool err error ) if exclusivePool { pool = x509.NewCertPool() } else { pool, err = SystemCertPool() if err != nil { return nil, fmt.Errorf("failed to read system certificates: %v", err) } } pemData, err := os.ReadFile(caFile) if err != nil { return nil, fmt.Errorf("could not read CA certificate %q: %v", caFile, err) } if !pool.AppendCertsFromPEM(pemData) { return nil, fmt.Errorf("failed to append certificates from PEM file: %q", caFile) } return pool, nil } // allTLSVersions lists all the TLS versions and is used by the code that validates // a uint16 value as a TLS version. var allTLSVersions = map[uint16]struct{}{ tls.VersionTLS10: {}, tls.VersionTLS11: {}, tls.VersionTLS12: {}, tls.VersionTLS13: {}, } // isValidMinVersion checks that the input value is a valid tls minimum version func isValidMinVersion(version uint16) bool { _, ok := allTLSVersions[version] return ok } // adjustMinVersion sets the MinVersion on `config`, the input configuration. // It assumes the current MinVersion on the `config` is the lowest allowed. func adjustMinVersion(options Options, config *tls.Config) error { if options.MinVersion > 0 { if !isValidMinVersion(options.MinVersion) { return fmt.Errorf("invalid minimum TLS version: %x", options.MinVersion) } if options.MinVersion < config.MinVersion { return fmt.Errorf("requested minimum TLS version is too low. Should be at-least: %x", config.MinVersion) } config.MinVersion = options.MinVersion } return nil } // errEncryptedKeyDeprecated is produced when we encounter an encrypted // (password-protected) key. From https://go-review.googlesource.com/c/go/+/264159; // // > Legacy PEM encryption as specified in RFC 1423 is insecure by design. Since // > it does not authenticate the ciphertext, it is vulnerable to padding oracle // > attacks that can let an attacker recover the plaintext // > // > It's unfortunate that we don't implement PKCS#8 encryption so we can't // > recommend an alternative but PEM encryption is so broken that it's worth // > deprecating outright. // // Also see https://docs.docker.com/go/deprecated/ var errEncryptedKeyDeprecated = errors.New("private key is encrypted; encrypted private keys are obsolete, and not supported") // getPrivateKey returns the private key in 'keyBytes', in PEM-encoded format. // It returns an error if the file could not be decoded or was protected by // a passphrase. func getPrivateKey(keyBytes []byte) ([]byte, error) { // this section makes some small changes to code from notary/tuf/utils/x509.go pemBlock, _ := pem.Decode(keyBytes) if pemBlock == nil { return nil, fmt.Errorf("no valid private key found") } if x509.IsEncryptedPEMBlock(pemBlock) { //nolint:staticcheck // Ignore SA1019 (IsEncryptedPEMBlock is deprecated) return nil, errEncryptedKeyDeprecated } return keyBytes, nil } // getCert returns a Certificate from the CertFile and KeyFile in 'options', // if the key is encrypted, the Passphrase in 'options' will be used to // decrypt it. func getCert(options Options) ([]tls.Certificate, error) { if options.CertFile == "" && options.KeyFile == "" { return nil, nil } cert, err := os.ReadFile(options.CertFile) if err != nil { return nil, err } prKeyBytes, err := os.ReadFile(options.KeyFile) if err != nil { return nil, err } prKeyBytes, err = getPrivateKey(prKeyBytes) if err != nil { return nil, err } tlsCert, err := tls.X509KeyPair(cert, prKeyBytes) if err != nil { return nil, err } return []tls.Certificate{tlsCert}, nil } // Client returns a TLS configuration meant to be used by a client. func Client(options Options) (*tls.Config, error) { tlsConfig := defaultConfig() tlsConfig.InsecureSkipVerify = options.InsecureSkipVerify if !options.InsecureSkipVerify && options.CAFile != "" { CAs, err := certPool(options.CAFile, options.ExclusiveRootPools) if err != nil { return nil, err } tlsConfig.RootCAs = CAs } tlsCerts, err := getCert(options) if err != nil { return nil, fmt.Errorf("could not load X509 key pair: %w", err) } tlsConfig.Certificates = tlsCerts if err := adjustMinVersion(options, tlsConfig); err != nil { return nil, err } return tlsConfig, nil } // Server returns a TLS configuration meant to be used by a server. func Server(options Options) (*tls.Config, error) { tlsConfig := defaultConfig() tlsConfig.ClientAuth = options.ClientAuth tlsCert, err := tls.LoadX509KeyPair(options.CertFile, options.KeyFile) if err != nil { if os.IsNotExist(err) { return nil, fmt.Errorf("could not load X509 key pair (cert: %q, key: %q): %v", options.CertFile, options.KeyFile, err) } return nil, fmt.Errorf("error reading X509 key pair - make sure the key is not encrypted (cert: %q, key: %q): %v", options.CertFile, options.KeyFile, err) } tlsConfig.Certificates = []tls.Certificate{tlsCert} if options.ClientAuth >= tls.VerifyClientCertIfGiven && options.CAFile != "" { CAs, err := certPool(options.CAFile, options.ExclusiveRootPools) if err != nil { return nil, err } tlsConfig.ClientCAs = CAs } if err := adjustMinVersion(options, tlsConfig); err != nil { return nil, err } return tlsConfig, nil } ================================================ FILE: vendor/github.com/docker/go-units/CONTRIBUTING.md ================================================ # Contributing to go-units Want to hack on go-units? Awesome! Here are instructions to get you started. go-units is a part of the [Docker](https://www.docker.com) project, and follows the same rules and principles. If you're already familiar with the way Docker does things, you'll feel right at home. Otherwise, go read Docker's [contributions guidelines](https://github.com/docker/docker/blob/master/CONTRIBUTING.md), [issue triaging](https://github.com/docker/docker/blob/master/project/ISSUE-TRIAGE.md), [review process](https://github.com/docker/docker/blob/master/project/REVIEWING.md) and [branches and tags](https://github.com/docker/docker/blob/master/project/BRANCHES-AND-TAGS.md). ### Sign your work The sign-off is a simple line at the end of the explanation for the patch. Your signature certifies that you wrote the patch or otherwise have the right to pass it on as an open-source patch. The rules are pretty simple: if you can certify the below (from [developercertificate.org](http://developercertificate.org/)): ``` Developer Certificate of Origin Version 1.1 Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 660 York Street, Suite 102, San Francisco, CA 94110 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. ``` Then you just add a line to every git commit message: Signed-off-by: Joe Smith Use your real name (sorry, no pseudonyms or anonymous contributions.) If you set your `user.name` and `user.email` git configs, you can sign your commit automatically with `git commit -s`. ================================================ FILE: vendor/github.com/docker/go-units/LICENSE ================================================ Apache License Version 2.0, January 2004 https://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 Copyright 2015 Docker, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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: vendor/github.com/docker/go-units/MAINTAINERS ================================================ # go-units maintainers file # # This file describes who runs the docker/go-units project and how. # This is a living document - if you see something out of date or missing, speak up! # # It is structured to be consumable by both humans and programs. # To extract its contents programmatically, use any TOML-compliant parser. # # This file is compiled into the MAINTAINERS file in docker/opensource. # [Org] [Org."Core maintainers"] people = [ "akihirosuda", "dnephin", "thajeztah", "vdemeester", ] [people] # A reference list of all people associated with the project. # All other sections should refer to people by their canonical key # in the people section. # ADD YOURSELF HERE IN ALPHABETICAL ORDER [people.akihirosuda] Name = "Akihiro Suda" Email = "akihiro.suda.cz@hco.ntt.co.jp" GitHub = "AkihiroSuda" [people.dnephin] Name = "Daniel Nephin" Email = "dnephin@gmail.com" GitHub = "dnephin" [people.thajeztah] Name = "Sebastiaan van Stijn" Email = "github@gone.nl" GitHub = "thaJeztah" [people.vdemeester] Name = "Vincent Demeester" Email = "vincent@sbr.pm" GitHub = "vdemeester" ================================================ FILE: vendor/github.com/docker/go-units/README.md ================================================ [![GoDoc](https://godoc.org/github.com/docker/go-units?status.svg)](https://godoc.org/github.com/docker/go-units) # Introduction go-units is a library to transform human friendly measurements into machine friendly values. ## Usage See the [docs in godoc](https://godoc.org/github.com/docker/go-units) for examples and documentation. ## Copyright and license Copyright © 2015 Docker, Inc. go-units is licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for the full text of the license. ================================================ FILE: vendor/github.com/docker/go-units/circle.yml ================================================ dependencies: post: # install golint - go get golang.org/x/lint/golint test: pre: # run analysis before tests - go vet ./... - test -z "$(golint ./... | tee /dev/stderr)" - test -z "$(gofmt -s -l . | tee /dev/stderr)" ================================================ FILE: vendor/github.com/docker/go-units/duration.go ================================================ // Package units provides helper function to parse and print size and time units // in human-readable format. package units import ( "fmt" "time" ) // HumanDuration returns a human-readable approximation of a duration // (eg. "About a minute", "4 hours ago", etc.). func HumanDuration(d time.Duration) string { if seconds := int(d.Seconds()); seconds < 1 { return "Less than a second" } else if seconds == 1 { return "1 second" } else if seconds < 60 { return fmt.Sprintf("%d seconds", seconds) } else if minutes := int(d.Minutes()); minutes == 1 { return "About a minute" } else if minutes < 60 { return fmt.Sprintf("%d minutes", minutes) } else if hours := int(d.Hours() + 0.5); hours == 1 { return "About an hour" } else if hours < 48 { return fmt.Sprintf("%d hours", hours) } else if hours < 24*7*2 { return fmt.Sprintf("%d days", hours/24) } else if hours < 24*30*2 { return fmt.Sprintf("%d weeks", hours/24/7) } else if hours < 24*365*2 { return fmt.Sprintf("%d months", hours/24/30) } return fmt.Sprintf("%d years", int(d.Hours())/24/365) } ================================================ FILE: vendor/github.com/docker/go-units/size.go ================================================ package units import ( "fmt" "strconv" "strings" ) // See: http://en.wikipedia.org/wiki/Binary_prefix const ( // Decimal KB = 1000 MB = 1000 * KB GB = 1000 * MB TB = 1000 * GB PB = 1000 * TB // Binary KiB = 1024 MiB = 1024 * KiB GiB = 1024 * MiB TiB = 1024 * GiB PiB = 1024 * TiB ) type unitMap map[byte]int64 var ( decimalMap = unitMap{'k': KB, 'm': MB, 'g': GB, 't': TB, 'p': PB} binaryMap = unitMap{'k': KiB, 'm': MiB, 'g': GiB, 't': TiB, 'p': PiB} ) var ( decimapAbbrs = []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"} binaryAbbrs = []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"} ) func getSizeAndUnit(size float64, base float64, _map []string) (float64, string) { i := 0 unitsLimit := len(_map) - 1 for size >= base && i < unitsLimit { size = size / base i++ } return size, _map[i] } // CustomSize returns a human-readable approximation of a size // using custom format. func CustomSize(format string, size float64, base float64, _map []string) string { size, unit := getSizeAndUnit(size, base, _map) return fmt.Sprintf(format, size, unit) } // HumanSizeWithPrecision allows the size to be in any precision, // instead of 4 digit precision used in units.HumanSize. func HumanSizeWithPrecision(size float64, precision int) string { size, unit := getSizeAndUnit(size, 1000.0, decimapAbbrs) return fmt.Sprintf("%.*g%s", precision, size, unit) } // HumanSize returns a human-readable approximation of a size // capped at 4 valid numbers (eg. "2.746 MB", "796 KB"). func HumanSize(size float64) string { return HumanSizeWithPrecision(size, 4) } // BytesSize returns a human-readable size in bytes, kibibytes, // mebibytes, gibibytes, or tebibytes (eg. "44kiB", "17MiB"). func BytesSize(size float64) string { return CustomSize("%.4g%s", size, 1024.0, binaryAbbrs) } // FromHumanSize returns an integer from a human-readable specification of a // size using SI standard (eg. "44kB", "17MB"). func FromHumanSize(size string) (int64, error) { return parseSize(size, decimalMap) } // RAMInBytes parses a human-readable string representing an amount of RAM // in bytes, kibibytes, mebibytes, gibibytes, or tebibytes and // returns the number of bytes, or -1 if the string is unparseable. // Units are case-insensitive, and the 'b' suffix is optional. func RAMInBytes(size string) (int64, error) { return parseSize(size, binaryMap) } // Parses the human-readable size string into the amount it represents. func parseSize(sizeStr string, uMap unitMap) (int64, error) { // TODO: rewrite to use strings.Cut if there's a space // once Go < 1.18 is deprecated. sep := strings.LastIndexAny(sizeStr, "01234567890. ") if sep == -1 { // There should be at least a digit. return -1, fmt.Errorf("invalid size: '%s'", sizeStr) } var num, sfx string if sizeStr[sep] != ' ' { num = sizeStr[:sep+1] sfx = sizeStr[sep+1:] } else { // Omit the space separator. num = sizeStr[:sep] sfx = sizeStr[sep+1:] } size, err := strconv.ParseFloat(num, 64) if err != nil { return -1, err } // Backward compatibility: reject negative sizes. if size < 0 { return -1, fmt.Errorf("invalid size: '%s'", sizeStr) } if len(sfx) == 0 { return int64(size), nil } // Process the suffix. if len(sfx) > 3 { // Too long. goto badSuffix } sfx = strings.ToLower(sfx) // Trivial case: b suffix. if sfx[0] == 'b' { if len(sfx) > 1 { // no extra characters allowed after b. goto badSuffix } return int64(size), nil } // A suffix from the map. if mul, ok := uMap[sfx[0]]; ok { size *= float64(mul) } else { goto badSuffix } // The suffix may have extra "b" or "ib" (e.g. KiB or MB). switch { case len(sfx) == 2 && sfx[1] != 'b': goto badSuffix case len(sfx) == 3 && sfx[1:] != "ib": goto badSuffix } return int64(size), nil badSuffix: return -1, fmt.Errorf("invalid suffix: '%s'", sfx) } ================================================ FILE: vendor/github.com/docker/go-units/ulimit.go ================================================ package units import ( "fmt" "strconv" "strings" ) // Ulimit is a human friendly version of Rlimit. type Ulimit struct { Name string Hard int64 Soft int64 } // Rlimit specifies the resource limits, such as max open files. type Rlimit struct { Type int `json:"type,omitempty"` Hard uint64 `json:"hard,omitempty"` Soft uint64 `json:"soft,omitempty"` } const ( // magic numbers for making the syscall // some of these are defined in the syscall package, but not all. // Also since Windows client doesn't get access to the syscall package, need to // define these here rlimitAs = 9 rlimitCore = 4 rlimitCPU = 0 rlimitData = 2 rlimitFsize = 1 rlimitLocks = 10 rlimitMemlock = 8 rlimitMsgqueue = 12 rlimitNice = 13 rlimitNofile = 7 rlimitNproc = 6 rlimitRss = 5 rlimitRtprio = 14 rlimitRttime = 15 rlimitSigpending = 11 rlimitStack = 3 ) var ulimitNameMapping = map[string]int{ //"as": rlimitAs, // Disabled since this doesn't seem usable with the way Docker inits a container. "core": rlimitCore, "cpu": rlimitCPU, "data": rlimitData, "fsize": rlimitFsize, "locks": rlimitLocks, "memlock": rlimitMemlock, "msgqueue": rlimitMsgqueue, "nice": rlimitNice, "nofile": rlimitNofile, "nproc": rlimitNproc, "rss": rlimitRss, "rtprio": rlimitRtprio, "rttime": rlimitRttime, "sigpending": rlimitSigpending, "stack": rlimitStack, } // ParseUlimit parses and returns a Ulimit from the specified string. func ParseUlimit(val string) (*Ulimit, error) { parts := strings.SplitN(val, "=", 2) if len(parts) != 2 { return nil, fmt.Errorf("invalid ulimit argument: %s", val) } if _, exists := ulimitNameMapping[parts[0]]; !exists { return nil, fmt.Errorf("invalid ulimit type: %s", parts[0]) } var ( soft int64 hard = &soft // default to soft in case no hard was set temp int64 err error ) switch limitVals := strings.Split(parts[1], ":"); len(limitVals) { case 2: temp, err = strconv.ParseInt(limitVals[1], 10, 64) if err != nil { return nil, err } hard = &temp fallthrough case 1: soft, err = strconv.ParseInt(limitVals[0], 10, 64) if err != nil { return nil, err } default: return nil, fmt.Errorf("too many limit value arguments - %s, can only have up to two, `soft[:hard]`", parts[1]) } if *hard != -1 { if soft == -1 { return nil, fmt.Errorf("ulimit soft limit must be less than or equal to hard limit: soft: -1 (unlimited), hard: %d", *hard) } if soft > *hard { return nil, fmt.Errorf("ulimit soft limit must be less than or equal to hard limit: %d > %d", soft, *hard) } } return &Ulimit{Name: parts[0], Soft: soft, Hard: *hard}, nil } // GetRlimit returns the RLimit corresponding to Ulimit. func (u *Ulimit) GetRlimit() (*Rlimit, error) { t, exists := ulimitNameMapping[u.Name] if !exists { return nil, fmt.Errorf("invalid ulimit name %s", u.Name) } return &Rlimit{Type: t, Soft: uint64(u.Soft), Hard: uint64(u.Hard)}, nil } func (u *Ulimit) String() string { return fmt.Sprintf("%s=%d:%d", u.Name, u.Soft, u.Hard) } ================================================ FILE: vendor/github.com/felixge/httpsnoop/.gitignore ================================================ ================================================ FILE: vendor/github.com/felixge/httpsnoop/LICENSE.txt ================================================ Copyright (c) 2016 Felix Geisendörfer (felix@debuggable.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: vendor/github.com/felixge/httpsnoop/Makefile ================================================ .PHONY: ci generate clean ci: clean generate go test -race -v ./... generate: go generate . clean: rm -rf *_generated*.go ================================================ FILE: vendor/github.com/felixge/httpsnoop/README.md ================================================ # httpsnoop Package httpsnoop provides an easy way to capture http related metrics (i.e. response time, bytes written, and http status code) from your application's http.Handlers. Doing this requires non-trivial wrapping of the http.ResponseWriter interface, which is also exposed for users interested in a more low-level API. [![Go Reference](https://pkg.go.dev/badge/github.com/felixge/httpsnoop.svg)](https://pkg.go.dev/github.com/felixge/httpsnoop) [![Build Status](https://github.com/felixge/httpsnoop/actions/workflows/main.yaml/badge.svg)](https://github.com/felixge/httpsnoop/actions/workflows/main.yaml) ## Usage Example ```go // myH is your app's http handler, perhaps a http.ServeMux or similar. var myH http.Handler // wrappedH wraps myH in order to log every request. wrappedH := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { m := httpsnoop.CaptureMetrics(myH, w, r) log.Printf( "%s %s (code=%d dt=%s written=%d)", r.Method, r.URL, m.Code, m.Duration, m.Written, ) }) http.ListenAndServe(":8080", wrappedH) ``` ## Why this package exists Instrumenting an application's http.Handler is surprisingly difficult. However if you google for e.g. "capture ResponseWriter status code" you'll find lots of advise and code examples that suggest it to be a fairly trivial undertaking. Unfortunately everything I've seen so far has a high chance of breaking your application. The main problem is that a `http.ResponseWriter` often implements additional interfaces such as `http.Flusher`, `http.CloseNotifier`, `http.Hijacker`, `http.Pusher`, and `io.ReaderFrom`. So the naive approach of just wrapping `http.ResponseWriter` in your own struct that also implements the `http.ResponseWriter` interface will hide the additional interfaces mentioned above. This has a high change of introducing subtle bugs into any non-trivial application. Another approach I've seen people take is to return a struct that implements all of the interfaces above. However, that's also problematic, because it's difficult to fake some of these interfaces behaviors when the underlying `http.ResponseWriter` doesn't have an implementation. It's also dangerous, because an application may choose to operate differently, merely because it detects the presence of these additional interfaces. This package solves this problem by checking which additional interfaces a `http.ResponseWriter` implements, returning a wrapped version implementing the exact same set of interfaces. Additionally this package properly handles edge cases such as `WriteHeader` not being called, or called more than once, as well as concurrent calls to `http.ResponseWriter` methods, and even calls happening after the wrapped `ServeHTTP` has already returned. Unfortunately this package is not perfect either. It's possible that it is still missing some interfaces provided by the go core (let me know if you find one), and it won't work for applications adding their own interfaces into the mix. You can however use `httpsnoop.Unwrap(w)` to access the underlying `http.ResponseWriter` and type-assert the result to its other interfaces. However, hopefully the explanation above has sufficiently scared you of rolling your own solution to this problem. httpsnoop may still break your application, but at least it tries to avoid it as much as possible. Anyway, the real problem here is that smuggling additional interfaces inside `http.ResponseWriter` is a problematic design choice, but it probably goes as deep as the Go language specification itself. But that's okay, I still prefer Go over the alternatives ;). ## Performance ``` BenchmarkBaseline-8 20000 94912 ns/op BenchmarkCaptureMetrics-8 20000 95461 ns/op ``` As you can see, using `CaptureMetrics` on a vanilla http.Handler introduces an overhead of ~500 ns per http request on my machine. However, the margin of error appears to be larger than that, therefor it should be reasonable to assume that the overhead introduced by `CaptureMetrics` is absolutely negligible. ## License MIT ================================================ FILE: vendor/github.com/felixge/httpsnoop/capture_metrics.go ================================================ package httpsnoop import ( "io" "net/http" "time" ) // Metrics holds metrics captured from CaptureMetrics. type Metrics struct { // Code is the first http response code passed to the WriteHeader func of // the ResponseWriter. If no such call is made, a default code of 200 is // assumed instead. Code int // Duration is the time it took to execute the handler. Duration time.Duration // Written is the number of bytes successfully written by the Write or // ReadFrom function of the ResponseWriter. ResponseWriters may also write // data to their underlaying connection directly (e.g. headers), but those // are not tracked. Therefor the number of Written bytes will usually match // the size of the response body. Written int64 } // CaptureMetrics wraps the given hnd, executes it with the given w and r, and // returns the metrics it captured from it. func CaptureMetrics(hnd http.Handler, w http.ResponseWriter, r *http.Request) Metrics { return CaptureMetricsFn(w, func(ww http.ResponseWriter) { hnd.ServeHTTP(ww, r) }) } // CaptureMetricsFn wraps w and calls fn with the wrapped w and returns the // resulting metrics. This is very similar to CaptureMetrics (which is just // sugar on top of this func), but is a more usable interface if your // application doesn't use the Go http.Handler interface. func CaptureMetricsFn(w http.ResponseWriter, fn func(http.ResponseWriter)) Metrics { m := Metrics{Code: http.StatusOK} m.CaptureMetrics(w, fn) return m } // CaptureMetrics wraps w and calls fn with the wrapped w and updates // Metrics m with the resulting metrics. This is similar to CaptureMetricsFn, // but allows one to customize starting Metrics object. func (m *Metrics) CaptureMetrics(w http.ResponseWriter, fn func(http.ResponseWriter)) { var ( start = time.Now() headerWritten bool hooks = Hooks{ WriteHeader: func(next WriteHeaderFunc) WriteHeaderFunc { return func(code int) { next(code) if !(code >= 100 && code <= 199) && !headerWritten { m.Code = code headerWritten = true } } }, Write: func(next WriteFunc) WriteFunc { return func(p []byte) (int, error) { n, err := next(p) m.Written += int64(n) headerWritten = true return n, err } }, ReadFrom: func(next ReadFromFunc) ReadFromFunc { return func(src io.Reader) (int64, error) { n, err := next(src) headerWritten = true m.Written += n return n, err } }, } ) fn(Wrap(w, hooks)) m.Duration += time.Since(start) } ================================================ FILE: vendor/github.com/felixge/httpsnoop/docs.go ================================================ // Package httpsnoop provides an easy way to capture http related metrics (i.e. // response time, bytes written, and http status code) from your application's // http.Handlers. // // Doing this requires non-trivial wrapping of the http.ResponseWriter // interface, which is also exposed for users interested in a more low-level // API. package httpsnoop //go:generate go run codegen/main.go ================================================ FILE: vendor/github.com/felixge/httpsnoop/wrap_generated_gteq_1.8.go ================================================ // +build go1.8 // Code generated by "httpsnoop/codegen"; DO NOT EDIT. package httpsnoop import ( "bufio" "io" "net" "net/http" ) // HeaderFunc is part of the http.ResponseWriter interface. type HeaderFunc func() http.Header // WriteHeaderFunc is part of the http.ResponseWriter interface. type WriteHeaderFunc func(code int) // WriteFunc is part of the http.ResponseWriter interface. type WriteFunc func(b []byte) (int, error) // FlushFunc is part of the http.Flusher interface. type FlushFunc func() // CloseNotifyFunc is part of the http.CloseNotifier interface. type CloseNotifyFunc func() <-chan bool // HijackFunc is part of the http.Hijacker interface. type HijackFunc func() (net.Conn, *bufio.ReadWriter, error) // ReadFromFunc is part of the io.ReaderFrom interface. type ReadFromFunc func(src io.Reader) (int64, error) // PushFunc is part of the http.Pusher interface. type PushFunc func(target string, opts *http.PushOptions) error // Hooks defines a set of method interceptors for methods included in // http.ResponseWriter as well as some others. You can think of them as // middleware for the function calls they target. See Wrap for more details. type Hooks struct { Header func(HeaderFunc) HeaderFunc WriteHeader func(WriteHeaderFunc) WriteHeaderFunc Write func(WriteFunc) WriteFunc Flush func(FlushFunc) FlushFunc CloseNotify func(CloseNotifyFunc) CloseNotifyFunc Hijack func(HijackFunc) HijackFunc ReadFrom func(ReadFromFunc) ReadFromFunc Push func(PushFunc) PushFunc } // Wrap returns a wrapped version of w that provides the exact same interface // as w. Specifically if w implements any combination of: // // - http.Flusher // - http.CloseNotifier // - http.Hijacker // - io.ReaderFrom // - http.Pusher // // The wrapped version will implement the exact same combination. If no hooks // are set, the wrapped version also behaves exactly as w. Hooks targeting // methods not supported by w are ignored. Any other hooks will intercept the // method they target and may modify the call's arguments and/or return values. // The CaptureMetrics implementation serves as a working example for how the // hooks can be used. func Wrap(w http.ResponseWriter, hooks Hooks) http.ResponseWriter { rw := &rw{w: w, h: hooks} _, i0 := w.(http.Flusher) _, i1 := w.(http.CloseNotifier) _, i2 := w.(http.Hijacker) _, i3 := w.(io.ReaderFrom) _, i4 := w.(http.Pusher) switch { // combination 1/32 case !i0 && !i1 && !i2 && !i3 && !i4: return struct { Unwrapper http.ResponseWriter }{rw, rw} // combination 2/32 case !i0 && !i1 && !i2 && !i3 && i4: return struct { Unwrapper http.ResponseWriter http.Pusher }{rw, rw, rw} // combination 3/32 case !i0 && !i1 && !i2 && i3 && !i4: return struct { Unwrapper http.ResponseWriter io.ReaderFrom }{rw, rw, rw} // combination 4/32 case !i0 && !i1 && !i2 && i3 && i4: return struct { Unwrapper http.ResponseWriter io.ReaderFrom http.Pusher }{rw, rw, rw, rw} // combination 5/32 case !i0 && !i1 && i2 && !i3 && !i4: return struct { Unwrapper http.ResponseWriter http.Hijacker }{rw, rw, rw} // combination 6/32 case !i0 && !i1 && i2 && !i3 && i4: return struct { Unwrapper http.ResponseWriter http.Hijacker http.Pusher }{rw, rw, rw, rw} // combination 7/32 case !i0 && !i1 && i2 && i3 && !i4: return struct { Unwrapper http.ResponseWriter http.Hijacker io.ReaderFrom }{rw, rw, rw, rw} // combination 8/32 case !i0 && !i1 && i2 && i3 && i4: return struct { Unwrapper http.ResponseWriter http.Hijacker io.ReaderFrom http.Pusher }{rw, rw, rw, rw, rw} // combination 9/32 case !i0 && i1 && !i2 && !i3 && !i4: return struct { Unwrapper http.ResponseWriter http.CloseNotifier }{rw, rw, rw} // combination 10/32 case !i0 && i1 && !i2 && !i3 && i4: return struct { Unwrapper http.ResponseWriter http.CloseNotifier http.Pusher }{rw, rw, rw, rw} // combination 11/32 case !i0 && i1 && !i2 && i3 && !i4: return struct { Unwrapper http.ResponseWriter http.CloseNotifier io.ReaderFrom }{rw, rw, rw, rw} // combination 12/32 case !i0 && i1 && !i2 && i3 && i4: return struct { Unwrapper http.ResponseWriter http.CloseNotifier io.ReaderFrom http.Pusher }{rw, rw, rw, rw, rw} // combination 13/32 case !i0 && i1 && i2 && !i3 && !i4: return struct { Unwrapper http.ResponseWriter http.CloseNotifier http.Hijacker }{rw, rw, rw, rw} // combination 14/32 case !i0 && i1 && i2 && !i3 && i4: return struct { Unwrapper http.ResponseWriter http.CloseNotifier http.Hijacker http.Pusher }{rw, rw, rw, rw, rw} // combination 15/32 case !i0 && i1 && i2 && i3 && !i4: return struct { Unwrapper http.ResponseWriter http.CloseNotifier http.Hijacker io.ReaderFrom }{rw, rw, rw, rw, rw} // combination 16/32 case !i0 && i1 && i2 && i3 && i4: return struct { Unwrapper http.ResponseWriter http.CloseNotifier http.Hijacker io.ReaderFrom http.Pusher }{rw, rw, rw, rw, rw, rw} // combination 17/32 case i0 && !i1 && !i2 && !i3 && !i4: return struct { Unwrapper http.ResponseWriter http.Flusher }{rw, rw, rw} // combination 18/32 case i0 && !i1 && !i2 && !i3 && i4: return struct { Unwrapper http.ResponseWriter http.Flusher http.Pusher }{rw, rw, rw, rw} // combination 19/32 case i0 && !i1 && !i2 && i3 && !i4: return struct { Unwrapper http.ResponseWriter http.Flusher io.ReaderFrom }{rw, rw, rw, rw} // combination 20/32 case i0 && !i1 && !i2 && i3 && i4: return struct { Unwrapper http.ResponseWriter http.Flusher io.ReaderFrom http.Pusher }{rw, rw, rw, rw, rw} // combination 21/32 case i0 && !i1 && i2 && !i3 && !i4: return struct { Unwrapper http.ResponseWriter http.Flusher http.Hijacker }{rw, rw, rw, rw} // combination 22/32 case i0 && !i1 && i2 && !i3 && i4: return struct { Unwrapper http.ResponseWriter http.Flusher http.Hijacker http.Pusher }{rw, rw, rw, rw, rw} // combination 23/32 case i0 && !i1 && i2 && i3 && !i4: return struct { Unwrapper http.ResponseWriter http.Flusher http.Hijacker io.ReaderFrom }{rw, rw, rw, rw, rw} // combination 24/32 case i0 && !i1 && i2 && i3 && i4: return struct { Unwrapper http.ResponseWriter http.Flusher http.Hijacker io.ReaderFrom http.Pusher }{rw, rw, rw, rw, rw, rw} // combination 25/32 case i0 && i1 && !i2 && !i3 && !i4: return struct { Unwrapper http.ResponseWriter http.Flusher http.CloseNotifier }{rw, rw, rw, rw} // combination 26/32 case i0 && i1 && !i2 && !i3 && i4: return struct { Unwrapper http.ResponseWriter http.Flusher http.CloseNotifier http.Pusher }{rw, rw, rw, rw, rw} // combination 27/32 case i0 && i1 && !i2 && i3 && !i4: return struct { Unwrapper http.ResponseWriter http.Flusher http.CloseNotifier io.ReaderFrom }{rw, rw, rw, rw, rw} // combination 28/32 case i0 && i1 && !i2 && i3 && i4: return struct { Unwrapper http.ResponseWriter http.Flusher http.CloseNotifier io.ReaderFrom http.Pusher }{rw, rw, rw, rw, rw, rw} // combination 29/32 case i0 && i1 && i2 && !i3 && !i4: return struct { Unwrapper http.ResponseWriter http.Flusher http.CloseNotifier http.Hijacker }{rw, rw, rw, rw, rw} // combination 30/32 case i0 && i1 && i2 && !i3 && i4: return struct { Unwrapper http.ResponseWriter http.Flusher http.CloseNotifier http.Hijacker http.Pusher }{rw, rw, rw, rw, rw, rw} // combination 31/32 case i0 && i1 && i2 && i3 && !i4: return struct { Unwrapper http.ResponseWriter http.Flusher http.CloseNotifier http.Hijacker io.ReaderFrom }{rw, rw, rw, rw, rw, rw} // combination 32/32 case i0 && i1 && i2 && i3 && i4: return struct { Unwrapper http.ResponseWriter http.Flusher http.CloseNotifier http.Hijacker io.ReaderFrom http.Pusher }{rw, rw, rw, rw, rw, rw, rw} } panic("unreachable") } type rw struct { w http.ResponseWriter h Hooks } func (w *rw) Unwrap() http.ResponseWriter { return w.w } func (w *rw) Header() http.Header { f := w.w.(http.ResponseWriter).Header if w.h.Header != nil { f = w.h.Header(f) } return f() } func (w *rw) WriteHeader(code int) { f := w.w.(http.ResponseWriter).WriteHeader if w.h.WriteHeader != nil { f = w.h.WriteHeader(f) } f(code) } func (w *rw) Write(b []byte) (int, error) { f := w.w.(http.ResponseWriter).Write if w.h.Write != nil { f = w.h.Write(f) } return f(b) } func (w *rw) Flush() { f := w.w.(http.Flusher).Flush if w.h.Flush != nil { f = w.h.Flush(f) } f() } func (w *rw) CloseNotify() <-chan bool { f := w.w.(http.CloseNotifier).CloseNotify if w.h.CloseNotify != nil { f = w.h.CloseNotify(f) } return f() } func (w *rw) Hijack() (net.Conn, *bufio.ReadWriter, error) { f := w.w.(http.Hijacker).Hijack if w.h.Hijack != nil { f = w.h.Hijack(f) } return f() } func (w *rw) ReadFrom(src io.Reader) (int64, error) { f := w.w.(io.ReaderFrom).ReadFrom if w.h.ReadFrom != nil { f = w.h.ReadFrom(f) } return f(src) } func (w *rw) Push(target string, opts *http.PushOptions) error { f := w.w.(http.Pusher).Push if w.h.Push != nil { f = w.h.Push(f) } return f(target, opts) } type Unwrapper interface { Unwrap() http.ResponseWriter } // Unwrap returns the underlying http.ResponseWriter from within zero or more // layers of httpsnoop wrappers. func Unwrap(w http.ResponseWriter) http.ResponseWriter { if rw, ok := w.(Unwrapper); ok { // recurse until rw.Unwrap() returns a non-Unwrapper return Unwrap(rw.Unwrap()) } else { return w } } ================================================ FILE: vendor/github.com/felixge/httpsnoop/wrap_generated_lt_1.8.go ================================================ // +build !go1.8 // Code generated by "httpsnoop/codegen"; DO NOT EDIT. package httpsnoop import ( "bufio" "io" "net" "net/http" ) // HeaderFunc is part of the http.ResponseWriter interface. type HeaderFunc func() http.Header // WriteHeaderFunc is part of the http.ResponseWriter interface. type WriteHeaderFunc func(code int) // WriteFunc is part of the http.ResponseWriter interface. type WriteFunc func(b []byte) (int, error) // FlushFunc is part of the http.Flusher interface. type FlushFunc func() // CloseNotifyFunc is part of the http.CloseNotifier interface. type CloseNotifyFunc func() <-chan bool // HijackFunc is part of the http.Hijacker interface. type HijackFunc func() (net.Conn, *bufio.ReadWriter, error) // ReadFromFunc is part of the io.ReaderFrom interface. type ReadFromFunc func(src io.Reader) (int64, error) // Hooks defines a set of method interceptors for methods included in // http.ResponseWriter as well as some others. You can think of them as // middleware for the function calls they target. See Wrap for more details. type Hooks struct { Header func(HeaderFunc) HeaderFunc WriteHeader func(WriteHeaderFunc) WriteHeaderFunc Write func(WriteFunc) WriteFunc Flush func(FlushFunc) FlushFunc CloseNotify func(CloseNotifyFunc) CloseNotifyFunc Hijack func(HijackFunc) HijackFunc ReadFrom func(ReadFromFunc) ReadFromFunc } // Wrap returns a wrapped version of w that provides the exact same interface // as w. Specifically if w implements any combination of: // // - http.Flusher // - http.CloseNotifier // - http.Hijacker // - io.ReaderFrom // // The wrapped version will implement the exact same combination. If no hooks // are set, the wrapped version also behaves exactly as w. Hooks targeting // methods not supported by w are ignored. Any other hooks will intercept the // method they target and may modify the call's arguments and/or return values. // The CaptureMetrics implementation serves as a working example for how the // hooks can be used. func Wrap(w http.ResponseWriter, hooks Hooks) http.ResponseWriter { rw := &rw{w: w, h: hooks} _, i0 := w.(http.Flusher) _, i1 := w.(http.CloseNotifier) _, i2 := w.(http.Hijacker) _, i3 := w.(io.ReaderFrom) switch { // combination 1/16 case !i0 && !i1 && !i2 && !i3: return struct { Unwrapper http.ResponseWriter }{rw, rw} // combination 2/16 case !i0 && !i1 && !i2 && i3: return struct { Unwrapper http.ResponseWriter io.ReaderFrom }{rw, rw, rw} // combination 3/16 case !i0 && !i1 && i2 && !i3: return struct { Unwrapper http.ResponseWriter http.Hijacker }{rw, rw, rw} // combination 4/16 case !i0 && !i1 && i2 && i3: return struct { Unwrapper http.ResponseWriter http.Hijacker io.ReaderFrom }{rw, rw, rw, rw} // combination 5/16 case !i0 && i1 && !i2 && !i3: return struct { Unwrapper http.ResponseWriter http.CloseNotifier }{rw, rw, rw} // combination 6/16 case !i0 && i1 && !i2 && i3: return struct { Unwrapper http.ResponseWriter http.CloseNotifier io.ReaderFrom }{rw, rw, rw, rw} // combination 7/16 case !i0 && i1 && i2 && !i3: return struct { Unwrapper http.ResponseWriter http.CloseNotifier http.Hijacker }{rw, rw, rw, rw} // combination 8/16 case !i0 && i1 && i2 && i3: return struct { Unwrapper http.ResponseWriter http.CloseNotifier http.Hijacker io.ReaderFrom }{rw, rw, rw, rw, rw} // combination 9/16 case i0 && !i1 && !i2 && !i3: return struct { Unwrapper http.ResponseWriter http.Flusher }{rw, rw, rw} // combination 10/16 case i0 && !i1 && !i2 && i3: return struct { Unwrapper http.ResponseWriter http.Flusher io.ReaderFrom }{rw, rw, rw, rw} // combination 11/16 case i0 && !i1 && i2 && !i3: return struct { Unwrapper http.ResponseWriter http.Flusher http.Hijacker }{rw, rw, rw, rw} // combination 12/16 case i0 && !i1 && i2 && i3: return struct { Unwrapper http.ResponseWriter http.Flusher http.Hijacker io.ReaderFrom }{rw, rw, rw, rw, rw} // combination 13/16 case i0 && i1 && !i2 && !i3: return struct { Unwrapper http.ResponseWriter http.Flusher http.CloseNotifier }{rw, rw, rw, rw} // combination 14/16 case i0 && i1 && !i2 && i3: return struct { Unwrapper http.ResponseWriter http.Flusher http.CloseNotifier io.ReaderFrom }{rw, rw, rw, rw, rw} // combination 15/16 case i0 && i1 && i2 && !i3: return struct { Unwrapper http.ResponseWriter http.Flusher http.CloseNotifier http.Hijacker }{rw, rw, rw, rw, rw} // combination 16/16 case i0 && i1 && i2 && i3: return struct { Unwrapper http.ResponseWriter http.Flusher http.CloseNotifier http.Hijacker io.ReaderFrom }{rw, rw, rw, rw, rw, rw} } panic("unreachable") } type rw struct { w http.ResponseWriter h Hooks } func (w *rw) Unwrap() http.ResponseWriter { return w.w } func (w *rw) Header() http.Header { f := w.w.(http.ResponseWriter).Header if w.h.Header != nil { f = w.h.Header(f) } return f() } func (w *rw) WriteHeader(code int) { f := w.w.(http.ResponseWriter).WriteHeader if w.h.WriteHeader != nil { f = w.h.WriteHeader(f) } f(code) } func (w *rw) Write(b []byte) (int, error) { f := w.w.(http.ResponseWriter).Write if w.h.Write != nil { f = w.h.Write(f) } return f(b) } func (w *rw) Flush() { f := w.w.(http.Flusher).Flush if w.h.Flush != nil { f = w.h.Flush(f) } f() } func (w *rw) CloseNotify() <-chan bool { f := w.w.(http.CloseNotifier).CloseNotify if w.h.CloseNotify != nil { f = w.h.CloseNotify(f) } return f() } func (w *rw) Hijack() (net.Conn, *bufio.ReadWriter, error) { f := w.w.(http.Hijacker).Hijack if w.h.Hijack != nil { f = w.h.Hijack(f) } return f() } func (w *rw) ReadFrom(src io.Reader) (int64, error) { f := w.w.(io.ReaderFrom).ReadFrom if w.h.ReadFrom != nil { f = w.h.ReadFrom(f) } return f(src) } type Unwrapper interface { Unwrap() http.ResponseWriter } // Unwrap returns the underlying http.ResponseWriter from within zero or more // layers of httpsnoop wrappers. func Unwrap(w http.ResponseWriter) http.ResponseWriter { if rw, ok := w.(Unwrapper); ok { // recurse until rw.Unwrap() returns a non-Unwrapper return Unwrap(rw.Unwrap()) } else { return w } } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/.cirrus.yml ================================================ freebsd_task: name: 'FreeBSD' freebsd_instance: image_family: freebsd-14-2 install_script: - pkg update -f - pkg install -y go test_script: # run tests as user "cirrus" instead of root - pw useradd cirrus -m - chown -R cirrus:cirrus . - FSNOTIFY_BUFFER=4096 sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race ./... - sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race ./... - FSNOTIFY_DEBUG=1 sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race -v ./... ================================================ FILE: vendor/github.com/fsnotify/fsnotify/.gitignore ================================================ # go test -c output *.test *.test.exe # Output of go build ./cmd/fsnotify /fsnotify /fsnotify.exe /test/kqueue /test/a.out ================================================ FILE: vendor/github.com/fsnotify/fsnotify/.mailmap ================================================ Chris Howey Nathan Youngman <4566+nathany@users.noreply.github.com> ================================================ FILE: vendor/github.com/fsnotify/fsnotify/CHANGELOG.md ================================================ # Changelog 1.9.0 2024-04-04 ---------------- ### Changes and fixes - all: make BufferedWatcher buffered again ([#657]) - inotify: fix race when adding/removing watches while a watched path is being deleted ([#678], [#686]) - inotify: don't send empty event if a watched path is unmounted ([#655]) - inotify: don't register duplicate watches when watching both a symlink and its target; previously that would get "half-added" and removing the second would panic ([#679]) - kqueue: fix watching relative symlinks ([#681]) - kqueue: correctly mark pre-existing entries when watching a link to a dir on kqueue ([#682]) - illumos: don't send error if changed file is deleted while processing the event ([#678]) [#657]: https://github.com/fsnotify/fsnotify/pull/657 [#678]: https://github.com/fsnotify/fsnotify/pull/678 [#686]: https://github.com/fsnotify/fsnotify/pull/686 [#655]: https://github.com/fsnotify/fsnotify/pull/655 [#681]: https://github.com/fsnotify/fsnotify/pull/681 [#679]: https://github.com/fsnotify/fsnotify/pull/679 [#682]: https://github.com/fsnotify/fsnotify/pull/682 1.8.0 2024-10-31 ---------------- ### Additions - all: add `FSNOTIFY_DEBUG` to print debug logs to stderr ([#619]) ### Changes and fixes - windows: fix behaviour of `WatchList()` to be consistent with other platforms ([#610]) - kqueue: ignore events with Ident=0 ([#590]) - kqueue: set O_CLOEXEC to prevent passing file descriptors to children ([#617]) - kqueue: emit events as "/path/dir/file" instead of "path/link/file" when watching a symlink ([#625]) - inotify: don't send event for IN_DELETE_SELF when also watching the parent ([#620]) - inotify: fix panic when calling Remove() in a goroutine ([#650]) - fen: allow watching subdirectories of watched directories ([#621]) [#590]: https://github.com/fsnotify/fsnotify/pull/590 [#610]: https://github.com/fsnotify/fsnotify/pull/610 [#617]: https://github.com/fsnotify/fsnotify/pull/617 [#619]: https://github.com/fsnotify/fsnotify/pull/619 [#620]: https://github.com/fsnotify/fsnotify/pull/620 [#621]: https://github.com/fsnotify/fsnotify/pull/621 [#625]: https://github.com/fsnotify/fsnotify/pull/625 [#650]: https://github.com/fsnotify/fsnotify/pull/650 1.7.0 - 2023-10-22 ------------------ This version of fsnotify needs Go 1.17. ### Additions - illumos: add FEN backend to support illumos and Solaris. ([#371]) - all: add `NewBufferedWatcher()` to use a buffered channel, which can be useful in cases where you can't control the kernel buffer and receive a large number of events in bursts. ([#550], [#572]) - all: add `AddWith()`, which is identical to `Add()` but allows passing options. ([#521]) - windows: allow setting the ReadDirectoryChangesW() buffer size with `fsnotify.WithBufferSize()`; the default of 64K is the highest value that works on all platforms and is enough for most purposes, but in some cases a highest buffer is needed. ([#521]) ### Changes and fixes - inotify: remove watcher if a watched path is renamed ([#518]) After a rename the reported name wasn't updated, or even an empty string. Inotify doesn't provide any good facilities to update it, so just remove the watcher. This is already how it worked on kqueue and FEN. On Windows this does work, and remains working. - windows: don't listen for file attribute changes ([#520]) File attribute changes are sent as `FILE_ACTION_MODIFIED` by the Windows API, with no way to see if they're a file write or attribute change, so would show up as a fsnotify.Write event. This is never useful, and could result in many spurious Write events. - windows: return `ErrEventOverflow` if the buffer is full ([#525]) Before it would merely return "short read", making it hard to detect this error. - kqueue: make sure events for all files are delivered properly when removing a watched directory ([#526]) Previously they would get sent with `""` (empty string) or `"."` as the path name. - kqueue: don't emit spurious Create events for symbolic links ([#524]) The link would get resolved but kqueue would "forget" it already saw the link itself, resulting on a Create for every Write event for the directory. - all: return `ErrClosed` on `Add()` when the watcher is closed ([#516]) - other: add `Watcher.Errors` and `Watcher.Events` to the no-op `Watcher` in `backend_other.go`, making it easier to use on unsupported platforms such as WASM, AIX, etc. ([#528]) - other: use the `backend_other.go` no-op if the `appengine` build tag is set; Google AppEngine forbids usage of the unsafe package so the inotify backend won't compile there. [#371]: https://github.com/fsnotify/fsnotify/pull/371 [#516]: https://github.com/fsnotify/fsnotify/pull/516 [#518]: https://github.com/fsnotify/fsnotify/pull/518 [#520]: https://github.com/fsnotify/fsnotify/pull/520 [#521]: https://github.com/fsnotify/fsnotify/pull/521 [#524]: https://github.com/fsnotify/fsnotify/pull/524 [#525]: https://github.com/fsnotify/fsnotify/pull/525 [#526]: https://github.com/fsnotify/fsnotify/pull/526 [#528]: https://github.com/fsnotify/fsnotify/pull/528 [#537]: https://github.com/fsnotify/fsnotify/pull/537 [#550]: https://github.com/fsnotify/fsnotify/pull/550 [#572]: https://github.com/fsnotify/fsnotify/pull/572 1.6.0 - 2022-10-13 ------------------ This version of fsnotify needs Go 1.16 (this was already the case since 1.5.1, but not documented). It also increases the minimum Linux version to 2.6.32. ### Additions - all: add `Event.Has()` and `Op.Has()` ([#477]) This makes checking events a lot easier; for example: if event.Op&Write == Write && !(event.Op&Remove == Remove) { } Becomes: if event.Has(Write) && !event.Has(Remove) { } - all: add cmd/fsnotify ([#463]) A command-line utility for testing and some examples. ### Changes and fixes - inotify: don't ignore events for files that don't exist ([#260], [#470]) Previously the inotify watcher would call `os.Lstat()` to check if a file still exists before emitting events. This was inconsistent with other platforms and resulted in inconsistent event reporting (e.g. when a file is quickly removed and re-created), and generally a source of confusion. It was added in 2013 to fix a memory leak that no longer exists. - all: return `ErrNonExistentWatch` when `Remove()` is called on a path that's not watched ([#460]) - inotify: replace epoll() with non-blocking inotify ([#434]) Non-blocking inotify was not generally available at the time this library was written in 2014, but now it is. As a result, the minimum Linux version is bumped from 2.6.27 to 2.6.32. This hugely simplifies the code and is faster. - kqueue: don't check for events every 100ms ([#480]) The watcher would wake up every 100ms, even when there was nothing to do. Now it waits until there is something to do. - macos: retry opening files on EINTR ([#475]) - kqueue: skip unreadable files ([#479]) kqueue requires a file descriptor for every file in a directory; this would fail if a file was unreadable by the current user. Now these files are simply skipped. - windows: fix renaming a watched directory if the parent is also watched ([#370]) - windows: increase buffer size from 4K to 64K ([#485]) - windows: close file handle on Remove() ([#288]) - kqueue: put pathname in the error if watching a file fails ([#471]) - inotify, windows: calling Close() more than once could race ([#465]) - kqueue: improve Close() performance ([#233]) - all: various documentation additions and clarifications. [#233]: https://github.com/fsnotify/fsnotify/pull/233 [#260]: https://github.com/fsnotify/fsnotify/pull/260 [#288]: https://github.com/fsnotify/fsnotify/pull/288 [#370]: https://github.com/fsnotify/fsnotify/pull/370 [#434]: https://github.com/fsnotify/fsnotify/pull/434 [#460]: https://github.com/fsnotify/fsnotify/pull/460 [#463]: https://github.com/fsnotify/fsnotify/pull/463 [#465]: https://github.com/fsnotify/fsnotify/pull/465 [#470]: https://github.com/fsnotify/fsnotify/pull/470 [#471]: https://github.com/fsnotify/fsnotify/pull/471 [#475]: https://github.com/fsnotify/fsnotify/pull/475 [#477]: https://github.com/fsnotify/fsnotify/pull/477 [#479]: https://github.com/fsnotify/fsnotify/pull/479 [#480]: https://github.com/fsnotify/fsnotify/pull/480 [#485]: https://github.com/fsnotify/fsnotify/pull/485 ## [1.5.4] - 2022-04-25 * Windows: add missing defer to `Watcher.WatchList` [#447](https://github.com/fsnotify/fsnotify/pull/447) * go.mod: use latest x/sys [#444](https://github.com/fsnotify/fsnotify/pull/444) * Fix compilation for OpenBSD [#443](https://github.com/fsnotify/fsnotify/pull/443) ## [1.5.3] - 2022-04-22 * This version is retracted. An incorrect branch is published accidentally [#445](https://github.com/fsnotify/fsnotify/issues/445) ## [1.5.2] - 2022-04-21 * Add a feature to return the directories and files that are being monitored [#374](https://github.com/fsnotify/fsnotify/pull/374) * Fix potential crash on windows if `raw.FileNameLength` exceeds `syscall.MAX_PATH` [#361](https://github.com/fsnotify/fsnotify/pull/361) * Allow build on unsupported GOOS [#424](https://github.com/fsnotify/fsnotify/pull/424) * Don't set `poller.fd` twice in `newFdPoller` [#406](https://github.com/fsnotify/fsnotify/pull/406) * fix go vet warnings: call to `(*T).Fatalf` from a non-test goroutine [#416](https://github.com/fsnotify/fsnotify/pull/416) ## [1.5.1] - 2021-08-24 * Revert Add AddRaw to not follow symlinks [#394](https://github.com/fsnotify/fsnotify/pull/394) ## [1.5.0] - 2021-08-20 * Go: Increase minimum required version to Go 1.12 [#381](https://github.com/fsnotify/fsnotify/pull/381) * Feature: Add AddRaw method which does not follow symlinks when adding a watch [#289](https://github.com/fsnotify/fsnotify/pull/298) * Windows: Follow symlinks by default like on all other systems [#289](https://github.com/fsnotify/fsnotify/pull/289) * CI: Use GitHub Actions for CI and cover go 1.12-1.17 [#378](https://github.com/fsnotify/fsnotify/pull/378) [#381](https://github.com/fsnotify/fsnotify/pull/381) [#385](https://github.com/fsnotify/fsnotify/pull/385) * Go 1.14+: Fix unsafe pointer conversion [#325](https://github.com/fsnotify/fsnotify/pull/325) ## [1.4.9] - 2020-03-11 * Move example usage to the readme #329. This may resolve #328. ## [1.4.8] - 2020-03-10 * CI: test more go versions (@nathany 1d13583d846ea9d66dcabbfefbfb9d8e6fb05216) * Tests: Queued inotify events could have been read by the test before max_queued_events was hit (@matthias-stone #265) * Tests: t.Fatalf -> t.Errorf in go routines (@gdey #266) * CI: Less verbosity (@nathany #267) * Tests: Darwin: Exchangedata is deprecated on 10.13 (@nathany #267) * Tests: Check if channels are closed in the example (@alexeykazakov #244) * CI: Only run golint on latest version of go and fix issues (@cpuguy83 #284) * CI: Add windows to travis matrix (@cpuguy83 #284) * Docs: Remover appveyor badge (@nathany 11844c0959f6fff69ba325d097fce35bd85a8e93) * Linux: create epoll and pipe fds with close-on-exec (@JohannesEbke #219) * Linux: open files with close-on-exec (@linxiulei #273) * Docs: Plan to support fanotify (@nathany ab058b44498e8b7566a799372a39d150d9ea0119 ) * Project: Add go.mod (@nathany #309) * Project: Revise editor config (@nathany #309) * Project: Update copyright for 2019 (@nathany #309) * CI: Drop go1.8 from CI matrix (@nathany #309) * Docs: Updating the FAQ section for supportability with NFS & FUSE filesystems (@Pratik32 4bf2d1fec78374803a39307bfb8d340688f4f28e ) ## [1.4.7] - 2018-01-09 * BSD/macOS: Fix possible deadlock on closing the watcher on kqueue (thanks @nhooyr and @glycerine) * Tests: Fix missing verb on format string (thanks @rchiossi) * Linux: Fix deadlock in Remove (thanks @aarondl) * Linux: Watch.Add improvements (avoid race, fix consistency, reduce garbage) (thanks @twpayne) * Docs: Moved FAQ into the README (thanks @vahe) * Linux: Properly handle inotify's IN_Q_OVERFLOW event (thanks @zeldovich) * Docs: replace references to OS X with macOS ## [1.4.2] - 2016-10-10 * Linux: use InotifyInit1 with IN_CLOEXEC to stop leaking a file descriptor to a child process when using fork/exec [#178](https://github.com/fsnotify/fsnotify/pull/178) (thanks @pattyshack) ## [1.4.1] - 2016-10-04 * Fix flaky inotify stress test on Linux [#177](https://github.com/fsnotify/fsnotify/pull/177) (thanks @pattyshack) ## [1.4.0] - 2016-10-01 * add a String() method to Event.Op [#165](https://github.com/fsnotify/fsnotify/pull/165) (thanks @oozie) ## [1.3.1] - 2016-06-28 * Windows: fix for double backslash when watching the root of a drive [#151](https://github.com/fsnotify/fsnotify/issues/151) (thanks @brunoqc) ## [1.3.0] - 2016-04-19 * Support linux/arm64 by [patching](https://go-review.googlesource.com/#/c/21971/) x/sys/unix and switching to to it from syscall (thanks @suihkulokki) [#135](https://github.com/fsnotify/fsnotify/pull/135) ## [1.2.10] - 2016-03-02 * Fix golint errors in windows.go [#121](https://github.com/fsnotify/fsnotify/pull/121) (thanks @tiffanyfj) ## [1.2.9] - 2016-01-13 kqueue: Fix logic for CREATE after REMOVE [#111](https://github.com/fsnotify/fsnotify/pull/111) (thanks @bep) ## [1.2.8] - 2015-12-17 * kqueue: fix race condition in Close [#105](https://github.com/fsnotify/fsnotify/pull/105) (thanks @djui for reporting the issue and @ppknap for writing a failing test) * inotify: fix race in test * enable race detection for continuous integration (Linux, Mac, Windows) ## [1.2.5] - 2015-10-17 * inotify: use epoll_create1 for arm64 support (requires Linux 2.6.27 or later) [#100](https://github.com/fsnotify/fsnotify/pull/100) (thanks @suihkulokki) * inotify: fix path leaks [#73](https://github.com/fsnotify/fsnotify/pull/73) (thanks @chamaken) * kqueue: watch for rename events on subdirectories [#83](https://github.com/fsnotify/fsnotify/pull/83) (thanks @guotie) * kqueue: avoid infinite loops from symlinks cycles [#101](https://github.com/fsnotify/fsnotify/pull/101) (thanks @illicitonion) ## [1.2.1] - 2015-10-14 * kqueue: don't watch named pipes [#98](https://github.com/fsnotify/fsnotify/pull/98) (thanks @evanphx) ## [1.2.0] - 2015-02-08 * inotify: use epoll to wake up readEvents [#66](https://github.com/fsnotify/fsnotify/pull/66) (thanks @PieterD) * inotify: closing watcher should now always shut down goroutine [#63](https://github.com/fsnotify/fsnotify/pull/63) (thanks @PieterD) * kqueue: close kqueue after removing watches, fixes [#59](https://github.com/fsnotify/fsnotify/issues/59) ## [1.1.1] - 2015-02-05 * inotify: Retry read on EINTR [#61](https://github.com/fsnotify/fsnotify/issues/61) (thanks @PieterD) ## [1.1.0] - 2014-12-12 * kqueue: rework internals [#43](https://github.com/fsnotify/fsnotify/pull/43) * add low-level functions * only need to store flags on directories * less mutexes [#13](https://github.com/fsnotify/fsnotify/issues/13) * done can be an unbuffered channel * remove calls to os.NewSyscallError * More efficient string concatenation for Event.String() [#52](https://github.com/fsnotify/fsnotify/pull/52) (thanks @mdlayher) * kqueue: fix regression in rework causing subdirectories to be watched [#48](https://github.com/fsnotify/fsnotify/issues/48) * kqueue: cleanup internal watch before sending remove event [#51](https://github.com/fsnotify/fsnotify/issues/51) ## [1.0.4] - 2014-09-07 * kqueue: add dragonfly to the build tags. * Rename source code files, rearrange code so exported APIs are at the top. * Add done channel to example code. [#37](https://github.com/fsnotify/fsnotify/pull/37) (thanks @chenyukang) ## [1.0.3] - 2014-08-19 * [Fix] Windows MOVED_TO now translates to Create like on BSD and Linux. [#36](https://github.com/fsnotify/fsnotify/issues/36) ## [1.0.2] - 2014-08-17 * [Fix] Missing create events on macOS. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso) * [Fix] Make ./path and path equivalent. (thanks @zhsso) ## [1.0.0] - 2014-08-15 * [API] Remove AddWatch on Windows, use Add. * Improve documentation for exported identifiers. [#30](https://github.com/fsnotify/fsnotify/issues/30) * Minor updates based on feedback from golint. ## dev / 2014-07-09 * Moved to [github.com/fsnotify/fsnotify](https://github.com/fsnotify/fsnotify). * Use os.NewSyscallError instead of returning errno (thanks @hariharan-uno) ## dev / 2014-07-04 * kqueue: fix incorrect mutex used in Close() * Update example to demonstrate usage of Op. ## dev / 2014-06-28 * [API] Don't set the Write Op for attribute notifications [#4](https://github.com/fsnotify/fsnotify/issues/4) * Fix for String() method on Event (thanks Alex Brainman) * Don't build on Plan 9 or Solaris (thanks @4ad) ## dev / 2014-06-21 * Events channel of type Event rather than *Event. * [internal] use syscall constants directly for inotify and kqueue. * [internal] kqueue: rename events to kevents and fileEvent to event. ## dev / 2014-06-19 * Go 1.3+ required on Windows (uses syscall.ERROR_MORE_DATA internally). * [internal] remove cookie from Event struct (unused). * [internal] Event struct has the same definition across every OS. * [internal] remove internal watch and removeWatch methods. ## dev / 2014-06-12 * [API] Renamed Watch() to Add() and RemoveWatch() to Remove(). * [API] Pluralized channel names: Events and Errors. * [API] Renamed FileEvent struct to Event. * [API] Op constants replace methods like IsCreate(). ## dev / 2014-06-12 * Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98) ## dev / 2014-05-23 * [API] Remove current implementation of WatchFlags. * current implementation doesn't take advantage of OS for efficiency * provides little benefit over filtering events as they are received, but has extra bookkeeping and mutexes * no tests for the current implementation * not fully implemented on Windows [#93](https://github.com/howeyc/fsnotify/issues/93#issuecomment-39285195) ## [0.9.3] - 2014-12-31 * kqueue: cleanup internal watch before sending remove event [#51](https://github.com/fsnotify/fsnotify/issues/51) ## [0.9.2] - 2014-08-17 * [Backport] Fix missing create events on macOS. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso) ## [0.9.1] - 2014-06-12 * Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98) ## [0.9.0] - 2014-01-17 * IsAttrib() for events that only concern a file's metadata [#79][] (thanks @abustany) * [Fix] kqueue: fix deadlock [#77][] (thanks @cespare) * [NOTICE] Development has moved to `code.google.com/p/go.exp/fsnotify` in preparation for inclusion in the Go standard library. ## [0.8.12] - 2013-11-13 * [API] Remove FD_SET and friends from Linux adapter ## [0.8.11] - 2013-11-02 * [Doc] Add Changelog [#72][] (thanks @nathany) * [Doc] Spotlight and double modify events on macOS [#62][] (reported by @paulhammond) ## [0.8.10] - 2013-10-19 * [Fix] kqueue: remove file watches when parent directory is removed [#71][] (reported by @mdwhatcott) * [Fix] kqueue: race between Close and readEvents [#70][] (reported by @bernerdschaefer) * [Doc] specify OS-specific limits in README (thanks @debrando) ## [0.8.9] - 2013-09-08 * [Doc] Contributing (thanks @nathany) * [Doc] update package path in example code [#63][] (thanks @paulhammond) * [Doc] GoCI badge in README (Linux only) [#60][] * [Doc] Cross-platform testing with Vagrant [#59][] (thanks @nathany) ## [0.8.8] - 2013-06-17 * [Fix] Windows: handle `ERROR_MORE_DATA` on Windows [#49][] (thanks @jbowtie) ## [0.8.7] - 2013-06-03 * [API] Make syscall flags internal * [Fix] inotify: ignore event changes * [Fix] race in symlink test [#45][] (reported by @srid) * [Fix] tests on Windows * lower case error messages ## [0.8.6] - 2013-05-23 * kqueue: Use EVT_ONLY flag on Darwin * [Doc] Update README with full example ## [0.8.5] - 2013-05-09 * [Fix] inotify: allow monitoring of "broken" symlinks (thanks @tsg) ## [0.8.4] - 2013-04-07 * [Fix] kqueue: watch all file events [#40][] (thanks @ChrisBuchholz) ## [0.8.3] - 2013-03-13 * [Fix] inoitfy/kqueue memory leak [#36][] (reported by @nbkolchin) * [Fix] kqueue: use fsnFlags for watching a directory [#33][] (reported by @nbkolchin) ## [0.8.2] - 2013-02-07 * [Doc] add Authors * [Fix] fix data races for map access [#29][] (thanks @fsouza) ## [0.8.1] - 2013-01-09 * [Fix] Windows path separators * [Doc] BSD License ## [0.8.0] - 2012-11-09 * kqueue: directory watching improvements (thanks @vmirage) * inotify: add `IN_MOVED_TO` [#25][] (requested by @cpisto) * [Fix] kqueue: deleting watched directory [#24][] (reported by @jakerr) ## [0.7.4] - 2012-10-09 * [Fix] inotify: fixes from https://codereview.appspot.com/5418045/ (ugorji) * [Fix] kqueue: preserve watch flags when watching for delete [#21][] (reported by @robfig) * [Fix] kqueue: watch the directory even if it isn't a new watch (thanks @robfig) * [Fix] kqueue: modify after recreation of file ## [0.7.3] - 2012-09-27 * [Fix] kqueue: watch with an existing folder inside the watched folder (thanks @vmirage) * [Fix] kqueue: no longer get duplicate CREATE events ## [0.7.2] - 2012-09-01 * kqueue: events for created directories ## [0.7.1] - 2012-07-14 * [Fix] for renaming files ## [0.7.0] - 2012-07-02 * [Feature] FSNotify flags * [Fix] inotify: Added file name back to event path ## [0.6.0] - 2012-06-06 * kqueue: watch files after directory created (thanks @tmc) ## [0.5.1] - 2012-05-22 * [Fix] inotify: remove all watches before Close() ## [0.5.0] - 2012-05-03 * [API] kqueue: return errors during watch instead of sending over channel * kqueue: match symlink behavior on Linux * inotify: add `DELETE_SELF` (requested by @taralx) * [Fix] kqueue: handle EINTR (reported by @robfig) * [Doc] Godoc example [#1][] (thanks @davecheney) ## [0.4.0] - 2012-03-30 * Go 1 released: build with go tool * [Feature] Windows support using winfsnotify * Windows does not have attribute change notifications * Roll attribute notifications into IsModify ## [0.3.0] - 2012-02-19 * kqueue: add files when watch directory ## [0.2.0] - 2011-12-30 * update to latest Go weekly code ## [0.1.0] - 2011-10-19 * kqueue: add watch on file creation to match inotify * kqueue: create file event * inotify: ignore `IN_IGNORED` events * event String() * linux: common FileEvent functions * initial commit [#79]: https://github.com/howeyc/fsnotify/pull/79 [#77]: https://github.com/howeyc/fsnotify/pull/77 [#72]: https://github.com/howeyc/fsnotify/issues/72 [#71]: https://github.com/howeyc/fsnotify/issues/71 [#70]: https://github.com/howeyc/fsnotify/issues/70 [#63]: https://github.com/howeyc/fsnotify/issues/63 [#62]: https://github.com/howeyc/fsnotify/issues/62 [#60]: https://github.com/howeyc/fsnotify/issues/60 [#59]: https://github.com/howeyc/fsnotify/issues/59 [#49]: https://github.com/howeyc/fsnotify/issues/49 [#45]: https://github.com/howeyc/fsnotify/issues/45 [#40]: https://github.com/howeyc/fsnotify/issues/40 [#36]: https://github.com/howeyc/fsnotify/issues/36 [#33]: https://github.com/howeyc/fsnotify/issues/33 [#29]: https://github.com/howeyc/fsnotify/issues/29 [#25]: https://github.com/howeyc/fsnotify/issues/25 [#24]: https://github.com/howeyc/fsnotify/issues/24 [#21]: https://github.com/howeyc/fsnotify/issues/21 ================================================ FILE: vendor/github.com/fsnotify/fsnotify/CONTRIBUTING.md ================================================ Thank you for your interest in contributing to fsnotify! We try to review and merge PRs in a reasonable timeframe, but please be aware that: - To avoid "wasted" work, please discuss changes on the issue tracker first. You can just send PRs, but they may end up being rejected for one reason or the other. - fsnotify is a cross-platform library, and changes must work reasonably well on all supported platforms. - Changes will need to be compatible; old code should still compile, and the runtime behaviour can't change in ways that are likely to lead to problems for users. Testing ------- Just `go test ./...` runs all the tests; the CI runs this on all supported platforms. Testing different platforms locally can be done with something like [goon] or [Vagrant], but this isn't super-easy to set up at the moment. Use the `-short` flag to make the "stress test" run faster. Writing new tests ----------------- Scripts in the testdata directory allow creating test cases in a "shell-like" syntax. The basic format is: script Output: desired output For example: # Create a new empty file with some data. watch / echo data >/file Output: create /file write /file Just create a new file to add a new test; select which tests to run with `-run TestScript/[path]`. script ------ The script is a "shell-like" script: cmd arg arg Comments are supported with `#`: # Comment cmd arg arg # Comment All operations are done in a temp directory; a path like "/foo" is rewritten to "/tmp/TestFoo/foo". Arguments can be quoted with `"` or `'`; there are no escapes and they're functionally identical right now, but this may change in the future, so best to assume shell-like rules. touch "/file with spaces" End-of-line escapes with `\` are not supported. ### Supported commands watch path [ops] # Watch the path, reporting events for it. Nothing is # watched by default. Optionally a list of ops can be # given, as with AddWith(path, WithOps(...)). unwatch path # Stop watching the path. watchlist n # Assert watchlist length. stop # Stop running the script; for debugging. debug [yes/no] # Enable/disable FSNOTIFY_DEBUG (tests are run in parallel by default, so -parallel=1 is probably a good idea). print [any strings] # Print text to stdout; for debugging. touch path mkdir [-p] dir ln -s target link # Only ln -s supported. mkfifo path mknod dev path mv src dst rm [-r] path chmod mode path # Octal only sleep time-in-ms cat path # Read path (does nothing with the data; just reads it). echo str >>path # Append "str" to "path". echo str >path # Truncate "path" and write "str". require reason # Skip the test if "reason" is true; "skip" and skip reason # "require" behave identical; it supports both for # readability. Possible reasons are: # # always Always skip this test. # symlink Symlinks are supported (requires admin # permissions on Windows). # mkfifo Platform doesn't support FIFO named sockets. # mknod Platform doesn't support device nodes. output ------ After `Output:` the desired output is given; this is indented by convention, but that's not required. The format of that is: # Comment event path # Comment system: event path system2: event path Every event is one line, and any whitespace between the event and path are ignored. The path can optionally be surrounded in ". Anything after a "#" is ignored. Platform-specific tests can be added after GOOS; for example: watch / touch /file Output: # Tested if nothing else matches create /file # Windows-specific test. windows: write /file You can specify multiple platforms with a comma (e.g. "windows, linux:"). "kqueue" is a shortcut for all kqueue systems (BSD, macOS). [goon]: https://github.com/arp242/goon [Vagrant]: https://www.vagrantup.com/ [integration_test.go]: /integration_test.go ================================================ FILE: vendor/github.com/fsnotify/fsnotify/LICENSE ================================================ Copyright © 2012 The Go Authors. All rights reserved. Copyright © fsnotify Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: vendor/github.com/fsnotify/fsnotify/README.md ================================================ fsnotify is a Go library to provide cross-platform filesystem notifications on Windows, Linux, macOS, BSD, and illumos. Go 1.17 or newer is required; the full documentation is at https://pkg.go.dev/github.com/fsnotify/fsnotify --- Platform support: | Backend | OS | Status | | :-------------------- | :--------- | :------------------------------------------------------------------------ | | inotify | Linux | Supported | | kqueue | BSD, macOS | Supported | | ReadDirectoryChangesW | Windows | Supported | | FEN | illumos | Supported | | fanotify | Linux 5.9+ | [Not yet](https://github.com/fsnotify/fsnotify/issues/114) | | FSEvents | macOS | [Needs support in x/sys/unix][fsevents] | | USN Journals | Windows | [Needs support in x/sys/windows][usn] | | Polling | *All* | [Not yet](https://github.com/fsnotify/fsnotify/issues/9) | Linux and illumos should include Android and Solaris, but these are currently untested. [fsevents]: https://github.com/fsnotify/fsnotify/issues/11#issuecomment-1279133120 [usn]: https://github.com/fsnotify/fsnotify/issues/53#issuecomment-1279829847 Usage ----- A basic example: ```go package main import ( "log" "github.com/fsnotify/fsnotify" ) func main() { // Create new watcher. watcher, err := fsnotify.NewWatcher() if err != nil { log.Fatal(err) } defer watcher.Close() // Start listening for events. go func() { for { select { case event, ok := <-watcher.Events: if !ok { return } log.Println("event:", event) if event.Has(fsnotify.Write) { log.Println("modified file:", event.Name) } case err, ok := <-watcher.Errors: if !ok { return } log.Println("error:", err) } } }() // Add a path. err = watcher.Add("/tmp") if err != nil { log.Fatal(err) } // Block main goroutine forever. <-make(chan struct{}) } ``` Some more examples can be found in [cmd/fsnotify](cmd/fsnotify), which can be run with: % go run ./cmd/fsnotify Further detailed documentation can be found in godoc: https://pkg.go.dev/github.com/fsnotify/fsnotify FAQ --- ### Will a file still be watched when it's moved to another directory? No, not unless you are watching the location it was moved to. ### Are subdirectories watched? No, you must add watches for any directory you want to watch (a recursive watcher is on the roadmap: [#18]). [#18]: https://github.com/fsnotify/fsnotify/issues/18 ### Do I have to watch the Error and Event channels in a goroutine? Yes. You can read both channels in the same goroutine using `select` (you don't need a separate goroutine for both channels; see the example). ### Why don't notifications work with NFS, SMB, FUSE, /proc, or /sys? fsnotify requires support from underlying OS to work. The current NFS and SMB protocols does not provide network level support for file notifications, and neither do the /proc and /sys virtual filesystems. This could be fixed with a polling watcher ([#9]), but it's not yet implemented. [#9]: https://github.com/fsnotify/fsnotify/issues/9 ### Why do I get many Chmod events? Some programs may generate a lot of attribute changes; for example Spotlight on macOS, anti-virus programs, backup applications, and some others are known to do this. As a rule, it's typically best to ignore Chmod events. They're often not useful, and tend to cause problems. Spotlight indexing on macOS can result in multiple events (see [#15]). A temporary workaround is to add your folder(s) to the *Spotlight Privacy settings* until we have a native FSEvents implementation (see [#11]). [#11]: https://github.com/fsnotify/fsnotify/issues/11 [#15]: https://github.com/fsnotify/fsnotify/issues/15 ### Watching a file doesn't work well Watching individual files (rather than directories) is generally not recommended as many programs (especially editors) update files atomically: it will write to a temporary file which is then moved to to destination, overwriting the original (or some variant thereof). The watcher on the original file is now lost, as that no longer exists. The upshot of this is that a power failure or crash won't leave a half-written file. Watch the parent directory and use `Event.Name` to filter out files you're not interested in. There is an example of this in `cmd/fsnotify/file.go`. Platform-specific notes ----------------------- ### Linux When a file is removed a REMOVE event won't be emitted until all file descriptors are closed; it will emit a CHMOD instead: fp := os.Open("file") os.Remove("file") // CHMOD fp.Close() // REMOVE This is the event that inotify sends, so not much can be changed about this. The `fs.inotify.max_user_watches` sysctl variable specifies the upper limit for the number of watches per user, and `fs.inotify.max_user_instances` specifies the maximum number of inotify instances per user. Every Watcher you create is an "instance", and every path you add is a "watch". These are also exposed in `/proc` as `/proc/sys/fs/inotify/max_user_watches` and `/proc/sys/fs/inotify/max_user_instances` To increase them you can use `sysctl` or write the value to proc file: # The default values on Linux 5.18 sysctl fs.inotify.max_user_watches=124983 sysctl fs.inotify.max_user_instances=128 To make the changes persist on reboot edit `/etc/sysctl.conf` or `/usr/lib/sysctl.d/50-default.conf` (details differ per Linux distro; check your distro's documentation): fs.inotify.max_user_watches=124983 fs.inotify.max_user_instances=128 Reaching the limit will result in a "no space left on device" or "too many open files" error. ### kqueue (macOS, all BSD systems) kqueue requires opening a file descriptor for every file that's being watched; so if you're watching a directory with five files then that's six file descriptors. You will run in to your system's "max open files" limit faster on these platforms. The sysctl variables `kern.maxfiles` and `kern.maxfilesperproc` can be used to control the maximum number of open files. ================================================ FILE: vendor/github.com/fsnotify/fsnotify/backend_fen.go ================================================ //go:build solaris // FEN backend for illumos (supported) and Solaris (untested, but should work). // // See port_create(3c) etc. for docs. https://www.illumos.org/man/3C/port_create package fsnotify import ( "errors" "fmt" "io/fs" "os" "path/filepath" "sync" "time" "github.com/fsnotify/fsnotify/internal" "golang.org/x/sys/unix" ) type fen struct { *shared Events chan Event Errors chan error mu sync.Mutex port *unix.EventPort dirs map[string]Op // Explicitly watched directories watches map[string]Op // Explicitly watched non-directories } var defaultBufferSize = 0 func newBackend(ev chan Event, errs chan error) (backend, error) { w := &fen{ shared: newShared(ev, errs), Events: ev, Errors: errs, dirs: make(map[string]Op), watches: make(map[string]Op), } var err error w.port, err = unix.NewEventPort() if err != nil { return nil, fmt.Errorf("fsnotify.NewWatcher: %w", err) } go w.readEvents() return w, nil } func (w *fen) Close() error { if w.shared.close() { return nil } return w.port.Close() } func (w *fen) Add(name string) error { return w.AddWith(name) } func (w *fen) AddWith(name string, opts ...addOpt) error { if w.isClosed() { return ErrClosed } if debug { fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s AddWith(%q)\n", time.Now().Format("15:04:05.000000000"), name) } with := getOptions(opts...) if !w.xSupports(with.op) { return fmt.Errorf("%w: %s", xErrUnsupported, with.op) } // Currently we resolve symlinks that were explicitly requested to be // watched. Otherwise we would use LStat here. stat, err := os.Stat(name) if err != nil { return err } // Associate all files in the directory. if stat.IsDir() { err := w.handleDirectory(name, stat, true, w.associateFile) if err != nil { return err } w.mu.Lock() w.dirs[name] = with.op w.mu.Unlock() return nil } err = w.associateFile(name, stat, true) if err != nil { return err } w.mu.Lock() w.watches[name] = with.op w.mu.Unlock() return nil } func (w *fen) Remove(name string) error { if w.isClosed() { return nil } if !w.port.PathIsWatched(name) { return fmt.Errorf("%w: %s", ErrNonExistentWatch, name) } if debug { fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s Remove(%q)\n", time.Now().Format("15:04:05.000000000"), name) } // The user has expressed an intent. Immediately remove this name from // whichever watch list it might be in. If it's not in there the delete // doesn't cause harm. w.mu.Lock() delete(w.watches, name) delete(w.dirs, name) w.mu.Unlock() stat, err := os.Stat(name) if err != nil { return err } // Remove associations for every file in the directory. if stat.IsDir() { err := w.handleDirectory(name, stat, false, w.dissociateFile) if err != nil { return err } return nil } err = w.port.DissociatePath(name) if err != nil { return err } return nil } // readEvents contains the main loop that runs in a goroutine watching for events. func (w *fen) readEvents() { // If this function returns, the watcher has been closed and we can close // these channels defer func() { close(w.Errors) close(w.Events) }() pevents := make([]unix.PortEvent, 8) for { count, err := w.port.Get(pevents, 1, nil) if err != nil && err != unix.ETIME { // Interrupted system call (count should be 0) ignore and continue if errors.Is(err, unix.EINTR) && count == 0 { continue } // Get failed because we called w.Close() if errors.Is(err, unix.EBADF) && w.isClosed() { return } // There was an error not caused by calling w.Close() if !w.sendError(fmt.Errorf("port.Get: %w", err)) { return } } p := pevents[:count] for _, pevent := range p { if pevent.Source != unix.PORT_SOURCE_FILE { // Event from unexpected source received; should never happen. if !w.sendError(errors.New("Event from unexpected source received")) { return } continue } if debug { internal.Debug(pevent.Path, pevent.Events) } err = w.handleEvent(&pevent) if !w.sendError(err) { return } } } } func (w *fen) handleDirectory(path string, stat os.FileInfo, follow bool, handler func(string, os.FileInfo, bool) error) error { files, err := os.ReadDir(path) if err != nil { return err } // Handle all children of the directory. for _, entry := range files { finfo, err := entry.Info() if err != nil { return err } err = handler(filepath.Join(path, finfo.Name()), finfo, false) if err != nil { return err } } // And finally handle the directory itself. return handler(path, stat, follow) } // handleEvent might need to emit more than one fsnotify event if the events // bitmap matches more than one event type (e.g. the file was both modified and // had the attributes changed between when the association was created and the // when event was returned) func (w *fen) handleEvent(event *unix.PortEvent) error { var ( events = event.Events path = event.Path fmode = event.Cookie.(os.FileMode) reRegister = true ) w.mu.Lock() _, watchedDir := w.dirs[path] _, watchedPath := w.watches[path] w.mu.Unlock() isWatched := watchedDir || watchedPath if events&unix.FILE_DELETE != 0 { if !w.sendEvent(Event{Name: path, Op: Remove}) { return nil } reRegister = false } if events&unix.FILE_RENAME_FROM != 0 { if !w.sendEvent(Event{Name: path, Op: Rename}) { return nil } // Don't keep watching the new file name reRegister = false } if events&unix.FILE_RENAME_TO != 0 { // We don't report a Rename event for this case, because Rename events // are interpreted as referring to the _old_ name of the file, and in // this case the event would refer to the new name of the file. This // type of rename event is not supported by fsnotify. // inotify reports a Remove event in this case, so we simulate this // here. if !w.sendEvent(Event{Name: path, Op: Remove}) { return nil } // Don't keep watching the file that was removed reRegister = false } // The file is gone, nothing left to do. if !reRegister { if watchedDir { w.mu.Lock() delete(w.dirs, path) w.mu.Unlock() } if watchedPath { w.mu.Lock() delete(w.watches, path) w.mu.Unlock() } return nil } // If we didn't get a deletion the file still exists and we're going to have // to watch it again. Let's Stat it now so that we can compare permissions // and have what we need to continue watching the file stat, err := os.Lstat(path) if err != nil { // This is unexpected, but we should still emit an event. This happens // most often on "rm -r" of a subdirectory inside a watched directory We // get a modify event of something happening inside, but by the time we // get here, the sudirectory is already gone. Clearly we were watching // this path but now it is gone. Let's tell the user that it was // removed. if !w.sendEvent(Event{Name: path, Op: Remove}) { return nil } // Suppress extra write events on removed directories; they are not // informative and can be confusing. return nil } // resolve symlinks that were explicitly watched as we would have at Add() // time. this helps suppress spurious Chmod events on watched symlinks if isWatched { stat, err = os.Stat(path) if err != nil { // The symlink still exists, but the target is gone. Report the // Remove similar to above. if !w.sendEvent(Event{Name: path, Op: Remove}) { return nil } // Don't return the error } } if events&unix.FILE_MODIFIED != 0 { if fmode.IsDir() && watchedDir { if err := w.updateDirectory(path); err != nil { return err } } else { if !w.sendEvent(Event{Name: path, Op: Write}) { return nil } } } if events&unix.FILE_ATTRIB != 0 && stat != nil { // Only send Chmod if perms changed if stat.Mode().Perm() != fmode.Perm() { if !w.sendEvent(Event{Name: path, Op: Chmod}) { return nil } } } if stat != nil { // If we get here, it means we've hit an event above that requires us to // continue watching the file or directory err := w.associateFile(path, stat, isWatched) if errors.Is(err, fs.ErrNotExist) { // Path may have been removed since the stat. err = nil } return err } return nil } // The directory was modified, so we must find unwatched entities and watch // them. If something was removed from the directory, nothing will happen, as // everything else should still be watched. func (w *fen) updateDirectory(path string) error { files, err := os.ReadDir(path) if err != nil { // Directory no longer exists: probably just deleted since we got the // event. if errors.Is(err, fs.ErrNotExist) { return nil } return err } for _, entry := range files { path := filepath.Join(path, entry.Name()) if w.port.PathIsWatched(path) { continue } finfo, err := entry.Info() if err != nil { return err } err = w.associateFile(path, finfo, false) if errors.Is(err, fs.ErrNotExist) { // File may have disappeared between getting the dir listing and // adding the port: that's okay to ignore. continue } if !w.sendError(err) { return nil } if !w.sendEvent(Event{Name: path, Op: Create}) { return nil } } return nil } func (w *fen) associateFile(path string, stat os.FileInfo, follow bool) error { if w.isClosed() { return ErrClosed } // This is primarily protecting the call to AssociatePath but it is // important and intentional that the call to PathIsWatched is also // protected by this mutex. Without this mutex, AssociatePath has been seen // to error out that the path is already associated. w.mu.Lock() defer w.mu.Unlock() if w.port.PathIsWatched(path) { // Remove the old association in favor of this one If we get ENOENT, // then while the x/sys/unix wrapper still thought that this path was // associated, the underlying event port did not. This call will have // cleared up that discrepancy. The most likely cause is that the event // has fired but we haven't processed it yet. err := w.port.DissociatePath(path) if err != nil && !errors.Is(err, unix.ENOENT) { return fmt.Errorf("port.DissociatePath(%q): %w", path, err) } } var events int if !follow { // Watch symlinks themselves rather than their targets unless this entry // is explicitly watched. events |= unix.FILE_NOFOLLOW } if true { // TODO: implement withOps() events |= unix.FILE_MODIFIED } if true { events |= unix.FILE_ATTRIB } err := w.port.AssociatePath(path, stat, events, stat.Mode()) if err != nil { return fmt.Errorf("port.AssociatePath(%q): %w", path, err) } return nil } func (w *fen) dissociateFile(path string, stat os.FileInfo, unused bool) error { if !w.port.PathIsWatched(path) { return nil } err := w.port.DissociatePath(path) if err != nil { return fmt.Errorf("port.DissociatePath(%q): %w", path, err) } return nil } func (w *fen) WatchList() []string { if w.isClosed() { return nil } w.mu.Lock() defer w.mu.Unlock() entries := make([]string, 0, len(w.watches)+len(w.dirs)) for pathname := range w.dirs { entries = append(entries, pathname) } for pathname := range w.watches { entries = append(entries, pathname) } return entries } func (w *fen) xSupports(op Op) bool { if op.Has(xUnportableOpen) || op.Has(xUnportableRead) || op.Has(xUnportableCloseWrite) || op.Has(xUnportableCloseRead) { return false } return true } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/backend_inotify.go ================================================ //go:build linux && !appengine package fsnotify import ( "errors" "fmt" "io" "io/fs" "os" "path/filepath" "strings" "sync" "time" "unsafe" "github.com/fsnotify/fsnotify/internal" "golang.org/x/sys/unix" ) type inotify struct { *shared Events chan Event Errors chan error // Store fd here as os.File.Read() will no longer return on close after // calling Fd(). See: https://github.com/golang/go/issues/26439 fd int inotifyFile *os.File watches *watches doneResp chan struct{} // Channel to respond to Close // Store rename cookies in an array, with the index wrapping to 0. Almost // all of the time what we get is a MOVED_FROM to set the cookie and the // next event inotify sends will be MOVED_TO to read it. However, this is // not guaranteed – as described in inotify(7) – and we may get other events // between the two MOVED_* events (including other MOVED_* ones). // // A second issue is that moving a file outside the watched directory will // trigger a MOVED_FROM to set the cookie, but we never see the MOVED_TO to // read and delete it. So just storing it in a map would slowly leak memory. // // Doing it like this gives us a simple fast LRU-cache that won't allocate. // Ten items should be more than enough for our purpose, and a loop over // such a short array is faster than a map access anyway (not that it hugely // matters since we're talking about hundreds of ns at the most, but still). cookies [10]koekje cookieIndex uint8 cookiesMu sync.Mutex } type ( watches struct { wd map[uint32]*watch // wd → watch path map[string]uint32 // pathname → wd } watch struct { wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall) flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags) path string // Watch path. recurse bool // Recursion with ./...? } koekje struct { cookie uint32 path string } ) func newWatches() *watches { return &watches{ wd: make(map[uint32]*watch), path: make(map[string]uint32), } } func (w *watches) byPath(path string) *watch { return w.wd[w.path[path]] } func (w *watches) byWd(wd uint32) *watch { return w.wd[wd] } func (w *watches) len() int { return len(w.wd) } func (w *watches) add(ww *watch) { w.wd[ww.wd] = ww; w.path[ww.path] = ww.wd } func (w *watches) remove(watch *watch) { delete(w.path, watch.path); delete(w.wd, watch.wd) } func (w *watches) removePath(path string) ([]uint32, error) { path, recurse := recursivePath(path) wd, ok := w.path[path] if !ok { return nil, fmt.Errorf("%w: %s", ErrNonExistentWatch, path) } watch := w.wd[wd] if recurse && !watch.recurse { return nil, fmt.Errorf("can't use /... with non-recursive watch %q", path) } delete(w.path, path) delete(w.wd, wd) if !watch.recurse { return []uint32{wd}, nil } wds := make([]uint32, 0, 8) wds = append(wds, wd) for p, rwd := range w.path { if strings.HasPrefix(p, path) { delete(w.path, p) delete(w.wd, rwd) wds = append(wds, rwd) } } return wds, nil } func (w *watches) updatePath(path string, f func(*watch) (*watch, error)) error { var existing *watch wd, ok := w.path[path] if ok { existing = w.wd[wd] } upd, err := f(existing) if err != nil { return err } if upd != nil { w.wd[upd.wd] = upd w.path[upd.path] = upd.wd if upd.wd != wd { delete(w.wd, wd) } } return nil } var defaultBufferSize = 0 func newBackend(ev chan Event, errs chan error) (backend, error) { // Need to set nonblocking mode for SetDeadline to work, otherwise blocking // I/O operations won't terminate on close. fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK) if fd == -1 { return nil, errno } w := &inotify{ shared: newShared(ev, errs), Events: ev, Errors: errs, fd: fd, inotifyFile: os.NewFile(uintptr(fd), ""), watches: newWatches(), doneResp: make(chan struct{}), } go w.readEvents() return w, nil } func (w *inotify) Close() error { if w.shared.close() { return nil } // Causes any blocking reads to return with an error, provided the file // still supports deadline operations. err := w.inotifyFile.Close() if err != nil { return err } <-w.doneResp // Wait for readEvents() to finish. return nil } func (w *inotify) Add(name string) error { return w.AddWith(name) } func (w *inotify) AddWith(path string, opts ...addOpt) error { if w.isClosed() { return ErrClosed } if debug { fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s AddWith(%q)\n", time.Now().Format("15:04:05.000000000"), path) } with := getOptions(opts...) if !w.xSupports(with.op) { return fmt.Errorf("%w: %s", xErrUnsupported, with.op) } add := func(path string, with withOpts, recurse bool) error { var flags uint32 if with.noFollow { flags |= unix.IN_DONT_FOLLOW } if with.op.Has(Create) { flags |= unix.IN_CREATE } if with.op.Has(Write) { flags |= unix.IN_MODIFY } if with.op.Has(Remove) { flags |= unix.IN_DELETE | unix.IN_DELETE_SELF } if with.op.Has(Rename) { flags |= unix.IN_MOVED_TO | unix.IN_MOVED_FROM | unix.IN_MOVE_SELF } if with.op.Has(Chmod) { flags |= unix.IN_ATTRIB } if with.op.Has(xUnportableOpen) { flags |= unix.IN_OPEN } if with.op.Has(xUnportableRead) { flags |= unix.IN_ACCESS } if with.op.Has(xUnportableCloseWrite) { flags |= unix.IN_CLOSE_WRITE } if with.op.Has(xUnportableCloseRead) { flags |= unix.IN_CLOSE_NOWRITE } return w.register(path, flags, recurse) } w.mu.Lock() defer w.mu.Unlock() path, recurse := recursivePath(path) if recurse { return filepath.WalkDir(path, func(root string, d fs.DirEntry, err error) error { if err != nil { return err } if !d.IsDir() { if root == path { return fmt.Errorf("fsnotify: not a directory: %q", path) } return nil } // Send a Create event when adding new directory from a recursive // watch; this is for "mkdir -p one/two/three". Usually all those // directories will be created before we can set up watchers on the // subdirectories, so only "one" would be sent as a Create event and // not "one/two" and "one/two/three" (inotifywait -r has the same // problem). if with.sendCreate && root != path { w.sendEvent(Event{Name: root, Op: Create}) } return add(root, with, true) }) } return add(path, with, false) } func (w *inotify) register(path string, flags uint32, recurse bool) error { return w.watches.updatePath(path, func(existing *watch) (*watch, error) { if existing != nil { flags |= existing.flags | unix.IN_MASK_ADD } wd, err := unix.InotifyAddWatch(w.fd, path, flags) if wd == -1 { return nil, err } if e, ok := w.watches.wd[uint32(wd)]; ok { return e, nil } if existing == nil { return &watch{ wd: uint32(wd), path: path, flags: flags, recurse: recurse, }, nil } existing.wd = uint32(wd) existing.flags = flags return existing, nil }) } func (w *inotify) Remove(name string) error { if w.isClosed() { return nil } if debug { fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s Remove(%q)\n", time.Now().Format("15:04:05.000000000"), name) } w.mu.Lock() defer w.mu.Unlock() return w.remove(filepath.Clean(name)) } func (w *inotify) remove(name string) error { wds, err := w.watches.removePath(name) if err != nil { return err } for _, wd := range wds { _, err := unix.InotifyRmWatch(w.fd, wd) if err != nil { // TODO: Perhaps it's not helpful to return an error here in every // case; the only two possible errors are: // // EBADF, which happens when w.fd is not a valid file descriptor of // any kind. // // EINVAL, which is when fd is not an inotify descriptor or wd is // not a valid watch descriptor. Watch descriptors are invalidated // when they are removed explicitly or implicitly; explicitly by // inotify_rm_watch, implicitly when the file they are watching is // deleted. return err } } return nil } func (w *inotify) WatchList() []string { if w.isClosed() { return nil } w.mu.Lock() defer w.mu.Unlock() entries := make([]string, 0, w.watches.len()) for pathname := range w.watches.path { entries = append(entries, pathname) } return entries } // readEvents reads from the inotify file descriptor, converts the // received events into Event objects and sends them via the Events channel func (w *inotify) readEvents() { defer func() { close(w.doneResp) close(w.Errors) close(w.Events) }() var buf [unix.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events for { if w.isClosed() { return } n, err := w.inotifyFile.Read(buf[:]) if err != nil { if errors.Is(err, os.ErrClosed) { return } if !w.sendError(err) { return } continue } if n < unix.SizeofInotifyEvent { err := errors.New("notify: short read in readEvents()") // Read was too short. if n == 0 { err = io.EOF // If EOF is received. This should really never happen. } if !w.sendError(err) { return } continue } // We don't know how many events we just read into the buffer While the // offset points to at least one whole event. var offset uint32 for offset <= uint32(n-unix.SizeofInotifyEvent) { // Point to the event in the buffer. inEvent := (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset])) if inEvent.Mask&unix.IN_Q_OVERFLOW != 0 { if !w.sendError(ErrEventOverflow) { return } } ev, ok := w.handleEvent(inEvent, &buf, offset) if !ok { return } if !w.sendEvent(ev) { return } // Move to the next event in the buffer offset += unix.SizeofInotifyEvent + inEvent.Len } } } func (w *inotify) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byte, offset uint32) (Event, bool) { w.mu.Lock() defer w.mu.Unlock() /// If the event happened to the watched directory or the watched file, the /// kernel doesn't append the filename to the event, but we would like to /// always fill the the "Name" field with a valid filename. We retrieve the /// path of the watch from the "paths" map. /// /// Can be nil if Remove() was called in another goroutine for this path /// inbetween reading the events from the kernel and reading the internal /// state. Not much we can do about it, so just skip. See #616. watch := w.watches.byWd(uint32(inEvent.Wd)) if watch == nil { return Event{}, true } var ( name = watch.path nameLen = uint32(inEvent.Len) ) if nameLen > 0 { /// Point "bytes" at the first byte of the filename bb := *buf bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&bb[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen] /// The filename is padded with NULL bytes. TrimRight() gets rid of those. name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\x00") } if debug { internal.Debug(name, inEvent.Mask, inEvent.Cookie) } if inEvent.Mask&unix.IN_IGNORED != 0 || inEvent.Mask&unix.IN_UNMOUNT != 0 { w.watches.remove(watch) return Event{}, true } // inotify will automatically remove the watch on deletes; just need // to clean our state here. if inEvent.Mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF { w.watches.remove(watch) } // We can't really update the state when a watched path is moved; only // IN_MOVE_SELF is sent and not IN_MOVED_{FROM,TO}. So remove the watch. if inEvent.Mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF { if watch.recurse { // Do nothing return Event{}, true } err := w.remove(watch.path) if err != nil && !errors.Is(err, ErrNonExistentWatch) { if !w.sendError(err) { return Event{}, false } } } /// Skip if we're watching both this path and the parent; the parent will /// already send a delete so no need to do it twice. if inEvent.Mask&unix.IN_DELETE_SELF != 0 { _, ok := w.watches.path[filepath.Dir(watch.path)] if ok { return Event{}, true } } ev := w.newEvent(name, inEvent.Mask, inEvent.Cookie) // Need to update watch path for recurse. if watch.recurse { isDir := inEvent.Mask&unix.IN_ISDIR == unix.IN_ISDIR /// New directory created: set up watch on it. if isDir && ev.Has(Create) { err := w.register(ev.Name, watch.flags, true) if !w.sendError(err) { return Event{}, false } // This was a directory rename, so we need to update all the // children. // // TODO: this is of course pretty slow; we should use a better data // structure for storing all of this, e.g. store children in the // watch. I have some code for this in my kqueue refactor we can use // in the future. For now I'm okay with this as it's not publicly // available. Correctness first, performance second. if ev.renamedFrom != "" { for k, ww := range w.watches.wd { if k == watch.wd || ww.path == ev.Name { continue } if strings.HasPrefix(ww.path, ev.renamedFrom) { ww.path = strings.Replace(ww.path, ev.renamedFrom, ev.Name, 1) w.watches.wd[k] = ww } } } } } return ev, true } func (w *inotify) isRecursive(path string) bool { ww := w.watches.byPath(path) if ww == nil { // path could be a file, so also check the Dir. ww = w.watches.byPath(filepath.Dir(path)) } return ww != nil && ww.recurse } func (w *inotify) newEvent(name string, mask, cookie uint32) Event { e := Event{Name: name} if mask&unix.IN_CREATE == unix.IN_CREATE || mask&unix.IN_MOVED_TO == unix.IN_MOVED_TO { e.Op |= Create } if mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF || mask&unix.IN_DELETE == unix.IN_DELETE { e.Op |= Remove } if mask&unix.IN_MODIFY == unix.IN_MODIFY { e.Op |= Write } if mask&unix.IN_OPEN == unix.IN_OPEN { e.Op |= xUnportableOpen } if mask&unix.IN_ACCESS == unix.IN_ACCESS { e.Op |= xUnportableRead } if mask&unix.IN_CLOSE_WRITE == unix.IN_CLOSE_WRITE { e.Op |= xUnportableCloseWrite } if mask&unix.IN_CLOSE_NOWRITE == unix.IN_CLOSE_NOWRITE { e.Op |= xUnportableCloseRead } if mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF || mask&unix.IN_MOVED_FROM == unix.IN_MOVED_FROM { e.Op |= Rename } if mask&unix.IN_ATTRIB == unix.IN_ATTRIB { e.Op |= Chmod } if cookie != 0 { if mask&unix.IN_MOVED_FROM == unix.IN_MOVED_FROM { w.cookiesMu.Lock() w.cookies[w.cookieIndex] = koekje{cookie: cookie, path: e.Name} w.cookieIndex++ if w.cookieIndex > 9 { w.cookieIndex = 0 } w.cookiesMu.Unlock() } else if mask&unix.IN_MOVED_TO == unix.IN_MOVED_TO { w.cookiesMu.Lock() var prev string for _, c := range w.cookies { if c.cookie == cookie { prev = c.path break } } w.cookiesMu.Unlock() e.renamedFrom = prev } } return e } func (w *inotify) xSupports(op Op) bool { return true // Supports everything. } func (w *inotify) state() { w.mu.Lock() defer w.mu.Unlock() for wd, ww := range w.watches.wd { fmt.Fprintf(os.Stderr, "%4d: recurse=%t %q\n", wd, ww.recurse, ww.path) } } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/backend_kqueue.go ================================================ //go:build freebsd || openbsd || netbsd || dragonfly || darwin package fsnotify import ( "errors" "fmt" "os" "path/filepath" "runtime" "sync" "time" "github.com/fsnotify/fsnotify/internal" "golang.org/x/sys/unix" ) type kqueue struct { *shared Events chan Event Errors chan error kq int // File descriptor (as returned by the kqueue() syscall). closepipe [2]int // Pipe used for closing kq. watches *watches } type ( watches struct { mu sync.RWMutex wd map[int]watch // wd → watch path map[string]int // pathname → wd byDir map[string]map[int]struct{} // dirname(path) → wd seen map[string]struct{} // Keep track of if we know this file exists. byUser map[string]struct{} // Watches added with Watcher.Add() } watch struct { wd int name string linkName string // In case of links; name is the target, and this is the link. isDir bool dirFlags uint32 } ) func newWatches() *watches { return &watches{ wd: make(map[int]watch), path: make(map[string]int), byDir: make(map[string]map[int]struct{}), seen: make(map[string]struct{}), byUser: make(map[string]struct{}), } } func (w *watches) listPaths(userOnly bool) []string { w.mu.RLock() defer w.mu.RUnlock() if userOnly { l := make([]string, 0, len(w.byUser)) for p := range w.byUser { l = append(l, p) } return l } l := make([]string, 0, len(w.path)) for p := range w.path { l = append(l, p) } return l } func (w *watches) watchesInDir(path string) []string { w.mu.RLock() defer w.mu.RUnlock() l := make([]string, 0, 4) for fd := range w.byDir[path] { info := w.wd[fd] if _, ok := w.byUser[info.name]; !ok { l = append(l, info.name) } } return l } // Mark path as added by the user. func (w *watches) addUserWatch(path string) { w.mu.Lock() defer w.mu.Unlock() w.byUser[path] = struct{}{} } func (w *watches) addLink(path string, fd int) { w.mu.Lock() defer w.mu.Unlock() w.path[path] = fd w.seen[path] = struct{}{} } func (w *watches) add(path, linkPath string, fd int, isDir bool) { w.mu.Lock() defer w.mu.Unlock() w.path[path] = fd w.wd[fd] = watch{wd: fd, name: path, linkName: linkPath, isDir: isDir} parent := filepath.Dir(path) byDir, ok := w.byDir[parent] if !ok { byDir = make(map[int]struct{}, 1) w.byDir[parent] = byDir } byDir[fd] = struct{}{} } func (w *watches) byWd(fd int) (watch, bool) { w.mu.RLock() defer w.mu.RUnlock() info, ok := w.wd[fd] return info, ok } func (w *watches) byPath(path string) (watch, bool) { w.mu.RLock() defer w.mu.RUnlock() info, ok := w.wd[w.path[path]] return info, ok } func (w *watches) updateDirFlags(path string, flags uint32) bool { w.mu.Lock() defer w.mu.Unlock() fd, ok := w.path[path] if !ok { // Already deleted: don't re-set it here. return false } info := w.wd[fd] info.dirFlags = flags w.wd[fd] = info return true } func (w *watches) remove(fd int, path string) bool { w.mu.Lock() defer w.mu.Unlock() isDir := w.wd[fd].isDir delete(w.path, path) delete(w.byUser, path) parent := filepath.Dir(path) delete(w.byDir[parent], fd) if len(w.byDir[parent]) == 0 { delete(w.byDir, parent) } delete(w.wd, fd) delete(w.seen, path) return isDir } func (w *watches) markSeen(path string, exists bool) { w.mu.Lock() defer w.mu.Unlock() if exists { w.seen[path] = struct{}{} } else { delete(w.seen, path) } } func (w *watches) seenBefore(path string) bool { w.mu.RLock() defer w.mu.RUnlock() _, ok := w.seen[path] return ok } var defaultBufferSize = 0 func newBackend(ev chan Event, errs chan error) (backend, error) { kq, closepipe, err := newKqueue() if err != nil { return nil, err } w := &kqueue{ shared: newShared(ev, errs), Events: ev, Errors: errs, kq: kq, closepipe: closepipe, watches: newWatches(), } go w.readEvents() return w, nil } // newKqueue creates a new kernel event queue and returns a descriptor. // // This registers a new event on closepipe, which will trigger an event when // it's closed. This way we can use kevent() without timeout/polling; without // the closepipe, it would block forever and we wouldn't be able to stop it at // all. func newKqueue() (kq int, closepipe [2]int, err error) { kq, err = unix.Kqueue() if err != nil { return kq, closepipe, err } // Register the close pipe. err = unix.Pipe(closepipe[:]) if err != nil { unix.Close(kq) return kq, closepipe, err } unix.CloseOnExec(closepipe[0]) unix.CloseOnExec(closepipe[1]) // Register changes to listen on the closepipe. changes := make([]unix.Kevent_t, 1) // SetKevent converts int to the platform-specific types. unix.SetKevent(&changes[0], closepipe[0], unix.EVFILT_READ, unix.EV_ADD|unix.EV_ENABLE|unix.EV_ONESHOT) ok, err := unix.Kevent(kq, changes, nil, nil) if ok == -1 { unix.Close(kq) unix.Close(closepipe[0]) unix.Close(closepipe[1]) return kq, closepipe, err } return kq, closepipe, nil } func (w *kqueue) Close() error { if w.shared.close() { return nil } pathsToRemove := w.watches.listPaths(false) for _, name := range pathsToRemove { w.Remove(name) } unix.Close(w.closepipe[1]) // Send "quit" message to readEvents return nil } func (w *kqueue) Add(name string) error { return w.AddWith(name) } func (w *kqueue) AddWith(name string, opts ...addOpt) error { if debug { fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s AddWith(%q)\n", time.Now().Format("15:04:05.000000000"), name) } with := getOptions(opts...) if !w.xSupports(with.op) { return fmt.Errorf("%w: %s", xErrUnsupported, with.op) } _, err := w.addWatch(name, noteAllEvents, false) if err != nil { return err } w.watches.addUserWatch(name) return nil } func (w *kqueue) Remove(name string) error { if debug { fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s Remove(%q)\n", time.Now().Format("15:04:05.000000000"), name) } return w.remove(name, true) } func (w *kqueue) remove(name string, unwatchFiles bool) error { if w.isClosed() { return nil } name = filepath.Clean(name) info, ok := w.watches.byPath(name) if !ok { return fmt.Errorf("%w: %s", ErrNonExistentWatch, name) } err := w.register([]int{info.wd}, unix.EV_DELETE, 0) if err != nil { return err } unix.Close(info.wd) isDir := w.watches.remove(info.wd, name) // Find all watched paths that are in this directory that are not external. if unwatchFiles && isDir { pathsToRemove := w.watches.watchesInDir(name) for _, name := range pathsToRemove { // Since these are internal, not much sense in propagating error to // the user, as that will just confuse them with an error about a // path they did not explicitly watch themselves. w.Remove(name) } } return nil } func (w *kqueue) WatchList() []string { if w.isClosed() { return nil } return w.watches.listPaths(true) } // Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE) const noteAllEvents = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_ATTRIB | unix.NOTE_RENAME // addWatch adds name to the watched file set; the flags are interpreted as // described in kevent(2). // // Returns the real path to the file which was added, with symlinks resolved. func (w *kqueue) addWatch(name string, flags uint32, listDir bool) (string, error) { if w.isClosed() { return "", ErrClosed } name = filepath.Clean(name) info, alreadyWatching := w.watches.byPath(name) if !alreadyWatching { fi, err := os.Lstat(name) if err != nil { return "", err } // Don't watch sockets or named pipes. if (fi.Mode()&os.ModeSocket == os.ModeSocket) || (fi.Mode()&os.ModeNamedPipe == os.ModeNamedPipe) { return "", nil } // Follow symlinks, but only for paths added with Add(), and not paths // we're adding from internalWatch from a listdir. if !listDir && fi.Mode()&os.ModeSymlink == os.ModeSymlink { link, err := os.Readlink(name) if err != nil { return "", err } if !filepath.IsAbs(link) { link = filepath.Join(filepath.Dir(name), link) } _, alreadyWatching = w.watches.byPath(link) if alreadyWatching { // Add to watches so we don't get spurious Create events later // on when we diff the directories. w.watches.addLink(name, 0) return link, nil } info.linkName = name name = link fi, err = os.Lstat(name) if err != nil { return "", err } } // Retry on EINTR; open() can return EINTR in practice on macOS. // See #354, and Go issues 11180 and 39237. for { info.wd, err = unix.Open(name, openMode, 0) if err == nil { break } if errors.Is(err, unix.EINTR) { continue } return "", err } info.isDir = fi.IsDir() } err := w.register([]int{info.wd}, unix.EV_ADD|unix.EV_CLEAR|unix.EV_ENABLE, flags) if err != nil { unix.Close(info.wd) return "", err } if !alreadyWatching { w.watches.add(name, info.linkName, info.wd, info.isDir) } // Watch the directory if it has not been watched before, or if it was // watched before, but perhaps only a NOTE_DELETE (watchDirectoryFiles) if info.isDir { watchDir := (flags&unix.NOTE_WRITE) == unix.NOTE_WRITE && (!alreadyWatching || (info.dirFlags&unix.NOTE_WRITE) != unix.NOTE_WRITE) if !w.watches.updateDirFlags(name, flags) { return "", nil } if watchDir { d := name if info.linkName != "" { d = info.linkName } if err := w.watchDirectoryFiles(d); err != nil { return "", err } } } return name, nil } // readEvents reads from kqueue and converts the received kevents into // Event values that it sends down the Events channel. func (w *kqueue) readEvents() { defer func() { close(w.Events) close(w.Errors) _ = unix.Close(w.kq) unix.Close(w.closepipe[0]) }() eventBuffer := make([]unix.Kevent_t, 10) for { kevents, err := w.read(eventBuffer) // EINTR is okay, the syscall was interrupted before timeout expired. if err != nil && err != unix.EINTR { if !w.sendError(fmt.Errorf("fsnotify.readEvents: %w", err)) { return } } for _, kevent := range kevents { var ( wd = int(kevent.Ident) mask = uint32(kevent.Fflags) ) // Shut down the loop when the pipe is closed, but only after all // other events have been processed. if wd == w.closepipe[0] { return } path, ok := w.watches.byWd(wd) if debug { internal.Debug(path.name, &kevent) } // On macOS it seems that sometimes an event with Ident=0 is // delivered, and no other flags/information beyond that, even // though we never saw such a file descriptor. For example in // TestWatchSymlink/277 (usually at the end, but sometimes sooner): // // fmt.Printf("READ: %2d %#v\n", kevent.Ident, kevent) // unix.Kevent_t{Ident:0x2a, Filter:-4, Flags:0x25, Fflags:0x2, Data:0, Udata:(*uint8)(nil)} // unix.Kevent_t{Ident:0x0, Filter:-4, Flags:0x25, Fflags:0x2, Data:0, Udata:(*uint8)(nil)} // // The first is a normal event, the second with Ident 0. No error // flag, no data, no ... nothing. // // I read a bit through bsd/kern_event.c from the xnu source, but I // don't really see an obvious location where this is triggered – // this doesn't seem intentional, but idk... // // Technically fd 0 is a valid descriptor, so only skip it if // there's no path, and if we're on macOS. if !ok && kevent.Ident == 0 && runtime.GOOS == "darwin" { continue } event := w.newEvent(path.name, path.linkName, mask) if event.Has(Rename) || event.Has(Remove) { w.remove(event.Name, false) w.watches.markSeen(event.Name, false) } if path.isDir && event.Has(Write) && !event.Has(Remove) { w.dirChange(event.Name) } else if !w.sendEvent(event) { return } if event.Has(Remove) { // Look for a file that may have overwritten this; for example, // mv f1 f2 will delete f2, then create f2. if path.isDir { fileDir := filepath.Clean(event.Name) _, found := w.watches.byPath(fileDir) if found { // TODO: this branch is never triggered in any test. // Added in d6220df (2012). // isDir check added in 8611c35 (2016): https://github.com/fsnotify/fsnotify/pull/111 // // I don't really get how this can be triggered either. // And it wasn't triggered in the patch that added it, // either. // // Original also had a comment: // make sure the directory exists before we watch for // changes. When we do a recursive watch and perform // rm -rf, the parent directory might have gone // missing, ignore the missing directory and let the // upcoming delete event remove the watch from the // parent directory. err := w.dirChange(fileDir) if !w.sendError(err) { return } } } else { path := filepath.Clean(event.Name) if fi, err := os.Lstat(path); err == nil { err := w.sendCreateIfNew(path, fi) if !w.sendError(err) { return } } } } } } } // newEvent returns an platform-independent Event based on kqueue Fflags. func (w *kqueue) newEvent(name, linkName string, mask uint32) Event { e := Event{Name: name} if linkName != "" { // If the user watched "/path/link" then emit events as "/path/link" // rather than "/path/target". e.Name = linkName } if mask&unix.NOTE_DELETE == unix.NOTE_DELETE { e.Op |= Remove } if mask&unix.NOTE_WRITE == unix.NOTE_WRITE { e.Op |= Write } if mask&unix.NOTE_RENAME == unix.NOTE_RENAME { e.Op |= Rename } if mask&unix.NOTE_ATTRIB == unix.NOTE_ATTRIB { e.Op |= Chmod } // No point sending a write and delete event at the same time: if it's gone, // then it's gone. if e.Op.Has(Write) && e.Op.Has(Remove) { e.Op &^= Write } return e } // watchDirectoryFiles to mimic inotify when adding a watch on a directory func (w *kqueue) watchDirectoryFiles(dirPath string) error { files, err := os.ReadDir(dirPath) if err != nil { return err } for _, f := range files { path := filepath.Join(dirPath, f.Name()) fi, err := f.Info() if err != nil { return fmt.Errorf("%q: %w", path, err) } cleanPath, err := w.internalWatch(path, fi) if err != nil { // No permission to read the file; that's not a problem: just skip. // But do add it to w.fileExists to prevent it from being picked up // as a "new" file later (it still shows up in the directory // listing). switch { case errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM): cleanPath = filepath.Clean(path) default: return fmt.Errorf("%q: %w", path, err) } } w.watches.markSeen(cleanPath, true) } return nil } // Search the directory for new files and send an event for them. // // This functionality is to have the BSD watcher match the inotify, which sends // a create event for files created in a watched directory. func (w *kqueue) dirChange(dir string) error { files, err := os.ReadDir(dir) if err != nil { // Directory no longer exists: we can ignore this safely. kqueue will // still give us the correct events. if errors.Is(err, os.ErrNotExist) { return nil } return fmt.Errorf("fsnotify.dirChange %q: %w", dir, err) } for _, f := range files { fi, err := f.Info() if err != nil { if errors.Is(err, os.ErrNotExist) { return nil } return fmt.Errorf("fsnotify.dirChange: %w", err) } err = w.sendCreateIfNew(filepath.Join(dir, fi.Name()), fi) if err != nil { // Don't need to send an error if this file isn't readable. if errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) || errors.Is(err, os.ErrNotExist) { return nil } return fmt.Errorf("fsnotify.dirChange: %w", err) } } return nil } // Send a create event if the file isn't already being tracked, and start // watching this file. func (w *kqueue) sendCreateIfNew(path string, fi os.FileInfo) error { if !w.watches.seenBefore(path) { if !w.sendEvent(Event{Name: path, Op: Create}) { return nil } } // Like watchDirectoryFiles, but without doing another ReadDir. path, err := w.internalWatch(path, fi) if err != nil { return err } w.watches.markSeen(path, true) return nil } func (w *kqueue) internalWatch(name string, fi os.FileInfo) (string, error) { if fi.IsDir() { // mimic Linux providing delete events for subdirectories, but preserve // the flags used if currently watching subdirectory info, _ := w.watches.byPath(name) return w.addWatch(name, info.dirFlags|unix.NOTE_DELETE|unix.NOTE_RENAME, true) } // Watch file to mimic Linux inotify. return w.addWatch(name, noteAllEvents, true) } // Register events with the queue. func (w *kqueue) register(fds []int, flags int, fflags uint32) error { changes := make([]unix.Kevent_t, len(fds)) for i, fd := range fds { // SetKevent converts int to the platform-specific types. unix.SetKevent(&changes[i], fd, unix.EVFILT_VNODE, flags) changes[i].Fflags = fflags } // Register the events. success, err := unix.Kevent(w.kq, changes, nil, nil) if success == -1 { return err } return nil } // read retrieves pending events, or waits until an event occurs. func (w *kqueue) read(events []unix.Kevent_t) ([]unix.Kevent_t, error) { n, err := unix.Kevent(w.kq, nil, events, nil) if err != nil { return nil, err } return events[0:n], nil } func (w *kqueue) xSupports(op Op) bool { //if runtime.GOOS == "freebsd" { // return true // Supports everything. //} if op.Has(xUnportableOpen) || op.Has(xUnportableRead) || op.Has(xUnportableCloseWrite) || op.Has(xUnportableCloseRead) { return false } return true } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/backend_other.go ================================================ //go:build appengine || (!darwin && !dragonfly && !freebsd && !openbsd && !linux && !netbsd && !solaris && !windows) package fsnotify import "errors" type other struct { Events chan Event Errors chan error } var defaultBufferSize = 0 func newBackend(ev chan Event, errs chan error) (backend, error) { return nil, errors.New("fsnotify not supported on the current platform") } func (w *other) Close() error { return nil } func (w *other) WatchList() []string { return nil } func (w *other) Add(name string) error { return nil } func (w *other) AddWith(name string, opts ...addOpt) error { return nil } func (w *other) Remove(name string) error { return nil } func (w *other) xSupports(op Op) bool { return false } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/backend_windows.go ================================================ //go:build windows // Windows backend based on ReadDirectoryChangesW() // // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw package fsnotify import ( "errors" "fmt" "os" "path/filepath" "reflect" "runtime" "strings" "sync" "time" "unsafe" "github.com/fsnotify/fsnotify/internal" "golang.org/x/sys/windows" ) type readDirChangesW struct { Events chan Event Errors chan error port windows.Handle // Handle to completion port input chan *input // Inputs to the reader are sent on this channel done chan chan<- error mu sync.Mutex // Protects access to watches, closed watches watchMap // Map of watches (key: i-number) closed bool // Set to true when Close() is first called } var defaultBufferSize = 50 func newBackend(ev chan Event, errs chan error) (backend, error) { port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0) if err != nil { return nil, os.NewSyscallError("CreateIoCompletionPort", err) } w := &readDirChangesW{ Events: ev, Errors: errs, port: port, watches: make(watchMap), input: make(chan *input, 1), done: make(chan chan<- error, 1), } go w.readEvents() return w, nil } func (w *readDirChangesW) isClosed() bool { w.mu.Lock() defer w.mu.Unlock() return w.closed } func (w *readDirChangesW) sendEvent(name, renamedFrom string, mask uint64) bool { if mask == 0 { return false } event := w.newEvent(name, uint32(mask)) event.renamedFrom = renamedFrom select { case ch := <-w.done: w.done <- ch case w.Events <- event: } return true } // Returns true if the error was sent, or false if watcher is closed. func (w *readDirChangesW) sendError(err error) bool { if err == nil { return true } select { case <-w.done: return false case w.Errors <- err: return true } } func (w *readDirChangesW) Close() error { if w.isClosed() { return nil } w.mu.Lock() w.closed = true w.mu.Unlock() // Send "done" message to the reader goroutine ch := make(chan error) w.done <- ch if err := w.wakeupReader(); err != nil { return err } return <-ch } func (w *readDirChangesW) Add(name string) error { return w.AddWith(name) } func (w *readDirChangesW) AddWith(name string, opts ...addOpt) error { if w.isClosed() { return ErrClosed } if debug { fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s AddWith(%q)\n", time.Now().Format("15:04:05.000000000"), filepath.ToSlash(name)) } with := getOptions(opts...) if !w.xSupports(with.op) { return fmt.Errorf("%w: %s", xErrUnsupported, with.op) } if with.bufsize < 4096 { return fmt.Errorf("fsnotify.WithBufferSize: buffer size cannot be smaller than 4096 bytes") } in := &input{ op: opAddWatch, path: filepath.Clean(name), flags: sysFSALLEVENTS, reply: make(chan error), bufsize: with.bufsize, } w.input <- in if err := w.wakeupReader(); err != nil { return err } return <-in.reply } func (w *readDirChangesW) Remove(name string) error { if w.isClosed() { return nil } if debug { fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s Remove(%q)\n", time.Now().Format("15:04:05.000000000"), filepath.ToSlash(name)) } in := &input{ op: opRemoveWatch, path: filepath.Clean(name), reply: make(chan error), } w.input <- in if err := w.wakeupReader(); err != nil { return err } return <-in.reply } func (w *readDirChangesW) WatchList() []string { if w.isClosed() { return nil } w.mu.Lock() defer w.mu.Unlock() entries := make([]string, 0, len(w.watches)) for _, entry := range w.watches { for _, watchEntry := range entry { for name := range watchEntry.names { entries = append(entries, filepath.Join(watchEntry.path, name)) } // the directory itself is being watched if watchEntry.mask != 0 { entries = append(entries, watchEntry.path) } } } return entries } // These options are from the old golang.org/x/exp/winfsnotify, where you could // add various options to the watch. This has long since been removed. // // The "sys" in the name is misleading as they're not part of any "system". // // This should all be removed at some point, and just use windows.FILE_NOTIFY_* const ( sysFSALLEVENTS = 0xfff sysFSCREATE = 0x100 sysFSDELETE = 0x200 sysFSDELETESELF = 0x400 sysFSMODIFY = 0x2 sysFSMOVE = 0xc0 sysFSMOVEDFROM = 0x40 sysFSMOVEDTO = 0x80 sysFSMOVESELF = 0x800 sysFSIGNORED = 0x8000 ) func (w *readDirChangesW) newEvent(name string, mask uint32) Event { e := Event{Name: name} if mask&sysFSCREATE == sysFSCREATE || mask&sysFSMOVEDTO == sysFSMOVEDTO { e.Op |= Create } if mask&sysFSDELETE == sysFSDELETE || mask&sysFSDELETESELF == sysFSDELETESELF { e.Op |= Remove } if mask&sysFSMODIFY == sysFSMODIFY { e.Op |= Write } if mask&sysFSMOVE == sysFSMOVE || mask&sysFSMOVESELF == sysFSMOVESELF || mask&sysFSMOVEDFROM == sysFSMOVEDFROM { e.Op |= Rename } return e } const ( opAddWatch = iota opRemoveWatch ) const ( provisional uint64 = 1 << (32 + iota) ) type input struct { op int path string flags uint32 bufsize int reply chan error } type inode struct { handle windows.Handle volume uint32 index uint64 } type watch struct { ov windows.Overlapped ino *inode // i-number recurse bool // Recursive watch? path string // Directory path mask uint64 // Directory itself is being watched with these notify flags names map[string]uint64 // Map of names being watched and their notify flags rename string // Remembers the old name while renaming a file buf []byte // buffer, allocated later } type ( indexMap map[uint64]*watch watchMap map[uint32]indexMap ) func (w *readDirChangesW) wakeupReader() error { err := windows.PostQueuedCompletionStatus(w.port, 0, 0, nil) if err != nil { return os.NewSyscallError("PostQueuedCompletionStatus", err) } return nil } func (w *readDirChangesW) getDir(pathname string) (dir string, err error) { attr, err := windows.GetFileAttributes(windows.StringToUTF16Ptr(pathname)) if err != nil { return "", os.NewSyscallError("GetFileAttributes", err) } if attr&windows.FILE_ATTRIBUTE_DIRECTORY != 0 { dir = pathname } else { dir, _ = filepath.Split(pathname) dir = filepath.Clean(dir) } return } func (w *readDirChangesW) getIno(path string) (ino *inode, err error) { h, err := windows.CreateFile(windows.StringToUTF16Ptr(path), windows.FILE_LIST_DIRECTORY, windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE, nil, windows.OPEN_EXISTING, windows.FILE_FLAG_BACKUP_SEMANTICS|windows.FILE_FLAG_OVERLAPPED, 0) if err != nil { return nil, os.NewSyscallError("CreateFile", err) } var fi windows.ByHandleFileInformation err = windows.GetFileInformationByHandle(h, &fi) if err != nil { windows.CloseHandle(h) return nil, os.NewSyscallError("GetFileInformationByHandle", err) } ino = &inode{ handle: h, volume: fi.VolumeSerialNumber, index: uint64(fi.FileIndexHigh)<<32 | uint64(fi.FileIndexLow), } return ino, nil } // Must run within the I/O thread. func (m watchMap) get(ino *inode) *watch { if i := m[ino.volume]; i != nil { return i[ino.index] } return nil } // Must run within the I/O thread. func (m watchMap) set(ino *inode, watch *watch) { i := m[ino.volume] if i == nil { i = make(indexMap) m[ino.volume] = i } i[ino.index] = watch } // Must run within the I/O thread. func (w *readDirChangesW) addWatch(pathname string, flags uint64, bufsize int) error { pathname, recurse := recursivePath(pathname) dir, err := w.getDir(pathname) if err != nil { return err } ino, err := w.getIno(dir) if err != nil { return err } w.mu.Lock() watchEntry := w.watches.get(ino) w.mu.Unlock() if watchEntry == nil { _, err := windows.CreateIoCompletionPort(ino.handle, w.port, 0, 0) if err != nil { windows.CloseHandle(ino.handle) return os.NewSyscallError("CreateIoCompletionPort", err) } watchEntry = &watch{ ino: ino, path: dir, names: make(map[string]uint64), recurse: recurse, buf: make([]byte, bufsize), } w.mu.Lock() w.watches.set(ino, watchEntry) w.mu.Unlock() flags |= provisional } else { windows.CloseHandle(ino.handle) } if pathname == dir { watchEntry.mask |= flags } else { watchEntry.names[filepath.Base(pathname)] |= flags } err = w.startRead(watchEntry) if err != nil { return err } if pathname == dir { watchEntry.mask &= ^provisional } else { watchEntry.names[filepath.Base(pathname)] &= ^provisional } return nil } // Must run within the I/O thread. func (w *readDirChangesW) remWatch(pathname string) error { pathname, recurse := recursivePath(pathname) dir, err := w.getDir(pathname) if err != nil { return err } ino, err := w.getIno(dir) if err != nil { return err } w.mu.Lock() watch := w.watches.get(ino) w.mu.Unlock() if recurse && !watch.recurse { return fmt.Errorf("can't use \\... with non-recursive watch %q", pathname) } err = windows.CloseHandle(ino.handle) if err != nil { w.sendError(os.NewSyscallError("CloseHandle", err)) } if watch == nil { return fmt.Errorf("%w: %s", ErrNonExistentWatch, pathname) } if pathname == dir { w.sendEvent(watch.path, "", watch.mask&sysFSIGNORED) watch.mask = 0 } else { name := filepath.Base(pathname) w.sendEvent(filepath.Join(watch.path, name), "", watch.names[name]&sysFSIGNORED) delete(watch.names, name) } return w.startRead(watch) } // Must run within the I/O thread. func (w *readDirChangesW) deleteWatch(watch *watch) { for name, mask := range watch.names { if mask&provisional == 0 { w.sendEvent(filepath.Join(watch.path, name), "", mask&sysFSIGNORED) } delete(watch.names, name) } if watch.mask != 0 { if watch.mask&provisional == 0 { w.sendEvent(watch.path, "", watch.mask&sysFSIGNORED) } watch.mask = 0 } } // Must run within the I/O thread. func (w *readDirChangesW) startRead(watch *watch) error { err := windows.CancelIo(watch.ino.handle) if err != nil { w.sendError(os.NewSyscallError("CancelIo", err)) w.deleteWatch(watch) } mask := w.toWindowsFlags(watch.mask) for _, m := range watch.names { mask |= w.toWindowsFlags(m) } if mask == 0 { err := windows.CloseHandle(watch.ino.handle) if err != nil { w.sendError(os.NewSyscallError("CloseHandle", err)) } w.mu.Lock() delete(w.watches[watch.ino.volume], watch.ino.index) w.mu.Unlock() return nil } // We need to pass the array, rather than the slice. hdr := (*reflect.SliceHeader)(unsafe.Pointer(&watch.buf)) rdErr := windows.ReadDirectoryChanges(watch.ino.handle, (*byte)(unsafe.Pointer(hdr.Data)), uint32(hdr.Len), watch.recurse, mask, nil, &watch.ov, 0) if rdErr != nil { err := os.NewSyscallError("ReadDirectoryChanges", rdErr) if rdErr == windows.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 { // Watched directory was probably removed w.sendEvent(watch.path, "", watch.mask&sysFSDELETESELF) err = nil } w.deleteWatch(watch) w.startRead(watch) return err } return nil } // readEvents reads from the I/O completion port, converts the // received events into Event objects and sends them via the Events channel. // Entry point to the I/O thread. func (w *readDirChangesW) readEvents() { var ( n uint32 key uintptr ov *windows.Overlapped ) runtime.LockOSThread() for { // This error is handled after the watch == nil check below. qErr := windows.GetQueuedCompletionStatus(w.port, &n, &key, &ov, windows.INFINITE) watch := (*watch)(unsafe.Pointer(ov)) if watch == nil { select { case ch := <-w.done: w.mu.Lock() var indexes []indexMap for _, index := range w.watches { indexes = append(indexes, index) } w.mu.Unlock() for _, index := range indexes { for _, watch := range index { w.deleteWatch(watch) w.startRead(watch) } } err := windows.CloseHandle(w.port) if err != nil { err = os.NewSyscallError("CloseHandle", err) } close(w.Events) close(w.Errors) ch <- err return case in := <-w.input: switch in.op { case opAddWatch: in.reply <- w.addWatch(in.path, uint64(in.flags), in.bufsize) case opRemoveWatch: in.reply <- w.remWatch(in.path) } default: } continue } switch qErr { case nil: // No error case windows.ERROR_MORE_DATA: if watch == nil { w.sendError(errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer")) } else { // The i/o succeeded but the buffer is full. // In theory we should be building up a full packet. // In practice we can get away with just carrying on. n = uint32(unsafe.Sizeof(watch.buf)) } case windows.ERROR_ACCESS_DENIED: // Watched directory was probably removed w.sendEvent(watch.path, "", watch.mask&sysFSDELETESELF) w.deleteWatch(watch) w.startRead(watch) continue case windows.ERROR_OPERATION_ABORTED: // CancelIo was called on this handle continue default: w.sendError(os.NewSyscallError("GetQueuedCompletionPort", qErr)) continue } var offset uint32 for { if n == 0 { w.sendError(ErrEventOverflow) break } // Point "raw" to the event in the buffer raw := (*windows.FileNotifyInformation)(unsafe.Pointer(&watch.buf[offset])) // Create a buf that is the size of the path name size := int(raw.FileNameLength / 2) var buf []uint16 // TODO: Use unsafe.Slice in Go 1.17; https://stackoverflow.com/questions/51187973 sh := (*reflect.SliceHeader)(unsafe.Pointer(&buf)) sh.Data = uintptr(unsafe.Pointer(&raw.FileName)) sh.Len = size sh.Cap = size name := windows.UTF16ToString(buf) fullname := filepath.Join(watch.path, name) if debug { internal.Debug(fullname, raw.Action) } var mask uint64 switch raw.Action { case windows.FILE_ACTION_REMOVED: mask = sysFSDELETESELF case windows.FILE_ACTION_MODIFIED: mask = sysFSMODIFY case windows.FILE_ACTION_RENAMED_OLD_NAME: watch.rename = name case windows.FILE_ACTION_RENAMED_NEW_NAME: // Update saved path of all sub-watches. old := filepath.Join(watch.path, watch.rename) w.mu.Lock() for _, watchMap := range w.watches { for _, ww := range watchMap { if strings.HasPrefix(ww.path, old) { ww.path = filepath.Join(fullname, strings.TrimPrefix(ww.path, old)) } } } w.mu.Unlock() if watch.names[watch.rename] != 0 { watch.names[name] |= watch.names[watch.rename] delete(watch.names, watch.rename) mask = sysFSMOVESELF } } if raw.Action != windows.FILE_ACTION_RENAMED_NEW_NAME { w.sendEvent(fullname, "", watch.names[name]&mask) } if raw.Action == windows.FILE_ACTION_REMOVED { w.sendEvent(fullname, "", watch.names[name]&sysFSIGNORED) delete(watch.names, name) } if watch.rename != "" && raw.Action == windows.FILE_ACTION_RENAMED_NEW_NAME { w.sendEvent(fullname, filepath.Join(watch.path, watch.rename), watch.mask&w.toFSnotifyFlags(raw.Action)) } else { w.sendEvent(fullname, "", watch.mask&w.toFSnotifyFlags(raw.Action)) } if raw.Action == windows.FILE_ACTION_RENAMED_NEW_NAME { w.sendEvent(filepath.Join(watch.path, watch.rename), "", watch.names[name]&mask) } // Move to the next event in the buffer if raw.NextEntryOffset == 0 { break } offset += raw.NextEntryOffset // Error! if offset >= n { //lint:ignore ST1005 Windows should be capitalized w.sendError(errors.New("Windows system assumed buffer larger than it is, events have likely been missed")) break } } if err := w.startRead(watch); err != nil { w.sendError(err) } } } func (w *readDirChangesW) toWindowsFlags(mask uint64) uint32 { var m uint32 if mask&sysFSMODIFY != 0 { m |= windows.FILE_NOTIFY_CHANGE_LAST_WRITE } if mask&(sysFSMOVE|sysFSCREATE|sysFSDELETE) != 0 { m |= windows.FILE_NOTIFY_CHANGE_FILE_NAME | windows.FILE_NOTIFY_CHANGE_DIR_NAME } return m } func (w *readDirChangesW) toFSnotifyFlags(action uint32) uint64 { switch action { case windows.FILE_ACTION_ADDED: return sysFSCREATE case windows.FILE_ACTION_REMOVED: return sysFSDELETE case windows.FILE_ACTION_MODIFIED: return sysFSMODIFY case windows.FILE_ACTION_RENAMED_OLD_NAME: return sysFSMOVEDFROM case windows.FILE_ACTION_RENAMED_NEW_NAME: return sysFSMOVEDTO } return 0 } func (w *readDirChangesW) xSupports(op Op) bool { if op.Has(xUnportableOpen) || op.Has(xUnportableRead) || op.Has(xUnportableCloseWrite) || op.Has(xUnportableCloseRead) { return false } return true } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/fsnotify.go ================================================ // Package fsnotify provides a cross-platform interface for file system // notifications. // // Currently supported systems: // // - Linux via inotify // - BSD, macOS via kqueue // - Windows via ReadDirectoryChangesW // - illumos via FEN // // # FSNOTIFY_DEBUG // // Set the FSNOTIFY_DEBUG environment variable to "1" to print debug messages to // stderr. This can be useful to track down some problems, especially in cases // where fsnotify is used as an indirect dependency. // // Every event will be printed as soon as there's something useful to print, // with as little processing from fsnotify. // // Example output: // // FSNOTIFY_DEBUG: 11:34:23.633087586 256:IN_CREATE → "/tmp/file-1" // FSNOTIFY_DEBUG: 11:34:23.633202319 4:IN_ATTRIB → "/tmp/file-1" // FSNOTIFY_DEBUG: 11:34:28.989728764 512:IN_DELETE → "/tmp/file-1" package fsnotify import ( "errors" "fmt" "os" "path/filepath" "strings" ) // Watcher watches a set of paths, delivering events on a channel. // // A watcher should not be copied (e.g. pass it by pointer, rather than by // value). // // # Linux notes // // When a file is removed a Remove event won't be emitted until all file // descriptors are closed, and deletes will always emit a Chmod. For example: // // fp := os.Open("file") // os.Remove("file") // Triggers Chmod // fp.Close() // Triggers Remove // // This is the event that inotify sends, so not much can be changed about this. // // The fs.inotify.max_user_watches sysctl variable specifies the upper limit // for the number of watches per user, and fs.inotify.max_user_instances // specifies the maximum number of inotify instances per user. Every Watcher you // create is an "instance", and every path you add is a "watch". // // These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and // /proc/sys/fs/inotify/max_user_instances // // To increase them you can use sysctl or write the value to the /proc file: // // # Default values on Linux 5.18 // sysctl fs.inotify.max_user_watches=124983 // sysctl fs.inotify.max_user_instances=128 // // To make the changes persist on reboot edit /etc/sysctl.conf or // /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check // your distro's documentation): // // fs.inotify.max_user_watches=124983 // fs.inotify.max_user_instances=128 // // Reaching the limit will result in a "no space left on device" or "too many open // files" error. // // # kqueue notes (macOS, BSD) // // kqueue requires opening a file descriptor for every file that's being watched; // so if you're watching a directory with five files then that's six file // descriptors. You will run in to your system's "max open files" limit faster on // these platforms. // // The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to // control the maximum number of open files, as well as /etc/login.conf on BSD // systems. // // # Windows notes // // Paths can be added as "C:\\path\\to\\dir", but forward slashes // ("C:/path/to/dir") will also work. // // When a watched directory is removed it will always send an event for the // directory itself, but may not send events for all files in that directory. // Sometimes it will send events for all files, sometimes it will send no // events, and often only for some files. // // The default ReadDirectoryChangesW() buffer size is 64K, which is the largest // value that is guaranteed to work with SMB filesystems. If you have many // events in quick succession this may not be enough, and you will have to use // [WithBufferSize] to increase the value. type Watcher struct { b backend // Events sends the filesystem change events. // // fsnotify can send the following events; a "path" here can refer to a // file, directory, symbolic link, or special file like a FIFO. // // fsnotify.Create A new path was created; this may be followed by one // or more Write events if data also gets written to a // file. // // fsnotify.Remove A path was removed. // // fsnotify.Rename A path was renamed. A rename is always sent with the // old path as Event.Name, and a Create event will be // sent with the new name. Renames are only sent for // paths that are currently watched; e.g. moving an // unmonitored file into a monitored directory will // show up as just a Create. Similarly, renaming a file // to outside a monitored directory will show up as // only a Rename. // // fsnotify.Write A file or named pipe was written to. A Truncate will // also trigger a Write. A single "write action" // initiated by the user may show up as one or multiple // writes, depending on when the system syncs things to // disk. For example when compiling a large Go program // you may get hundreds of Write events, and you may // want to wait until you've stopped receiving them // (see the dedup example in cmd/fsnotify). // // Some systems may send Write event for directories // when the directory content changes. // // fsnotify.Chmod Attributes were changed. On Linux this is also sent // when a file is removed (or more accurately, when a // link to an inode is removed). On kqueue it's sent // when a file is truncated. On Windows it's never // sent. Events chan Event // Errors sends any errors. Errors chan error } // Event represents a file system notification. type Event struct { // Path to the file or directory. // // Paths are relative to the input; for example with Add("dir") the Name // will be set to "dir/file" if you create that file, but if you use // Add("/path/to/dir") it will be "/path/to/dir/file". Name string // File operation that triggered the event. // // This is a bitmask and some systems may send multiple operations at once. // Use the Event.Has() method instead of comparing with ==. Op Op // Create events will have this set to the old path if it's a rename. This // only works when both the source and destination are watched. It's not // reliable when watching individual files, only directories. // // For example "mv /tmp/file /tmp/rename" will emit: // // Event{Op: Rename, Name: "/tmp/file"} // Event{Op: Create, Name: "/tmp/rename", RenamedFrom: "/tmp/file"} renamedFrom string } // Op describes a set of file operations. type Op uint32 // The operations fsnotify can trigger; see the documentation on [Watcher] for a // full description, and check them with [Event.Has]. const ( // A new pathname was created. Create Op = 1 << iota // The pathname was written to; this does *not* mean the write has finished, // and a write can be followed by more writes. Write // The path was removed; any watches on it will be removed. Some "remove" // operations may trigger a Rename if the file is actually moved (for // example "remove to trash" is often a rename). Remove // The path was renamed to something else; any watches on it will be // removed. Rename // File attributes were changed. // // It's generally not recommended to take action on this event, as it may // get triggered very frequently by some software. For example, Spotlight // indexing on macOS, anti-virus software, backup software, etc. Chmod // File descriptor was opened. // // Only works on Linux and FreeBSD. xUnportableOpen // File was read from. // // Only works on Linux and FreeBSD. xUnportableRead // File opened for writing was closed. // // Only works on Linux and FreeBSD. // // The advantage of using this over Write is that it's more reliable than // waiting for Write events to stop. It's also faster (if you're not // listening to Write events): copying a file of a few GB can easily // generate tens of thousands of Write events in a short span of time. xUnportableCloseWrite // File opened for reading was closed. // // Only works on Linux and FreeBSD. xUnportableCloseRead ) var ( // ErrNonExistentWatch is used when Remove() is called on a path that's not // added. ErrNonExistentWatch = errors.New("fsnotify: can't remove non-existent watch") // ErrClosed is used when trying to operate on a closed Watcher. ErrClosed = errors.New("fsnotify: watcher already closed") // ErrEventOverflow is reported from the Errors channel when there are too // many events: // // - inotify: inotify returns IN_Q_OVERFLOW – because there are too // many queued events (the fs.inotify.max_queued_events // sysctl can be used to increase this). // - windows: The buffer size is too small; WithBufferSize() can be used to increase it. // - kqueue, fen: Not used. ErrEventOverflow = errors.New("fsnotify: queue or buffer overflow") // ErrUnsupported is returned by AddWith() when WithOps() specified an // Unportable event that's not supported on this platform. //lint:ignore ST1012 not relevant xErrUnsupported = errors.New("fsnotify: not supported with this backend") ) // NewWatcher creates a new Watcher. func NewWatcher() (*Watcher, error) { ev, errs := make(chan Event, defaultBufferSize), make(chan error) b, err := newBackend(ev, errs) if err != nil { return nil, err } return &Watcher{b: b, Events: ev, Errors: errs}, nil } // NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events // channel. // // The main use case for this is situations with a very large number of events // where the kernel buffer size can't be increased (e.g. due to lack of // permissions). An unbuffered Watcher will perform better for almost all use // cases, and whenever possible you will be better off increasing the kernel // buffers instead of adding a large userspace buffer. func NewBufferedWatcher(sz uint) (*Watcher, error) { ev, errs := make(chan Event, sz), make(chan error) b, err := newBackend(ev, errs) if err != nil { return nil, err } return &Watcher{b: b, Events: ev, Errors: errs}, nil } // Add starts monitoring the path for changes. // // A path can only be watched once; watching it more than once is a no-op and will // not return an error. Paths that do not yet exist on the filesystem cannot be // watched. // // A watch will be automatically removed if the watched path is deleted or // renamed. The exception is the Windows backend, which doesn't remove the // watcher on renames. // // Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special // filesystems (/proc, /sys, etc.) generally don't work. // // Returns [ErrClosed] if [Watcher.Close] was called. // // See [Watcher.AddWith] for a version that allows adding options. // // # Watching directories // // All files in a directory are monitored, including new files that are created // after the watcher is started. Subdirectories are not watched (i.e. it's // non-recursive). // // # Watching files // // Watching individual files (rather than directories) is generally not // recommended as many programs (especially editors) update files atomically: it // will write to a temporary file which is then moved to destination, // overwriting the original (or some variant thereof). The watcher on the // original file is now lost, as that no longer exists. // // The upshot of this is that a power failure or crash won't leave a // half-written file. // // Watch the parent directory and use Event.Name to filter out files you're not // interested in. There is an example of this in cmd/fsnotify/file.go. func (w *Watcher) Add(path string) error { return w.b.Add(path) } // AddWith is like [Watcher.Add], but allows adding options. When using Add() // the defaults described below are used. // // Possible options are: // // - [WithBufferSize] sets the buffer size for the Windows backend; no-op on // other platforms. The default is 64K (65536 bytes). func (w *Watcher) AddWith(path string, opts ...addOpt) error { return w.b.AddWith(path, opts...) } // Remove stops monitoring the path for changes. // // Directories are always removed non-recursively. For example, if you added // /tmp/dir and /tmp/dir/subdir then you will need to remove both. // // Removing a path that has not yet been added returns [ErrNonExistentWatch]. // // Returns nil if [Watcher.Close] was called. func (w *Watcher) Remove(path string) error { return w.b.Remove(path) } // Close removes all watches and closes the Events channel. func (w *Watcher) Close() error { return w.b.Close() } // WatchList returns all paths explicitly added with [Watcher.Add] (and are not // yet removed). // // The order is undefined, and may differ per call. Returns nil if // [Watcher.Close] was called. func (w *Watcher) WatchList() []string { return w.b.WatchList() } // Supports reports if all the listed operations are supported by this platform. // // Create, Write, Remove, Rename, and Chmod are always supported. It can only // return false for an Op starting with Unportable. func (w *Watcher) xSupports(op Op) bool { return w.b.xSupports(op) } func (o Op) String() string { var b strings.Builder if o.Has(Create) { b.WriteString("|CREATE") } if o.Has(Remove) { b.WriteString("|REMOVE") } if o.Has(Write) { b.WriteString("|WRITE") } if o.Has(xUnportableOpen) { b.WriteString("|OPEN") } if o.Has(xUnportableRead) { b.WriteString("|READ") } if o.Has(xUnportableCloseWrite) { b.WriteString("|CLOSE_WRITE") } if o.Has(xUnportableCloseRead) { b.WriteString("|CLOSE_READ") } if o.Has(Rename) { b.WriteString("|RENAME") } if o.Has(Chmod) { b.WriteString("|CHMOD") } if b.Len() == 0 { return "[no events]" } return b.String()[1:] } // Has reports if this operation has the given operation. func (o Op) Has(h Op) bool { return o&h != 0 } // Has reports if this event has the given operation. func (e Event) Has(op Op) bool { return e.Op.Has(op) } // String returns a string representation of the event with their path. func (e Event) String() string { if e.renamedFrom != "" { return fmt.Sprintf("%-13s %q ← %q", e.Op.String(), e.Name, e.renamedFrom) } return fmt.Sprintf("%-13s %q", e.Op.String(), e.Name) } type ( backend interface { Add(string) error AddWith(string, ...addOpt) error Remove(string) error WatchList() []string Close() error xSupports(Op) bool } addOpt func(opt *withOpts) withOpts struct { bufsize int op Op noFollow bool sendCreate bool } ) var debug = func() bool { // Check for exactly "1" (rather than mere existence) so we can add // options/flags in the future. I don't know if we ever want that, but it's // nice to leave the option open. return os.Getenv("FSNOTIFY_DEBUG") == "1" }() var defaultOpts = withOpts{ bufsize: 65536, // 64K op: Create | Write | Remove | Rename | Chmod, } func getOptions(opts ...addOpt) withOpts { with := defaultOpts for _, o := range opts { if o != nil { o(&with) } } return with } // WithBufferSize sets the [ReadDirectoryChangesW] buffer size. // // This only has effect on Windows systems, and is a no-op for other backends. // // The default value is 64K (65536 bytes) which is the highest value that works // on all filesystems and should be enough for most applications, but if you // have a large burst of events it may not be enough. You can increase it if // you're hitting "queue or buffer overflow" errors ([ErrEventOverflow]). // // [ReadDirectoryChangesW]: https://learn.microsoft.com/en-gb/windows/win32/api/winbase/nf-winbase-readdirectorychangesw func WithBufferSize(bytes int) addOpt { return func(opt *withOpts) { opt.bufsize = bytes } } // WithOps sets which operations to listen for. The default is [Create], // [Write], [Remove], [Rename], and [Chmod]. // // Excluding operations you're not interested in can save quite a bit of CPU // time; in some use cases there may be hundreds of thousands of useless Write // or Chmod operations per second. // // This can also be used to add unportable operations not supported by all // platforms; unportable operations all start with "Unportable": // [UnportableOpen], [UnportableRead], [UnportableCloseWrite], and // [UnportableCloseRead]. // // AddWith returns an error when using an unportable operation that's not // supported. Use [Watcher.Support] to check for support. func withOps(op Op) addOpt { return func(opt *withOpts) { opt.op = op } } // WithNoFollow disables following symlinks, so the symlinks themselves are // watched. func withNoFollow() addOpt { return func(opt *withOpts) { opt.noFollow = true } } // "Internal" option for recursive watches on inotify. func withCreate() addOpt { return func(opt *withOpts) { opt.sendCreate = true } } var enableRecurse = false // Check if this path is recursive (ends with "/..." or "\..."), and return the // path with the /... stripped. func recursivePath(path string) (string, bool) { path = filepath.Clean(path) if !enableRecurse { // Only enabled in tests for now. return path, false } if filepath.Base(path) == "..." { return filepath.Dir(path), true } return path, false } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/internal/darwin.go ================================================ //go:build darwin package internal import ( "syscall" "golang.org/x/sys/unix" ) var ( ErrSyscallEACCES = syscall.EACCES ErrUnixEACCES = unix.EACCES ) var maxfiles uint64 func SetRlimit() { // Go 1.19 will do this automatically: https://go-review.googlesource.com/c/go/+/393354/ var l syscall.Rlimit err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &l) if err == nil && l.Cur != l.Max { l.Cur = l.Max syscall.Setrlimit(syscall.RLIMIT_NOFILE, &l) } maxfiles = l.Cur if n, err := syscall.SysctlUint32("kern.maxfiles"); err == nil && uint64(n) < maxfiles { maxfiles = uint64(n) } if n, err := syscall.SysctlUint32("kern.maxfilesperproc"); err == nil && uint64(n) < maxfiles { maxfiles = uint64(n) } } func Maxfiles() uint64 { return maxfiles } func Mkfifo(path string, mode uint32) error { return unix.Mkfifo(path, mode) } func Mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, dev) } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/internal/debug_darwin.go ================================================ package internal import "golang.org/x/sys/unix" var names = []struct { n string m uint32 }{ {"NOTE_ABSOLUTE", unix.NOTE_ABSOLUTE}, {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, {"NOTE_BACKGROUND", unix.NOTE_BACKGROUND}, {"NOTE_CHILD", unix.NOTE_CHILD}, {"NOTE_CRITICAL", unix.NOTE_CRITICAL}, {"NOTE_DELETE", unix.NOTE_DELETE}, {"NOTE_EXEC", unix.NOTE_EXEC}, {"NOTE_EXIT", unix.NOTE_EXIT}, {"NOTE_EXITSTATUS", unix.NOTE_EXITSTATUS}, {"NOTE_EXIT_CSERROR", unix.NOTE_EXIT_CSERROR}, {"NOTE_EXIT_DECRYPTFAIL", unix.NOTE_EXIT_DECRYPTFAIL}, {"NOTE_EXIT_DETAIL", unix.NOTE_EXIT_DETAIL}, {"NOTE_EXIT_DETAIL_MASK", unix.NOTE_EXIT_DETAIL_MASK}, {"NOTE_EXIT_MEMORY", unix.NOTE_EXIT_MEMORY}, {"NOTE_EXIT_REPARENTED", unix.NOTE_EXIT_REPARENTED}, {"NOTE_EXTEND", unix.NOTE_EXTEND}, {"NOTE_FFAND", unix.NOTE_FFAND}, {"NOTE_FFCOPY", unix.NOTE_FFCOPY}, {"NOTE_FFCTRLMASK", unix.NOTE_FFCTRLMASK}, {"NOTE_FFLAGSMASK", unix.NOTE_FFLAGSMASK}, {"NOTE_FFNOP", unix.NOTE_FFNOP}, {"NOTE_FFOR", unix.NOTE_FFOR}, {"NOTE_FORK", unix.NOTE_FORK}, {"NOTE_FUNLOCK", unix.NOTE_FUNLOCK}, {"NOTE_LEEWAY", unix.NOTE_LEEWAY}, {"NOTE_LINK", unix.NOTE_LINK}, {"NOTE_LOWAT", unix.NOTE_LOWAT}, {"NOTE_MACHTIME", unix.NOTE_MACHTIME}, {"NOTE_MACH_CONTINUOUS_TIME", unix.NOTE_MACH_CONTINUOUS_TIME}, {"NOTE_NONE", unix.NOTE_NONE}, {"NOTE_NSECONDS", unix.NOTE_NSECONDS}, {"NOTE_OOB", unix.NOTE_OOB}, //{"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, -0x100000 (?!) {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK}, {"NOTE_REAP", unix.NOTE_REAP}, {"NOTE_RENAME", unix.NOTE_RENAME}, {"NOTE_REVOKE", unix.NOTE_REVOKE}, {"NOTE_SECONDS", unix.NOTE_SECONDS}, {"NOTE_SIGNAL", unix.NOTE_SIGNAL}, {"NOTE_TRACK", unix.NOTE_TRACK}, {"NOTE_TRACKERR", unix.NOTE_TRACKERR}, {"NOTE_TRIGGER", unix.NOTE_TRIGGER}, {"NOTE_USECONDS", unix.NOTE_USECONDS}, {"NOTE_VM_ERROR", unix.NOTE_VM_ERROR}, {"NOTE_VM_PRESSURE", unix.NOTE_VM_PRESSURE}, {"NOTE_VM_PRESSURE_SUDDEN_TERMINATE", unix.NOTE_VM_PRESSURE_SUDDEN_TERMINATE}, {"NOTE_VM_PRESSURE_TERMINATE", unix.NOTE_VM_PRESSURE_TERMINATE}, {"NOTE_WRITE", unix.NOTE_WRITE}, } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/internal/debug_dragonfly.go ================================================ package internal import "golang.org/x/sys/unix" var names = []struct { n string m uint32 }{ {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, {"NOTE_CHILD", unix.NOTE_CHILD}, {"NOTE_DELETE", unix.NOTE_DELETE}, {"NOTE_EXEC", unix.NOTE_EXEC}, {"NOTE_EXIT", unix.NOTE_EXIT}, {"NOTE_EXTEND", unix.NOTE_EXTEND}, {"NOTE_FFAND", unix.NOTE_FFAND}, {"NOTE_FFCOPY", unix.NOTE_FFCOPY}, {"NOTE_FFCTRLMASK", unix.NOTE_FFCTRLMASK}, {"NOTE_FFLAGSMASK", unix.NOTE_FFLAGSMASK}, {"NOTE_FFNOP", unix.NOTE_FFNOP}, {"NOTE_FFOR", unix.NOTE_FFOR}, {"NOTE_FORK", unix.NOTE_FORK}, {"NOTE_LINK", unix.NOTE_LINK}, {"NOTE_LOWAT", unix.NOTE_LOWAT}, {"NOTE_OOB", unix.NOTE_OOB}, {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK}, {"NOTE_RENAME", unix.NOTE_RENAME}, {"NOTE_REVOKE", unix.NOTE_REVOKE}, {"NOTE_TRACK", unix.NOTE_TRACK}, {"NOTE_TRACKERR", unix.NOTE_TRACKERR}, {"NOTE_TRIGGER", unix.NOTE_TRIGGER}, {"NOTE_WRITE", unix.NOTE_WRITE}, } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/internal/debug_freebsd.go ================================================ package internal import "golang.org/x/sys/unix" var names = []struct { n string m uint32 }{ {"NOTE_ABSTIME", unix.NOTE_ABSTIME}, {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, {"NOTE_CHILD", unix.NOTE_CHILD}, {"NOTE_CLOSE", unix.NOTE_CLOSE}, {"NOTE_CLOSE_WRITE", unix.NOTE_CLOSE_WRITE}, {"NOTE_DELETE", unix.NOTE_DELETE}, {"NOTE_EXEC", unix.NOTE_EXEC}, {"NOTE_EXIT", unix.NOTE_EXIT}, {"NOTE_EXTEND", unix.NOTE_EXTEND}, {"NOTE_FFAND", unix.NOTE_FFAND}, {"NOTE_FFCOPY", unix.NOTE_FFCOPY}, {"NOTE_FFCTRLMASK", unix.NOTE_FFCTRLMASK}, {"NOTE_FFLAGSMASK", unix.NOTE_FFLAGSMASK}, {"NOTE_FFNOP", unix.NOTE_FFNOP}, {"NOTE_FFOR", unix.NOTE_FFOR}, {"NOTE_FILE_POLL", unix.NOTE_FILE_POLL}, {"NOTE_FORK", unix.NOTE_FORK}, {"NOTE_LINK", unix.NOTE_LINK}, {"NOTE_LOWAT", unix.NOTE_LOWAT}, {"NOTE_MSECONDS", unix.NOTE_MSECONDS}, {"NOTE_NSECONDS", unix.NOTE_NSECONDS}, {"NOTE_OPEN", unix.NOTE_OPEN}, {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK}, {"NOTE_READ", unix.NOTE_READ}, {"NOTE_RENAME", unix.NOTE_RENAME}, {"NOTE_REVOKE", unix.NOTE_REVOKE}, {"NOTE_SECONDS", unix.NOTE_SECONDS}, {"NOTE_TRACK", unix.NOTE_TRACK}, {"NOTE_TRACKERR", unix.NOTE_TRACKERR}, {"NOTE_TRIGGER", unix.NOTE_TRIGGER}, {"NOTE_USECONDS", unix.NOTE_USECONDS}, {"NOTE_WRITE", unix.NOTE_WRITE}, } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/internal/debug_kqueue.go ================================================ //go:build freebsd || openbsd || netbsd || dragonfly || darwin package internal import ( "fmt" "os" "strings" "time" "golang.org/x/sys/unix" ) func Debug(name string, kevent *unix.Kevent_t) { mask := uint32(kevent.Fflags) var ( l []string unknown = mask ) for _, n := range names { if mask&n.m == n.m { l = append(l, n.n) unknown ^= n.m } } if unknown > 0 { l = append(l, fmt.Sprintf("0x%x", unknown)) } fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s %10d:%-60s → %q\n", time.Now().Format("15:04:05.000000000"), mask, strings.Join(l, " | "), name) } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/internal/debug_linux.go ================================================ package internal import ( "fmt" "os" "strings" "time" "golang.org/x/sys/unix" ) func Debug(name string, mask, cookie uint32) { names := []struct { n string m uint32 }{ {"IN_ACCESS", unix.IN_ACCESS}, {"IN_ATTRIB", unix.IN_ATTRIB}, {"IN_CLOSE", unix.IN_CLOSE}, {"IN_CLOSE_NOWRITE", unix.IN_CLOSE_NOWRITE}, {"IN_CLOSE_WRITE", unix.IN_CLOSE_WRITE}, {"IN_CREATE", unix.IN_CREATE}, {"IN_DELETE", unix.IN_DELETE}, {"IN_DELETE_SELF", unix.IN_DELETE_SELF}, {"IN_IGNORED", unix.IN_IGNORED}, {"IN_ISDIR", unix.IN_ISDIR}, {"IN_MODIFY", unix.IN_MODIFY}, {"IN_MOVE", unix.IN_MOVE}, {"IN_MOVED_FROM", unix.IN_MOVED_FROM}, {"IN_MOVED_TO", unix.IN_MOVED_TO}, {"IN_MOVE_SELF", unix.IN_MOVE_SELF}, {"IN_OPEN", unix.IN_OPEN}, {"IN_Q_OVERFLOW", unix.IN_Q_OVERFLOW}, {"IN_UNMOUNT", unix.IN_UNMOUNT}, } var ( l []string unknown = mask ) for _, n := range names { if mask&n.m == n.m { l = append(l, n.n) unknown ^= n.m } } if unknown > 0 { l = append(l, fmt.Sprintf("0x%x", unknown)) } var c string if cookie > 0 { c = fmt.Sprintf("(cookie: %d) ", cookie) } fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s %-30s → %s%q\n", time.Now().Format("15:04:05.000000000"), strings.Join(l, "|"), c, name) } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/internal/debug_netbsd.go ================================================ package internal import "golang.org/x/sys/unix" var names = []struct { n string m uint32 }{ {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, {"NOTE_CHILD", unix.NOTE_CHILD}, {"NOTE_DELETE", unix.NOTE_DELETE}, {"NOTE_EXEC", unix.NOTE_EXEC}, {"NOTE_EXIT", unix.NOTE_EXIT}, {"NOTE_EXTEND", unix.NOTE_EXTEND}, {"NOTE_FORK", unix.NOTE_FORK}, {"NOTE_LINK", unix.NOTE_LINK}, {"NOTE_LOWAT", unix.NOTE_LOWAT}, {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK}, {"NOTE_RENAME", unix.NOTE_RENAME}, {"NOTE_REVOKE", unix.NOTE_REVOKE}, {"NOTE_TRACK", unix.NOTE_TRACK}, {"NOTE_TRACKERR", unix.NOTE_TRACKERR}, {"NOTE_WRITE", unix.NOTE_WRITE}, } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/internal/debug_openbsd.go ================================================ package internal import "golang.org/x/sys/unix" var names = []struct { n string m uint32 }{ {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, // {"NOTE_CHANGE", unix.NOTE_CHANGE}, // Not on 386? {"NOTE_CHILD", unix.NOTE_CHILD}, {"NOTE_DELETE", unix.NOTE_DELETE}, {"NOTE_EOF", unix.NOTE_EOF}, {"NOTE_EXEC", unix.NOTE_EXEC}, {"NOTE_EXIT", unix.NOTE_EXIT}, {"NOTE_EXTEND", unix.NOTE_EXTEND}, {"NOTE_FORK", unix.NOTE_FORK}, {"NOTE_LINK", unix.NOTE_LINK}, {"NOTE_LOWAT", unix.NOTE_LOWAT}, {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK}, {"NOTE_RENAME", unix.NOTE_RENAME}, {"NOTE_REVOKE", unix.NOTE_REVOKE}, {"NOTE_TRACK", unix.NOTE_TRACK}, {"NOTE_TRACKERR", unix.NOTE_TRACKERR}, {"NOTE_TRUNCATE", unix.NOTE_TRUNCATE}, {"NOTE_WRITE", unix.NOTE_WRITE}, } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/internal/debug_solaris.go ================================================ package internal import ( "fmt" "os" "strings" "time" "golang.org/x/sys/unix" ) func Debug(name string, mask int32) { names := []struct { n string m int32 }{ {"FILE_ACCESS", unix.FILE_ACCESS}, {"FILE_MODIFIED", unix.FILE_MODIFIED}, {"FILE_ATTRIB", unix.FILE_ATTRIB}, {"FILE_TRUNC", unix.FILE_TRUNC}, {"FILE_NOFOLLOW", unix.FILE_NOFOLLOW}, {"FILE_DELETE", unix.FILE_DELETE}, {"FILE_RENAME_TO", unix.FILE_RENAME_TO}, {"FILE_RENAME_FROM", unix.FILE_RENAME_FROM}, {"UNMOUNTED", unix.UNMOUNTED}, {"MOUNTEDOVER", unix.MOUNTEDOVER}, {"FILE_EXCEPTION", unix.FILE_EXCEPTION}, } var ( l []string unknown = mask ) for _, n := range names { if mask&n.m == n.m { l = append(l, n.n) unknown ^= n.m } } if unknown > 0 { l = append(l, fmt.Sprintf("0x%x", unknown)) } fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s %10d:%-30s → %q\n", time.Now().Format("15:04:05.000000000"), mask, strings.Join(l, " | "), name) } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/internal/debug_windows.go ================================================ package internal import ( "fmt" "os" "path/filepath" "strings" "time" "golang.org/x/sys/windows" ) func Debug(name string, mask uint32) { names := []struct { n string m uint32 }{ {"FILE_ACTION_ADDED", windows.FILE_ACTION_ADDED}, {"FILE_ACTION_REMOVED", windows.FILE_ACTION_REMOVED}, {"FILE_ACTION_MODIFIED", windows.FILE_ACTION_MODIFIED}, {"FILE_ACTION_RENAMED_OLD_NAME", windows.FILE_ACTION_RENAMED_OLD_NAME}, {"FILE_ACTION_RENAMED_NEW_NAME", windows.FILE_ACTION_RENAMED_NEW_NAME}, } var ( l []string unknown = mask ) for _, n := range names { if mask&n.m == n.m { l = append(l, n.n) unknown ^= n.m } } if unknown > 0 { l = append(l, fmt.Sprintf("0x%x", unknown)) } fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s %-65s → %q\n", time.Now().Format("15:04:05.000000000"), strings.Join(l, " | "), filepath.ToSlash(name)) } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/internal/freebsd.go ================================================ //go:build freebsd package internal import ( "syscall" "golang.org/x/sys/unix" ) var ( ErrSyscallEACCES = syscall.EACCES ErrUnixEACCES = unix.EACCES ) var maxfiles uint64 func SetRlimit() { // Go 1.19 will do this automatically: https://go-review.googlesource.com/c/go/+/393354/ var l syscall.Rlimit err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &l) if err == nil && l.Cur != l.Max { l.Cur = l.Max syscall.Setrlimit(syscall.RLIMIT_NOFILE, &l) } maxfiles = uint64(l.Cur) } func Maxfiles() uint64 { return maxfiles } func Mkfifo(path string, mode uint32) error { return unix.Mkfifo(path, mode) } func Mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, uint64(dev)) } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/internal/internal.go ================================================ // Package internal contains some helpers. package internal ================================================ FILE: vendor/github.com/fsnotify/fsnotify/internal/unix.go ================================================ //go:build !windows && !darwin && !freebsd && !plan9 package internal import ( "syscall" "golang.org/x/sys/unix" ) var ( ErrSyscallEACCES = syscall.EACCES ErrUnixEACCES = unix.EACCES ) var maxfiles uint64 func SetRlimit() { // Go 1.19 will do this automatically: https://go-review.googlesource.com/c/go/+/393354/ var l syscall.Rlimit err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &l) if err == nil && l.Cur != l.Max { l.Cur = l.Max syscall.Setrlimit(syscall.RLIMIT_NOFILE, &l) } maxfiles = uint64(l.Cur) } func Maxfiles() uint64 { return maxfiles } func Mkfifo(path string, mode uint32) error { return unix.Mkfifo(path, mode) } func Mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, dev) } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/internal/unix2.go ================================================ //go:build !windows package internal func HasPrivilegesForSymlink() bool { return true } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/internal/windows.go ================================================ //go:build windows package internal import ( "errors" "golang.org/x/sys/windows" ) // Just a dummy. var ( ErrSyscallEACCES = errors.New("dummy") ErrUnixEACCES = errors.New("dummy") ) func SetRlimit() {} func Maxfiles() uint64 { return 1<<64 - 1 } func Mkfifo(path string, mode uint32) error { return errors.New("no FIFOs on Windows") } func Mknod(path string, mode uint32, dev int) error { return errors.New("no device nodes on Windows") } func HasPrivilegesForSymlink() bool { var sid *windows.SID err := windows.AllocateAndInitializeSid( &windows.SECURITY_NT_AUTHORITY, 2, windows.SECURITY_BUILTIN_DOMAIN_RID, windows.DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &sid) if err != nil { return false } defer windows.FreeSid(sid) token := windows.Token(0) member, err := token.IsMember(sid) if err != nil { return false } return member || token.IsElevated() } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/shared.go ================================================ package fsnotify import "sync" type shared struct { Events chan Event Errors chan error done chan struct{} mu sync.Mutex } func newShared(ev chan Event, errs chan error) *shared { return &shared{ Events: ev, Errors: errs, done: make(chan struct{}), } } // Returns true if the event was sent, or false if watcher is closed. func (w *shared) sendEvent(e Event) bool { if e.Op == 0 { return true } select { case <-w.done: return false case w.Events <- e: return true } } // Returns true if the error was sent, or false if watcher is closed. func (w *shared) sendError(err error) bool { if err == nil { return true } select { case <-w.done: return false case w.Errors <- err: return true } } func (w *shared) isClosed() bool { select { case <-w.done: return true default: return false } } // Mark as closed; returns true if it was already closed. func (w *shared) close() bool { w.mu.Lock() defer w.mu.Unlock() if w.isClosed() { return true } close(w.done) return false } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/staticcheck.conf ================================================ checks = ['all', '-U1000', # Don't complain about unused functions. ] ================================================ FILE: vendor/github.com/fsnotify/fsnotify/system_bsd.go ================================================ //go:build freebsd || openbsd || netbsd || dragonfly package fsnotify import "golang.org/x/sys/unix" const openMode = unix.O_NONBLOCK | unix.O_RDONLY | unix.O_CLOEXEC ================================================ FILE: vendor/github.com/fsnotify/fsnotify/system_darwin.go ================================================ //go:build darwin package fsnotify import "golang.org/x/sys/unix" // note: this constant is not defined on BSD const openMode = unix.O_EVTONLY | unix.O_CLOEXEC ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/.gitattributes ================================================ * text=auto eol=lf ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/.gitignore ================================================ # temporary symlink for testing testing/data/symlink ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/.golangci.yaml ================================================ version: "2" linters: default: none exclusions: generated: lax presets: - comments - common-false-positives - legacy - std-error-handling paths: - third_party$ - builtin$ - examples$ formatters: enable: - gofumpt - goimports exclusions: generated: lax paths: - third_party$ - builtin$ - examples$ ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/DOCKER-LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ You can find the Docker license at the following link: https://raw.githubusercontent.com/docker/docker/HEAD/LICENSE ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/LICENSE ================================================ Copyright (c) go-dockerclient authors All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/Makefile ================================================ ifeq "$(strip $(shell go env GOARCH))" "amd64" RACE_FLAG := -race endif .PHONY: test test: pretest gotest .PHONY: golangci-lint golangci-lint: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest golangci-lint run .PHONY: staticcheck staticcheck: go install honnef.co/go/tools/cmd/staticcheck@master staticcheck ./... .PHONY: lint lint: golangci-lint staticcheck .PHONY: pretest pretest: lint .PHONY: gotest gotest: go test $(RACE_FLAG) -vet all ./... .PHONY: integration integration: go test -tags docker_integration -run TestIntegration -v ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/README.md ================================================ # go-dockerclient [![Build Status](https://github.com/fsouza/go-dockerclient/workflows/Build/badge.svg)](https://github.com/fsouza/go-dockerclient/actions?query=branch:main+workflow:Build) [![GoDoc](https://img.shields.io/badge/api-Godoc-blue.svg?style=flat-square)](https://pkg.go.dev/github.com/fsouza/go-dockerclient) This package presents a client for the Docker remote API. It also provides support for the extensions in the [Swarm API](https://docs.docker.com/swarm/swarm-api/). This package also provides support for docker's network API, which is a simple passthrough to the libnetwork remote API. For more details, check the [remote API documentation](https://docs.docker.com/engine/api/latest/). ## Difference between go-dockerclient and the official SDK Link for the official SDK: https://docs.docker.com/develop/sdk/ go-dockerclient was created before Docker had an official Go SDK and is still maintained and active because it's still used out there. New features in the Docker API do not get automatically implemented here: it's based on demand, if someone wants it, they can file an issue or a PR and the feature may get implemented/merged. For new projects, using the official SDK is probably more appropriate as go-dockerclient lags behind the official SDK. ## Example ```go package main import ( "fmt" docker "github.com/fsouza/go-dockerclient" ) func main() { client, err := docker.NewClientFromEnv() if err != nil { panic(err) } imgs, err := client.ListImages(docker.ListImagesOptions{All: false}) if err != nil { panic(err) } for _, img := range imgs { fmt.Println("ID: ", img.ID) fmt.Println("RepoTags: ", img.RepoTags) fmt.Println("Created: ", img.Created) fmt.Println("Size: ", img.Size) fmt.Println("VirtualSize: ", img.VirtualSize) fmt.Println("ParentId: ", img.ParentID) } } ``` ## Using with TLS In order to instantiate the client for a TLS-enabled daemon, you should use NewTLSClient, passing the endpoint and path for key and certificates as parameters. ```go package main import ( "fmt" docker "github.com/fsouza/go-dockerclient" ) func main() { const endpoint = "tcp://[ip]:[port]" path := os.Getenv("DOCKER_CERT_PATH") ca := fmt.Sprintf("%s/ca.pem", path) cert := fmt.Sprintf("%s/cert.pem", path) key := fmt.Sprintf("%s/key.pem", path) client, _ := docker.NewTLSClient(endpoint, cert, key, ca) // use client } ``` If using [docker-machine](https://docs.docker.com/machine/), or another application that exports environment variables `DOCKER_HOST`, `DOCKER_TLS_VERIFY`, `DOCKER_CERT_PATH`, `DOCKER_API_VERSION`, you can use NewClientFromEnv. ```go package main import ( "fmt" docker "github.com/fsouza/go-dockerclient" ) func main() { client, err := docker.NewClientFromEnv() if err != nil { // handle err } // use client } ``` See the documentation for more details. ## Developing All development commands can be seen in the [Makefile](Makefile). Committed code must pass: * [golangci-lint](https://github.com/golangci/golangci-lint) * [go test](https://golang.org/cmd/go/#hdr-Test_packages) * [staticcheck](https://staticcheck.io/) Running ``make test`` will run all checks, as well as install any required dependencies. ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/auth.go ================================================ // Copyright 2015 go-dockerclient authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package docker import ( "bytes" "context" "encoding/base64" "encoding/json" "errors" "io" "net/http" "os" "os/exec" "path" "strings" ) // ErrCannotParseDockercfg is the error returned by NewAuthConfigurations when the dockercfg cannot be parsed. var ErrCannotParseDockercfg = errors.New("failed to read authentication from dockercfg") // AuthConfiguration represents authentication options to use in the PushImage // method. It represents the authentication in the Docker index server. type AuthConfiguration struct { Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` Email string `json:"email,omitempty"` ServerAddress string `json:"serveraddress,omitempty"` // IdentityToken can be supplied with the identitytoken response of the AuthCheck call // see https://pkg.go.dev/github.com/docker/docker/api/types?tab=doc#AuthConfig // It can be used in place of password not in conjunction with it IdentityToken string `json:"identitytoken,omitempty"` // RegistryToken can be supplied with the registrytoken RegistryToken string `json:"registrytoken,omitempty"` } func (c AuthConfiguration) isEmpty() bool { return c == AuthConfiguration{} } func (c AuthConfiguration) headerKey() string { return "X-Registry-Auth" } // AuthConfigurations represents authentication options to use for the // PushImage method accommodating the new X-Registry-Config header type AuthConfigurations struct { Configs map[string]AuthConfiguration `json:"configs"` } func (c AuthConfigurations) isEmpty() bool { return len(c.Configs) == 0 } func (AuthConfigurations) headerKey() string { return "X-Registry-Config" } // merge updates the configuration. If a key is defined in both maps, the one // in c.Configs takes precedence. func (c *AuthConfigurations) merge(other AuthConfigurations) { for k, v := range other.Configs { if c.Configs == nil { c.Configs = make(map[string]AuthConfiguration) } if _, ok := c.Configs[k]; !ok { c.Configs[k] = v } } } // AuthConfigurations119 is used to serialize a set of AuthConfigurations // for Docker API >= 1.19. type AuthConfigurations119 map[string]AuthConfiguration func (c AuthConfigurations119) isEmpty() bool { return len(c) == 0 } func (c AuthConfigurations119) headerKey() string { return "X-Registry-Config" } // dockerConfig represents a registry authentation configuration from the // .dockercfg file. type dockerConfig struct { Auth string `json:"auth"` Email string `json:"email"` IdentityToken string `json:"identitytoken"` RegistryToken string `json:"registrytoken"` } // NewAuthConfigurationsFromFile returns AuthConfigurations from a path containing JSON // in the same format as the .dockercfg file. func NewAuthConfigurationsFromFile(path string) (*AuthConfigurations, error) { r, err := os.Open(path) if err != nil { return nil, err } return NewAuthConfigurations(r) } func cfgPaths(dockerConfigEnv string, homeEnv string) []string { if dockerConfigEnv != "" { return []string{ path.Join(dockerConfigEnv, "plaintext-passwords.json"), path.Join(dockerConfigEnv, "config.json"), } } if homeEnv != "" { return []string{ path.Join(homeEnv, ".docker", "plaintext-passwords.json"), path.Join(homeEnv, ".docker", "config.json"), path.Join(homeEnv, ".dockercfg"), } } return nil } // NewAuthConfigurationsFromDockerCfg returns AuthConfigurations from system // config files. The following files are checked in the order listed: // // If the environment variable DOCKER_CONFIG is set to a non-empty string: // // - $DOCKER_CONFIG/plaintext-passwords.json // - $DOCKER_CONFIG/config.json // // Otherwise, it looks for files in the $HOME directory and the legacy // location: // // - $HOME/.docker/plaintext-passwords.json // - $HOME/.docker/config.json // - $HOME/.dockercfg func NewAuthConfigurationsFromDockerCfg() (*AuthConfigurations, error) { pathsToTry := cfgPaths(os.Getenv("DOCKER_CONFIG"), os.Getenv("HOME")) if len(pathsToTry) < 1 { return nil, errors.New("no docker configuration found") } return newAuthConfigurationsFromDockerCfg(pathsToTry) } func newAuthConfigurationsFromDockerCfg(pathsToTry []string) (*AuthConfigurations, error) { var result *AuthConfigurations var auths *AuthConfigurations var err error for _, path := range pathsToTry { auths, err = NewAuthConfigurationsFromFile(path) if err != nil { continue } if result == nil { result = auths } else { result.merge(*auths) } } if result != nil { return result, nil } return result, err } // NewAuthConfigurations returns AuthConfigurations from a JSON encoded string in the // same format as the .dockercfg file. func NewAuthConfigurations(r io.Reader) (*AuthConfigurations, error) { var auth *AuthConfigurations confs, err := parseDockerConfig(r) if err != nil { return nil, err } auth, err = authConfigs(confs) if err != nil { return nil, err } return auth, nil } func parseDockerConfig(r io.Reader) (map[string]dockerConfig, error) { buf := new(bytes.Buffer) buf.ReadFrom(r) byteData := buf.Bytes() confsWrapper := struct { Auths map[string]dockerConfig `json:"auths"` }{} if err := json.Unmarshal(byteData, &confsWrapper); err == nil { if len(confsWrapper.Auths) > 0 { return confsWrapper.Auths, nil } } var confs map[string]dockerConfig if err := json.Unmarshal(byteData, &confs); err != nil { return nil, err } return confs, nil } // authConfigs converts a dockerConfigs map to a AuthConfigurations object. func authConfigs(confs map[string]dockerConfig) (*AuthConfigurations, error) { c := &AuthConfigurations{ Configs: make(map[string]AuthConfiguration), } for reg, conf := range confs { if conf.Auth == "" { continue } // support both padded and unpadded encoding data, err := base64.StdEncoding.DecodeString(conf.Auth) if err != nil { data, err = base64.StdEncoding.WithPadding(base64.NoPadding).DecodeString(conf.Auth) } if err != nil { return nil, errors.New("error decoding plaintext credentials") } userpass := strings.SplitN(string(data), ":", 2) if len(userpass) != 2 { return nil, ErrCannotParseDockercfg } authConfig := AuthConfiguration{ Email: conf.Email, Username: userpass[0], Password: userpass[1], ServerAddress: reg, } // if identitytoken provided then zero the password and set it if conf.IdentityToken != "" { authConfig.Password = "" authConfig.IdentityToken = conf.IdentityToken } // if registrytoken provided then zero the password and set it if conf.RegistryToken != "" { authConfig.Password = "" authConfig.RegistryToken = conf.RegistryToken } c.Configs[reg] = authConfig } return c, nil } // AuthStatus returns the authentication status for Docker API versions >= 1.23. type AuthStatus struct { Status string `json:"Status,omitempty" yaml:"Status,omitempty" toml:"Status,omitempty"` IdentityToken string `json:"IdentityToken,omitempty" yaml:"IdentityToken,omitempty" toml:"IdentityToken,omitempty"` } // AuthCheck validates the given credentials. It returns nil if successful. // // For Docker API versions >= 1.23, the AuthStatus struct will be populated, otherwise it will be empty.` // // See https://goo.gl/6nsZkH for more details. func (c *Client) AuthCheck(conf *AuthConfiguration) (AuthStatus, error) { return c.AuthCheckWithContext(conf, context.TODO()) } // AuthCheckWithContext validates the given credentials. It returns nil if successful. The context object // can be used to cancel the request. // // For Docker API versions >= 1.23, the AuthStatus struct will be populated, otherwise it will be empty. // // See https://goo.gl/6nsZkH for more details. func (c *Client) AuthCheckWithContext(conf *AuthConfiguration, ctx context.Context) (AuthStatus, error) { var authStatus AuthStatus if conf == nil { return authStatus, errors.New("conf is nil") } resp, err := c.do(http.MethodPost, "/auth", doOptions{data: conf, context: ctx}) if err != nil { return authStatus, err } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { return authStatus, err } if len(data) == 0 { return authStatus, nil } if err := json.Unmarshal(data, &authStatus); err != nil { return authStatus, err } return authStatus, nil } // helperCredentials represents credentials commit from an helper type helperCredentials struct { Username string `json:"Username,omitempty"` Secret string `json:"Secret,omitempty"` } // NewAuthConfigurationsFromCredsHelpers returns AuthConfigurations from // installed credentials helpers func NewAuthConfigurationsFromCredsHelpers(registry string) (*AuthConfiguration, error) { // Load docker configuration file in order to find a possible helper provider pathsToTry := cfgPaths(os.Getenv("DOCKER_CONFIG"), os.Getenv("HOME")) if len(pathsToTry) < 1 { return nil, errors.New("no docker configuration found") } provider, err := getHelperProviderFromDockerCfg(pathsToTry, registry) if err != nil { return nil, err } c, err := getCredentialsFromHelper(provider, registry) if err != nil { return nil, err } creds := new(AuthConfiguration) creds.Username = c.Username creds.Password = c.Secret return creds, nil } func getHelperProviderFromDockerCfg(pathsToTry []string, registry string) (string, error) { for _, path := range pathsToTry { content, err := os.ReadFile(path) if err != nil { // if we can't read the file keep going continue } provider, err := parseCredsDockerConfig(content, registry) if err != nil { continue } if provider != "" { return provider, nil } } return "", errors.New("no docker credentials provider found") } func parseCredsDockerConfig(config []byte, registry string) (string, error) { creds := struct { CredsStore string `json:"credsStore,omitempty"` CredHelpers map[string]string `json:"credHelpers,omitempty"` }{} err := json.Unmarshal(config, &creds) if err != nil { return "", err } provider, ok := creds.CredHelpers[registry] if ok { return provider, nil } return creds.CredsStore, nil } // Run and parse the found credential helper func getCredentialsFromHelper(provider string, registry string) (*helperCredentials, error) { helpercreds, err := runDockerCredentialsHelper(provider, registry) if err != nil { return nil, err } c := new(helperCredentials) err = json.Unmarshal(helpercreds, c) if err != nil { return nil, err } return c, nil } func runDockerCredentialsHelper(provider string, registry string) ([]byte, error) { cmd := exec.Command("docker-credential-"+provider, "get") var stdout bytes.Buffer cmd.Stdin = bytes.NewBuffer([]byte(registry)) cmd.Stdout = &stdout err := cmd.Run() if err != nil { return nil, err } return stdout.Bytes(), nil } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/change.go ================================================ // Copyright 2014 go-dockerclient authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package docker import "fmt" // ChangeType is a type for constants indicating the type of change // in a container type ChangeType int const ( // ChangeModify is the ChangeType for container modifications ChangeModify ChangeType = iota // ChangeAdd is the ChangeType for additions to a container ChangeAdd // ChangeDelete is the ChangeType for deletions from a container ChangeDelete ) // Change represents a change in a container. // // See https://goo.gl/Wo0JJp for more details. type Change struct { Path string Kind ChangeType } func (change *Change) String() string { var kind string switch change.Kind { case ChangeModify: kind = "C" case ChangeAdd: kind = "A" case ChangeDelete: kind = "D" } return fmt.Sprintf("%s %s", kind, change.Path) } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/client.go ================================================ // Copyright 2013 go-dockerclient authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package docker provides a client for the Docker remote API. // // See https://goo.gl/o2v3rk for more details on the remote API. package docker import ( "bufio" "bytes" "context" "crypto/tls" "crypto/x509" "encoding/json" "errors" "fmt" "io" "net" "net/http" "net/http/httputil" "net/url" "os" "path/filepath" "reflect" "runtime" "strconv" "strings" "sync/atomic" "time" "github.com/moby/moby/api/pkg/stdcopy" "github.com/moby/moby/client/pkg/jsonmessage" "github.com/moby/moby/v2/pkg/homedir" ) const ( userAgent = "go-dockerclient" unixProtocol = "unix" namedPipeProtocol = "npipe" ) var ( // ErrInvalidEndpoint is returned when the endpoint is not a valid HTTP URL. ErrInvalidEndpoint = errors.New("invalid endpoint") // ErrConnectionRefused is returned when the client cannot connect to the given endpoint. ErrConnectionRefused = errors.New("cannot connect to Docker endpoint") // ErrInactivityTimeout is returned when a streamable call has been inactive for some time. ErrInactivityTimeout = errors.New("inactivity time exceeded timeout") apiVersion112, _ = NewAPIVersion("1.12") apiVersion118, _ = NewAPIVersion("1.18") apiVersion119, _ = NewAPIVersion("1.19") apiVersion121, _ = NewAPIVersion("1.21") apiVersion124, _ = NewAPIVersion("1.24") apiVersion125, _ = NewAPIVersion("1.25") apiVersion135, _ = NewAPIVersion("1.35") ) // APIVersion is an internal representation of a version of the Remote API. type APIVersion []int // NewAPIVersion returns an instance of APIVersion for the given string. // // The given string must be in the form .., where , // and are integer numbers. func NewAPIVersion(input string) (APIVersion, error) { if !strings.Contains(input, ".") { return nil, fmt.Errorf("unable to parse version %q", input) } raw := strings.Split(input, "-") arr := strings.Split(raw[0], ".") ret := make(APIVersion, len(arr)) var err error for i, val := range arr { ret[i], err = strconv.Atoi(val) if err != nil { return nil, fmt.Errorf("unable to parse version %q: %q is not an integer", input, val) } } return ret, nil } func (version APIVersion) String() string { parts := make([]string, len(version)) for i, val := range version { parts[i] = strconv.Itoa(val) } return strings.Join(parts, ".") } // LessThan is a function for comparing APIVersion structs. func (version APIVersion) LessThan(other APIVersion) bool { return version.compare(other) < 0 } // LessThanOrEqualTo is a function for comparing APIVersion structs. func (version APIVersion) LessThanOrEqualTo(other APIVersion) bool { return version.compare(other) <= 0 } // GreaterThan is a function for comparing APIVersion structs. func (version APIVersion) GreaterThan(other APIVersion) bool { return version.compare(other) > 0 } // GreaterThanOrEqualTo is a function for comparing APIVersion structs. func (version APIVersion) GreaterThanOrEqualTo(other APIVersion) bool { return version.compare(other) >= 0 } func (version APIVersion) compare(other APIVersion) int { for i, v := range version { if i <= len(other)-1 { otherVersion := other[i] if v < otherVersion { return -1 } else if v > otherVersion { return 1 } } } if len(version) > len(other) { return 1 } if len(version) < len(other) { return -1 } return 0 } // Client is the basic type of this package. It provides methods for // interaction with the API. type Client struct { SkipServerVersionCheck bool HTTPClient *http.Client TLSConfig *tls.Config Dialer Dialer endpoint string endpointURL *url.URL eventMonitor *eventMonitoringState requestedAPIVersion APIVersion serverAPIVersion APIVersion expectedAPIVersion APIVersion } // Dialer is an interface that allows network connections to be dialed // (net.Dialer fulfills this interface) and named pipes (a shim using // winio.DialPipe) type Dialer interface { Dial(network, address string) (net.Conn, error) } // NewClient returns a Client instance ready for communication with the given // server endpoint. It will use the latest remote API version available in the // server. func NewClient(endpoint string) (*Client, error) { client, err := NewVersionedClient(endpoint, "") if err != nil { return nil, err } client.SkipServerVersionCheck = true return client, nil } // NewTLSClient returns a Client instance ready for TLS communications with the givens // server endpoint, key and certificates . It will use the latest remote API version // available in the server. func NewTLSClient(endpoint string, cert, key, ca string) (*Client, error) { client, err := NewVersionedTLSClient(endpoint, cert, key, ca, "") if err != nil { return nil, err } client.SkipServerVersionCheck = true return client, nil } // NewTLSClientFromBytes returns a Client instance ready for TLS communications with the givens // server endpoint, key and certificates (passed inline to the function as opposed to being // read from a local file). It will use the latest remote API version available in the server. func NewTLSClientFromBytes(endpoint string, certPEMBlock, keyPEMBlock, caPEMCert []byte) (*Client, error) { client, err := NewVersionedTLSClientFromBytes(endpoint, certPEMBlock, keyPEMBlock, caPEMCert, "") if err != nil { return nil, err } client.SkipServerVersionCheck = true return client, nil } // NewVersionedClient returns a Client instance ready for communication with // the given server endpoint, using a specific remote API version. func NewVersionedClient(endpoint string, apiVersionString string) (*Client, error) { u, err := parseEndpoint(endpoint, false) if err != nil { return nil, err } var requestedAPIVersion APIVersion if strings.Contains(apiVersionString, ".") { requestedAPIVersion, err = NewAPIVersion(apiVersionString) if err != nil { return nil, err } } c := &Client{ HTTPClient: defaultClient(), Dialer: &net.Dialer{}, endpoint: endpoint, endpointURL: u, eventMonitor: new(eventMonitoringState), requestedAPIVersion: requestedAPIVersion, } c.initializeNativeClient(defaultTransport) return c, nil } // WithTransport replaces underlying HTTP client of Docker Client by accepting // a function that returns pointer to a transport object. func (c *Client) WithTransport(trFunc func() *http.Transport) { c.initializeNativeClient(trFunc) } // NewVersionnedTLSClient is like NewVersionedClient, but with ann extra n. // // Deprecated: Use NewVersionedTLSClient instead. func NewVersionnedTLSClient(endpoint string, cert, key, ca, apiVersionString string) (*Client, error) { return NewVersionedTLSClient(endpoint, cert, key, ca, apiVersionString) } // NewVersionedTLSClient returns a Client instance ready for TLS communications with the givens // server endpoint, key and certificates, using a specific remote API version. func NewVersionedTLSClient(endpoint string, cert, key, ca, apiVersionString string) (*Client, error) { var certPEMBlock []byte var keyPEMBlock []byte var caPEMCert []byte if _, err := os.Stat(cert); !os.IsNotExist(err) { certPEMBlock, err = os.ReadFile(cert) if err != nil { return nil, err } } if _, err := os.Stat(key); !os.IsNotExist(err) { keyPEMBlock, err = os.ReadFile(key) if err != nil { return nil, err } } if _, err := os.Stat(ca); !os.IsNotExist(err) { caPEMCert, err = os.ReadFile(ca) if err != nil { return nil, err } } return NewVersionedTLSClientFromBytes(endpoint, certPEMBlock, keyPEMBlock, caPEMCert, apiVersionString) } // NewClientFromEnv returns a Client instance ready for communication created from // Docker's default logic for the environment variables DOCKER_HOST, DOCKER_TLS_VERIFY, DOCKER_CERT_PATH, // and DOCKER_API_VERSION. // // See https://github.com/docker/docker/blob/1f963af697e8df3a78217f6fdbf67b8123a7db94/docker/docker.go#L68. // See https://github.com/docker/compose/blob/81707ef1ad94403789166d2fe042c8a718a4c748/compose/cli/docker_client.py#L7. // See https://github.com/moby/moby/blob/28d7dba41d0c0d9c7f0dafcc79d3c59f2b3f5dc3/client/options.go#L51 func NewClientFromEnv() (*Client, error) { apiVersionString := os.Getenv("DOCKER_API_VERSION") client, err := NewVersionedClientFromEnv(apiVersionString) if err != nil { return nil, err } client.SkipServerVersionCheck = apiVersionString == "" return client, nil } // NewVersionedClientFromEnv returns a Client instance ready for TLS communications created from // Docker's default logic for the environment variables DOCKER_HOST, DOCKER_TLS_VERIFY, and DOCKER_CERT_PATH, // and using a specific remote API version. // // See https://github.com/docker/docker/blob/1f963af697e8df3a78217f6fdbf67b8123a7db94/docker/docker.go#L68. // See https://github.com/docker/compose/blob/81707ef1ad94403789166d2fe042c8a718a4c748/compose/cli/docker_client.py#L7. func NewVersionedClientFromEnv(apiVersionString string) (*Client, error) { dockerEnv, err := getDockerEnv() if err != nil { return nil, err } dockerHost := dockerEnv.dockerHost if dockerEnv.dockerTLSVerify { parts := strings.SplitN(dockerEnv.dockerHost, "://", 2) if len(parts) != 2 { return nil, fmt.Errorf("could not split %s into two parts by ://", dockerHost) } cert := filepath.Join(dockerEnv.dockerCertPath, "cert.pem") key := filepath.Join(dockerEnv.dockerCertPath, "key.pem") ca := filepath.Join(dockerEnv.dockerCertPath, "ca.pem") return NewVersionedTLSClient(dockerEnv.dockerHost, cert, key, ca, apiVersionString) } return NewVersionedClient(dockerEnv.dockerHost, apiVersionString) } // NewVersionedTLSClientFromBytes returns a Client instance ready for TLS communications with the givens // server endpoint, key and certificates (passed inline to the function as opposed to being // read from a local file), using a specific remote API version. func NewVersionedTLSClientFromBytes(endpoint string, certPEMBlock, keyPEMBlock, caPEMCert []byte, apiVersionString string) (*Client, error) { u, err := parseEndpoint(endpoint, true) if err != nil { return nil, err } var requestedAPIVersion APIVersion if strings.Contains(apiVersionString, ".") { requestedAPIVersion, err = NewAPIVersion(apiVersionString) if err != nil { return nil, err } } tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12} if certPEMBlock != nil && keyPEMBlock != nil { tlsCert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock) if err != nil { return nil, err } tlsConfig.Certificates = []tls.Certificate{tlsCert} } if caPEMCert == nil { tlsConfig.InsecureSkipVerify = true } else { caPool := x509.NewCertPool() if !caPool.AppendCertsFromPEM(caPEMCert) { return nil, errors.New("could not add RootCA pem") } tlsConfig.RootCAs = caPool } tr := defaultTransport() tr.TLSClientConfig = tlsConfig if err != nil { return nil, err } c := &Client{ HTTPClient: &http.Client{Transport: tr}, TLSConfig: tlsConfig, Dialer: &net.Dialer{}, endpoint: endpoint, endpointURL: u, eventMonitor: new(eventMonitoringState), requestedAPIVersion: requestedAPIVersion, } c.initializeNativeClient(defaultTransport) return c, nil } // SetTimeout takes a timeout and applies it to the HTTPClient. It should not // be called concurrently with any other Client methods. func (c *Client) SetTimeout(t time.Duration) { if c.HTTPClient != nil { c.HTTPClient.Timeout = t } } func (c *Client) checkAPIVersion() error { serverAPIVersionString, err := c.getServerAPIVersionString() if err != nil { return err } c.serverAPIVersion, err = NewAPIVersion(serverAPIVersionString) if err != nil { return err } if c.requestedAPIVersion == nil { c.expectedAPIVersion = c.serverAPIVersion } else { c.expectedAPIVersion = c.requestedAPIVersion } return nil } // Endpoint returns the current endpoint. It's useful for getting the endpoint // when using functions that get this data from the environment (like // NewClientFromEnv. func (c *Client) Endpoint() string { return c.endpoint } // Ping pings the docker server // // See https://goo.gl/wYfgY1 for more details. func (c *Client) Ping() error { return c.PingWithContext(context.TODO()) } // PingWithContext pings the docker server // The context object can be used to cancel the ping request. // // See https://goo.gl/wYfgY1 for more details. func (c *Client) PingWithContext(ctx context.Context) error { path := "/_ping" resp, err := c.do(http.MethodGet, path, doOptions{context: ctx}) if err != nil { return err } if resp.StatusCode != http.StatusOK { return newError(resp) } resp.Body.Close() return nil } func (c *Client) getServerAPIVersionString() (version string, err error) { resp, err := c.do(http.MethodGet, "/version", doOptions{}) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("received unexpected status %d while trying to retrieve the server version", resp.StatusCode) } var versionResponse map[string]any if err := json.NewDecoder(resp.Body).Decode(&versionResponse); err != nil { return "", err } if version, ok := (versionResponse["ApiVersion"]).(string); ok { return version, nil } return "", nil } type doOptions struct { data any forceJSON bool headers map[string]string context context.Context } func (c *Client) do(method, path string, doOptions doOptions) (*http.Response, error) { var params io.Reader if doOptions.data != nil || doOptions.forceJSON { buf, err := json.Marshal(doOptions.data) if err != nil { return nil, err } params = bytes.NewBuffer(buf) } if path != "/version" && !c.SkipServerVersionCheck && c.expectedAPIVersion == nil { err := c.checkAPIVersion() if err != nil { return nil, err } } protocol := c.endpointURL.Scheme var u string switch protocol { case unixProtocol, namedPipeProtocol: u = c.getFakeNativeURL(path) default: u = c.getURL(path) } req, err := http.NewRequest(method, u, params) if err != nil { return nil, err } req.Header.Set("User-Agent", userAgent) if doOptions.data != nil { req.Header.Set("Content-Type", "application/json") } else if method == http.MethodPost { req.Header.Set("Content-Type", "plain/text") } for k, v := range doOptions.headers { req.Header.Set(k, v) } ctx := doOptions.context if ctx == nil { ctx = context.Background() } resp, err := c.HTTPClient.Do(req.WithContext(ctx)) if err != nil { if strings.Contains(err.Error(), "connection refused") { return nil, ErrConnectionRefused } return nil, chooseError(ctx, err) } if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest { return nil, newError(resp) } return resp, nil } type streamOptions struct { setRawTerminal bool rawJSONStream bool useJSONDecoder bool headers map[string]string in io.Reader stdout io.Writer stderr io.Writer reqSent chan struct{} // timeout is the initial connection timeout timeout time.Duration // Timeout with no data is received, it's reset every time new data // arrives inactivityTimeout time.Duration context context.Context } func chooseError(ctx context.Context, err error) error { select { case <-ctx.Done(): return context.Cause(ctx) default: return err } } func (c *Client) stream(method, path string, streamOptions streamOptions) error { if (method == http.MethodPost || method == http.MethodPut) && streamOptions.in == nil { streamOptions.in = bytes.NewReader(nil) } if path != "/version" && !c.SkipServerVersionCheck && c.expectedAPIVersion == nil { err := c.checkAPIVersion() if err != nil { return err } } return c.streamURL(method, c.getURL(path), streamOptions) } func (c *Client) streamURL(method, url string, streamOptions streamOptions) error { if (method == http.MethodPost || method == http.MethodPut) && streamOptions.in == nil { streamOptions.in = bytes.NewReader(nil) } if !c.SkipServerVersionCheck && c.expectedAPIVersion == nil { err := c.checkAPIVersion() if err != nil { return err } } // make a sub-context so that our active cancellation does not affect parent ctx := streamOptions.context if ctx == nil { ctx = context.Background() } subCtx, cancelRequest := context.WithCancel(ctx) defer cancelRequest() req, err := http.NewRequestWithContext(ctx, method, url, streamOptions.in) if err != nil { return err } req.Header.Set("User-Agent", userAgent) if method == http.MethodPost { req.Header.Set("Content-Type", "plain/text") } for key, val := range streamOptions.headers { req.Header.Set(key, val) } var resp *http.Response protocol := c.endpointURL.Scheme address := c.endpointURL.Path if streamOptions.stdout == nil { streamOptions.stdout = io.Discard } if streamOptions.stderr == nil { streamOptions.stderr = io.Discard } if protocol == unixProtocol || protocol == namedPipeProtocol { var dial net.Conn dial, err = c.Dialer.Dial(protocol, address) if err != nil { return err } go func() { <-subCtx.Done() dial.Close() }() breader := bufio.NewReader(dial) err = req.Write(dial) if err != nil { return chooseError(subCtx, err) } // ReadResponse may hang if server does not replay if streamOptions.timeout > 0 { dial.SetDeadline(time.Now().Add(streamOptions.timeout)) } if streamOptions.reqSent != nil { close(streamOptions.reqSent) } if resp, err = http.ReadResponse(breader, req); err != nil { // Cancel timeout for future I/O operations if streamOptions.timeout > 0 { dial.SetDeadline(time.Time{}) } if strings.Contains(err.Error(), "connection refused") { return ErrConnectionRefused } return chooseError(subCtx, err) } defer resp.Body.Close() } else { if resp, err = c.HTTPClient.Do(req.WithContext(subCtx)); err != nil { if strings.Contains(err.Error(), "connection refused") { return ErrConnectionRefused } return chooseError(subCtx, err) } defer resp.Body.Close() if streamOptions.reqSent != nil { close(streamOptions.reqSent) } } if resp.StatusCode < 200 || resp.StatusCode >= 400 { return newError(resp) } var canceled uint32 if streamOptions.inactivityTimeout > 0 { var ch chan<- struct{} resp.Body, ch = handleInactivityTimeout(resp.Body, streamOptions.inactivityTimeout, cancelRequest, &canceled) defer close(ch) } err = handleStreamResponse(resp, &streamOptions) if err != nil { if atomic.LoadUint32(&canceled) != 0 { return ErrInactivityTimeout } return chooseError(subCtx, err) } return nil } func handleStreamResponse(resp *http.Response, streamOptions *streamOptions) error { var err error if !streamOptions.useJSONDecoder && resp.Header.Get("Content-Type") != "application/json" { if streamOptions.setRawTerminal { _, err = io.Copy(streamOptions.stdout, resp.Body) } else { _, err = stdcopy.StdCopy(streamOptions.stdout, streamOptions.stderr, resp.Body) } return err } // if we want to get raw json stream, just copy it back to output // without decoding it if streamOptions.rawJSONStream { _, err = io.Copy(streamOptions.stdout, resp.Body) return err } if st, ok := streamOptions.stdout.(stream); ok { err = jsonmessage.DisplayJSONMessagesStream(resp.Body, st, st.FD(), st.IsTerminal(), nil) } else { err = jsonmessage.DisplayJSONMessagesStream(resp.Body, streamOptions.stdout, 0, false, nil) } return err } type stream interface { io.Writer FD() uintptr IsTerminal() bool } type proxyReader struct { io.ReadCloser calls uint64 } func (p *proxyReader) callCount() uint64 { return atomic.LoadUint64(&p.calls) } func (p *proxyReader) Read(data []byte) (int, error) { atomic.AddUint64(&p.calls, 1) return p.ReadCloser.Read(data) } func handleInactivityTimeout(reader io.ReadCloser, timeout time.Duration, cancelRequest func(), canceled *uint32) (io.ReadCloser, chan<- struct{}) { done := make(chan struct{}) proxyReader := &proxyReader{ReadCloser: reader} go func() { var lastCallCount uint64 for { select { case <-time.After(timeout): case <-done: return } curCallCount := proxyReader.callCount() if curCallCount == lastCallCount { atomic.AddUint32(canceled, 1) cancelRequest() return } lastCallCount = curCallCount } }() return proxyReader, done } type hijackOptions struct { success chan struct{} setRawTerminal bool in io.Reader stdout io.Writer stderr io.Writer data any } // CloseWaiter is an interface with methods for closing the underlying resource // and then waiting for it to finish processing. type CloseWaiter interface { io.Closer Wait() error } type waiterFunc func() error func (w waiterFunc) Wait() error { return w() } type closerFunc func() error func (c closerFunc) Close() error { return c() } func (c *Client) hijack(method, path string, hijackOptions hijackOptions) (CloseWaiter, error) { if path != "/version" && !c.SkipServerVersionCheck && c.expectedAPIVersion == nil { err := c.checkAPIVersion() if err != nil { return nil, err } } var params io.Reader if hijackOptions.data != nil { buf, err := json.Marshal(hijackOptions.data) if err != nil { return nil, err } params = bytes.NewBuffer(buf) } req, err := http.NewRequest(method, c.getURL(path), params) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Connection", "Upgrade") req.Header.Set("Upgrade", "tcp") protocol := c.endpointURL.Scheme address := c.endpointURL.Path if protocol != unixProtocol && protocol != namedPipeProtocol { protocol = "tcp" address = c.endpointURL.Host } var dial net.Conn if c.TLSConfig != nil && protocol != unixProtocol && protocol != namedPipeProtocol { netDialer, ok := c.Dialer.(*net.Dialer) if !ok { return nil, ErrTLSNotSupported } dial, err = tlsDialWithDialer(netDialer, protocol, address, c.TLSConfig) if err != nil { return nil, err } } else { dial, err = c.Dialer.Dial(protocol, address) if err != nil { return nil, err } } errs := make(chan error, 1) quit := make(chan struct{}) go func() { //lint:ignore SA1019 the alternative doesn't quite work, so keep using the deprecated thing. clientconn := httputil.NewClientConn(dial, nil) defer clientconn.Close() clientconn.Do(req) if hijackOptions.success != nil { hijackOptions.success <- struct{}{} <-hijackOptions.success } rwc, br := clientconn.Hijack() defer rwc.Close() errChanOut := make(chan error, 1) errChanIn := make(chan error, 2) if hijackOptions.stdout == nil && hijackOptions.stderr == nil { close(errChanOut) } else { // Only copy if hijackOptions.stdout and/or hijackOptions.stderr is actually set. // Otherwise, if the only stream you care about is stdin, your attach session // will "hang" until the container terminates, even though you're not reading // stdout/stderr if hijackOptions.stdout == nil { hijackOptions.stdout = io.Discard } if hijackOptions.stderr == nil { hijackOptions.stderr = io.Discard } go func() { defer func() { if hijackOptions.in != nil { if closer, ok := hijackOptions.in.(io.Closer); ok { closer.Close() } errChanIn <- nil } }() var err error if hijackOptions.setRawTerminal { _, err = io.Copy(hijackOptions.stdout, br) } else { _, err = stdcopy.StdCopy(hijackOptions.stdout, hijackOptions.stderr, br) } errChanOut <- err }() } go func() { var err error if hijackOptions.in != nil { _, err = io.Copy(rwc, hijackOptions.in) } errChanIn <- err rwc.(interface { CloseWrite() error }).CloseWrite() }() var errIn error select { case errIn = <-errChanIn: case <-quit: } var errOut error select { case errOut = <-errChanOut: case <-quit: } if errIn != nil { errs <- errIn } else { errs <- errOut } }() return struct { closerFunc waiterFunc }{ closerFunc(func() error { close(quit); return nil }), waiterFunc(func() error { return <-errs }), }, nil } func (c *Client) getURL(path string) string { urlStr := strings.TrimRight(c.endpointURL.String(), "/") if c.endpointURL.Scheme == unixProtocol || c.endpointURL.Scheme == namedPipeProtocol { urlStr = "" } if c.requestedAPIVersion != nil { return fmt.Sprintf("%s/v%s%s", urlStr, c.requestedAPIVersion, path) } return fmt.Sprintf("%s%s", urlStr, path) } func (c *Client) getPath(basepath string, opts any) (string, error) { queryStr, requiredAPIVersion := queryStringVersion(opts) return c.pathVersionCheck(basepath, queryStr, requiredAPIVersion) } func (c *Client) pathVersionCheck(basepath, queryStr string, requiredAPIVersion APIVersion) (string, error) { urlStr := strings.TrimRight(c.endpointURL.String(), "/") if c.endpointURL.Scheme == unixProtocol || c.endpointURL.Scheme == namedPipeProtocol { urlStr = "" } if c.requestedAPIVersion != nil { if c.requestedAPIVersion.GreaterThanOrEqualTo(requiredAPIVersion) { return fmt.Sprintf("%s/v%s%s?%s", urlStr, c.requestedAPIVersion, basepath, queryStr), nil } return "", fmt.Errorf("API %s requires version %s, requested version %s is insufficient", basepath, requiredAPIVersion, c.requestedAPIVersion) } if requiredAPIVersion != nil { return fmt.Sprintf("%s/v%s%s?%s", urlStr, requiredAPIVersion, basepath, queryStr), nil } return fmt.Sprintf("%s%s?%s", urlStr, basepath, queryStr), nil } // getFakeNativeURL returns the URL needed to make an HTTP request over a UNIX // domain socket to the given path. func (c *Client) getFakeNativeURL(path string) string { u := *c.endpointURL // Copy. // Override URL so that net/http will not complain. u.Scheme = "http" u.Host = "unix.sock" // Doesn't matter what this is - it's not used. u.Path = "" urlStr := strings.TrimRight(u.String(), "/") if c.requestedAPIVersion != nil { return fmt.Sprintf("%s/v%s%s", urlStr, c.requestedAPIVersion, path) } return fmt.Sprintf("%s%s", urlStr, path) } func queryStringVersion(opts any) (string, APIVersion) { if opts == nil { return "", nil } value := reflect.ValueOf(opts) if value.Kind() == reflect.Ptr { value = value.Elem() } if value.Kind() != reflect.Struct { return "", nil } var apiVersion APIVersion items := url.Values(map[string][]string{}) for i := 0; i < value.NumField(); i++ { field := value.Type().Field(i) if field.PkgPath != "" { continue } key := field.Tag.Get("qs") if key == "" { key = strings.ToLower(field.Name) } else if key == "-" { continue } if addQueryStringValue(items, key, value.Field(i)) { verstr := field.Tag.Get("ver") if verstr != "" { ver, _ := NewAPIVersion(verstr) if apiVersion == nil { apiVersion = ver } else if ver.GreaterThan(apiVersion) { apiVersion = ver } } } } return items.Encode(), apiVersion } func queryString(opts any) string { s, _ := queryStringVersion(opts) return s } func addQueryStringValue(items url.Values, key string, v reflect.Value) bool { switch v.Kind() { case reflect.Bool: if v.Bool() { items.Add(key, "1") return true } case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if v.Int() > 0 { items.Add(key, strconv.FormatInt(v.Int(), 10)) return true } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: if v.Uint() > 0 { items.Add(key, strconv.FormatUint(v.Uint(), 10)) return true } case reflect.Float32, reflect.Float64: if v.Float() > 0 { items.Add(key, strconv.FormatFloat(v.Float(), 'f', -1, 64)) return true } case reflect.String: if v.String() != "" { items.Add(key, v.String()) return true } case reflect.Ptr: if !v.IsNil() { if b, err := json.Marshal(v.Interface()); err == nil { items.Add(key, string(b)) return true } } case reflect.Map: if len(v.MapKeys()) > 0 { if b, err := json.Marshal(v.Interface()); err == nil { items.Add(key, string(b)) return true } } case reflect.Array, reflect.Slice: vLen := v.Len() var valuesAdded int if vLen > 0 { for i := 0; i < vLen; i++ { if addQueryStringValue(items, key, v.Index(i)) { valuesAdded++ } } } return valuesAdded > 0 } return false } // Error represents failures in the API. It represents a failure from the API. type Error struct { Status int Message string } func newError(resp *http.Response) *Error { type ErrMsg struct { Message string `json:"message"` } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { return &Error{Status: resp.StatusCode, Message: fmt.Sprintf("cannot read body, err: %v", err)} } var emsg ErrMsg err = json.Unmarshal(data, &emsg) if err != nil { return &Error{Status: resp.StatusCode, Message: string(data)} } return &Error{Status: resp.StatusCode, Message: emsg.Message} } func (e *Error) Error() string { return fmt.Sprintf("API error (%d): %s", e.Status, e.Message) } func parseEndpoint(endpoint string, tls bool) (*url.URL, error) { if endpoint != "" && !strings.Contains(endpoint, "://") { endpoint = "tcp://" + endpoint } u, err := url.Parse(endpoint) if err != nil { return nil, ErrInvalidEndpoint } if tls && u.Scheme != "unix" { u.Scheme = "https" } switch u.Scheme { case unixProtocol, namedPipeProtocol: return u, nil case "http", "https", "tcp": _, port, err := net.SplitHostPort(u.Host) if err != nil { var e *net.AddrError if errors.As(err, &e) { if e.Err == "missing port in address" { return u, nil } } return nil, ErrInvalidEndpoint } number, err := strconv.ParseInt(port, 10, 64) if err == nil && number > 0 && number < 65536 { if u.Scheme == "tcp" { if tls { u.Scheme = "https" } else { u.Scheme = "http" } } return u, nil } return nil, ErrInvalidEndpoint default: return nil, ErrInvalidEndpoint } } type dockerEnv struct { dockerHost string dockerTLSVerify bool dockerCertPath string } func getDockerEnv() (*dockerEnv, error) { dockerHost := os.Getenv("DOCKER_HOST") var err error if dockerHost == "" { dockerHost = defaultHost } dockerTLSVerify := os.Getenv("DOCKER_TLS_VERIFY") != "" var dockerCertPath string if dockerTLSVerify { dockerCertPath = os.Getenv("DOCKER_CERT_PATH") if dockerCertPath == "" { home := homedir.Get() if home == "" { return nil, errors.New("environment variable HOME must be set if DOCKER_CERT_PATH is not set") } dockerCertPath = filepath.Join(home, ".docker") dockerCertPath, err = filepath.Abs(dockerCertPath) if err != nil { return nil, err } } } return &dockerEnv{ dockerHost: dockerHost, dockerTLSVerify: dockerTLSVerify, dockerCertPath: dockerCertPath, }, nil } // defaultTransport returns a new http.Transport with similar default values to // http.DefaultTransport, but with idle connections and keepalives disabled. func defaultTransport() *http.Transport { transport := defaultPooledTransport() transport.DisableKeepAlives = true transport.MaxIdleConnsPerHost = -1 return transport } // defaultPooledTransport returns a new http.Transport with similar default // values to http.DefaultTransport. Do not use this for transient transports as // it can leak file descriptors over time. Only use this for transports that // will be re-used for the same host(s). func defaultPooledTransport() *http.Transport { transport := &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1, } return transport } // defaultClient returns a new http.Client with similar default values to // http.Client, but with a non-shared Transport, idle connections disabled, and // keepalives disabled. func defaultClient() *http.Client { return &http.Client{ Transport: defaultTransport(), } } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/client_unix.go ================================================ // Copyright 2016 go-dockerclient authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build !windows package docker import ( "context" "net" "net/http" ) const defaultHost = "unix:///var/run/docker.sock" // initializeNativeClient initializes the native Unix domain socket client on // Unix-style operating systems func (c *Client) initializeNativeClient(trFunc func() *http.Transport) { if c.endpointURL.Scheme != unixProtocol { return } sockPath := c.endpointURL.Path tr := trFunc() tr.Proxy = nil tr.DialContext = func(_ context.Context, network, addr string) (net.Conn, error) { return c.Dialer.Dial(unixProtocol, sockPath) } c.HTTPClient.Transport = tr } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/client_windows.go ================================================ // Copyright 2016 go-dockerclient authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package docker import ( "context" "net" "net/http" "time" winio "github.com/Microsoft/go-winio" ) const ( defaultHost = "npipe:////./pipe/docker_engine" namedPipeConnectTimeout = 2 * time.Second ) type pipeDialer struct { dialFunc func(network, addr string) (net.Conn, error) } func (p pipeDialer) Dial(network, address string) (net.Conn, error) { return p.dialFunc(network, address) } // initializeNativeClient initializes the native Named Pipe client for Windows func (c *Client) initializeNativeClient(trFunc func() *http.Transport) { if c.endpointURL.Scheme != namedPipeProtocol { return } namedPipePath := c.endpointURL.Path dialFunc := func(_, addr string) (net.Conn, error) { timeout := namedPipeConnectTimeout return winio.DialPipe(namedPipePath, &timeout) } tr := trFunc() tr.Proxy = nil tr.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { return dialFunc(network, addr) } c.Dialer = &pipeDialer{dialFunc} c.HTTPClient.Transport = tr } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/container.go ================================================ // Copyright 2013 go-dockerclient authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package docker import ( "fmt" "strconv" "strings" "time" units "github.com/docker/go-units" ) // APIPort is a type that represents a port mapping returned by the Docker API type APIPort struct { PrivatePort int64 `json:"PrivatePort,omitempty" yaml:"PrivatePort,omitempty" toml:"PrivatePort,omitempty"` PublicPort int64 `json:"PublicPort,omitempty" yaml:"PublicPort,omitempty" toml:"PublicPort,omitempty"` Type string `json:"Type,omitempty" yaml:"Type,omitempty" toml:"Type,omitempty"` IP string `json:"IP,omitempty" yaml:"IP,omitempty" toml:"IP,omitempty"` } // APIMount represents a mount point for a container. type APIMount struct { Name string `json:"Name,omitempty" yaml:"Name,omitempty" toml:"Name,omitempty"` Source string `json:"Source,omitempty" yaml:"Source,omitempty" toml:"Source,omitempty"` Destination string `json:"Destination,omitempty" yaml:"Destination,omitempty" toml:"Destination,omitempty"` Driver string `json:"Driver,omitempty" yaml:"Driver,omitempty" toml:"Driver,omitempty"` Mode string `json:"Mode,omitempty" yaml:"Mode,omitempty" toml:"Mode,omitempty"` RW bool `json:"RW,omitempty" yaml:"RW,omitempty" toml:"RW,omitempty"` Propagation string `json:"Propagation,omitempty" yaml:"Propagation,omitempty" toml:"Propagation,omitempty"` Type string `json:"Type,omitempty" yaml:"Type,omitempty" toml:"Type,omitempty"` } // APIContainers represents each container in the list returned by // ListContainers. type APIContainers struct { ID string `json:"Id" yaml:"Id" toml:"Id"` Image string `json:"Image,omitempty" yaml:"Image,omitempty" toml:"Image,omitempty"` Command string `json:"Command,omitempty" yaml:"Command,omitempty" toml:"Command,omitempty"` Created int64 `json:"Created,omitempty" yaml:"Created,omitempty" toml:"Created,omitempty"` State string `json:"State,omitempty" yaml:"State,omitempty" toml:"State,omitempty"` Status string `json:"Status,omitempty" yaml:"Status,omitempty" toml:"Status,omitempty"` Ports []APIPort `json:"Ports,omitempty" yaml:"Ports,omitempty" toml:"Ports,omitempty"` SizeRw int64 `json:"SizeRw,omitempty" yaml:"SizeRw,omitempty" toml:"SizeRw,omitempty"` SizeRootFs int64 `json:"SizeRootFs,omitempty" yaml:"SizeRootFs,omitempty" toml:"SizeRootFs,omitempty"` Names []string `json:"Names,omitempty" yaml:"Names,omitempty" toml:"Names,omitempty"` Labels map[string]string `json:"Labels,omitempty" yaml:"Labels,omitempty" toml:"Labels,omitempty"` Networks NetworkList `json:"NetworkSettings,omitempty" yaml:"NetworkSettings,omitempty" toml:"NetworkSettings,omitempty"` Mounts []APIMount `json:"Mounts,omitempty" yaml:"Mounts,omitempty" toml:"Mounts,omitempty"` } // NetworkList encapsulates a map of networks, as returned by the Docker API in // ListContainers. type NetworkList struct { Networks map[string]ContainerNetwork `json:"Networks" yaml:"Networks,omitempty" toml:"Networks,omitempty"` } // Port represents the port number and the protocol, in the form // /. For example: 80/tcp. type Port string // Port returns the number of the port. func (p Port) Port() string { return strings.Split(string(p), "/")[0] } // Proto returns the name of the protocol. func (p Port) Proto() string { parts := strings.Split(string(p), "/") if len(parts) == 1 { return "tcp" } return parts[1] } // HealthCheck represents one check of health. type HealthCheck struct { Start time.Time `json:"Start,omitempty" yaml:"Start,omitempty" toml:"Start,omitempty"` End time.Time `json:"End,omitempty" yaml:"End,omitempty" toml:"End,omitempty"` ExitCode int `json:"ExitCode,omitempty" yaml:"ExitCode,omitempty" toml:"ExitCode,omitempty"` Output string `json:"Output,omitempty" yaml:"Output,omitempty" toml:"Output,omitempty"` } // Health represents the health of a container. type Health struct { Status string `json:"Status,omitempty" yaml:"Status,omitempty" toml:"Status,omitempty"` FailingStreak int `json:"FailingStreak,omitempty" yaml:"FailingStreak,omitempty" toml:"FailingStreak,omitempty"` Log []HealthCheck `json:"Log,omitempty" yaml:"Log,omitempty" toml:"Log,omitempty"` } // State represents the state of a container. type State struct { Status string `json:"Status,omitempty" yaml:"Status,omitempty" toml:"Status,omitempty"` Running bool `json:"Running,omitempty" yaml:"Running,omitempty" toml:"Running,omitempty"` Paused bool `json:"Paused,omitempty" yaml:"Paused,omitempty" toml:"Paused,omitempty"` Restarting bool `json:"Restarting,omitempty" yaml:"Restarting,omitempty" toml:"Restarting,omitempty"` OOMKilled bool `json:"OOMKilled,omitempty" yaml:"OOMKilled,omitempty" toml:"OOMKilled,omitempty"` RemovalInProgress bool `json:"RemovalInProgress,omitempty" yaml:"RemovalInProgress,omitempty" toml:"RemovalInProgress,omitempty"` Dead bool `json:"Dead,omitempty" yaml:"Dead,omitempty" toml:"Dead,omitempty"` Pid int `json:"Pid,omitempty" yaml:"Pid,omitempty" toml:"Pid,omitempty"` ExitCode int `json:"ExitCode,omitempty" yaml:"ExitCode,omitempty" toml:"ExitCode,omitempty"` Error string `json:"Error,omitempty" yaml:"Error,omitempty" toml:"Error,omitempty"` StartedAt time.Time `json:"StartedAt,omitempty" yaml:"StartedAt,omitempty" toml:"StartedAt,omitempty"` FinishedAt time.Time `json:"FinishedAt,omitempty" yaml:"FinishedAt,omitempty" toml:"FinishedAt,omitempty"` Health Health `json:"Health,omitempty" yaml:"Health,omitempty" toml:"Health,omitempty"` } // String returns a human-readable description of the state func (s *State) String() string { if s.Running { if s.Paused { return fmt.Sprintf("Up %s (Paused)", units.HumanDuration(time.Now().UTC().Sub(s.StartedAt))) } if s.Restarting { return fmt.Sprintf("Restarting (%d) %s ago", s.ExitCode, units.HumanDuration(time.Now().UTC().Sub(s.FinishedAt))) } return fmt.Sprintf("Up %s", units.HumanDuration(time.Now().UTC().Sub(s.StartedAt))) } if s.RemovalInProgress { return "Removal In Progress" } if s.Dead { return "Dead" } if s.StartedAt.IsZero() { return "Created" } if s.FinishedAt.IsZero() { return "" } return fmt.Sprintf("Exited (%d) %s ago", s.ExitCode, units.HumanDuration(time.Now().UTC().Sub(s.FinishedAt))) } // StateString returns a single string to describe state func (s *State) StateString() string { if s.Running { if s.Paused { return "paused" } if s.Restarting { return "restarting" } return "running" } if s.Dead { return "dead" } if s.StartedAt.IsZero() { return "created" } return "exited" } // PortBinding represents the host/container port mapping as returned in the // `docker inspect` json type PortBinding struct { HostIP string `json:"HostIp,omitempty" yaml:"HostIp,omitempty" toml:"HostIp,omitempty"` HostPort string `json:"HostPort,omitempty" yaml:"HostPort,omitempty" toml:"HostPort,omitempty"` } // PortMapping represents a deprecated field in the `docker inspect` output, // and its value as found in NetworkSettings should always be nil type PortMapping map[string]string // ContainerNetwork represents the networking settings of a container per network. type ContainerNetwork struct { Aliases []string `json:"Aliases,omitempty" yaml:"Aliases,omitempty" toml:"Aliases,omitempty"` MacAddress string `json:"MacAddress,omitempty" yaml:"MacAddress,omitempty" toml:"MacAddress,omitempty"` GlobalIPv6PrefixLen int `json:"GlobalIPv6PrefixLen,omitempty" yaml:"GlobalIPv6PrefixLen,omitempty" toml:"GlobalIPv6PrefixLen,omitempty"` GlobalIPv6Address string `json:"GlobalIPv6Address,omitempty" yaml:"GlobalIPv6Address,omitempty" toml:"GlobalIPv6Address,omitempty"` IPv6Gateway string `json:"IPv6Gateway,omitempty" yaml:"IPv6Gateway,omitempty" toml:"IPv6Gateway,omitempty"` IPPrefixLen int `json:"IPPrefixLen,omitempty" yaml:"IPPrefixLen,omitempty" toml:"IPPrefixLen,omitempty"` IPAddress string `json:"IPAddress,omitempty" yaml:"IPAddress,omitempty" toml:"IPAddress,omitempty"` Gateway string `json:"Gateway,omitempty" yaml:"Gateway,omitempty" toml:"Gateway,omitempty"` EndpointID string `json:"EndpointID,omitempty" yaml:"EndpointID,omitempty" toml:"EndpointID,omitempty"` NetworkID string `json:"NetworkID,omitempty" yaml:"NetworkID,omitempty" toml:"NetworkID,omitempty"` DNSNames []string `json:"DNSNames,omitempty" yaml:"DNSNames,omitempty" toml:"DNSNames,omitempty"` } // NetworkSettings contains network-related information about a container type NetworkSettings struct { Networks map[string]ContainerNetwork `json:"Networks,omitempty" yaml:"Networks,omitempty" toml:"Networks,omitempty"` IPAddress string `json:"IPAddress,omitempty" yaml:"IPAddress,omitempty" toml:"IPAddress,omitempty"` IPPrefixLen int `json:"IPPrefixLen,omitempty" yaml:"IPPrefixLen,omitempty" toml:"IPPrefixLen,omitempty"` MacAddress string `json:"MacAddress,omitempty" yaml:"MacAddress,omitempty" toml:"MacAddress,omitempty"` Gateway string `json:"Gateway,omitempty" yaml:"Gateway,omitempty" toml:"Gateway,omitempty"` Bridge string `json:"Bridge,omitempty" yaml:"Bridge,omitempty" toml:"Bridge,omitempty"` PortMapping map[string]PortMapping `json:"PortMapping,omitempty" yaml:"PortMapping,omitempty" toml:"PortMapping,omitempty"` Ports map[Port][]PortBinding `json:"Ports,omitempty" yaml:"Ports,omitempty" toml:"Ports,omitempty"` NetworkID string `json:"NetworkID,omitempty" yaml:"NetworkID,omitempty" toml:"NetworkID,omitempty"` EndpointID string `json:"EndpointID,omitempty" yaml:"EndpointID,omitempty" toml:"EndpointID,omitempty"` SandboxKey string `json:"SandboxKey,omitempty" yaml:"SandboxKey,omitempty" toml:"SandboxKey,omitempty"` GlobalIPv6Address string `json:"GlobalIPv6Address,omitempty" yaml:"GlobalIPv6Address,omitempty" toml:"GlobalIPv6Address,omitempty"` GlobalIPv6PrefixLen int `json:"GlobalIPv6PrefixLen,omitempty" yaml:"GlobalIPv6PrefixLen,omitempty" toml:"GlobalIPv6PrefixLen,omitempty"` IPv6Gateway string `json:"IPv6Gateway,omitempty" yaml:"IPv6Gateway,omitempty" toml:"IPv6Gateway,omitempty"` LinkLocalIPv6Address string `json:"LinkLocalIPv6Address,omitempty" yaml:"LinkLocalIPv6Address,omitempty" toml:"LinkLocalIPv6Address,omitempty"` LinkLocalIPv6PrefixLen int `json:"LinkLocalIPv6PrefixLen,omitempty" yaml:"LinkLocalIPv6PrefixLen,omitempty" toml:"LinkLocalIPv6PrefixLen,omitempty"` SecondaryIPAddresses []string `json:"SecondaryIPAddresses,omitempty" yaml:"SecondaryIPAddresses,omitempty" toml:"SecondaryIPAddresses,omitempty"` SecondaryIPv6Addresses []string `json:"SecondaryIPv6Addresses,omitempty" yaml:"SecondaryIPv6Addresses,omitempty" toml:"SecondaryIPv6Addresses,omitempty"` } // PortMappingAPI translates the port mappings as contained in NetworkSettings // into the format in which they would appear when returned by the API func (settings *NetworkSettings) PortMappingAPI() []APIPort { var mapping []APIPort for port, bindings := range settings.Ports { p, _ := parsePort(port.Port()) if len(bindings) == 0 { mapping = append(mapping, APIPort{ PrivatePort: int64(p), Type: port.Proto(), }) continue } for _, binding := range bindings { p, _ := parsePort(port.Port()) h, _ := parsePort(binding.HostPort) mapping = append(mapping, APIPort{ PrivatePort: int64(p), PublicPort: int64(h), Type: port.Proto(), IP: binding.HostIP, }) } } return mapping } func parsePort(rawPort string) (int, error) { port, err := strconv.ParseUint(rawPort, 10, 16) if err != nil { return 0, err } return int(port), nil } // Config is the list of configuration options used when creating a container. // Config does not contain the options that are specific to starting a container on a // given host. Those are contained in HostConfig type Config struct { Hostname string `json:"Hostname,omitempty" yaml:"Hostname,omitempty" toml:"Hostname,omitempty"` Domainname string `json:"Domainname,omitempty" yaml:"Domainname,omitempty" toml:"Domainname,omitempty"` User string `json:"User,omitempty" yaml:"User,omitempty" toml:"User,omitempty"` Memory int64 `json:"Memory,omitempty" yaml:"Memory,omitempty" toml:"Memory,omitempty"` MemorySwap int64 `json:"MemorySwap,omitempty" yaml:"MemorySwap,omitempty" toml:"MemorySwap,omitempty"` MemoryReservation int64 `json:"MemoryReservation,omitempty" yaml:"MemoryReservation,omitempty" toml:"MemoryReservation,omitempty"` KernelMemory int64 `json:"KernelMemory,omitempty" yaml:"KernelMemory,omitempty" toml:"KernelMemory,omitempty"` CPUShares int64 `json:"CpuShares,omitempty" yaml:"CpuShares,omitempty" toml:"CpuShares,omitempty"` CPUSet string `json:"Cpuset,omitempty" yaml:"Cpuset,omitempty" toml:"Cpuset,omitempty"` PortSpecs []string `json:"PortSpecs,omitempty" yaml:"PortSpecs,omitempty" toml:"PortSpecs,omitempty"` ExposedPorts map[Port]struct{} `json:"ExposedPorts,omitempty" yaml:"ExposedPorts,omitempty" toml:"ExposedPorts,omitempty"` PublishService string `json:"PublishService,omitempty" yaml:"PublishService,omitempty" toml:"PublishService,omitempty"` StopSignal string `json:"StopSignal,omitempty" yaml:"StopSignal,omitempty" toml:"StopSignal,omitempty"` StopTimeout int `json:"StopTimeout,omitempty" yaml:"StopTimeout,omitempty" toml:"StopTimeout,omitempty"` Env []string `json:"Env,omitempty" yaml:"Env,omitempty" toml:"Env,omitempty"` Cmd []string `json:"Cmd" yaml:"Cmd" toml:"Cmd"` Shell []string `json:"Shell,omitempty" yaml:"Shell,omitempty" toml:"Shell,omitempty"` Healthcheck *HealthConfig `json:"Healthcheck,omitempty" yaml:"Healthcheck,omitempty" toml:"Healthcheck,omitempty"` DNS []string `json:"Dns,omitempty" yaml:"Dns,omitempty" toml:"Dns,omitempty"` // For Docker API v1.9 and below only Image string `json:"Image,omitempty" yaml:"Image,omitempty" toml:"Image,omitempty"` Volumes map[string]struct{} `json:"Volumes,omitempty" yaml:"Volumes,omitempty" toml:"Volumes,omitempty"` VolumeDriver string `json:"VolumeDriver,omitempty" yaml:"VolumeDriver,omitempty" toml:"VolumeDriver,omitempty"` WorkingDir string `json:"WorkingDir,omitempty" yaml:"WorkingDir,omitempty" toml:"WorkingDir,omitempty"` MacAddress string `json:"MacAddress,omitempty" yaml:"MacAddress,omitempty" toml:"MacAddress,omitempty"` Entrypoint []string `json:"Entrypoint" yaml:"Entrypoint" toml:"Entrypoint"` SecurityOpts []string `json:"SecurityOpts,omitempty" yaml:"SecurityOpts,omitempty" toml:"SecurityOpts,omitempty"` OnBuild []string `json:"OnBuild,omitempty" yaml:"OnBuild,omitempty" toml:"OnBuild,omitempty"` Mounts []Mount `json:"Mounts,omitempty" yaml:"Mounts,omitempty" toml:"Mounts,omitempty"` Labels map[string]string `json:"Labels,omitempty" yaml:"Labels,omitempty" toml:"Labels,omitempty"` AttachStdin bool `json:"AttachStdin,omitempty" yaml:"AttachStdin,omitempty" toml:"AttachStdin,omitempty"` AttachStdout bool `json:"AttachStdout,omitempty" yaml:"AttachStdout,omitempty" toml:"AttachStdout,omitempty"` AttachStderr bool `json:"AttachStderr,omitempty" yaml:"AttachStderr,omitempty" toml:"AttachStderr,omitempty"` ArgsEscaped bool `json:"ArgsEscaped,omitempty" yaml:"ArgsEscaped,omitempty" toml:"ArgsEscaped,omitempty"` Tty bool `json:"Tty,omitempty" yaml:"Tty,omitempty" toml:"Tty,omitempty"` OpenStdin bool `json:"OpenStdin,omitempty" yaml:"OpenStdin,omitempty" toml:"OpenStdin,omitempty"` StdinOnce bool `json:"StdinOnce,omitempty" yaml:"StdinOnce,omitempty" toml:"StdinOnce,omitempty"` NetworkDisabled bool `json:"NetworkDisabled,omitempty" yaml:"NetworkDisabled,omitempty" toml:"NetworkDisabled,omitempty"` // This is no longer used and has been kept here for backward // compatibility, please use HostConfig.VolumesFrom. VolumesFrom string `json:"VolumesFrom,omitempty" yaml:"VolumesFrom,omitempty" toml:"VolumesFrom,omitempty"` } // HostMount represents a mount point in the container in HostConfig. // // It has been added in the version 1.25 of the Docker API type HostMount struct { Target string `json:"Target,omitempty" yaml:"Target,omitempty" toml:"Target,omitempty"` Source string `json:"Source,omitempty" yaml:"Source,omitempty" toml:"Source,omitempty"` Type string `json:"Type,omitempty" yaml:"Type,omitempty" toml:"Type,omitempty"` ReadOnly bool `json:"ReadOnly,omitempty" yaml:"ReadOnly,omitempty" toml:"ReadOnly,omitempty"` BindOptions *BindOptions `json:"BindOptions,omitempty" yaml:"BindOptions,omitempty" toml:"BindOptions,omitempty"` VolumeOptions *VolumeOptions `json:"VolumeOptions,omitempty" yaml:"VolumeOptions,omitempty" toml:"VolumeOptions,omitempty"` TempfsOptions *TempfsOptions `json:"TmpfsOptions,omitempty" yaml:"TmpfsOptions,omitempty" toml:"TmpfsOptions,omitempty"` } // BindOptions contains optional configuration for the bind type type BindOptions struct { Propagation string `json:"Propagation,omitempty" yaml:"Propagation,omitempty" toml:"Propagation,omitempty"` } // VolumeOptions contains optional configuration for the volume type type VolumeOptions struct { NoCopy bool `json:"NoCopy,omitempty" yaml:"NoCopy,omitempty" toml:"NoCopy,omitempty"` Labels map[string]string `json:"Labels,omitempty" yaml:"Labels,omitempty" toml:"Labels,omitempty"` DriverConfig VolumeDriverConfig `json:"DriverConfig,omitempty" yaml:"DriverConfig,omitempty" toml:"DriverConfig,omitempty"` } // TempfsOptions contains optional configuration for the tempfs type type TempfsOptions struct { SizeBytes int64 `json:"SizeBytes,omitempty" yaml:"SizeBytes,omitempty" toml:"SizeBytes,omitempty"` Mode int `json:"Mode,omitempty" yaml:"Mode,omitempty" toml:"Mode,omitempty"` Options [][]string `json:"Options,omitempty" yaml:"Options,omitempty" toml:"Options,omitempty"` } // VolumeDriverConfig holds a map of volume driver specific options type VolumeDriverConfig struct { Name string `json:"Name,omitempty" yaml:"Name,omitempty" toml:"Name,omitempty"` Options map[string]string `json:"Options,omitempty" yaml:"Options,omitempty" toml:"Options,omitempty"` } // Mount represents a mount point in the container. // // It has been added in the version 1.20 of the Docker API, available since // Docker 1.8. type Mount struct { Name string Source string Destination string Driver string Mode string RW bool } // LogConfig defines the log driver type and the configuration for it. type LogConfig struct { Type string `json:"Type,omitempty" yaml:"Type,omitempty" toml:"Type,omitempty"` Config map[string]string `json:"Config,omitempty" yaml:"Config,omitempty" toml:"Config,omitempty"` } // ULimit defines system-wide resource limitations This can help a lot in // system administration, e.g. when a user starts too many processes and // therefore makes the system unresponsive for other users. type ULimit struct { Name string `json:"Name,omitempty" yaml:"Name,omitempty" toml:"Name,omitempty"` Soft int64 `json:"Soft,omitempty" yaml:"Soft,omitempty" toml:"Soft,omitempty"` Hard int64 `json:"Hard,omitempty" yaml:"Hard,omitempty" toml:"Hard,omitempty"` } // SwarmNode containers information about which Swarm node the container is on. type SwarmNode struct { ID string `json:"ID,omitempty" yaml:"ID,omitempty" toml:"ID,omitempty"` IP string `json:"IP,omitempty" yaml:"IP,omitempty" toml:"IP,omitempty"` Addr string `json:"Addr,omitempty" yaml:"Addr,omitempty" toml:"Addr,omitempty"` Name string `json:"Name,omitempty" yaml:"Name,omitempty" toml:"Name,omitempty"` CPUs int64 `json:"CPUs,omitempty" yaml:"CPUs,omitempty" toml:"CPUs,omitempty"` Memory int64 `json:"Memory,omitempty" yaml:"Memory,omitempty" toml:"Memory,omitempty"` Labels map[string]string `json:"Labels,omitempty" yaml:"Labels,omitempty" toml:"Labels,omitempty"` } // GraphDriver contains information about the GraphDriver used by the // container. type GraphDriver struct { Name string `json:"Name,omitempty" yaml:"Name,omitempty" toml:"Name,omitempty"` Data map[string]string `json:"Data,omitempty" yaml:"Data,omitempty" toml:"Data,omitempty"` } // HealthConfig holds configuration settings for the HEALTHCHECK feature // // It has been added in the version 1.24 of the Docker API, available since // Docker 1.12. type HealthConfig struct { // Test is the test to perform to check that the container is healthy. // An empty slice means to inherit the default. // The options are: // {} : inherit healthcheck // {"NONE"} : disable healthcheck // {"CMD", args...} : exec arguments directly // {"CMD-SHELL", command} : run command with system's default shell Test []string `json:"Test,omitempty" yaml:"Test,omitempty" toml:"Test,omitempty"` // Zero means to inherit. Durations are expressed as integer nanoseconds. Interval time.Duration `json:"Interval,omitempty" yaml:"Interval,omitempty" toml:"Interval,omitempty"` // Interval is the time to wait between checks. Timeout time.Duration `json:"Timeout,omitempty" yaml:"Timeout,omitempty" toml:"Timeout,omitempty"` // Timeout is the time to wait before considering the check to have hung. StartPeriod time.Duration `json:"StartPeriod,omitempty" yaml:"StartPeriod,omitempty" toml:"StartPeriod,omitempty"` // The start period for the container to initialize before the retries starts to count down. StartInterval time.Duration `json:"StartInterval,omitempty" yaml:"StartInterval,omitempty" toml:"StartInterval,omitempty"` // The start interval is the time to wait between checks during the start period. // Retries is the number of consecutive failures needed to consider a container as unhealthy. // Zero means inherit. Retries int `json:"Retries,omitempty" yaml:"Retries,omitempty" toml:"Retries,omitempty"` } // Container is the type encompasing everything about a container - its config, // hostconfig, etc. type Container struct { ID string `json:"Id" yaml:"Id" toml:"Id"` Created time.Time `json:"Created,omitempty" yaml:"Created,omitempty" toml:"Created,omitempty"` Path string `json:"Path,omitempty" yaml:"Path,omitempty" toml:"Path,omitempty"` Args []string `json:"Args,omitempty" yaml:"Args,omitempty" toml:"Args,omitempty"` Config *Config `json:"Config,omitempty" yaml:"Config,omitempty" toml:"Config,omitempty"` State State `json:"State,omitempty" yaml:"State,omitempty" toml:"State,omitempty"` Image string `json:"Image,omitempty" yaml:"Image,omitempty" toml:"Image,omitempty"` Node *SwarmNode `json:"Node,omitempty" yaml:"Node,omitempty" toml:"Node,omitempty"` NetworkSettings *NetworkSettings `json:"NetworkSettings,omitempty" yaml:"NetworkSettings,omitempty" toml:"NetworkSettings,omitempty"` SysInitPath string `json:"SysInitPath,omitempty" yaml:"SysInitPath,omitempty" toml:"SysInitPath,omitempty"` ResolvConfPath string `json:"ResolvConfPath,omitempty" yaml:"ResolvConfPath,omitempty" toml:"ResolvConfPath,omitempty"` HostnamePath string `json:"HostnamePath,omitempty" yaml:"HostnamePath,omitempty" toml:"HostnamePath,omitempty"` HostsPath string `json:"HostsPath,omitempty" yaml:"HostsPath,omitempty" toml:"HostsPath,omitempty"` LogPath string `json:"LogPath,omitempty" yaml:"LogPath,omitempty" toml:"LogPath,omitempty"` Name string `json:"Name,omitempty" yaml:"Name,omitempty" toml:"Name,omitempty"` Driver string `json:"Driver,omitempty" yaml:"Driver,omitempty" toml:"Driver,omitempty"` Mounts []Mount `json:"Mounts,omitempty" yaml:"Mounts,omitempty" toml:"Mounts,omitempty"` Volumes map[string]string `json:"Volumes,omitempty" yaml:"Volumes,omitempty" toml:"Volumes,omitempty"` VolumesRW map[string]bool `json:"VolumesRW,omitempty" yaml:"VolumesRW,omitempty" toml:"VolumesRW,omitempty"` HostConfig *HostConfig `json:"HostConfig,omitempty" yaml:"HostConfig,omitempty" toml:"HostConfig,omitempty"` ExecIDs []string `json:"ExecIDs,omitempty" yaml:"ExecIDs,omitempty" toml:"ExecIDs,omitempty"` GraphDriver *GraphDriver `json:"GraphDriver,omitempty" yaml:"GraphDriver,omitempty" toml:"GraphDriver,omitempty"` RestartCount int `json:"RestartCount,omitempty" yaml:"RestartCount,omitempty" toml:"RestartCount,omitempty"` AppArmorProfile string `json:"AppArmorProfile,omitempty" yaml:"AppArmorProfile,omitempty" toml:"AppArmorProfile,omitempty"` MountLabel string `json:"MountLabel,omitempty" yaml:"MountLabel,omitempty" toml:"MountLabel,omitempty"` ProcessLabel string `json:"ProcessLabel,omitempty" yaml:"ProcessLabel,omitempty" toml:"ProcessLabel,omitempty"` Platform string `json:"Platform,omitempty" yaml:"Platform,omitempty" toml:"Platform,omitempty"` SizeRw int64 `json:"SizeRw,omitempty" yaml:"SizeRw,omitempty" toml:"SizeRw,omitempty"` SizeRootFs int64 `json:"SizeRootFs,omitempty" yaml:"SizeRootFs,omitempty" toml:"SizeRootFs,omitempty"` } // KeyValuePair is a type for generic key/value pairs as used in the Lxc // configuration type KeyValuePair struct { Key string `json:"Key,omitempty" yaml:"Key,omitempty" toml:"Key,omitempty"` Value string `json:"Value,omitempty" yaml:"Value,omitempty" toml:"Value,omitempty"` } // Device represents a device mapping between the Docker host and the // container. type Device struct { PathOnHost string `json:"PathOnHost,omitempty" yaml:"PathOnHost,omitempty" toml:"PathOnHost,omitempty"` PathInContainer string `json:"PathInContainer,omitempty" yaml:"PathInContainer,omitempty" toml:"PathInContainer,omitempty"` CgroupPermissions string `json:"CgroupPermissions,omitempty" yaml:"CgroupPermissions,omitempty" toml:"CgroupPermissions,omitempty"` } // DeviceRequest represents a request for device that's sent to device drivers. type DeviceRequest struct { Driver string `json:"Driver,omitempty" yaml:"Driver,omitempty" toml:"Driver,omitempty"` Count int `json:"Count,omitempty" yaml:"Count,omitempty" toml:"Count,omitempty"` DeviceIDs []string `json:"DeviceIDs,omitempty" yaml:"DeviceIDs,omitempty" toml:"DeviceIDs,omitempty"` Capabilities [][]string `json:"Capabilities,omitempty" yaml:"Capabilities,omitempty" toml:"Capabilities,omitempty"` Options map[string]string `json:"Options,omitempty" yaml:"Options,omitempty" toml:"Options,omitempty"` } // BlockWeight represents a relative device weight for an individual device inside // of a container type BlockWeight struct { Path string `json:"Path,omitempty"` Weight string `json:"Weight,omitempty"` } // BlockLimit represents a read/write limit in IOPS or Bandwidth for a device // inside of a container type BlockLimit struct { Path string `json:"Path,omitempty"` Rate int64 `json:"Rate,omitempty"` } // HostConfig contains the container options related to starting a container on // a given host type HostConfig struct { Binds []string `json:"Binds,omitempty" yaml:"Binds,omitempty" toml:"Binds,omitempty"` CapAdd []string `json:"CapAdd,omitempty" yaml:"CapAdd,omitempty" toml:"CapAdd,omitempty"` CapDrop []string `json:"CapDrop,omitempty" yaml:"CapDrop,omitempty" toml:"CapDrop,omitempty"` Capabilities []string `json:"Capabilities,omitempty" yaml:"Capabilities,omitempty" toml:"Capabilities,omitempty"` // Mutually exclusive w.r.t. CapAdd and CapDrop API v1.40 GroupAdd []string `json:"GroupAdd,omitempty" yaml:"GroupAdd,omitempty" toml:"GroupAdd,omitempty"` ContainerIDFile string `json:"ContainerIDFile,omitempty" yaml:"ContainerIDFile,omitempty" toml:"ContainerIDFile,omitempty"` LxcConf []KeyValuePair `json:"LxcConf,omitempty" yaml:"LxcConf,omitempty" toml:"LxcConf,omitempty"` PortBindings map[Port][]PortBinding `json:"PortBindings,omitempty" yaml:"PortBindings,omitempty" toml:"PortBindings,omitempty"` Links []string `json:"Links,omitempty" yaml:"Links,omitempty" toml:"Links,omitempty"` DNS []string `json:"Dns,omitempty" yaml:"Dns,omitempty" toml:"Dns,omitempty"` // For Docker API v1.10 and above only DNSOptions []string `json:"DnsOptions,omitempty" yaml:"DnsOptions,omitempty" toml:"DnsOptions,omitempty"` DNSSearch []string `json:"DnsSearch,omitempty" yaml:"DnsSearch,omitempty" toml:"DnsSearch,omitempty"` ExtraHosts []string `json:"ExtraHosts,omitempty" yaml:"ExtraHosts,omitempty" toml:"ExtraHosts,omitempty"` VolumesFrom []string `json:"VolumesFrom,omitempty" yaml:"VolumesFrom,omitempty" toml:"VolumesFrom,omitempty"` UsernsMode string `json:"UsernsMode,omitempty" yaml:"UsernsMode,omitempty" toml:"UsernsMode,omitempty"` NetworkMode string `json:"NetworkMode,omitempty" yaml:"NetworkMode,omitempty" toml:"NetworkMode,omitempty"` IpcMode string `json:"IpcMode,omitempty" yaml:"IpcMode,omitempty" toml:"IpcMode,omitempty"` Isolation string `json:"Isolation,omitempty" yaml:"Isolation,omitempty" toml:"Isolation,omitempty"` // Windows only ConsoleSize [2]int `json:"ConsoleSize,omitempty" yaml:"ConsoleSize,omitempty" toml:"ConsoleSize,omitempty"` // Windows only height x width PidMode string `json:"PidMode,omitempty" yaml:"PidMode,omitempty" toml:"PidMode,omitempty"` UTSMode string `json:"UTSMode,omitempty" yaml:"UTSMode,omitempty" toml:"UTSMode,omitempty"` RestartPolicy RestartPolicy `json:"RestartPolicy,omitempty" yaml:"RestartPolicy,omitempty" toml:"RestartPolicy,omitempty"` Devices []Device `json:"Devices,omitempty" yaml:"Devices,omitempty" toml:"Devices,omitempty"` DeviceCgroupRules []string `json:"DeviceCgroupRules,omitempty" yaml:"DeviceCgroupRules,omitempty" toml:"DeviceCgroupRules,omitempty"` DeviceRequests []DeviceRequest `json:"DeviceRequests,omitempty" yaml:"DeviceRequests,omitempty" toml:"DeviceRequests,omitempty"` LogConfig LogConfig `json:"LogConfig,omitempty" yaml:"LogConfig,omitempty" toml:"LogConfig,omitempty"` SecurityOpt []string `json:"SecurityOpt,omitempty" yaml:"SecurityOpt,omitempty" toml:"SecurityOpt,omitempty"` CgroupnsMode string `json:"CgroupnsMode,omitempty" yaml:"CgroupnsMode,omitempty" toml:"CgroupnsMode,omitempty"` // v1.40+ Cgroup string `json:"Cgroup,omitempty" yaml:"Cgroup,omitempty" toml:"Cgroup,omitempty"` CgroupParent string `json:"CgroupParent,omitempty" yaml:"CgroupParent,omitempty" toml:"CgroupParent,omitempty"` Memory int64 `json:"Memory,omitempty" yaml:"Memory,omitempty" toml:"Memory,omitempty"` MemoryReservation int64 `json:"MemoryReservation,omitempty" yaml:"MemoryReservation,omitempty" toml:"MemoryReservation,omitempty"` // Deprecated: KernelMemory is deprecated as of API 1.42 and removed in API 1.52. KernelMemory int64 `json:"KernelMemory,omitempty" yaml:"KernelMemory,omitempty" toml:"KernelMemory,omitempty"` MemorySwap int64 `json:"MemorySwap,omitempty" yaml:"MemorySwap,omitempty" toml:"MemorySwap,omitempty"` CPUShares int64 `json:"CpuShares,omitempty" yaml:"CpuShares,omitempty" toml:"CpuShares,omitempty"` CPUSet string `json:"Cpuset,omitempty" yaml:"Cpuset,omitempty" toml:"Cpuset,omitempty"` CPUSetCPUs string `json:"CpusetCpus,omitempty" yaml:"CpusetCpus,omitempty" toml:"CpusetCpus,omitempty"` CPUSetMEMs string `json:"CpusetMems,omitempty" yaml:"CpusetMems,omitempty" toml:"CpusetMems,omitempty"` CPUQuota int64 `json:"CpuQuota,omitempty" yaml:"CpuQuota,omitempty" toml:"CpuQuota,omitempty"` CPUPeriod int64 `json:"CpuPeriod,omitempty" yaml:"CpuPeriod,omitempty" toml:"CpuPeriod,omitempty"` CPURealtimePeriod int64 `json:"CpuRealtimePeriod,omitempty" yaml:"CpuRealtimePeriod,omitempty" toml:"CpuRealtimePeriod,omitempty"` CPURealtimeRuntime int64 `json:"CpuRealtimeRuntime,omitempty" yaml:"CpuRealtimeRuntime,omitempty" toml:"CpuRealtimeRuntime,omitempty"` NanoCPUs int64 `json:"NanoCpus,omitempty" yaml:"NanoCpus,omitempty" toml:"NanoCpus,omitempty"` BlkioWeight int64 `json:"BlkioWeight,omitempty" yaml:"BlkioWeight,omitempty" toml:"BlkioWeight,omitempty"` BlkioWeightDevice []BlockWeight `json:"BlkioWeightDevice,omitempty" yaml:"BlkioWeightDevice,omitempty" toml:"BlkioWeightDevice,omitempty"` BlkioDeviceReadBps []BlockLimit `json:"BlkioDeviceReadBps,omitempty" yaml:"BlkioDeviceReadBps,omitempty" toml:"BlkioDeviceReadBps,omitempty"` BlkioDeviceReadIOps []BlockLimit `json:"BlkioDeviceReadIOps,omitempty" yaml:"BlkioDeviceReadIOps,omitempty" toml:"BlkioDeviceReadIOps,omitempty"` BlkioDeviceWriteBps []BlockLimit `json:"BlkioDeviceWriteBps,omitempty" yaml:"BlkioDeviceWriteBps,omitempty" toml:"BlkioDeviceWriteBps,omitempty"` BlkioDeviceWriteIOps []BlockLimit `json:"BlkioDeviceWriteIOps,omitempty" yaml:"BlkioDeviceWriteIOps,omitempty" toml:"BlkioDeviceWriteIOps,omitempty"` Ulimits []ULimit `json:"Ulimits,omitempty" yaml:"Ulimits,omitempty" toml:"Ulimits,omitempty"` VolumeDriver string `json:"VolumeDriver,omitempty" yaml:"VolumeDriver,omitempty" toml:"VolumeDriver,omitempty"` OomScoreAdj int `json:"OomScoreAdj,omitempty" yaml:"OomScoreAdj,omitempty" toml:"OomScoreAdj,omitempty"` MemorySwappiness *int64 `json:"MemorySwappiness,omitempty" yaml:"MemorySwappiness,omitempty" toml:"MemorySwappiness,omitempty"` PidsLimit *int64 `json:"PidsLimit,omitempty" yaml:"PidsLimit,omitempty" toml:"PidsLimit,omitempty"` OOMKillDisable *bool `json:"OomKillDisable,omitempty" yaml:"OomKillDisable,omitempty" toml:"OomKillDisable,omitempty"` ShmSize int64 `json:"ShmSize,omitempty" yaml:"ShmSize,omitempty" toml:"ShmSize,omitempty"` Tmpfs map[string]string `json:"Tmpfs,omitempty" yaml:"Tmpfs,omitempty" toml:"Tmpfs,omitempty"` StorageOpt map[string]string `json:"StorageOpt,omitempty" yaml:"StorageOpt,omitempty" toml:"StorageOpt,omitempty"` Sysctls map[string]string `json:"Sysctls,omitempty" yaml:"Sysctls,omitempty" toml:"Sysctls,omitempty"` CPUCount int64 `json:"CpuCount,omitempty" yaml:"CpuCount,omitempty"` CPUPercent int64 `json:"CpuPercent,omitempty" yaml:"CpuPercent,omitempty"` IOMaximumBandwidth int64 `json:"IOMaximumBandwidth,omitempty" yaml:"IOMaximumBandwidth,omitempty"` IOMaximumIOps int64 `json:"IOMaximumIOps,omitempty" yaml:"IOMaximumIOps,omitempty"` Mounts []HostMount `json:"Mounts,omitempty" yaml:"Mounts,omitempty" toml:"Mounts,omitempty"` MaskedPaths []string `json:"MaskedPaths,omitempty" yaml:"MaskedPaths,omitempty" toml:"MaskedPaths,omitempty"` ReadonlyPaths []string `json:"ReadonlyPaths,omitempty" yaml:"ReadonlyPaths,omitempty" toml:"ReadonlyPaths,omitempty"` Runtime string `json:"Runtime,omitempty" yaml:"Runtime,omitempty" toml:"Runtime,omitempty"` Init bool `json:",omitempty" yaml:",omitempty"` Privileged bool `json:"Privileged,omitempty" yaml:"Privileged,omitempty" toml:"Privileged,omitempty"` PublishAllPorts bool `json:"PublishAllPorts,omitempty" yaml:"PublishAllPorts,omitempty" toml:"PublishAllPorts,omitempty"` ReadonlyRootfs bool `json:"ReadonlyRootfs,omitempty" yaml:"ReadonlyRootfs,omitempty" toml:"ReadonlyRootfs,omitempty"` AutoRemove bool `json:"AutoRemove,omitempty" yaml:"AutoRemove,omitempty" toml:"AutoRemove,omitempty"` Annotations map[string]string `json:"Annotations,omitempty" yaml:"Annotations,omitempty" toml:"Annotations,omitempty"` } // NetworkingConfig represents the container's networking configuration for each of its interfaces // Carries the networking configs specified in the `docker run` and `docker network connect` commands type NetworkingConfig struct { EndpointsConfig map[string]*EndpointConfig `json:"EndpointsConfig" yaml:"EndpointsConfig" toml:"EndpointsConfig"` // Endpoint configs for each connecting network } // NoSuchContainer is the error returned when a given container does not exist. type NoSuchContainer struct { ID string Err error } func (err *NoSuchContainer) Error() string { if err.Err != nil { return err.Err.Error() } return "No such container: " + err.ID } // ContainerAlreadyRunning is the error returned when a given container is // already running. type ContainerAlreadyRunning struct { ID string } func (err *ContainerAlreadyRunning) Error() string { return "Container already running: " + err.ID } // ContainerNotRunning is the error returned when a given container is not // running. type ContainerNotRunning struct { ID string } func (err *ContainerNotRunning) Error() string { return "Container not running: " + err.ID } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/container_archive.go ================================================ package docker import ( "context" "fmt" "io" "net/http" "time" ) // UploadToContainerOptions is the set of options that can be used when // uploading an archive into a container. // // See https://goo.gl/g25o7u for more details. type UploadToContainerOptions struct { InputStream io.Reader `json:"-" qs:"-"` Path string `qs:"path"` NoOverwriteDirNonDir bool `qs:"noOverwriteDirNonDir"` Context context.Context } // UploadToContainer uploads a tar archive to be extracted to a path in the // filesystem of the container. // // See https://goo.gl/g25o7u for more details. func (c *Client) UploadToContainer(id string, opts UploadToContainerOptions) error { url := fmt.Sprintf("/containers/%s/archive?", id) + queryString(opts) return c.stream(http.MethodPut, url, streamOptions{ in: opts.InputStream, context: opts.Context, }) } // DownloadFromContainerOptions is the set of options that can be used when // downloading resources from a container. // // See https://goo.gl/W49jxK for more details. type DownloadFromContainerOptions struct { OutputStream io.Writer `json:"-" qs:"-"` Path string `qs:"path"` InactivityTimeout time.Duration `qs:"-"` Context context.Context } // DownloadFromContainer downloads a tar archive of files or folders in a container. // // See https://goo.gl/W49jxK for more details. func (c *Client) DownloadFromContainer(id string, opts DownloadFromContainerOptions) error { url := fmt.Sprintf("/containers/%s/archive?", id) + queryString(opts) return c.stream(http.MethodGet, url, streamOptions{ setRawTerminal: true, stdout: opts.OutputStream, inactivityTimeout: opts.InactivityTimeout, context: opts.Context, }) } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/container_attach.go ================================================ package docker import ( "io" "net/http" ) // AttachToContainerOptions is the set of options that can be used when // attaching to a container. // // See https://goo.gl/JF10Zk for more details. type AttachToContainerOptions struct { Container string `qs:"-"` InputStream io.Reader `qs:"-"` OutputStream io.Writer `qs:"-"` ErrorStream io.Writer `qs:"-"` // If set, after a successful connect, a sentinel will be sent and then the // client will block on receive before continuing. // // It must be an unbuffered channel. Using a buffered channel can lead // to unexpected behavior. Success chan struct{} // Override the key sequence for detaching a container. DetachKeys string // Use raw terminal? Usually true when the container contains a TTY. RawTerminal bool `qs:"-"` // Get container logs, sending it to OutputStream. Logs bool // Stream the response? Stream bool // Attach to stdin, and use InputStream. Stdin bool // Attach to stdout, and use OutputStream. Stdout bool // Attach to stderr, and use ErrorStream. Stderr bool } // AttachToContainer attaches to a container, using the given options. // // See https://goo.gl/JF10Zk for more details. func (c *Client) AttachToContainer(opts AttachToContainerOptions) error { cw, err := c.AttachToContainerNonBlocking(opts) if err != nil { return err } return cw.Wait() } // AttachToContainerNonBlocking attaches to a container, using the given options. // This function does not block. // // See https://goo.gl/NKpkFk for more details. func (c *Client) AttachToContainerNonBlocking(opts AttachToContainerOptions) (CloseWaiter, error) { if opts.Container == "" { return nil, &NoSuchContainer{ID: opts.Container} } path := "/containers/" + opts.Container + "/attach?" + queryString(opts) return c.hijack(http.MethodPost, path, hijackOptions{ success: opts.Success, setRawTerminal: opts.RawTerminal, in: opts.InputStream, stdout: opts.OutputStream, stderr: opts.ErrorStream, }) } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/container_changes.go ================================================ package docker import ( "encoding/json" "errors" "net/http" ) // ContainerChanges returns changes in the filesystem of the given container. // // See https://goo.gl/15KKzh for more details. func (c *Client) ContainerChanges(id string) ([]Change, error) { path := "/containers/" + id + "/changes" resp, err := c.do(http.MethodGet, path, doOptions{}) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return nil, &NoSuchContainer{ID: id} } return nil, err } defer resp.Body.Close() var changes []Change if err := json.NewDecoder(resp.Body).Decode(&changes); err != nil { return nil, err } return changes, nil } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/container_commit.go ================================================ package docker import ( "context" "encoding/json" "errors" "net/http" ) // CommitContainerOptions aggregates parameters to the CommitContainer method. // // See https://goo.gl/CzIguf for more details. type CommitContainerOptions struct { Container string Repository string `qs:"repo"` Tag string Message string `qs:"comment"` Author string Changes []string `qs:"changes"` Run *Config `qs:"-"` Context context.Context } // CommitContainer creates a new image from a container's changes. // // See https://goo.gl/CzIguf for more details. func (c *Client) CommitContainer(opts CommitContainerOptions) (*Image, error) { path := "/commit?" + queryString(opts) resp, err := c.do(http.MethodPost, path, doOptions{ data: opts.Run, context: opts.Context, }) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return nil, &NoSuchContainer{ID: opts.Container} } return nil, err } defer resp.Body.Close() var image Image if err := json.NewDecoder(resp.Body).Decode(&image); err != nil { return nil, err } return &image, nil } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/container_copy.go ================================================ package docker import ( "context" "errors" "fmt" "io" "net/http" ) // CopyFromContainerOptions contains the set of options used for copying // files from a container. // // Deprecated: Use DownloadFromContainerOptions and DownloadFromContainer instead. type CopyFromContainerOptions struct { OutputStream io.Writer `json:"-"` Container string `json:"-"` Resource string Context context.Context `json:"-"` } // CopyFromContainer copies files from a container. // // Deprecated: Use DownloadFromContainer and DownloadFromContainer instead. func (c *Client) CopyFromContainer(opts CopyFromContainerOptions) error { if opts.Container == "" { return &NoSuchContainer{ID: opts.Container} } if c.serverAPIVersion == nil { c.checkAPIVersion() } if c.serverAPIVersion != nil && c.serverAPIVersion.GreaterThanOrEqualTo(apiVersion124) { return errors.New("go-dockerclient: CopyFromContainer is no longer available in Docker >= 1.12, use DownloadFromContainer instead") } url := fmt.Sprintf("/containers/%s/copy", opts.Container) resp, err := c.do(http.MethodPost, url, doOptions{ data: opts, context: opts.Context, }) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return &NoSuchContainer{ID: opts.Container} } return err } defer resp.Body.Close() _, err = io.Copy(opts.OutputStream, resp.Body) return err } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/container_create.go ================================================ package docker import ( "context" "encoding/json" "errors" "net/http" "strings" ) // ErrContainerAlreadyExists is the error returned by CreateContainer when the // container already exists. var ErrContainerAlreadyExists = errors.New("container already exists") // CreateContainerOptions specify parameters to the CreateContainer function. // // See https://goo.gl/tyzwVM for more details. type CreateContainerOptions struct { Name string Platform string Config *Config `qs:"-"` HostConfig *HostConfig `qs:"-"` NetworkingConfig *NetworkingConfig `qs:"-"` Context context.Context } // CreateContainer creates a new container, returning the container instance, // or an error in case of failure. // // The returned container instance contains only the container ID. To get more // details about the container after creating it, use InspectContainer. // // See https://goo.gl/tyzwVM for more details. func (c *Client) CreateContainer(opts CreateContainerOptions) (*Container, error) { path := "/containers/create?" + queryString(opts) resp, err := c.do( http.MethodPost, path, doOptions{ data: struct { *Config HostConfig *HostConfig `json:"HostConfig,omitempty" yaml:"HostConfig,omitempty" toml:"HostConfig,omitempty"` NetworkingConfig *NetworkingConfig `json:"NetworkingConfig,omitempty" yaml:"NetworkingConfig,omitempty" toml:"NetworkingConfig,omitempty"` }{ opts.Config, opts.HostConfig, opts.NetworkingConfig, }, context: opts.Context, }, ) var e *Error if errors.As(err, &e) { if e.Status == http.StatusNotFound && strings.Contains(e.Message, "No such image") { return nil, ErrNoSuchImage } if e.Status == http.StatusConflict { return nil, ErrContainerAlreadyExists } // Workaround for 17.09 bug returning 400 instead of 409. // See https://github.com/moby/moby/issues/35021 if e.Status == http.StatusBadRequest && strings.Contains(e.Message, "Conflict.") { return nil, ErrContainerAlreadyExists } } if err != nil { return nil, err } defer resp.Body.Close() var container Container if err := json.NewDecoder(resp.Body).Decode(&container); err != nil { return nil, err } container.Name = opts.Name return &container, nil } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/container_export.go ================================================ package docker import ( "context" "fmt" "io" "net/http" "time" ) // ExportContainerOptions is the set of parameters to the ExportContainer // method. // // See https://goo.gl/yGJCIh for more details. type ExportContainerOptions struct { ID string OutputStream io.Writer InactivityTimeout time.Duration `qs:"-"` Context context.Context } // ExportContainer export the contents of container id as tar archive // and prints the exported contents to stdout. // // See https://goo.gl/yGJCIh for more details. func (c *Client) ExportContainer(opts ExportContainerOptions) error { if opts.ID == "" { return &NoSuchContainer{ID: opts.ID} } url := fmt.Sprintf("/containers/%s/export", opts.ID) return c.stream(http.MethodGet, url, streamOptions{ setRawTerminal: true, stdout: opts.OutputStream, inactivityTimeout: opts.InactivityTimeout, context: opts.Context, }) } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/container_inspect.go ================================================ package docker import ( "context" "encoding/json" "errors" "net/http" ) // InspectContainer returns information about a container by its ID. // // Deprecated: Use InspectContainerWithOptions instead. func (c *Client) InspectContainer(id string) (*Container, error) { return c.InspectContainerWithOptions(InspectContainerOptions{ID: id}) } // InspectContainerWithContext returns information about a container by its ID. // The context object can be used to cancel the inspect request. // // Deprecated: Use InspectContainerWithOptions instead. func (c *Client) InspectContainerWithContext(id string, ctx context.Context) (*Container, error) { return c.InspectContainerWithOptions(InspectContainerOptions{ID: id, Context: ctx}) } // InspectContainerWithOptions returns information about a container by its ID. // // See https://goo.gl/FaI5JT for more details. func (c *Client) InspectContainerWithOptions(opts InspectContainerOptions) (*Container, error) { path := "/containers/" + opts.ID + "/json?" + queryString(opts) resp, err := c.do(http.MethodGet, path, doOptions{ context: opts.Context, }) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return nil, &NoSuchContainer{ID: opts.ID} } return nil, err } defer resp.Body.Close() var container Container if err := json.NewDecoder(resp.Body).Decode(&container); err != nil { return nil, err } return &container, nil } // InspectContainerOptions specifies parameters for InspectContainerWithOptions. // // See https://goo.gl/FaI5JT for more details. type InspectContainerOptions struct { Context context.Context ID string `qs:"-"` Size bool } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/container_kill.go ================================================ package docker import ( "context" "errors" "net/http" ) // KillContainerOptions represents the set of options that can be used in a // call to KillContainer. // // See https://goo.gl/JnTxXZ for more details. type KillContainerOptions struct { // The ID of the container. ID string `qs:"-"` // The signal to send to the container. When omitted, Docker server // will assume SIGKILL. Signal Signal Context context.Context } // KillContainer sends a signal to a container, returning an error in case of // failure. // // See https://goo.gl/JnTxXZ for more details. func (c *Client) KillContainer(opts KillContainerOptions) error { path := "/containers/" + opts.ID + "/kill" + "?" + queryString(opts) resp, err := c.do(http.MethodPost, path, doOptions{context: opts.Context}) if err != nil { var e *Error if !errors.As(err, &e) { return err } switch e.Status { case http.StatusNotFound: return &NoSuchContainer{ID: opts.ID} case http.StatusConflict: return &ContainerNotRunning{ID: opts.ID} default: return err } } resp.Body.Close() return nil } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/container_list.go ================================================ package docker import ( "context" "encoding/json" "net/http" ) // ListContainersOptions specify parameters to the ListContainers function. // // See https://goo.gl/kaOHGw for more details. type ListContainersOptions struct { All bool Size bool Limit int Since string Before string Filters map[string][]string Context context.Context } // ListContainers returns a slice of containers matching the given criteria. // // See https://goo.gl/kaOHGw for more details. func (c *Client) ListContainers(opts ListContainersOptions) ([]APIContainers, error) { path := "/containers/json?" + queryString(opts) resp, err := c.do(http.MethodGet, path, doOptions{context: opts.Context}) if err != nil { return nil, err } defer resp.Body.Close() var containers []APIContainers if err := json.NewDecoder(resp.Body).Decode(&containers); err != nil { return nil, err } return containers, nil } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/container_logs.go ================================================ package docker import ( "context" "io" "net/http" "time" ) // LogsOptions represents the set of options used when getting logs from a // container. // // See https://goo.gl/krK0ZH for more details. type LogsOptions struct { Context context.Context Container string `qs:"-"` OutputStream io.Writer `qs:"-"` ErrorStream io.Writer `qs:"-"` InactivityTimeout time.Duration `qs:"-"` Tail string Since int64 Follow bool Stdout bool Stderr bool Timestamps bool // Use raw terminal? Usually true when the container contains a TTY. RawTerminal bool `qs:"-"` } // Logs gets stdout and stderr logs from the specified container. // // When LogsOptions.RawTerminal is set to false, go-dockerclient will multiplex // the streams and send the containers stdout to LogsOptions.OutputStream, and // stderr to LogsOptions.ErrorStream. // // When LogsOptions.RawTerminal is true, callers will get the raw stream on // LogsOptions.OutputStream. The caller can use libraries such as dlog // (github.com/ahmetalpbalkan/dlog). // // See https://goo.gl/krK0ZH for more details. func (c *Client) Logs(opts LogsOptions) error { if opts.Container == "" { return &NoSuchContainer{ID: opts.Container} } if opts.Tail == "" { opts.Tail = "all" } path := "/containers/" + opts.Container + "/logs?" + queryString(opts) return c.stream(http.MethodGet, path, streamOptions{ setRawTerminal: opts.RawTerminal, stdout: opts.OutputStream, stderr: opts.ErrorStream, inactivityTimeout: opts.InactivityTimeout, context: opts.Context, }) } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/container_pause.go ================================================ package docker import ( "errors" "fmt" "net/http" ) // PauseContainer pauses the given container. // // See https://goo.gl/D1Yaii for more details. func (c *Client) PauseContainer(id string) error { path := fmt.Sprintf("/containers/%s/pause", id) resp, err := c.do(http.MethodPost, path, doOptions{}) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return &NoSuchContainer{ID: id} } return err } resp.Body.Close() return nil } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/container_prune.go ================================================ package docker import ( "context" "encoding/json" "net/http" ) // PruneContainersOptions specify parameters to the PruneContainers function. // // See https://goo.gl/wnkgDT for more details. type PruneContainersOptions struct { Filters map[string][]string Context context.Context } // PruneContainersResults specify results from the PruneContainers function. // // See https://goo.gl/wnkgDT for more details. type PruneContainersResults struct { ContainersDeleted []string SpaceReclaimed int64 } // PruneContainers deletes containers which are stopped. // // See https://goo.gl/wnkgDT for more details. func (c *Client) PruneContainers(opts PruneContainersOptions) (*PruneContainersResults, error) { path := "/containers/prune?" + queryString(opts) resp, err := c.do(http.MethodPost, path, doOptions{context: opts.Context}) if err != nil { return nil, err } defer resp.Body.Close() var results PruneContainersResults if err := json.NewDecoder(resp.Body).Decode(&results); err != nil { return nil, err } return &results, nil } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/container_remove.go ================================================ package docker import ( "context" "errors" "net/http" ) // RemoveContainerOptions encapsulates options to remove a container. // // See https://goo.gl/hL5IPC for more details. type RemoveContainerOptions struct { // The ID of the container. ID string `qs:"-"` // A flag that indicates whether Docker should remove the volumes // associated to the container. RemoveVolumes bool `qs:"v"` // A flag that indicates whether Docker should remove the container // even if it is currently running. Force bool Context context.Context } // RemoveContainer removes a container, returning an error in case of failure. // // See https://goo.gl/hL5IPC for more details. func (c *Client) RemoveContainer(opts RemoveContainerOptions) error { path := "/containers/" + opts.ID + "?" + queryString(opts) resp, err := c.do(http.MethodDelete, path, doOptions{context: opts.Context}) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return &NoSuchContainer{ID: opts.ID} } return err } resp.Body.Close() return nil } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/container_rename.go ================================================ package docker import ( "context" "fmt" "net/http" ) // RenameContainerOptions specify parameters to the RenameContainer function. // // See https://goo.gl/46inai for more details. type RenameContainerOptions struct { // ID of container to rename ID string `qs:"-"` // New name Name string `json:"name,omitempty" yaml:"name,omitempty"` Context context.Context } // RenameContainer updates and existing containers name // // See https://goo.gl/46inai for more details. func (c *Client) RenameContainer(opts RenameContainerOptions) error { resp, err := c.do(http.MethodPost, fmt.Sprintf("/containers/"+opts.ID+"/rename?%s", queryString(opts)), doOptions{ context: opts.Context, }) if err != nil { return err } resp.Body.Close() return nil } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/container_resize.go ================================================ package docker import ( "net/http" "net/url" "strconv" ) // ResizeContainerTTY resizes the terminal to the given height and width. // // See https://goo.gl/FImjeq for more details. func (c *Client) ResizeContainerTTY(id string, height, width int) error { params := make(url.Values) params.Set("h", strconv.Itoa(height)) params.Set("w", strconv.Itoa(width)) resp, err := c.do(http.MethodPost, "/containers/"+id+"/resize?"+params.Encode(), doOptions{}) if err != nil { return err } resp.Body.Close() return nil } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/container_restart.go ================================================ package docker import ( "errors" "fmt" "net/http" ) // RestartPolicy represents the policy for automatically restarting a container. // // Possible values are: // // - always: the docker daemon will always restart the container // - on-failure: the docker daemon will restart the container on failures, at // most MaximumRetryCount times // - unless-stopped: the docker daemon will always restart the container except // when user has manually stopped the container // - no: the docker daemon will not restart the container automatically type RestartPolicy struct { Name string `json:"Name,omitempty" yaml:"Name,omitempty" toml:"Name,omitempty"` MaximumRetryCount int `json:"MaximumRetryCount,omitempty" yaml:"MaximumRetryCount,omitempty" toml:"MaximumRetryCount,omitempty"` } // AlwaysRestart returns a restart policy that tells the Docker daemon to // always restart the container. func AlwaysRestart() RestartPolicy { return RestartPolicy{Name: "always"} } // RestartOnFailure returns a restart policy that tells the Docker daemon to // restart the container on failures, trying at most maxRetry times. func RestartOnFailure(maxRetry int) RestartPolicy { return RestartPolicy{Name: "on-failure", MaximumRetryCount: maxRetry} } // RestartUnlessStopped returns a restart policy that tells the Docker daemon to // always restart the container except when user has manually stopped the container. func RestartUnlessStopped() RestartPolicy { return RestartPolicy{Name: "unless-stopped"} } // NeverRestart returns a restart policy that tells the Docker daemon to never // restart the container on failures. func NeverRestart() RestartPolicy { return RestartPolicy{Name: "no"} } // RestartContainer stops a container, killing it after the given timeout (in // seconds), during the stop process. // // See https://goo.gl/MrAKQ5 for more details. func (c *Client) RestartContainer(id string, timeout uint) error { path := fmt.Sprintf("/containers/%s/restart?t=%d", id, timeout) resp, err := c.do(http.MethodPost, path, doOptions{}) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return &NoSuchContainer{ID: id} } return err } resp.Body.Close() return nil } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/container_start.go ================================================ package docker import ( "context" "errors" "net/http" ) // StartContainer starts a container, returning an error in case of failure. // // Passing the HostConfig to this method has been deprecated in Docker API 1.22 // (Docker Engine 1.10.x) and totally removed in Docker API 1.24 (Docker Engine // 1.12.x). The client will ignore the parameter when communicating with Docker // API 1.24 or greater. // // See https://goo.gl/fbOSZy for more details. func (c *Client) StartContainer(id string, hostConfig *HostConfig) error { return c.startContainer(id, hostConfig, doOptions{}) } // StartContainerWithContext starts a container, returning an error in case of // failure. The context can be used to cancel the outstanding start container // request. // // Passing the HostConfig to this method has been deprecated in Docker API 1.22 // (Docker Engine 1.10.x) and totally removed in Docker API 1.24 (Docker Engine // 1.12.x). The client will ignore the parameter when communicating with Docker // API 1.24 or greater. // // See https://goo.gl/fbOSZy for more details. func (c *Client) StartContainerWithContext(id string, hostConfig *HostConfig, ctx context.Context) error { return c.startContainer(id, hostConfig, doOptions{context: ctx}) } func (c *Client) startContainer(id string, hostConfig *HostConfig, opts doOptions) error { path := "/containers/" + id + "/start" if c.serverAPIVersion == nil { c.checkAPIVersion() } if c.serverAPIVersion != nil && c.serverAPIVersion.LessThan(apiVersion124) { opts.data = hostConfig opts.forceJSON = true } resp, err := c.do(http.MethodPost, path, opts) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return &NoSuchContainer{ID: id, Err: err} } return err } defer resp.Body.Close() if resp.StatusCode == http.StatusNotModified { return &ContainerAlreadyRunning{ID: id} } return nil } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/container_stats.go ================================================ package docker import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "time" ) // Stats represents container statistics, returned by /containers//stats. // // See https://goo.gl/Dk3Xio for more details. type Stats struct { Read time.Time `json:"read,omitempty" yaml:"read,omitempty" toml:"read,omitempty"` PreRead time.Time `json:"preread,omitempty" yaml:"preread,omitempty" toml:"preread,omitempty"` NumProcs uint32 `json:"num_procs" yaml:"num_procs" toml:"num_procs"` PidsStats struct { Current uint64 `json:"current,omitempty" yaml:"current,omitempty"` } `json:"pids_stats,omitempty" yaml:"pids_stats,omitempty" toml:"pids_stats,omitempty"` Network NetworkStats `json:"network,omitempty" yaml:"network,omitempty" toml:"network,omitempty"` Networks map[string]NetworkStats `json:"networks,omitempty" yaml:"networks,omitempty" toml:"networks,omitempty"` MemoryStats struct { Stats struct { TotalPgmafault uint64 `json:"total_pgmafault,omitempty" yaml:"total_pgmafault,omitempty" toml:"total_pgmafault,omitempty"` Cache uint64 `json:"cache,omitempty" yaml:"cache,omitempty" toml:"cache,omitempty"` MappedFile uint64 `json:"mapped_file,omitempty" yaml:"mapped_file,omitempty" toml:"mapped_file,omitempty"` TotalInactiveFile uint64 `json:"total_inactive_file,omitempty" yaml:"total_inactive_file,omitempty" toml:"total_inactive_file,omitempty"` Pgpgout uint64 `json:"pgpgout,omitempty" yaml:"pgpgout,omitempty" toml:"pgpgout,omitempty"` Rss uint64 `json:"rss,omitempty" yaml:"rss,omitempty" toml:"rss,omitempty"` TotalMappedFile uint64 `json:"total_mapped_file,omitempty" yaml:"total_mapped_file,omitempty" toml:"total_mapped_file,omitempty"` Writeback uint64 `json:"writeback,omitempty" yaml:"writeback,omitempty" toml:"writeback,omitempty"` Unevictable uint64 `json:"unevictable,omitempty" yaml:"unevictable,omitempty" toml:"unevictable,omitempty"` Pgpgin uint64 `json:"pgpgin,omitempty" yaml:"pgpgin,omitempty" toml:"pgpgin,omitempty"` TotalUnevictable uint64 `json:"total_unevictable,omitempty" yaml:"total_unevictable,omitempty" toml:"total_unevictable,omitempty"` Pgmajfault uint64 `json:"pgmajfault,omitempty" yaml:"pgmajfault,omitempty" toml:"pgmajfault,omitempty"` TotalRss uint64 `json:"total_rss,omitempty" yaml:"total_rss,omitempty" toml:"total_rss,omitempty"` TotalRssHuge uint64 `json:"total_rss_huge,omitempty" yaml:"total_rss_huge,omitempty" toml:"total_rss_huge,omitempty"` TotalWriteback uint64 `json:"total_writeback,omitempty" yaml:"total_writeback,omitempty" toml:"total_writeback,omitempty"` TotalInactiveAnon uint64 `json:"total_inactive_anon,omitempty" yaml:"total_inactive_anon,omitempty" toml:"total_inactive_anon,omitempty"` RssHuge uint64 `json:"rss_huge,omitempty" yaml:"rss_huge,omitempty" toml:"rss_huge,omitempty"` HierarchicalMemoryLimit uint64 `json:"hierarchical_memory_limit,omitempty" yaml:"hierarchical_memory_limit,omitempty" toml:"hierarchical_memory_limit,omitempty"` TotalPgfault uint64 `json:"total_pgfault,omitempty" yaml:"total_pgfault,omitempty" toml:"total_pgfault,omitempty"` TotalActiveFile uint64 `json:"total_active_file,omitempty" yaml:"total_active_file,omitempty" toml:"total_active_file,omitempty"` ActiveAnon uint64 `json:"active_anon,omitempty" yaml:"active_anon,omitempty" toml:"active_anon,omitempty"` TotalActiveAnon uint64 `json:"total_active_anon,omitempty" yaml:"total_active_anon,omitempty" toml:"total_active_anon,omitempty"` TotalPgpgout uint64 `json:"total_pgpgout,omitempty" yaml:"total_pgpgout,omitempty" toml:"total_pgpgout,omitempty"` TotalCache uint64 `json:"total_cache,omitempty" yaml:"total_cache,omitempty" toml:"total_cache,omitempty"` InactiveAnon uint64 `json:"inactive_anon,omitempty" yaml:"inactive_anon,omitempty" toml:"inactive_anon,omitempty"` ActiveFile uint64 `json:"active_file,omitempty" yaml:"active_file,omitempty" toml:"active_file,omitempty"` Pgfault uint64 `json:"pgfault,omitempty" yaml:"pgfault,omitempty" toml:"pgfault,omitempty"` InactiveFile uint64 `json:"inactive_file,omitempty" yaml:"inactive_file,omitempty" toml:"inactive_file,omitempty"` TotalPgpgin uint64 `json:"total_pgpgin,omitempty" yaml:"total_pgpgin,omitempty" toml:"total_pgpgin,omitempty"` HierarchicalMemswLimit uint64 `json:"hierarchical_memsw_limit,omitempty" yaml:"hierarchical_memsw_limit,omitempty" toml:"hierarchical_memsw_limit,omitempty"` Swap uint64 `json:"swap,omitempty" yaml:"swap,omitempty" toml:"swap,omitempty"` Anon uint64 `json:"anon,omitempty" yaml:"anon,omitempty" toml:"anon,omitempty"` AnonThp uint64 `json:"anon_thp,omitempty" yaml:"anon_thp,omitempty" toml:"anon_thp,omitempty"` File uint64 `json:"file,omitempty" yaml:"file,omitempty" toml:"file,omitempty"` FileDirty uint64 `json:"file_dirty,omitempty" yaml:"file_dirty,omitempty" toml:"file_dirty,omitempty"` FileMapped uint64 `json:"file_mapped,omitempty" yaml:"file_mapped,omitempty" toml:"file_mapped,omitempty"` FileWriteback uint64 `json:"file_writeback,omitempty" yaml:"file_writeback,omitempty" toml:"file_writeback,omitempty"` KernelStack uint64 `json:"kernel_stack,omitempty" yaml:"kernel_stack,omitempty" toml:"kernel_stack,omitempty"` Pgactivate uint64 `json:"pgactivate,omitempty" yaml:"pgactivate,omitempty" toml:"pgactivate,omitempty"` Pgdeactivate uint64 `json:"pgdeactivate,omitempty" yaml:"pgdeactivate,omitempty" toml:"pgdeactivate,omitempty"` Pglazyfree uint64 `json:"pglazyfree,omitempty" yaml:"pglazyfree,omitempty" toml:"pglazyfree,omitempty"` Pglazyfreed uint64 `json:"pglazyfreed,omitempty" yaml:"pglazyfreed,omitempty" toml:"pglazyfreed,omitempty"` Pgrefill uint64 `json:"pgrefill,omitempty" yaml:"pgrefill,omitempty" toml:"pgrefill,omitempty"` Pgscan uint64 `json:"pgscan,omitempty" yaml:"pgscan,omitempty" toml:"pgscan,omitempty"` Pgsteal uint64 `json:"pgsteal,omitempty" yaml:"pgsteal,omitempty" toml:"pgsteal,omitempty"` Shmem uint64 `json:"shmem,omitempty" yaml:"shmem,omitempty" toml:"shmem,omitempty"` Slab uint64 `json:"slab,omitempty" yaml:"slab,omitempty" toml:"slab,omitempty"` SlabReclaimable uint64 `json:"slab_reclaimable,omitempty" yaml:"slab_reclaimable,omitempty" toml:"slab_reclaimable,omitempty"` SlabUnreclaimable uint64 `json:"slab_unreclaimable,omitempty" yaml:"slab_unreclaimable,omitempty" toml:"slab_unreclaimable,omitempty"` Sock uint64 `json:"sock,omitempty" yaml:"sock,omitempty" toml:"sock,omitempty"` ThpCollapseAlloc uint64 `json:"thp_collapse_alloc,omitempty" yaml:"thp_collapse_alloc,omitempty" toml:"thp_collapse_alloc,omitempty"` ThpFaultAlloc uint64 `json:"thp_fault_alloc,omitempty" yaml:"thp_fault_alloc,omitempty" toml:"thp_fault_alloc,omitempty"` WorkingsetActivate uint64 `json:"workingset_activate,omitempty" yaml:"workingset_activate,omitempty" toml:"workingset_activate,omitempty"` WorkingsetNodereclaim uint64 `json:"workingset_nodereclaim,omitempty" yaml:"workingset_nodereclaim,omitempty" toml:"workingset_nodereclaim,omitempty"` WorkingsetRefault uint64 `json:"workingset_refault,omitempty" yaml:"workingset_refault,omitempty" toml:"workingset_refault,omitempty"` } `json:"stats,omitempty" yaml:"stats,omitempty" toml:"stats,omitempty"` MaxUsage uint64 `json:"max_usage,omitempty" yaml:"max_usage,omitempty" toml:"max_usage,omitempty"` Usage uint64 `json:"usage,omitempty" yaml:"usage,omitempty" toml:"usage,omitempty"` Failcnt uint64 `json:"failcnt,omitempty" yaml:"failcnt,omitempty" toml:"failcnt,omitempty"` Limit uint64 `json:"limit,omitempty" yaml:"limit,omitempty" toml:"limit,omitempty"` Commit uint64 `json:"commitbytes,omitempty" yaml:"commitbytes,omitempty" toml:"privateworkingset,omitempty"` CommitPeak uint64 `json:"commitpeakbytes,omitempty" yaml:"commitpeakbytes,omitempty" toml:"commitpeakbytes,omitempty"` PrivateWorkingSet uint64 `json:"privateworkingset,omitempty" yaml:"privateworkingset,omitempty" toml:"privateworkingset,omitempty"` } `json:"memory_stats,omitempty" yaml:"memory_stats,omitempty" toml:"memory_stats,omitempty"` BlkioStats struct { IOServiceBytesRecursive []BlkioStatsEntry `json:"io_service_bytes_recursive,omitempty" yaml:"io_service_bytes_recursive,omitempty" toml:"io_service_bytes_recursive,omitempty"` IOServicedRecursive []BlkioStatsEntry `json:"io_serviced_recursive,omitempty" yaml:"io_serviced_recursive,omitempty" toml:"io_serviced_recursive,omitempty"` IOQueueRecursive []BlkioStatsEntry `json:"io_queue_recursive,omitempty" yaml:"io_queue_recursive,omitempty" toml:"io_queue_recursive,omitempty"` IOServiceTimeRecursive []BlkioStatsEntry `json:"io_service_time_recursive,omitempty" yaml:"io_service_time_recursive,omitempty" toml:"io_service_time_recursive,omitempty"` IOWaitTimeRecursive []BlkioStatsEntry `json:"io_wait_time_recursive,omitempty" yaml:"io_wait_time_recursive,omitempty" toml:"io_wait_time_recursive,omitempty"` IOMergedRecursive []BlkioStatsEntry `json:"io_merged_recursive,omitempty" yaml:"io_merged_recursive,omitempty" toml:"io_merged_recursive,omitempty"` IOTimeRecursive []BlkioStatsEntry `json:"io_time_recursive,omitempty" yaml:"io_time_recursive,omitempty" toml:"io_time_recursive,omitempty"` SectorsRecursive []BlkioStatsEntry `json:"sectors_recursive,omitempty" yaml:"sectors_recursive,omitempty" toml:"sectors_recursive,omitempty"` } `json:"blkio_stats,omitempty" yaml:"blkio_stats,omitempty" toml:"blkio_stats,omitempty"` CPUStats CPUStats `json:"cpu_stats,omitempty" yaml:"cpu_stats,omitempty" toml:"cpu_stats,omitempty"` PreCPUStats CPUStats `json:"precpu_stats,omitempty"` StorageStats struct { ReadCountNormalized uint64 `json:"read_count_normalized,omitempty" yaml:"read_count_normalized,omitempty" toml:"read_count_normalized,omitempty"` ReadSizeBytes uint64 `json:"read_size_bytes,omitempty" yaml:"read_size_bytes,omitempty" toml:"read_size_bytes,omitempty"` WriteCountNormalized uint64 `json:"write_count_normalized,omitempty" yaml:"write_count_normalized,omitempty" toml:"write_count_normalized,omitempty"` WriteSizeBytes uint64 `json:"write_size_bytes,omitempty" yaml:"write_size_bytes,omitempty" toml:"write_size_bytes,omitempty"` } `json:"storage_stats,omitempty" yaml:"storage_stats,omitempty" toml:"storage_stats,omitempty"` } // NetworkStats is a stats entry for network stats type NetworkStats struct { RxDropped uint64 `json:"rx_dropped,omitempty" yaml:"rx_dropped,omitempty" toml:"rx_dropped,omitempty"` RxBytes uint64 `json:"rx_bytes,omitempty" yaml:"rx_bytes,omitempty" toml:"rx_bytes,omitempty"` RxErrors uint64 `json:"rx_errors,omitempty" yaml:"rx_errors,omitempty" toml:"rx_errors,omitempty"` TxPackets uint64 `json:"tx_packets,omitempty" yaml:"tx_packets,omitempty" toml:"tx_packets,omitempty"` TxDropped uint64 `json:"tx_dropped,omitempty" yaml:"tx_dropped,omitempty" toml:"tx_dropped,omitempty"` RxPackets uint64 `json:"rx_packets,omitempty" yaml:"rx_packets,omitempty" toml:"rx_packets,omitempty"` TxErrors uint64 `json:"tx_errors,omitempty" yaml:"tx_errors,omitempty" toml:"tx_errors,omitempty"` TxBytes uint64 `json:"tx_bytes,omitempty" yaml:"tx_bytes,omitempty" toml:"tx_bytes,omitempty"` } // CPUStats is a stats entry for cpu stats type CPUStats struct { CPUUsage struct { PercpuUsage []uint64 `json:"percpu_usage,omitempty" yaml:"percpu_usage,omitempty" toml:"percpu_usage,omitempty"` UsageInUsermode uint64 `json:"usage_in_usermode,omitempty" yaml:"usage_in_usermode,omitempty" toml:"usage_in_usermode,omitempty"` TotalUsage uint64 `json:"total_usage,omitempty" yaml:"total_usage,omitempty" toml:"total_usage,omitempty"` UsageInKernelmode uint64 `json:"usage_in_kernelmode,omitempty" yaml:"usage_in_kernelmode,omitempty" toml:"usage_in_kernelmode,omitempty"` } `json:"cpu_usage,omitempty" yaml:"cpu_usage,omitempty" toml:"cpu_usage,omitempty"` SystemCPUUsage uint64 `json:"system_cpu_usage,omitempty" yaml:"system_cpu_usage,omitempty" toml:"system_cpu_usage,omitempty"` OnlineCPUs uint64 `json:"online_cpus,omitempty" yaml:"online_cpus,omitempty" toml:"online_cpus,omitempty"` ThrottlingData struct { Periods uint64 `json:"periods,omitempty"` ThrottledPeriods uint64 `json:"throttled_periods,omitempty"` ThrottledTime uint64 `json:"throttled_time,omitempty"` } `json:"throttling_data,omitempty" yaml:"throttling_data,omitempty" toml:"throttling_data,omitempty"` } // BlkioStatsEntry is a stats entry for blkio_stats type BlkioStatsEntry struct { Major uint64 `json:"major,omitempty" yaml:"major,omitempty" toml:"major,omitempty"` Minor uint64 `json:"minor,omitempty" yaml:"minor,omitempty" toml:"minor,omitempty"` Op string `json:"op,omitempty" yaml:"op,omitempty" toml:"op,omitempty"` Value uint64 `json:"value,omitempty" yaml:"value,omitempty" toml:"value,omitempty"` } // StatsOptions specify parameters to the Stats function. // // See https://goo.gl/Dk3Xio for more details. type StatsOptions struct { ID string Stats chan<- *Stats Stream bool // A flag that enables stopping the stats operation Done <-chan bool // Initial connection timeout Timeout time.Duration // Timeout with no data is received, it's reset every time new data // arrives InactivityTimeout time.Duration `qs:"-"` Context context.Context } // Stats sends container statistics for the given container to the given channel. // // This function is blocking, similar to a streaming call for logs, and should be run // on a separate goroutine from the caller. Note that this function will block until // the given container is removed, not just exited. When finished, this function // will close the given channel. Alternatively, function can be stopped by // signaling on the Done channel. // // See https://goo.gl/Dk3Xio for more details. func (c *Client) Stats(opts StatsOptions) (retErr error) { errC := make(chan error, 1) readCloser, writeCloser := io.Pipe() defer func() { close(opts.Stats) if err := <-errC; err != nil && retErr == nil { retErr = err } if err := readCloser.Close(); err != nil && retErr == nil { retErr = err } }() reqSent := make(chan struct{}) go func() { defer close(errC) err := c.stream(http.MethodGet, fmt.Sprintf("/containers/%s/stats?stream=%v", opts.ID, opts.Stream), streamOptions{ rawJSONStream: true, useJSONDecoder: true, stdout: writeCloser, timeout: opts.Timeout, inactivityTimeout: opts.InactivityTimeout, context: opts.Context, reqSent: reqSent, }) if err != nil { var dockerError *Error if errors.As(err, &dockerError) { if dockerError.Status == http.StatusNotFound { err = &NoSuchContainer{ID: opts.ID} } } } if closeErr := writeCloser.Close(); closeErr != nil && err == nil { err = closeErr } errC <- err }() quit := make(chan struct{}) defer close(quit) go func() { // block here waiting for the signal to stop function select { case <-opts.Done: readCloser.Close() case <-quit: return } }() decoder := json.NewDecoder(readCloser) stats := new(Stats) <-reqSent for err := decoder.Decode(stats); !errors.Is(err, io.EOF); err = decoder.Decode(stats) { if err != nil { return err } opts.Stats <- stats stats = new(Stats) } return nil } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/container_stop.go ================================================ package docker import ( "context" "errors" "fmt" "net/http" ) // StopContainer stops a container, killing it after the given timeout (in // seconds). // // See https://goo.gl/R9dZcV for more details. func (c *Client) StopContainer(id string, timeout uint) error { return c.stopContainer(id, timeout, doOptions{}) } // StopContainerWithContext stops a container, killing it after the given // timeout (in seconds). The context can be used to cancel the stop // container request. // // See https://goo.gl/R9dZcV for more details. func (c *Client) StopContainerWithContext(id string, timeout uint, ctx context.Context) error { return c.stopContainer(id, timeout, doOptions{context: ctx}) } func (c *Client) stopContainer(id string, timeout uint, opts doOptions) error { path := fmt.Sprintf("/containers/%s/stop?t=%d", id, timeout) resp, err := c.do(http.MethodPost, path, opts) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return &NoSuchContainer{ID: id} } return err } defer resp.Body.Close() if resp.StatusCode == http.StatusNotModified { return &ContainerNotRunning{ID: id} } return nil } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/container_top.go ================================================ package docker import ( "encoding/json" "errors" "fmt" "net/http" ) // TopResult represents the list of processes running in a container, as // returned by /containers//top. // // See https://goo.gl/FLwpPl for more details. type TopResult struct { Titles []string Processes [][]string } // TopContainer returns processes running inside a container // // See https://goo.gl/FLwpPl for more details. func (c *Client) TopContainer(id string, psArgs string) (TopResult, error) { var args string var result TopResult if psArgs != "" { args = fmt.Sprintf("?ps_args=%s", psArgs) } path := fmt.Sprintf("/containers/%s/top%s", id, args) resp, err := c.do(http.MethodGet, path, doOptions{}) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return result, &NoSuchContainer{ID: id} } return result, err } defer resp.Body.Close() err = json.NewDecoder(resp.Body).Decode(&result) return result, err } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/container_unpause.go ================================================ package docker import ( "errors" "fmt" "net/http" ) // UnpauseContainer unpauses the given container. // // See https://goo.gl/sZ2faO for more details. func (c *Client) UnpauseContainer(id string) error { path := fmt.Sprintf("/containers/%s/unpause", id) resp, err := c.do(http.MethodPost, path, doOptions{}) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return &NoSuchContainer{ID: id} } return err } resp.Body.Close() return nil } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/container_update.go ================================================ package docker import ( "context" "fmt" "net/http" ) // UpdateContainerOptions specify parameters to the UpdateContainer function. // // See https://goo.gl/Y6fXUy for more details. type UpdateContainerOptions struct { BlkioWeight int `json:"BlkioWeight"` CPUShares int `json:"CpuShares"` CPUPeriod int `json:"CpuPeriod"` CPURealtimePeriod int64 `json:"CpuRealtimePeriod"` CPURealtimeRuntime int64 `json:"CpuRealtimeRuntime"` CPUQuota int `json:"CpuQuota"` CpusetCpus string `json:"CpusetCpus"` CpusetMems string `json:"CpusetMems"` Memory int `json:"Memory"` MemorySwap int `json:"MemorySwap"` MemoryReservation int `json:"MemoryReservation"` // Deprecated: KernelMemory is deprecated as of API 1.42 and removed in API 1.52. KernelMemory int `json:"KernelMemory"` RestartPolicy RestartPolicy `json:"RestartPolicy,omitempty"` Context context.Context } // UpdateContainer updates the container at ID with the options // // See https://goo.gl/Y6fXUy for more details. func (c *Client) UpdateContainer(id string, opts UpdateContainerOptions) error { resp, err := c.do(http.MethodPost, fmt.Sprintf("/containers/%s/update", id), doOptions{ data: opts, forceJSON: true, context: opts.Context, }) if err != nil { return err } defer resp.Body.Close() return nil } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/container_wait.go ================================================ package docker import ( "context" "encoding/json" "errors" "net/http" ) // WaitContainer blocks until the given container stops, return the exit code // of the container status. // // See https://goo.gl/4AGweZ for more details. func (c *Client) WaitContainer(id string) (int, error) { return c.waitContainer(id, doOptions{}) } // WaitContainerWithContext blocks until the given container stops, return the exit code // of the container status. The context object can be used to cancel the // inspect request. // // See https://goo.gl/4AGweZ for more details. func (c *Client) WaitContainerWithContext(id string, ctx context.Context) (int, error) { return c.waitContainer(id, doOptions{context: ctx}) } func (c *Client) waitContainer(id string, opts doOptions) (int, error) { resp, err := c.do(http.MethodPost, "/containers/"+id+"/wait", opts) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return 0, &NoSuchContainer{ID: id} } return 0, err } defer resp.Body.Close() var r struct{ StatusCode int } if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { return 0, err } return r.StatusCode, nil } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/distribution.go ================================================ // Copyright 2017 go-dockerclient authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package docker import ( "encoding/json" "net/http" "github.com/moby/moby/api/types/registry" ) // InspectDistribution returns image digest and platform information by contacting the registry func (c *Client) InspectDistribution(name string) (*registry.DistributionInspect, error) { path := "/distribution/" + name + "/json" resp, err := c.do(http.MethodGet, path, doOptions{}) if err != nil { return nil, err } defer resp.Body.Close() var distributionInspect registry.DistributionInspect if err := json.NewDecoder(resp.Body).Decode(&distributionInspect); err != nil { return nil, err } return &distributionInspect, nil } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/env.go ================================================ // Copyright 2014 Docker authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the DOCKER-LICENSE file. package docker import ( "encoding/json" "fmt" "io" "strconv" "strings" ) // Env represents a list of key-pair represented in the form KEY=VALUE. type Env []string // Get returns the string value of the given key. func (env *Env) Get(key string) (value string) { return env.Map()[key] } // Exists checks whether the given key is defined in the internal Env // representation. func (env *Env) Exists(key string) bool { _, exists := env.Map()[key] return exists } // GetBool returns a boolean representation of the given key. The key is false // whenever its value if 0, no, false, none or an empty string. Any other value // will be interpreted as true. func (env *Env) GetBool(key string) (value bool) { s := strings.ToLower(strings.Trim(env.Get(key), " \t")) if s == "" || s == "0" || s == "no" || s == "false" || s == "none" { return false } return true } // SetBool defines a boolean value to the given key. func (env *Env) SetBool(key string, value bool) { if value { env.Set(key, "1") } else { env.Set(key, "0") } } // GetInt returns the value of the provided key, converted to int. // // It the value cannot be represented as an integer, it returns -1. func (env *Env) GetInt(key string) int { return int(env.GetInt64(key)) } // SetInt defines an integer value to the given key. func (env *Env) SetInt(key string, value int) { env.Set(key, strconv.Itoa(value)) } // GetInt64 returns the value of the provided key, converted to int64. // // It the value cannot be represented as an integer, it returns -1. func (env *Env) GetInt64(key string) int64 { s := strings.Trim(env.Get(key), " \t") val, err := strconv.ParseInt(s, 10, 64) if err != nil { return -1 } return val } // SetInt64 defines an integer (64-bit wide) value to the given key. func (env *Env) SetInt64(key string, value int64) { env.Set(key, strconv.FormatInt(value, 10)) } // GetJSON unmarshals the value of the provided key in the provided iface. // // iface is a value that can be provided to the json.Unmarshal function. func (env *Env) GetJSON(key string, iface any) error { sval := env.Get(key) if sval == "" { return nil } return json.Unmarshal([]byte(sval), iface) } // SetJSON marshals the given value to JSON format and stores it using the // provided key. func (env *Env) SetJSON(key string, value any) error { sval, err := json.Marshal(value) if err != nil { return err } env.Set(key, string(sval)) return nil } // GetList returns a list of strings matching the provided key. It handles the // list as a JSON representation of a list of strings. // // If the given key matches to a single string, it will return a list // containing only the value that matches the key. func (env *Env) GetList(key string) []string { sval := env.Get(key) if sval == "" { return nil } var l []string if err := json.Unmarshal([]byte(sval), &l); err != nil { l = append(l, sval) } return l } // SetList stores the given list in the provided key, after serializing it to // JSON format. func (env *Env) SetList(key string, value []string) error { return env.SetJSON(key, value) } // Set defines the value of a key to the given string. func (env *Env) Set(key, value string) { *env = append(*env, key+"="+value) } // Decode decodes `src` as a json dictionary, and adds each decoded key-value // pair to the environment. // // If `src` cannot be decoded as a json dictionary, an error is returned. func (env *Env) Decode(src io.Reader) error { m := make(map[string]any) if err := json.NewDecoder(src).Decode(&m); err != nil { return err } for k, v := range m { env.SetAuto(k, v) } return nil } // SetAuto will try to define the Set* method to call based on the given value. func (env *Env) SetAuto(key string, value any) { if fval, ok := value.(float64); ok { env.SetInt64(key, int64(fval)) } else if sval, ok := value.(string); ok { env.Set(key, sval) } else if val, err := json.Marshal(value); err == nil { env.Set(key, string(val)) } else { env.Set(key, fmt.Sprintf("%v", value)) } } // Map returns the map representation of the env. func (env *Env) Map() map[string]string { if env == nil || len(*env) == 0 { return nil } m := make(map[string]string) for _, kv := range *env { parts := strings.SplitN(kv, "=", 2) if len(parts) == 1 { m[parts[0]] = "" } else { m[parts[0]] = parts[1] } } return m } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/event.go ================================================ // Copyright 2014 go-dockerclient authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package docker import ( "encoding/json" "errors" "io" "math" "net" "net/http" "net/http/httputil" "strconv" "sync" "sync/atomic" "time" ) // EventsOptions to filter events // See https://docs.docker.com/engine/api/v1.41/#operation/SystemEvents for more details. type EventsOptions struct { // Show events created since this timestamp then stream new events. Since string // Show events created until this timestamp then stop streaming. Until string // Filter for events. For example: // map[string][]string{"type": {"container"}, "event": {"start", "die"}} // will return events when container was started and stopped or killed // // Available filters: // config= config name or ID // container= container name or ID // daemon= daemon name or ID // event= event type // image= image name or ID // label= image or container label // network= network name or ID // node= node ID // plugin= plugin name or ID // scope= local or swarm // secret= secret name or ID // service= service name or ID // type= container, image, volume, network, daemon, plugin, node, service, secret or config // volume= volume name Filters map[string][]string } // APIEvents represents events coming from the Docker API // The fields in the Docker API changed in API version 1.22, and // events for more than images and containers are now fired off. // To maintain forward and backward compatibility, go-dockerclient // replicates the event in both the new and old format as faithfully as possible. // // For events that only exist in 1.22 in later, `Status` is filled in as // `"Type:Action"` instead of just `Action` to allow for older clients to // differentiate and not break if they rely on the pre-1.22 Status types. // // The transformEvent method can be consulted for more information about how // events are translated from new/old API formats type APIEvents struct { // New API Fields in 1.22 Action string `json:"action,omitempty"` Type string `json:"type,omitempty"` Actor APIActor `json:"actor,omitempty"` // Old API fields for < 1.22 Status string `json:"status,omitempty"` ID string `json:"id,omitempty"` From string `json:"from,omitempty"` // Fields in both Time int64 `json:"time,omitempty"` TimeNano int64 `json:"timeNano,omitempty"` } // APIActor represents an actor that accomplishes something for an event type APIActor struct { ID string `json:"id,omitempty"` Attributes map[string]string `json:"attributes,omitempty"` } type eventMonitoringState struct { // `sync/atomic` expects the first word in an allocated struct to be 64-bit // aligned on both ARM and x86-32. See https://goo.gl/zW7dgq for more details. lastSeen int64 sync.RWMutex sync.WaitGroup enabled bool C chan *APIEvents errC chan error listeners []chan<- *APIEvents closeConn func() } const ( maxMonitorConnRetries = 5 retryInitialWaitTime = 10. ) var ( // ErrNoListeners is the error returned when no listeners are available // to receive an event. ErrNoListeners = errors.New("no listeners present to receive event") // ErrListenerAlreadyExists is the error returned when the listerner already // exists. ErrListenerAlreadyExists = errors.New("listener already exists for docker events") // ErrTLSNotSupported is the error returned when the client does not support // TLS (this applies to the Windows named pipe client). ErrTLSNotSupported = errors.New("tls not supported by this client") // EOFEvent is sent when the event listener receives an EOF error. EOFEvent = &APIEvents{ Type: "EOF", Status: "EOF", } ) // AddEventListener adds a new listener to container events in the Docker API. // // The parameter is a channel through which events will be sent. func (c *Client) AddEventListener(listener chan<- *APIEvents) error { return c.AddEventListenerWithOptions(EventsOptions{}, listener) } // AddEventListener adds a new listener to container events in the Docker API. // See https://docs.docker.com/engine/api/v1.41/#operation/SystemEvents for more details. // // The listener parameter is a channel through which events will be sent. func (c *Client) AddEventListenerWithOptions(options EventsOptions, listener chan<- *APIEvents) error { var err error if !c.eventMonitor.isEnabled() { err = c.eventMonitor.enableEventMonitoring(c, options) if err != nil { return err } } return c.eventMonitor.addListener(listener) } // RemoveEventListener removes a listener from the monitor. func (c *Client) RemoveEventListener(listener chan *APIEvents) error { err := c.eventMonitor.removeListener(listener) if err != nil { return err } if c.eventMonitor.listernersCount() == 0 { c.eventMonitor.disableEventMonitoring() } return nil } func (eventState *eventMonitoringState) addListener(listener chan<- *APIEvents) error { eventState.Lock() defer eventState.Unlock() if listenerExists(listener, &eventState.listeners) { return ErrListenerAlreadyExists } eventState.Add(1) eventState.listeners = append(eventState.listeners, listener) return nil } func (eventState *eventMonitoringState) removeListener(listener chan<- *APIEvents) error { eventState.Lock() defer eventState.Unlock() if listenerExists(listener, &eventState.listeners) { var newListeners []chan<- *APIEvents for _, l := range eventState.listeners { if l != listener { newListeners = append(newListeners, l) } } eventState.listeners = newListeners eventState.Add(-1) } return nil } func (eventState *eventMonitoringState) closeListeners() { for _, l := range eventState.listeners { close(l) eventState.Add(-1) } eventState.listeners = nil } func (eventState *eventMonitoringState) listernersCount() int { eventState.RLock() defer eventState.RUnlock() return len(eventState.listeners) } func listenerExists(a chan<- *APIEvents, list *[]chan<- *APIEvents) bool { for _, b := range *list { if b == a { return true } } return false } func (eventState *eventMonitoringState) enableEventMonitoring(c *Client, opts EventsOptions) error { eventState.Lock() defer eventState.Unlock() if !eventState.enabled { eventState.enabled = true atomic.StoreInt64(&eventState.lastSeen, 0) eventState.C = make(chan *APIEvents, 100) eventState.errC = make(chan error, 1) go eventState.monitorEvents(c, opts) } return nil } func (eventState *eventMonitoringState) disableEventMonitoring() { eventState.Lock() defer eventState.Unlock() eventState.closeListeners() eventState.Wait() if eventState.enabled { eventState.enabled = false close(eventState.C) close(eventState.errC) if eventState.closeConn != nil { eventState.closeConn() eventState.closeConn = nil } } } func (eventState *eventMonitoringState) monitorEvents(c *Client, opts EventsOptions) { const ( noListenersTimeout = 5 * time.Second noListenersInterval = 10 * time.Millisecond noListenersMaxTries = noListenersTimeout / noListenersInterval ) var err error for i := time.Duration(0); i < noListenersMaxTries && eventState.noListeners(); i++ { time.Sleep(10 * time.Millisecond) } if eventState.noListeners() { // terminate if no listener is available after 5 seconds. // Prevents goroutine leak when RemoveEventListener is called // right after AddEventListener. eventState.disableEventMonitoring() return } if err = eventState.connectWithRetry(c, opts); err != nil { // terminate if connect failed eventState.disableEventMonitoring() return } for eventState.isEnabled() { timeout := time.After(100 * time.Millisecond) select { case ev, ok := <-eventState.C: if !ok { return } if ev == EOFEvent { go eventState.disableEventMonitoring() return } go func(ev *APIEvents) { eventState.updateLastSeen(ev) eventState.sendEvent(ev) }(ev) case err = <-eventState.errC: if errors.Is(err, ErrNoListeners) { eventState.disableEventMonitoring() return } else if err != nil { defer func() { go eventState.monitorEvents(c, opts) }() return } case <-timeout: continue } } } func (eventState *eventMonitoringState) connectWithRetry(c *Client, opts EventsOptions) error { var retries int eventState.RLock() eventChan := eventState.C errChan := eventState.errC eventState.RUnlock() closeConn, err := c.eventHijack(opts, atomic.LoadInt64(&eventState.lastSeen), eventChan, errChan) for ; err != nil && retries < maxMonitorConnRetries; retries++ { waitTime := int64(retryInitialWaitTime * math.Pow(2, float64(retries))) time.Sleep(time.Duration(waitTime) * time.Millisecond) eventState.RLock() eventChan = eventState.C errChan = eventState.errC eventState.RUnlock() closeConn, err = c.eventHijack(opts, atomic.LoadInt64(&eventState.lastSeen), eventChan, errChan) } eventState.Lock() defer eventState.Unlock() eventState.closeConn = closeConn return err } func (eventState *eventMonitoringState) noListeners() bool { eventState.RLock() defer eventState.RUnlock() return len(eventState.listeners) == 0 } func (eventState *eventMonitoringState) isEnabled() bool { eventState.RLock() defer eventState.RUnlock() return eventState.enabled } func (eventState *eventMonitoringState) sendEvent(event *APIEvents) { eventState.RLock() defer eventState.RUnlock() eventState.Add(1) defer eventState.Done() if eventState.enabled { if len(eventState.listeners) == 0 { eventState.errC <- ErrNoListeners return } for _, listener := range eventState.listeners { select { case listener <- event: default: } } } } func (eventState *eventMonitoringState) updateLastSeen(e *APIEvents) { eventState.Lock() defer eventState.Unlock() if atomic.LoadInt64(&eventState.lastSeen) < e.Time { atomic.StoreInt64(&eventState.lastSeen, e.Time) } } func (c *Client) eventHijack(opts EventsOptions, startTime int64, eventChan chan *APIEvents, errChan chan error) (closeConn func(), err error) { // on reconnect override initial Since with last event seen time if startTime != 0 { opts.Since = strconv.FormatInt(startTime, 10) } uri := "/events?" + queryString(opts) protocol := c.endpointURL.Scheme address := c.endpointURL.Path if protocol != "unix" && protocol != "npipe" { protocol = "tcp" address = c.endpointURL.Host } var dial net.Conn if c.TLSConfig == nil { dial, err = c.Dialer.Dial(protocol, address) } else { netDialer, ok := c.Dialer.(*net.Dialer) if !ok { return nil, ErrTLSNotSupported } dial, err = tlsDialWithDialer(netDialer, protocol, address, c.TLSConfig) } if err != nil { return nil, err } //lint:ignore SA1019 the alternative doesn't quite work, so keep using the deprecated thing. conn := httputil.NewClientConn(dial, nil) req, err := http.NewRequest(http.MethodGet, uri, nil) if err != nil { return nil, err } res, err := conn.Do(req) if err != nil { return nil, err } keepRunning := int32(1) //lint:ignore SA1019 the alternative doesn't quite work, so keep using the deprecated thing. go func(res *http.Response, conn *httputil.ClientConn) { defer conn.Close() defer res.Body.Close() decoder := json.NewDecoder(res.Body) for atomic.LoadInt32(&keepRunning) == 1 { var event APIEvents if err := decoder.Decode(&event); err != nil { if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { c.eventMonitor.RLock() if c.eventMonitor.enabled && c.eventMonitor.C == eventChan { // Signal that we're exiting. eventChan <- EOFEvent } c.eventMonitor.RUnlock() break } errChan <- err } if event.Time == 0 { continue } transformEvent(&event) c.eventMonitor.RLock() if c.eventMonitor.enabled && c.eventMonitor.C == eventChan { eventChan <- &event } c.eventMonitor.RUnlock() } }(res, conn) return func() { atomic.StoreInt32(&keepRunning, 0) }, nil } // transformEvent takes an event and determines what version it is from // then populates both versions of the event func transformEvent(event *APIEvents) { // if event version is <= 1.21 there will be no Action and no Type if event.Action == "" && event.Type == "" { event.Action = event.Status event.Actor.ID = event.ID event.Actor.Attributes = map[string]string{} switch event.Status { case "delete", "import", "pull", "push", "tag", "untag": event.Type = "image" default: event.Type = "container" if event.From != "" { event.Actor.Attributes["image"] = event.From } } } else { if event.Status == "" { if event.Type == "image" || event.Type == "container" { event.Status = event.Action } else { // Because just the Status has been overloaded with different Types // if an event is not for an image or a container, we prepend the type // to avoid problems for people relying on actions being only for // images and containers event.Status = event.Type + ":" + event.Action } } if event.ID == "" { event.ID = event.Actor.ID } if event.From == "" { event.From = event.Actor.Attributes["image"] } } } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/exec.go ================================================ // Copyright 2014 go-dockerclient authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package docker import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strconv" ) // Exec is the type representing a `docker exec` instance and containing the // instance ID type Exec struct { ID string `json:"Id,omitempty" yaml:"Id,omitempty"` } // CreateExecOptions specify parameters to the CreateExecContainer function. // // See https://goo.gl/60TeBP for more details type CreateExecOptions struct { Env []string `json:"Env,omitempty" yaml:"Env,omitempty" toml:"Env,omitempty"` Cmd []string `json:"Cmd,omitempty" yaml:"Cmd,omitempty" toml:"Cmd,omitempty"` Container string `json:"Container,omitempty" yaml:"Container,omitempty" toml:"Container,omitempty"` User string `json:"User,omitempty" yaml:"User,omitempty" toml:"User,omitempty"` WorkingDir string `json:"WorkingDir,omitempty" yaml:"WorkingDir,omitempty" toml:"WorkingDir,omitempty"` DetachKeys string `json:"DetachKeys,omitempty" yaml:"DetachKeys,omitempty" toml:"DetachKeys,omitempty"` Context context.Context `json:"-"` AttachStdin bool `json:"AttachStdin,omitempty" yaml:"AttachStdin,omitempty" toml:"AttachStdin,omitempty"` AttachStdout bool `json:"AttachStdout,omitempty" yaml:"AttachStdout,omitempty" toml:"AttachStdout,omitempty"` AttachStderr bool `json:"AttachStderr,omitempty" yaml:"AttachStderr,omitempty" toml:"AttachStderr,omitempty"` Tty bool `json:"Tty,omitempty" yaml:"Tty,omitempty" toml:"Tty,omitempty"` Privileged bool `json:"Privileged,omitempty" yaml:"Privileged,omitempty" toml:"Privileged,omitempty"` } // CreateExec sets up an exec instance in a running container `id`, returning the exec // instance, or an error in case of failure. // // See https://goo.gl/60TeBP for more details func (c *Client) CreateExec(opts CreateExecOptions) (*Exec, error) { if c.serverAPIVersion == nil { c.checkAPIVersion() } if len(opts.Env) > 0 && c.serverAPIVersion.LessThan(apiVersion125) { return nil, errors.New("exec configuration Env is only supported in API#1.25 and above") } if len(opts.WorkingDir) > 0 && c.serverAPIVersion.LessThan(apiVersion135) { return nil, errors.New("exec configuration WorkingDir is only supported in API#1.35 and above") } path := fmt.Sprintf("/containers/%s/exec", opts.Container) resp, err := c.do(http.MethodPost, path, doOptions{data: opts, context: opts.Context}) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return nil, &NoSuchContainer{ID: opts.Container} } return nil, err } defer resp.Body.Close() var exec Exec if err := json.NewDecoder(resp.Body).Decode(&exec); err != nil { return nil, err } return &exec, nil } // StartExecOptions specify parameters to the StartExecContainer function. // // See https://goo.gl/1EeDWi for more details type StartExecOptions struct { InputStream io.Reader `qs:"-"` OutputStream io.Writer `qs:"-"` ErrorStream io.Writer `qs:"-"` Detach bool `json:"Detach,omitempty" yaml:"Detach,omitempty" toml:"Detach,omitempty"` Tty bool `json:"Tty,omitempty" yaml:"Tty,omitempty" toml:"Tty,omitempty"` // Use raw terminal? Usually true when the container contains a TTY. RawTerminal bool `qs:"-"` // If set, after a successful connect, a sentinel will be sent and then the // client will block on receive before continuing. // // It must be an unbuffered channel. Using a buffered channel can lead // to unexpected behavior. Success chan struct{} `json:"-"` Context context.Context `json:"-"` } // StartExec starts a previously set up exec instance id. If opts.Detach is // true, it returns after starting the exec command. Otherwise, it sets up an // interactive session with the exec command. // // See https://goo.gl/1EeDWi for more details func (c *Client) StartExec(id string, opts StartExecOptions) error { cw, err := c.StartExecNonBlocking(id, opts) if err != nil { return err } if cw != nil { return cw.Wait() } return nil } // StartExecNonBlocking starts a previously set up exec instance id. If opts.Detach is // true, it returns after starting the exec command. Otherwise, it sets up an // interactive session with the exec command. // // See https://goo.gl/1EeDWi for more details func (c *Client) StartExecNonBlocking(id string, opts StartExecOptions) (CloseWaiter, error) { if id == "" { return nil, &NoSuchExec{ID: id} } path := fmt.Sprintf("/exec/%s/start", id) if opts.Detach { resp, err := c.do(http.MethodPost, path, doOptions{data: opts, context: opts.Context}) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return nil, &NoSuchExec{ID: id} } return nil, err } defer resp.Body.Close() return nil, nil } return c.hijack(http.MethodPost, path, hijackOptions{ success: opts.Success, setRawTerminal: opts.RawTerminal, in: opts.InputStream, stdout: opts.OutputStream, stderr: opts.ErrorStream, data: opts, }) } // ResizeExecTTY resizes the tty session used by the exec command id. This API // is valid only if Tty was specified as part of creating and starting the exec // command. // // See https://goo.gl/Mo5bxx for more details func (c *Client) ResizeExecTTY(id string, height, width int) error { params := make(url.Values) params.Set("h", strconv.Itoa(height)) params.Set("w", strconv.Itoa(width)) path := fmt.Sprintf("/exec/%s/resize?%s", id, params.Encode()) resp, err := c.do(http.MethodPost, path, doOptions{}) if err != nil { return err } resp.Body.Close() return nil } // ExecProcessConfig is a type describing the command associated to a Exec // instance. It's used in the ExecInspect type. type ExecProcessConfig struct { User string `json:"user,omitempty" yaml:"user,omitempty" toml:"user,omitempty"` Privileged bool `json:"privileged,omitempty" yaml:"privileged,omitempty" toml:"privileged,omitempty"` Tty bool `json:"tty,omitempty" yaml:"tty,omitempty" toml:"tty,omitempty"` EntryPoint string `json:"entrypoint,omitempty" yaml:"entrypoint,omitempty" toml:"entrypoint,omitempty"` Arguments []string `json:"arguments,omitempty" yaml:"arguments,omitempty" toml:"arguments,omitempty"` } // ExecInspect is a type with details about a exec instance, including the // exit code if the command has finished running. It's returned by a api // call to /exec/(id)/json // // See https://goo.gl/ctMUiW for more details type ExecInspect struct { ID string `json:"ID,omitempty" yaml:"ID,omitempty" toml:"ID,omitempty"` ExitCode int `json:"ExitCode,omitempty" yaml:"ExitCode,omitempty" toml:"ExitCode,omitempty"` ProcessConfig ExecProcessConfig `json:"ProcessConfig,omitempty" yaml:"ProcessConfig,omitempty" toml:"ProcessConfig,omitempty"` ContainerID string `json:"ContainerID,omitempty" yaml:"ContainerID,omitempty" toml:"ContainerID,omitempty"` DetachKeys string `json:"DetachKeys,omitempty" yaml:"DetachKeys,omitempty" toml:"DetachKeys,omitempty"` Running bool `json:"Running,omitempty" yaml:"Running,omitempty" toml:"Running,omitempty"` OpenStdin bool `json:"OpenStdin,omitempty" yaml:"OpenStdin,omitempty" toml:"OpenStdin,omitempty"` OpenStderr bool `json:"OpenStderr,omitempty" yaml:"OpenStderr,omitempty" toml:"OpenStderr,omitempty"` OpenStdout bool `json:"OpenStdout,omitempty" yaml:"OpenStdout,omitempty" toml:"OpenStdout,omitempty"` CanRemove bool `json:"CanRemove,omitempty" yaml:"CanRemove,omitempty" toml:"CanRemove,omitempty"` } // InspectExec returns low-level information about the exec command id. // // See https://goo.gl/ctMUiW for more details func (c *Client) InspectExec(id string) (*ExecInspect, error) { path := fmt.Sprintf("/exec/%s/json", id) resp, err := c.do(http.MethodGet, path, doOptions{}) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return nil, &NoSuchExec{ID: id} } return nil, err } defer resp.Body.Close() var exec ExecInspect if err := json.NewDecoder(resp.Body).Decode(&exec); err != nil { return nil, err } return &exec, nil } // NoSuchExec is the error returned when a given exec instance does not exist. type NoSuchExec struct { ID string } func (err *NoSuchExec) Error() string { return "No such exec instance: " + err.ID } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/image.go ================================================ // Copyright 2013 go-dockerclient authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package docker import ( "context" "encoding/base64" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) // APIImages represent an image returned in the ListImages call. type APIImages struct { ID string `json:"Id" yaml:"Id" toml:"Id"` RepoTags []string `json:"RepoTags,omitempty" yaml:"RepoTags,omitempty" toml:"RepoTags,omitempty"` Created int64 `json:"Created,omitempty" yaml:"Created,omitempty" toml:"Created,omitempty"` Size int64 `json:"Size,omitempty" yaml:"Size,omitempty" toml:"Size,omitempty"` // Deprecated: VirtualSize is deprecated as of API 1.44. Use Size instead. VirtualSize int64 `json:"VirtualSize,omitempty" yaml:"VirtualSize,omitempty" toml:"VirtualSize,omitempty"` ParentID string `json:"ParentId,omitempty" yaml:"ParentId,omitempty" toml:"ParentId,omitempty"` RepoDigests []string `json:"RepoDigests,omitempty" yaml:"RepoDigests,omitempty" toml:"RepoDigests,omitempty"` Labels map[string]string `json:"Labels,omitempty" yaml:"Labels,omitempty" toml:"Labels,omitempty"` } // RootFS represents the underlying layers used by an image type RootFS struct { Type string `json:"Type,omitempty" yaml:"Type,omitempty" toml:"Type,omitempty"` Layers []string `json:"Layers,omitempty" yaml:"Layers,omitempty" toml:"Layers,omitempty"` } // Image is the type representing a docker image and its various properties type Image struct { ID string `json:"Id" yaml:"Id" toml:"Id"` RepoTags []string `json:"RepoTags,omitempty" yaml:"RepoTags,omitempty" toml:"RepoTags,omitempty"` Parent string `json:"Parent,omitempty" yaml:"Parent,omitempty" toml:"Parent,omitempty"` Comment string `json:"Comment,omitempty" yaml:"Comment,omitempty" toml:"Comment,omitempty"` Created time.Time `json:"Created,omitempty" yaml:"Created,omitempty" toml:"Created,omitempty"` Container string `json:"Container,omitempty" yaml:"Container,omitempty" toml:"Container,omitempty"` ContainerConfig Config `json:"ContainerConfig,omitempty" yaml:"ContainerConfig,omitempty" toml:"ContainerConfig,omitempty"` DockerVersion string `json:"DockerVersion,omitempty" yaml:"DockerVersion,omitempty" toml:"DockerVersion,omitempty"` Author string `json:"Author,omitempty" yaml:"Author,omitempty" toml:"Author,omitempty"` Config *Config `json:"Config,omitempty" yaml:"Config,omitempty" toml:"Config,omitempty"` Architecture string `json:"Architecture,omitempty" yaml:"Architecture,omitempty"` Size int64 `json:"Size,omitempty" yaml:"Size,omitempty" toml:"Size,omitempty"` // Deprecated: VirtualSize is deprecated as of API 1.44. Use Size instead. VirtualSize int64 `json:"VirtualSize,omitempty" yaml:"VirtualSize,omitempty" toml:"VirtualSize,omitempty"` RepoDigests []string `json:"RepoDigests,omitempty" yaml:"RepoDigests,omitempty" toml:"RepoDigests,omitempty"` RootFS *RootFS `json:"RootFS,omitempty" yaml:"RootFS,omitempty" toml:"RootFS,omitempty"` OS string `json:"Os,omitempty" yaml:"Os,omitempty" toml:"Os,omitempty"` } // ImagePre012 serves the same purpose as the Image type except that it is for // earlier versions of the Docker API (pre-012 to be specific) type ImagePre012 struct { ID string `json:"id"` Parent string `json:"parent,omitempty"` Comment string `json:"comment,omitempty"` Created time.Time `json:"created"` Container string `json:"container,omitempty"` ContainerConfig Config `json:"container_config,omitempty"` DockerVersion string `json:"docker_version,omitempty"` Author string `json:"author,omitempty"` Config *Config `json:"config,omitempty"` Architecture string `json:"architecture,omitempty"` Size int64 `json:"size,omitempty"` } var ( // ErrNoSuchImage is the error returned when the image does not exist. ErrNoSuchImage = errors.New("no such image") // ErrMissingRepo is the error returned when the remote repository is // missing. ErrMissingRepo = errors.New("missing remote repository e.g. 'github.com/user/repo'") // ErrMissingOutputStream is the error returned when no output stream // is provided to some calls, like BuildImage. ErrMissingOutputStream = errors.New("missing output stream") // ErrMultipleContexts is the error returned when both a ContextDir and // InputStream are provided in BuildImageOptions ErrMultipleContexts = errors.New("image build may not be provided BOTH context dir and input stream") // ErrMustSpecifyNames is the error returned when the Names field on // ExportImagesOptions is nil or empty ErrMustSpecifyNames = errors.New("must specify at least one name to export") ) // ListImagesOptions specify parameters to the ListImages function. // // See https://goo.gl/BVzauZ for more details. type ListImagesOptions struct { Filters map[string][]string All bool Digests bool Filter string Context context.Context } // ListImages returns the list of available images in the server. // // See https://goo.gl/BVzauZ for more details. func (c *Client) ListImages(opts ListImagesOptions) ([]APIImages, error) { path := "/images/json?" + queryString(opts) resp, err := c.do(http.MethodGet, path, doOptions{context: opts.Context}) if err != nil { return nil, err } defer resp.Body.Close() var images []APIImages if err := json.NewDecoder(resp.Body).Decode(&images); err != nil { return nil, err } return images, nil } // ImageHistory represent a layer in an image's history returned by the // ImageHistory call. type ImageHistory struct { ID string `json:"Id" yaml:"Id" toml:"Id"` Tags []string `json:"Tags,omitempty" yaml:"Tags,omitempty" toml:"Tags,omitempty"` Created int64 `json:"Created,omitempty" yaml:"Created,omitempty" toml:"Tags,omitempty"` CreatedBy string `json:"CreatedBy,omitempty" yaml:"CreatedBy,omitempty" toml:"CreatedBy,omitempty"` Size int64 `json:"Size,omitempty" yaml:"Size,omitempty" toml:"Size,omitempty"` Comment string `json:"Comment,omitempty" yaml:"Comment,omitempty" toml:"Comment,omitempty"` } // ImageHistory returns the history of the image by its name or ID. // // See https://goo.gl/fYtxQa for more details. func (c *Client) ImageHistory(name string) ([]ImageHistory, error) { resp, err := c.do(http.MethodGet, "/images/"+name+"/history", doOptions{}) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return nil, ErrNoSuchImage } return nil, err } defer resp.Body.Close() var history []ImageHistory if err := json.NewDecoder(resp.Body).Decode(&history); err != nil { return nil, err } return history, nil } // RemoveImage removes an image by its name or ID. // // See https://goo.gl/Vd2Pck for more details. func (c *Client) RemoveImage(name string) error { resp, err := c.do(http.MethodDelete, "/images/"+name, doOptions{}) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return ErrNoSuchImage } return err } resp.Body.Close() return nil } // RemoveImageOptions present the set of options available for removing an image // from a registry. // // See https://goo.gl/Vd2Pck for more details. type RemoveImageOptions struct { Force bool `qs:"force"` NoPrune bool `qs:"noprune"` Context context.Context } // RemoveImageExtended removes an image by its name or ID. // Extra params can be passed, see RemoveImageOptions // // See https://goo.gl/Vd2Pck for more details. func (c *Client) RemoveImageExtended(name string, opts RemoveImageOptions) error { uri := fmt.Sprintf("/images/%s?%s", name, queryString(&opts)) resp, err := c.do(http.MethodDelete, uri, doOptions{context: opts.Context}) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return ErrNoSuchImage } return err } resp.Body.Close() return nil } // InspectImage returns an image by its name or ID. // // See https://goo.gl/ncLTG8 for more details. func (c *Client) InspectImage(name string) (*Image, error) { resp, err := c.do(http.MethodGet, "/images/"+name+"/json", doOptions{}) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return nil, ErrNoSuchImage } return nil, err } defer resp.Body.Close() var image Image // if the caller elected to skip checking the server's version, assume it's the latest if c.SkipServerVersionCheck || c.expectedAPIVersion.GreaterThanOrEqualTo(apiVersion112) { if err := json.NewDecoder(resp.Body).Decode(&image); err != nil { return nil, err } } else { var imagePre012 ImagePre012 if err := json.NewDecoder(resp.Body).Decode(&imagePre012); err != nil { return nil, err } image.ID = imagePre012.ID image.Parent = imagePre012.Parent image.Comment = imagePre012.Comment image.Created = imagePre012.Created image.Container = imagePre012.Container image.ContainerConfig = imagePre012.ContainerConfig image.DockerVersion = imagePre012.DockerVersion image.Author = imagePre012.Author image.Config = imagePre012.Config image.Architecture = imagePre012.Architecture image.Size = imagePre012.Size } return &image, nil } // PushImageOptions represents options to use in the PushImage method. // // See https://goo.gl/BZemGg for more details. type PushImageOptions struct { // Name of the image Name string // Tag of the image Tag string // Registry server to push the image Registry string OutputStream io.Writer `qs:"-"` RawJSONStream bool `qs:"-"` InactivityTimeout time.Duration `qs:"-"` Context context.Context } // PushImage pushes an image to a remote registry, logging progress to w. // // An empty instance of AuthConfiguration may be used for unauthenticated // pushes. // // See https://goo.gl/BZemGg for more details. func (c *Client) PushImage(opts PushImageOptions, auth AuthConfiguration) error { if opts.Name == "" { return ErrNoSuchImage } headers, err := headersWithAuth(auth) if err != nil { return err } name := opts.Name opts.Name = "" path := "/images/" + name + "/push?" + queryString(&opts) return c.stream(http.MethodPost, path, streamOptions{ setRawTerminal: true, rawJSONStream: opts.RawJSONStream, headers: headers, stdout: opts.OutputStream, inactivityTimeout: opts.InactivityTimeout, context: opts.Context, }) } // PullImageOptions present the set of options available for pulling an image // from a registry. // // See https://goo.gl/qkoSsn for more details. type PullImageOptions struct { All bool Repository string `qs:"fromImage"` Tag string Platform string `ver:"1.32"` // Only required for Docker Engine 1.9 or 1.10 w/ Remote API < 1.21 // and Docker Engine < 1.9 // This parameter was removed in Docker Engine 1.11 Registry string OutputStream io.Writer `qs:"-"` RawJSONStream bool `qs:"-"` InactivityTimeout time.Duration `qs:"-"` Context context.Context } // PullImage pulls an image from a remote registry, logging progress to // opts.OutputStream. // // See https://goo.gl/qkoSsn for more details. func (c *Client) PullImage(opts PullImageOptions, auth AuthConfiguration) error { if opts.Repository == "" { return ErrNoSuchImage } headers, err := headersWithAuth(auth) if err != nil { return err } if opts.Tag == "" && strings.Contains(opts.Repository, "@") { parts := strings.SplitN(opts.Repository, "@", 2) opts.Repository = parts[0] opts.Tag = parts[1] } return c.createImage(&opts, headers, nil, opts.OutputStream, opts.RawJSONStream, opts.InactivityTimeout, opts.Context) } func (c *Client) createImage(opts any, headers map[string]string, in io.Reader, w io.Writer, rawJSONStream bool, timeout time.Duration, context context.Context) error { url, err := c.getPath("/images/create", opts) if err != nil { return err } return c.streamURL(http.MethodPost, url, streamOptions{ setRawTerminal: true, headers: headers, in: in, stdout: w, rawJSONStream: rawJSONStream, inactivityTimeout: timeout, context: context, }) } // LoadImageOptions represents the options for LoadImage Docker API Call // // See https://goo.gl/rEsBV3 for more details. type LoadImageOptions struct { InputStream io.Reader OutputStream io.Writer Context context.Context } // LoadImage imports a tarball docker image // // See https://goo.gl/rEsBV3 for more details. func (c *Client) LoadImage(opts LoadImageOptions) error { return c.stream(http.MethodPost, "/images/load", streamOptions{ setRawTerminal: true, in: opts.InputStream, stdout: opts.OutputStream, context: opts.Context, }) } // ExportImageOptions represent the options for ExportImage Docker API call. // // See https://goo.gl/AuySaA for more details. type ExportImageOptions struct { Name string OutputStream io.Writer InactivityTimeout time.Duration Context context.Context } // ExportImage exports an image (as a tar file) into the stream. // // See https://goo.gl/AuySaA for more details. func (c *Client) ExportImage(opts ExportImageOptions) error { return c.stream(http.MethodGet, fmt.Sprintf("/images/%s/get", opts.Name), streamOptions{ setRawTerminal: true, stdout: opts.OutputStream, inactivityTimeout: opts.InactivityTimeout, context: opts.Context, }) } // ExportImagesOptions represent the options for ExportImages Docker API call // // See https://goo.gl/N9XlDn for more details. type ExportImagesOptions struct { Names []string OutputStream io.Writer `qs:"-"` InactivityTimeout time.Duration `qs:"-"` Context context.Context } // ExportImages exports one or more images (as a tar file) into the stream // // See https://goo.gl/N9XlDn for more details. func (c *Client) ExportImages(opts ExportImagesOptions) error { if len(opts.Names) == 0 { return ErrMustSpecifyNames } // API < 1.25 allows multiple name values // 1.25 says name must be a comma separated list var err error var exporturl string if c.requestedAPIVersion.GreaterThanOrEqualTo(apiVersion125) { str := opts.Names[0] for _, val := range opts.Names[1:] { str += "," + val } exporturl, err = c.getPath("/images/get", ExportImagesOptions{ Names: []string{str}, OutputStream: opts.OutputStream, InactivityTimeout: opts.InactivityTimeout, Context: opts.Context, }) } else { exporturl, err = c.getPath("/images/get", &opts) } if err != nil { return err } return c.streamURL(http.MethodGet, exporturl, streamOptions{ setRawTerminal: true, stdout: opts.OutputStream, inactivityTimeout: opts.InactivityTimeout, }) } // ImportImageOptions present the set of informations available for importing // an image from a source file or the stdin. // // See https://goo.gl/qkoSsn for more details. type ImportImageOptions struct { Repository string `qs:"repo"` Source string `qs:"fromSrc"` Tag string `qs:"tag"` InputStream io.Reader `qs:"-"` OutputStream io.Writer `qs:"-"` RawJSONStream bool `qs:"-"` InactivityTimeout time.Duration `qs:"-"` Context context.Context } // ImportImage imports an image from a url, a file or stdin // // See https://goo.gl/qkoSsn for more details. func (c *Client) ImportImage(opts ImportImageOptions) error { if opts.Repository == "" { return ErrNoSuchImage } if opts.Source != "-" { opts.InputStream = nil } if opts.Source != "-" && !isURL(opts.Source) { f, err := os.Open(opts.Source) if err != nil { return err } opts.InputStream = f opts.Source = "-" } return c.createImage(&opts, nil, opts.InputStream, opts.OutputStream, opts.RawJSONStream, opts.InactivityTimeout, opts.Context) } // BuilderVersion represents either the BuildKit or V1 ("classic") builder. type BuilderVersion string const ( BuilderV1 BuilderVersion = "1" BuilderBuildKit BuilderVersion = "2" ) // BuildImageOptions present the set of informations available for building an // image from a tarfile with a Dockerfile in it. // // For more details about the Docker building process, see // https://goo.gl/4nYHwV. type BuildImageOptions struct { Context context.Context Name string `qs:"t"` Dockerfile string `ver:"1.25"` ExtraHosts string `ver:"1.28"` CacheFrom []string `qs:"-" ver:"1.25"` Memory int64 Memswap int64 ShmSize int64 CPUShares int64 CPUQuota int64 `ver:"1.21"` CPUPeriod int64 `ver:"1.21"` CPUSetCPUs string Labels map[string]string InputStream io.Reader `qs:"-"` OutputStream io.Writer `qs:"-"` Remote string Auth AuthConfiguration `qs:"-"` // for older docker X-Registry-Auth header AuthConfigs AuthConfigurations `qs:"-"` // for newer docker X-Registry-Config header ContextDir string `qs:"-"` Ulimits []ULimit `qs:"-" ver:"1.18"` BuildArgs []BuildArg `qs:"-" ver:"1.21"` NetworkMode string `ver:"1.25"` Platform string `ver:"1.32"` InactivityTimeout time.Duration `qs:"-"` CgroupParent string SecurityOpt []string Target string Outputs string `ver:"1.40"` NoCache bool SuppressOutput bool `qs:"q"` Pull bool `ver:"1.16"` RmTmpContainer bool `qs:"rm"` ForceRmTmpContainer bool `qs:"forcerm" ver:"1.12"` RawJSONStream bool `qs:"-"` Version BuilderVersion `qs:"version" ver:"1.39"` } // BuildArg represents arguments that can be passed to the image when building // it from a Dockerfile. // // For more details about the Docker building process, see // https://goo.gl/4nYHwV. type BuildArg struct { Name string `json:"Name,omitempty" yaml:"Name,omitempty" toml:"Name,omitempty"` Value string `json:"Value,omitempty" yaml:"Value,omitempty" toml:"Value,omitempty"` } // BuildImage builds an image from a tarball's url or a Dockerfile in the input // stream. // // See https://goo.gl/4nYHwV for more details. func (c *Client) BuildImage(opts BuildImageOptions) error { if opts.OutputStream == nil { return ErrMissingOutputStream } headers, err := headersWithAuth(opts.Auth, c.versionedAuthConfigs(opts.AuthConfigs)) if err != nil { return err } if opts.Remote != "" && opts.Name == "" { opts.Name = opts.Remote } if opts.InputStream != nil || opts.ContextDir != "" { headers["Content-Type"] = "application/tar" } else if opts.Remote == "" { return ErrMissingRepo } if opts.ContextDir != "" { if opts.InputStream != nil { return ErrMultipleContexts } var err error if opts.InputStream, err = createTarStream(opts.ContextDir, opts.Dockerfile); err != nil { return err } } qs, ver := queryStringVersion(&opts) if len(opts.CacheFrom) > 0 { if b, err := json.Marshal(opts.CacheFrom); err == nil { item := url.Values(map[string][]string{}) item.Add("cachefrom", string(b)) qs = fmt.Sprintf("%s&%s", qs, item.Encode()) if ver == nil || apiVersion125.GreaterThan(ver) { ver = apiVersion125 } } } if len(opts.Ulimits) > 0 { if b, err := json.Marshal(opts.Ulimits); err == nil { item := url.Values(map[string][]string{}) item.Add("ulimits", string(b)) qs = fmt.Sprintf("%s&%s", qs, item.Encode()) if ver == nil || apiVersion118.GreaterThan(ver) { ver = apiVersion118 } } } if len(opts.BuildArgs) > 0 { v := make(map[string]string) for _, arg := range opts.BuildArgs { v[arg.Name] = arg.Value } if b, err := json.Marshal(v); err == nil { item := url.Values(map[string][]string{}) item.Add("buildargs", string(b)) qs = fmt.Sprintf("%s&%s", qs, item.Encode()) if ver == nil || apiVersion121.GreaterThan(ver) { ver = apiVersion121 } } } buildURL, err := c.pathVersionCheck("/build", qs, ver) if err != nil { return err } return c.streamURL(http.MethodPost, buildURL, streamOptions{ setRawTerminal: true, rawJSONStream: opts.RawJSONStream, headers: headers, in: opts.InputStream, stdout: opts.OutputStream, inactivityTimeout: opts.InactivityTimeout, context: opts.Context, }) } func (c *Client) versionedAuthConfigs(authConfigs AuthConfigurations) registryAuth { if c.serverAPIVersion == nil { c.checkAPIVersion() } if c.serverAPIVersion != nil && c.serverAPIVersion.GreaterThanOrEqualTo(apiVersion119) { return AuthConfigurations119(authConfigs.Configs) } return authConfigs } // TagImageOptions present the set of options to tag an image. // // See https://goo.gl/prHrvo for more details. type TagImageOptions struct { Repo string Tag string Force bool Context context.Context } // TagImage adds a tag to the image identified by the given name. // // See https://goo.gl/prHrvo for more details. func (c *Client) TagImage(name string, opts TagImageOptions) error { if name == "" { return ErrNoSuchImage } resp, err := c.do(http.MethodPost, "/images/"+name+"/tag?"+queryString(&opts), doOptions{ context: opts.Context, }) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return ErrNoSuchImage } return err } func isURL(u string) bool { p, err := url.Parse(u) if err != nil { return false } return p.Scheme == "http" || p.Scheme == "https" } func headersWithAuth(auths ...registryAuth) (map[string]string, error) { headers := make(map[string]string) for _, auth := range auths { if auth.isEmpty() { continue } data, err := json.Marshal(auth) if err != nil { return nil, err } headers[auth.headerKey()] = base64.URLEncoding.EncodeToString(data) } return headers, nil } // APIImageSearch reflect the result of a search on the Docker Hub. // // See https://goo.gl/KLO9IZ for more details. type APIImageSearch struct { Description string `json:"description,omitempty" yaml:"description,omitempty" toml:"description,omitempty"` IsOfficial bool `json:"is_official,omitempty" yaml:"is_official,omitempty" toml:"is_official,omitempty"` IsAutomated bool `json:"is_automated,omitempty" yaml:"is_automated,omitempty" toml:"is_automated,omitempty"` Name string `json:"name,omitempty" yaml:"name,omitempty" toml:"name,omitempty"` StarCount int `json:"star_count,omitempty" yaml:"star_count,omitempty" toml:"star_count,omitempty"` } // SearchImages search the docker hub with a specific given term. // // See https://goo.gl/KLO9IZ for more details. func (c *Client) SearchImages(term string) ([]APIImageSearch, error) { resp, err := c.do(http.MethodGet, "/images/search?term="+term, doOptions{}) if err != nil { return nil, err } defer resp.Body.Close() var searchResult []APIImageSearch if err := json.NewDecoder(resp.Body).Decode(&searchResult); err != nil { return nil, err } return searchResult, nil } // SearchImagesEx search the docker hub with a specific given term and authentication. // // See https://goo.gl/KLO9IZ for more details. func (c *Client) SearchImagesEx(term string, auth AuthConfiguration) ([]APIImageSearch, error) { headers, err := headersWithAuth(auth) if err != nil { return nil, err } resp, err := c.do(http.MethodGet, "/images/search?term="+term, doOptions{ headers: headers, }) if err != nil { return nil, err } defer resp.Body.Close() var searchResult []APIImageSearch if err := json.NewDecoder(resp.Body).Decode(&searchResult); err != nil { return nil, err } return searchResult, nil } // PruneImagesOptions specify parameters to the PruneImages function. // // See https://goo.gl/qfZlbZ for more details. type PruneImagesOptions struct { Filters map[string][]string Context context.Context } // PruneImagesResults specify results from the PruneImages function. // // See https://goo.gl/qfZlbZ for more details. type PruneImagesResults struct { ImagesDeleted []struct{ Untagged, Deleted string } SpaceReclaimed int64 } // PruneImages deletes images which are unused. // // See https://goo.gl/qfZlbZ for more details. func (c *Client) PruneImages(opts PruneImagesOptions) (*PruneImagesResults, error) { path := "/images/prune?" + queryString(opts) resp, err := c.do(http.MethodPost, path, doOptions{context: opts.Context}) if err != nil { return nil, err } defer resp.Body.Close() var results PruneImagesResults if err := json.NewDecoder(resp.Body).Decode(&results); err != nil { return nil, err } return &results, nil } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/misc.go ================================================ // Copyright 2013 go-dockerclient authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package docker import ( "context" "encoding/json" "net" "net/http" "strings" ) // Version returns version information about the docker server. // // See https://goo.gl/mU7yje for more details. func (c *Client) Version() (*Env, error) { return c.VersionWithContext(context.TODO()) } // VersionWithContext returns version information about the docker server. func (c *Client) VersionWithContext(ctx context.Context) (*Env, error) { resp, err := c.do(http.MethodGet, "/version", doOptions{context: ctx}) if err != nil { return nil, err } defer resp.Body.Close() var env Env if err := env.Decode(resp.Body); err != nil { return nil, err } return &env, nil } // DockerInfo contains information about the Docker server // // See https://goo.gl/bHUoz9 for more details. type DockerInfo struct { ID string Containers int ContainersRunning int ContainersPaused int ContainersStopped int Images int Driver string DriverStatus [][2]string SystemStatus [][2]string Plugins PluginsInfo NFd int NGoroutines int SystemTime string ExecutionDriver string LoggingDriver string CgroupDriver string NEventsListener int KernelVersion string OperatingSystem string OSType string Architecture string IndexServerAddress string RegistryConfig *ServiceConfig SecurityOptions []string NCPU int MemTotal int64 DockerRootDir string HTTPProxy string `json:"HttpProxy"` HTTPSProxy string `json:"HttpsProxy"` NoProxy string Name string Labels []string ServerVersion string ClusterStore string Runtimes map[string]Runtime ClusterAdvertise string Isolation string InitBinary string DefaultRuntime string Swarm SwarmInfo LiveRestoreEnabled bool MemoryLimit bool SwapLimit bool // Deprecated: KernelMemory is deprecated as of API 1.42 and removed in API 1.52. KernelMemory bool CPUCfsPeriod bool `json:"CpuCfsPeriod"` CPUCfsQuota bool `json:"CpuCfsQuota"` CPUShares bool CPUSet bool IPv4Forwarding bool BridgeNfIptables bool BridgeNfIP6tables bool `json:"BridgeNfIp6tables"` Debug bool OomKillDisable bool ExperimentalBuild bool CDISpecDirs []string `json:"CDISpecDirs,omitempty"` Containerd *ContainerdInfo `json:"Containerd,omitempty"` } // Runtime describes an OCI runtime // // for more information, see: https://dockr.ly/2NKM8qq type Runtime struct { Path string Args []string `json:"runtimeArgs"` } // ContainerdInfo contains information about the containerd instance. type ContainerdInfo struct { Address string `json:"Address,omitempty"` Namespaces struct { Containers string `json:"Containers,omitempty"` Plugins string `json:"Plugins,omitempty"` } `json:"Namespaces,omitempty"` } // SwarmInfo contains basic swarm information from docker info. // Deprecated: Docker Swarm is deprecated. type SwarmInfo struct { NodeID string NodeAddr string } // PluginsInfo is a struct with the plugins registered with the docker daemon // // for more information, see: https://goo.gl/bHUoz9 type PluginsInfo struct { // List of Volume plugins registered Volume []string // List of Network plugins registered Network []string // List of Authorization plugins registered Authorization []string } // ServiceConfig stores daemon registry services configuration. // // for more information, see: https://goo.gl/7iFFDz type ServiceConfig struct { InsecureRegistryCIDRs []*NetIPNet IndexConfigs map[string]*IndexInfo Mirrors []string } // NetIPNet is the net.IPNet type, which can be marshalled and // unmarshalled to JSON. // // for more information, see: https://goo.gl/7iFFDz type NetIPNet net.IPNet // MarshalJSON returns the JSON representation of the IPNet. func (ipnet *NetIPNet) MarshalJSON() ([]byte, error) { return json.Marshal((*net.IPNet)(ipnet).String()) } // UnmarshalJSON sets the IPNet from a byte array of JSON. func (ipnet *NetIPNet) UnmarshalJSON(b []byte) (err error) { var ipnetStr string if err = json.Unmarshal(b, &ipnetStr); err == nil { var cidr *net.IPNet if _, cidr, err = net.ParseCIDR(ipnetStr); err == nil { *ipnet = NetIPNet(*cidr) } } return err } // IndexInfo contains information about a registry. // // for more information, see: https://goo.gl/7iFFDz type IndexInfo struct { Name string Mirrors []string Secure bool Official bool } // Info returns system-wide information about the Docker server. // // See https://goo.gl/ElTHi2 for more details. func (c *Client) Info() (*DockerInfo, error) { resp, err := c.do(http.MethodGet, "/info", doOptions{}) if err != nil { return nil, err } defer resp.Body.Close() var info DockerInfo if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { return nil, err } return &info, nil } // ParseRepositoryTag gets the name of the repository and returns it splitted // in two parts: the repository and the tag. It ignores the digest when it is // present. // // Some examples: // // localhost.localdomain:5000/samalba/hipache:latest -> localhost.localdomain:5000/samalba/hipache, latest // localhost.localdomain:5000/samalba/hipache -> localhost.localdomain:5000/samalba/hipache, "" // busybox:latest@sha256:4a731fb46adc5cefe3ae374a8b6020fc1b6ad667a279647766e9a3cd89f6fa92 -> busybox, latest func ParseRepositoryTag(repoTag string) (repository string, tag string) { parts := strings.SplitN(repoTag, "@", 2) repoTag = parts[0] n := strings.LastIndex(repoTag, ":") if n < 0 { return repoTag, "" } if tag := repoTag[n+1:]; !strings.Contains(tag, "/") { return repoTag[:n], tag } return repoTag, "" } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/network.go ================================================ // Copyright 2015 go-dockerclient authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package docker import ( "context" "encoding/json" "errors" "fmt" "net/http" "net/url" ) // ErrNetworkAlreadyExists is the error returned by CreateNetwork when the // network already exists. var ErrNetworkAlreadyExists = errors.New("network already exists") // Network represents a network. // // See https://goo.gl/6GugX3 for more details. type Network struct { Name string ID string `json:"Id"` Scope string Driver string IPAM IPAMOptions Containers map[string]Endpoint Options map[string]string Internal bool EnableIPv6 bool `json:"EnableIPv6"` Labels map[string]string } // Endpoint contains network resources allocated and used for a container in a network // // See https://goo.gl/6GugX3 for more details. type Endpoint struct { Name string ID string `json:"EndpointID"` MacAddress string IPv4Address string IPv6Address string } // ListNetworks returns all networks. // // See https://goo.gl/6GugX3 for more details. func (c *Client) ListNetworks() ([]Network, error) { resp, err := c.do(http.MethodGet, "/networks", doOptions{}) if err != nil { return nil, err } defer resp.Body.Close() var networks []Network if err := json.NewDecoder(resp.Body).Decode(&networks); err != nil { return nil, err } return networks, nil } // NetworkFilterOpts is an aggregation of key=value that Docker // uses to filter networks type NetworkFilterOpts map[string]map[string]bool // FilteredListNetworks returns all networks with the filters applied // // See goo.gl/zd2mx4 for more details. func (c *Client) FilteredListNetworks(opts NetworkFilterOpts) ([]Network, error) { params, err := json.Marshal(opts) if err != nil { return nil, err } qs := make(url.Values) qs.Add("filters", string(params)) path := "/networks?" + qs.Encode() resp, err := c.do(http.MethodGet, path, doOptions{}) if err != nil { return nil, err } defer resp.Body.Close() var networks []Network if err := json.NewDecoder(resp.Body).Decode(&networks); err != nil { return nil, err } return networks, nil } // NetworkInfo returns information about a network by its ID. // // See https://goo.gl/6GugX3 for more details. func (c *Client) NetworkInfo(id string) (*Network, error) { path := "/networks/" + id resp, err := c.do(http.MethodGet, path, doOptions{}) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return nil, &NoSuchNetwork{ID: id} } return nil, err } defer resp.Body.Close() var network Network if err := json.NewDecoder(resp.Body).Decode(&network); err != nil { return nil, err } return &network, nil } // CreateNetworkOptions specify parameters to the CreateNetwork function and // (for now) is the expected body of the "create network" http request message // // See https://goo.gl/6GugX3 for more details. type CreateNetworkOptions struct { Name string `json:"Name" yaml:"Name" toml:"Name"` Driver string `json:"Driver" yaml:"Driver" toml:"Driver"` Scope string `json:"Scope" yaml:"Scope" toml:"Scope"` IPAM *IPAMOptions `json:"IPAM,omitempty" yaml:"IPAM" toml:"IPAM"` ConfigFrom *NetworkConfigFrom `json:"ConfigFrom,omitempty" yaml:"ConfigFrom" toml:"ConfigFrom"` Options map[string]any `json:"Options" yaml:"Options" toml:"Options"` Labels map[string]string `json:"Labels" yaml:"Labels" toml:"Labels"` CheckDuplicate bool `json:"CheckDuplicate" yaml:"CheckDuplicate" toml:"CheckDuplicate"` Internal bool `json:"Internal" yaml:"Internal" toml:"Internal"` EnableIPv6 bool `json:"EnableIPv6" yaml:"EnableIPv6" toml:"EnableIPv6"` EnableIPv4 bool `json:"EnableIPv4,omitempty" yaml:"EnableIPv4,omitempty" toml:"EnableIPv4,omitempty"` Attachable bool `json:"Attachable" yaml:"Attachable" toml:"Attachable"` ConfigOnly bool `json:"ConfigOnly" yaml:"ConfigOnly" toml:"ConfigOnly"` Ingress bool `json:"Ingress" yaml:"Ingress" toml:"Ingress"` Context context.Context `json:"-"` } // NetworkConfigFrom is used in network creation for specifying the source of a // network configuration. type NetworkConfigFrom struct { Network string `json:"Network" yaml:"Network" toml:"Network"` } // IPAMOptions controls IP Address Management when creating a network // // See https://goo.gl/T8kRVH for more details. type IPAMOptions struct { Driver string `json:"Driver" yaml:"Driver" toml:"Driver"` Config []IPAMConfig `json:"Config" yaml:"Config" toml:"Config"` Options map[string]string `json:"Options" yaml:"Options" toml:"Options"` } // IPAMConfig represents IPAM configurations // // See https://goo.gl/T8kRVH for more details. type IPAMConfig struct { Subnet string `json:",omitempty"` IPRange string `json:",omitempty"` Gateway string `json:",omitempty"` AuxAddress map[string]string `json:"AuxiliaryAddresses,omitempty"` } // CreateNetwork creates a new network, returning the network instance, // or an error in case of failure. // // See https://goo.gl/6GugX3 for more details. func (c *Client) CreateNetwork(opts CreateNetworkOptions) (*Network, error) { resp, err := c.do( http.MethodPost, "/networks/create", doOptions{ data: opts, context: opts.Context, }, ) if err != nil { return nil, err } defer resp.Body.Close() type createNetworkResponse struct { ID string } var ( network Network cnr createNetworkResponse ) if err := json.NewDecoder(resp.Body).Decode(&cnr); err != nil { return nil, err } network.Name = opts.Name network.ID = cnr.ID network.Driver = opts.Driver return &network, nil } // RemoveNetwork removes a network or returns an error in case of failure. // // See https://goo.gl/6GugX3 for more details. func (c *Client) RemoveNetwork(id string) error { resp, err := c.do(http.MethodDelete, "/networks/"+id, doOptions{}) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return &NoSuchNetwork{ID: id} } return err } resp.Body.Close() return nil } // NetworkConnectionOptions specify parameters to the ConnectNetwork and // DisconnectNetwork function. // // See https://goo.gl/RV7BJU for more details. type NetworkConnectionOptions struct { Container string // EndpointConfig is only applicable to the ConnectNetwork call EndpointConfig *EndpointConfig `json:"EndpointConfig,omitempty"` // Force is only applicable to the DisconnectNetwork call Force bool Context context.Context `json:"-"` } // EndpointConfig stores network endpoint details // // See https://goo.gl/RV7BJU for more details. type EndpointConfig struct { IPAMConfig *EndpointIPAMConfig `json:"IPAMConfig,omitempty" yaml:"IPAMConfig,omitempty" toml:"IPAMConfig,omitempty"` Links []string `json:"Links,omitempty" yaml:"Links,omitempty" toml:"Links,omitempty"` Aliases []string `json:"Aliases,omitempty" yaml:"Aliases,omitempty" toml:"Aliases,omitempty"` NetworkID string `json:"NetworkID,omitempty" yaml:"NetworkID,omitempty" toml:"NetworkID,omitempty"` EndpointID string `json:"EndpointID,omitempty" yaml:"EndpointID,omitempty" toml:"EndpointID,omitempty"` Gateway string `json:"Gateway,omitempty" yaml:"Gateway,omitempty" toml:"Gateway,omitempty"` IPAddress string `json:"IPAddress,omitempty" yaml:"IPAddress,omitempty" toml:"IPAddress,omitempty"` IPPrefixLen int `json:"IPPrefixLen,omitempty" yaml:"IPPrefixLen,omitempty" toml:"IPPrefixLen,omitempty"` IPv6Gateway string `json:"IPv6Gateway,omitempty" yaml:"IPv6Gateway,omitempty" toml:"IPv6Gateway,omitempty"` GlobalIPv6Address string `json:"GlobalIPv6Address,omitempty" yaml:"GlobalIPv6Address,omitempty" toml:"GlobalIPv6Address,omitempty"` GlobalIPv6PrefixLen int `json:"GlobalIPv6PrefixLen,omitempty" yaml:"GlobalIPv6PrefixLen,omitempty" toml:"GlobalIPv6PrefixLen,omitempty"` MacAddress string `json:"MacAddress,omitempty" yaml:"MacAddress,omitempty" toml:"MacAddress,omitempty"` DriverOpts map[string]string `json:"DriverOpts,omitempty" yaml:"DriverOpts,omitempty" toml:"DriverOpts,omitempty"` GwPriority int `json:"GwPriority,omitempty" yaml:"GwPriority,omitempty" toml:"GwPriority,omitempty"` } // EndpointIPAMConfig represents IPAM configurations for an // endpoint // // See https://goo.gl/RV7BJU for more details. type EndpointIPAMConfig struct { IPv4Address string `json:",omitempty" yaml:"IPv4Address,omitempty"` IPv6Address string `json:",omitempty" yaml:"IPv6Address,omitempty"` } // ConnectNetwork adds a container to a network or returns an error in case of // failure. // // See https://goo.gl/6GugX3 for more details. func (c *Client) ConnectNetwork(id string, opts NetworkConnectionOptions) error { resp, err := c.do(http.MethodPost, "/networks/"+id+"/connect", doOptions{ data: opts, context: opts.Context, }) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return &NoSuchNetworkOrContainer{NetworkID: id, ContainerID: opts.Container} } return err } resp.Body.Close() return nil } // DisconnectNetwork removes a container from a network or returns an error in // case of failure. // // See https://goo.gl/6GugX3 for more details. func (c *Client) DisconnectNetwork(id string, opts NetworkConnectionOptions) error { resp, err := c.do(http.MethodPost, "/networks/"+id+"/disconnect", doOptions{data: opts}) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return &NoSuchNetworkOrContainer{NetworkID: id, ContainerID: opts.Container} } return err } resp.Body.Close() return nil } // PruneNetworksOptions specify parameters to the PruneNetworks function. // // See https://goo.gl/kX0S9h for more details. type PruneNetworksOptions struct { Filters map[string][]string Context context.Context } // PruneNetworksResults specify results from the PruneNetworks function. // // See https://goo.gl/kX0S9h for more details. type PruneNetworksResults struct { NetworksDeleted []string } // PruneNetworks deletes networks which are unused. // // See https://goo.gl/kX0S9h for more details. func (c *Client) PruneNetworks(opts PruneNetworksOptions) (*PruneNetworksResults, error) { path := "/networks/prune?" + queryString(opts) resp, err := c.do(http.MethodPost, path, doOptions{context: opts.Context}) if err != nil { return nil, err } defer resp.Body.Close() var results PruneNetworksResults if err := json.NewDecoder(resp.Body).Decode(&results); err != nil { return nil, err } return &results, nil } // NoSuchNetwork is the error returned when a given network does not exist. type NoSuchNetwork struct { ID string } func (err *NoSuchNetwork) Error() string { return fmt.Sprintf("No such network: %s", err.ID) } // NoSuchNetworkOrContainer is the error returned when a given network or // container does not exist. type NoSuchNetworkOrContainer struct { NetworkID string ContainerID string } func (err *NoSuchNetworkOrContainer) Error() string { return fmt.Sprintf("No such network (%s) or container (%s)", err.NetworkID, err.ContainerID) } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/plugin.go ================================================ // Copyright 2018 go-dockerclient authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package docker import ( "context" "encoding/json" "errors" "io" "net/http" ) // PluginPrivilege represents a privilege for a plugin. type PluginPrivilege struct { Name string `json:"Name,omitempty" yaml:"Name,omitempty" toml:"Name,omitempty"` Description string `json:"Description,omitempty" yaml:"Description,omitempty" toml:"Description,omitempty"` Value []string `json:"Value,omitempty" yaml:"Value,omitempty" toml:"Value,omitempty"` } // InstallPluginOptions specify parameters to the InstallPlugins function. // // See https://goo.gl/C4t7Tz for more details. type InstallPluginOptions struct { Remote string Name string Plugins []PluginPrivilege `qs:"-"` Auth AuthConfiguration Context context.Context } // InstallPlugins installs a plugin or returns an error in case of failure. // // See https://goo.gl/C4t7Tz for more details. func (c *Client) InstallPlugins(opts InstallPluginOptions) error { headers, err := headersWithAuth(opts.Auth) if err != nil { return err } path := "/plugins/pull?" + queryString(opts) resp, err := c.do(http.MethodPost, path, doOptions{ data: opts.Plugins, context: opts.Context, headers: headers, }) if err != nil { return err } defer resp.Body.Close() // PullPlugin streams back the progress of the pull, we must consume the whole body // otherwise the pull will be canceled on the engine. if _, err := io.ReadAll(resp.Body); err != nil { return err } return nil } // PluginSettings stores plugin settings. // // See https://goo.gl/C4t7Tz for more details. type PluginSettings struct { Env []string `json:"Env,omitempty" yaml:"Env,omitempty" toml:"Env,omitempty"` Args []string `json:"Args,omitempty" yaml:"Args,omitempty" toml:"Args,omitempty"` Devices []string `json:"Devices,omitempty" yaml:"Devices,omitempty" toml:"Devices,omitempty"` } // PluginInterface stores plugin interface. // // See https://goo.gl/C4t7Tz for more details. type PluginInterface struct { Types []string `json:"Types,omitempty" yaml:"Types,omitempty" toml:"Types,omitempty"` Socket string `json:"Socket,omitempty" yaml:"Socket,omitempty" toml:"Socket,omitempty"` } // PluginNetwork stores plugin network type. // // See https://goo.gl/C4t7Tz for more details. type PluginNetwork struct { Type string `json:"Type,omitempty" yaml:"Type,omitempty" toml:"Type,omitempty"` } // PluginLinux stores plugin linux setting. // // See https://goo.gl/C4t7Tz for more details. type PluginLinux struct { Capabilities []string `json:"Capabilities,omitempty" yaml:"Capabilities,omitempty" toml:"Capabilities,omitempty"` AllowAllDevices bool `json:"AllowAllDevices,omitempty" yaml:"AllowAllDevices,omitempty" toml:"AllowAllDevices,omitempty"` Devices []PluginLinuxDevices `json:"Devices,omitempty" yaml:"Devices,omitempty" toml:"Devices,omitempty"` } // PluginLinuxDevices stores plugin linux device setting. // // See https://goo.gl/C4t7Tz for more details. type PluginLinuxDevices struct { Name string `json:"Name,omitempty" yaml:"Name,omitempty" toml:"Name,omitempty"` Description string `json:"Documentation,omitempty" yaml:"Documentation,omitempty" toml:"Documentation,omitempty"` Settable []string `json:"Settable,omitempty" yaml:"Settable,omitempty" toml:"Settable,omitempty"` Path string `json:"Path,omitempty" yaml:"Path,omitempty" toml:"Path,omitempty"` } // PluginEnv stores plugin environment. // // See https://goo.gl/C4t7Tz for more details. type PluginEnv struct { Name string `json:"Name,omitempty" yaml:"Name,omitempty" toml:"Name,omitempty"` Description string `json:"Description,omitempty" yaml:"Description,omitempty" toml:"Description,omitempty"` Settable []string `json:"Settable,omitempty" yaml:"Settable,omitempty" toml:"Settable,omitempty"` Value string `json:"Value,omitempty" yaml:"Value,omitempty" toml:"Value,omitempty"` } // PluginArgs stores plugin arguments. // // See https://goo.gl/C4t7Tz for more details. type PluginArgs struct { Name string `json:"Name,omitempty" yaml:"Name,omitempty" toml:"Name,omitempty"` Description string `json:"Description,omitempty" yaml:"Description,omitempty" toml:"Description,omitempty"` Settable []string `json:"Settable,omitempty" yaml:"Settable,omitempty" toml:"Settable,omitempty"` Value []string `json:"Value,omitempty" yaml:"Value,omitempty" toml:"Value,omitempty"` } // PluginUser stores plugin user. // // See https://goo.gl/C4t7Tz for more details. type PluginUser struct { UID int32 `json:"UID,omitempty" yaml:"UID,omitempty" toml:"UID,omitempty"` GID int32 `json:"GID,omitempty" yaml:"GID,omitempty" toml:"GID,omitempty"` } // PluginConfig stores plugin config. // // See https://goo.gl/C4t7Tz for more details. type PluginConfig struct { Description string `json:"Description,omitempty" yaml:"Description,omitempty" toml:"Description,omitempty"` Documentation string Interface PluginInterface `json:"Interface,omitempty" yaml:"Interface,omitempty" toml:"Interface,omitempty"` Entrypoint []string `json:"Entrypoint,omitempty" yaml:"Entrypoint,omitempty" toml:"Entrypoint,omitempty"` WorkDir string `json:"WorkDir,omitempty" yaml:"WorkDir,omitempty" toml:"WorkDir,omitempty"` User PluginUser `json:"User,omitempty" yaml:"User,omitempty" toml:"User,omitempty"` Network PluginNetwork `json:"Network,omitempty" yaml:"Network,omitempty" toml:"Network,omitempty"` Linux PluginLinux `json:"Linux,omitempty" yaml:"Linux,omitempty" toml:"Linux,omitempty"` PropagatedMount string `json:"PropagatedMount,omitempty" yaml:"PropagatedMount,omitempty" toml:"PropagatedMount,omitempty"` Mounts []Mount `json:"Mounts,omitempty" yaml:"Mounts,omitempty" toml:"Mounts,omitempty"` Env []PluginEnv `json:"Env,omitempty" yaml:"Env,omitempty" toml:"Env,omitempty"` Args PluginArgs `json:"Args,omitempty" yaml:"Args,omitempty" toml:"Args,omitempty"` } // PluginDetail specify results from the ListPlugins function. // // See https://goo.gl/C4t7Tz for more details. type PluginDetail struct { ID string `json:"Id,omitempty" yaml:"Id,omitempty" toml:"Id,omitempty"` Name string `json:"Name,omitempty" yaml:"Name,omitempty" toml:"Name,omitempty"` Tag string `json:"Tag,omitempty" yaml:"Tag,omitempty" toml:"Tag,omitempty"` Active bool `json:"Enabled,omitempty" yaml:"Active,omitempty" toml:"Active,omitempty"` Settings PluginSettings `json:"Settings,omitempty" yaml:"Settings,omitempty" toml:"Settings,omitempty"` Config PluginConfig `json:"Config,omitempty" yaml:"Config,omitempty" toml:"Config,omitempty"` } // ListPlugins returns pluginDetails or an error. // // See https://goo.gl/C4t7Tz for more details. func (c *Client) ListPlugins(ctx context.Context) ([]PluginDetail, error) { resp, err := c.do(http.MethodGet, "/plugins", doOptions{ context: ctx, }) if err != nil { return nil, err } defer resp.Body.Close() pluginDetails := make([]PluginDetail, 0) if err := json.NewDecoder(resp.Body).Decode(&pluginDetails); err != nil { return nil, err } return pluginDetails, nil } // ListFilteredPluginsOptions specify parameters to the ListFilteredPlugins function. // // See https://goo.gl/C4t7Tz for more details. type ListFilteredPluginsOptions struct { Filters map[string][]string Context context.Context } // ListFilteredPlugins returns pluginDetails or an error. // // See https://goo.gl/rmdmWg for more details. func (c *Client) ListFilteredPlugins(opts ListFilteredPluginsOptions) ([]PluginDetail, error) { path := "/plugins/json?" + queryString(opts) resp, err := c.do(http.MethodGet, path, doOptions{ context: opts.Context, }) if err != nil { return nil, err } defer resp.Body.Close() pluginDetails := make([]PluginDetail, 0) if err := json.NewDecoder(resp.Body).Decode(&pluginDetails); err != nil { return nil, err } return pluginDetails, nil } // GetPluginPrivileges returns pluginPrivileges or an error. // // See https://goo.gl/C4t7Tz for more details. func (c *Client) GetPluginPrivileges(remote string, ctx context.Context) ([]PluginPrivilege, error) { return c.GetPluginPrivilegesWithOptions( GetPluginPrivilegesOptions{ Remote: remote, Context: ctx, }) } // GetPluginPrivilegesOptions specify parameters to the GetPluginPrivilegesWithOptions function. // // See https://goo.gl/C4t7Tz for more details. type GetPluginPrivilegesOptions struct { Remote string Auth AuthConfiguration Context context.Context } // GetPluginPrivilegesWithOptions returns pluginPrivileges or an error. // // See https://goo.gl/C4t7Tz for more details. func (c *Client) GetPluginPrivilegesWithOptions(opts GetPluginPrivilegesOptions) ([]PluginPrivilege, error) { headers, err := headersWithAuth(opts.Auth) if err != nil { return nil, err } path := "/plugins/privileges?" + queryString(opts) resp, err := c.do(http.MethodGet, path, doOptions{ context: opts.Context, headers: headers, }) if err != nil { return nil, err } defer resp.Body.Close() var pluginPrivileges []PluginPrivilege if err := json.NewDecoder(resp.Body).Decode(&pluginPrivileges); err != nil { return nil, err } return pluginPrivileges, nil } // InspectPlugins returns a pluginDetail or an error. // // See https://goo.gl/C4t7Tz for more details. func (c *Client) InspectPlugins(name string, ctx context.Context) (*PluginDetail, error) { resp, err := c.do(http.MethodGet, "/plugins/"+name+"/json", doOptions{ context: ctx, }) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return nil, &NoSuchPlugin{ID: name} } return nil, err } defer resp.Body.Close() var pluginDetail PluginDetail if err := json.NewDecoder(resp.Body).Decode(&pluginDetail); err != nil { return nil, err } return &pluginDetail, nil } // RemovePluginOptions specify parameters to the RemovePlugin function. // // See https://goo.gl/C4t7Tz for more details. type RemovePluginOptions struct { // The Name of the plugin. Name string `qs:"-"` Force bool `qs:"force"` Context context.Context } // RemovePlugin returns a PluginDetail or an error. // // See https://goo.gl/C4t7Tz for more details. func (c *Client) RemovePlugin(opts RemovePluginOptions) (*PluginDetail, error) { path := "/plugins/" + opts.Name + "?" + queryString(opts) resp, err := c.do(http.MethodDelete, path, doOptions{context: opts.Context}) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return nil, &NoSuchPlugin{ID: opts.Name} } return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if len(body) == 0 { // Seems like newer docker versions won't return the plugindetail after removal return nil, nil } var pluginDetail PluginDetail if err := json.Unmarshal(body, &pluginDetail); err != nil { return nil, err } return &pluginDetail, nil } // EnablePluginOptions specify parameters to the EnablePlugin function. // // See https://goo.gl/C4t7Tz for more details. type EnablePluginOptions struct { // The Name of the plugin. Name string `qs:"-"` Timeout int64 `qs:"timeout"` Context context.Context } // EnablePlugin enables plugin that opts point or returns an error. // // See https://goo.gl/C4t7Tz for more details. func (c *Client) EnablePlugin(opts EnablePluginOptions) error { path := "/plugins/" + opts.Name + "/enable?" + queryString(opts) resp, err := c.do(http.MethodPost, path, doOptions{context: opts.Context}) if err != nil { return err } resp.Body.Close() return nil } // DisablePluginOptions specify parameters to the DisablePlugin function. // // See https://goo.gl/C4t7Tz for more details. type DisablePluginOptions struct { // The Name of the plugin. Name string `qs:"-"` Context context.Context } // DisablePlugin disables plugin that opts point or returns an error. // // See https://goo.gl/C4t7Tz for more details. func (c *Client) DisablePlugin(opts DisablePluginOptions) error { path := "/plugins/" + opts.Name + "/disable" resp, err := c.do(http.MethodPost, path, doOptions{context: opts.Context}) if err != nil { return err } resp.Body.Close() return nil } // CreatePluginOptions specify parameters to the CreatePlugin function. // // See https://goo.gl/C4t7Tz for more details. type CreatePluginOptions struct { // The Name of the plugin. Name string `qs:"name"` // Path to tar containing plugin Path string `qs:"-"` Context context.Context } // CreatePlugin creates plugin that opts point or returns an error. // // See https://goo.gl/C4t7Tz for more details. func (c *Client) CreatePlugin(opts CreatePluginOptions) (string, error) { path := "/plugins/create?" + queryString(opts) resp, err := c.do(http.MethodPost, path, doOptions{ data: opts.Path, context: opts.Context, }) if err != nil { return "", err } defer resp.Body.Close() containerNameBytes, err := io.ReadAll(resp.Body) if err != nil { return "", err } return string(containerNameBytes), nil } // PushPluginOptions specify parameters to PushPlugin function. // // See https://goo.gl/C4t7Tz for more details. type PushPluginOptions struct { // The Name of the plugin. Name string Context context.Context } // PushPlugin pushes plugin that opts point or returns an error. // // See https://goo.gl/C4t7Tz for more details. func (c *Client) PushPlugin(opts PushPluginOptions) error { path := "/plugins/" + opts.Name + "/push" resp, err := c.do(http.MethodPost, path, doOptions{context: opts.Context}) if err != nil { return err } resp.Body.Close() return nil } // ConfigurePluginOptions specify parameters to the ConfigurePlugin // // See https://goo.gl/C4t7Tz for more details. type ConfigurePluginOptions struct { // The Name of the plugin. Name string `qs:"name"` Envs []string Context context.Context } // ConfigurePlugin configures plugin that opts point or returns an error. // // See https://goo.gl/C4t7Tz for more details. func (c *Client) ConfigurePlugin(opts ConfigurePluginOptions) error { path := "/plugins/" + opts.Name + "/set" resp, err := c.do(http.MethodPost, path, doOptions{ data: opts.Envs, context: opts.Context, }) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return &NoSuchPlugin{ID: opts.Name} } return err } resp.Body.Close() return nil } // NoSuchPlugin is the error returned when a given plugin does not exist. type NoSuchPlugin struct { ID string Err error } func (err *NoSuchPlugin) Error() string { if err.Err != nil { return err.Err.Error() } return "No such plugin: " + err.ID } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/registry_auth.go ================================================ // Copyright 2013 go-dockerclient authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package docker type registryAuth interface { isEmpty() bool headerKey() string } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/signal.go ================================================ // Copyright 2014 go-dockerclient authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package docker // Signal represents a signal that can be send to the container on // KillContainer call. type Signal int // These values represent all signals available on Linux, where containers will // be running. const ( SIGABRT = Signal(0x6) SIGALRM = Signal(0xe) SIGBUS = Signal(0x7) SIGCHLD = Signal(0x11) SIGCLD = Signal(0x11) SIGCONT = Signal(0x12) SIGFPE = Signal(0x8) SIGHUP = Signal(0x1) SIGILL = Signal(0x4) SIGINT = Signal(0x2) SIGIO = Signal(0x1d) SIGIOT = Signal(0x6) SIGKILL = Signal(0x9) SIGPIPE = Signal(0xd) SIGPOLL = Signal(0x1d) SIGPROF = Signal(0x1b) SIGPWR = Signal(0x1e) SIGQUIT = Signal(0x3) SIGSEGV = Signal(0xb) SIGSTKFLT = Signal(0x10) SIGSTOP = Signal(0x13) SIGSYS = Signal(0x1f) SIGTERM = Signal(0xf) SIGTRAP = Signal(0x5) SIGTSTP = Signal(0x14) SIGTTIN = Signal(0x15) SIGTTOU = Signal(0x16) SIGUNUSED = Signal(0x1f) SIGURG = Signal(0x17) SIGUSR1 = Signal(0xa) SIGUSR2 = Signal(0xc) SIGVTALRM = Signal(0x1a) SIGWINCH = Signal(0x1c) SIGXCPU = Signal(0x18) SIGXFSZ = Signal(0x19) ) ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/system.go ================================================ package docker import ( "context" "encoding/json" "net/http" ) // VolumeUsageData represents usage data from the docker system api // More Info Here https://dockr.ly/2PNzQyO type VolumeUsageData struct { // The number of containers referencing this volume. This field // is set to `-1` if the reference-count is not available. // // Required: true RefCount int64 `json:"RefCount"` // Amount of disk space used by the volume (in bytes). This information // is only available for volumes created with the `"local"` volume // driver. For volumes created with other volume drivers, this field // is set to `-1` ("not available") // // Required: true Size int64 `json:"Size"` } // ImageSummary represents data about what images are // currently known to docker // More Info Here https://dockr.ly/2PNzQyO type ImageSummary struct { Containers int64 `json:"Containers"` Created int64 `json:"Created"` ID string `json:"Id"` Labels map[string]string `json:"Labels"` ParentID string `json:"ParentId"` RepoDigests []string `json:"RepoDigests"` RepoTags []string `json:"RepoTags"` SharedSize int64 `json:"SharedSize"` Size int64 `json:"Size"` VirtualSize int64 `json:"VirtualSize"` } // DiskUsage holds information about what docker is using disk space on. // More Info Here https://dockr.ly/2PNzQyO type DiskUsage struct { LayersSize int64 Images []*ImageSummary Containers []*APIContainers Volumes []*Volume } // DiskUsageOptions only contains a context for canceling. type DiskUsageOptions struct { Context context.Context } // DiskUsage returns a *DiskUsage describing what docker is using disk on. // // More Info Here https://dockr.ly/2PNzQyO func (c *Client) DiskUsage(opts DiskUsageOptions) (*DiskUsage, error) { path := "/system/df" resp, err := c.do(http.MethodGet, path, doOptions{context: opts.Context}) if err != nil { return nil, err } defer resp.Body.Close() var du *DiskUsage if err := json.NewDecoder(resp.Body).Decode(&du); err != nil { return nil, err } return du, nil } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/tar.go ================================================ // Copyright 2014 go-dockerclient authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package docker import ( "fmt" "io" "os" "path" "path/filepath" "strings" "github.com/moby/go-archive" "github.com/moby/go-archive/compression" "github.com/moby/patternmatcher" ) func createTarStream(srcPath, dockerfilePath string) (io.ReadCloser, error) { srcPath, err := filepath.Abs(srcPath) if err != nil { return nil, err } excludes, err := parseDockerignore(srcPath) if err != nil { return nil, err } includes := []string{"."} // If .dockerignore mentions .dockerignore or the Dockerfile // then make sure we send both files over to the daemon // because Dockerfile is, obviously, needed no matter what, and // .dockerignore is needed to know if either one needs to be // removed. The deamon will remove them for us, if needed, after it // parses the Dockerfile. // // https://github.com/docker/docker/issues/8330 // forceIncludeFiles := []string{".dockerignore", dockerfilePath} for _, includeFile := range forceIncludeFiles { if includeFile == "" { continue } keepThem, err := patternmatcher.Matches(includeFile, excludes) if err != nil { return nil, fmt.Errorf("cannot match .dockerfileignore: '%s', error: %w", includeFile, err) } if keepThem { includes = append(includes, includeFile) } } if err := validateContextDirectory(srcPath, excludes); err != nil { return nil, err } tarOpts := &archive.TarOptions{ ExcludePatterns: excludes, IncludeFiles: includes, Compression: compression.None, NoLchown: true, } return archive.TarWithOptions(srcPath, tarOpts) } // validateContextDirectory checks if all the contents of the directory // can be read and returns an error if some files can't be read. // Symlinks which point to non-existing files don't trigger an error func validateContextDirectory(srcPath string, excludes []string) error { return filepath.Walk(filepath.Join(srcPath, "."), func(filePath string, f os.FileInfo, err error) error { // skip this directory/file if it's not in the path, it won't get added to the context if relFilePath, relErr := filepath.Rel(srcPath, filePath); relErr != nil { return relErr } else if skip, matchErr := patternmatcher.Matches(relFilePath, excludes); matchErr != nil { return matchErr } else if skip { if f.IsDir() { return filepath.SkipDir } return nil } if err != nil { if os.IsPermission(err) { return fmt.Errorf("cannot stat %q: %w", filePath, err) } if os.IsNotExist(err) { return nil } return err } // skip checking if symlinks point to non-existing files, such symlinks can be useful // also skip named pipes, because they hanging on open if f.Mode()&(os.ModeSymlink|os.ModeNamedPipe) != 0 { return nil } if !f.IsDir() { currentFile, err := os.Open(filePath) if err != nil { return fmt.Errorf("cannot open %q for reading: %w", filePath, err) } currentFile.Close() } return nil }) } func parseDockerignore(root string) ([]string, error) { var excludes []string ignore, err := os.ReadFile(path.Join(root, ".dockerignore")) if err != nil && !os.IsNotExist(err) { return excludes, fmt.Errorf("error reading .dockerignore: %w", err) } excludes = strings.Split(string(ignore), "\n") return excludes, nil } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/tls.go ================================================ // Copyright 2014 go-dockerclient authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // // The content is borrowed from Docker's own source code to provide a simple // tls based dialer package docker import ( "crypto/tls" "errors" "net" "strings" "time" ) type tlsClientCon struct { *tls.Conn rawConn net.Conn } func (c *tlsClientCon) CloseWrite() error { // Go standard tls.Conn doesn't provide the CloseWrite() method so we do it // on its underlying connection. if cwc, ok := c.rawConn.(interface { CloseWrite() error }); ok { return cwc.CloseWrite() } return nil } func tlsDialWithDialer(dialer *net.Dialer, network, addr string, config *tls.Config) (net.Conn, error) { // We want the Timeout and Deadline values from dialer to cover the // whole process: TCP connection and TLS handshake. This means that we // also need to start our own timers now. timeout := dialer.Timeout if !dialer.Deadline.IsZero() { deadlineTimeout := time.Until(dialer.Deadline) if timeout == 0 || deadlineTimeout < timeout { timeout = deadlineTimeout } } var errChannel chan error if timeout != 0 { errChannel = make(chan error, 2) time.AfterFunc(timeout, func() { errChannel <- errors.New("") }) } rawConn, err := dialer.Dial(network, addr) if err != nil { return nil, err } colonPos := strings.LastIndex(addr, ":") if colonPos == -1 { colonPos = len(addr) } hostname := addr[:colonPos] // If no ServerName is set, infer the ServerName // from the hostname we're connecting to. if config.ServerName == "" { // Make a copy to avoid polluting argument or default. config = copyTLSConfig(config) config.ServerName = hostname } conn := tls.Client(rawConn, config) if timeout == 0 { err = conn.Handshake() } else { go func() { errChannel <- conn.Handshake() }() err = <-errChannel } if err != nil { rawConn.Close() return nil, err } // This is Docker difference with standard's crypto/tls package: returned a // wrapper which holds both the TLS and raw connections. return &tlsClientCon{conn, rawConn}, nil } // this exists to silent an error message in go vet func copyTLSConfig(cfg *tls.Config) *tls.Config { return &tls.Config{ Certificates: cfg.Certificates, CipherSuites: cfg.CipherSuites, ClientAuth: cfg.ClientAuth, ClientCAs: cfg.ClientCAs, ClientSessionCache: cfg.ClientSessionCache, CurvePreferences: cfg.CurvePreferences, InsecureSkipVerify: cfg.InsecureSkipVerify, MaxVersion: cfg.MaxVersion, MinVersion: cfg.MinVersion, NextProtos: cfg.NextProtos, Rand: cfg.Rand, RootCAs: cfg.RootCAs, ServerName: cfg.ServerName, SessionTicketsDisabled: cfg.SessionTicketsDisabled, } } ================================================ FILE: vendor/github.com/fsouza/go-dockerclient/volume.go ================================================ // Copyright 2015 go-dockerclient authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package docker import ( "context" "encoding/json" "errors" "net/http" "time" ) var ( // ErrNoSuchVolume is the error returned when the volume does not exist. ErrNoSuchVolume = errors.New("no such volume") // ErrVolumeInUse is the error returned when the volume requested to be removed is still in use. ErrVolumeInUse = errors.New("volume in use and cannot be removed") ) // Volume represents a volume. // // See https://goo.gl/3wgTsd for more details. type Volume struct { Name string `json:"Name" yaml:"Name" toml:"Name"` Driver string `json:"Driver,omitempty" yaml:"Driver,omitempty" toml:"Driver,omitempty"` Mountpoint string `json:"Mountpoint,omitempty" yaml:"Mountpoint,omitempty" toml:"Mountpoint,omitempty"` Labels map[string]string `json:"Labels,omitempty" yaml:"Labels,omitempty" toml:"Labels,omitempty"` Options map[string]string `json:"Options,omitempty" yaml:"Options,omitempty" toml:"Options,omitempty"` CreatedAt time.Time `json:"CreatedAt,omitempty" yaml:"CreatedAt,omitempty" toml:"CreatedAt,omitempty"` } // ListVolumesOptions specify parameters to the ListVolumes function. // // See https://goo.gl/3wgTsd for more details. type ListVolumesOptions struct { Filters map[string][]string Context context.Context } // ListVolumes returns a list of available volumes in the server. // // See https://goo.gl/3wgTsd for more details. func (c *Client) ListVolumes(opts ListVolumesOptions) ([]Volume, error) { resp, err := c.do(http.MethodGet, "/volumes?"+queryString(opts), doOptions{ context: opts.Context, }) if err != nil { return nil, err } defer resp.Body.Close() m := make(map[string]any) if err = json.NewDecoder(resp.Body).Decode(&m); err != nil { return nil, err } var volumes []Volume volumesJSON, ok := m["Volumes"] if !ok { return volumes, nil } data, err := json.Marshal(volumesJSON) if err != nil { return nil, err } if err := json.Unmarshal(data, &volumes); err != nil { return nil, err } return volumes, nil } // CreateVolumeOptions specify parameters to the CreateVolume function. // // See https://goo.gl/qEhmEC for more details. type CreateVolumeOptions struct { Name string Driver string DriverOpts map[string]string Context context.Context `json:"-"` Labels map[string]string } // CreateVolume creates a volume on the server. // // See https://goo.gl/qEhmEC for more details. func (c *Client) CreateVolume(opts CreateVolumeOptions) (*Volume, error) { resp, err := c.do(http.MethodPost, "/volumes/create", doOptions{ data: opts, context: opts.Context, }) if err != nil { return nil, err } defer resp.Body.Close() var volume Volume if err := json.NewDecoder(resp.Body).Decode(&volume); err != nil { return nil, err } return &volume, nil } // InspectVolume returns a volume by its name. // // See https://goo.gl/GMjsMc for more details. func (c *Client) InspectVolume(name string) (*Volume, error) { resp, err := c.do(http.MethodGet, "/volumes/"+name, doOptions{}) if err != nil { var e *Error if errors.As(err, &e) && e.Status == http.StatusNotFound { return nil, ErrNoSuchVolume } return nil, err } defer resp.Body.Close() var volume Volume if err := json.NewDecoder(resp.Body).Decode(&volume); err != nil { return nil, err } return &volume, nil } // RemoveVolume removes a volume by its name. // // Deprecated: Use RemoveVolumeWithOptions instead. func (c *Client) RemoveVolume(name string) error { return c.RemoveVolumeWithOptions(RemoveVolumeOptions{Name: name}) } // RemoveVolumeOptions specify parameters to the RemoveVolumeWithOptions // function. // // See https://goo.gl/nvd6qj for more details. type RemoveVolumeOptions struct { Context context.Context Name string `qs:"-"` Force bool } // RemoveVolumeWithOptions removes a volume by its name and takes extra // parameters. // // See https://goo.gl/nvd6qj for more details. func (c *Client) RemoveVolumeWithOptions(opts RemoveVolumeOptions) error { path := "/volumes/" + opts.Name resp, err := c.do(http.MethodDelete, path+"?"+queryString(opts), doOptions{context: opts.Context}) if err != nil { var e *Error if errors.As(err, &e) { if e.Status == http.StatusNotFound { return ErrNoSuchVolume } if e.Status == http.StatusConflict { return ErrVolumeInUse } } return err } defer resp.Body.Close() return nil } // PruneVolumesOptions specify parameters to the PruneVolumes function. // // See https://goo.gl/f9XDem for more details. type PruneVolumesOptions struct { Filters map[string][]string Context context.Context } // PruneVolumesResults specify results from the PruneVolumes function. // // See https://goo.gl/f9XDem for more details. type PruneVolumesResults struct { VolumesDeleted []string SpaceReclaimed int64 } // PruneVolumes deletes volumes which are unused. // // See https://goo.gl/f9XDem for more details. func (c *Client) PruneVolumes(opts PruneVolumesOptions) (*PruneVolumesResults, error) { path := "/volumes/prune?" + queryString(opts) resp, err := c.do(http.MethodPost, path, doOptions{context: opts.Context}) if err != nil { return nil, err } defer resp.Body.Close() var results PruneVolumesResults if err := json.NewDecoder(resp.Body).Decode(&results); err != nil { return nil, err } return &results, nil } ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/.gitignore ================================================ jose-util/jose-util jose-util.t.err ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/.golangci.yml ================================================ # https://github.com/golangci/golangci-lint run: skip-files: - doc_test.go modules-download-mode: readonly linters: enable-all: true disable: - gochecknoglobals - goconst - lll - maligned - nakedret - scopelint - unparam - funlen # added in 1.18 (requires go-jose changes before it can be enabled) linters-settings: gocyclo: min-complexity: 35 issues: exclude-rules: - text: "don't use ALL_CAPS in Go names" linters: - golint - text: "hardcoded credentials" linters: - gosec - text: "weak cryptographic primitive" linters: - gosec - path: json/ linters: - dupl - errcheck - gocritic - gocyclo - golint - govet - ineffassign - staticcheck - structcheck - stylecheck - unused - path: _test\.go linters: - scopelint - path: jwk.go linters: - gocyclo ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/.travis.yml ================================================ language: go matrix: fast_finish: true allow_failures: - go: tip go: - "1.13.x" - "1.14.x" - tip before_script: - export PATH=$HOME/.local/bin:$PATH before_install: - go get -u github.com/mattn/goveralls github.com/wadey/gocovmerge - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.18.0 - pip install cram --user script: - go test -v -covermode=count -coverprofile=profile.cov . - go test -v -covermode=count -coverprofile=cryptosigner/profile.cov ./cryptosigner - go test -v -covermode=count -coverprofile=cipher/profile.cov ./cipher - go test -v -covermode=count -coverprofile=jwt/profile.cov ./jwt - go test -v ./json # no coverage for forked encoding/json package - golangci-lint run - cd jose-util && go build && PATH=$PWD:$PATH cram -v jose-util.t # cram tests jose-util - cd .. after_success: - gocovmerge *.cov */*.cov > merged.coverprofile - goveralls -coverprofile merged.coverprofile -service=travis-ci ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/CONTRIBUTING.md ================================================ # Contributing If you would like to contribute code to go-jose you can do so through GitHub by forking the repository and sending a pull request. When submitting code, please make every effort to follow existing conventions and style in order to keep the code as readable as possible. Please also make sure all tests pass by running `go test`, and format your code with `go fmt`. We also recommend using `golint` and `errcheck`. ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/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: vendor/github.com/go-jose/go-jose/v4/README.md ================================================ # Go JOSE [![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v4.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v4) [![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v4/jwt.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v4/jwt) [![license](https://img.shields.io/badge/license-apache_2.0-blue.svg?style=flat)](https://raw.githubusercontent.com/go-jose/go-jose/master/LICENSE) Package jose aims to provide an implementation of the Javascript Object Signing and Encryption set of standards. This includes support for JSON Web Encryption, JSON Web Signature, and JSON Web Token standards. ## Overview The implementation follows the [JSON Web Encryption](https://dx.doi.org/10.17487/RFC7516) (RFC 7516), [JSON Web Signature](https://dx.doi.org/10.17487/RFC7515) (RFC 7515), and [JSON Web Token](https://dx.doi.org/10.17487/RFC7519) (RFC 7519) specifications. Tables of supported algorithms are shown below. The library supports both the compact and JWS/JWE JSON Serialization formats, and has optional support for multiple recipients. It also comes with a small command-line utility ([`jose-util`](https://pkg.go.dev/github.com/go-jose/go-jose/jose-util)) for dealing with JOSE messages in a shell. **Note**: We use a forked version of the `encoding/json` package from the Go standard library which uses case-sensitive matching for member names (instead of [case-insensitive matching](https://www.ietf.org/mail-archive/web/json/current/msg03763.html)). This is to avoid differences in interpretation of messages between go-jose and libraries in other languages. ### Versions The forthcoming Version 5 will be released with several breaking API changes, and will require Golang's `encoding/json/v2`, which is currently requires Go 1.25 built with GOEXPERIMENT=jsonv2. Version 4 is the current stable version: import "github.com/go-jose/go-jose/v4" It supports at least the current and previous Golang release. Currently it requires Golang 1.24. Version 3 is only receiving critical security updates. Migration to Version 4 is recommended. Versions 1 and 2 are obsolete, but can be found in the old repository, [square/go-jose](https://github.com/square/go-jose). ### Supported algorithms See below for a table of supported algorithms. Algorithm identifiers match the names in the [JSON Web Algorithms](https://dx.doi.org/10.17487/RFC7518) standard where possible. The Godoc reference has a list of constants. | Key encryption | Algorithm identifier(s) | |:-----------------------|:-----------------------------------------------| | RSA-PKCS#1v1.5 | RSA1_5 | | RSA-OAEP | RSA-OAEP, RSA-OAEP-256 | | AES key wrap | A128KW, A192KW, A256KW | | AES-GCM key wrap | A128GCMKW, A192GCMKW, A256GCMKW | | ECDH-ES + AES key wrap | ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW | | ECDH-ES (direct) | ECDH-ES1 | | Direct encryption | dir1 | 1. Not supported in multi-recipient mode | Signing / MAC | Algorithm identifier(s) | |:------------------|:------------------------| | RSASSA-PKCS#1v1.5 | RS256, RS384, RS512 | | RSASSA-PSS | PS256, PS384, PS512 | | HMAC | HS256, HS384, HS512 | | ECDSA | ES256, ES384, ES512 | | Ed25519 | EdDSA2 | 2. Only available in version 2 of the package | Content encryption | Algorithm identifier(s) | |:-------------------|:--------------------------------------------| | AES-CBC+HMAC | A128CBC-HS256, A192CBC-HS384, A256CBC-HS512 | | AES-GCM | A128GCM, A192GCM, A256GCM | | Compression | Algorithm identifiers(s) | |:-------------------|--------------------------| | DEFLATE (RFC 1951) | DEF | ### Supported key types See below for a table of supported key types. These are understood by the library, and can be passed to corresponding functions such as `NewEncrypter` or `NewSigner`. Each of these keys can also be wrapped in a JWK if desired, which allows attaching a key id. | Algorithm(s) | Corresponding types | |:------------------|--------------------------------------------------------------------------------------------------------------------------------------| | RSA | *[rsa.PublicKey](https://pkg.go.dev/crypto/rsa/#PublicKey), *[rsa.PrivateKey](https://pkg.go.dev/crypto/rsa/#PrivateKey) | | ECDH, ECDSA | *[ecdsa.PublicKey](https://pkg.go.dev/crypto/ecdsa/#PublicKey), *[ecdsa.PrivateKey](https://pkg.go.dev/crypto/ecdsa/#PrivateKey) | | EdDSA1 | [ed25519.PublicKey](https://pkg.go.dev/crypto/ed25519#PublicKey), [ed25519.PrivateKey](https://pkg.go.dev/crypto/ed25519#PrivateKey) | | AES, HMAC | []byte | 1. Only available in version 2 or later of the package ## Examples [![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v4.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v4) [![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v4/jwt.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v4/jwt) Examples can be found in the Godoc reference for this package. The [`jose-util`](https://github.com/go-jose/go-jose/tree/main/jose-util) subdirectory also contains a small command-line utility which might be useful as an example as well. ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/SECURITY.md ================================================ # Security Policy This document explains how to contact the Let's Encrypt security team to report security vulnerabilities. ## Supported Versions | Version | Supported | | ------- | ----------| | >= v3 | ✓ | | v2 | ✗ | | v1 | ✗ | ## Reporting a vulnerability Please see [https://letsencrypt.org/contact/#security](https://letsencrypt.org/contact/#security) for the email address to report a vulnerability. Ensure that the subject line for your report contains the word `vulnerability` and is descriptive. Your email should be acknowledged within 24 hours. If you do not receive a response within 24 hours, please follow-up again with another email. ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/asymmetric.go ================================================ /*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jose import ( "crypto" "crypto/aes" "crypto/ecdsa" "crypto/ed25519" "crypto/rand" "crypto/rsa" "crypto/sha1" "crypto/sha256" "errors" "fmt" "math/big" josecipher "github.com/go-jose/go-jose/v4/cipher" "github.com/go-jose/go-jose/v4/json" ) // A generic RSA-based encrypter/verifier type rsaEncrypterVerifier struct { publicKey *rsa.PublicKey } // A generic RSA-based decrypter/signer type rsaDecrypterSigner struct { privateKey *rsa.PrivateKey } // A generic EC-based encrypter/verifier type ecEncrypterVerifier struct { publicKey *ecdsa.PublicKey } type edEncrypterVerifier struct { publicKey ed25519.PublicKey } // A key generator for ECDH-ES type ecKeyGenerator struct { size int algID string publicKey *ecdsa.PublicKey } // A generic EC-based decrypter/signer type ecDecrypterSigner struct { privateKey *ecdsa.PrivateKey } type edDecrypterSigner struct { privateKey ed25519.PrivateKey } // newRSARecipient creates recipientKeyInfo based on the given key. func newRSARecipient(keyAlg KeyAlgorithm, publicKey *rsa.PublicKey) (recipientKeyInfo, error) { // Verify that key management algorithm is supported by this encrypter switch keyAlg { case RSA1_5, RSA_OAEP, RSA_OAEP_256: default: return recipientKeyInfo{}, ErrUnsupportedAlgorithm } if publicKey == nil { return recipientKeyInfo{}, errors.New("invalid public key") } return recipientKeyInfo{ keyAlg: keyAlg, keyEncrypter: &rsaEncrypterVerifier{ publicKey: publicKey, }, }, nil } // newRSASigner creates a recipientSigInfo based on the given key. func newRSASigner(sigAlg SignatureAlgorithm, privateKey *rsa.PrivateKey) (recipientSigInfo, error) { // Verify that key management algorithm is supported by this encrypter switch sigAlg { case RS256, RS384, RS512, PS256, PS384, PS512: default: return recipientSigInfo{}, ErrUnsupportedAlgorithm } if privateKey == nil { return recipientSigInfo{}, errors.New("invalid private key") } return recipientSigInfo{ sigAlg: sigAlg, publicKey: staticPublicKey(&JSONWebKey{ Key: privateKey.Public(), }), signer: &rsaDecrypterSigner{ privateKey: privateKey, }, }, nil } func newEd25519Signer(sigAlg SignatureAlgorithm, privateKey ed25519.PrivateKey) (recipientSigInfo, error) { if sigAlg != EdDSA { return recipientSigInfo{}, ErrUnsupportedAlgorithm } if privateKey == nil { return recipientSigInfo{}, errors.New("invalid private key") } return recipientSigInfo{ sigAlg: sigAlg, publicKey: staticPublicKey(&JSONWebKey{ Key: privateKey.Public(), }), signer: &edDecrypterSigner{ privateKey: privateKey, }, }, nil } // newECDHRecipient creates recipientKeyInfo based on the given key. func newECDHRecipient(keyAlg KeyAlgorithm, publicKey *ecdsa.PublicKey) (recipientKeyInfo, error) { // Verify that key management algorithm is supported by this encrypter switch keyAlg { case ECDH_ES, ECDH_ES_A128KW, ECDH_ES_A192KW, ECDH_ES_A256KW: default: return recipientKeyInfo{}, ErrUnsupportedAlgorithm } if publicKey == nil || !publicKey.Curve.IsOnCurve(publicKey.X, publicKey.Y) { return recipientKeyInfo{}, errors.New("invalid public key") } return recipientKeyInfo{ keyAlg: keyAlg, keyEncrypter: &ecEncrypterVerifier{ publicKey: publicKey, }, }, nil } // newECDSASigner creates a recipientSigInfo based on the given key. func newECDSASigner(sigAlg SignatureAlgorithm, privateKey *ecdsa.PrivateKey) (recipientSigInfo, error) { // Verify that key management algorithm is supported by this encrypter switch sigAlg { case ES256, ES384, ES512: default: return recipientSigInfo{}, ErrUnsupportedAlgorithm } if privateKey == nil { return recipientSigInfo{}, errors.New("invalid private key") } return recipientSigInfo{ sigAlg: sigAlg, publicKey: staticPublicKey(&JSONWebKey{ Key: privateKey.Public(), }), signer: &ecDecrypterSigner{ privateKey: privateKey, }, }, nil } // Encrypt the given payload and update the object. func (ctx rsaEncrypterVerifier) encryptKey(cek []byte, alg KeyAlgorithm) (recipientInfo, error) { encryptedKey, err := ctx.encrypt(cek, alg) if err != nil { return recipientInfo{}, err } return recipientInfo{ encryptedKey: encryptedKey, header: &rawHeader{}, }, nil } // Encrypt the given payload. Based on the key encryption algorithm, // this will either use RSA-PKCS1v1.5 or RSA-OAEP (with SHA-1 or SHA-256). func (ctx rsaEncrypterVerifier) encrypt(cek []byte, alg KeyAlgorithm) ([]byte, error) { switch alg { case RSA1_5: return rsa.EncryptPKCS1v15(RandReader, ctx.publicKey, cek) case RSA_OAEP: return rsa.EncryptOAEP(sha1.New(), RandReader, ctx.publicKey, cek, []byte{}) case RSA_OAEP_256: return rsa.EncryptOAEP(sha256.New(), RandReader, ctx.publicKey, cek, []byte{}) } return nil, ErrUnsupportedAlgorithm } // Decrypt the given payload and return the content encryption key. func (ctx rsaDecrypterSigner) decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) { return ctx.decrypt(recipient.encryptedKey, headers.getAlgorithm(), generator) } // Decrypt the given payload. Based on the key encryption algorithm, // this will either use RSA-PKCS1v1.5 or RSA-OAEP (with SHA-1 or SHA-256). func (ctx rsaDecrypterSigner) decrypt(jek []byte, alg KeyAlgorithm, generator keyGenerator) ([]byte, error) { // Note: The random reader on decrypt operations is only used for blinding, // so stubbing is meanlingless (hence the direct use of rand.Reader). switch alg { case RSA1_5: defer func() { // DecryptPKCS1v15SessionKey sometimes panics on an invalid payload // because of an index out of bounds error, which we want to ignore. // This has been fixed in Go 1.3.1 (released 2014/08/13), the recover() // only exists for preventing crashes with unpatched versions. // See: https://groups.google.com/forum/#!topic/golang-dev/7ihX6Y6kx9k // See: https://code.google.com/p/go/source/detail?r=58ee390ff31602edb66af41ed10901ec95904d33 _ = recover() }() // Perform some input validation. keyBytes := ctx.privateKey.PublicKey.N.BitLen() / 8 if keyBytes != len(jek) { // Input size is incorrect, the encrypted payload should always match // the size of the public modulus (e.g. using a 2048 bit key will // produce 256 bytes of output). Reject this since it's invalid input. return nil, ErrCryptoFailure } cek, _, err := generator.genKey() if err != nil { return nil, ErrCryptoFailure } // When decrypting an RSA-PKCS1v1.5 payload, we must take precautions to // prevent chosen-ciphertext attacks as described in RFC 3218, "Preventing // the Million Message Attack on Cryptographic Message Syntax". We are // therefore deliberately ignoring errors here. _ = rsa.DecryptPKCS1v15SessionKey(rand.Reader, ctx.privateKey, jek, cek) return cek, nil case RSA_OAEP: // Use rand.Reader for RSA blinding return rsa.DecryptOAEP(sha1.New(), rand.Reader, ctx.privateKey, jek, []byte{}) case RSA_OAEP_256: // Use rand.Reader for RSA blinding return rsa.DecryptOAEP(sha256.New(), rand.Reader, ctx.privateKey, jek, []byte{}) } return nil, ErrUnsupportedAlgorithm } // Sign the given payload func (ctx rsaDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error) { var hash crypto.Hash switch alg { case RS256, PS256: hash = crypto.SHA256 case RS384, PS384: hash = crypto.SHA384 case RS512, PS512: hash = crypto.SHA512 default: return Signature{}, ErrUnsupportedAlgorithm } hasher := hash.New() // According to documentation, Write() on hash never fails _, _ = hasher.Write(payload) hashed := hasher.Sum(nil) var out []byte var err error switch alg { case RS256, RS384, RS512: // TODO(https://github.com/go-jose/go-jose/issues/40): As of go1.20, the // random parameter is legacy and ignored, and it can be nil. // https://cs.opensource.google/go/go/+/refs/tags/go1.20:src/crypto/rsa/pkcs1v15.go;l=263;bpv=0;bpt=1 out, err = rsa.SignPKCS1v15(RandReader, ctx.privateKey, hash, hashed) case PS256, PS384, PS512: out, err = rsa.SignPSS(RandReader, ctx.privateKey, hash, hashed, &rsa.PSSOptions{ SaltLength: rsa.PSSSaltLengthEqualsHash, }) } if err != nil { return Signature{}, err } return Signature{ Signature: out, protected: &rawHeader{}, }, nil } // Verify the given payload func (ctx rsaEncrypterVerifier) verifyPayload(payload []byte, signature []byte, alg SignatureAlgorithm) error { var hash crypto.Hash switch alg { case RS256, PS256: hash = crypto.SHA256 case RS384, PS384: hash = crypto.SHA384 case RS512, PS512: hash = crypto.SHA512 default: return ErrUnsupportedAlgorithm } hasher := hash.New() // According to documentation, Write() on hash never fails _, _ = hasher.Write(payload) hashed := hasher.Sum(nil) switch alg { case RS256, RS384, RS512: return rsa.VerifyPKCS1v15(ctx.publicKey, hash, hashed, signature) case PS256, PS384, PS512: return rsa.VerifyPSS(ctx.publicKey, hash, hashed, signature, nil) } return ErrUnsupportedAlgorithm } // Encrypt the given payload and update the object. func (ctx ecEncrypterVerifier) encryptKey(cek []byte, alg KeyAlgorithm) (recipientInfo, error) { switch alg { case ECDH_ES: // ECDH-ES mode doesn't wrap a key, the shared secret is used directly as the key. return recipientInfo{ header: &rawHeader{}, }, nil case ECDH_ES_A128KW, ECDH_ES_A192KW, ECDH_ES_A256KW: default: return recipientInfo{}, ErrUnsupportedAlgorithm } generator := ecKeyGenerator{ algID: string(alg), publicKey: ctx.publicKey, } switch alg { case ECDH_ES_A128KW: generator.size = 16 case ECDH_ES_A192KW: generator.size = 24 case ECDH_ES_A256KW: generator.size = 32 } kek, header, err := generator.genKey() if err != nil { return recipientInfo{}, err } block, err := aes.NewCipher(kek) if err != nil { return recipientInfo{}, err } jek, err := josecipher.KeyWrap(block, cek) if err != nil { return recipientInfo{}, err } return recipientInfo{ encryptedKey: jek, header: &header, }, nil } // Get key size for EC key generator func (ctx ecKeyGenerator) keySize() int { return ctx.size } // Get a content encryption key for ECDH-ES func (ctx ecKeyGenerator) genKey() ([]byte, rawHeader, error) { priv, err := ecdsa.GenerateKey(ctx.publicKey.Curve, RandReader) if err != nil { return nil, rawHeader{}, err } out := josecipher.DeriveECDHES(ctx.algID, []byte{}, []byte{}, priv, ctx.publicKey, ctx.size) b, err := json.Marshal(&JSONWebKey{ Key: &priv.PublicKey, }) if err != nil { return nil, nil, err } headers := rawHeader{ headerEPK: makeRawMessage(b), } return out, headers, nil } // Decrypt the given payload and return the content encryption key. func (ctx ecDecrypterSigner) decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) { epk, err := headers.getEPK() if err != nil { return nil, errors.New("go-jose/go-jose: invalid epk header") } if epk == nil { return nil, errors.New("go-jose/go-jose: missing epk header") } publicKey, ok := epk.Key.(*ecdsa.PublicKey) if publicKey == nil || !ok { return nil, errors.New("go-jose/go-jose: invalid epk header") } if !ctx.privateKey.Curve.IsOnCurve(publicKey.X, publicKey.Y) { return nil, errors.New("go-jose/go-jose: invalid public key in epk header") } apuData, err := headers.getAPU() if err != nil { return nil, errors.New("go-jose/go-jose: invalid apu header") } apvData, err := headers.getAPV() if err != nil { return nil, errors.New("go-jose/go-jose: invalid apv header") } deriveKey := func(algID string, size int) []byte { return josecipher.DeriveECDHES(algID, apuData.bytes(), apvData.bytes(), ctx.privateKey, publicKey, size) } var keySize int algorithm := headers.getAlgorithm() switch algorithm { case ECDH_ES: // ECDH-ES uses direct key agreement, no key unwrapping necessary. return deriveKey(string(headers.getEncryption()), generator.keySize()), nil case ECDH_ES_A128KW: keySize = 16 case ECDH_ES_A192KW: keySize = 24 case ECDH_ES_A256KW: keySize = 32 default: return nil, ErrUnsupportedAlgorithm } key := deriveKey(string(algorithm), keySize) block, err := aes.NewCipher(key) if err != nil { return nil, err } return josecipher.KeyUnwrap(block, recipient.encryptedKey) } func (ctx edDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error) { if alg != EdDSA { return Signature{}, ErrUnsupportedAlgorithm } sig, err := ctx.privateKey.Sign(RandReader, payload, crypto.Hash(0)) if err != nil { return Signature{}, err } return Signature{ Signature: sig, protected: &rawHeader{}, }, nil } func (ctx edEncrypterVerifier) verifyPayload(payload []byte, signature []byte, alg SignatureAlgorithm) error { if alg != EdDSA { return ErrUnsupportedAlgorithm } ok := ed25519.Verify(ctx.publicKey, payload, signature) if !ok { return errors.New("go-jose/go-jose: ed25519 signature failed to verify") } return nil } // Sign the given payload func (ctx ecDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error) { var expectedBitSize int var hash crypto.Hash switch alg { case ES256: expectedBitSize = 256 hash = crypto.SHA256 case ES384: expectedBitSize = 384 hash = crypto.SHA384 case ES512: expectedBitSize = 521 hash = crypto.SHA512 } curveBits := ctx.privateKey.Curve.Params().BitSize if expectedBitSize != curveBits { return Signature{}, fmt.Errorf("go-jose/go-jose: expected %d bit key, got %d bits instead", expectedBitSize, curveBits) } hasher := hash.New() // According to documentation, Write() on hash never fails _, _ = hasher.Write(payload) hashed := hasher.Sum(nil) r, s, err := ecdsa.Sign(RandReader, ctx.privateKey, hashed) if err != nil { return Signature{}, err } keyBytes := curveBits / 8 if curveBits%8 > 0 { keyBytes++ } // We serialize the outputs (r and s) into big-endian byte arrays and pad // them with zeros on the left to make sure the sizes work out. Both arrays // must be keyBytes long, and the output must be 2*keyBytes long. rBytes := r.Bytes() rBytesPadded := make([]byte, keyBytes) copy(rBytesPadded[keyBytes-len(rBytes):], rBytes) sBytes := s.Bytes() sBytesPadded := make([]byte, keyBytes) copy(sBytesPadded[keyBytes-len(sBytes):], sBytes) out := append(rBytesPadded, sBytesPadded...) return Signature{ Signature: out, protected: &rawHeader{}, }, nil } // Verify the given payload func (ctx ecEncrypterVerifier) verifyPayload(payload []byte, signature []byte, alg SignatureAlgorithm) error { var keySize int var hash crypto.Hash switch alg { case ES256: keySize = 32 hash = crypto.SHA256 case ES384: keySize = 48 hash = crypto.SHA384 case ES512: keySize = 66 hash = crypto.SHA512 default: return ErrUnsupportedAlgorithm } if len(signature) != 2*keySize { return fmt.Errorf("go-jose/go-jose: invalid signature size, have %d bytes, wanted %d", len(signature), 2*keySize) } hasher := hash.New() // According to documentation, Write() on hash never fails _, _ = hasher.Write(payload) hashed := hasher.Sum(nil) r := big.NewInt(0).SetBytes(signature[:keySize]) s := big.NewInt(0).SetBytes(signature[keySize:]) match := ecdsa.Verify(ctx.publicKey, hashed, r, s) if !match { return errors.New("go-jose/go-jose: ecdsa signature failed to verify") } return nil } ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/cipher/cbc_hmac.go ================================================ /*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package josecipher import ( "bytes" "crypto/cipher" "crypto/hmac" "crypto/sha256" "crypto/sha512" "crypto/subtle" "encoding/binary" "errors" "hash" ) const ( nonceBytes = 16 ) // NewCBCHMAC instantiates a new AEAD based on CBC+HMAC. func NewCBCHMAC(key []byte, newBlockCipher func([]byte) (cipher.Block, error)) (cipher.AEAD, error) { keySize := len(key) / 2 integrityKey := key[:keySize] encryptionKey := key[keySize:] blockCipher, err := newBlockCipher(encryptionKey) if err != nil { return nil, err } var hash func() hash.Hash switch keySize { case 16: hash = sha256.New case 24: hash = sha512.New384 case 32: hash = sha512.New } return &cbcAEAD{ hash: hash, blockCipher: blockCipher, authtagBytes: keySize, integrityKey: integrityKey, }, nil } // An AEAD based on CBC+HMAC type cbcAEAD struct { hash func() hash.Hash authtagBytes int integrityKey []byte blockCipher cipher.Block } func (ctx *cbcAEAD) NonceSize() int { return nonceBytes } func (ctx *cbcAEAD) Overhead() int { // Maximum overhead is block size (for padding) plus auth tag length, where // the length of the auth tag is equivalent to the key size. return ctx.blockCipher.BlockSize() + ctx.authtagBytes } // Seal encrypts and authenticates the plaintext. func (ctx *cbcAEAD) Seal(dst, nonce, plaintext, data []byte) []byte { // Output buffer -- must take care not to mangle plaintext input. ciphertext := make([]byte, uint64(len(plaintext))+uint64(ctx.Overhead()))[:len(plaintext)] copy(ciphertext, plaintext) ciphertext = padBuffer(ciphertext, ctx.blockCipher.BlockSize()) cbc := cipher.NewCBCEncrypter(ctx.blockCipher, nonce) cbc.CryptBlocks(ciphertext, ciphertext) authtag := ctx.computeAuthTag(data, nonce, ciphertext) ret, out := resize(dst, uint64(len(dst))+uint64(len(ciphertext))+uint64(len(authtag))) copy(out, ciphertext) copy(out[len(ciphertext):], authtag) return ret } // Open decrypts and authenticates the ciphertext. func (ctx *cbcAEAD) Open(dst, nonce, ciphertext, data []byte) ([]byte, error) { if len(ciphertext) < ctx.authtagBytes { return nil, errors.New("go-jose/go-jose: invalid ciphertext (too short)") } offset := len(ciphertext) - ctx.authtagBytes expectedTag := ctx.computeAuthTag(data, nonce, ciphertext[:offset]) match := subtle.ConstantTimeCompare(expectedTag, ciphertext[offset:]) if match != 1 { return nil, errors.New("go-jose/go-jose: invalid ciphertext (auth tag mismatch)") } cbc := cipher.NewCBCDecrypter(ctx.blockCipher, nonce) // Make copy of ciphertext buffer, don't want to modify in place buffer := append([]byte{}, ciphertext[:offset]...) if len(buffer)%ctx.blockCipher.BlockSize() > 0 { return nil, errors.New("go-jose/go-jose: invalid ciphertext (invalid length)") } cbc.CryptBlocks(buffer, buffer) // Remove padding plaintext, err := unpadBuffer(buffer, ctx.blockCipher.BlockSize()) if err != nil { return nil, err } ret, out := resize(dst, uint64(len(dst))+uint64(len(plaintext))) copy(out, plaintext) return ret, nil } // Compute an authentication tag func (ctx *cbcAEAD) computeAuthTag(aad, nonce, ciphertext []byte) []byte { buffer := make([]byte, uint64(len(aad))+uint64(len(nonce))+uint64(len(ciphertext))+8) n := 0 n += copy(buffer, aad) n += copy(buffer[n:], nonce) n += copy(buffer[n:], ciphertext) binary.BigEndian.PutUint64(buffer[n:], uint64(len(aad))*8) // According to documentation, Write() on hash.Hash never fails. hmac := hmac.New(ctx.hash, ctx.integrityKey) _, _ = hmac.Write(buffer) return hmac.Sum(nil)[:ctx.authtagBytes] } // resize ensures that the given slice has a capacity of at least n bytes. // If the capacity of the slice is less than n, a new slice is allocated // and the existing data will be copied. func resize(in []byte, n uint64) (head, tail []byte) { if uint64(cap(in)) >= n { head = in[:n] } else { head = make([]byte, n) copy(head, in) } tail = head[len(in):] return } // Apply padding func padBuffer(buffer []byte, blockSize int) []byte { missing := blockSize - (len(buffer) % blockSize) ret, out := resize(buffer, uint64(len(buffer))+uint64(missing)) padding := bytes.Repeat([]byte{byte(missing)}, missing) copy(out, padding) return ret } // Remove padding func unpadBuffer(buffer []byte, blockSize int) ([]byte, error) { if len(buffer)%blockSize != 0 { return nil, errors.New("go-jose/go-jose: invalid padding") } last := buffer[len(buffer)-1] count := int(last) if count == 0 || count > blockSize || count > len(buffer) { return nil, errors.New("go-jose/go-jose: invalid padding") } padding := bytes.Repeat([]byte{last}, count) if !bytes.HasSuffix(buffer, padding) { return nil, errors.New("go-jose/go-jose: invalid padding") } return buffer[:len(buffer)-count], nil } ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/cipher/concat_kdf.go ================================================ /*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package josecipher import ( "crypto" "encoding/binary" "hash" "io" ) type concatKDF struct { z, info []byte i uint32 cache []byte hasher hash.Hash } // NewConcatKDF builds a KDF reader based on the given inputs. func NewConcatKDF(hash crypto.Hash, z, algID, ptyUInfo, ptyVInfo, supPubInfo, supPrivInfo []byte) io.Reader { buffer := make([]byte, uint64(len(algID))+uint64(len(ptyUInfo))+uint64(len(ptyVInfo))+uint64(len(supPubInfo))+uint64(len(supPrivInfo))) n := 0 n += copy(buffer, algID) n += copy(buffer[n:], ptyUInfo) n += copy(buffer[n:], ptyVInfo) n += copy(buffer[n:], supPubInfo) copy(buffer[n:], supPrivInfo) hasher := hash.New() return &concatKDF{ z: z, info: buffer, hasher: hasher, cache: []byte{}, i: 1, } } func (ctx *concatKDF) Read(out []byte) (int, error) { copied := copy(out, ctx.cache) ctx.cache = ctx.cache[copied:] for copied < len(out) { ctx.hasher.Reset() // Write on a hash.Hash never fails _ = binary.Write(ctx.hasher, binary.BigEndian, ctx.i) _, _ = ctx.hasher.Write(ctx.z) _, _ = ctx.hasher.Write(ctx.info) hash := ctx.hasher.Sum(nil) chunkCopied := copy(out[copied:], hash) copied += chunkCopied ctx.cache = hash[chunkCopied:] ctx.i++ } return copied, nil } ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/cipher/ecdh_es.go ================================================ /*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package josecipher import ( "bytes" "crypto" "crypto/ecdsa" "crypto/elliptic" "encoding/binary" ) // DeriveECDHES derives a shared encryption key using ECDH/ConcatKDF as described in JWE/JWA. // It is an error to call this function with a private/public key that are not on the same // curve. Callers must ensure that the keys are valid before calling this function. Output // size may be at most 1<<16 bytes (64 KiB). func DeriveECDHES(alg string, apuData, apvData []byte, priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, size int) []byte { if size > 1<<16 { panic("ECDH-ES output size too large, must be less than or equal to 1<<16") } // algId, partyUInfo, partyVInfo inputs must be prefixed with the length algID := lengthPrefixed([]byte(alg)) ptyUInfo := lengthPrefixed(apuData) ptyVInfo := lengthPrefixed(apvData) // suppPubInfo is the encoded length of the output size in bits supPubInfo := make([]byte, 4) binary.BigEndian.PutUint32(supPubInfo, uint32(size)*8) if !priv.PublicKey.Curve.IsOnCurve(pub.X, pub.Y) { panic("public key not on same curve as private key") } z, _ := priv.Curve.ScalarMult(pub.X, pub.Y, priv.D.Bytes()) zBytes := z.Bytes() // Note that calling z.Bytes() on a big.Int may strip leading zero bytes from // the returned byte array. This can lead to a problem where zBytes will be // shorter than expected which breaks the key derivation. Therefore we must pad // to the full length of the expected coordinate here before calling the KDF. octSize := dSize(priv.Curve) if len(zBytes) != octSize { zBytes = append(bytes.Repeat([]byte{0}, octSize-len(zBytes)), zBytes...) } reader := NewConcatKDF(crypto.SHA256, zBytes, algID, ptyUInfo, ptyVInfo, supPubInfo, []byte{}) key := make([]byte, size) // Read on the KDF will never fail _, _ = reader.Read(key) return key } // dSize returns the size in octets for a coordinate on a elliptic curve. func dSize(curve elliptic.Curve) int { order := curve.Params().P bitLen := order.BitLen() size := bitLen / 8 if bitLen%8 != 0 { size++ } return size } func lengthPrefixed(data []byte) []byte { out := make([]byte, len(data)+4) binary.BigEndian.PutUint32(out, uint32(len(data))) copy(out[4:], data) return out } ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/cipher/key_wrap.go ================================================ /*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package josecipher import ( "crypto/cipher" "crypto/subtle" "encoding/binary" "errors" ) var defaultIV = []byte{0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6} // KeyWrap implements NIST key wrapping; it wraps a content encryption key (cek) with the given block cipher. func KeyWrap(block cipher.Block, cek []byte) ([]byte, error) { if len(cek)%8 != 0 { return nil, errors.New("go-jose/go-jose: key wrap input must be 8 byte blocks") } n := len(cek) / 8 r := make([][]byte, n) for i := range r { r[i] = make([]byte, 8) copy(r[i], cek[i*8:]) } buffer := make([]byte, 16) tBytes := make([]byte, 8) copy(buffer, defaultIV) for t := 0; t < 6*n; t++ { copy(buffer[8:], r[t%n]) block.Encrypt(buffer, buffer) binary.BigEndian.PutUint64(tBytes, uint64(t+1)) for i := 0; i < 8; i++ { buffer[i] ^= tBytes[i] } copy(r[t%n], buffer[8:]) } out := make([]byte, (n+1)*8) copy(out, buffer[:8]) for i := range r { copy(out[(i+1)*8:], r[i]) } return out, nil } // KeyUnwrap implements NIST key unwrapping; it unwraps a content encryption key (cek) with the given block cipher. func KeyUnwrap(block cipher.Block, ciphertext []byte) ([]byte, error) { if len(ciphertext)%8 != 0 { return nil, errors.New("go-jose/go-jose: key wrap input must be 8 byte blocks") } n := (len(ciphertext) / 8) - 1 r := make([][]byte, n) for i := range r { r[i] = make([]byte, 8) copy(r[i], ciphertext[(i+1)*8:]) } buffer := make([]byte, 16) tBytes := make([]byte, 8) copy(buffer[:8], ciphertext[:8]) for t := 6*n - 1; t >= 0; t-- { binary.BigEndian.PutUint64(tBytes, uint64(t+1)) for i := 0; i < 8; i++ { buffer[i] ^= tBytes[i] } copy(buffer[8:], r[t%n]) block.Decrypt(buffer, buffer) copy(r[t%n], buffer[8:]) } if subtle.ConstantTimeCompare(buffer[:8], defaultIV) == 0 { return nil, errors.New("go-jose/go-jose: failed to unwrap key") } out := make([]byte, n*8) for i := range r { copy(out[i*8:], r[i]) } return out, nil } ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/crypter.go ================================================ /*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jose import ( "crypto/ecdsa" "crypto/rsa" "errors" "fmt" "github.com/go-jose/go-jose/v4/json" ) // Encrypter represents an encrypter which produces an encrypted JWE object. type Encrypter interface { Encrypt(plaintext []byte) (*JSONWebEncryption, error) EncryptWithAuthData(plaintext []byte, aad []byte) (*JSONWebEncryption, error) Options() EncrypterOptions } // A generic content cipher type contentCipher interface { keySize() int encrypt(cek []byte, aad, plaintext []byte) (*aeadParts, error) decrypt(cek []byte, aad []byte, parts *aeadParts) ([]byte, error) } // A key generator (for generating/getting a CEK) type keyGenerator interface { keySize() int genKey() ([]byte, rawHeader, error) } // A generic key encrypter type keyEncrypter interface { encryptKey(cek []byte, alg KeyAlgorithm) (recipientInfo, error) // Encrypt a key } // A generic key decrypter type keyDecrypter interface { decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) // Decrypt a key } // A generic encrypter based on the given key encrypter and content cipher. type genericEncrypter struct { contentAlg ContentEncryption compressionAlg CompressionAlgorithm cipher contentCipher recipients []recipientKeyInfo keyGenerator keyGenerator extraHeaders map[HeaderKey]interface{} } type recipientKeyInfo struct { keyID string keyAlg KeyAlgorithm keyEncrypter keyEncrypter } // EncrypterOptions represents options that can be set on new encrypters. type EncrypterOptions struct { Compression CompressionAlgorithm // Optional map of name/value pairs to be inserted into the protected // header of a JWS object. Some specifications which make use of // JWS require additional values here. // // Values will be serialized by [json.Marshal] and must be valid inputs to // that function. // // [json.Marshal]: https://pkg.go.dev/encoding/json#Marshal ExtraHeaders map[HeaderKey]interface{} } // WithHeader adds an arbitrary value to the ExtraHeaders map, initializing it // if necessary, and returns the updated EncrypterOptions. // // The v parameter will be serialized by [json.Marshal] and must be a valid // input to that function. // // [json.Marshal]: https://pkg.go.dev/encoding/json#Marshal func (eo *EncrypterOptions) WithHeader(k HeaderKey, v interface{}) *EncrypterOptions { if eo.ExtraHeaders == nil { eo.ExtraHeaders = map[HeaderKey]interface{}{} } eo.ExtraHeaders[k] = v return eo } // WithContentType adds a content type ("cty") header and returns the updated // EncrypterOptions. func (eo *EncrypterOptions) WithContentType(contentType ContentType) *EncrypterOptions { return eo.WithHeader(HeaderContentType, contentType) } // WithType adds a type ("typ") header and returns the updated EncrypterOptions. func (eo *EncrypterOptions) WithType(typ ContentType) *EncrypterOptions { return eo.WithHeader(HeaderType, typ) } // Recipient represents an algorithm/key to encrypt messages to. // // PBES2Count and PBES2Salt correspond with the "p2c" and "p2s" headers used // on the password-based encryption algorithms PBES2-HS256+A128KW, // PBES2-HS384+A192KW, and PBES2-HS512+A256KW. If they are not provided a safe // default of 100000 will be used for the count and a 128-bit random salt will // be generated. type Recipient struct { Algorithm KeyAlgorithm // Key must have one of these types: // - ed25519.PublicKey // - *ecdsa.PublicKey // - *rsa.PublicKey // - *JSONWebKey // - JSONWebKey // - []byte (a symmetric key) // - Any type that satisfies the OpaqueKeyEncrypter interface // // The type of Key must match the value of Algorithm. Key interface{} KeyID string PBES2Count int PBES2Salt []byte } // NewEncrypter creates an appropriate encrypter based on the key type func NewEncrypter(enc ContentEncryption, rcpt Recipient, opts *EncrypterOptions) (Encrypter, error) { encrypter := &genericEncrypter{ contentAlg: enc, recipients: []recipientKeyInfo{}, cipher: getContentCipher(enc), } if opts != nil { encrypter.compressionAlg = opts.Compression encrypter.extraHeaders = opts.ExtraHeaders } if encrypter.cipher == nil { return nil, ErrUnsupportedAlgorithm } var keyID string var rawKey interface{} switch encryptionKey := rcpt.Key.(type) { case JSONWebKey: keyID, rawKey = encryptionKey.KeyID, encryptionKey.Key case *JSONWebKey: keyID, rawKey = encryptionKey.KeyID, encryptionKey.Key case OpaqueKeyEncrypter: keyID, rawKey = encryptionKey.KeyID(), encryptionKey default: rawKey = encryptionKey } switch rcpt.Algorithm { case DIRECT: // Direct encryption mode must be treated differently keyBytes, ok := rawKey.([]byte) if !ok { return nil, ErrUnsupportedKeyType } if encrypter.cipher.keySize() != len(keyBytes) { return nil, ErrInvalidKeySize } encrypter.keyGenerator = staticKeyGenerator{ key: keyBytes, } recipientInfo, _ := newSymmetricRecipient(rcpt.Algorithm, keyBytes) recipientInfo.keyID = keyID if rcpt.KeyID != "" { recipientInfo.keyID = rcpt.KeyID } encrypter.recipients = []recipientKeyInfo{recipientInfo} return encrypter, nil case ECDH_ES: // ECDH-ES (w/o key wrapping) is similar to DIRECT mode keyDSA, ok := rawKey.(*ecdsa.PublicKey) if !ok { return nil, ErrUnsupportedKeyType } encrypter.keyGenerator = ecKeyGenerator{ size: encrypter.cipher.keySize(), algID: string(enc), publicKey: keyDSA, } recipientInfo, _ := newECDHRecipient(rcpt.Algorithm, keyDSA) recipientInfo.keyID = keyID if rcpt.KeyID != "" { recipientInfo.keyID = rcpt.KeyID } encrypter.recipients = []recipientKeyInfo{recipientInfo} return encrypter, nil default: // Can just add a standard recipient encrypter.keyGenerator = randomKeyGenerator{ size: encrypter.cipher.keySize(), } err := encrypter.addRecipient(rcpt) return encrypter, err } } // NewMultiEncrypter creates a multi-encrypter based on the given parameters func NewMultiEncrypter(enc ContentEncryption, rcpts []Recipient, opts *EncrypterOptions) (Encrypter, error) { cipher := getContentCipher(enc) if cipher == nil { return nil, ErrUnsupportedAlgorithm } if len(rcpts) == 0 { return nil, fmt.Errorf("go-jose/go-jose: recipients is nil or empty") } encrypter := &genericEncrypter{ contentAlg: enc, recipients: []recipientKeyInfo{}, cipher: cipher, keyGenerator: randomKeyGenerator{ size: cipher.keySize(), }, } if opts != nil { encrypter.compressionAlg = opts.Compression encrypter.extraHeaders = opts.ExtraHeaders } for _, recipient := range rcpts { err := encrypter.addRecipient(recipient) if err != nil { return nil, err } } return encrypter, nil } func (ctx *genericEncrypter) addRecipient(recipient Recipient) (err error) { var recipientInfo recipientKeyInfo switch recipient.Algorithm { case DIRECT, ECDH_ES: return fmt.Errorf("go-jose/go-jose: key algorithm '%s' not supported in multi-recipient mode", recipient.Algorithm) } recipientInfo, err = makeJWERecipient(recipient.Algorithm, recipient.Key) if recipient.KeyID != "" { recipientInfo.keyID = recipient.KeyID } switch recipient.Algorithm { case PBES2_HS256_A128KW, PBES2_HS384_A192KW, PBES2_HS512_A256KW: if sr, ok := recipientInfo.keyEncrypter.(*symmetricKeyCipher); ok { sr.p2c = recipient.PBES2Count sr.p2s = recipient.PBES2Salt } } if err == nil { ctx.recipients = append(ctx.recipients, recipientInfo) } return err } func makeJWERecipient(alg KeyAlgorithm, encryptionKey interface{}) (recipientKeyInfo, error) { switch encryptionKey := encryptionKey.(type) { case *rsa.PublicKey: return newRSARecipient(alg, encryptionKey) case *ecdsa.PublicKey: return newECDHRecipient(alg, encryptionKey) case []byte: return newSymmetricRecipient(alg, encryptionKey) case string: return newSymmetricRecipient(alg, []byte(encryptionKey)) case JSONWebKey: recipient, err := makeJWERecipient(alg, encryptionKey.Key) recipient.keyID = encryptionKey.KeyID return recipient, err case *JSONWebKey: recipient, err := makeJWERecipient(alg, encryptionKey.Key) recipient.keyID = encryptionKey.KeyID return recipient, err case OpaqueKeyEncrypter: return newOpaqueKeyEncrypter(alg, encryptionKey) } return recipientKeyInfo{}, ErrUnsupportedKeyType } // newDecrypter creates an appropriate decrypter based on the key type func newDecrypter(decryptionKey interface{}) (keyDecrypter, error) { switch decryptionKey := decryptionKey.(type) { case *rsa.PrivateKey: return &rsaDecrypterSigner{ privateKey: decryptionKey, }, nil case *ecdsa.PrivateKey: return &ecDecrypterSigner{ privateKey: decryptionKey, }, nil case []byte: return &symmetricKeyCipher{ key: decryptionKey, }, nil case string: return &symmetricKeyCipher{ key: []byte(decryptionKey), }, nil case JSONWebKey: return newDecrypter(decryptionKey.Key) case *JSONWebKey: return newDecrypter(decryptionKey.Key) case OpaqueKeyDecrypter: return &opaqueKeyDecrypter{decrypter: decryptionKey}, nil default: return nil, ErrUnsupportedKeyType } } // Implementation of encrypt method producing a JWE object. func (ctx *genericEncrypter) Encrypt(plaintext []byte) (*JSONWebEncryption, error) { return ctx.EncryptWithAuthData(plaintext, nil) } // Implementation of encrypt method producing a JWE object. func (ctx *genericEncrypter) EncryptWithAuthData(plaintext, aad []byte) (*JSONWebEncryption, error) { obj := &JSONWebEncryption{} obj.aad = aad obj.protected = &rawHeader{} err := obj.protected.set(headerEncryption, ctx.contentAlg) if err != nil { return nil, err } obj.recipients = make([]recipientInfo, len(ctx.recipients)) if len(ctx.recipients) == 0 { return nil, fmt.Errorf("go-jose/go-jose: no recipients to encrypt to") } cek, headers, err := ctx.keyGenerator.genKey() if err != nil { return nil, err } obj.protected.merge(&headers) for i, info := range ctx.recipients { recipient, err := info.keyEncrypter.encryptKey(cek, info.keyAlg) if err != nil { return nil, err } err = recipient.header.set(headerAlgorithm, info.keyAlg) if err != nil { return nil, err } if info.keyID != "" { err = recipient.header.set(headerKeyID, info.keyID) if err != nil { return nil, err } } obj.recipients[i] = recipient } if len(ctx.recipients) == 1 { // Move per-recipient headers into main protected header if there's // only a single recipient. obj.protected.merge(obj.recipients[0].header) obj.recipients[0].header = nil } if ctx.compressionAlg != NONE { plaintext, err = compress(ctx.compressionAlg, plaintext) if err != nil { return nil, err } err = obj.protected.set(headerCompression, ctx.compressionAlg) if err != nil { return nil, err } } for k, v := range ctx.extraHeaders { b, err := json.Marshal(v) if err != nil { return nil, err } (*obj.protected)[k] = makeRawMessage(b) } authData := obj.computeAuthData() parts, err := ctx.cipher.encrypt(cek, authData, plaintext) if err != nil { return nil, err } obj.iv = parts.iv obj.ciphertext = parts.ciphertext obj.tag = parts.tag return obj, nil } func (ctx *genericEncrypter) Options() EncrypterOptions { return EncrypterOptions{ Compression: ctx.compressionAlg, ExtraHeaders: ctx.extraHeaders, } } // Decrypt and validate the object and return the plaintext. This // function does not support multi-recipient. If you desire multi-recipient // decryption use DecryptMulti instead. // // The decryptionKey argument must contain a private or symmetric key // and must have one of these types: // - *ecdsa.PrivateKey // - *rsa.PrivateKey // - *JSONWebKey // - JSONWebKey // - *JSONWebKeySet // - JSONWebKeySet // - []byte (a symmetric key) // - string (a symmetric key) // - Any type that satisfies the OpaqueKeyDecrypter interface. // // Note that ed25519 is only available for signatures, not encryption, so is // not an option here. // // Automatically decompresses plaintext, but returns an error if the decompressed // data would be >250kB or >10x the size of the compressed data, whichever is larger. func (obj JSONWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error) { headers := obj.mergedHeaders(nil) if len(obj.recipients) > 1 { return nil, errors.New("go-jose/go-jose: too many recipients in payload; expecting only one") } err := headers.checkNoCritical() if err != nil { return nil, err } key, err := tryJWKS(decryptionKey, obj.Header) if err != nil { return nil, err } decrypter, err := newDecrypter(key) if err != nil { return nil, err } cipher := getContentCipher(headers.getEncryption()) if cipher == nil { return nil, fmt.Errorf("go-jose/go-jose: unsupported enc value '%s'", string(headers.getEncryption())) } generator := randomKeyGenerator{ size: cipher.keySize(), } parts := &aeadParts{ iv: obj.iv, ciphertext: obj.ciphertext, tag: obj.tag, } authData := obj.computeAuthData() var plaintext []byte recipient := obj.recipients[0] recipientHeaders := obj.mergedHeaders(&recipient) cek, err := decrypter.decryptKey(recipientHeaders, &recipient, generator) if err == nil { // Found a valid CEK -- let's try to decrypt. plaintext, err = cipher.decrypt(cek, authData, parts) } if plaintext == nil { return nil, ErrCryptoFailure } // The "zip" header parameter may only be present in the protected header. if comp := obj.protected.getCompression(); comp != "" { plaintext, err = decompress(comp, plaintext) if err != nil { return nil, fmt.Errorf("go-jose/go-jose: failed to decompress plaintext: %v", err) } } return plaintext, nil } // DecryptMulti decrypts and validates the object and returns the plaintexts, // with support for multiple recipients. It returns the index of the recipient // for which the decryption was successful, the merged headers for that recipient, // and the plaintext. // // The decryptionKey argument must have one of the types allowed for the // decryptionKey argument of Decrypt(). // // Automatically decompresses plaintext, but returns an error if the decompressed // data would be >250kB or >3x the size of the compressed data, whichever is larger. func (obj JSONWebEncryption) DecryptMulti(decryptionKey interface{}) (int, Header, []byte, error) { globalHeaders := obj.mergedHeaders(nil) err := globalHeaders.checkNoCritical() if err != nil { return -1, Header{}, nil, err } key, err := tryJWKS(decryptionKey, obj.Header) if err != nil { return -1, Header{}, nil, err } decrypter, err := newDecrypter(key) if err != nil { return -1, Header{}, nil, err } encryption := globalHeaders.getEncryption() cipher := getContentCipher(encryption) if cipher == nil { return -1, Header{}, nil, fmt.Errorf("go-jose/go-jose: unsupported enc value '%s'", string(encryption)) } generator := randomKeyGenerator{ size: cipher.keySize(), } parts := &aeadParts{ iv: obj.iv, ciphertext: obj.ciphertext, tag: obj.tag, } authData := obj.computeAuthData() index := -1 var plaintext []byte var headers rawHeader for i, recipient := range obj.recipients { recipientHeaders := obj.mergedHeaders(&recipient) cek, err := decrypter.decryptKey(recipientHeaders, &recipient, generator) if err == nil { // Found a valid CEK -- let's try to decrypt. plaintext, err = cipher.decrypt(cek, authData, parts) if err == nil { index = i headers = recipientHeaders break } } } if plaintext == nil { return -1, Header{}, nil, ErrCryptoFailure } // The "zip" header parameter may only be present in the protected header. if comp := obj.protected.getCompression(); comp != "" { plaintext, err = decompress(comp, plaintext) if err != nil { return -1, Header{}, nil, fmt.Errorf("go-jose/go-jose: failed to decompress plaintext: %v", err) } } sanitized, err := headers.sanitized() if err != nil { return -1, Header{}, nil, fmt.Errorf("go-jose/go-jose: failed to sanitize header: %v", err) } return index, sanitized, plaintext, err } ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/doc.go ================================================ /*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* Package jose aims to provide an implementation of the Javascript Object Signing and Encryption set of standards. It implements encryption and signing based on the JSON Web Encryption and JSON Web Signature standards, with optional JSON Web Token support available in a sub-package. The library supports both the compact and JWS/JWE JSON Serialization formats, and has optional support for multiple recipients. */ package jose ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/encoding.go ================================================ /*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jose import ( "bytes" "compress/flate" "encoding/base64" "encoding/binary" "fmt" "io" "math/big" "strings" "unicode" "github.com/go-jose/go-jose/v4/json" ) // Helper function to serialize known-good objects. // Precondition: value is not a nil pointer. func mustSerializeJSON(value interface{}) []byte { out, err := json.Marshal(value) if err != nil { panic(err) } // We never want to serialize the top-level value "null," since it's not a // valid JOSE message. But if a caller passes in a nil pointer to this method, // MarshalJSON will happily serialize it as the top-level value "null". If // that value is then embedded in another operation, for instance by being // base64-encoded and fed as input to a signing algorithm // (https://github.com/go-jose/go-jose/issues/22), the result will be // incorrect. Because this method is intended for known-good objects, and a nil // pointer is not a known-good object, we are free to panic in this case. // Note: It's not possible to directly check whether the data pointed at by an // interface is a nil pointer, so we do this hacky workaround. // https://groups.google.com/forum/#!topic/golang-nuts/wnH302gBa4I if string(out) == "null" { panic("Tried to serialize a nil pointer.") } return out } // Strip all newlines and whitespace func stripWhitespace(data string) string { buf := strings.Builder{} buf.Grow(len(data)) for _, r := range data { if !unicode.IsSpace(r) { buf.WriteRune(r) } } return buf.String() } // Perform compression based on algorithm func compress(algorithm CompressionAlgorithm, input []byte) ([]byte, error) { switch algorithm { case DEFLATE: return deflate(input) default: return nil, ErrUnsupportedAlgorithm } } // Perform decompression based on algorithm func decompress(algorithm CompressionAlgorithm, input []byte) ([]byte, error) { switch algorithm { case DEFLATE: return inflate(input) default: return nil, ErrUnsupportedAlgorithm } } // deflate compresses the input. func deflate(input []byte) ([]byte, error) { output := new(bytes.Buffer) // Writing to byte buffer, err is always nil writer, _ := flate.NewWriter(output, 1) _, _ = io.Copy(writer, bytes.NewBuffer(input)) err := writer.Close() return output.Bytes(), err } // inflate decompresses the input. // // Errors if the decompressed data would be >250kB or >10x the size of the // compressed data, whichever is larger. func inflate(input []byte) ([]byte, error) { output := new(bytes.Buffer) reader := flate.NewReader(bytes.NewBuffer(input)) maxCompressedSize := max(250_000, 10*int64(len(input))) limit := maxCompressedSize + 1 n, err := io.CopyN(output, reader, limit) if err != nil && err != io.EOF { return nil, err } if n == limit { return nil, fmt.Errorf("uncompressed data would be too large (>%d bytes)", maxCompressedSize) } err = reader.Close() return output.Bytes(), err } // byteBuffer represents a slice of bytes that can be serialized to url-safe base64. type byteBuffer struct { data []byte } func newBuffer(data []byte) *byteBuffer { if data == nil { return nil } return &byteBuffer{ data: data, } } func newFixedSizeBuffer(data []byte, length int) *byteBuffer { if len(data) > length { panic("go-jose/go-jose: invalid call to newFixedSizeBuffer (len(data) > length)") } pad := make([]byte, length-len(data)) return newBuffer(append(pad, data...)) } func newBufferFromInt(num uint64) *byteBuffer { data := make([]byte, 8) binary.BigEndian.PutUint64(data, num) return newBuffer(bytes.TrimLeft(data, "\x00")) } func (b *byteBuffer) MarshalJSON() ([]byte, error) { return json.Marshal(b.base64()) } func (b *byteBuffer) UnmarshalJSON(data []byte) error { var encoded string err := json.Unmarshal(data, &encoded) if err != nil { return err } if encoded == "" { return nil } decoded, err := base64.RawURLEncoding.DecodeString(encoded) if err != nil { return err } *b = *newBuffer(decoded) return nil } func (b *byteBuffer) base64() string { return base64.RawURLEncoding.EncodeToString(b.data) } func (b *byteBuffer) bytes() []byte { // Handling nil here allows us to transparently handle nil slices when serializing. if b == nil { return nil } return b.data } func (b byteBuffer) bigInt() *big.Int { return new(big.Int).SetBytes(b.data) } func (b byteBuffer) toInt() int { return int(b.bigInt().Int64()) } func base64EncodeLen(sl []byte) int { return base64.RawURLEncoding.EncodedLen(len(sl)) } func base64JoinWithDots(inputs ...[]byte) string { if len(inputs) == 0 { return "" } // Count of dots. totalCount := len(inputs) - 1 for _, input := range inputs { totalCount += base64EncodeLen(input) } out := make([]byte, totalCount) startEncode := 0 for i, input := range inputs { base64.RawURLEncoding.Encode(out[startEncode:], input) if i == len(inputs)-1 { continue } startEncode += base64EncodeLen(input) out[startEncode] = '.' startEncode++ } return string(out) } ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/json/LICENSE ================================================ Copyright (c) 2012 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/json/README.md ================================================ # Safe JSON This repository contains a fork of the `encoding/json` package from Go 1.6. The following changes were made: * Object deserialization uses case-sensitive member name matching instead of [case-insensitive matching](https://www.ietf.org/mail-archive/web/json/current/msg03763.html). This is to avoid differences in the interpretation of JOSE messages between go-jose and libraries written in other languages. * When deserializing a JSON object, we check for duplicate keys and reject the input whenever we detect a duplicate. Rather than trying to work with malformed data, we prefer to reject it right away. ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/json/decode.go ================================================ // Copyright 2010 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Represents JSON data structure using native Go types: booleans, floats, // strings, arrays, and maps. package json import ( "bytes" "encoding" "encoding/base64" "errors" "fmt" "math" "reflect" "runtime" "strconv" "unicode" "unicode/utf16" "unicode/utf8" ) // Unmarshal parses the JSON-encoded data and stores the result // in the value pointed to by v. // // Unmarshal uses the inverse of the encodings that // Marshal uses, allocating maps, slices, and pointers as necessary, // with the following additional rules: // // To unmarshal JSON into a pointer, Unmarshal first handles the case of // the JSON being the JSON literal null. In that case, Unmarshal sets // the pointer to nil. Otherwise, Unmarshal unmarshals the JSON into // the value pointed at by the pointer. If the pointer is nil, Unmarshal // allocates a new value for it to point to. // // To unmarshal JSON into a struct, Unmarshal matches incoming object // keys to the keys used by Marshal (either the struct field name or its tag), // preferring an exact match but also accepting a case-insensitive match. // Unmarshal will only set exported fields of the struct. // // To unmarshal JSON into an interface value, // Unmarshal stores one of these in the interface value: // // bool, for JSON booleans // float64, for JSON numbers // string, for JSON strings // []interface{}, for JSON arrays // map[string]interface{}, for JSON objects // nil for JSON null // // To unmarshal a JSON array into a slice, Unmarshal resets the slice length // to zero and then appends each element to the slice. // As a special case, to unmarshal an empty JSON array into a slice, // Unmarshal replaces the slice with a new empty slice. // // To unmarshal a JSON array into a Go array, Unmarshal decodes // JSON array elements into corresponding Go array elements. // If the Go array is smaller than the JSON array, // the additional JSON array elements are discarded. // If the JSON array is smaller than the Go array, // the additional Go array elements are set to zero values. // // To unmarshal a JSON object into a string-keyed map, Unmarshal first // establishes a map to use, If the map is nil, Unmarshal allocates a new map. // Otherwise Unmarshal reuses the existing map, keeping existing entries. // Unmarshal then stores key-value pairs from the JSON object into the map. // // If a JSON value is not appropriate for a given target type, // or if a JSON number overflows the target type, Unmarshal // skips that field and completes the unmarshaling as best it can. // If no more serious errors are encountered, Unmarshal returns // an UnmarshalTypeError describing the earliest such error. // // The JSON null value unmarshals into an interface, map, pointer, or slice // by setting that Go value to nil. Because null is often used in JSON to mean // “not present,” unmarshaling a JSON null into any other Go type has no effect // on the value and produces no error. // // When unmarshaling quoted strings, invalid UTF-8 or // invalid UTF-16 surrogate pairs are not treated as an error. // Instead, they are replaced by the Unicode replacement // character U+FFFD. func Unmarshal(data []byte, v interface{}) error { // Check for well-formedness. // Avoids filling out half a data structure // before discovering a JSON syntax error. var d decodeState err := checkValid(data, &d.scan) if err != nil { return err } d.init(data) return d.unmarshal(v) } // Unmarshaler is the interface implemented by objects // that can unmarshal a JSON description of themselves. // The input can be assumed to be a valid encoding of // a JSON value. UnmarshalJSON must copy the JSON data // if it wishes to retain the data after returning. type Unmarshaler interface { UnmarshalJSON([]byte) error } // An UnmarshalTypeError describes a JSON value that was // not appropriate for a value of a specific Go type. type UnmarshalTypeError struct { Value string // description of JSON value - "bool", "array", "number -5" Type reflect.Type // type of Go value it could not be assigned to Offset int64 // error occurred after reading Offset bytes } func (e *UnmarshalTypeError) Error() string { return "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String() } // An UnmarshalFieldError describes a JSON object key that // led to an unexported (and therefore unwritable) struct field. // (No longer used; kept for compatibility.) type UnmarshalFieldError struct { Key string Type reflect.Type Field reflect.StructField } func (e *UnmarshalFieldError) Error() string { return "json: cannot unmarshal object key " + strconv.Quote(e.Key) + " into unexported field " + e.Field.Name + " of type " + e.Type.String() } // An InvalidUnmarshalError describes an invalid argument passed to Unmarshal. // (The argument to Unmarshal must be a non-nil pointer.) type InvalidUnmarshalError struct { Type reflect.Type } func (e *InvalidUnmarshalError) Error() string { if e.Type == nil { return "json: Unmarshal(nil)" } if e.Type.Kind() != reflect.Ptr { return "json: Unmarshal(non-pointer " + e.Type.String() + ")" } return "json: Unmarshal(nil " + e.Type.String() + ")" } func (d *decodeState) unmarshal(v interface{}) (err error) { defer func() { if r := recover(); r != nil { if _, ok := r.(runtime.Error); ok { panic(r) } err = r.(error) } }() rv := reflect.ValueOf(v) if rv.Kind() != reflect.Ptr || rv.IsNil() { return &InvalidUnmarshalError{reflect.TypeOf(v)} } d.scan.reset() // We decode rv not rv.Elem because the Unmarshaler interface // test must be applied at the top level of the value. d.value(rv) return d.savedError } // A Number represents a JSON number literal. type Number string // String returns the literal text of the number. func (n Number) String() string { return string(n) } // Float64 returns the number as a float64. func (n Number) Float64() (float64, error) { return strconv.ParseFloat(string(n), 64) } // Int64 returns the number as an int64. func (n Number) Int64() (int64, error) { return strconv.ParseInt(string(n), 10, 64) } // isValidNumber reports whether s is a valid JSON number literal. func isValidNumber(s string) bool { // This function implements the JSON numbers grammar. // See https://tools.ietf.org/html/rfc7159#section-6 // and http://json.org/number.gif if s == "" { return false } // Optional - if s[0] == '-' { s = s[1:] if s == "" { return false } } // Digits switch { default: return false case s[0] == '0': s = s[1:] case '1' <= s[0] && s[0] <= '9': s = s[1:] for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { s = s[1:] } } // . followed by 1 or more digits. if len(s) >= 2 && s[0] == '.' && '0' <= s[1] && s[1] <= '9' { s = s[2:] for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { s = s[1:] } } // e or E followed by an optional - or + and // 1 or more digits. if len(s) >= 2 && (s[0] == 'e' || s[0] == 'E') { s = s[1:] if s[0] == '+' || s[0] == '-' { s = s[1:] if s == "" { return false } } for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { s = s[1:] } } // Make sure we are at the end. return s == "" } type NumberUnmarshalType int const ( // unmarshal a JSON number into an interface{} as a float64 UnmarshalFloat NumberUnmarshalType = iota // unmarshal a JSON number into an interface{} as a `json.Number` UnmarshalJSONNumber // unmarshal a JSON number into an interface{} as a int64 // if value is an integer otherwise float64 UnmarshalIntOrFloat ) // decodeState represents the state while decoding a JSON value. type decodeState struct { data []byte off int // read offset in data scan scanner nextscan scanner // for calls to nextValue savedError error numberType NumberUnmarshalType } // errPhase is used for errors that should not happen unless // there is a bug in the JSON decoder or something is editing // the data slice while the decoder executes. var errPhase = errors.New("JSON decoder out of sync - data changing underfoot?") func (d *decodeState) init(data []byte) *decodeState { d.data = data d.off = 0 d.savedError = nil return d } // error aborts the decoding by panicking with err. func (d *decodeState) error(err error) { panic(err) } // saveError saves the first err it is called with, // for reporting at the end of the unmarshal. func (d *decodeState) saveError(err error) { if d.savedError == nil { d.savedError = err } } // next cuts off and returns the next full JSON value in d.data[d.off:]. // The next value is known to be an object or array, not a literal. func (d *decodeState) next() []byte { c := d.data[d.off] item, rest, err := nextValue(d.data[d.off:], &d.nextscan) if err != nil { d.error(err) } d.off = len(d.data) - len(rest) // Our scanner has seen the opening brace/bracket // and thinks we're still in the middle of the object. // invent a closing brace/bracket to get it out. if c == '{' { d.scan.step(&d.scan, '}') } else { d.scan.step(&d.scan, ']') } return item } // scanWhile processes bytes in d.data[d.off:] until it // receives a scan code not equal to op. // It updates d.off and returns the new scan code. func (d *decodeState) scanWhile(op int) int { var newOp int for { if d.off >= len(d.data) { newOp = d.scan.eof() d.off = len(d.data) + 1 // mark processed EOF with len+1 } else { c := d.data[d.off] d.off++ newOp = d.scan.step(&d.scan, c) } if newOp != op { break } } return newOp } // value decodes a JSON value from d.data[d.off:] into the value. // it updates d.off to point past the decoded value. func (d *decodeState) value(v reflect.Value) { if !v.IsValid() { _, rest, err := nextValue(d.data[d.off:], &d.nextscan) if err != nil { d.error(err) } d.off = len(d.data) - len(rest) // d.scan thinks we're still at the beginning of the item. // Feed in an empty string - the shortest, simplest value - // so that it knows we got to the end of the value. if d.scan.redo { // rewind. d.scan.redo = false d.scan.step = stateBeginValue } d.scan.step(&d.scan, '"') d.scan.step(&d.scan, '"') n := len(d.scan.parseState) if n > 0 && d.scan.parseState[n-1] == parseObjectKey { // d.scan thinks we just read an object key; finish the object d.scan.step(&d.scan, ':') d.scan.step(&d.scan, '"') d.scan.step(&d.scan, '"') d.scan.step(&d.scan, '}') } return } switch op := d.scanWhile(scanSkipSpace); op { default: d.error(errPhase) case scanBeginArray: d.array(v) case scanBeginObject: d.object(v) case scanBeginLiteral: d.literal(v) } } type unquotedValue struct{} // valueQuoted is like value but decodes a // quoted string literal or literal null into an interface value. // If it finds anything other than a quoted string literal or null, // valueQuoted returns unquotedValue{}. func (d *decodeState) valueQuoted() interface{} { switch op := d.scanWhile(scanSkipSpace); op { default: d.error(errPhase) case scanBeginArray: d.array(reflect.Value{}) case scanBeginObject: d.object(reflect.Value{}) case scanBeginLiteral: switch v := d.literalInterface().(type) { case nil, string: return v } } return unquotedValue{} } // indirect walks down v allocating pointers as needed, // until it gets to a non-pointer. // if it encounters an Unmarshaler, indirect stops and returns that. // if decodingNull is true, indirect stops at the last pointer so it can be set to nil. func (d *decodeState) indirect(v reflect.Value, decodingNull bool) (Unmarshaler, encoding.TextUnmarshaler, reflect.Value) { // If v is a named type and is addressable, // start with its address, so that if the type has pointer methods, // we find them. if v.Kind() != reflect.Ptr && v.Type().Name() != "" && v.CanAddr() { v = v.Addr() } for { // Load value from interface, but only if the result will be // usefully addressable. if v.Kind() == reflect.Interface && !v.IsNil() { e := v.Elem() if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) { v = e continue } } if v.Kind() != reflect.Ptr { break } if v.Elem().Kind() != reflect.Ptr && decodingNull && v.CanSet() { break } if v.IsNil() { v.Set(reflect.New(v.Type().Elem())) } if v.Type().NumMethod() > 0 { if u, ok := v.Interface().(Unmarshaler); ok { return u, nil, reflect.Value{} } if u, ok := v.Interface().(encoding.TextUnmarshaler); ok { return nil, u, reflect.Value{} } } v = v.Elem() } return nil, nil, v } // array consumes an array from d.data[d.off-1:], decoding into the value v. // the first byte of the array ('[') has been read already. func (d *decodeState) array(v reflect.Value) { // Check for unmarshaler. u, ut, pv := d.indirect(v, false) if u != nil { d.off-- err := u.UnmarshalJSON(d.next()) if err != nil { d.error(err) } return } if ut != nil { d.saveError(&UnmarshalTypeError{"array", v.Type(), int64(d.off)}) d.off-- d.next() return } v = pv // Check type of target. switch v.Kind() { case reflect.Interface: if v.NumMethod() == 0 { // Decoding into nil interface? Switch to non-reflect code. v.Set(reflect.ValueOf(d.arrayInterface())) return } // Otherwise it's invalid. fallthrough default: d.saveError(&UnmarshalTypeError{"array", v.Type(), int64(d.off)}) d.off-- d.next() return case reflect.Array: case reflect.Slice: break } i := 0 for { // Look ahead for ] - can only happen on first iteration. op := d.scanWhile(scanSkipSpace) if op == scanEndArray { break } // Back up so d.value can have the byte we just read. d.off-- d.scan.undo(op) // Get element of array, growing if necessary. if v.Kind() == reflect.Slice { // Grow slice if necessary if i >= v.Cap() { newcap := v.Cap() + v.Cap()/2 if newcap < 4 { newcap = 4 } newv := reflect.MakeSlice(v.Type(), v.Len(), newcap) reflect.Copy(newv, v) v.Set(newv) } if i >= v.Len() { v.SetLen(i + 1) } } if i < v.Len() { // Decode into element. d.value(v.Index(i)) } else { // Ran out of fixed array: skip. d.value(reflect.Value{}) } i++ // Next token must be , or ]. op = d.scanWhile(scanSkipSpace) if op == scanEndArray { break } if op != scanArrayValue { d.error(errPhase) } } if i < v.Len() { if v.Kind() == reflect.Array { // Array. Zero the rest. z := reflect.Zero(v.Type().Elem()) for ; i < v.Len(); i++ { v.Index(i).Set(z) } } else { v.SetLen(i) } } if i == 0 && v.Kind() == reflect.Slice { v.Set(reflect.MakeSlice(v.Type(), 0, 0)) } } var nullLiteral = []byte("null") // object consumes an object from d.data[d.off-1:], decoding into the value v. // the first byte ('{') of the object has been read already. func (d *decodeState) object(v reflect.Value) { // Check for unmarshaler. u, ut, pv := d.indirect(v, false) if u != nil { d.off-- err := u.UnmarshalJSON(d.next()) if err != nil { d.error(err) } return } if ut != nil { d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)}) d.off-- d.next() // skip over { } in input return } v = pv // Decoding into nil interface? Switch to non-reflect code. if v.Kind() == reflect.Interface && v.NumMethod() == 0 { v.Set(reflect.ValueOf(d.objectInterface())) return } // Check type of target: struct or map[string]T switch v.Kind() { case reflect.Map: // map must have string kind t := v.Type() if t.Key().Kind() != reflect.String { d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)}) d.off-- d.next() // skip over { } in input return } if v.IsNil() { v.Set(reflect.MakeMap(t)) } case reflect.Struct: default: d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)}) d.off-- d.next() // skip over { } in input return } var mapElem reflect.Value keys := map[string]bool{} for { // Read opening " of string key or closing }. op := d.scanWhile(scanSkipSpace) if op == scanEndObject { // closing } - can only happen on first iteration. break } if op != scanBeginLiteral { d.error(errPhase) } // Read key. start := d.off - 1 op = d.scanWhile(scanContinue) item := d.data[start : d.off-1] key, ok := unquote(item) if !ok { d.error(errPhase) } // Check for duplicate keys. _, ok = keys[key] if !ok { keys[key] = true } else { d.error(fmt.Errorf("json: duplicate key '%s' in object", key)) } // Figure out field corresponding to key. var subv reflect.Value destring := false // whether the value is wrapped in a string to be decoded first if v.Kind() == reflect.Map { elemType := v.Type().Elem() if !mapElem.IsValid() { mapElem = reflect.New(elemType).Elem() } else { mapElem.Set(reflect.Zero(elemType)) } subv = mapElem } else { var f *field fields := cachedTypeFields(v.Type()) for i := range fields { ff := &fields[i] if bytes.Equal(ff.nameBytes, []byte(key)) { f = ff break } } if f != nil { subv = v destring = f.quoted for _, i := range f.index { if subv.Kind() == reflect.Ptr { if subv.IsNil() { subv.Set(reflect.New(subv.Type().Elem())) } subv = subv.Elem() } subv = subv.Field(i) } } } // Read : before value. if op == scanSkipSpace { op = d.scanWhile(scanSkipSpace) } if op != scanObjectKey { d.error(errPhase) } // Read value. if destring { switch qv := d.valueQuoted().(type) { case nil: d.literalStore(nullLiteral, subv, false) case string: d.literalStore([]byte(qv), subv, true) default: d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal unquoted value into %v", subv.Type())) } } else { d.value(subv) } // Write value back to map; // if using struct, subv points into struct already. if v.Kind() == reflect.Map { kv := reflect.ValueOf(key).Convert(v.Type().Key()) v.SetMapIndex(kv, subv) } // Next token must be , or }. op = d.scanWhile(scanSkipSpace) if op == scanEndObject { break } if op != scanObjectValue { d.error(errPhase) } } } // literal consumes a literal from d.data[d.off-1:], decoding into the value v. // The first byte of the literal has been read already // (that's how the caller knows it's a literal). func (d *decodeState) literal(v reflect.Value) { // All bytes inside literal return scanContinue op code. start := d.off - 1 op := d.scanWhile(scanContinue) // Scan read one byte too far; back up. d.off-- d.scan.undo(op) d.literalStore(d.data[start:d.off], v, false) } // convertNumber converts the number literal s to a float64, int64 or a Number // depending on d.numberDecodeType. func (d *decodeState) convertNumber(s string) (interface{}, error) { switch d.numberType { case UnmarshalJSONNumber: return Number(s), nil case UnmarshalIntOrFloat: v, err := strconv.ParseInt(s, 10, 64) if err == nil { return v, nil } // tries to parse integer number in scientific notation f, err := strconv.ParseFloat(s, 64) if err != nil { return nil, &UnmarshalTypeError{"number " + s, reflect.TypeOf(0.0), int64(d.off)} } // if it has no decimal value use int64 if fi, fd := math.Modf(f); fd == 0.0 { return int64(fi), nil } return f, nil default: f, err := strconv.ParseFloat(s, 64) if err != nil { return nil, &UnmarshalTypeError{"number " + s, reflect.TypeOf(0.0), int64(d.off)} } return f, nil } } var numberType = reflect.TypeOf(Number("")) // literalStore decodes a literal stored in item into v. // // fromQuoted indicates whether this literal came from unwrapping a // string from the ",string" struct tag option. this is used only to // produce more helpful error messages. func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool) { // Check for unmarshaler. if len(item) == 0 { //Empty string given d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) return } wantptr := item[0] == 'n' // null u, ut, pv := d.indirect(v, wantptr) if u != nil { err := u.UnmarshalJSON(item) if err != nil { d.error(err) } return } if ut != nil { if item[0] != '"' { if fromQuoted { d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) } else { d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) } return } s, ok := unquoteBytes(item) if !ok { if fromQuoted { d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) } else { d.error(errPhase) } } err := ut.UnmarshalText(s) if err != nil { d.error(err) } return } v = pv switch c := item[0]; c { case 'n': // null switch v.Kind() { case reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice: v.Set(reflect.Zero(v.Type())) // otherwise, ignore null for primitives/string } case 't', 'f': // true, false value := c == 't' switch v.Kind() { default: if fromQuoted { d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) } else { d.saveError(&UnmarshalTypeError{"bool", v.Type(), int64(d.off)}) } case reflect.Bool: v.SetBool(value) case reflect.Interface: if v.NumMethod() == 0 { v.Set(reflect.ValueOf(value)) } else { d.saveError(&UnmarshalTypeError{"bool", v.Type(), int64(d.off)}) } } case '"': // string s, ok := unquoteBytes(item) if !ok { if fromQuoted { d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) } else { d.error(errPhase) } } switch v.Kind() { default: d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) case reflect.Slice: if v.Type().Elem().Kind() != reflect.Uint8 { d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) break } b := make([]byte, base64.StdEncoding.DecodedLen(len(s))) n, err := base64.StdEncoding.Decode(b, s) if err != nil { d.saveError(err) break } v.SetBytes(b[:n]) case reflect.String: v.SetString(string(s)) case reflect.Interface: if v.NumMethod() == 0 { v.Set(reflect.ValueOf(string(s))) } else { d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) } } default: // number if c != '-' && (c < '0' || c > '9') { if fromQuoted { d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) } else { d.error(errPhase) } } s := string(item) switch v.Kind() { default: if v.Kind() == reflect.String && v.Type() == numberType { v.SetString(s) if !isValidNumber(s) { d.error(fmt.Errorf("json: invalid number literal, trying to unmarshal %q into Number", item)) } break } if fromQuoted { d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) } else { d.error(&UnmarshalTypeError{"number", v.Type(), int64(d.off)}) } case reflect.Interface: n, err := d.convertNumber(s) if err != nil { d.saveError(err) break } if v.NumMethod() != 0 { d.saveError(&UnmarshalTypeError{"number", v.Type(), int64(d.off)}) break } v.Set(reflect.ValueOf(n)) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: n, err := strconv.ParseInt(s, 10, 64) if err != nil || v.OverflowInt(n) { d.saveError(&UnmarshalTypeError{"number " + s, v.Type(), int64(d.off)}) break } v.SetInt(n) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: n, err := strconv.ParseUint(s, 10, 64) if err != nil || v.OverflowUint(n) { d.saveError(&UnmarshalTypeError{"number " + s, v.Type(), int64(d.off)}) break } v.SetUint(n) case reflect.Float32, reflect.Float64: n, err := strconv.ParseFloat(s, v.Type().Bits()) if err != nil || v.OverflowFloat(n) { d.saveError(&UnmarshalTypeError{"number " + s, v.Type(), int64(d.off)}) break } v.SetFloat(n) } } } // The xxxInterface routines build up a value to be stored // in an empty interface. They are not strictly necessary, // but they avoid the weight of reflection in this common case. // valueInterface is like value but returns interface{} func (d *decodeState) valueInterface() interface{} { switch d.scanWhile(scanSkipSpace) { default: d.error(errPhase) panic("unreachable") case scanBeginArray: return d.arrayInterface() case scanBeginObject: return d.objectInterface() case scanBeginLiteral: return d.literalInterface() } } // arrayInterface is like array but returns []interface{}. func (d *decodeState) arrayInterface() []interface{} { var v = make([]interface{}, 0) for { // Look ahead for ] - can only happen on first iteration. op := d.scanWhile(scanSkipSpace) if op == scanEndArray { break } // Back up so d.value can have the byte we just read. d.off-- d.scan.undo(op) v = append(v, d.valueInterface()) // Next token must be , or ]. op = d.scanWhile(scanSkipSpace) if op == scanEndArray { break } if op != scanArrayValue { d.error(errPhase) } } return v } // objectInterface is like object but returns map[string]interface{}. func (d *decodeState) objectInterface() map[string]interface{} { m := make(map[string]interface{}) keys := map[string]bool{} for { // Read opening " of string key or closing }. op := d.scanWhile(scanSkipSpace) if op == scanEndObject { // closing } - can only happen on first iteration. break } if op != scanBeginLiteral { d.error(errPhase) } // Read string key. start := d.off - 1 op = d.scanWhile(scanContinue) item := d.data[start : d.off-1] key, ok := unquote(item) if !ok { d.error(errPhase) } // Check for duplicate keys. _, ok = keys[key] if !ok { keys[key] = true } else { d.error(fmt.Errorf("json: duplicate key '%s' in object", key)) } // Read : before value. if op == scanSkipSpace { op = d.scanWhile(scanSkipSpace) } if op != scanObjectKey { d.error(errPhase) } // Read value. m[key] = d.valueInterface() // Next token must be , or }. op = d.scanWhile(scanSkipSpace) if op == scanEndObject { break } if op != scanObjectValue { d.error(errPhase) } } return m } // literalInterface is like literal but returns an interface value. func (d *decodeState) literalInterface() interface{} { // All bytes inside literal return scanContinue op code. start := d.off - 1 op := d.scanWhile(scanContinue) // Scan read one byte too far; back up. d.off-- d.scan.undo(op) item := d.data[start:d.off] switch c := item[0]; c { case 'n': // null return nil case 't', 'f': // true, false return c == 't' case '"': // string s, ok := unquote(item) if !ok { d.error(errPhase) } return s default: // number if c != '-' && (c < '0' || c > '9') { d.error(errPhase) } n, err := d.convertNumber(string(item)) if err != nil { d.saveError(err) } return n } } // getu4 decodes \uXXXX from the beginning of s, returning the hex value, // or it returns -1. func getu4(s []byte) rune { if len(s) < 6 || s[0] != '\\' || s[1] != 'u' { return -1 } r, err := strconv.ParseUint(string(s[2:6]), 16, 64) if err != nil { return -1 } return rune(r) } // unquote converts a quoted JSON string literal s into an actual string t. // The rules are different than for Go, so cannot use strconv.Unquote. func unquote(s []byte) (t string, ok bool) { s, ok = unquoteBytes(s) t = string(s) return } func unquoteBytes(s []byte) (t []byte, ok bool) { if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' { return } s = s[1 : len(s)-1] // Check for unusual characters. If there are none, // then no unquoting is needed, so return a slice of the // original bytes. r := 0 for r < len(s) { c := s[r] if c == '\\' || c == '"' || c < ' ' { break } if c < utf8.RuneSelf { r++ continue } rr, size := utf8.DecodeRune(s[r:]) if rr == utf8.RuneError && size == 1 { break } r += size } if r == len(s) { return s, true } b := make([]byte, len(s)+2*utf8.UTFMax) w := copy(b, s[0:r]) for r < len(s) { // Out of room? Can only happen if s is full of // malformed UTF-8 and we're replacing each // byte with RuneError. if w >= len(b)-2*utf8.UTFMax { nb := make([]byte, (len(b)+utf8.UTFMax)*2) copy(nb, b[0:w]) b = nb } switch c := s[r]; { case c == '\\': r++ if r >= len(s) { return } switch s[r] { default: return case '"', '\\', '/', '\'': b[w] = s[r] r++ w++ case 'b': b[w] = '\b' r++ w++ case 'f': b[w] = '\f' r++ w++ case 'n': b[w] = '\n' r++ w++ case 'r': b[w] = '\r' r++ w++ case 't': b[w] = '\t' r++ w++ case 'u': r-- rr := getu4(s[r:]) if rr < 0 { return } r += 6 if utf16.IsSurrogate(rr) { rr1 := getu4(s[r:]) if dec := utf16.DecodeRune(rr, rr1); dec != unicode.ReplacementChar { // A valid pair; consume. r += 6 w += utf8.EncodeRune(b[w:], dec) break } // Invalid surrogate; fall back to replacement rune. rr = unicode.ReplacementChar } w += utf8.EncodeRune(b[w:], rr) } // Quote, control characters are invalid. case c == '"', c < ' ': return // ASCII case c < utf8.RuneSelf: b[w] = c r++ w++ // Coerce to well-formed UTF-8. default: rr, size := utf8.DecodeRune(s[r:]) r += size w += utf8.EncodeRune(b[w:], rr) } } return b[0:w], true } ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/json/encode.go ================================================ // Copyright 2010 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package json implements encoding and decoding of JSON objects as defined in // RFC 4627. The mapping between JSON objects and Go values is described // in the documentation for the Marshal and Unmarshal functions. // // See "JSON and Go" for an introduction to this package: // https://golang.org/doc/articles/json_and_go.html package json import ( "bytes" "encoding" "encoding/base64" "fmt" "math" "reflect" "runtime" "sort" "strconv" "strings" "sync" "unicode" "unicode/utf8" ) // Marshal returns the JSON encoding of v. // // Marshal traverses the value v recursively. // If an encountered value implements the Marshaler interface // and is not a nil pointer, Marshal calls its MarshalJSON method // to produce JSON. If no MarshalJSON method is present but the // value implements encoding.TextMarshaler instead, Marshal calls // its MarshalText method. // The nil pointer exception is not strictly necessary // but mimics a similar, necessary exception in the behavior of // UnmarshalJSON. // // Otherwise, Marshal uses the following type-dependent default encodings: // // Boolean values encode as JSON booleans. // // Floating point, integer, and Number values encode as JSON numbers. // // String values encode as JSON strings coerced to valid UTF-8, // replacing invalid bytes with the Unicode replacement rune. // The angle brackets "<" and ">" are escaped to "\u003c" and "\u003e" // to keep some browsers from misinterpreting JSON output as HTML. // Ampersand "&" is also escaped to "\u0026" for the same reason. // // Array and slice values encode as JSON arrays, except that // []byte encodes as a base64-encoded string, and a nil slice // encodes as the null JSON object. // // Struct values encode as JSON objects. Each exported struct field // becomes a member of the object unless // - the field's tag is "-", or // - the field is empty and its tag specifies the "omitempty" option. // // The empty values are false, 0, any // nil pointer or interface value, and any array, slice, map, or string of // length zero. The object's default key string is the struct field name // but can be specified in the struct field's tag value. The "json" key in // the struct field's tag value is the key name, followed by an optional comma // and options. Examples: // // // Field is ignored by this package. // Field int `json:"-"` // // // Field appears in JSON as key "myName". // Field int `json:"myName"` // // // Field appears in JSON as key "myName" and // // the field is omitted from the object if its value is empty, // // as defined above. // Field int `json:"myName,omitempty"` // // // Field appears in JSON as key "Field" (the default), but // // the field is skipped if empty. // // Note the leading comma. // Field int `json:",omitempty"` // // The "string" option signals that a field is stored as JSON inside a // JSON-encoded string. It applies only to fields of string, floating point, // integer, or boolean types. This extra level of encoding is sometimes used // when communicating with JavaScript programs: // // Int64String int64 `json:",string"` // // The key name will be used if it's a non-empty string consisting of // only Unicode letters, digits, dollar signs, percent signs, hyphens, // underscores and slashes. // // Anonymous struct fields are usually marshaled as if their inner exported fields // were fields in the outer struct, subject to the usual Go visibility rules amended // as described in the next paragraph. // An anonymous struct field with a name given in its JSON tag is treated as // having that name, rather than being anonymous. // An anonymous struct field of interface type is treated the same as having // that type as its name, rather than being anonymous. // // The Go visibility rules for struct fields are amended for JSON when // deciding which field to marshal or unmarshal. If there are // multiple fields at the same level, and that level is the least // nested (and would therefore be the nesting level selected by the // usual Go rules), the following extra rules apply: // // 1) Of those fields, if any are JSON-tagged, only tagged fields are considered, // even if there are multiple untagged fields that would otherwise conflict. // 2) If there is exactly one field (tagged or not according to the first rule), that is selected. // 3) Otherwise there are multiple fields, and all are ignored; no error occurs. // // Handling of anonymous struct fields is new in Go 1.1. // Prior to Go 1.1, anonymous struct fields were ignored. To force ignoring of // an anonymous struct field in both current and earlier versions, give the field // a JSON tag of "-". // // Map values encode as JSON objects. // The map's key type must be string; the map keys are used as JSON object // keys, subject to the UTF-8 coercion described for string values above. // // Pointer values encode as the value pointed to. // A nil pointer encodes as the null JSON object. // // Interface values encode as the value contained in the interface. // A nil interface value encodes as the null JSON object. // // Channel, complex, and function values cannot be encoded in JSON. // Attempting to encode such a value causes Marshal to return // an UnsupportedTypeError. // // JSON cannot represent cyclic data structures and Marshal does not // handle them. Passing cyclic structures to Marshal will result in // an infinite recursion. func Marshal(v interface{}) ([]byte, error) { e := &encodeState{} err := e.marshal(v) if err != nil { return nil, err } return e.Bytes(), nil } // MarshalIndent is like Marshal but applies Indent to format the output. func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) { b, err := Marshal(v) if err != nil { return nil, err } var buf bytes.Buffer err = Indent(&buf, b, prefix, indent) if err != nil { return nil, err } return buf.Bytes(), nil } // HTMLEscape appends to dst the JSON-encoded src with <, >, &, U+2028 and U+2029 // characters inside string literals changed to \u003c, \u003e, \u0026, \u2028, \u2029 // so that the JSON will be safe to embed inside HTML