Skip to content

Commit e8288a4

Browse files
Merge pull request #271 from lintermansjens/feat/previewconfig-target-path-support
feat: support previewConfig.target.path
2 parents 1fd7dd2 + 0ab381e commit e8288a4

File tree

10 files changed

+179
-17
lines changed

10 files changed

+179
-17
lines changed

docs/getting-started.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ The GitOps CLI provides several commands which can be used to perform typical op
44

55
```
66
usage: gitopscli [-h]
7-
{deploy,sync-apps,add-pr-comment,create-preview,delete-preview,version}
7+
{deploy,sync-apps,add-pr-comment,create-preview,create-pr-preview,delete-preview,delete-pr-preview,version}
88
...
99
1010
GitOps CLI
@@ -13,13 +13,15 @@ options:
1313
-h, --help show this help message and exit
1414
1515
commands:
16-
{deploy,sync-apps,add-pr-comment,create-preview,delete-preview,version}
16+
{deploy,sync-apps,add-pr-comment,create-preview,create-pr-preview,delete-preview,delete-pr-preview,version}
1717
deploy Trigger a new deployment by changing YAML values
1818
sync-apps Synchronize applications (= every directory) from apps
1919
config repository to apps root config
2020
add-pr-comment Create a comment on the pull request
2121
create-preview Create a preview environment
22+
create-pr-preview Create a preview environment for a pull request
2223
delete-preview Delete a preview environment
24+
delete-pr-preview Delete a preview environment for a pull request
2325
version Show the GitOps CLI version information
2426
```
2527

docs/includes/preview-configuration.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
## Configuration
22
### Preview Templates
33

4-
You have to provide a folder with the deployment configuration templates for every application you want to use this command for. By default it is assumed that this folder is located in your *deployment config repository* under the top-level folder `.preview-templates`. For example `.preview-templates/app-xy` for your app `app-xy`. The `create-preview` command simply copies this directory to the root of your *deployment config repository* and replaces e.g. image tag and route host which are specific to this preview.
4+
You have to provide a folder with the deployment configuration templates for every application you want to use this command for.
5+
6+
By default it is assumed that this folder is located in your *deployment config repository* under the top-level folder `.preview-templates`. For example `.preview-templates/app-xy` for your app `app-xy`. The `create-preview` command simply copies this directory to your *deployment config repository* and replaces e.g. image tag and route host which are specific to this preview.
7+
8+
By default previews are created in the repository root. If `previewConfig.target.path` is set, previews are created below that path instead.
59

