-
Notifications
You must be signed in to change notification settings - Fork 199
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
feat(inventory): introduce OmegaConf as optional inventory backend #995
Closed
Closed
Changes from all commits
Commits
Show all changes
42 commits
Select commit
Hold shift + click to select a range
ea3131b
feat(inv): introduce OmegaConf as optional inventory backend
MatteoVoges 9c57cae
remove print statement
MatteoVoges 770404c
fix(lint): fix linting
MatteoVoges 5de9d50
Merge branch 'master' into pluggable-inventory
MatteoVoges c42b1b1
Merge branch 'master' into pluggable-inventory
MatteoVoges 083160f
add first version of migration script
MatteoVoges d9a1d23
add flag behavior for `--compose-node-name` and `-t`
MatteoVoges fc314f7
add resolving step for references in migration script
MatteoVoges 0e76778
feat: finish migration script
MatteoVoges 742b2bb
feat: enable migration support as query
MatteoVoges 79bf1e9
add local module omegaconf to have new features
MatteoVoges 6b54600
add migrated inventory to test the feature
MatteoVoges 60c56ea
refactor: add error handling and correct merging flags
MatteoVoges b1f85e7
deps: add temporarily deps ruamel and regex for migrating
MatteoVoges 3b5c8db
feat: prepare omegaconf for multiprocessing
MatteoVoges d20be19
feat: add generated grammar
MatteoVoges a7944a6
change route of module oc
MatteoVoges 91a85e8
fix: change module import path
MatteoVoges 3217401
Merge branch 'master' into pluggable-inventory
MatteoVoges 3e94da2
fix: change import paths for omegaconf
MatteoVoges 88175c6
feat: resolve relative class name
MatteoVoges 6b53e02
refactor: adapt new merge interface
MatteoVoges 6147f0d
refactor: remove examples inventory for omegaconf
MatteoVoges f38696f
feat: change migration via flag, not query
MatteoVoges 45d44a3
refactor: remove unneccessary debug and comments
MatteoVoges c684376
feat: add option to pull omegaconf locally
MatteoVoges 4a72a40
lint: fix type annotations
MatteoVoges 22fb477
refactor: remove directory oc after pulling
MatteoVoges cfc85b7
feat: support init class
MatteoVoges 5155f0e
perf: use faster function `unsafe_merge`
MatteoVoges be32b38
feat: add more custom resolvers
MatteoVoges 641dc73
refactor: add more resolvers
MatteoVoges 7dc7585
fix: namespace error with flag migrate
MatteoVoges 47b2c54
feat: add ability to define user resolvers in inventory
MatteoVoges b605005
fix: user written resolvers replace system resolvers
MatteoVoges 3c616d5
feat: restructure resolving and migrating
MatteoVoges 08a2194
chore: remove ruamel-yaml and add omegaconf in poetry-file
MatteoVoges 228ac23
Merge branch 'master' into pluggable-inventory
MatteoVoges ba04981
fix: resolver escape_tag was missing braces
MatteoVoges 78e83ff
fix: correct wrong behavior of resolver `tag`
MatteoVoges 1f7923a
feat: prepare support for lint
MatteoVoges 8fd6d93
Merge branch 'dev' into pluggable-inventory
MatteoVoges File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
#!/usr/bin/env python3 | ||
|
||
# Copyright 2023 nexenio | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same copyright msg here too |
||
import logging | ||
import os | ||
import time | ||
|
||
import regex | ||
|
||
from kapitan.errors import InventoryError | ||
from kapitan.resolvers import register_resolvers | ||
from omegaconf import ListMergeMode, Node, OmegaConf, errors | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def inventory_omegaconf( | ||
inventory_path: str, | ||
ignore_class_notfound: bool = False, | ||
targets: list = [], | ||
compose_node_name: bool = False, | ||
) -> dict: | ||
""" | ||
generates inventory from yaml files using OmegaConf | ||
""" | ||
|
||
# add config option to specify paths | ||
targets_searchpath = os.path.join(inventory_path, "targets") | ||
classes_searchpath = os.path.join(inventory_path, "classes") | ||
|
||
register_resolvers(inventory_path) | ||
|
||
selected_targets = [] | ||
|
||
# loop through targets searchpath and load all targets | ||
for root, dirs, files in os.walk(targets_searchpath): | ||
for target_name in files: | ||
target_path = os.path.join(root, target_name) | ||
|
||
# split file extension and check if yml/yaml | ||
target_name, ext = os.path.splitext(target_name) | ||
if ext not in (".yml", ".yaml"): | ||
logger.debug(f"{target_name}: targets have to be .yml or .yaml files.") | ||
# RAISE ERROR | ||
continue | ||
|
||
# skip targets if they are not specified with -t flag | ||
if targets and target_name not in targets: | ||
continue | ||
|
||
# compose node name | ||
if compose_node_name: | ||
target_name = str(os.path.splitext(target_path)[0]).replace(targets_searchpath + os.sep, "") | ||
target_name = target_name.replace("/", ".") | ||
|
||
selected_targets.append({"name": target_name, "path": target_path}) | ||
|
||
# using nodes for reclass legacy code | ||
inv = {"nodes": {}} | ||
|
||
# prepare logging | ||
logger.info(f"Found {len(selected_targets)} targets") | ||
|
||
# load targets | ||
for target in selected_targets: | ||
try: | ||
# start = time.time() | ||
name, config = load_target(target, classes_searchpath, ignore_class_notfound) | ||
inv["nodes"][name] = config | ||
# print(time.time() - start) | ||
except Exception as e: | ||
raise InventoryError(f"{target['name']}: {e}") | ||
|
||
return inv | ||
|
||
|
||
def load_target(target: dict, classes_searchpath: str, ignore_class_notfound: bool = False): | ||
""" | ||
load only one target with all its classes | ||
""" | ||
|
||
target_name = target["name"] | ||
target_path = target["path"] | ||
|
||
target_config = OmegaConf.load(target_path) | ||
target_config_classes = target_config.get("classes", []) | ||
target_config_parameters = OmegaConf.create(target_config.get("parameters", {})) | ||
target_config = {} | ||
|
||
classes_redundancy_check = set() | ||
|
||
# load classes for targets | ||
for class_name in target_config_classes: | ||
# resolve class path | ||
class_path = os.path.join(classes_searchpath, *class_name.split(".")) | ||
|
||
if class_path in classes_redundancy_check: | ||
continue | ||
|
||
classes_redundancy_check.add(class_path) | ||
|
||
if os.path.isfile(class_path + ".yml"): | ||
class_path += ".yml" | ||
elif os.path.isdir(class_path): | ||
# search for init file | ||
init_path = os.path.join(classes_searchpath, *class_name.split("."), "init") + ".yml" | ||
if os.path.isfile(init_path): | ||
class_path = init_path | ||
elif ignore_class_notfound: | ||
logger.debug(f"Could not find {class_path}") | ||
continue | ||
else: | ||
raise InventoryError(f"Class {class_name} not found.") | ||
|
||
# load classes recursively | ||
class_config = OmegaConf.load(class_path) | ||
|
||
# resolve relative class names | ||
new_classes = class_config.pop("classes", []) | ||
for new in new_classes: | ||
if new.startswith("."): | ||
new = ".".join(class_name.split(".")[0:-1]) + new | ||
|
||
target_config_classes.append(new) | ||
|
||
class_config_parameters = OmegaConf.create(class_config.get("parameters", {})) | ||
|
||
# merge target with loaded classes | ||
if target_config_parameters: | ||
target_config_parameters = OmegaConf.unsafe_merge( | ||
class_config_parameters, target_config_parameters, list_merge_mode=ListMergeMode.EXTEND | ||
) | ||
else: | ||
target_config_parameters = class_config_parameters | ||
|
||
if not target_config_parameters: | ||
raise InventoryError("empty target") | ||
|
||
# append meta data (legacy: _reclass_) | ||
target_config_parameters["_reclass_"] = { | ||
"name": { | ||
"full": target_name, | ||
"parts": target_name.split("."), | ||
"path": target_name.replace(".", "/"), | ||
"short": target_name.split(".")[-1], | ||
} | ||
} | ||
|
||
# resolve references / interpolate values | ||
OmegaConf.resolve(target_config_parameters) | ||
target_config["parameters"] = OmegaConf.to_object(target_config_parameters) | ||
|
||
# obtain target name to insert in inv dict | ||
try: | ||
target_name = target_config["parameters"]["kapitan"]["vars"]["target"] | ||
except KeyError: | ||
logger.warning(f"Could not resolve target name on target {target_name}") | ||
|
||
return target_name, target_config | ||
|
||
|
||
def migrate(inventory_path: str) -> None: | ||
"""migrates all .yml/.yaml files in the given path to omegaconfs syntax""" | ||
|
||
for root, subdirs, files in os.walk(inventory_path): | ||
for file in files: | ||
file = os.path.join(root, file) | ||
name, ext = os.path.splitext(file) | ||
|
||
if ext not in (".yml", ".yaml"): | ||
continue | ||
|
||
try: | ||
with open(file, "r+") as file: | ||
content = file.read() | ||
file.seek(0) | ||
|
||
# replace colons in tags and replace _reclass_ with _meta_ | ||
updated_content = regex.sub( | ||
r"(?<!\\)\${([^{}\\]+?)}", | ||
lambda match: "${" | ||
+ match.group(1).replace(":", ".").replace("_reclass_", "_meta_") | ||
+ "}", | ||
content, | ||
) | ||
|
||
# replace escaped tags with specific resolver | ||
excluded_chars = "!" | ||
invalid = any(c in updated_content for c in excluded_chars) | ||
updated_content = regex.sub( | ||
r"\\\${([^{}]+?)}", | ||
lambda match: ("${tag:" if not invalid else "\\\\\\${") + match.group(1) + "}", | ||
updated_content, | ||
) | ||
|
||
file.write(updated_content) | ||
except Exception as e: | ||
InventoryError(f"{file}: error with migration: {e}") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@MatteoVoges this is not needed anymore now that it has been merged right?