Skip to content
Open
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
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)
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)
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)

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}")
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