Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support different versions per instance of a component #559

Merged
merged 19 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3bc7026
Remove no-longer needed pylint ignores for type annotations
simu Jul 19, 2022
a154173
WIP - Support different versions per alias of a component
simu Jul 18, 2022
8942dfd
Update tests for new alias checkout structure
simu Jul 19, 2022
74bc54d
WIP - Allow users to specify instance versions
simu Jul 19, 2022
591f081
Update tests to work with new alias version structure
simu Jul 19, 2022
a588aea
Merge branch 'master' into feat/per-alias-versions
HappyTetrahedron Jan 7, 2025
1ed6ef6
Add support for overriding repo URL in component instances
HappyTetrahedron Jan 21, 2025
7894de9
Verify whether component supports multi-versioning
HappyTetrahedron Jan 21, 2025
7ffd2f2
Apply black, flake8, mypy
HappyTetrahedron Jan 21, 2025
ea934d2
Merge branch 'master' into feat/per-alias-versions
HappyTetrahedron Jan 21, 2025
5b1b243
Increase code coverage and support overriding repo path
HappyTetrahedron Jan 22, 2025
4eed4a8
Increase coverage of error cases
HappyTetrahedron Jan 24, 2025
ff83124
Implement suggestions from code review
HappyTetrahedron Jan 28, 2025
fc7e23a
Raise if alias was not registered when attempting to read alias direc…
HappyTetrahedron Jan 30, 2025
d9bc703
Add documentation
HappyTetrahedron Jan 30, 2025
8329fcc
Update documentation to note that we now create a Git worktree per in…
simu Jan 31, 2025
84bf538
Change "multi_versioning" to "multi_version"
HappyTetrahedron Jan 31, 2025
21be94e
Update docs to incorporate PR feedback
HappyTetrahedron Jan 31, 2025
92dfbf3
Merge branch 'master' into feat/per-alias-versions
HappyTetrahedron Jan 31, 2025
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: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,9 @@ Commodore also supports additional processing on the output of Kapitan, such as

1. Run linting and tests

Auto format with autopep8
Automatically apply Black formatting
```console
poetry run autopep
poetry run black .
```

List all Tox targets
Expand All @@ -132,7 +132,7 @@ Commodore also supports additional processing on the output of Kapitan, such as

Run just a specific target
```console
poetry run tox -e py38
poetry run tox -e py312
```


Expand Down
25 changes: 18 additions & 7 deletions commodore/component/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class Component:
_version: Optional[str] = None
_dir: P
_sub_path: str
_aliases: dict[str, str]
_aliases: dict[str, tuple[str, str]]
_work_dir: Optional[P]

@classmethod
Expand Down Expand Up @@ -59,7 +59,7 @@ def __init__(
self.version = version
self._sub_path = sub_path
self._repo = None
self._aliases = {self.name: self.version or ""}
self._aliases = {self.name: (self.version or "", self.sub_path or "")}
self._work_dir = work_dir

@property
Expand Down Expand Up @@ -134,6 +134,10 @@ def sub_path(self) -> str:
def repo_directory(self) -> P:
return self._dir

@property
def work_directory(self) -> Optional[P]:
return self._work_dir

@property
def target_directory(self) -> P:
return self.alias_directory(self.name)
Expand All @@ -156,6 +160,8 @@ def alias_directory(self, alias: str) -> P:
apath = self._dependency.get_component(alias)
if not apath:
raise ValueError(f"unknown alias {alias} for component {self.name}")
if alias in self._aliases:
return apath / self._aliases[alias][1]
return apath / self._sub_path

def alias_class_file(self, alias: str) -> P:
Expand Down Expand Up @@ -202,7 +208,7 @@ def checkout(self):
)
self._dependency.checkout_component(self.name, self.version)

