Skip to content

Commit

Permalink
RHOAIENG-16517: chore(tests): add sandboxing so that Dockerfile build…
Browse files Browse the repository at this point in the history
…s can only access files we know they access (#803)
  • Loading branch information
jiridanek authored Dec 11, 2024
1 parent 059a835 commit 33f95dd
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 20 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/build-notebooks-TEMPLATE.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ jobs:

- uses: actions/checkout@v4

- run: mkdir -p $TMPDIR

# for bin/buildinputs in scripts/sandbox.py
- uses: actions/setup-go@v5
with:
cache-dependency-path: "**/*.sum"

- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ define build_image
$(eval BUILD_ARGS := --build-arg BASE_IMAGE=$(BASE_IMAGE_NAME)),
$(eval BUILD_ARGS :=)
)
$(CONTAINER_ENGINE) build $(CONTAINER_BUILD_CACHE_ARGS) --tag $(IMAGE_NAME) --file $(2)/Dockerfile $(BUILD_ARGS) $(ROOT_DIR)/
$(ROOT_DIR)/scripts/sandbox.py --dockerfile '$(2)/Dockerfile' -- \
$(CONTAINER_ENGINE) build $(CONTAINER_BUILD_CACHE_ARGS) --tag $(IMAGE_NAME) --file '$(2)/Dockerfile' $(BUILD_ARGS) {}\;
endef

# Push function for the notebok image:
Expand Down
3 changes: 2 additions & 1 deletion ci/cached-builds/gha_lvm_overlay.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ set -Eeuo pipefail
# This script creates file-backed volumes on /dev/root and /dev/sda1, then creates ext4 over both, and mounts it for our use
# https://github.com/easimon/maximize-build-space/blob/master/action.yml

root_reserve_mb=2048
# root_reserve_mb=2048 was running out of disk space building cuda images
root_reserve_mb=4096
temp_reserve_mb=100
swap_size_mb=4096

Expand Down
47 changes: 29 additions & 18 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ python = "~3.12"
[tool.poetry.group.dev.dependencies]
pytest = "^8.2.2"
pytest-subtests = "^0.12.1"
pyfakefs = "^5.7.2"

[build-system]
requires = ["poetry-core"]
Expand Down
77 changes: 77 additions & 0 deletions scripts/sandbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#! /usr/bin/env python3

import argparse
import logging
import pathlib
import shutil
import subprocess
import sys
import tempfile
import json
from typing import cast

ROOT_DIR = pathlib.Path(__file__).parent.parent
MAKE = shutil.which("gmake") or shutil.which("make")

logging.basicConfig()
logging.root.name = pathlib.Path(__file__).name


class Args(argparse.Namespace):
dockerfile: pathlib.Path
remaining: list[str]


def main() -> int:
p = argparse.ArgumentParser(allow_abbrev=False)
p.add_argument("--dockerfile", type=pathlib.Path, required=True)
p.add_argument('remaining', nargs=argparse.REMAINDER)

args = cast(Args, p.parse_args())

print(f"{__file__=} started with {args=}")

if not args.remaining or args.remaining[0] != "--":
print("must specify command to execute after double dashes at the end, such as `-- command --args ...`")
return 1
if not "{};" in args.remaining:
print("must give a `{};` parameter that will be replaced with new build context")
return 1

if not (ROOT_DIR / "bin/buildinputs").exists():
subprocess.check_call([MAKE, "bin/buildinputs"], cwd=ROOT_DIR)
stdout = subprocess.check_output([ROOT_DIR / "bin/buildinputs", str(args.dockerfile)],
text=True, cwd=ROOT_DIR)
prereqs = [pathlib.Path(file) for file in json.loads(stdout)] if stdout != "\n" else []
print(f"{prereqs=}")

with tempfile.TemporaryDirectory(delete=True) as tmpdir:
setup_sandbox(prereqs, pathlib.Path(tmpdir))
command = [arg if arg != "{};" else tmpdir for arg in args.remaining[1:]]
print(f"running {command=}")
try:
subprocess.check_call(command)
except subprocess.CalledProcessError as err:
logging.error("Failed to execute process, see errors logged above ^^^")
return err.returncode
return 0


def setup_sandbox(prereqs: list[pathlib.Path], tmpdir: pathlib.Path):
# always adding .gitignore
gitignore = ROOT_DIR / ".gitignore"
if gitignore.exists():
shutil.copy(gitignore, tmpdir)

for dep in prereqs:
if dep.is_absolute():
dep = dep.relative_to(ROOT_DIR)
if dep.is_dir():
shutil.copytree(dep, tmpdir / dep, symlinks=False, dirs_exist_ok=True)
else:
(tmpdir / dep.parent).mkdir(parents=True, exist_ok=True)
shutil.copy(dep, tmpdir / dep.parent)


if __name__ == '__main__':
sys.exit(main())
55 changes: 55 additions & 0 deletions scripts/sandbox_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#! /usr/bin/env python3

import glob
import logging
import pathlib
import sys
import tempfile

import pyfakefs.fake_filesystem

from scripts.sandbox import setup_sandbox

ROOT_DIR = pathlib.Path(__file__).parent.parent

logging.basicConfig()
logging.root.name = pathlib.Path(__file__).name

class TestSandbox:
def test_filesystem_file(self, fs: pyfakefs.fake_filesystem.FakeFilesystem):
pathlib.Path("a").write_text("a")

with tempfile.TemporaryDirectory(delete=True) as tmpdir:
setup_sandbox([pathlib.Path("a")], pathlib.Path(tmpdir))
assert (pathlib.Path(tmpdir) / "a").is_file()

def test_filesystem_dir_with_file(self, fs: pyfakefs.fake_filesystem.FakeFilesystem):
pathlib.Path("a/").mkdir()
pathlib.Path("a/b").write_text("b")

with tempfile.TemporaryDirectory(delete=True) as tmpdir:
setup_sandbox([pathlib.Path("a")], pathlib.Path(tmpdir))
assert (pathlib.Path(tmpdir) / "a").is_dir()
assert (pathlib.Path(tmpdir) / "a" / "b").is_file()

def test_filesystem_dir_with_dir_with_file(self, fs: pyfakefs.fake_filesystem.FakeFilesystem):
pathlib.Path("a/b").mkdir(parents=True)
pathlib.Path("a/b/c").write_text("c")

with tempfile.TemporaryDirectory(delete=True) as tmpdir:
setup_sandbox([pathlib.Path("a")], pathlib.Path(tmpdir))
assert (pathlib.Path(tmpdir) / "a").is_dir()
assert (pathlib.Path(tmpdir) / "a" / "b").is_dir()
assert (pathlib.Path(tmpdir) / "a" / "b" / "c").is_file()

def test_filesystem_file_in_dir_in_dir(self, fs: pyfakefs.fake_filesystem.FakeFilesystem):
pathlib.Path("a/b").mkdir(parents=True)
pathlib.Path("a/b/c").write_text("c")

with tempfile.TemporaryDirectory(delete=True) as tmpdir:
setup_sandbox([pathlib.Path("a/b/c")], pathlib.Path(tmpdir))
for g in glob.glob("**/*", recursive=True):
logging.debug("%s", g)
assert (pathlib.Path(tmpdir) / "a").is_dir()
assert (pathlib.Path(tmpdir) / "a" / "b").is_dir()
assert (pathlib.Path(tmpdir) / "a" / "b" / "c").is_file()

0 comments on commit 33f95dd

Please sign in to comment.