From 8d9546adbe63b505e0a65034291b3db6c7bb58cc Mon Sep 17 00:00:00 2001 From: Christian Siegel Date: Tue, 3 Aug 2021 11:51:42 +0200 Subject: [PATCH 1/2] feat(GitOpsConfig): introduce apiVersion v2_beta Version 2 will use different string interpolation (`${VAR}` instead of `{VAR}`). This fixes #162. Version 1 is kept with simple backwards compatability by replacing old variable placeholders with new ones. Version 2 is still beta and not mentioned in the docs yet. This way we can add some more breaking fixes. --- gitopscli/gitops_config.py | 66 ++++-- tests/commands/test_create_preview.py | 12 +- tests/commands/test_delete_preview.py | 2 +- tests/test_gitops_config_v0.py | 12 +- tests/test_gitops_config_v1.py | 22 +- tests/test_gitops_config_v2.py | 298 ++++++++++++++++++++++++++ 6 files changed, 368 insertions(+), 44 deletions(-) create mode 100644 tests/test_gitops_config_v2.py diff --git a/gitopscli/gitops_config.py b/gitopscli/gitops_config.py index b68dfca1..f24f6acd 100644 --- a/gitopscli/gitops_config.py +++ b/gitopscli/gitops_config.py @@ -6,7 +6,7 @@ from gitopscli.gitops_exception import GitOpsException _MAX_NAMESPACE_LENGTH = 63 -_VARIABLE_REGEX = re.compile(r"{(\w+)}") +_VARIABLE_REGEX = re.compile(r"\${(\w+)}") @dataclass(frozen=True) @@ -44,7 +44,7 @@ def __init__(self, path: str, value_template: str): def get_value(self, context: PreviewContext) -> str: val = self.value_template for variable, value_func in self.__VARIABLE_MAPPERS.items(): - val = val.replace(f"{{{variable}}}", value_func(context)) + val = val.replace(f"${{{variable}}}", value_func(context)) return val api_version: int @@ -66,7 +66,7 @@ def get_value(self, context: PreviewContext) -> str: @property def preview_template_path(self) -> str: - return self.preview_template_path_template.replace("{APPLICATION_NAME}", self.application_name) + return self.preview_template_path_template.replace("${APPLICATION_NAME}", self.application_name) def __post_init__(self) -> None: assert isinstance(self.application_name, str), "application_name of wrong type!" @@ -98,22 +98,22 @@ def __post_init__(self) -> None: def get_preview_host(self, preview_id: str) -> str: preview_host = self.preview_host_template - preview_host = preview_host.replace("{APPLICATION_NAME}", self.application_name) - preview_host = preview_host.replace("{PREVIEW_ID_HASH}", self.create_preview_id_hash(preview_id)) - preview_host = preview_host.replace("{PREVIEW_ID}", self.__sanitize(preview_id)) - preview_host = preview_host.replace("{PREVIEW_NAMESPACE}", self.get_preview_namespace(preview_id)) + preview_host = preview_host.replace("${APPLICATION_NAME}", self.application_name) + preview_host = preview_host.replace("${PREVIEW_ID_HASH}", self.create_preview_id_hash(preview_id)) + preview_host = preview_host.replace("${PREVIEW_ID}", self.__sanitize(preview_id)) + preview_host = preview_host.replace("${PREVIEW_NAMESPACE}", self.get_preview_namespace(preview_id)) return preview_host 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) - preview_namespace = preview_namespace.replace("{PREVIEW_ID_HASH}", self.create_preview_id_hash(preview_id)) + preview_namespace = preview_namespace.replace("${APPLICATION_NAME}", self.application_name) + preview_namespace = preview_namespace.replace("${PREVIEW_ID_HASH}", self.create_preview_id_hash(preview_id)) - current_length = len(preview_namespace) - len("{PREVIEW_ID}") + current_length = len(preview_namespace) - len("${PREVIEW_ID}") remaining_length = _MAX_NAMESPACE_LENGTH - current_length if remaining_length < 1: - preview_namespace = preview_namespace.replace("{PREVIEW_ID}", "") + preview_namespace = preview_namespace.replace("${PREVIEW_ID}", "") raise GitOpsException( f"Preview namespace is too long (max {_MAX_NAMESPACE_LENGTH} chars): " f"{preview_namespace} ({len(preview_namespace)} chars)" @@ -121,7 +121,7 @@ def get_preview_namespace(self, preview_id: str) -> str: sanitized_preview_id = self.__sanitize(preview_id, remaining_length) - preview_namespace = preview_namespace.replace("{PREVIEW_ID}", sanitized_preview_id) + preview_namespace = preview_namespace.replace("${PREVIEW_ID}", sanitized_preview_id) preview_namespace = preview_namespace.lower() invalid_character = re.search(r"[^a-z0-9-]", preview_namespace) @@ -216,11 +216,13 @@ def parse(self) -> GitOpsConfig: return self.__parse_v0() if api_version == "v1": return self.__parse_v1() + if api_version == "v2_beta": + return self.__parse_v2() raise GitOpsException(f"GitOps config apiVersion '{api_version}' is not supported!") def __parse_v0(self) -> GitOpsConfig: replacements: Dict[str, List[GitOpsConfig.Replacement]] = { - "Chart.yaml": [GitOpsConfig.Replacement("name", "{PREVIEW_NAMESPACE}")], + "Chart.yaml": [GitOpsConfig.Replacement("name", "${PREVIEW_NAMESPACE}")], "values.yaml": [], } replacement_dicts = self.__get_list_value("previewConfig.replace") @@ -247,7 +249,7 @@ def __parse_v0(self) -> GitOpsConfig: variable = "PREVIEW_HOST" # backwards compatability if variable == "GIT_COMMIT": variable = "GIT_HASH" # backwards compatability - replacements["values.yaml"].append(GitOpsConfig.Replacement(path, f"{{{variable}}}")) + replacements["values.yaml"].append(GitOpsConfig.Replacement(path, f"${{{variable}}}")) preview_target_organisation = self.__get_string_value("deploymentConfig.org") preview_target_repository = self.__get_string_value("deploymentConfig.repository") @@ -256,20 +258,44 @@ def __parse_v0(self) -> GitOpsConfig: api_version=0, application_name=self.__get_string_value("deploymentConfig.applicationName"), preview_host_template=self.__get_string_value("previewConfig.route.host.template").replace( - "{SHA256_8CHAR_BRANCH_HASH}", "{PREVIEW_ID_HASH}" # backwards compatibility + "{SHA256_8CHAR_BRANCH_HASH}", "${PREVIEW_ID_HASH}" # backwards compatibility ), preview_template_organisation=preview_target_organisation, preview_template_repository=preview_target_repository, - preview_template_path_template=f".preview-templates/{{APPLICATION_NAME}}", + preview_template_path_template=".preview-templates/${APPLICATION_NAME}", preview_template_branch=None, # use default branch preview_target_organisation=preview_target_organisation, preview_target_repository=preview_target_repository, preview_target_branch=None, # use default branch - preview_target_namespace_template=f"{{APPLICATION_NAME}}-{{PREVIEW_ID_HASH}}-preview", + preview_target_namespace_template="${APPLICATION_NAME}-${PREVIEW_ID_HASH}-preview", replacements=replacements, ) def __parse_v1(self) -> GitOpsConfig: + config = self.__parse_v2() + # add $ in front of variables for backwards compatability (e.g. ${FOO}): + add_var_dollar: Callable[[str], str] = lambda template: re.sub(r"(^|[^\$])({(\w+)})", r"\1$\2", template) + replacements: Dict[str, List[GitOpsConfig.Replacement]] = {} + for filename, file_replacements in config.replacements.items(): + replacements[filename] = [ + GitOpsConfig.Replacement(r.path, add_var_dollar(r.value_template)) for r in file_replacements + ] + return GitOpsConfig( + api_version=1, + application_name=config.application_name, + preview_host_template=add_var_dollar(config.preview_host_template), + preview_template_organisation=config.preview_template_organisation, + preview_template_repository=config.preview_template_repository, + preview_template_path_template=add_var_dollar(config.preview_template_path_template), + preview_template_branch=config.preview_template_branch, + preview_target_organisation=config.preview_target_organisation, + preview_target_repository=config.preview_target_repository, + preview_target_branch=config.preview_target_branch, + preview_target_namespace_template=add_var_dollar(config.preview_target_namespace_template), + replacements=replacements, + ) + + def __parse_v2(self) -> GitOpsConfig: preview_target_organisation = self.__get_string_value("previewConfig.target.organisation") preview_target_repository = self.__get_string_value("previewConfig.target.repository") preview_target_branch = self.__get_string_value_or_none("previewConfig.target.branch") @@ -298,7 +324,7 @@ def __parse_v1(self) -> GitOpsConfig: replacements[filename].append(GitOpsConfig.Replacement(path, value)) return GitOpsConfig( - api_version=1, + api_version=2, application_name=self.__get_string_value("applicationName"), preview_host_template=self.__get_string_value("previewConfig.host"), preview_template_organisation=self.__get_string_value_or_default( @@ -308,7 +334,7 @@ def __parse_v1(self) -> GitOpsConfig: "previewConfig.template.repository", preview_target_repository ), preview_template_path_template=self.__get_string_value_or_default( - "previewConfig.template.path", f".preview-templates/{{APPLICATION_NAME}}" + "previewConfig.template.path", ".preview-templates/${APPLICATION_NAME}" ), preview_template_branch=self.__get_string_value_or_none("previewConfig.template.branch") or preview_target_branch, @@ -316,7 +342,7 @@ def __parse_v1(self) -> GitOpsConfig: preview_target_repository=preview_target_repository, preview_target_branch=preview_target_branch, preview_target_namespace_template=self.__get_string_value_or_default( - "previewConfig.target.namespace", f"{{APPLICATION_NAME}}-{{PREVIEW_ID}}-{{PREVIEW_ID_HASH}}-preview" + "previewConfig.target.namespace", "${APPLICATION_NAME}-${PREVIEW_ID}-${PREVIEW_ID_HASH}-preview", ), replacements=replacements, ) diff --git a/tests/commands/test_create_preview.py b/tests/commands/test_create_preview.py index c91cbe86..61e96bfa 100644 --- a/tests/commands/test_create_preview.py +++ b/tests/commands/test_create_preview.py @@ -54,9 +54,9 @@ def setUp(self): self.load_gitops_config_mock = self.monkey_patch(load_gitops_config) self.load_gitops_config_mock.return_value = GitOpsConfig( - api_version=1, + api_version=2, application_name="my-app", - preview_host_template="app.xy-{PREVIEW_ID_HASH}.example.tld", + preview_host_template="app.xy-${PREVIEW_ID_HASH}.example.tld", preview_template_organisation="PREVIEW_TEMPLATE_ORG", preview_template_repository="PREVIEW_TEMPLATE_REPO", preview_template_path_template=".preview-templates/my-app", @@ -64,12 +64,12 @@ def setUp(self): preview_target_organisation="PREVIEW_TARGET_ORG", preview_target_repository="PREVIEW_TARGET_REPO", preview_target_branch=None, - preview_target_namespace_template=f"my-app-{{PREVIEW_ID_HASH}}-preview", + preview_target_namespace_template="my-app-${PREVIEW_ID_HASH}-preview", replacements={ - "Chart.yaml": [GitOpsConfig.Replacement(path="name", value_template="{PREVIEW_NAMESPACE}"),], + "Chart.yaml": [GitOpsConfig.Replacement(path="name", value_template="${PREVIEW_NAMESPACE}"),], "values.yaml": [ - GitOpsConfig.Replacement(path="image.tag", value_template="{GIT_HASH}"), - GitOpsConfig.Replacement(path="route.host", value_template="{PREVIEW_HOST}"), + GitOpsConfig.Replacement(path="image.tag", value_template="${GIT_HASH}"), + GitOpsConfig.Replacement(path="route.host", value_template="${PREVIEW_HOST}"), ], }, ) diff --git a/tests/commands/test_delete_preview.py b/tests/commands/test_delete_preview.py index 6567e4a2..2185d9da 100644 --- a/tests/commands/test_delete_preview.py +++ b/tests/commands/test_delete_preview.py @@ -36,7 +36,7 @@ def setUp(self): preview_target_organisation="PREVIEW_TARGET_ORG", preview_target_repository="PREVIEW_TARGET_REPO", preview_target_branch="target-branch", - preview_target_namespace_template=f"APP-{{PREVIEW_ID_HASH}}-preview", + preview_target_namespace_template="APP-${PREVIEW_ID_HASH}-preview", replacements={}, ) diff --git a/tests/test_gitops_config_v0.py b/tests/test_gitops_config_v0.py index ec5e2f68..46f83074 100644 --- a/tests/test_gitops_config_v0.py +++ b/tests/test_gitops_config_v0.py @@ -73,7 +73,7 @@ def test_preview_target_branch_is_none(self): def test_route_host_template(self): config = self.load() - self.assertEqual(config.preview_host_template, "my-{PREVIEW_ID_HASH}-host-template") + self.assertEqual(config.preview_host_template, "my-${PREVIEW_ID_HASH}-host-template") def test_route_host(self): config = self.load() @@ -97,7 +97,7 @@ def test_route_host_template_not_a_string(self): def test_namespace_template(self): config = self.load() - self.assertEqual(config.preview_target_namespace_template, "{APPLICATION_NAME}-{PREVIEW_ID_HASH}-preview") + self.assertEqual(config.preview_target_namespace_template, "${APPLICATION_NAME}-${PREVIEW_ID_HASH}-preview") def test_namespace(self): config = self.load() @@ -109,13 +109,13 @@ def test_replacements(self): self.assertEqual(len(config.replacements["Chart.yaml"]), 1) self.assertEqual(config.replacements["Chart.yaml"][0].path, "name") - self.assertEqual(config.replacements["Chart.yaml"][0].value_template, "{PREVIEW_NAMESPACE}") + self.assertEqual(config.replacements["Chart.yaml"][0].value_template, "${PREVIEW_NAMESPACE}") self.assertEqual(len(config.replacements["values.yaml"]), 2) self.assertEqual(config.replacements["values.yaml"][0].path, "a.b") - self.assertEqual(config.replacements["values.yaml"][0].value_template, "{PREVIEW_HOST}") + self.assertEqual(config.replacements["values.yaml"][0].value_template, "${PREVIEW_HOST}") self.assertEqual(config.replacements["values.yaml"][1].path, "c.d") - self.assertEqual(config.replacements["values.yaml"][1].value_template, "{GIT_HASH}") + self.assertEqual(config.replacements["values.yaml"][1].value_template, "${GIT_HASH}") def test_replacements_missing(self): del self.yaml["previewConfig"]["replace"] @@ -147,7 +147,7 @@ def test_replacements_invalid_list_items_variable_not_a_string(self): def test_replacements_invalid_list_items_unknown_variable(self): self.yaml["previewConfig"]["replace"][0]["variable"] = "FOO" - self.assert_load_error("Replacement value '{FOO}' for path 'a.b' contains invalid variable: FOO") + self.assert_load_error("Replacement value '${FOO}' for path 'a.b' contains invalid variable: FOO") def test_replacements_invalid_list_items_invalid_variable(self): self.yaml["previewConfig"]["replace"][0]["variable"] = "{FOO" diff --git a/tests/test_gitops_config_v1.py b/tests/test_gitops_config_v1.py index 17e18a27..1cbf9de2 100644 --- a/tests/test_gitops_config_v1.py +++ b/tests/test_gitops_config_v1.py @@ -64,7 +64,7 @@ def test_application_name_not_a_string(self): def test_preview_host_template(self): config = self.load() - self.assertEqual(config.preview_host_template, "my-{PREVIEW_ID}-{PREVIEW_ID_HASH}-host-template") + self.assertEqual(config.preview_host_template, "my-${PREVIEW_ID}-${PREVIEW_ID_HASH}-host-template") def test_preview_host(self): config = self.load() @@ -83,7 +83,7 @@ def test_preview_host_not_a_string(self): def test_preview_host_contains_invalid_variable(self): self.yaml["previewConfig"]["host"] = "{FOO}-bar" - self.assert_load_error("GitOps config template '{FOO}-bar' contains invalid variable: FOO") + self.assert_load_error("GitOps config template '${FOO}-bar' contains invalid variable: FOO") def test_preview_template_organisation(self): config = self.load() @@ -143,7 +143,7 @@ def test_preview_template_path_not_a_string(self): def test_preview_template_path_contains_invalid_variable(self): self.yaml["previewConfig"]["template"]["path"] = "{FOO}-bar" - self.assert_load_error("GitOps config template '{FOO}-bar' contains invalid variable: FOO") + self.assert_load_error("GitOps config template '${FOO}-bar' contains invalid variable: FOO") def test_preview_target_organisation(self): config = self.load() @@ -201,14 +201,14 @@ def test_is_preview_template_equal_target(self): def test_preview_target_namespace(self): config = self.load() - self.assertEqual(config.preview_target_namespace_template, "{APPLICATION_NAME}-{PREVIEW_ID_HASH}-dev") + self.assertEqual(config.preview_target_namespace_template, "${APPLICATION_NAME}-${PREVIEW_ID_HASH}-dev") self.assertEqual(config.get_preview_namespace("preview-1"), "my-app-3e355b4a-dev") def test_preview_target_namespace_default(self): del self.yaml["previewConfig"]["target"]["namespace"] config = self.load() self.assertEqual( - config.preview_target_namespace_template, "{APPLICATION_NAME}-{PREVIEW_ID}-{PREVIEW_ID_HASH}-preview" + config.preview_target_namespace_template, "${APPLICATION_NAME}-${PREVIEW_ID}-${PREVIEW_ID_HASH}-preview" ) actual_namespace = config.get_preview_namespace( "Very long preview ID. It will be cut to have max 63 chars of namespace in total!!" @@ -230,7 +230,7 @@ def test_preview_target_namespace_invalid_template(self): def test_preview_target_namespace_too_long(self): self.yaml["previewConfig"]["target"][ "namespace" - ] = "veeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeery-long-{PREVIEW_ID}-{PREVIEW_ID_HASH}" + ] = "veeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeery-long-${PREVIEW_ID}-${PREVIEW_ID_HASH}" config = self.load() with pytest.raises(GitOpsException) as ex: config.get_preview_namespace("x") @@ -241,7 +241,7 @@ def test_preview_target_namespace_too_long(self): def test_preview_target_namespace_contains_invalid_variable(self): self.yaml["previewConfig"]["target"]["namespace"] = "{FOO}-bar" - self.assert_load_error("GitOps config template '{FOO}-bar' contains invalid variable: FOO") + self.assert_load_error("GitOps config template '${FOO}-bar' contains invalid variable: FOO") def test_replacements(self): config = self.load() @@ -249,13 +249,13 @@ def test_replacements(self): self.assertEqual(len(config.replacements["file_1.yaml"]), 2) self.assertEqual(config.replacements["file_1.yaml"][0].path, "a.b") - self.assertEqual(config.replacements["file_1.yaml"][0].value_template, "{PREVIEW_HOST}-foo") + self.assertEqual(config.replacements["file_1.yaml"][0].value_template, "${PREVIEW_HOST}-foo") self.assertEqual(config.replacements["file_1.yaml"][1].path, "c.d") - self.assertEqual(config.replacements["file_1.yaml"][1].value_template, "bar-{PREVIEW_NAMESPACE}") + self.assertEqual(config.replacements["file_1.yaml"][1].value_template, "bar-${PREVIEW_NAMESPACE}") self.assertEqual(len(config.replacements["file_2.yaml"]), 1) self.assertEqual(config.replacements["file_2.yaml"][0].path, "e.f") - self.assertEqual(config.replacements["file_2.yaml"][0].value_template, "{GIT_HASH}") + self.assertEqual(config.replacements["file_2.yaml"][0].value_template, "${GIT_HASH}") def test_replacements_missing(self): del self.yaml["previewConfig"]["replace"] @@ -295,4 +295,4 @@ def test_replacements_invalid_list_items_value_not_a_string(self): def test_replacements_invalid_list_items_unknown_variable(self): self.yaml["previewConfig"]["replace"]["file_2.yaml"][0]["value"] = "{FOO}bar" - self.assert_load_error("Replacement value '{FOO}bar' for path 'e.f' contains invalid variable: FOO") + self.assert_load_error("Replacement value '${FOO}bar' for path 'e.f' contains invalid variable: FOO") diff --git a/tests/test_gitops_config_v2.py b/tests/test_gitops_config_v2.py new file mode 100644 index 00000000..f3c6dd18 --- /dev/null +++ b/tests/test_gitops_config_v2.py @@ -0,0 +1,298 @@ +import unittest +import pytest + +from gitopscli.gitops_config import GitOpsConfig +from gitopscli.gitops_exception import GitOpsException + + +class GitOpsConfigV2Test(unittest.TestCase): + def setUp(self): + self.yaml = { + "apiVersion": "v2_beta", + "applicationName": "my-app", + "previewConfig": { + "host": "my-${PREVIEW_ID}-${PREVIEW_ID_HASH}-host-template", + "template": { + "organisation": "my-template-org", + "repository": "my-template-repo", + "branch": "my-template-branch", + "path": ".my-template-dir/${APPLICATION_NAME}", + }, + "target": { + "organisation": "my-target-org", + "repository": "my-target-repo", + "branch": "my-target-branch", + "namespace": "${APPLICATION_NAME}-${PREVIEW_ID_HASH}-dev", + }, + "replace": { + "file_1.yaml": [ + {"path": "a.b", "value": "${PREVIEW_HOST}-foo"}, + {"path": "c.d", "value": "bar-${PREVIEW_NAMESPACE}"}, + ], + "file_2.yaml": [{"path": "e.f", "value": "${GIT_HASH}"}], + }, + }, + } + + def load(self) -> GitOpsConfig: + return GitOpsConfig.from_yaml(self.yaml) + + def assert_load_error(self, error_msg: str) -> None: + with pytest.raises(GitOpsException) as ex: + self.load() + self.assertEqual(error_msg, str(ex.value)) + + def test_apiVersion(self): + config = self.load() + self.assertEqual(config.api_version, 2) + + def test_invalid_apiVersion(self): + self.yaml["apiVersion"] = "foo" + self.assert_load_error("GitOps config apiVersion 'foo' is not supported!") + + def test_application_name(self): + config = self.load() + self.assertEqual(config.application_name, "my-app") + + def test_application_name_missing(self): + del self.yaml["applicationName"] + self.assert_load_error("Key 'applicationName' not found in GitOps config!") + + def test_application_name_not_a_string(self): + self.yaml["applicationName"] = 1 + self.assert_load_error("Item 'applicationName' should be a string in GitOps config!") + + def test_preview_host_template(self): + config = self.load() + self.assertEqual(config.preview_host_template, "my-${PREVIEW_ID}-${PREVIEW_ID_HASH}-host-template") + + def test_preview_host(self): + config = self.load() + self.assertEqual( + config.get_preview_host("PREVIEW_ID/with_odd_chars__"), + "my-preview-id-with-odd-chars-cd2cb125-host-template", + ) + + def test_preview_host_missing(self): + del self.yaml["previewConfig"]["host"] + self.assert_load_error("Key 'previewConfig.host' not found in GitOps config!") + + def test_preview_host_not_a_string(self): + self.yaml["previewConfig"]["host"] = [] + self.assert_load_error("Item 'previewConfig.host' should be a string in GitOps config!") + + def test_preview_host_contains_invalid_variable(self): + self.yaml["previewConfig"]["host"] = "${FOO}-bar" + self.assert_load_error("GitOps config template '${FOO}-bar' contains invalid variable: FOO") + + def test_preview_template_organisation(self): + config = self.load() + self.assertEqual(config.preview_template_organisation, "my-template-org") + + def test_preview_template_organisation_default(self): + del self.yaml["previewConfig"]["template"]["organisation"] + config = self.load() + self.assertEqual(config.preview_template_organisation, "my-target-org") + + def test_preview_template_organisation_not_a_string(self): + self.yaml["previewConfig"]["template"]["organisation"] = True + self.assert_load_error("Item 'previewConfig.template.organisation' should be a string in GitOps config!") + + def test_preview_template_repository(self): + config = self.load() + self.assertEqual(config.preview_template_repository, "my-template-repo") + + def test_preview_template_repository_default(self): + del self.yaml["previewConfig"]["template"]["repository"] + config = self.load() + self.assertEqual(config.preview_template_repository, "my-target-repo") + + def test_preview_template_repository_not_a_string(self): + self.yaml["previewConfig"]["template"]["repository"] = [] + self.assert_load_error("Item 'previewConfig.template.repository' should be a string in GitOps config!") + + def test_preview_template_branch(self): + config = self.load() + self.assertEqual(config.preview_template_branch, "my-template-branch") + + def test_preview_template_branch_default(self): + del self.yaml["previewConfig"]["template"]["branch"] + config = self.load() + self.assertEqual(config.preview_template_branch, "my-target-branch") + + del self.yaml["previewConfig"]["target"]["branch"] + config = self.load() + self.assertIsNone(config.preview_template_branch) + + def test_preview_template_branch_not_a_string(self): + self.yaml["previewConfig"]["template"]["branch"] = [] + self.assert_load_error("Item 'previewConfig.template.branch' should be a string in GitOps config!") + + def test_preview_template_path(self): + config = self.load() + self.assertEqual(config.preview_template_path, ".my-template-dir/my-app") + + def test_preview_template_path_default(self): + del self.yaml["previewConfig"]["template"]["path"] + config = self.load() + self.assertEqual(config.preview_template_path, ".preview-templates/my-app") + + def test_preview_template_path_not_a_string(self): + self.yaml["previewConfig"]["template"]["path"] = [] + self.assert_load_error("Item 'previewConfig.template.path' should be a string in GitOps config!") + + def test_preview_template_path_contains_invalid_variable(self): + self.yaml["previewConfig"]["template"]["path"] = "${FOO}-bar" + self.assert_load_error("GitOps config template '${FOO}-bar' contains invalid variable: FOO") + + def test_preview_target_organisation(self): + config = self.load() + self.assertEqual(config.preview_target_organisation, "my-target-org") + + def test_preview_target_organisation_missing(self): + del self.yaml["previewConfig"]["target"]["organisation"] + self.assert_load_error("Key 'previewConfig.target.organisation' not found in GitOps config!") + + def test_preview_target_organisation_not_a_string(self): + self.yaml["previewConfig"]["target"]["organisation"] = [] + self.assert_load_error("Item 'previewConfig.target.organisation' should be a string in GitOps config!") + + def test_preview_target_repository(self): + config = self.load() + self.assertEqual(config.preview_target_repository, "my-target-repo") + + def test_preview_target_repository_missing(self): + del self.yaml["previewConfig"]["target"]["repository"] + self.assert_load_error("Key 'previewConfig.target.repository' not found in GitOps config!") + + def test_preview_target_repository_not_a_string(self): + self.yaml["previewConfig"]["target"]["repository"] = [] + self.assert_load_error("Item 'previewConfig.target.repository' should be a string in GitOps config!") + + def test_preview_target_branch(self): + config = self.load() + self.assertEqual(config.preview_target_branch, "my-target-branch") + + def test_preview_target_branch_default(self): + del self.yaml["previewConfig"]["target"]["branch"] + config = self.load() + self.assertIsNone(config.preview_target_branch) + + def test_preview_target_branch_not_a_string(self): + self.yaml["previewConfig"]["target"]["branch"] = [] + self.assert_load_error("Item 'previewConfig.target.branch' should be a string in GitOps config!") + + def test_is_preview_template_equal_target(self): + for x in {"organisation", "repository", "branch"}: + self.yaml["previewConfig"]["template"][x] = self.yaml["previewConfig"]["target"][x] + + for x in {"organisation", "repository", "branch"}: + self.yaml["previewConfig"]["template"][x] = "custom-template-value" + config = self.load() + self.assertFalse(config.is_preview_template_equal_target(), x) + + del self.yaml["previewConfig"]["template"][x] + config = self.load() + self.assertTrue(config.is_preview_template_equal_target(), x) + + self.yaml["previewConfig"]["template"][x] = self.yaml["previewConfig"]["target"][x] + config = self.load() + self.assertTrue(config.is_preview_template_equal_target(), x) + + def test_preview_target_namespace(self): + config = self.load() + self.assertEqual(config.preview_target_namespace_template, "${APPLICATION_NAME}-${PREVIEW_ID_HASH}-dev") + self.assertEqual(config.get_preview_namespace("preview-1"), "my-app-3e355b4a-dev") + + def test_preview_target_namespace_default(self): + del self.yaml["previewConfig"]["target"]["namespace"] + config = self.load() + self.assertEqual( + config.preview_target_namespace_template, "${APPLICATION_NAME}-${PREVIEW_ID}-${PREVIEW_ID_HASH}-preview" + ) + actual_namespace = config.get_preview_namespace( + "Very long preview ID. It will be cut to have max 63 chars of namespace in total!!" + ) + self.assertEqual(actual_namespace, "my-app-very-long-preview-id-it-will-be-cut-to-05d9825a-preview") + self.assertTrue(len(actual_namespace) <= 63) + + def test_preview_target_namespace_not_a_string(self): + self.yaml["previewConfig"]["target"]["namespace"] = [] + self.assert_load_error("Item 'previewConfig.target.namespace' should be a string in GitOps config!") + + def test_preview_target_namespace_invalid_template(self): + self.yaml["previewConfig"]["target"]["namespace"] = "-*+ยง-weird chars-${PREVIEW_ID_HASH}" + config = self.load() + with pytest.raises(GitOpsException) as ex: + config.get_preview_namespace("preview-1") + self.assertEqual("Invalid character in preview namespace: '*'", str(ex.value)) + + def test_preview_target_namespace_too_long(self): + self.yaml["previewConfig"]["target"][ + "namespace" + ] = "veeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeery-long-${PREVIEW_ID}-${PREVIEW_ID_HASH}" + config = self.load() + with pytest.raises(GitOpsException) as ex: + config.get_preview_namespace("x") + self.assertEqual( + "Preview namespace is too long (max 63 chars): veeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeery-long--2d711642 (68 chars)", + str(ex.value), + ) + + def test_preview_target_namespace_contains_invalid_variable(self): + self.yaml["previewConfig"]["target"]["namespace"] = "${FOO}-bar" + self.assert_load_error("GitOps config template '${FOO}-bar' contains invalid variable: FOO") + + def test_replacements(self): + config = self.load() + self.assertEqual(config.replacements.keys(), {"file_1.yaml", "file_2.yaml"}) + + self.assertEqual(len(config.replacements["file_1.yaml"]), 2) + self.assertEqual(config.replacements["file_1.yaml"][0].path, "a.b") + self.assertEqual(config.replacements["file_1.yaml"][0].value_template, "${PREVIEW_HOST}-foo") + self.assertEqual(config.replacements["file_1.yaml"][1].path, "c.d") + self.assertEqual(config.replacements["file_1.yaml"][1].value_template, "bar-${PREVIEW_NAMESPACE}") + + self.assertEqual(len(config.replacements["file_2.yaml"]), 1) + self.assertEqual(config.replacements["file_2.yaml"][0].path, "e.f") + self.assertEqual(config.replacements["file_2.yaml"][0].value_template, "${GIT_HASH}") + + def test_replacements_missing(self): + del self.yaml["previewConfig"]["replace"] + self.assert_load_error("Key 'previewConfig.replace' not found in GitOps config!") + + def test_replacements_not_an_object(self): + self.yaml["previewConfig"]["replace"] = "foo" + self.assert_load_error("Item 'previewConfig.replace' should be an object in GitOps config!") + + def test_replacements_file_item_not_a_list(self): + self.yaml["previewConfig"]["replace"]["file_1.yaml"] = 1 + self.assert_load_error("Item 'previewConfig.replace.file_1\\.yaml' should be a list in GitOps config!") + + def test_replacements_invalid_list(self): + self.yaml["previewConfig"]["replace"]["file_1.yaml"] = ["foo"] + self.assert_load_error("Item 'previewConfig.replace.file_1\\.yaml.[0]' should be an object in GitOps config!") + + def test_replacements_invalid_list_items_missing_path(self): + del self.yaml["previewConfig"]["replace"]["file_1.yaml"][1]["path"] + self.assert_load_error("Key 'previewConfig.replace.file_1\\.yaml.[1].path' not found in GitOps config!") + + def test_replacements_invalid_list_items_missing_value(self): + del self.yaml["previewConfig"]["replace"]["file_1.yaml"][0]["value"] + self.assert_load_error("Key 'previewConfig.replace.file_1\\.yaml.[0].value' not found in GitOps config!") + + def test_replacements_invalid_list_items_path_not_a_string(self): + self.yaml["previewConfig"]["replace"]["file_1.yaml"][0]["path"] = 42 + self.assert_load_error( + "Item 'previewConfig.replace.file_1\\.yaml.[0].path' should be a string in GitOps config!" + ) + + def test_replacements_invalid_list_items_value_not_a_string(self): + self.yaml["previewConfig"]["replace"]["file_2.yaml"][0]["value"] = [] + self.assert_load_error( + "Item 'previewConfig.replace.file_2\\.yaml.[0].value' should be a string in GitOps config!" + ) + + def test_replacements_invalid_list_items_unknown_variable(self): + self.yaml["previewConfig"]["replace"]["file_2.yaml"][0]["value"] = "${FOO}bar" + self.assert_load_error("Replacement value '${FOO}bar' for path 'e.f' contains invalid variable: FOO") From 8452a034deb1dab4c03bb8cdc929e7d3246089ff Mon Sep 17 00:00:00 2001 From: Christian Siegel Date: Tue, 3 Aug 2021 11:58:23 +0200 Subject: [PATCH 2/2] doc(GitOpsConfig): Fix config example Fixes #162 (until we officially release and document apiVersion v2) --- docs/includes/preview-configuration.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/includes/preview-configuration.md b/docs/includes/preview-configuration.md index b97051ef..bb063130 100644 --- a/docs/includes/preview-configuration.md +++ b/docs/includes/preview-configuration.md @@ -30,7 +30,7 @@ Make sure that your *app repository* contains a `.gitops.config.yaml` file. This apiVersion: v1 applicationName: app-xy previewConfig: - host: {PREVIEW_NAMESPACE}.example.tld + host: '{PREVIEW_NAMESPACE}.example.tld' # template: # optional section # organisation: templates # optional (default: target.organisation) # repository: template-repo # optional (default: target.repository) @@ -40,18 +40,18 @@ previewConfig: organisation: deployments repository: deployment-config-repo # 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}-preview', - # Invalid characters in PREVIEW_ID will be replaced. PREVIEW_ID will be - # truncated if max namespace length exceeds 63 chars.) + namespace: '{APPLICATION_NAME}-{PREVIEW_ID_HASH}-preview' # optional (default: '{APPLICATION_NAME}-{PREVIEW_ID}-{PREVIEW_ID_HASH}-preview', + # Invalid characters in PREVIEW_ID will be replaced. PREVIEW_ID will be + # truncated if max namespace length exceeds 63 chars.) replace: Chart.yaml: - path: name - value: {PREVIEW_NAMESPACE} + value: '{PREVIEW_NAMESPACE}' values.yaml: - path: app.image value: registry.example.tld/my-app:{GIT_HASH} - path: route.host - value: {PREVIEW_HOST} + value: '{PREVIEW_HOST}' ``` #### Variables