From 66205265b98597a5661402bb293d2b59d4a2a652 Mon Sep 17 00:00:00 2001 From: Daniel McIlvaney Date: Tue, 12 Nov 2024 14:31:39 -0800 Subject: [PATCH 1/7] Move to 'systemd-detect-virt' for container detection --- .../docs/building/prerequisites-mariner.md | 1 + .../internal/buildpipeline/buildpipeline.go | 140 ++++++++++++++++-- 2 files changed, 129 insertions(+), 12 deletions(-) diff --git a/toolkit/docs/building/prerequisites-mariner.md b/toolkit/docs/building/prerequisites-mariner.md index a8acb5dd06b..18173595f5c 100644 --- a/toolkit/docs/building/prerequisites-mariner.md +++ b/toolkit/docs/building/prerequisites-mariner.md @@ -32,6 +32,7 @@ sudo tdnf -y install \ rpm \ rpm-build \ sudo \ + systemd \ tar \ wget \ xfsprogs \ diff --git a/toolkit/tools/internal/buildpipeline/buildpipeline.go b/toolkit/tools/internal/buildpipeline/buildpipeline.go index 873c937ef00..d8aff554865 100644 --- a/toolkit/tools/internal/buildpipeline/buildpipeline.go +++ b/toolkit/tools/internal/buildpipeline/buildpipeline.go @@ -8,28 +8,144 @@ package buildpipeline import ( "fmt" "os" + "os/exec" "path/filepath" + "strings" + + "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" "github.com/microsoft/azurelinux/toolkit/tools/internal/file" "github.com/microsoft/azurelinux/toolkit/tools/internal/logger" - - "golang.org/x/sys/unix" + "github.com/microsoft/azurelinux/toolkit/tools/internal/shell" ) const ( - rootBaseDirEnv = "CHROOT_DIR" - chrootLock = "chroot-pool.lock" - chrootUse = "chroot-used" + rootBaseDirEnv = "CHROOT_DIR" + chrootLock = "chroot-pool.lock" + chrootUse = "chroot-used" + systemdDetectVirtTool = "systemd-detect-virt" ) +var isRegularBuildCached *bool + +// checkIfContainerDockerEnvFile checks if the tool is running in a Docker container by checking if /.dockerenv exists. This +// check may not be reliable in all environments, so it is recommended to use systemd-detect-virt if available. +func checkIfContainerDockerEnvFile() bool { + exists, err := file.PathExists("/.dockerenv") + if err != nil { + logger.Log.Errorf("Error checking /.dockerenv: %v", err) + return false + } + return exists +} + +// checkIfContainerIgnoreDockerEnvFile checks if the user has placed a file in the root directory to ignore the Docker +// environment check. +func checkIfContainerIgnoreDockerEnvFile() bool { + ignoreDockerEnvExists, err := file.PathExists("/.mariner-toolkit-ignore-dockerenv") + if err != nil { + logger.Log.Errorf("Failed to check if /.mariner-toolkit-ignore-dockerenv exists: %s", err) + return false + } + return ignoreDockerEnvExists +} + +// checkIfContainerChrootDirEnv checks if the user has set the CHROOT_DIR environment variable, which is a requirement for +// Docker-based builds. If the variable exists, it is likely that the tool is running in a Docker container. +func checkIfContainerChrootDirEnv() bool { + _, exists := os.LookupEnv(rootBaseDirEnv) + return exists +} + +// checkIfContainerSystemdDetectVirt uses systemd-detect-virt, a tool that can be used to detect if the system is running +// in a virtualized environment. More specifically, using '-c' flag will detect container-based virtualization only. +func checkIfContainerSystemdDetectVirt() (bool, error) { + // We should have the systemd-detect-virt command available in the environment, but check for it just in case since it + // was previously not explicitly required for the toolkit. + _, err := exec.LookPath(systemdDetectVirtTool) + if err != nil { + err = fmt.Errorf("failed to find %s in the PATH: %w", systemdDetectVirtTool, err) + return false, err + } + + // The tool will return error code 1 based on detection, we only care about the stdout so ignore the return code. + stdout, _, _ := shell.Execute(systemdDetectVirtTool, "-c") + + // There are several possible outputs from systemd-detect-virt we care about: + // - none: Not running in a virtualized environment, easy + // - wsl: Reports as a container, but we don't want to treat it as such. It should be able to handle regular builds + // - anything else: We'll assume it's a container + stdout = strings.TrimSpace(stdout) + switch stdout { + case "none": + logger.Log.Debugf("Tool is not running in a container, systemd-detect-virt reports: '%s'", stdout) + return false, nil + case "wsl": + logger.Log.Debugf("Tool is running in WSL, treating as a non-container environment, systemd-detect-virt reports: '%s'", stdout) + return false, nil + default: + logger.Log.Debugf("Tool is running in a container, systemd-detect-virt reports: '%s'", stdout) + return true, nil + } +} + // IsRegularBuild indicates if it is a regular build (without using docker) func IsRegularBuild() bool { - // some specific build pipeline builds Azure Linux from a Docker container and - // consequently have special requirements with regards to chroot - // check if .dockerenv file exist to disambiguate build pipeline - dockerEnvExists, _ := file.PathExists("/.dockerenv") - ignoreDockerEnvExists, _ := file.PathExists("/.mariner-toolkit-ignore-dockerenv") - return ignoreDockerEnvExists || !dockerEnvExists + if isRegularBuildCached != nil { + return *isRegularBuildCached + } + + // If /.mariner-toolkit-ignore-dockerenv exists, then it is a regular build no matter what. + if checkIfContainerIgnoreDockerEnvFile() { + isRegularBuild := true + isRegularBuildCached = &isRegularBuild + return isRegularBuild + } + + // There are multiple ways to detect if the build is running in a Docker container. + // - Check with systemd-detect-virt tool first. This is the most reliable way. + // - The legacy way is to check if /.dockerenv exists. However, this is not reliable + // as it may not be present in all environments. + // - If the user has set the CHROOT_DIR environment variable, then it is likely a Docker build. + isRegularBuild := true + isDockerContainer, err := checkIfContainerSystemdDetectVirt() + if err != nil { + isRegularBuild = !checkIfContainerDockerEnvFile() + message := []string{ + "Failed to detect if the system is running in a container using systemd-detect-virt.", + err.Error(), + "Checking if the system is running in a container by checking /.dockerenv.", + } + if isRegularBuild { + message = append(message, "Result: Not a container.") + } else { + message = append(message, "Result: Container detected.") + } + logger.PrintMessageBox(logrus.WarnLevel, message) + } else { + isRegularBuild = !isDockerContainer + if !isRegularBuild { + logger.Log.Info("systemd-detect-virt reports that the tool is running in a container, running as a container build") + } + } + + // If the user set the CHROOT_DIR environment variable, but we don't detect a container, print a warning. This is + // likely a misconfiguration, however trust the user and force the build to run as a container. If this is a mistake, + // the tools should fail very quickly after this point. + if checkIfContainerChrootDirEnv() && isRegularBuild { + message := []string{ + "CHROOT_DIR is set, but the system is not detected as a container.", + "This is likely a misconfiguration!", + "**Forcing the build to run as a container build**, however chroot operations may fail.", + } + logger.PrintMessageBox(logrus.WarnLevel, message) + isRegularBuild = false + } + + // Cache the result + isRegularBuildCached = &isRegularBuild + return isRegularBuild } // GetChrootDir returns the chroot folder @@ -43,7 +159,7 @@ func GetChrootDir(proposedDir string) (chrootDir string, err error) { // In docker based pipeline pre-existing chroot pool is under a folder which path // is indicated by an env variable - chrootPoolFolder, varExist := unix.Getenv(rootBaseDirEnv) + chrootPoolFolder, varExist := os.LookupEnv(rootBaseDirEnv) if !varExist || len(chrootPoolFolder) == 0 { err = fmt.Errorf("env variable %s not defined", rootBaseDirEnv) logger.Log.Errorf("%s", err.Error()) From 29f8095ad135eaffe449acfe5fc24eb2e233bac6 Mon Sep 17 00:00:00 2001 From: Daniel McIlvaney Date: Tue, 12 Nov 2024 15:02:49 -0800 Subject: [PATCH 2/7] Add missing prereq to ubuntu --- toolkit/docs/building/prerequisites-ubuntu.md | 1 + 1 file changed, 1 insertion(+) diff --git a/toolkit/docs/building/prerequisites-ubuntu.md b/toolkit/docs/building/prerequisites-ubuntu.md index c96944df2ad..7e949957287 100644 --- a/toolkit/docs/building/prerequisites-ubuntu.md +++ b/toolkit/docs/building/prerequisites-ubuntu.md @@ -23,6 +23,7 @@ sudo apt -y install \ parted \ pigz \ openssl \ + systemd \ qemu-utils \ rpm \ tar \ From 6bb80e19c06a3a19d303c7074ef647f7369ca2de Mon Sep 17 00:00:00 2001 From: Daniel McIlvaney Date: Wed, 13 Nov 2024 14:20:00 -0800 Subject: [PATCH 3/7] Wrap new detection logic in simple tool --- toolkit/scripts/chroot.mk | 3 +- toolkit/scripts/tools.mk | 1 + .../tools/containercheck/containercheck.go | 33 +++++++++++++++++++ .../pkggen/worker/create_worker_chroot.sh | 9 ++--- 4 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 toolkit/tools/containercheck/containercheck.go diff --git a/toolkit/scripts/chroot.mk b/toolkit/scripts/chroot.mk index ec461909ee4..e43965e9e31 100644 --- a/toolkit/scripts/chroot.mk +++ b/toolkit/scripts/chroot.mk @@ -34,6 +34,7 @@ worker_chroot_rpm_paths := $(shell sed -nr $(sed_regex_full_path) < $(WORKER_CHR worker_chroot_deps := \ $(WORKER_CHROOT_MANIFEST) \ $(worker_chroot_rpm_paths) \ + $(go-containercheck) \ $(PKGGEN_DIR)/worker/create_worker_chroot.sh ifeq ($(REFRESH_WORKER_CHROOT),y) @@ -41,7 +42,7 @@ $(chroot_worker): $(worker_chroot_deps) $(depend_REBUILD_TOOLCHAIN) $(depend_TOO else $(chroot_worker): endif - $(PKGGEN_DIR)/worker/create_worker_chroot.sh $(BUILD_DIR)/worker $(WORKER_CHROOT_MANIFEST) $(TOOLCHAIN_RPMS_DIR) $(LOGS_DIR) + $(PKGGEN_DIR)/worker/create_worker_chroot.sh $(BUILD_DIR)/worker $(WORKER_CHROOT_MANIFEST) $(TOOLCHAIN_RPMS_DIR) $(go-containercheck) $(LOGS_DIR) validate-chroot: $(go-validatechroot) $(chroot_worker) $(go-validatechroot) \ diff --git a/toolkit/scripts/tools.mk b/toolkit/scripts/tools.mk index a9ab0c0383b..35d427dfb31 100644 --- a/toolkit/scripts/tools.mk +++ b/toolkit/scripts/tools.mk @@ -31,6 +31,7 @@ endif go_tool_list = \ bldtracker \ boilerplate \ + containercheck \ depsearch \ downloader \ grapher \ diff --git a/toolkit/tools/containercheck/containercheck.go b/toolkit/tools/containercheck/containercheck.go new file mode 100644 index 00000000000..f96237b9068 --- /dev/null +++ b/toolkit/tools/containercheck/containercheck.go @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Returns true (exit code 0) if the current build is a container build, false (exit code 1) otherwise + +package main + +import ( + "os" + + "github.com/microsoft/azurelinux/toolkit/tools/internal/buildpipeline" + "github.com/microsoft/azurelinux/toolkit/tools/internal/exe" + "github.com/microsoft/azurelinux/toolkit/tools/internal/logger" + + "gopkg.in/alecthomas/kingpin.v2" +) + +var ( + app = kingpin.New("containercheck", "Returns true (0) if the current build is a container build, false (1) otherwise") + logFlags = exe.SetupLogFlags(app) +) + +func main() { + app.Version(exe.ToolkitVersion) + kingpin.MustParse(app.Parse(os.Args[1:])) + logger.InitBestEffort(logFlags) + + if buildpipeline.IsRegularBuild() { + os.Exit(1) + } else { + os.Exit(0) + } +} diff --git a/toolkit/tools/pkggen/worker/create_worker_chroot.sh b/toolkit/tools/pkggen/worker/create_worker_chroot.sh index 67ec391e39d..53b44695fe4 100755 --- a/toolkit/tools/pkggen/worker/create_worker_chroot.sh +++ b/toolkit/tools/pkggen/worker/create_worker_chroot.sh @@ -10,12 +10,13 @@ set -o pipefail # $3 path to find RPMs. May be in PATH//*.rpm # $4 path to log directory -[ -n "$1" ] && [ -n "$2" ] && [ -n "$3" ] && [ -n "$4" ] || { echo "Usage: create_worker.sh <./worker_base_folder> <./path_to_rpms> <./log_dir>"; exit; } +[ -n "$1" ] && [ -n "$2" ] && [ -n "$3" ] && [ -n "$4" ] && [ -n "$4" ] || { echo "Usage: create_worker.sh <./worker_base_folder> <./path_to_rpms> <./containercheck> <./log_dir>"; exit; } chroot_base=$1 packages=$2 rpm_path=$3 -log_path=$4 +container_check_tool=$4 +log_path=$5 chroot_name="worker_chroot" chroot_builder_folder=$chroot_base/$chroot_name @@ -121,8 +122,8 @@ HOME=$ORIGINAL_HOME # In case of Docker based build do not add the below folders into chroot tarball # otherwise safechroot will fail to "untar" the tarball -DOCKERCONTAINERONLY=/.dockerenv -if [[ -f "$DOCKERCONTAINERONLY" ]]; then +if $container_check_tool; then + echo "Removing /dev, /proc, /run, /sys from chroot tarball for container based build." | tee -a "$chroot_log" rm -rf "${chroot_base:?}/$chroot_name"/dev rm -rf "${chroot_base:?}/$chroot_name"/proc rm -rf "${chroot_base:?}/$chroot_name"/run From c61f7b6566a215e5f6db8f3e37996bdbf80da726 Mon Sep 17 00:00:00 2001 From: Daniel McIlvaney Date: Wed, 13 Nov 2024 14:45:30 -0800 Subject: [PATCH 4/7] debug facl --- toolkit/scripts/utils.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolkit/scripts/utils.mk b/toolkit/scripts/utils.mk index 4cc53c4f874..937ec860ebd 100644 --- a/toolkit/scripts/utils.mk +++ b/toolkit/scripts/utils.mk @@ -100,7 +100,7 @@ $(foreach var,$(watch_vars),$(eval $(call depend_on_var,$(var)))) # to always run the "setfacl" command but not trigger a re-run of the targets # depending on this target. $(no_repo_acl): setfacl_always_run_phony - @setfacl -bnR $(PROJECT_ROOT) &>/dev/null && \ + @setfacl -bnR $(PROJECT_ROOT) && \ if [ ! -f $@ ]; then \ touch $@; \ fi From 668b447b69269ead0ce9062bb1e37aff4389eff6 Mon Sep 17 00:00:00 2001 From: Daniel McIlvaney Date: Thu, 14 Nov 2024 15:10:31 -0800 Subject: [PATCH 5/7] Update toolkit/scripts/utils.mk --- toolkit/scripts/utils.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolkit/scripts/utils.mk b/toolkit/scripts/utils.mk index 937ec860ebd..4cc53c4f874 100644 --- a/toolkit/scripts/utils.mk +++ b/toolkit/scripts/utils.mk @@ -100,7 +100,7 @@ $(foreach var,$(watch_vars),$(eval $(call depend_on_var,$(var)))) # to always run the "setfacl" command but not trigger a re-run of the targets # depending on this target. $(no_repo_acl): setfacl_always_run_phony - @setfacl -bnR $(PROJECT_ROOT) && \ + @setfacl -bnR $(PROJECT_ROOT) &>/dev/null && \ if [ ! -f $@ ]; then \ touch $@; \ fi From 4fb50b24938fc8cfdb8721d7d68e5ba3f54a2b89 Mon Sep 17 00:00:00 2001 From: Daniel McIlvaney Date: Thu, 14 Nov 2024 17:16:43 -0800 Subject: [PATCH 6/7] Address feedback --- .../internal/buildpipeline/buildpipeline.go | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/toolkit/tools/internal/buildpipeline/buildpipeline.go b/toolkit/tools/internal/buildpipeline/buildpipeline.go index d8aff554865..902f540375f 100644 --- a/toolkit/tools/internal/buildpipeline/buildpipeline.go +++ b/toolkit/tools/internal/buildpipeline/buildpipeline.go @@ -31,24 +31,24 @@ var isRegularBuildCached *bool // checkIfContainerDockerEnvFile checks if the tool is running in a Docker container by checking if /.dockerenv exists. This // check may not be reliable in all environments, so it is recommended to use systemd-detect-virt if available. -func checkIfContainerDockerEnvFile() bool { +func checkIfContainerDockerEnvFile() (bool, error) { exists, err := file.PathExists("/.dockerenv") if err != nil { - logger.Log.Errorf("Error checking /.dockerenv: %v", err) - return false + err = fmt.Errorf("failed to check if /.dockerenv exists:\n%w", err) + return false, err } - return exists + return exists, nil } // checkIfContainerIgnoreDockerEnvFile checks if the user has placed a file in the root directory to ignore the Docker // environment check. -func checkIfContainerIgnoreDockerEnvFile() bool { +func checkIfContainerIgnoreDockerEnvFile() (bool, error) { ignoreDockerEnvExists, err := file.PathExists("/.mariner-toolkit-ignore-dockerenv") if err != nil { - logger.Log.Errorf("Failed to check if /.mariner-toolkit-ignore-dockerenv exists: %s", err) - return false + err = fmt.Errorf("failed to check if /.mariner-toolkit-ignore-dockerenv exists:\n%w", err) + return false, err } - return ignoreDockerEnvExists + return ignoreDockerEnvExists, nil } // checkIfContainerChrootDirEnv checks if the user has set the CHROOT_DIR environment variable, which is a requirement for @@ -65,7 +65,7 @@ func checkIfContainerSystemdDetectVirt() (bool, error) { // was previously not explicitly required for the toolkit. _, err := exec.LookPath(systemdDetectVirtTool) if err != nil { - err = fmt.Errorf("failed to find %s in the PATH: %w", systemdDetectVirtTool, err) + err = fmt.Errorf("failed to find %s in the PATH:\n%w", systemdDetectVirtTool, err) return false, err } @@ -97,7 +97,12 @@ func IsRegularBuild() bool { } // If /.mariner-toolkit-ignore-dockerenv exists, then it is a regular build no matter what. - if checkIfContainerIgnoreDockerEnvFile() { + hasIgnoreFile, err := checkIfContainerIgnoreDockerEnvFile() + if err != nil { + // Log the error, but continue with the check. + logger.Log.Warnf("Failed to check if /.mariner-toolkit-ignore-dockerenv exists: %s", err) + } + if hasIgnoreFile { isRegularBuild := true isRegularBuildCached = &isRegularBuild return isRegularBuild @@ -111,7 +116,12 @@ func IsRegularBuild() bool { isRegularBuild := true isDockerContainer, err := checkIfContainerSystemdDetectVirt() if err != nil { - isRegularBuild = !checkIfContainerDockerEnvFile() + isContainerBuild, err := checkIfContainerDockerEnvFile() + if err != nil { + // Log the error, but continue with the check. + logger.Log.Warnf("Failed to check if /.dockerenv exists: %s", err) + } + isRegularBuild = !isContainerBuild message := []string{ "Failed to detect if the system is running in a container using systemd-detect-virt.", err.Error(), From d5b68e2ccb6a0b47601550a105717a1d42b970b8 Mon Sep 17 00:00:00 2001 From: Daniel McIlvaney Date: Fri, 15 Nov 2024 11:50:43 -0800 Subject: [PATCH 7/7] Update toolkit/tools/pkggen/worker/create_worker_chroot.sh --- toolkit/tools/pkggen/worker/create_worker_chroot.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolkit/tools/pkggen/worker/create_worker_chroot.sh b/toolkit/tools/pkggen/worker/create_worker_chroot.sh index 53b44695fe4..17bdd555856 100755 --- a/toolkit/tools/pkggen/worker/create_worker_chroot.sh +++ b/toolkit/tools/pkggen/worker/create_worker_chroot.sh @@ -10,7 +10,7 @@ set -o pipefail # $3 path to find RPMs. May be in PATH//*.rpm # $4 path to log directory -[ -n "$1" ] && [ -n "$2" ] && [ -n "$3" ] && [ -n "$4" ] && [ -n "$4" ] || { echo "Usage: create_worker.sh <./worker_base_folder> <./path_to_rpms> <./containercheck> <./log_dir>"; exit; } +[ -n "$1" ] && [ -n "$2" ] && [ -n "$3" ] && [ -n "$4" ] && [ -n "$5" ] || { echo "Usage: create_worker.sh <./worker_base_folder> <./path_to_rpms> <./containercheck> <./log_dir>"; exit; } chroot_base=$1 packages=$2