Skip to content

Commit

Permalink
Toolkit: Use systemd-detect-virt instead of /.dockerenv to detect con…
Browse files Browse the repository at this point in the history
…tainer builds. (#11039)
  • Loading branch information
dmcilvaney authored Nov 20, 2024
1 parent 2a82335 commit 1159d26
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 17 deletions.
1 change: 1 addition & 0 deletions toolkit/docs/building/prerequisites-mariner.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,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 @@ -23,6 +23,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 @@ -34,14 +34,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)
}
}
153 changes: 141 additions & 12 deletions toolkit/tools/internal/buildpipeline/buildpipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,157 @@ 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 {
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(logrus.WarnLevel, message)
}

// 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
Expand All @@ -43,7 +172,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

0 comments on commit 1159d26

Please sign in to comment.