Skip to content

Commit 15b043a

Browse files
authored
Backport toolkit container detection using systemd-detect-virt (#11135)
1 parent 5c14b0e commit 15b043a

7 files changed

Lines changed: 166 additions & 16 deletions

File tree

toolkit/docs/building/prerequisites-mariner.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ sudo tdnf -y install \
3030
rpm \
3131
rpm-build \
3232
sudo \
33+
systemd \
3334
tar \
3435
wget \
3536
xfsprogs

toolkit/docs/building/prerequisites-ubuntu.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ sudo apt -y install \
2222
parted \
2323
pigz \
2424
openssl \
25+
systemd \
2526
qemu-utils \
2627
rpm \
2728
tar \

toolkit/scripts/chroot.mk

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,15 @@ worker_chroot_rpm_paths := $(shell sed -nr $(sed_regex_full_path) < $(worker_chr
3838
worker_chroot_deps := \
3939
$(worker_chroot_manifest) \
4040
$(worker_chroot_rpm_paths) \
41+
$(go-containercheck) \
4142
$(PKGGEN_DIR)/worker/create_worker_chroot.sh
4243
4344
ifeq ($(REFRESH_WORKER_CHROOT),y)
4445
$(chroot_worker): $(worker_chroot_deps) $(depend_REBUILD_TOOLCHAIN) $(depend_TOOLCHAIN_ARCHIVE)
4546
else
4647
$(chroot_worker):
4748
endif
48-
$(PKGGEN_DIR)/worker/create_worker_chroot.sh $(BUILD_DIR)/worker $(worker_chroot_manifest) $(TOOLCHAIN_RPMS_DIR) $(LOGS_DIR)
49+
$(PKGGEN_DIR)/worker/create_worker_chroot.sh $(BUILD_DIR)/worker $(worker_chroot_manifest) $(TOOLCHAIN_RPMS_DIR) $(go-containercheck) $(LOGS_DIR)
4950
5051
validate-chroot: $(go-validatechroot) $(chroot_worker)
5152
$(go-validatechroot) \

toolkit/scripts/tools.mk

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ endif
3131
go_tool_list = \
3232
bldtracker \
3333
boilerplate \
34+
containercheck \
3435
depsearch \
3536
downloader \
3637
grapher \
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
// Returns true (exit code 0) if the current build is a container build, false (exit code 1) otherwise
5+
6+
package main
7+
8+
import (
9+
"os"
10+
11+
"github.com/microsoft/azurelinux/toolkit/tools/internal/buildpipeline"
12+
"github.com/microsoft/azurelinux/toolkit/tools/internal/exe"
13+
"github.com/microsoft/azurelinux/toolkit/tools/internal/logger"
14+
15+
"gopkg.in/alecthomas/kingpin.v2"
16+
)
17+
18+
var (
19+
app = kingpin.New("containercheck", "Returns true (0) if the current build is a container build, false (1) otherwise")
20+
logFlags = exe.SetupLogFlags(app)
21+
)
22+
23+
func main() {
24+
app.Version(exe.ToolkitVersion)
25+
kingpin.MustParse(app.Parse(os.Args[1:]))
26+
logger.InitBestEffort(logFlags)
27+
28+
if buildpipeline.IsRegularBuild() {
29+
os.Exit(1)
30+
} else {
31+
os.Exit(0)
32+
}
33+
}

toolkit/tools/internal/buildpipeline/buildpipeline.go

Lines changed: 123 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,139 @@ package buildpipeline
88
import (
99
"fmt"
1010
"os"
11+
"os/exec"
1112
"path/filepath"
13+
"strings"
14+
15+
"golang.org/x/sys/unix"
1216

1317
"github.com/microsoft/azurelinux/toolkit/tools/internal/file"
1418
"github.com/microsoft/azurelinux/toolkit/tools/internal/logger"
15-
16-
"golang.org/x/sys/unix"
19+
"github.com/microsoft/azurelinux/toolkit/tools/internal/shell"
1720
)
1821

1922
const (
20-
rootBaseDirEnv = "CHROOT_DIR"
21-
chrootLock = "chroot-pool.lock"
22-
chrootUse = "chroot-used"
23+
rootBaseDirEnv = "CHROOT_DIR"
24+
chrootLock = "chroot-pool.lock"
25+
chrootUse = "chroot-used"
26+
systemdDetectVirtTool = "systemd-detect-virt"
2327
)
2428

29+
var isRegularBuildCached *bool
30+
31+
// checkIfContainerDockerEnvFile checks if the tool is running in a Docker container by checking if /.dockerenv exists. This
32+
// check may not be reliable in all environments, so it is recommended to use systemd-detect-virt if available.
33+
func checkIfContainerDockerEnvFile() (bool, error) {
34+
exists, err := file.PathExists("/.dockerenv")
35+
if err != nil {
36+
err = fmt.Errorf("failed to check if /.dockerenv exists:\n%w", err)
37+
return false, err
38+
}
39+
return exists, nil
40+
}
41+
42+
// checkIfContainerIgnoreDockerEnvFile checks if the user has placed a file in the root directory to ignore the Docker
43+
// environment check.
44+
func checkIfContainerIgnoreDockerEnvFile() (bool, error) {
45+
ignoreDockerEnvExists, err := file.PathExists("/.mariner-toolkit-ignore-dockerenv")
46+
if err != nil {
47+
err = fmt.Errorf("failed to check if /.mariner-toolkit-ignore-dockerenv exists:\n%w", err)
48+
return false, err
49+
}
50+
return ignoreDockerEnvExists, nil
51+
}
52+
53+
// checkIfContainerSystemdDetectVirt uses systemd-detect-virt, a tool that can be used to detect if the system is running
54+
// in a virtualized environment. More specifically, using '-c' flag will detect container-based virtualization only.
55+
func checkIfContainerSystemdDetectVirt() (bool, error) {
56+
// We should have the systemd-detect-virt command available in the environment, but check for it just in case since it
57+
// was previously not explicitly required for the toolkit.
58+
_, err := exec.LookPath(systemdDetectVirtTool)
59+
if err != nil {
60+
err = fmt.Errorf("failed to find %s in the PATH:\n%w", systemdDetectVirtTool, err)
61+
return false, err
62+
}
63+
64+
// The tool will return error code 1 based on detection, we only care about the stdout so ignore the return code.
65+
stdout, _, _ := shell.Execute(systemdDetectVirtTool, "-c")
66+
67+
// There are several possible outputs from systemd-detect-virt we care about:
68+
// - none: Not running in a virtualized environment, easy
69+
// - wsl: Reports as a container, but we don't want to treat it as such. It should be able to handle regular builds
70+
// - anything else: We'll assume it's a container
71+
stdout = strings.TrimSpace(stdout)
72+
switch stdout {
73+
case "none":
74+
logger.Log.Debugf("Tool is not running in a container, systemd-detect-virt reports: '%s'", stdout)
75+
return false, nil
76+
case "wsl":
77+
logger.Log.Debugf("Tool is running in WSL, treating as a non-container environment, systemd-detect-virt reports: '%s'", stdout)
78+
return false, nil
79+
default:
80+
logger.Log.Debugf("Tool is running in a container, systemd-detect-virt reports: '%s'", stdout)
81+
return true, nil
82+
}
83+
}
84+
2585
// IsRegularBuild indicates if it is a regular build (without using docker)
2686
func IsRegularBuild() bool {
27-
// some specific build pipeline builds Mariner from a Docker container and
28-
// consequently have special requirements with regards to chroot
29-
// check if .dockerenv file exist to disambiguate build pipeline
30-
exists, _ := file.PathExists("/.dockerenv")
31-
return !exists
87+
if isRegularBuildCached != nil {
88+
return *isRegularBuildCached
89+
}
90+
91+
// If /.mariner-toolkit-ignore-dockerenv exists, then it is a regular build no matter what.
92+
hasIgnoreFile, err := checkIfContainerIgnoreDockerEnvFile()
93+
if err != nil {
94+
// Log the error, but continue with the check.
95+
logger.Log.Warnf("Failed to check if /.mariner-toolkit-ignore-dockerenv exists: %s", err)
96+
}
97+
if hasIgnoreFile {
98+
isRegularBuild := true
99+
isRegularBuildCached = &isRegularBuild
100+
return isRegularBuild
101+
}
102+
103+
// There are multiple ways to detect if the build is running in a Docker container.
104+
// - Check with systemd-detect-virt tool first. This is the most reliable way.
105+
// - The legacy way is to check if /.dockerenv exists. However, this is not reliable
106+
// as it may not be present in all environments.
107+
// - If the user has set the CHROOT_DIR environment variable, then it is likely a Docker build.
108+
isRegularBuild := true
109+
isDockerContainer, err := checkIfContainerSystemdDetectVirt()
110+
if err == nil {
111+
isRegularBuild = !isDockerContainer
112+
if !isRegularBuild {
113+
logger.Log.Info("systemd-detect-virt reports that the tool is running in a container, running as a container build")
114+
}
115+
} else {
116+
// Fallback if systemd-detect-virt isn't available.
117+
systemdErrMsg := err.Error()
118+
isDockerContainer, err = checkIfContainerDockerEnvFile()
119+
if err != nil {
120+
// Log the error, but continue with the check.
121+
logger.Log.Warnf("Failed to check if /.dockerenv exists: %s", err)
122+
} else {
123+
isRegularBuild = !isDockerContainer
124+
}
125+
message := []string{
126+
"Failed to detect if the system is running in a container using systemd-detect-virt.",
127+
systemdErrMsg,
128+
"Checking if the system is running in a container by checking /.dockerenv.",
129+
}
130+
if isRegularBuild {
131+
message = append(message, "Result: Not a container.")
132+
} else {
133+
message = append(message, "Result: Container detected.")
134+
}
135+
// logger.PrintMessageBox is not available in 2.0, so print each line separately.
136+
for _, line := range message {
137+
logger.Log.Warn(line)
138+
}
139+
}
140+
141+
// Cache the result
142+
isRegularBuildCached = &isRegularBuild
143+
return isRegularBuild
32144
}
33145

34146
// GetChrootDir returns the chroot folder
@@ -42,7 +154,7 @@ func GetChrootDir(proposedDir string) (chrootDir string, err error) {
42154

43155
// In docker based pipeline pre-existing chroot pool is under a folder which path
44156
// is indicated by an env variable
45-
chrootPoolFolder, varExist := unix.Getenv(rootBaseDirEnv)
157+
chrootPoolFolder, varExist := os.LookupEnv(rootBaseDirEnv)
46158
if !varExist || len(chrootPoolFolder) == 0 {
47159
err = fmt.Errorf("env variable %s not defined", rootBaseDirEnv)
48160
logger.Log.Errorf("%s", err.Error())

toolkit/tools/pkggen/worker/create_worker_chroot.sh

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ set -o pipefail
1010
# $3 path to find RPMs. May be in PATH/<arch>/*.rpm
1111
# $4 path to log directory
1212

13-
[ -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; }
13+
[ -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; }
1414

1515
chroot_base=$1
1616
packages=$2
1717
rpm_path=$3
18-
log_path=$4
18+
container_check_tool=$4
19+
log_path=$5
1920

2021
chroot_name="worker_chroot"
2122
chroot_builder_folder=$chroot_base/$chroot_name
@@ -121,8 +122,8 @@ HOME=$ORIGINAL_HOME
121122

122123
# In case of Docker based build do not add the below folders into chroot tarball
123124
# otherwise safechroot will fail to "untar" the tarball
124-
DOCKERCONTAINERONLY=/.dockerenv
125-
if [[ -f "$DOCKERCONTAINERONLY" ]]; then
125+
if $container_check_tool; then
126+
echo "Removing /dev, /proc, /run, /sys from chroot tarball for container based build." | tee -a "$chroot_log"
126127
rm -rf "${chroot_base:?}/$chroot_name"/dev
127128
rm -rf "${chroot_base:?}/$chroot_name"/proc
128129
rm -rf "${chroot_base:?}/$chroot_name"/run

0 commit comments

Comments
 (0)