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/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 \ 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/internal/buildpipeline/buildpipeline.go b/toolkit/tools/internal/buildpipeline/buildpipeline.go index 873c937ef00..902f540375f 100644 --- a/toolkit/tools/internal/buildpipeline/buildpipeline.go +++ b/toolkit/tools/internal/buildpipeline/buildpipeline.go @@ -8,28 +8,154 @@ 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, error) { + exists, err := file.PathExists("/.dockerenv") + if err != nil { + err = fmt.Errorf("failed to check if /.dockerenv exists:\n%w", err) + return false, err + } + 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, error) { + ignoreDockerEnvExists, err := file.PathExists("/.mariner-toolkit-ignore-dockerenv") + if err != nil { + err = fmt.Errorf("failed to check if /.mariner-toolkit-ignore-dockerenv exists:\n%w", err) + return false, err + } + return ignoreDockerEnvExists, nil +} + +// 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:\n%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. + 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 + } + + // 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 { + 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(), + "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 +169,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()) diff --git a/toolkit/tools/pkggen/worker/create_worker_chroot.sh b/toolkit/tools/pkggen/worker/create_worker_chroot.sh index 67ec391e39d..17bdd555856 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 "$5" ] || { 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