610
```
711
deployment-config-repo/
@@ -44,6 +48,7 @@ previewConfig:
4448
target:
4549
organisation: deployments
4650
repository: deployment-config-repo
51+
# path: custom/${APPLICATION_NAME} # optional (defaults to repo's root directory)
4752
# branch: master # optional (defaults to repo's default branch)
4853
# namespace: ${APPLICATION_NAME}-${PREVIEW_ID_HASH}-preview' # optional (default: '${APPLICATION_NAME}-${PREVIEW_ID}-${PREVIEW_ID_HASH_SHORT}-preview',
4954
# Invalid characters in PREVIEW_ID will be replaced. PREVIEW_ID will be

gitopscli/commands/create_preview.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,13 @@ def __create_preview_from_template_if_not_existing(
127127
target_git_repo: GitRepo,
128128
gitops_config: GitOpsConfig,
129129
) -> bool:
130-
preview_namespace = gitops_config.get_preview_namespace(self.__args.preview_id)
131-
full_preview_folder_path = target_git_repo.get_full_file_path(preview_namespace)
130+
preview_folder_path = gitops_config.get_preview_folder_path(self.__args.preview_id)
131+
full_preview_folder_path = target_git_repo.get_full_file_path(preview_folder_path)
132132
preview_env_already_exist = Path(full_preview_folder_path).is_dir()
133133
if preview_env_already_exist:
134-
logging.info("Use existing folder for preview: %s", preview_namespace)
134+
logging.info("Use existing folder for preview: %s", preview_folder_path)
135135
return False
136-
logging.info("Create new folder for preview: %s", preview_namespace)
136+
logging.info("Create new folder for preview: %s", preview_folder_path)
137137
full_preview_template_folder_path = template_git_repo.get_full_file_path(gitops_config.preview_template_path)
138138
if not Path(full_preview_template_folder_path).is_dir():
139139
raise GitOpsException(f"The preview template folder does not exist: {gitops_config.preview_template_path}")
@@ -143,15 +143,15 @@ def __create_preview_from_template_if_not_existing(
143143

144144
def __replace_values(self, git_repo: GitRepo, gitops_config: GitOpsConfig) -> bool:
145145
preview_id = self.__args.preview_id
146-
preview_folder_name = gitops_config.get_preview_namespace(self.__args.preview_id)
146+
preview_folder_path = gitops_config.get_preview_folder_path(self.__args.preview_id)
147147
context = GitOpsConfig.Replacement.PreviewContext(gitops_config, preview_id, self.__args.git_hash)
148148
any_value_replaced = False
149149
for file, replacements in gitops_config.replacements.items():
150150
for replacement in replacements:
151151
replacement_value = replacement.get_value(context)
152152
value_replaced = self.__update_yaml_file(
153153
git_repo,
154-
f"{preview_folder_name}/{file}",
154+
f"{preview_folder_path}/{file}",
155155
replacement.path,
156156
replacement_value,
157157
)

gitopscli/commands/delete_preview.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,13 @@ def execute(self) -> None:
3737
with GitRepo(preview_target_git_repo_api) as preview_target_git_repo:
3838
preview_target_git_repo.clone(gitops_config.preview_target_branch)
3939

40-
preview_namespace = gitops_config.get_preview_namespace(preview_id)
41-
logging.info("Preview folder name: %s", preview_namespace)
40+
preview_folder_path = gitops_config.get_preview_folder_path(preview_id)
41+
logging.info("Preview folder: %s", preview_folder_path)
4242

43-
preview_folder_exists = self.__delete_folder_if_exists(preview_target_git_repo, preview_namespace)
43+
preview_folder_exists = self.__delete_folder_if_exists(preview_target_git_repo, preview_folder_path)
4444
if not preview_folder_exists:
4545
if self.__args.expect_preview_exists:
46-
raise GitOpsException(f"There was no preview with name: {preview_namespace}")
46+
raise GitOpsException(f"There was no preview at path: {preview_folder_path}")
4747
logging.info(
4848
"No preview environment for '%s' and preview id '%s'. I'm done here.",
4949
gitops_config.application_name,

gitopscli/gitops_config.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,18 @@ def get_value(self, context: PreviewContext) -> str:
6969
preview_target_branch: str | None
7070
preview_target_namespace_template: str
7171
preview_target_max_namespace_length: int
72+
preview_target_path_template: str
7273

7374
replacements: dict[str, list[Replacement]]
7475

7576
@property
7677
def preview_template_path(self) -> str:
7778
return self.preview_template_path_template.replace("${APPLICATION_NAME}", self.application_name)
7879

80+
@property
81+
def preview_target_path(self) -> str:
82+
return self.preview_target_path_template.replace("${APPLICATION_NAME}", self.application_name)
83+
7984
def __post_init__(self) -> None:
8085
assert isinstance(self.application_name, str), "application_name of wrong type!"
8186
assert isinstance(self.preview_host_template, str), "preview_host_template of wrong type!"
@@ -106,6 +111,8 @@ def __post_init__(self) -> None:
106111
int,
107112
), "preview_target_max_namespace_length of wrong type!"
108113
assert self.preview_target_max_namespace_length >= 1, "preview_target_max_namespace_length is < 1!"
114+
assert isinstance(self.preview_target_path_template, str), "preview_target_path_template of wrong type!"
115+
self.__assert_variables(self.preview_target_path_template, {"APPLICATION_NAME"})
109116
assert isinstance(self.replacements, dict), "replacements of wrong type!"
110117
for file, replacements in self.replacements.items():
111118
assert isinstance(file, str), f"replacement file '{file}' of wrong type!"
@@ -120,6 +127,12 @@ def get_preview_host(self, preview_id: str) -> str:
120127
preview_host = preview_host.replace("${PREVIEW_ID}", self.__sanitize(preview_id))
121128
return preview_host.replace("${PREVIEW_NAMESPACE}", self.get_preview_namespace(preview_id))
122129

130+
def get_preview_folder_path(self, preview_id: str) -> str:
131+
preview_namespace = self.get_preview_namespace(preview_id)
132+
if self.preview_target_path:
133+
return f"{self.preview_target_path}/{preview_namespace}"
134+
return preview_namespace
135+
123136
def get_preview_namespace(self, preview_id: str) -> str:
124137
preview_namespace = self.preview_target_namespace_template
125138
preview_namespace = preview_namespace.replace("${APPLICATION_NAME}", self.application_name)
@@ -325,6 +338,7 @@ def __parse_v0(self) -> GitOpsConfig:
325338
preview_target_branch=None, # use default branch
326339
preview_target_namespace_template="${APPLICATION_NAME}-${PREVIEW_ID_HASH}-preview",
327340
preview_target_max_namespace_length=63,
341+
preview_target_path_template="",
328342
replacements=replacements,
329343
)
330344

@@ -364,6 +378,9 @@ def add_var_dollar(template: str) -> str:
364378
),
365379
),
366380
preview_target_max_namespace_length=63,
381+
preview_target_path_template=add_var_dollar(
382+
self.__get_string_value_or_default("previewConfig.target.path", ""),
383+
),
367384
replacements=replacements,
368385
)
369386

@@ -439,5 +456,9 @@ def __parse_v2(self) -> GitOpsConfig:
439456
"${APPLICATION_NAME}-${PREVIEW_ID}-${PREVIEW_ID_HASH_SHORT}-preview",
440457
),
441458
preview_target_max_namespace_length=preview_target_max_namespace_length,
459+
preview_target_path_template=self.__get_string_value_or_default(
460+
"previewConfig.target.path",
461+
"",
462+
),
442463
replacements=replacements,
443464
)

tests/commands/test_create_preview.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ def setUp(self):
7878
preview_target_branch=None,
7979
preview_target_namespace_template="my-app-${PREVIEW_ID_HASH}-preview",
8080
preview_target_max_namespace_length=50,
81+
preview_target_path_template="",
8182
replacements={
8283
"Chart.yaml": [GitOpsConfig.Replacement(path="name", value_template="${PREVIEW_NAMESPACE}")],
8384
"values.yaml": [
@@ -231,6 +232,7 @@ def test_create_new_preview_from_same_template_target_repo(self):
231232
preview_target_branch=gitops_config.preview_target_branch,
232233
preview_target_namespace_template=gitops_config.preview_target_namespace_template,
233234
preview_target_max_namespace_length=gitops_config.preview_target_max_namespace_length,
235+
preview_target_path_template=gitops_config.preview_target_path_template,
234236
replacements=gitops_config.replacements,
235237
)
236238

@@ -600,3 +602,50 @@ def test_create_new_preview_invalid_chart_template(self):
600602
"/tmp/target-repo/my-app-685912d3-preview/Chart.yaml", "name", "my-app-685912d3-preview"
601603
),
602604
]
605+
606+
def test_create_new_preview_with_target_path(self):
607+
gitops_config: GitOpsConfig = self.load_gitops_config_mock.return_value
608+
self.load_gitops_config_mock.return_value = GitOpsConfig(
609+
api_version=gitops_config.api_version,
610+
application_name=gitops_config.application_name,
611+
messages_created_template=gitops_config.messages_created_template,
612+
messages_updated_template=gitops_config.messages_updated_template,
613+
messages_uptodate_template=gitops_config.messages_uptodate_template,
614+
preview_host_template=gitops_config.preview_host_template,
615+
preview_template_organisation=gitops_config.preview_template_organisation,
616+
preview_template_repository=gitops_config.preview_template_repository,
617+
preview_template_path_template=gitops_config.preview_template_path_template,
618+
preview_template_branch=gitops_config.preview_template_branch,
619+
preview_target_organisation=gitops_config.preview_target_organisation,
620+
preview_target_repository=gitops_config.preview_target_repository,
621+
preview_target_branch=gitops_config.preview_target_branch,
622+
preview_target_namespace_template=gitops_config.preview_target_namespace_template,
623+
preview_target_max_namespace_length=gitops_config.preview_target_max_namespace_length,
624+
preview_target_path_template="preview-envs/${APPLICATION_NAME}",
625+
replacements=gitops_config.replacements,
626+
)
627+
628+
self.path_mock.is_dir.side_effect = [
629+
False, # /tmp/target-repo/preview-envs/my-app/my-app-685912d3-preview, doesn't exist yet -> create
630+
True, # /tmp/template-repo/.preview-templates/my-app
631+
]
632+
633+
deployment_created_callback = Mock(return_value=None)
634+
635+
command = CreatePreviewCommand(ARGS)
636+
command.register_callbacks(
637+
deployment_already_up_to_date_callback=lambda _: self.fail("should not be called"),
638+
deployment_updated_callback=lambda _: self.fail("should not be called"),
639+
deployment_created_callback=deployment_created_callback,
640+
)
641+
command.execute()
642+
643+
deployment_created_callback.assert_called_once_with("created template 685912d3")
644+
self.target_git_repo_mock.get_full_file_path.assert_any_call("preview-envs/my-app/my-app-685912d3-preview")
645+
self.target_git_repo_mock.get_full_file_path.assert_any_call(
646+
"preview-envs/my-app/my-app-685912d3-preview/Chart.yaml"
647+
)
648+
self.shutil_mock.copytree.assert_called_once_with(
649+
"/tmp/template-repo/.preview-templates/my-app",
650+
"/tmp/target-repo/preview-envs/my-app/my-app-685912d3-preview",
651+
)

tests/commands/test_delete_preview.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def setUp(self):
4545
preview_target_branch="target-branch",
4646
preview_target_namespace_template="APP-${PREVIEW_ID_HASH}-preview",
4747
preview_target_max_namespace_length=50,
48+
preview_target_path_template="",
4849
replacements={},
4950
)
5051

@@ -89,7 +90,7 @@ def test_delete_existing_happy_flow(self):
8990
call.GitRepoApiFactory.create(args, "PREVIEW_TARGET_ORG", "PREVIEW_TARGET_REPO"),
9091
call.GitRepo(self.git_repo_api_mock),
9192
call.GitRepo.clone("target-branch"),
92-
call.logging.info("Preview folder name: %s", "app-685912d3-preview"),
93+
call.logging.info("Preview folder: %s", "app-685912d3-preview"),
9394
call.GitRepo.get_full_file_path("app-685912d3-preview"),
9495
call.Path("/tmp/created-tmp-dir/app-685912d3-preview"),
9596
call.Path.exists(),
@@ -128,7 +129,7 @@ def test_delete_missing_happy_flow(self):
128129
call.GitRepoApiFactory.create(args, "PREVIEW_TARGET_ORG", "PREVIEW_TARGET_REPO"),
129130
call.GitRepo(self.git_repo_api_mock),
130131
call.GitRepo.clone("target-branch"),
131-
call.logging.info("Preview folder name: %s", "app-685912d3-preview"),
132+
call.logging.info("Preview folder: %s", "app-685912d3-preview"),
132133
call.GitRepo.get_full_file_path("app-685912d3-preview"),
133134
call.Path("/tmp/created-tmp-dir/app-685912d3-preview"),
134135
call.Path.exists(),
@@ -156,14 +157,14 @@ def test_delete_missing_but_expected_error(self):
156157
)
157158
with pytest.raises(GitOpsException) as ex:
158159
DeletePreviewCommand(args).execute()
159-
self.assertEqual(str(ex.value), "There was no preview with name: app-685912d3-preview")
160+
self.assertEqual(str(ex.value), "There was no preview at path: app-685912d3-preview")
160161

161162
assert self.mock_manager.method_calls == [
162163
call.load_gitops_config(args, "ORGA", "REPO"),
163164
call.GitRepoApiFactory.create(args, "PREVIEW_TARGET_ORG", "PREVIEW_TARGET_REPO"),
164165
call.GitRepo(self.git_repo_api_mock),
165166
call.GitRepo.clone("target-branch"),
166-
call.logging.info("Preview folder name: %s", "app-685912d3-preview"),
167+
call.logging.info("Preview folder: %s", "app-685912d3-preview"),
167168
call.GitRepo.get_full_file_path("app-685912d3-preview"),
168169
call.Path("/tmp/created-tmp-dir/app-685912d3-preview"),
169170
call.Path.exists(),
@@ -191,3 +192,47 @@ def test_missing_gitops_config_yaml_error(self):
191192
assert self.mock_manager.method_calls == [
192193
call.load_gitops_config(args, "ORGA", "REPO"),
193194
]
195+
196+
def test_delete_existing_with_target_path(self):
197+
gitops_config: GitOpsConfig = self.load_gitops_config_mock.return_value
198+
self.load_gitops_config_mock.return_value = GitOpsConfig(
199+
api_version=gitops_config.api_version,
200+
application_name=gitops_config.application_name,
201+
messages_created_template=gitops_config.messages_created_template,
202+
messages_updated_template=gitops_config.messages_updated_template,
203+
messages_uptodate_template=gitops_config.messages_uptodate_template,
204+
preview_host_template=gitops_config.preview_host_template,
205+
preview_template_organisation=gitops_config.preview_template_organisation,
206+
preview_template_repository=gitops_config.preview_template_repository,
207+
preview_template_path_template=gitops_config.preview_template_path_template,
208+
preview_template_branch=gitops_config.preview_template_branch,
209+
preview_target_organisation=gitops_config.preview_target_organisation,
210+
preview_target_repository=gitops_config.preview_target_repository,
211+
preview_target_branch=gitops_config.preview_target_branch,
212+
preview_target_namespace_template=gitops_config.preview_target_namespace_template,
213+
preview_target_max_namespace_length=gitops_config.preview_target_max_namespace_length,
214+
preview_target_path_template="preview-envs/${APPLICATION_NAME}",
215+
replacements=gitops_config.replacements,
216+
)
217+
218+
args = DeletePreviewCommand.Args(
219+
username="USERNAME",
220+
password="PASSWORD",
221+
git_user="GIT_USER",
222+
git_email="GIT_EMAIL",
223+
git_author_name="GIT_AUTHOR_NAME",
224+
git_author_email="GIT_AUTHOR_EMAIL",
225+
organisation="ORGA",
226+
repository_name="REPO",
227+
git_provider=GitProvider.GITHUB,
228+
git_provider_url=None,
229+
preview_id="PREVIEW_ID",
230+
expect_preview_exists=False,
231+
)
232+
DeletePreviewCommand(args).execute()
233+
234+
self.git_repo_mock.get_full_file_path.assert_called_with("preview-envs/APP/app-685912d3-preview")
235+
self.shutil_mock.rmtree.assert_called_once_with(
236+
"/tmp/created-tmp-dir/preview-envs/APP/app-685912d3-preview",
237+
ignore_errors=True,
238+
)

tests/test_gitops_config_v0.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,8 @@ def test_replacements_invalid_list_items_unknown_variable(self):
153153
def test_replacements_invalid_list_items_invalid_variable(self):
154154
self.yaml["previewConfig"]["replace"][0]["variable"] = "{FOO"
155155
self.assert_load_error("Item 'previewConfig.replace.[0].variable' must not contain '{' or '}'!")
156+
157+
def test_preview_target_path_defaults_to_root(self):
158+
config = self.load()
159+
self.assertEqual(config.preview_target_path_template, "")
160+
self.assertEqual(config.preview_target_path, "")

0 commit comments

Comments
 (0)