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

Update remove_existing_renders to only delete qhub related files/directories #800

Merged
merged 6 commits into from
Sep 24, 2021
Merged
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
75 changes: 23 additions & 52 deletions qhub/render/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
import functools
import json
import os
from shutil import copyfile
from gitignore_parser import parse_gitignore
from shutil import rmtree

from ruamel import yaml
from cookiecutter.generate import generate_files
Expand Down Expand Up @@ -132,8 +131,8 @@ def render_template(output_directory, config_filename, force=False):
patch_versioning_extra_config(config)

remove_existing_renders(
source_repo_dir=input_directory / "{{ cookiecutter.repo_directory }}",
dest_repo_dir=output_directory / repo_directory,
verbosity=2,
)

generate_files(
Expand All @@ -144,60 +143,32 @@ def render_template(output_directory, config_filename, force=False):
)


def remove_existing_renders(source_repo_dir, dest_repo_dir):
def remove_existing_renders(dest_repo_dir, verbosity=0):
"""
Remove existing folder structure in output_dir apart from:
Files matching gitignore entries from the source template
Anything the user has added to a .qhubignore file in the output_dir (maybe their own github workflows)

No FILES in the dest_repo_dir are deleted.

The .git folder remains intact
Remove all files and directories beneath each directory in `deletable_dirs`. These files and directories will be regenerated in the next step (`generate_files`) based on the configurations set in `qhub-config.yml`.

Inputs must be pathlib.Path
"""
copyfile(str(source_repo_dir / ".gitignore"), str(dest_repo_dir / ".gitignore"))

gitignore_matches = parse_gitignore(dest_repo_dir / ".gitignore")

if (dest_repo_dir / ".qhubignore").is_file():
qhubignore_matches = parse_gitignore(dest_repo_dir / ".qhubignore")
else:
home_dir = pathlib.Path.home()
if pathlib.Path.cwd() == home_dir:
raise ValueError(
f"Deploying QHub from the home directory, {home_dir}, is not permitted."
)

def qhubignore_matches(_):
return False # Dummy blank qhubignore

for root, dirs, files in os.walk(dest_repo_dir, topdown=False):
if (
root.startswith(f"{str(dest_repo_dir)}/.git/")
or root == f"{str(dest_repo_dir)}/.git"
):
# Leave everything in the .git folder
continue

root_path = pathlib.Path(root)

if root != str(
dest_repo_dir
): # Do not delete top-level files such as qhub-config.yaml!
for file in files:

if not gitignore_matches(root_path / file) and not qhubignore_matches(
root_path / file
):

os.remove(root_path / file)

for dir in dirs:
if (
not gitignore_matches(root_path / dir)
and not (dir == ".git" and root_path == dest_repo_dir)
and not qhubignore_matches(root_path / dir)
):
try:
os.rmdir(root_path / dir)
except OSError:
pass # Silently fail if 'saved' files are present so dir not empty
deletable_dirs = [
"terraform-state",
".github",
"infrastructure",
"image",
".gitlab-ci.yml",
]

for deletable_dir in deletable_dirs:
deletable_dir = dest_repo_dir / deletable_dir
if deletable_dir.exists():
Copy link
Member

Choose a reason for hiding this comment

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

Ah. Sorry I shouldn't have mentioned that os.remove and shutil.rmtree shouldn't be used. I meant that more generally that long term i'd like to minimize the number of times that we make this call! Since it's a scary operation.

I'm fine with using shutil.rmtree here and I think that will dramatically reduce the code here.T his should remove all the walking in the code.

Also deletable_dirs should include .gitlab-ci.yml.

Copy link
Member Author

Choose a reason for hiding this comment

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

Got it! Thanks for the feedback. And I agree, using shutil.rmtree would be preferred.

if verbosity > 0:
print(f"Deleting all files and directories beneath {deletable_dir} ...")
rmtree(deletable_dir)


def set_env_vars_in_config(config):
Expand Down
103 changes: 84 additions & 19 deletions tests/test_render.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,42 @@
import pytest

from ruamel import yaml
from ruamel.yaml import YAML

