Skip to content

Commit

Permalink
Add support for config_merger to filter root keys (#162)
Browse files Browse the repository at this point in the history
  • Loading branch information
azun authored Jan 30, 2024
1 parent 51976af commit 6b75194
Show file tree
Hide file tree
Showing 14 changed files with 228 additions and 7 deletions.
52 changes: 51 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ kubeconfig_location: "{{env(KUBECONFIG)}}"
```


## himl config merger
## himl-config-merger

The `himl-config-merger` script, contains logic of merging a hierarchical config directory and creating the end result YAML files.

Expand Down Expand Up @@ -343,6 +343,56 @@ Leveraging HIML, the config-merger script loads the configs tree structure and d
Under each level, there is a mandatory "level key" that is used by config-merger for computing the end result. This key should be present in one of the files under each level. (eg. env.yaml under env).
### Output filtering
Some configs that are specified in the higher levels of the directory tree might not be needed in the end (leaf) result. For this reason, the config-merger script can apply a set of filter rules that are specified via the `--filter-rules-key` parameter. This property must be present in the config and contains rules for removing root level keys from the output. The filter is applied if the selector object matches a subset of the output keys and will keep the keys specified in the `values` list or the keys that match the `regex` pattern.
```yaml
# intermediate config after hierarchical merge
env: dev
cluster: cluster1
region: us-east-1
key1: persisted
key2: dropped
keep_1: persisted
tags:
cost_center: 123
_filters:
- selector:
env: "dev"
keys:
values:
- key1
regex: "keep_.*"
- selector:
cluster:
regex: "cluster1"
keys:
values:
- tags
```

Build the output with filtering:
```sh
himl-config-merger examples/filters --output-dir merged_output --levels env region cluster --leaf-directories cluster --filter-rules-key _filters
```

```yaml
# output after filtering
env: dev
cluster: cluster1
region: us-east-1
key1: persisted
keep_1: persisted
tags:
cost_center: 123
```
#### Filtering limitations
Rule selectors and keys filtering only works at the root level of the config. It is not possible to filter nested keys.
### Extra merger features
Apart from the standard features found in the `PyYaml` library, the `himl-config-merger` component also implements a custom YAML tag called `!include`.
Expand Down
66 changes: 66 additions & 0 deletions examples/filters/default.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
env: default
region: default
cluster: default

cluster_info:
name: default # this will be overridden by the inner cluster.yaml file

# Interpolation example
description: "This is cluster: {{cluster}}. It is using {{cluster_info.node_type}} instance type."
node_type: c3.2xlarge # default value, which can be overridden by each cluster
cluster_metrics:
- id: 1
metric: cpu
value: 90
- id: 2
metric: memory
value: 90
- id: 3
metric: disk
value: 90
metrics:
- cpu
- memory
- disk
myList:
- id1
- id4
# Fetching the secret value at runtime, from a secrets store (in this case AWS SSM).
# passphrase: "{{ssm.path(/key/coming/from/aws/secrets/store/manager).aws_profile(myprofile)}}"

# Fetching the value at runtime from S3
# my_secret: "{{s3.bucket(my-bucket).path(path/to/file.txt).base64encode(true).aws_profile(myprofile)}}"


_filters:
# Keep _filters key for all outputs. No selector matches all outputs by default.
# - keys:
# values:
# - "_filters"

- selector:
cluster: "cluster.*"
keys:
values:
- persisted_key
# - persisted_key_referenced
# - persisted_key_to_drop
# - persisted_key_to_drop2
# - cluster_persisted_object
# - cluster_persisted_list

- selector:
cluster: cluster1
keys:
values:
- testkey
- home
- cluster_persisted_key

- selector:
cluster: cluster2
keys:
values:
- metrics
- myList
regex: ".*persisted.*"
6 changes: 6 additions & 0 deletions examples/filters/env=dev/env.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
env: dev
persisted_key: &persisted persisted key
dropped_key: &dropped object will be filtered out
persisted_key_referenced: *persisted
persisted_key_to_drop: *dropped
persisted_key_to_drop2: *dropped
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
cluster: cluster1

testkey: |-
# Set to true to log user information returned from LDAP
verbose_logging = true
[[servers]]
# Ldap server host
host = "someaddress"
# Default port is 389 or 636 if use_ssl = true
port = 389
start_tls = true
cluster_persisted_key: this object will be persisted
cluster_filtered_key: this object will be filtered out
cluster_persisted_list: "{{ myList }}"
cluster_persisted_object:
cluster_info: "{{ cluster_info }}"
cluster_list: "{{ myList }}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
cluster: cluster2
cluster_metrics:
- id: 1
metric: cpu
value: 95
- id: 2
metric: memory
value: 95
- id: 3
metric: disk
remove: True
- metric: exec
value: 5
metrics:
- cpu
- exec
myList:
- id1
- id2
- id3
persisted_key: this object will be persisted
dropped_key: this object will be dropped
another_persisted_key: this object will also be persisted
1 change: 1 addition & 0 deletions examples/filters/env=dev/region=us-east-1/region.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
region: us-east-1
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
cluster: cluster1
home: "{{env(HOME)}}"
1 change: 1 addition & 0 deletions examples/filters/env=dev/region=us-west-2/region.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
region: us-west-2
1 change: 1 addition & 0 deletions examples/filters/env=prod/env.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
env: prod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
cluster: ireland1

file: "{{cwd}}/test.txt"
1 change: 1 addition & 0 deletions examples/filters/env=prod/region=eu-west-2/region.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
region: eu-west-2
17 changes: 13 additions & 4 deletions himl/config_merger.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import yaml
from .config_generator import ConfigProcessor
from multiprocessing import Pool, cpu_count

from .filter_rules import FilterRules
logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -71,7 +71,7 @@ def __traverse_path(self, path: str, yaml_dict: dict):
current_key, yaml_dict))


def merge_configs(directories, levels, output_dir, enable_parallel):
def merge_configs(directories, levels, output_dir, enable_parallel, filter_rules):
"""
Method for running the merge configuration logic under different formats
:param directories: list of paths for leaf directories
Expand All @@ -82,7 +82,7 @@ def merge_configs(directories, levels, output_dir, enable_parallel):
config_processor = ConfigProcessor()
process_config = []
for path in directories:
process_config.append((config_processor, path, levels, output_dir))
process_config.append((config_processor, path, levels, output_dir, filter_rules))

if enable_parallel:
logger.info("Processing config in parallel")
Expand All @@ -102,6 +102,7 @@ def merge_logic(process_params):
path = process_params[1]
levels = process_params[2]
output_dir = process_params[3]
filter_rules = process_params[4]

# load the !include tag
Loader.add_constructor('!include', Loader.include)
Expand All @@ -121,6 +122,12 @@ def merge_logic(process_params):
if not os.path.exists(publish_path):
os.makedirs(publish_path)

if filter_rules:
if filter_rules not in output:
raise Exception("Filter rule key '{}' not found in config".format(filter_rules))
filter = FilterRules(output[filter_rules], levels)
filter.run(output)

# create the yaml file for output using the publish_path and last level_values element
filename = "{0}/{1}.yaml".format(publish_path, level_values[-1])
logger.info("Found input config directory: %s", path)
Expand Down Expand Up @@ -171,6 +178,8 @@ def parser_options(args):
help='leaf directories, for instance: cluster', required=True)
parser.add_argument('--enable-parallel', dest='enable_parallel', default=False,
action='store_true', help='Process config using multiprocessing')
parser.add_argument('--filter-rules-key', dest='filter_rules', default=None, type=str,
help='keep these keys from the generated data, based on the configured filter key')
return parser.parse_args(args)


Expand All @@ -182,4 +191,4 @@ def run(args=None):

# merge the configs using HIML
merge_configs(dirs, opts.hierarchy_levels,
opts.output_dir, opts.enable_parallel)
opts.output_dir, opts.enable_parallel, opts.filter_rules)
37 changes: 37 additions & 0 deletions himl/filter_rules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import re


class FilterRules(object):

def __init__(self, rules, levels):
self.rules = rules
self.levels = levels

def run(self, output):

removable_keys = set(output.keys()) - set(self.levels)

for filter in self.rules:
selector = filter.get("selector", {})
if type(selector) != dict:
raise Exception("Filter selector must be a dictionary")

if not self.match(output, selector):
continue

keys = filter.get("keys")
if "values" in keys:
removable_keys = removable_keys - set(keys["values"])
if "regex" in keys:
key_re = re.compile(keys["regex"])
removable_keys = {k for k in removable_keys if not key_re.match(k)}

for key in removable_keys:
del output[key]

def match(self, output, selector):
for key, pattern in selector.items():
value = "" if key not in output else output[key]
if not re.match(pattern, value):
return False
return True
4 changes: 2 additions & 2 deletions himl/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ def do_run(self, opts):
opts.print_data = True

config_processor = ConfigProcessor()
config_processor.process(cwd, opts.path, filters, excluded_keys, opts.enclosing_key, opts.remove_enclosing_key,

config_processor.process(cwd, opts.path, filters, filter_config, excluded_keys, opts.enclosing_key, opts.remove_enclosing_key,
opts.output_format, opts.print_data, opts.output_file, opts.skip_interpolation_resolving,
opts.skip_interpolation_validation, opts.skip_secrets, opts.multi_line_string,
type_strategies= [(list, [opts.merge_list_strategy.value]), (dict, ["merge"])] )
Expand Down

0 comments on commit 6b75194

Please sign in to comment.