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

Add run_isolated and pass_docker_extra_args to docker hook #3952

Merged
merged 90 commits into from
Mar 17, 2024
Merged
Show file tree
Hide file tree
Changes from 66 commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
865c83a
fix logic
Jan 2, 2024
636c67c
better -u
Jan 2, 2024
12c7b3a
conftest copy fix
Jan 7, 2024
b6c0074
metadata only in_pack
Jan 7, 2024
52cbc16
add run in cwd
Jan 8, 2024
811d7d8
update entry
Jan 8, 2024
df905e0
cwd
Jan 8, 2024
bdb623a
no -w
Jan 8, 2024
a001cd0
add support quiet mode
Jan 8, 2024
48ba8fb
add use_args to run files
Jan 8, 2024
1fec4d7
fix to str
Jan 8, 2024
145a4d4
try change hook
Jan 9, 2024
df7265d
add base path
Jan 9, 2024
03c1a9a
fixes
Jan 9, 2024
51c3fc2
correct way to wd
Jan 9, 2024
8e662f9
performance
Jan 9, 2024
8fadcdd
1000:4000
Jan 9, 2024
8b0cfee
check if faster without exclude
Jan 9, 2024
8e5c718
bigger cache
Jan 9, 2024
4c3ac03
default cache
Jan 9, 2024
4c071bc
fix
Jan 9, 2024
07768c9
back to MP
Jan 9, 2024
aa0774e
try pop exclude
Jan 9, 2024
dc84978
another try
Jan 9, 2024
e4393b5
disable MP for test
Jan 9, 2024
ac72ac2
fix
Jan 9, 2024
bb343af
add support to network and isolated
Jan 10, 2024
dfb4bfd
populate in pack
Jan 10, 2024
66696b7
try rm=false
Jan 11, 2024
0ab2404
remove container only in gitlab
Jan 12, 2024
5271826
fix typo
Jan 14, 2024
1fdcdab
Fix DockerHook entrypoint command in pre-commit hook
Jan 14, 2024
d75a7bf
remove comment
Jan 16, 2024
a86f967
Merge remote-tracking branch 'origin/master' into pytest-runner
Jan 16, 2024
c5f1c95
CR
Jan 17, 2024
dce24a8
remove network feature and rename
Jan 17, 2024
912263a
add test
Jan 17, 2024
99680ac
use 4000
Jan 17, 2024
01cea5f
tmp file
Jan 18, 2024
b69788a
Add pytest-junit folder to pre-commit context
Jan 18, 2024
c94d1f3
add checks
Feb 1, 2024
ce31d04
move load dotenv
Feb 1, 2024
60fbc5c
Merge remote-tracking branch 'origin/master' into pytest-runner
Feb 20, 2024
0a374c9
add flags to precommit
Feb 20, 2024
fa1f2a6
use cpu count
Feb 20, 2024
5b09049
split by chunks
Feb 20, 2024
0e2e4ae
Refactor pre-commit context initialization
Feb 20, 2024
9b883dc
filter None
Feb 20, 2024
cd54a5b
Remove support_require_isolated parameter from _split_by_objects func…
Feb 21, 2024
b845feb
Refactor DockerHook class to support pack ignore config
Feb 21, 2024
0bb77df
support pack ignore config
Feb 21, 2024
8c40fe8
use threadpool executor
Feb 21, 2024
5e3d3e1
lower to 300
Feb 21, 2024
7848edf
add logs
Feb 21, 2024
fd54b1b
debug
Feb 21, 2024
bbed5c8
update docker to new design
Feb 21, 2024
2ab5ab6
fix leftovers
Feb 21, 2024
f07d146
absolute config file path
Feb 21, 2024
5b4b71a
Update DockerHook class to use "pass_extra_docker_args" instead of "p…
Feb 22, 2024
972af9e
back to pass_docker_extra_args
Feb 22, 2024
d1dec05
add comments
Feb 25, 2024
8874f77
Add support for hook arguments
Feb 25, 2024
04269f2
Update demisto_sdk/commands/common/docker_helper.py
ilaner Feb 25, 2024
b4a5fbd
added integration batch
Feb 25, 2024
fa6181c
fix lru cache
Feb 25, 2024
411b849
version parse
Feb 26, 2024
16d52bd
Update demisto_sdk/commands/pre_commit/hooks/docker.py
ilaner Feb 28, 2024
e43fd9d
fix
Feb 28, 2024
5012410
Merge branch 'pytest-runner' of github.com:demisto/demisto-sdk into p…
Feb 28, 2024
53d24e2
CR
Feb 28, 2024
7d40490
rn
Feb 28, 2024
922f9f1
Merge remote-tracking branch 'origin/master' into pytest-runner
Feb 28, 2024
5a3c098
moded .precommit
Feb 28, 2024
142ba15
Remove unnecessary code and update paths for pre-commit folder
Feb 28, 2024
548cfe9
Refactor pre-commit setup and merge pytest reports***
Feb 28, 2024
31b6082
mode
Feb 28, 2024
43c383c
fix
Feb 28, 2024
dc3c3ee
fix typo
Feb 28, 2024
a05d9c1
Create shared folders for coverage and pytest-junit
Feb 28, 2024
cc9edf5
Refactor folder creation in PreCommitContext
Feb 28, 2024
a8c43cd
try
Feb 28, 2024
5617450
or 4000
Feb 29, 2024
b103059
try with .
Feb 29, 2024
599cda9
try .
Feb 29, 2024
0943a45
change permissions on pre-commit folder
Feb 29, 2024
9713aac
mkdir
Feb 29, 2024
6370e15
Update permissions on pre-commit folder
Feb 29, 2024
2694167
another try
Feb 29, 2024
d4d298b
without -u?
Feb 29, 2024
7a24e3d
CI as it was
Feb 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 0 additions & 3 deletions demisto_sdk/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,6 @@ def main(ctx, config, version, release_notes, **kwargs):
handle_deprecated_args(ctx.args)

