Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 4 additions & 2 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The GitOps CLI provides several commands which can be used to perform typical op

```
usage: gitopscli [-h]
{deploy,sync-apps,add-pr-comment,create-preview,delete-preview,version}
{deploy,sync-apps,add-pr-comment,create-preview,create-pr-preview,delete-preview,delete-pr-preview,version}
...

GitOps CLI
Expand All @@ -13,13 +13,15 @@ options:
-h, --help show this help message and exit

commands:
{deploy,sync-apps,add-pr-comment,create-preview,delete-preview,version}
{deploy,sync-apps,add-pr-comment,create-preview,create-pr-preview,delete-preview,delete-pr-preview,version}
deploy Trigger a new deployment by changing YAML values
sync-apps Synchronize applications (= every directory) from apps
config repository to apps root config
add-pr-comment Create a comment on the pull request
create-preview Create a preview environment
create-pr-preview Create a preview environment for a pull request
delete-preview Delete a preview environment
delete-pr-preview Delete a preview environment for a pull request
version Show the GitOps CLI version information
```

Expand Down
7 changes: 6 additions & 1 deletion docs/includes/preview-configuration.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
## Configuration
### Preview Templates

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.
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 your *deployment config repository* and replaces e.g. image tag and route host which are specific to this preview.

By default previews are created in the repository root. If `previewConfig.target.path` is set, previews are created below that path instead.

