Skip to content

Commit 1159d26

Browse files
authored
Toolkit: Use systemd-detect-virt instead of /.dockerenv to detect container builds. (#11039)
1 parent 2a82335 commit 1159d26

7 files changed

Lines changed: 184 additions & 17 deletions

File tree

toolkit/docs/building/prerequisites-mariner.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ sudo tdnf -y install \
3232
rpm \
3333
rpm-build \
3434
sudo \
35+
systemd \
3536
tar \
3637
wget \
3738
xfsprogs \

toolkit/docs/building/prerequisites-ubuntu.md

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

toolkit/scripts/chroot.mk

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,15 @@ worker_chroot_rpm_paths := $(shell sed -nr $(sed_regex_full_path) < $(WORKER_CHR
3434
worker_chroot_deps := \
3535
$(WORKER_CHROOT_MANIFEST) \
3636
$(worker_chroot_rpm_paths) \
37+
$(go-containercheck) \
3738
$(PKGGEN_DIR)/worker/create_worker_chroot.sh
3839
3940
ifeq ($(REFRESH_WORKER_CHROOT),y)
4041
$(chroot_worker): $(worker_chroot_deps) $(depend_REBUILD_TOOLCHAIN) $(depend_TOOLCHAIN_ARCHIVE)
4142
else
4243
$(chroot_worker):
4344
endif
44-
$(PKGGEN_DIR)/worker/create_worker_chroot.sh $(BUILD_DIR)/worker $(WORKER_CHROOT_MANIFEST) $(TOOLCHAIN_RPMS_DIR) $(LOGS_DIR)
45+
$(PKGGEN_DIR)/worker/create_worker_chroot.sh $(BUILD_DIR)/worker $(WORKER_CHROOT_MANIFEST) $(TOOLCHAIN_RPMS_DIR) $(go-containercheck) $(LOGS_DIR)
4546
4647
validate-chroot: $(go-validatechroot) $(chroot_worker)
4748
$(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: 141 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,157 @@ package buildpipeline
88
import (
99
"fmt"
1010
"os"
11+
"os/exec"
1112
"path/filepath"
13+
"strings"
14+
15+
"github.com/sirupsen/logrus"
16+
"golang.org/x/sys/unix"
1217

1318
"github.com/microsoft/azurelinux/toolkit/tools/internal/file"
1419
"github.com/microsoft/azurelinux/toolkit/tools/internal/logger"
15-
16-
"golang.org/x/sys/unix"
20+
"github.com/microsoft/azurelinux/toolkit/tools/internal/shell"
1721
)
1822

1923
const (
20-
rootBaseDirEnv = "CHROOT_DIR"
21-
chrootLock = "chroot-pool.lock"
22-
chrootUse = "chroot-used"
24+
rootBaseDirEnv = "CHROOT_DIR"
25+
chrootLock = "chroot-pool.lock"
26+
chrootUse = "chroot-used"
27+
systemdDetectVirtTool = "systemd-detect-virt"
2328
)
2429

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

35164
// GetChrootDir returns the chroot folder
@@ -43,7 +172,7 @@ func GetChrootDir(proposedDir string) (chrootDir string, err error) {
43172

44173
// In docker based pipeline pre-existing chroot pool is under a folder which path
45174
// is indicated by an env variable
46-
chrootPoolFolder, varExist := unix.Getenv(rootBaseDirEnv)
175+
chrootPoolFolder, varExist := os.LookupEnv(rootBaseDirEnv)
47176
if !varExist || len(chrootPoolFolder) == 0 {
48177
err = fmt.Errorf("env variable %s not defined", rootBaseDirEnv)
49178
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)