Skip to content

Commit

Permalink
feat: adding init command to entrypoints (#326)
Browse files Browse the repository at this point in the history
* feat: make git arguments optional to support entrypoints that do not interact with git

* feat: adding init command entrypoint

* feat: adds logic to create provider cicd files for github

Signed-off-by: George Vauter <[email protected]>

* feat: adding unit tests for init command

Signed-off-by: George Vauter <[email protected]>

* fix: add license header and module docstring

Signed-off-by: George Vauter <[email protected]>

* feat: add templates directory

Signed-off-by: George Vauter <[email protected]>

* feat: add init command to poetry scripts

Signed-off-by: George Vauter <[email protected]>

* fix: updating workflow templates

Signed-off-by: George Vauter <[email protected]>

* fix: revert entrypoint_base and do not subclass init entrypoint

Signed-off-by: George Vauter <[email protected]>

* fix: lists should be sorted before assertion

Signed-off-by: George Vauter <[email protected]>

* fix: use importlib_resources backport for compatability with older python versions

Signed-off-by: George Vauter <[email protected]>

* fix: run workflows on push instead of pull_request

Signed-off-by: George Vauter <[email protected]>

* fix: check return code from call to compliance-trestle

Signed-off-by: George Vauter <[email protected]>

---------

Signed-off-by: George Vauter <[email protected]>
  • Loading branch information
gvauter authored Aug 22, 2024
1 parent 9666031 commit 868c1fa
Show file tree
Hide file tree
Showing 13 changed files with 580 additions and 7 deletions.
1 change: 1 addition & 0 deletions docs/contributing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{!CONTRIBUTING.md!}
1 change: 1 addition & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{!TROUBLESHOOTING.md!}
12 changes: 6 additions & 6 deletions poetry.lock

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

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ repository = 'https://github.com/RedHatProductSecurity/trestle-bot'


[tool.poetry.scripts]
trestlebot-init = "trestlebot.entrypoints.init:main"
trestlebot-autosync = "trestlebot.entrypoints.autosync:main"
trestlebot-rules-transform = "trestlebot.entrypoints.rule_transform:main"
trestlebot-create-cd = "trestlebot.entrypoints.create_cd:main"
Expand All @@ -33,6 +34,7 @@ github3-py = "^4.0.1"
python-gitlab = "^4.2.0"
ruamel-yaml = "^0.18.5"
pydantic = "^2.0.0"
importlib-resources = "^6.4.3"

[tool.poetry.group.dev.dependencies]
flake8 = "^7.0.0"
Expand Down
7 changes: 7 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ def tmp_repo() -> YieldFixture[Tuple[str, Repo]]:
clean(tmpdir, repo)


@pytest.fixture(scope="function")
def tmp_init_dir() -> YieldFixture[str]:
tmpdir = tempfile.mkdtemp(prefix=_TEST_PREFIX)
yield tmpdir
clean(tmpdir)


@pytest.fixture(scope="function")
def tmp_trestle_dir() -> YieldFixture[str]:
"""Create an initialized temporary trestle directory"""
Expand Down
8 changes: 7 additions & 1 deletion tests/testutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def configure_test_logger(level: int = logging.INFO) -> None:
configure_logger(level=level, propagate=True)


def clean(repo_path: str, repo: Optional[Repo]) -> None:
def clean(repo_path: str, repo: Optional[Repo] = None) -> None:
"""Clean up the temporary Git repository."""
if repo is not None:
repo.close()
Expand Down Expand Up @@ -270,6 +270,12 @@ def setup_rules_view(
load_from_yaml(comp_dir, "test_complete_rule_multiple_controls")


def setup_for_init(tmp_init_dir: pathlib.Path) -> None:
"""Creates an empty git repo"""
git_dir: pathlib.Path = tmp_init_dir.joinpath(pathlib.Path(".git"))
git_dir.mkdir()


def replace_string_in_file(file_path: str, old_string: str, new_string: str) -> None:
"""Replace a string in a file."""
# Read the content of the file
Expand Down
174 changes: 174 additions & 0 deletions tests/trestlebot/entrypoints/test_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2024 Red Hat, Inc.

"""Test for Init CLI entrypoint"""
import argparse
import logging
import pathlib
from typing import Dict
from unittest.mock import Mock, patch

import pytest
from trestle.common.const import TRESTLE_CONFIG_DIR, TRESTLE_KEEP_FILE
from trestle.common.file_utils import is_hidden

from tests.testutils import args_dict_to_list, configure_test_logger, setup_for_init
from trestlebot.const import GITHUB, TRESTLEBOT_CONFIG_DIR, TRESTLEBOT_KEEP_FILE
from trestlebot.entrypoints.init import InitEntrypoint
from trestlebot.entrypoints.init import main as cli_main
from trestlebot.tasks.authored import types as model_types


OSCAL_MODEL_SSP = model_types.AuthoredType.SSP.value
OSCAL_MODEL_COMPDEF = model_types.AuthoredType.COMPDEF.value


@pytest.fixture
def args_dict() -> Dict[str, str]:
return {
"working-dir": ".",
"provider": GITHUB,
"oscal-model": OSCAL_MODEL_COMPDEF,
}


@patch(
"trestlebot.entrypoints.log.configure_logger",
Mock(side_effect=configure_test_logger),
)
def test_init_fails_if_trestlebot_dir_exists(
tmp_init_dir: str, args_dict: Dict[str, str], caplog: pytest.LogCaptureFixture
) -> None:
"""Trestlebot init should fail if .trestlebot directory already exists"""
setup_for_init(pathlib.Path(tmp_init_dir))

args_dict["working-dir"] = tmp_init_dir

# Manulaly create .trestlebot dir so it already exists
trestlebot_dir = pathlib.Path(tmp_init_dir) / pathlib.Path(TRESTLEBOT_CONFIG_DIR)
trestlebot_dir.mkdir()

with patch("sys.argv", ["trestlebot", *args_dict_to_list(args_dict)]):
with pytest.raises(SystemExit, match="1"):
cli_main()

assert any(
record.levelno == logging.ERROR
and f"Initialization failed. Found existing {TRESTLEBOT_CONFIG_DIR} directory in"
in record.message
for record in caplog.records
)


@patch(
"trestlebot.entrypoints.log.configure_logger",
Mock(side_effect=configure_test_logger),
)
def test_init_if_not_git_repo(
tmp_init_dir: str, args_dict: Dict[str, str], caplog: pytest.LogCaptureFixture
) -> None:
"""Test init fails if not in git repo directory"""
args_dict["working-dir"] = tmp_init_dir
with patch("sys.argv", ["trestlebot", *args_dict_to_list(args_dict)]):
with pytest.raises(SystemExit, match="1"):
cli_main()

assert any(
record.levelno == logging.ERROR
and f"Initialization failed. Given directory {tmp_init_dir} is not a Git repository."
in record.message
for record in caplog.records
)


@patch(
"trestlebot.entrypoints.log.configure_logger",
Mock(side_effect=configure_test_logger),
)
def test_init_ssp_github(
tmp_init_dir: str, args_dict: Dict[str, str], caplog: pytest.LogCaptureFixture
) -> None:
"""Tests for expected init command directories and files"""
args_dict["working-dir"] = tmp_init_dir
args_dict["oscal-model"] = OSCAL_MODEL_SSP
args_dict["provider"] = GITHUB
setup_for_init(pathlib.Path(tmp_init_dir))
with patch("sys.argv", ["trestlebot", *args_dict_to_list(args_dict)]):
with pytest.raises(SystemExit, match="0"):
cli_main()

# .keep file should exist in .trestlebot repo
tmp_dir = pathlib.Path(tmp_init_dir)
trestlebot_dir = tmp_dir / pathlib.Path(TRESTLEBOT_CONFIG_DIR)
keep_file = trestlebot_dir / pathlib.Path(TRESTLEBOT_KEEP_FILE)
assert keep_file.exists() is True

# directories for ssp model should exist
model_dirs = [d.name for d in tmp_dir.iterdir() if not is_hidden(d)]
expected = InitEntrypoint.MODEL_DIRS[args_dict["oscal-model"]]
assert sorted(model_dirs) == sorted(expected)

# directories for github workflows should exist
workflow_dir = tmp_dir.joinpath(".github/workflows")
workflow_files = [f.name for f in workflow_dir.iterdir()]
expected = InitEntrypoint.PROVIDER_TEMPLATES[args_dict["provider"]][
args_dict["oscal-model"]
]
assert sorted(workflow_files) == sorted(expected)

assert any(
record.levelno == logging.INFO
and f"Initialized trestlebot project successfully in {tmp_init_dir}"
in record.message
for record in caplog.records
)


@patch(
"trestlebot.entrypoints.log.configure_logger",
Mock(side_effect=configure_test_logger),
)
def test_init_compdef_github(
tmp_init_dir: str, args_dict: Dict[str, str], caplog: pytest.LogCaptureFixture
) -> None:
"""Tests for expected init command directories and files"""
args_dict["working-dir"] = tmp_init_dir
args_dict["oscal-model"] = model_types.AuthoredType.COMPDEF.value
args_dict["provider"] = GITHUB
setup_for_init(pathlib.Path(tmp_init_dir))

with patch("sys.argv", ["trestlebot", *args_dict_to_list(args_dict)]):
with pytest.raises(SystemExit, match="0"):
cli_main()

# directories for compdef model should exist
tmp_dir = pathlib.Path(tmp_init_dir)
model_dirs = [d.name for d in tmp_dir.iterdir() if not is_hidden(d)]
expected = InitEntrypoint.MODEL_DIRS[args_dict["oscal-model"]]
assert sorted(model_dirs) == sorted(expected)

# directories for github workflows should exist
workflow_dir = tmp_dir.joinpath(".github/workflows")
workflow_files = [f.name for f in workflow_dir.iterdir()]
expected = InitEntrypoint.PROVIDER_TEMPLATES[args_dict["provider"]][
args_dict["oscal-model"]
]
assert sorted(workflow_files) == sorted(expected)

assert any(
record.levelno == logging.INFO
and f"Initialized trestlebot project successfully in {tmp_init_dir}"
in record.message
for record in caplog.records
)


def test_call_trestle_init(tmp_init_dir: str) -> None:
"""Tests for expected results of calling trestle init"""
parser = argparse.ArgumentParser()
args = argparse.Namespace(verbose=0, working_dir=tmp_init_dir)
InitEntrypoint(parser=parser)._call_trestle_init(args)
tmp_dir = pathlib.Path(tmp_init_dir)
trestle_dir = tmp_dir / pathlib.Path(TRESTLE_CONFIG_DIR)
keep_file = trestle_dir / pathlib.Path(TRESTLE_KEEP_FILE)
assert keep_file.exists() is True
5 changes: 5 additions & 0 deletions trestlebot/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,8 @@
GITHUB = "github"
GITLAB = "gitlab"
GITHUB_SERVER_URL = "https://github.com"
GITHUB_WORKFLOWS_DIR = ".github/workflows"

# Trestlebot init constants
TRESTLEBOT_CONFIG_DIR = ".trestlebot"
TRESTLEBOT_KEEP_FILE = ".keep"
Loading

0 comments on commit 868c1fa

Please sign in to comment.