```
deployment-config-repo/
Expand Down Expand Up @@ -44,6 +48,7 @@ previewConfig:
target:
organisation: deployments
repository: deployment-config-repo
# path: custom/${APPLICATION_NAME} # optional (defaults to repo's root directory)
# branch: master # optional (defaults to repo's default branch)
# namespace: ${APPLICATION_NAME}-${PREVIEW_ID_HASH}-preview' # optional (default: '${APPLICATION_NAME}-${PREVIEW_ID}-${PREVIEW_ID_HASH_SHORT}-preview',
# Invalid characters in PREVIEW_ID will be replaced. PREVIEW_ID will be
Expand Down
11 changes: 6 additions & 5 deletions gitopscli/commands/create_preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,13 @@ def __create_preview_from_template_if_not_existing(
gitops_config: GitOpsConfig,
) -> bool:
preview_namespace = gitops_config.get_preview_namespace(self.__args.preview_id)
full_preview_folder_path = target_git_repo.get_full_file_path(preview_namespace)
preview_folder_path = gitops_config.get_preview_folder_path(self.__args.preview_id)
full_preview_folder_path = target_git_repo.get_full_file_path(preview_folder_path)
preview_env_already_exist = Path(full_preview_folder_path).is_dir()
if preview_env_already_exist:
logging.info("Use existing folder for preview: %s", preview_namespace)
logging.info("Use existing folder for preview: %s (path: %s)", preview_namespace, preview_folder_path)
Comment thread
lintermansjens marked this conversation as resolved.
Outdated
return False
logging.info("Create new folder for preview: %s", preview_namespace)
logging.info("Create new folder for preview: %s (path: %s)", preview_namespace, preview_folder_path)
Comment thread
lintermansjens marked this conversation as resolved.
Outdated
full_preview_template_folder_path = template_git_repo.get_full_file_path(gitops_config.preview_template_path)
if not Path(full_preview_template_folder_path).is_dir():
raise GitOpsException(f"The preview template folder does not exist: {gitops_config.preview_template_path}")
Expand All @@ -143,15 +144,15 @@ def __create_preview_from_template_if_not_existing(

def __replace_values(self, git_repo: GitRepo, gitops_config: GitOpsConfig) -> bool:
preview_id = self.__args.preview_id
preview_folder_name = gitops_config.get_preview_namespace(self.__args.preview_id)
preview_folder_path = gitops_config.get_preview_folder_path(self.__args.preview_id)
context = GitOpsConfig.Replacement.PreviewContext(gitops_config, preview_id, self.__args.git_hash)
any_value_replaced = False
for file, replacements in gitops_config.replacements.items():
for replacement in replacements:
replacement_value = replacement.get_value(context)
value_replaced = self.__update_yaml_file(
git_repo,
f"{preview_folder_name}/{file}",
f"{preview_folder_path}/{file}",
replacement.path,
replacement_value,
)
Expand Down
5 changes: 3 additions & 2 deletions gitopscli/commands/delete_preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ def execute(self) -> None:
preview_target_git_repo.clone(gitops_config.preview_target_branch)

preview_namespace = gitops_config.get_preview_namespace(preview_id)
logging.info("Preview folder name: %s", preview_namespace)
preview_folder_path = gitops_config.get_preview_folder_path(preview_id)
logging.info("Preview folder name: %s (path: %s)", preview_namespace, preview_folder_path)
Comment thread
lintermansjens marked this conversation as resolved.
Outdated

preview_folder_exists = self.__delete_folder_if_exists(preview_target_git_repo, preview_namespace)
preview_folder_exists = self.__delete_folder_if_exists(preview_target_git_repo, preview_folder_path)
if not preview_folder_exists:
if self.__args.expect_preview_exists:
raise GitOpsException(f"There was no preview with name: {preview_namespace}")
Comment thread
lintermansjens marked this conversation as resolved.
Outdated
Expand Down
21 changes: 21 additions & 0 deletions gitopscli/gitops_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,18 @@ def get_value(self, context: PreviewContext) -> str:
preview_target_branch: str | None
preview_target_namespace_template: str
preview_target_max_namespace_length: int
preview_target_path_template: str

replacements: dict[str, list[Replacement]]

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

@property
def preview_target_path(self) -> str:
return self.preview_target_path_template.replace("${APPLICATION_NAME}", self.application_name)

def __post_init__(self) -> None:
assert isinstance(self.application_name, str), "application_name of wrong type!"
assert isinstance(self.preview_host_template, str), "preview_host_template of wrong type!"
Expand Down Expand Up @@ -106,6 +111,8 @@ def __post_init__(self) -> None:
int,
), "preview_target_max_namespace_length of wrong type!"
assert self.preview_target_max_namespace_length >= 1, "preview_target_max_namespace_length is < 1!"
assert isinstance(self.preview_target_path_template, str), "preview_target_path_template of wrong type!"
self.__assert_variables(self.preview_target_path_template, {"APPLICATION_NAME"})
assert isinstance(self.replacements, dict), "replacements of wrong type!"
for file, replacements in self.replacements.items():
assert isinstance(file, str), f"replacement file '{file}' of wrong type!"
Expand All @@ -120,6 +127,12 @@ def get_preview_host(self, preview_id: str) -> str:
preview_host = preview_host.replace("${PREVIEW_ID}", self.__sanitize(preview_id))
return preview_host.replace("${PREVIEW_NAMESPACE}", self.get_preview_namespace(preview_id))

def get_preview_folder_path(self, preview_id: str) -> str:
preview_namespace = self.get_preview_namespace(preview_id)
if self.preview_target_path:
return f"{self.preview_target_path}/{preview_namespace}"
return preview_namespace

def get_preview_namespace(self, preview_id: str) -> str:
preview_namespace = self.preview_target_namespace_template
preview_namespace = preview_namespace.replace("${APPLICATION_NAME}", self.application_name)
Expand Down Expand Up @@ -325,6 +338,7 @@ def __parse_v0(self) -> GitOpsConfig:
preview_target_branch=None, # use default branch
preview_target_namespace_template="${APPLICATION_NAME}-${PREVIEW_ID_HASH}-preview",
preview_target_max_namespace_length=63,
preview_target_path_template="",
replacements=replacements,
)

Expand Down Expand Up @@ -364,6 +378,9 @@ def add_var_dollar(template: str) -> str:
),
),
preview_target_max_namespace_length=63,
preview_target_path_template=add_var_dollar(
self.__get_string_value_or_default("previewConfig.target.path", ""),
),
replacements=replacements,
)

Expand Down Expand Up @@ -439,5 +456,9 @@ def __parse_v2(self) -> GitOpsConfig:
"${APPLICATION_NAME}-${PREVIEW_ID}-${PREVIEW_ID_HASH_SHORT}-preview",
),
preview_target_max_namespace_length=preview_target_max_namespace_length,
preview_target_path_template=self.__get_string_value_or_default(
"previewConfig.target.path",
"",
),
replacements=replacements,
)
85 changes: 76 additions & 9 deletions tests/commands/test_create_preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def setUp(self):
preview_target_branch=None,
preview_target_namespace_template="my-app-${PREVIEW_ID_HASH}-preview",
preview_target_max_namespace_length=50,
preview_target_path_template="",
replacements={
"Chart.yaml": [GitOpsConfig.Replacement(path="name", value_template="${PREVIEW_NAMESPACE}")],
"values.yaml": [
Expand Down Expand Up @@ -165,7 +166,9 @@ def test_create_new_preview(self):
call.GitRepo.get_full_file_path("my-app-685912d3-preview"),
call.Path("/tmp/target-repo/my-app-685912d3-preview"),
call.Path.is_dir(),
call.logging.info("Create new folder for preview: %s", "my-app-685912d3-preview"),
call.logging.info(
"Create new folder for preview: %s (path: %s)", "my-app-685912d3-preview", "my-app-685912d3-preview"
),
call.GitRepo.get_full_file_path(".preview-templates/my-app"),
call.Path("/tmp/template-repo/.preview-templates/my-app"),
call.Path.is_dir(),
Expand Down Expand Up @@ -231,6 +234,7 @@ def test_create_new_preview_from_same_template_target_repo(self):
preview_target_branch=gitops_config.preview_target_branch,
preview_target_namespace_template=gitops_config.preview_target_namespace_template,
preview_target_max_namespace_length=gitops_config.preview_target_max_namespace_length,
preview_target_path_template=gitops_config.preview_target_path_template,
replacements=gitops_config.replacements,
)

Expand Down Expand Up @@ -270,7 +274,9 @@ def test_create_new_preview_from_same_template_target_repo(self):
call.GitRepo.get_full_file_path("my-app-685912d3-preview"),
call.Path("/tmp/target-repo/my-app-685912d3-preview"),
call.Path.is_dir(),
call.logging.info("Create new folder for preview: %s", "my-app-685912d3-preview"),
call.logging.info(
"Create new folder for preview: %s (path: %s)", "my-app-685912d3-preview", "my-app-685912d3-preview"
),
call.GitRepo.get_full_file_path(".preview-templates/my-app"),
call.Path("/tmp/target-repo/.preview-templates/my-app"),
call.Path.is_dir(),
Expand Down Expand Up @@ -347,7 +353,9 @@ def test_update_existing_preview(self):
call.GitRepo.get_full_file_path("my-app-685912d3-preview"),
call.Path("/tmp/target-repo/my-app-685912d3-preview"),
call.Path.is_dir(),
call.logging.info("Use existing folder for preview: %s", "my-app-685912d3-preview"),
call.logging.info(
"Use existing folder for preview: %s (path: %s)", "my-app-685912d3-preview", "my-app-685912d3-preview"
),
call.GitRepo.get_full_file_path("my-app-685912d3-preview/Chart.yaml"),
call.update_yaml_file(
"/tmp/target-repo/my-app-685912d3-preview/Chart.yaml", "name", "my-app-685912d3-preview"
Expand Down Expand Up @@ -419,7 +427,9 @@ def test_preview_already_up_to_date(self):
call.GitRepo.get_full_file_path("my-app-685912d3-preview"),
call.Path("/tmp/target-repo/my-app-685912d3-preview"),
call.Path.is_dir(),
call.logging.info("Use existing folder for preview: %s", "my-app-685912d3-preview"),
call.logging.info(
"Use existing folder for preview: %s (path: %s)", "my-app-685912d3-preview", "my-app-685912d3-preview"
),
call.GitRepo.get_full_file_path("my-app-685912d3-preview/Chart.yaml"),
call.update_yaml_file(
"/tmp/target-repo/my-app-685912d3-preview/Chart.yaml", "name", "my-app-685912d3-preview"
Expand Down Expand Up @@ -471,7 +481,9 @@ def test_create_preview_for_unknown_template(self):
call.GitRepo.get_full_file_path("my-app-685912d3-preview"),
call.Path("/tmp/target-repo/my-app-685912d3-preview"),
call.Path.is_dir(),
call.logging.info("Create new folder for preview: %s", "my-app-685912d3-preview"),
call.logging.info(
"Create new folder for preview: %s (path: %s)", "my-app-685912d3-preview", "my-app-685912d3-preview"
),
call.GitRepo.get_full_file_path(".preview-templates/my-app"),
call.Path("/tmp/template-repo/.preview-templates/my-app"),
call.Path.is_dir(),
Expand All @@ -498,7 +510,9 @@ def test_create_preview_values_yaml_not_found(self):
call.GitRepo.get_full_file_path("my-app-685912d3-preview"),
call.Path("/tmp/target-repo/my-app-685912d3-preview"),
call.Path.is_dir(),
call.logging.info("Use existing folder for preview: %s", "my-app-685912d3-preview"),
call.logging.info(
"Use existing folder for preview: %s (path: %s)", "my-app-685912d3-preview", "my-app-685912d3-preview"
),
call.GitRepo.get_full_file_path("my-app-685912d3-preview/Chart.yaml"),
call.update_yaml_file(
"/tmp/target-repo/my-app-685912d3-preview/Chart.yaml", "name", "my-app-685912d3-preview"
Expand Down Expand Up @@ -526,7 +540,9 @@ def test_create_preview_values_yaml_parse_error(self):
call.GitRepo.get_full_file_path("my-app-685912d3-preview"),
call.Path("/tmp/target-repo/my-app-685912d3-preview"),
call.Path.is_dir(),
call.logging.info("Use existing folder for preview: %s", "my-app-685912d3-preview"),
call.logging.info(
"Use existing folder for preview: %s (path: %s)", "my-app-685912d3-preview", "my-app-685912d3-preview"
),
call.GitRepo.get_full_file_path("my-app-685912d3-preview/Chart.yaml"),
call.update_yaml_file(
"/tmp/target-repo/my-app-685912d3-preview/Chart.yaml", "name", "my-app-685912d3-preview"
Expand Down Expand Up @@ -554,7 +570,9 @@ def test_create_preview_with_invalid_replacement_path(self):
call.GitRepo.get_full_file_path("my-app-685912d3-preview"),
call.Path("/tmp/target-repo/my-app-685912d3-preview"),
call.Path.is_dir(),
call.logging.info("Use existing folder for preview: %s", "my-app-685912d3-preview"),
call.logging.info(
"Use existing folder for preview: %s (path: %s)", "my-app-685912d3-preview", "my-app-685912d3-preview"
),
call.GitRepo.get_full_file_path("my-app-685912d3-preview/Chart.yaml"),
call.update_yaml_file(
"/tmp/target-repo/my-app-685912d3-preview/Chart.yaml", "name", "my-app-685912d3-preview"
Expand Down Expand Up @@ -587,7 +605,9 @@ def test_create_new_preview_invalid_chart_template(self):
call.GitRepo.get_full_file_path("my-app-685912d3-preview"),
call.Path("/tmp/target-repo/my-app-685912d3-preview"),
call.Path.is_dir(),
call.logging.info("Create new folder for preview: %s", "my-app-685912d3-preview"),
call.logging.info(
"Create new folder for preview: %s (path: %s)", "my-app-685912d3-preview", "my-app-685912d3-preview"
),
call.GitRepo.get_full_file_path(".preview-templates/my-app"),
call.Path("/tmp/template-repo/.preview-templates/my-app"),
call.Path.is_dir(),
Expand All @@ -600,3 +620,50 @@ def test_create_new_preview_invalid_chart_template(self):
"/tmp/target-repo/my-app-685912d3-preview/Chart.yaml", "name", "my-app-685912d3-preview"
),
]

def test_create_new_preview_with_target_path(self):
gitops_config: GitOpsConfig = self.load_gitops_config_mock.return_value
self.load_gitops_config_mock.return_value = GitOpsConfig(
api_version=gitops_config.api_version,
application_name=gitops_config.application_name,
messages_created_template=gitops_config.messages_created_template,
messages_updated_template=gitops_config.messages_updated_template,
messages_uptodate_template=gitops_config.messages_uptodate_template,
preview_host_template=gitops_config.preview_host_template,
preview_template_organisation=gitops_config.preview_template_organisation,
preview_template_repository=gitops_config.preview_template_repository,
preview_template_path_template=gitops_config.preview_template_path_template,
preview_template_branch=gitops_config.preview_template_branch,
preview_target_organisation=gitops_config.preview_target_organisation,
preview_target_repository=gitops_config.preview_target_repository,
preview_target_branch=gitops_config.preview_target_branch,
preview_target_namespace_template=gitops_config.preview_target_namespace_template,
preview_target_max_namespace_length=gitops_config.preview_target_max_namespace_length,
preview_target_path_template="preview-envs/${APPLICATION_NAME}",
replacements=gitops_config.replacements,
)

self.path_mock.is_dir.side_effect = [
False, # /tmp/target-repo/preview-envs/my-app/my-app-685912d3-preview, doesn't exist yet -> create
True, # /tmp/template-repo/.preview-templates/my-app
]

deployment_created_callback = Mock(return_value=None)

command = CreatePreviewCommand(ARGS)
command.register_callbacks(
deployment_already_up_to_date_callback=lambda _: self.fail("should not be called"),
deployment_updated_callback=lambda _: self.fail("should not be called"),
deployment_created_callback=deployment_created_callback,
)
command.execute()

deployment_created_callback.assert_called_once_with("created template 685912d3")
self.target_git_repo_mock.get_full_file_path.assert_any_call("preview-envs/my-app/my-app-685912d3-preview")
self.target_git_repo_mock.get_full_file_path.assert_any_call(
"preview-envs/my-app/my-app-685912d3-preview/Chart.yaml"
)
self.shutil_mock.copytree.assert_called_once_with(
"/tmp/template-repo/.preview-templates/my-app",
"/tmp/target-repo/preview-envs/my-app/my-app-685912d3-preview",
)
Loading