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

Toolkit: Use systemd-detect-virt instead of /.dockerenv to detect container builds. #11039

Open
wants to merge 7 commits into
base: 3.0-dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 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 @@ -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)
}
}
140 changes: 128 additions & 12 deletions toolkit/tools/internal/buildpipeline/buildpipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
dmcilvaney marked this conversation as resolved.
Show resolved Hide resolved
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)
dmcilvaney marked this conversation as resolved.
Show resolved Hide resolved
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{
Copy link
Contributor

@jslobodzian jslobodzian Nov 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note sure what "PrintMessageBox" is doing, but it seems to me the code would be a little simpler here if we just emitted the "Failed to detect" message before we call "checkIfContainerDockerEnvFile" first, then composed the result

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PrintMessageBox is a really obvious warning to the user with big borders. I try to add these when I think there is a non-obvious thing that the user should fix that might cause confusing errors later. I think I see what you mean with the ordering though, I'll give it a go.

"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
Expand All @@ -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())
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 "$4" ] || { echo "Usage: create_worker.sh <./worker_base_folder> <rpms_to_install.txt> <./path_to_rpms> <./containercheck> <./log_dir>"; exit; }
dmcilvaney marked this conversation as resolved.
Show resolved Hide resolved

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