# lookup in hiera the classes to load ## A fix for the "knockout" problem described in ## https://tickets.puppetlabs.com/browse/HI-223 ## with corresponding documentation at https://puppet.com/docs/puppet/7.5/known_issues_puppet.html ## ## In sum, "The knockout_prefix identifier is effective only against values in an adjacent array, and not in hierarchies more than three levels deep" ## Nb: They actually mean 3 or more levels deep ## Written by Daniel Haag, University of Innsbruck, with contributions by ## Otheus, University of Innsbruck # Two independent Solutions: # 1. by Otheus: Override the status indicated in the classes array with a hiera variable: # class_override_CLASSNAME : enable or disable or none # any depth in the hierarchy can be knocked-out or re-enabled # any previous entry of this variable can be nullified with 'none', so that # the behavior is as if this variable were never defined. # # 2. by Daniel: Implement the merge/knockout as intended. # if the class is redefined in the deepest level ("first"), then it is re-enabled. # # Merged solution: my class_override always takes precedence, but # my override might disable a class un-knocked-out at the lower level. # example, # in hostgroup A, class += X, in hostgroup A/B, class += --X, # class A/B/C, $class_override_X: disable, # nodefile: N: class += X # class X is still disabled because variable takes precedence # # But if: # nodefile also includes $class_override_X: none, # then the un-knockout will take effect. # Step 1: lookup in hiera the classes to load # do a deep-merge of all entries $classes_merged_with_knockouts = lookup('classes', Array[String], { strategy => 'deep'}) # Step 2: Lookup *shallow* -- to allow overrides of previously knocked-out values $classes_top_level = lookup('classes', Array[String], { strategy => 'first'}) # Step 3: Identify and collect class names that were knocked out. # either using --CLASSNAME of by class_override_CLASSNAME # # NB: Reduce() is the only way to do it properly. # Every iteration yields the accumulated value ($m) or a new one ($m + $name) # # Use case 1.1: classname is included, no override/none: no nothing # Use case 1.2: class name is included, override is disable: add to list # Use case 1.3: class name is included, override is enable: do nothing # # # Use case 2.1: class name is --class, no override/none AND not in top-level: add to list # Use case 2.2: class name is --class, override is disable: add to list # Use case 2.3: class name is --class, else: do nothing # $classes_knockedout = $classes_merged_with_knockouts.unique.reduce([]) |$m,$c| { # $m is the accumulation of previous iterations # $c is next item in $classes # $name is classname, which is $c stripped of "--" # $ko is true if $c starts with --, false otherwise # $override is first-found lookup of class_override_${name}, defaulting to "none" # $r gets $name if it should be yielded if ( $c =~ /^--(.*)/) { $name = $1; $ko = true } else { $name = $c; $ko = false } $overname = "class_override_${$name}" $override = lookup($overname,String,{ strategy => first },"none") if (! $ko) { # Use cases 1.1, 1.2, ... case $override { "none": { } "enable": { } "disable": { $r = $name } default: { fail("Invalid parameter to $overname: $override. Must be none/enable/disable") } } } else { # $ko indicated, Use cases 2.1, 2.2... case $override { "none": { if (! ($name in $classes_top_level)) { $r = $name } } "disable": { $r = $name } "enable": { } default: { fail("Invalid parameter to $overname: $override. Must be none/enable/disable") } } } # Yield result: $m if no action or is a duplicate, otherwise $m debug("knockout result for $c: name=$name, ko=$ko, override=$override, tmpresult=$r") if ($r != undef and !($r in $m)) { $m + [ $r ] } else { $m } } # then we process the merged knockout class, removing the knocked-out ones, # as well as the knockout-class tokens $classes = $classes_merged_with_knockouts.filter |$item| { ($item !~ /^--/) and ! ($item in $classes_knockedout) } ## Debugging: if ($::debug_classes or lookup('debug_classes', Boolean, { strategy => first }, false)) { file { "/tmp/classes.yaml": mode => "0644", content => inline_template(" classes:\n<%= @classes.to_yaml %>\n#---\n merged:\n<%= @classes_merged_with_knockouts.sort.to_yaml %>#---\nknockedout: <%= @classes_knockedout.sort.to_yaml %> "), } } else { $classes.each | String $classname | { include($classname) } }