from qhub.render import render_template, set_env_vars_in_config
from qhub.render import render_template, set_env_vars_in_config, remove_existing_renders
from qhub.initialize import render_config

INIT_INPUTS = [
# project, namespace, domain, cloud_provider, ci_provider, auth_provider
("do-pytest", "dev", "do.qhub.dev", "do", "github-actions", "github"),
("aws-pytest", "dev", "aws.qhub.dev", "aws", "github-actions", "github"),
("gcp-pytest", "dev", "gcp.qhub.dev", "gcp", "github-actions", "github"),
("azure-pytest", "dev", "azure.qhub.dev", "azure", "github-actions", "github"),
]
QHUB_CONFIG_FN = "qhub-config.yaml"
PRESERVED_DIR = "preserved_dir"


@pytest.fixture
def init_render(request, tmp_path):
(
project,
namespace,
domain,
cloud_provider,
ci_provider,
auth_provider,
) = request.param

output_directory = tmp_path / f"{cloud_provider}_output_dir"
output_directory.mkdir()
qhub_config = output_directory / QHUB_CONFIG_FN

# data that should NOT be deleted when `qhub render` is called
preserved_directory = output_directory / PRESERVED_DIR
preserved_directory.mkdir()
preserved_filename = preserved_directory / "file.txt"
preserved_filename.write_text("This is a test...")

@pytest.mark.parametrize(
"project, namespace, domain, cloud_provider, ci_provider, auth_provider",
[
("do-pytest", "dev", "do.qhub.dev", "do", "github-actions", "github"),
("aws-pytest", "dev", "aws.qhub.dev", "aws", "github-actions", "github"),
("gcp-pytest", "dev", "gcp.qhub.dev", "gcp", "github-actions", "github"),
("azure-pytest", "dev", "azure.qhub.dev", "azure", "github-actions", "github"),
],
)
def test_render(
project, namespace, domain, cloud_provider, ci_provider, auth_provider, tmp_path
):
config = render_config(
project_name=project,
namespace=namespace,
Expand All @@ -32,13 +51,12 @@ def test_render(
kubernetes_version="1.18.0",
disable_prompt=True,
)
yaml = YAML(typ="unsafe", pure=True)
yaml.dump(config, qhub_config)

config_filename = tmp_path / (project + ".yaml")
with open(config_filename, "w") as f:
yaml.dump(config, f)
render_template(str(output_directory), qhub_config, force=True)

output_directory = tmp_path / "test"
render_template(str(output_directory), config_filename, force=True)
yield (output_directory, request)


def test_get_secret_config_entries(monkeypatch):
Expand Down Expand Up @@ -71,3 +89,50 @@ def test_get_secret_config_entries(monkeypatch):
config = config_orig.copy()
set_env_vars_in_config(config)
assert config == expected


@pytest.mark.parametrize(
"init_render",
INIT_INPUTS,
indirect=True,
)
def test_render_template(init_render):
output_directory, request = init_render
(
project,
namespace,
domain,
cloud_provider,
ci_provider,
auth_provider,
) = request.param
qhub_config = output_directory / QHUB_CONFIG_FN

yaml = YAML()
qhub_config_json = yaml.load(qhub_config.read_text())

assert qhub_config_json["project_name"] == project
assert qhub_config_json["namespace"] == namespace
assert qhub_config_json["domain"] == domain
assert qhub_config_json["provider"] == cloud_provider


@pytest.mark.parametrize(
"init_render",
INIT_INPUTS,
indirect=True,
)
def test_remove_existing_renders(init_render):
output_directory, request = init_render
dirs = [_.name for _ in output_directory.iterdir()]
preserved_files = [_ for _ in (output_directory / PRESERVED_DIR).iterdir()]

# test `remove_existing_renders` implicitly
assert PRESERVED_DIR in dirs
assert len(preserved_files[0].read_text()) > 0

# test `remove_existing_renders` explicitly
remove_existing_renders(output_directory)

assert PRESERVED_DIR in dirs
assert len(preserved_files[0].read_text()) > 0