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

Implement parameters to pass custom ignore file #1325

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
26 changes: 26 additions & 0 deletions repo2docker/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging
import os
import sys
from pathlib import Path

from . import __version__
from .app import Repo2Docker
Expand Down Expand Up @@ -282,6 +283,22 @@ def get_argparser():
help=Repo2Docker.engine.help,
)

argparser.add_argument(
"--extra-ignore-file",
dest="extra_ignore_file",
type=Path,
help=Repo2Docker.extra_ignore_file.help,
)

argparser.add_argument(
"--ignore-file-strategy",
dest="ignore_file_strategy",
type=str,
choices=Repo2Docker.ignore_file_strategy.values,
default=Repo2Docker.ignore_file_strategy.default_value,
help=Repo2Docker.ignore_file_strategy.help,
)

return argparser


Expand Down Expand Up @@ -464,6 +481,15 @@ def make_r2d(argv=None):
if args.target_repo_dir:
r2d.target_repo_dir = args.target_repo_dir

if args.extra_ignore_file is not None:
if not args.extra_ignore_file.exists():
print(f"The ignore file {args.extra_ignore_file} does not exist")
sys.exit(1)
r2d.extra_ignore_file = str(args.extra_ignore_file.resolve())

if args.ignore_file_strategy is not None:
r2d.ignore_file_strategy = args.ignore_file_strategy

return r2d


Expand Down
33 changes: 32 additions & 1 deletion repo2docker/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@
import entrypoints
import escapism
from pythonjsonlogger import jsonlogger
from traitlets import Any, Bool, Dict, Int, List, Unicode, default, observe
from traitlets import Any, Bool, Dict, Enum, Int, List, Unicode, default, observe
from traitlets.config import Application

from . import __version__, contentproviders
from .buildpacks import (
CondaBuildPack,
DockerBuildPack,
ExcludesStrategy,
JuliaProjectTomlBuildPack,
JuliaRequireBuildPack,
LegacyBinderDockerBuildPack,
Expand Down Expand Up @@ -462,6 +463,32 @@ def _dry_run_changed(self, change):
""",
)

extra_ignore_file = Unicode(
"",
config=True,
help="""
Path to an additional .dockerignore or .containerignore file to be applied
when building an image.

Depending on the strategy selected the content of the file will replace,
be merged or be ignored.
""",
)

ignore_file_strategy = Enum(
ExcludesStrategy.values(),
config=True,
default_value=ExcludesStrategy.THEIRS.value,
help="""
Strategy to use if an extra ignore file is passed:
- merge means that the content of the extra ignore file will be merged
with the ignore file contained in the repository (if any)
- ours means that the extra ignore file content will be used in any case
- theirs means that if there is an ignore file in the repository, the
extra ignore file will not be used.
""",
)

def get_engine(self):
"""Return an instance of the container engine.

Expand Down Expand Up @@ -860,6 +887,10 @@ def build(self):
self.cache_from,
self.extra_build_kwargs,
platform=self.platform,
extra_ignore_file=self.extra_ignore_file,
ignore_file_strategy=ExcludesStrategy(
self.ignore_file_strategy
),
):
if docker_client.string_output:
self.log.info(l, extra=dict(phase=R2dState.BUILDING))
Expand Down
2 changes: 1 addition & 1 deletion repo2docker/buildpacks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .base import BaseImage, BuildPack
from .base import BaseImage, BuildPack, ExcludesStrategy
from .conda import CondaBuildPack
from .docker import DockerBuildPack
from .julia import JuliaProjectTomlBuildPack, JuliaRequireBuildPack
Expand Down
52 changes: 38 additions & 14 deletions repo2docker/buildpacks/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import sys
import tarfile
import textwrap
from enum import Enum
from functools import lru_cache

import escapism
Expand Down Expand Up @@ -205,6 +206,16 @@
DEFAULT_NB_UID = 1000


class ExcludesStrategy(Enum):
THEIRS = "theirs"
OURS = "ours"
MERGE = "merge"

@classmethod
def values(cls):
return [item.value for item in cls]