def register_alias(self, alias: str, version: str):
def register_alias(self, alias: str, version: str, sub_path: str = ""):
if not self._work_dir:
raise ValueError(
f"Can't register alias on component {self.name} "
Expand All @@ -212,19 +218,24 @@ def register_alias(self, alias: str, version: str):
raise ValueError(
f"alias {alias} already registered on component {self.name}"
)
self._aliases[alias] = version
self._aliases[alias] = (version, sub_path)
if self._dependency:
self._dependency.register_component(
alias, component_dir(self._work_dir, alias)
)

def checkout_alias(self, alias: str):
def checkout_alias(
self, alias: str, alias_dependency: Optional[MultiDependency] = None
):
if alias not in self._aliases:
raise ValueError(
f"alias {alias} is not registered on component {self.name}"
)
if self._dependency:
self._dependency.checkout_component(alias, self._aliases[alias])

if alias_dependency:
alias_dependency.checkout_component(alias, self._aliases[alias][0])
elif self._dependency:
self._dependency.checkout_component(alias, self._aliases[alias][0])

def is_checked_out(self) -> bool:
return self.target_dir is not None and self.target_dir.is_dir()
Expand Down
33 changes: 29 additions & 4 deletions commodore/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,14 +377,27 @@ def get_component_aliases(self):
return self._component_aliases

def register_component_aliases(self, aliases: dict[str, str]):
self._component_aliases = aliases
self._component_aliases.update(aliases)

def verify_component_aliases(self, cluster_parameters: dict):
print(cluster_parameters)
for alias, cn in self._component_aliases.items():
if alias != cn and not _component_is_aliasable(cluster_parameters, cn):
raise click.ClickException(
f"Component {cn} with alias {alias} does not support instantiation."
if alias != cn:
if not _component_is_aliasable(cluster_parameters, cn):
raise click.ClickException(
f"Component {cn} with alias {alias} does not support instantiation."
)

cv = cluster_parameters.get("components", {}).get(alias, {})
alias_has_version = (
cv.get("url") is not None or cv.get("version") is not None
)
if alias_has_version and not _component_supports_alias_version(
cluster_parameters, cn, alias
):
raise click.ClickException(
f"Component {cn} with alias {alias} does not support overriding instance version."
)

def get_component_alias_versioninfos(self) -> dict[str, InstanceVersionInfo]:
return {
Expand Down Expand Up @@ -453,6 +466,18 @@ def _component_is_aliasable(cluster_parameters: dict, component_name: str):
return cmeta.get("multi_instance", False)


def _component_supports_alias_version(
cluster_parameters: dict,
component_name: str,
alias: str,
):
ckey = component_parameters_key(component_name)
cmeta = cluster_parameters[ckey].get("_metadata", {})
akey = component_parameters_key(alias)
ameta = cluster_parameters.get(akey, {}).get("_metadata", {})
return cmeta.get("multi_versioning", False) and ameta.get("multi_versioning", False)


def set_fact_value(facts: dict[str, Any], raw_key: str, value: Any) -> None:
"""Set value for nested fact at `raw_key` (expected form `path.to.key`) to `value`.

Expand Down
32 changes: 20 additions & 12 deletions commodore/dependency_mgmt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,19 @@ def fetch_components(cfg: Config):

c = components[component]
aspec = cspecs[alias]
if aspec.url != c.repo_url or aspec.path != c._sub_path:
# TODO: Figure out how we'll handle URL/subpath overrides
raise NotImplementedError(
"URL/path override for component alias not supported"
)
adep = None
if aspec.url != c.repo_url:
adep = cfg.register_dependency_repo(aspec.url)
wdir = c.work_directory
if not wdir:
raise ValueError(
"Cannot checkout repo for component alias if component does not have a working directory."
)
adep.register_component(alias, component_dir(wdir, alias))

print(alias, aspec)
c.register_alias(alias, aspec.version)
c.checkout_alias(alias)
c.register_alias(alias, aspec.version, aspec.path)
c.checkout_alias(alias, adep)

create_alias_symlinks(cfg, c, alias)

Expand Down Expand Up @@ -211,9 +216,8 @@ def register_components(cfg: Config):

c = registered_components[cn]
aspec = cspecs[alias]
if aspec.url != c.repo_url or aspec.path != c.sub_path:
raise NotImplementedError("Changing alias sub path / URL NYI")
c.register_alias(alias, aspec.version)
if aspec.url == c.repo_url and aspec.path == c.sub_path:
c.register_alias(alias, aspec.version)
if not component_dir(cfg.work_dir, alias).is_dir():
raise click.ClickException(f"Missing alias checkout for '{alias} as {cn}'")

Expand Down Expand Up @@ -297,8 +301,12 @@ def verify_version_overrides(cluster_parameters, component_aliases: dict[str, st
for cname, cspec in cluster_parameters["components"].items():
if cname in aliases:
# We don't require an url in component alias version configs
continue
if "url" not in cspec:
# but we do require the base component to have one
if component_aliases[cname] not in cluster_parameters["components"]:
errors.append(
f"component '{component_aliases[cname]}' (imported as {cname})"
)
elif "url" not in cspec:
errors.append(f"component '{cname}'")

for pname, pspec in cluster_parameters.get("packages", {}).items():
Expand Down
2 changes: 1 addition & 1 deletion commodore/dependency_mgmt/version_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def parse(
if base_config:
url = info.get("url", base_config.url)
version = info.get("version", base_config.version)
if path not in info:
if not path:
path = base_config.path
else:
url = info["url"]
Expand Down
92 changes: 92 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,98 @@ def test_verify_component_aliases_explicit_no_instance(config):
config.verify_component_aliases(params)


def test_verify_component_aliases_explicit_no_multiversion_exception(config):
alias_data = {"baz": "bar"}
config.register_component_aliases(alias_data)
params = {
"components": {
"bar": {"url": "foo", "version": "v1.0.0"},
"baz": {"version": "v1.1.0"},
},
"bar": {
"_metadata": {"multi_instance": True, "multi_versioning": False},
"namespace": "syn-bar",
},
"baz": {
"_metadata": {"multi_instance": True, "multi_versioning": False},
"namespace": "syn-baz",
},
}

with pytest.raises(click.ClickException) as e:
config.verify_component_aliases(params)

assert (
"Component bar with alias baz does not support overriding instance version."
in str(e.value)
)


def test_verify_component_aliases_explicit_no_multiversion_in_alias_exception(config):
alias_data = {"baz": "bar"}
config.register_component_aliases(alias_data)
params = {
"components": {
"bar": {"url": "foo", "version": "v1.0.0"},
"baz": {"version": "v1.1.0"},
},
"bar": {
"_metadata": {"multi_instance": True, "multi_versioning": True},
"namespace": "syn-bar",
},
"baz": {
"_metadata": {"multi_instance": True, "multi_versioning": False},
"namespace": "syn-baz",
},
}

with pytest.raises(click.ClickException) as e:
config.verify_component_aliases(params)

assert (
"Component bar with alias baz does not support overriding instance version."
in str(e.value)
)


def test_verify_component_multiversion_exception(config):
alias_data = {"baz": "bar"}
config.register_component_aliases(alias_data)
params = {
"components": {
"bar": {"url": "foo", "version": "v1.0.0"},
"baz": {"version": "v1.1.0"},
},
"bar": {"_metadata": {"multi_instance": True}},
}

with pytest.raises(click.ClickException) as e:
config.verify_component_aliases(params)

assert (
"Component bar with alias baz does not support overriding instance version."
in str(e.value)
)


def test_verify_component_multiversion(config):
alias_data = {"baz": "bar"}
config.register_component_aliases(alias_data)
params = {
"components": {
"bar": {"url": "foo", "version": "v1.0.0"},
"baz": {"version": "v1.1.0"},
},
"bar": {
"_metadata": {"multi_instance": True, "multi_versioning": True},
"namespace": "syn-bar",
},
"baz": {"_metadata": {"multi_versioning": True}, "namespace": "syn-baz"},
}

config.verify_component_aliases(params)


def test_verify_component_aliases_metadata(config):
alias_data = {"baz": "bar"}
config.register_component_aliases(alias_data)
Expand Down
Loading
Loading