diff --git a/r11k/interval.py b/r11k/interval.py index c51ffc353600c8258b0891033e227c64aee3baad..a29971ec590d6dd60d196cc09d308daba801caf7 100644 --- a/r11k/interval.py +++ b/r11k/interval.py @@ -49,8 +49,8 @@ class Interval: def __init__(self, minimi: VersionInfo = min_version, maximi: VersionInfo = max_version, - min_cmp: Comp = operator.gt, - max_cmp: Comp = operator.le, + min_cmp: Comp = operator.ge, + max_cmp: Comp = operator.lt, *, by: Optional[str] = None, min_by: Optional[str] = None, @@ -197,7 +197,7 @@ def intersect(a: Interval, b: Interval) -> Interval: max_cmp: Comp if not overlaps(a, b): - raise ValueError("Only overlapping intervals can intersect") + raise ValueError(f"Only overlapping intervals can intersect, {a}, {b}") # Lower Bound if a.minimi == b.minimi: diff --git a/r11k/puppetfile.py b/r11k/puppetfile.py index 72b2938a247401e69af05cf89cf36489e40e5331..19cbdf56baeeabe42742d2d15eab981c2b8bb6fe 100644 --- a/r11k/puppetfile.py +++ b/r11k/puppetfile.py @@ -46,6 +46,7 @@ modules: """ import logging +import os import os.path from dataclasses import dataclass, field @@ -69,6 +70,7 @@ from r11k.puppetmodule import ( logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) @dataclass @@ -251,6 +253,90 @@ def update_module_names(modules: list[PuppetModule]) -> None: module.name = metadata.name +def build_initial_constraints(modules: list[PuppetModule]) -> dict[str, list[Interval]]: + """ + Build the initial constraint set. + + Takes a list of puppet module information. Returns a mapping from + each modules name, to a list of all constraints placed on that + module. + """ + # Dict from which modules we want, to which versions we can have + constraints: dict[str, list[Interval]] = {} + + # For each module in puppetfile + for module in modules: + logger.debug(module) + # collect its dependencies + for depspec in module.metadata.dependencies: + # and merge them to the dependency set + constraints.setdefault(depspec.name, []) \ + .append(depspec.interval(by=module.name)) + return constraints + + +def resolve_constraints(constraints: dict[str, list[Interval]]) -> dict[str, Interval]: + """ + Merge all constraints on a module to a final constraint. + + For each module, find the intersection of all modules. If none + intersection can be found a warning is raised, and the interval + from the smallest minimum requested to the largest value requested + is used. + """ + # resolve constraints + resolved_constraints: dict[str, Interval] = {} + for module_name, intervals in constraints.items(): + try: + resolved_constraints[module_name] = reduce(interval.intersect, intervals) + except ValueError: + print("Print invalid constraints for", module_name, ":") + for intv in intervals: + print(' ', intv) + inter = Interval(min(i.minimi for i in intervals), + max(i.maximi for i in intervals),) + print("Defaulting to", inter) + resolved_constraints[module_name] = inter + + return resolved_constraints + + +def build_next_module_set(puppetfile: PuppetFile, + known_modules: dict[str, PuppetModule], + resolved_constraints: dict[str, Interval]) -> list[PuppetModule]: + # build the next iteration of modules + # If this turns out to be identical to modules, then + # everything is resolved and we exit. Otherwise we continue + # the loop + next_modules: dict[str, PuppetModule] = {} + for name, interval_ in resolved_constraints.items(): + if module_ := known_modules.get(name): + # An explicitly specified version is always used. + # TODO warn if that version is incompatible with our interval. + next_modules[name] = module_ + else: + # If we haven't explicitly stated the module, assume that + # it's to be downloaded from the puppet forge. + # TODO possibly allow dependencies on other git modules. + # + module_ = ForgePuppetModule(name, config=puppetfile.config) + # TODO keep interval instead of locking it in + # TODO continue here, fix this + module_.version = interval_.newest(module_.versions()) + next_modules[name] = module_ + known_modules[name] = module_ + + # TODO what are we even forcing here? + for name, module_ in puppetfile.modules.items(): + if x := next_modules.get(name): + if x.version != module_.version: + logger.warn('Forcing %s', name) + # print(x, module_) + next_modules[name] = module_ + + return list(next_modules.values()) + + # TODO figure out proper way to propagate config through everything def find_all_modules_for_environment(puppetfile: PuppetFile) -> ResolvedPuppetFile: """ @@ -262,6 +348,7 @@ def find_all_modules_for_environment(puppetfile: PuppetFile) -> ResolvedPuppetFi update_module_names(modules) + # Explicitly specified modules. known_modules: dict[str, PuppetModule] = {} for module in modules: known_modules[module.name] = module @@ -269,55 +356,18 @@ def find_all_modules_for_environment(puppetfile: PuppetFile) -> ResolvedPuppetFi # Resolve all modules, restarting with the new set of modules # repeatedly until we find a stable point. while True: - # Dict from which modules we want, to which versions we can have - constraints: dict[str, list[Interval]] = {} - - # For each module in puppetfile - for module in modules: - logger.debug(module) - # collect its dependencies - for depspec in module.metadata.dependencies: - # and merge them to the dependency set - constraints.setdefault(depspec.name, []) \ - .append(depspec.interval(by=module.name)) - - # resolve constraints - resolved_constraints: dict[str, Interval] = {} - for module_name, intervals in constraints.items(): - # TODO what to do if invalid constraints - resolved_constraints[module_name] = reduce(interval.intersect, intervals) - - # build the next iteration of modules - # If this turns out to be identical to modules, then - # everything is resolved and we exit. Otherwise we continue - # the loop - next_modules: dict[str, PuppetModule] = {} - for name, interval_ in resolved_constraints.items(): - if module_ := known_modules.get(name): - next_modules[name] = module_ - else: - # TODO keep interval instead of locking it in - module_ = ForgePuppetModule(name, config=puppetfile.config) - # TODO this crashes when the dependency is a git - # module, since it tries to fetch a forge module of - # that name. - # TODO continue here, fix this - module_.version = interval_.newest(module_.versions()) - next_modules[name] = module_ - known_modules[name] = module_ - - # TODO what are we even forcing here? - for name, module_ in puppetfile.modules.items(): - if next_modules.get(name): - logger.warn('Forcing %s', name) - next_modules[name] = module_ + constraints = build_initial_constraints(modules) + resolved_constraints = resolve_constraints(constraints) - next_modules_list: list[PuppetModule] = list(next_modules.values()) + next_modules: list[PuppetModule] = build_next_module_set(puppetfile, + known_modules, + resolved_constraints) - if next_modules_list == modules: + if next_modules == modules: break - modules = next_modules_list + modules = next_modules + print() return ResolvedPuppetFile(modules={m.name: m for m in modules}, environment_name=puppetfile.environment_name, diff --git a/r11k/puppetmodule/git.py b/r11k/puppetmodule/git.py index e99008c83c67da8001db7b94c315aaa2cd9fbd6c..f2ddbf75e87cd20630a2e2f22379ea1c56717a8d 100644 --- a/r11k/puppetmodule/git.py +++ b/r11k/puppetmodule/git.py @@ -157,6 +157,7 @@ class GitPuppetModule(PuppetModule): remote = find_remote(repo.remotes, git_url) if not remote: + print(repo) raise ValueError('Existing repo, but with different remote') last_modified = repo_last_fetch(repo)