Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backport toolkit container detection using systemd-detect-virt #11135

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions toolkit/docs/building/prerequisites-mariner.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ sudo tdnf -y install \
rpm \
rpm-build \
sudo \
systemd \
tar \
wget \
xfsprogs
Expand Down
1 change: 1 addition & 0 deletions toolkit/docs/building/prerequisites-ubuntu.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ sudo apt -y install \
parted \
pigz \
openssl \
systemd \
qemu-utils \
rpm \
tar \
Expand Down
3 changes: 2 additions & 1 deletion toolkit/scripts/chroot.mk
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,15 @@ 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)
$(chroot_worker): $(worker_chroot_deps) $(depend_REBUILD_TOOLCHAIN) $(depend_TOOLCHAIN_ARCHIVE)
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) \
Expand Down
1 change: 1 addition & 0 deletions toolkit/scripts/tools.mk
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ endif
go_tool_list = \
bldtracker \
boilerplate \
containercheck \
depsearch \
downloader \
grapher \
Expand Down
33 changes: 33 additions & 0 deletions toolkit/tools/containercheck/containercheck.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
157 changes: 146 additions & 11 deletions toolkit/tools/internal/buildpipeline/buildpipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,162 @@ package buildpipeline
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

"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 Mariner from a Docker container and
// consequently have special requirements with regards to chroot
// check if .dockerenv file exist to disambiguate build pipeline
exists, _ := file.PathExists("/.dockerenv")
return !exists
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 {
isRegularBuild = !isDockerContainer
if !isRegularBuild {
logger.Log.Info("systemd-detect-virt reports that the tool is running in a container, running as a container build")
}
} else {
// Fallback if systemd-detect-virt isn't available.
systemdErrMsg := err.Error()
isDockerContainer, err = checkIfContainerDockerEnvFile()
if err != nil {
// Log the error, but continue with the check.
logger.Log.Warnf("Failed to check if /.dockerenv exists: %s", err)
} else {
isRegularBuild = !isDockerContainer
}
message := []string{
"Failed to detect if the system is running in a container using systemd-detect-virt.",
systemdErrMsg,
"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 is not available in 2.0, so print each line separately.
for _, line := range message {
logger.Log.Warn(line)
}
}

// 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 is not available in 2.0, so print each line separately.
for _, line := range message {
logger.Log.Warn(line)
}
isRegularBuild = false
}

// Cache the result
isRegularBuildCached = &isRegularBuild
return isRegularBuild
}

// GetChrootDir returns the chroot folder
Expand All @@ -42,7 +177,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())
Expand Down
9 changes: 5 additions & 4 deletions toolkit/tools/pkggen/worker/create_worker_chroot.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ set -o pipefail
# $3 path to find RPMs. May be in PATH/<arch>/*.rpm
# $4 path to log directory

[ -n "$1" ] && [ -n "$2" ] && [ -n "$3" ] && [ -n "$4" ] || { echo "Usage: create_worker.sh <./worker_base_folder> <rpms_to_install.txt> <./path_to_rpms> <./log_dir>"; exit; }
[ -n "$1" ] && [ -n "$2" ] && [ -n "$3" ] && [ -n "$4" ] && [ -n "$5" ] || { echo "Usage: create_worker.sh <./worker_base_folder> <rpms_to_install.txt> <./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
Expand Down Expand Up @@ -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
Expand Down
Loading