class BuildPack:
"""
A composable BuildPack.
Expand Down Expand Up @@ -582,6 +593,8 @@ def build(
cache_from,
extra_build_kwargs,
platform=None,
extra_ignore_file=None,
ignore_file_strategy=ExcludesStrategy.THEIRS,
):
tarf = io.BytesIO()
tar = tarfile.open(fileobj=tarf, mode="w")
Expand Down Expand Up @@ -609,24 +622,35 @@ def _filter_tar(tarinfo):
for fname in ("repo2docker-entrypoint", "python3-login"):
tar.add(os.path.join(HERE, fname), fname, filter=_filter_tar)

exclude = []
def _read_excludes(filepath):
with open(filepath) as ignore_file:
cleaned_lines = [
line.strip() for line in ignore_file.read().splitlines()
]
return [line for line in cleaned_lines if line != "" and line[0] != "#"]

extra_excludes = []
if extra_ignore_file:
extra_excludes = _read_excludes(extra_ignore_file)

excludes = []
for ignore_file_name in [".dockerignore", ".containerignore"]:
ignore_file_name = self.binder_path(ignore_file_name)
if os.path.exists(ignore_file_name):
with open(ignore_file_name) as ignore_file:
cleaned_lines = [
line.strip() for line in ignore_file.read().splitlines()
]
exclude.extend(
[
line
for line in cleaned_lines
if line != "" and line[0] != "#"
]
)

files_to_add = exclude_paths(".", exclude)
excludes.extend(_read_excludes(ignore_file_name))

if extra_ignore_file is not None:
if ignore_file_strategy == ExcludesStrategy.OURS:
excludes = extra_excludes
elif ignore_file_strategy == ExcludesStrategy.MERGE:
excludes.extend(extra_excludes)
else:
# ignore means that if an ignore file exist, its content is used
# otherwise, the extra exclude
if not excludes:
excludes = extra_excludes

files_to_add = exclude_paths(".", excludes)

if files_to_add:
for item in files_to_add:
Expand Down
4 changes: 3 additions & 1 deletion repo2docker/buildpacks/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import docker

from .base import BuildPack
from .base import BuildPack, ExcludesStrategy


class DockerBuildPack(BuildPack):
Expand All @@ -31,6 +31,8 @@ def build(
cache_from,
extra_build_kwargs,
platform=None,
extra_ignore_file=None,
ignore_file_strategy=ExcludesStrategy.THEIRS,
):
"""Build a Docker image based on the Dockerfile in the source repo."""
# If you work on this bit of code check the corresponding code in
Expand Down
2 changes: 2 additions & 0 deletions tests/conda/ignore-file
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Docker compatible ignore file
from-extra-ignore
1 change: 1 addition & 0 deletions tests/conda/py311-extra-ignore-file-merge/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from-dockerignore
2 changes: 2 additions & 0 deletions tests/conda/py311-extra-ignore-file-merge/environment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dependencies:
- python=3.11
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Must be ignored from .dockerignore file
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Must be ignored from extra ignore file
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# This file is respected by repo2docker's test suite, but not repo2docker
# itself. It is used solely to help us test repo2docker's command line flags.
#
- --extra-ignore-file=tests/conda/ignore-file
- --ignore-file-strategy=merge
6 changes: 6 additions & 0 deletions tests/conda/py311-extra-ignore-file-merge/verify
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env python

import pathlib

assert not pathlib.Path("from-dockerignore").exists()
assert not pathlib.Path("from-extra-ignore").exists()
1 change: 1 addition & 0 deletions tests/conda/py311-extra-ignore-file-ours/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from-dockerignore
2 changes: 2 additions & 0 deletions tests/conda/py311-extra-ignore-file-ours/environment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dependencies:
- python=3.11
1 change: 1 addition & 0 deletions tests/conda/py311-extra-ignore-file-ours/from-dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Must not be ignored because of ours strategy and extra ignore file does not contain it.
1 change: 1 addition & 0 deletions tests/conda/py311-extra-ignore-file-ours/from-extra-ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Must be ignored
5 changes: 5 additions & 0 deletions tests/conda/py311-extra-ignore-file-ours/test-extra-args.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# This file is respected by repo2docker's test suite, but not repo2docker
# itself. It is used solely to help us test repo2docker's command line flags.
#
- --extra-ignore-file=tests/conda/ignore-file
- --ignore-file-strategy=ours
6 changes: 6 additions & 0 deletions tests/conda/py311-extra-ignore-file-ours/verify
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env python

import pathlib

assert pathlib.Path("from-dockerignore").exists()
assert not pathlib.Path("from-extra-ignore").exists()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dependencies:
- python=3.11
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
No docker ignore so should still appear
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Must be ignored because of extra ignore file
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# This file is respected by repo2docker's test suite, but not repo2docker
# itself. It is used solely to help us test repo2docker's command line flags.
#
- --extra-ignore-file=tests/conda/ignore-file
- --ignore-file-strategy=theirs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env python

import pathlib

assert pathlib.Path("from-dockerignore").exists()
assert not pathlib.Path("from-extra-ignore").exists()
1 change: 1 addition & 0 deletions tests/conda/py311-extra-ignore-file-theirs/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from-dockerignore
2 changes: 2 additions & 0 deletions tests/conda/py311-extra-ignore-file-theirs/environment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dependencies:
- python=3.11
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Must be ignored from .dockerignore file
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Shall be present due to strategy being theirs and this file does not appear in .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# This file is respected by repo2docker's test suite, but not repo2docker
# itself. It is used solely to help us test repo2docker's command line flags.
#
- --extra-ignore-file=tests/conda/ignore-file
- --ignore-file-strategy=theirs
6 changes: 6 additions & 0 deletions tests/conda/py311-extra-ignore-file-theirs/verify
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env python

import pathlib

assert not pathlib.Path("from-dockerignore").exists()
assert pathlib.Path("from-extra-ignore").exists()
5 changes: 5 additions & 0 deletions tests/unit/test_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,8 @@ def test_config_priority(tmp_path, trait, arg, default):
assert getattr(r2d, trait) == "config"
r2d = make_r2d(["--config", config_file, arg, "cli", "."])
assert getattr(r2d, trait) == "cli"


def test_non_existing_exclude_file():
with pytest.raises(SystemExit):
make_r2d(["--extra-ignore-file", "does-not-exist"])
25 changes: 25 additions & 0 deletions tests/unit/test_argumentvalidation.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,28 @@ def test_docker_no_build_success(temp_cwd):
args_list = ["--no-build", "--no-run"]

assert validate_arguments(builddir, args_list, disable_dockerd=True)


@pytest.mark.parametrize(
"strategy, is_valid",
[
("theirs", True),
("ours", True),
("merge", True),
("invalid", False),
],
)
def test_ignore_file_strategy(temp_cwd, strategy, is_valid):
""" """

args_list = ["--no-build", "--no-run", "--ignore-file-strategy", strategy]

assert (
validate_arguments(
builddir,
args_list,
"--ignore-file-strategy: invalid choice: 'invalid' (choose from 'theirs', 'ours', 'merge')",
disable_dockerd=True,
)
== is_valid
)
Loading