Skip to content

Commit

Permalink
feat(GitOpsConfig): introduce apiVersion v2_beta
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
christiansiegel committed Aug 3, 2021
1 parent 77df7cf commit 8d9546a
Show file tree
Hide file tree
Showing 6 changed files with 368 additions and 44 deletions.
66 changes: 46 additions & 20 deletions gitopscli/gitops_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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!"
Expand Down Expand Up @@ -98,30 +98,30 @@ 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)"
)

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)
Expand Down Expand Up @@ -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")
Expand All @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -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(
Expand All @@ -308,15 +334,15 @@ 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,
preview_target_organisation=preview_target_organisation,
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,
)
12 changes: 6 additions & 6 deletions tests/commands/test_create_preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,22 +54,22 @@ 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",
preview_template_branch="template-branch",
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}"),
],
},
)
Expand Down
2 changes: 1 addition & 1 deletion tests/commands/test_delete_preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -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={},
)

Expand Down
12 changes: 6 additions & 6 deletions tests/test_gitops_config_v0.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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"]
Expand Down Expand Up @@ -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"
Expand Down
22 changes: 11 additions & 11 deletions tests/test_gitops_config_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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!!"
Expand All @@ -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")
Expand All @@ -241,21 +241,21 @@ 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()
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"][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"]
Expand Down Expand Up @@ -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")
Loading

0 comments on commit 8d9546a

Please sign in to comment.