config.configuration = Configuration()
import dotenv

dotenv.load_dotenv(CONTENT_PATH / ".env", override=True) # type: ignore # load .env file from the cwd
Copy link
Contributor

Choose a reason for hiding this comment

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

why did you remove it completely?


if platform.system() == "Windows":
logger.warning(
Expand Down
12 changes: 11 additions & 1 deletion demisto_sdk/commands/common/docker_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import requests
import urllib3
from docker.types import Mount
from packaging.version import Version
from packaging.version import InvalidVersion, Version
from requests import JSONDecodeError
from requests.exceptions import RequestException

Expand Down Expand Up @@ -159,6 +159,16 @@ def __init__(self):
def __del__(self):
del self.tmp_dir_name

@staticmethod
@functools.lru_cache
def version() -> Version:
version = init_global_docker_client().version()["Version"]
try:
return Version(version)
except InvalidVersion:
# we don't care about the build number
GuyAfik marked this conversation as resolved.
Show resolved Hide resolved
return Version(version.split("-")[0])

def installation_files(self, container_type: str) -> FILES_SRC_TARGET:
files = self._files_to_push_on_installation.copy()
files.append((self.installation_scripts[container_type], "/install.sh"))
Expand Down
3 changes: 0 additions & 3 deletions demisto_sdk/commands/content_graph/objects/content_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,6 @@ def in_pack(self) -> Optional["Pack"]:
Returns:
Pack: Pack model.
"""
# This function converts the pack attribute, which is a parser object to the pack model
# This happens since we cant mark the pack type as `Pack` because it is a forward reference.
# When upgrading to pydantic v2, remove this method and change pack type to `Pack` directly.
pack = self.pack
if not pack or isinstance(pack, fields.FieldInfo):
pack = None
Expand Down
121 changes: 89 additions & 32 deletions demisto_sdk/commands/pre_commit/hooks/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,23 @@
import subprocess
import time
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor
from copy import deepcopy
from functools import lru_cache
from pathlib import Path
from typing import Dict, Iterable, List, Optional, Set, Tuple

from docker.errors import DockerException
from packaging.version import Version

from demisto_sdk.commands.common.constants import TYPE_PWSH, TYPE_PYTHON
from demisto_sdk.commands.common.constants import (
TYPE_PWSH,
TYPE_PYTHON,
)
from demisto_sdk.commands.common.content_constant_paths import CONTENT_PATH, PYTHONPATH
from demisto_sdk.commands.common.cpu_count import cpu_count
from demisto_sdk.commands.common.docker_helper import (
DockerBase,
docker_login,
get_docker,
init_global_docker_client,
Expand Down Expand Up @@ -153,15 +160,15 @@ def get_environment_flag(env: dict) -> str:
def _split_by_objects(
files_with_objects: List[Tuple[Path, IntegrationScript]],
config_arg: Optional[Tuple],
split_by_obj: bool = False,
run_isolated: bool = False,
) -> Dict[Optional[IntegrationScript], Set[Tuple[Path, IntegrationScript]]]:
"""
Will group files into groups that share the same configuration file.
If there is no config file, they get set to the NO_CONFIG_VALUE group
Args:
files: the files to split
config_arg: a tuple, argument_name, file_name
split_by_obj: a boolean. If true it will split all the objects into separate hooks.
run_isolated: a boolean. If true it will split all the objects into separate hooks.

Returns:
a dict where the keys are the names of the folder of the config and the value is a set of files for that config
Expand All @@ -171,8 +178,9 @@ def _split_by_objects(
] = defaultdict(set)

for file, obj in files_with_objects:
if split_by_obj or (config_arg and (obj.path.parent / config_arg[1]).exists()):
if run_isolated or (config_arg and (obj.path.parent / config_arg[1]).exists()):
Copy link
Contributor

Choose a reason for hiding this comment

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

extract confiug_arg[1] to a variable called file_name, would be much more readable IMO

object_to_files[obj].add((file, obj))

else:
object_to_files[NO_SPLIT].add((file, obj))

Expand All @@ -184,6 +192,38 @@ class DockerHook(Hook):
This class will make common manipulations on commands that need to run in docker
"""

def clean_args_from_hook(self, hooks: List[Dict]):
"""This clean unsupported args from the generated hooks

Args:
hooks (List[Dict]): The hooks generated
"""
for hook in hooks:
hook.pop("docker_image", None)
hook.pop("config_file_arg", None)
hook.pop("copy_files", None)
hook.pop("run_isolated", None)
hook.pop("pass_docker_extra_args", None)

def process_image(
self,
image: str,
files_with_objects: List[Tuple[Path, IntegrationScript]],
config_arg: Optional[Tuple],
run_isolated: bool,
) -> List[Dict]:
Copy link
Contributor

Choose a reason for hiding this comment

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

add docstring

object_to_files = _split_by_objects(
files_with_objects,
config_arg,
run_isolated,
)
image_is_powershell = any(obj.is_powershell for _, obj in files_with_objects)
ilaner marked this conversation as resolved.
Show resolved Hide resolved

dev_image = devtest_image(image, image_is_powershell, self.context.dry_run)
hooks = self.generate_hooks(dev_image, image, object_to_files, config_arg)
logger.debug(f"Generated {len(hooks)} hooks for image {image}")
return hooks

def prepare_hook(
self,
):
Expand Down Expand Up @@ -222,39 +262,39 @@ def prepare_hook(
for file in copy_files:
source: Path = CONTENT_PATH / file
target = obj.path.parent / Path(file).name
if source != target and source.exists():
if source != target and source.exists() and not target.exists():
shutil.copy(
CONTENT_PATH / file, obj.path.parent / Path(file).name
)
split_by_obj = self._get_property("split_by_object", False)
run_isolated = self._get_property("run_isolated", False)
config_arg = self._get_config_file_arg()
start_time = time.time()
logger.debug(f"{len(tag_to_files_objs)} images were collected from files")
logger.debug(f'collected images: {" ".join(tag_to_files_objs.keys())}')
for image, files_with_objects in sorted(
tag_to_files_objs.items(), key=lambda item: item[0]
):
object_to_files = _split_by_objects(
files_with_objects, config_arg, split_by_obj
)
image_is_powershell = any(
obj.is_powershell for _, obj in files_with_objects
)

dev_image = devtest_image(image, image_is_powershell, self.context.dry_run)
hooks = self.get_new_hooks(
dev_image,
image,
object_to_files,
config_arg,
)
with ThreadPoolExecutor(max_workers=cpu_count()) as executor:
Copy link
Contributor

Choose a reason for hiding this comment

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

arent we not pulling here? Whats the point of the threadpool?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we make requests to dockerhub api to check if the image is availible.

results = []
for image, files_objs in sorted(
tag_to_files_objs.items(), key=lambda item: item[0]
):
results.append(
executor.submit(
self.process_image,
image,
files_objs,
config_arg,
run_isolated,
)
)
for result in results:
hooks = result.result()
self.hooks.extend(hooks)

end_time = time.time()
logger.debug(
f"DockerHook - prepared images in {round(end_time - start_time, 2)} seconds"
)

def get_new_hooks(
def generate_hooks(
self,
dev_image,
image,
Expand All @@ -270,6 +310,7 @@ def get_new_hooks(
image: name of the base image (for naming)
object_to_files_with_objects: A dict where the key is the object (or None) and value is the set of files to run together.
config_arg: The config arg to set where relevant. This will be appended to the end of "args"

Returns:
All the hooks to be appended for this image
"""
Expand All @@ -278,24 +319,35 @@ def get_new_hooks(
new_hook["name"] = f"{new_hook.get('name')}-{image}"
new_hook["language"] = "docker_image"
env = new_hook.pop("env", {})
docker_version = DockerBase.version()
quiet = True
# quiet mode is silently pull the image, and it is supported only above 19.03
ilaner marked this conversation as resolved.
Show resolved Hide resolved
if docker_version < Version("19.03"):
quiet = False
docker_extra_args = self._get_property("pass_docker_extra_args", "")
new_hook[
"entry"
] = f'--entrypoint {new_hook.get("entry")} {get_environment_flag(env)} --quiet {dev_image}'
] = f'--entrypoint {new_hook.get("entry")} {docker_extra_args} {get_environment_flag(env)} {"--quiet" if quiet else ""} -u 4000:4000 {dev_image}'
ret_hooks = []
for (
integration_script,
files_with_objects,
) in object_to_files_with_objects.items():
change_working_directory = False
files = {file for file, _ in files_with_objects}
hook = deepcopy(new_hook)
if integration_script is not None:
change_working_directory = (
True # isolate container, so run in the same directory
)
if config_arg:
args = deepcopy(self._get_property("args", []))
args.extend(
[
config_arg[0],
str(
(
Path("/src")
/ (
integration_script.path.parent / config_arg[1]
).relative_to(CONTENT_PATH)
),
Expand All @@ -308,17 +360,22 @@ def get_new_hooks(
hook[
"name"
] = f"{hook['name']}-{integration_script.object_id}" # for uniqueness
# change the working directory to the integration script, as it runs in an isolated container
hook[
"entry"
] = f"-w {Path('/src') / integration_script.path.parent.relative_to(CONTENT_PATH)} {hook['entry']}"

if self._set_files_on_hook(
hook, files, should_filter=False
hook,
files,
should_filter=False,
use_args=change_working_directory,
base_path=Path("/src"),
): # no need to filter again, we have only filtered files
# disable multiprocessing on hook
hook["require_serial"] = True
ret_hooks.append(hook)
for hook in ret_hooks:
hook.pop("docker_image", None)
hook.pop("config_file_arg", None)
hook.pop("copy_files", None)
hook.pop("split_by_object", None)
self.clean_args_from_hook(ret_hooks)
return ret_hooks

def _get_config_file_arg(self) -> Optional[Tuple]:
Expand Down
17 changes: 15 additions & 2 deletions demisto_sdk/commands/pre_commit/hooks/hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from packaging.version import Version

from demisto_sdk.commands.common.content_constant_paths import CONTENT_PATH
from demisto_sdk.commands.common.logger import logger
from demisto_sdk.commands.pre_commit.hooks.utils import get_property
from demisto_sdk.commands.pre_commit.pre_commit_context import PreCommitContext
Expand Down Expand Up @@ -44,21 +45,33 @@ def exclude_irrelevant_files(self):
self._exclude_hooks_by_support_level()

def _set_files_on_hook(
self, hook: dict, files: Iterable[Path], should_filter: bool = True
self,
hook: dict,
files: Iterable[Path],
should_filter: bool = True,
use_args: bool = False,
base_path: Path = CONTENT_PATH,
ilaner marked this conversation as resolved.
Show resolved Hide resolved
) -> int:
"""

Args:
hook: mutates the hook with files returned from filter_files_matching_hook_config
files: the list of files to set on the hook

use_args: if True, the files will be added to the args of the hook, and pass_filenames will be set to False
Returns: the number of files that ultimately are set on the hook. Use this to decide if to run the hook at all

"""
files_to_run_on_hook = set(files)
if should_filter:
files_to_run_on_hook = self.filter_files_matching_hook_config(files)
hook["files"] = join_files(files_to_run_on_hook)
if use_args:
Copy link
Contributor

Choose a reason for hiding this comment

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

whats this do?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

because we run the script not in content working directory we will have to provide the filepaths in the args and mark pass_filenames to false (because the filenames are relative to content)

if "args" not in hook:
hook["args"] = []
hook["args"].extend(
(str(base_path / file) for file in files_to_run_on_hook)
)
hook["pass_filenames"] = False
return len(files_to_run_on_hook)

def filter_files_matching_hook_config(
Expand Down
15 changes: 10 additions & 5 deletions demisto_sdk/commands/pre_commit/pre_commit_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from pathlib import Path
from typing import Dict, Iterable, List, Optional, Set, Tuple

import more_itertools
from packaging.version import Version

from demisto_sdk.commands.common.constants import (
Expand Down Expand Up @@ -45,6 +46,7 @@
SKIPPED_HOOKS = {"format", "validate", "secrets"}

INTEGRATION_SCRIPT_REGEX = re.compile(r"^Packs/.*/(?:Integrations|Scripts)/.*.yml$")
INTEGRATIONS_BATCH = 300


class PreCommitRunner:
Expand Down Expand Up @@ -345,11 +347,14 @@ def group_by_language(
infra_files.append(file)

language_to_files: Dict[str, Set] = defaultdict(set)
with multiprocessing.Pool() as pool:
integrations_scripts = pool.map(
BaseContent.from_path, integrations_scripts_mapping.keys()
)

integrations_scripts = []
for integration_script_paths in more_itertools.chunked_even(
integrations_scripts_mapping.keys(), INTEGRATIONS_BATCH
):
with multiprocessing.Pool(processes=cpu_count()) as pool:
integrations_scripts.extend(
pool.map(BaseContent.from_path, integration_script_paths)
)
exclude_integration_script = set()
for integration_script in integrations_scripts:
if not integration_script or not isinstance(
Expand Down
6 changes: 4 additions & 2 deletions demisto_sdk/commands/pre_commit/pre_commit_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,12 @@ def __post_init__(self):
"""
shutil.rmtree(PRECOMMIT_FOLDER, ignore_errors=True)
PRECOMMIT_FOLDER.mkdir(parents=True)
(PRECOMMIT_FOLDER / "coverage").mkdir()
(PRECOMMIT_FOLDER / "pytest-junit").mkdir()
PRECOMMIT_CONFIG.mkdir()
PRECOMMIT_DOCKER_CONFIGS.mkdir()

self.precommit_template = get_file_or_remote(PRECOMMIT_TEMPLATE_PATH)
self.precommit_template: dict = get_file_or_remote(PRECOMMIT_TEMPLATE_PATH) # type: ignore[assignment]
GuyAfik marked this conversation as resolved.
Show resolved Hide resolved
remote_config_file = get_remote_file(str(PRECOMMIT_TEMPLATE_PATH))
if remote_config_file and remote_config_file != self.precommit_template:
logger.info(
Expand Down Expand Up @@ -108,7 +110,7 @@ def python_version_to_files(self) -> Dict[str, Set[Path]]:
def support_level_to_files(self) -> Dict[str, Set[Path]]:
support_level_to_files = defaultdict(set)
for path, obj in self.files_to_run_with_objects:
if obj:
if obj is not None:
support_level_to_files[obj.support_level].add(path)
return support_level_to_files

Expand Down