From 775e69e0909e028c2f90be004aa1734e8f26947f Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Tue, 8 Apr 2025 16:25:11 -0700 Subject: [PATCH 1/2] add capability to substitute existing node attributes in new attributes or tags. add existence check operator ~~ --- .../attributes/AttributeNodeEnhancer.java | 102 ++++++++++++------ .../AttributeNodeEnhancerSpec.groovy | 83 ++++++++++++++ 2 files changed, 154 insertions(+), 31 deletions(-) diff --git a/src/main/java/org/rundeck/plugins/nodes/attributes/AttributeNodeEnhancer.java b/src/main/java/org/rundeck/plugins/nodes/attributes/AttributeNodeEnhancer.java index da979b3..c84fa2d 100644 --- a/src/main/java/org/rundeck/plugins/nodes/attributes/AttributeNodeEnhancer.java +++ b/src/main/java/org/rundeck/plugins/nodes/attributes/AttributeNodeEnhancer.java @@ -1,7 +1,10 @@ package org.rundeck.plugins.nodes.attributes; import com.dtolabs.rundeck.core.plugins.Plugin; -import com.dtolabs.rundeck.plugins.descriptions.*; +import com.dtolabs.rundeck.plugins.descriptions.PluginDescription; +import com.dtolabs.rundeck.plugins.descriptions.PluginMetadata; +import com.dtolabs.rundeck.plugins.descriptions.PluginProperty; +import com.dtolabs.rundeck.plugins.descriptions.TextArea; import com.dtolabs.rundeck.plugins.nodes.IModifiableNodeEntry; import com.dtolabs.rundeck.plugins.nodes.NodeEnhancerPlugin; @@ -34,10 +37,12 @@ public class AttributeNodeEnhancer + "Where `key` is the attribute name, " + "`operator` is one of:\n\n" + "* `==` equality match\n" - + "* `!!` not present match\n" + + "* `~~` is present match (no value)\n" + + "* `!!` not present match (no value)\n" + "* `=~` regular expression match\n" + "* `!=` inequality match\n" - + "* `!~` negative regular expression match\n", + + "* `!~` negative regular expression match\n\n" + + "`value` is optional for some operators.\n", required = true) @TextArea private String match; @@ -55,11 +60,13 @@ public class AttributeNodeEnhancer private String addTags; + @PluginProperty(title = "Enable Attribute Substitution", + description = "If enabled, added tags and attribute values can use `${attribute}` to substitute existing node attribute values. E.g. `tag1,image-${ec2.imageId}` or `newattr=some-${oldattr}/${otherattr}`") + private boolean enableSubstitution; + - Map loadedProps; - Set loadedTags; static final Pattern ComparisonPattern = Pattern.compile( - "^(?.+?)(?==|!=|=~|!~|!!)(?.*)$" + "^(?.+?)(?==|!=|=~|!~|!!|~~)(?.*)$" ); @Override @@ -67,47 +74,78 @@ public void updateNode( final String project, final IModifiableNodeEntry node ) { - if (!matchesAll(node.getAttributes())) { + if (!matchesAll(node.getAttributes(), match)) { return; } - addAll(node.getAttributes()); - addAllTags(node.getTags()); + addAll(node.getAttributes(), add, enableSubstitution); + addAllTags(node.getTags(), node.getAttributes(), addTags, enableSubstitution); } - private void addAllTags(final Set tags) { - if (null == loadedTags) { - loadedTags = new HashSet<>(); - if (addTags != null && !"".equals(addTags.trim())) { - loadedTags.addAll(Arrays.asList(addTags.split(",\\s*"))); + public static void addAllTags(final Set tags, Map attributes, String addTags1, boolean enableSubstitution) { + Set loadedTags = new HashSet<>(); + if (addTags1 != null && !addTags1.trim().isEmpty()) { + String[] list = addTags1.split("\\s*,\\s*"); + for (String tag : list) { + if (!tag.trim().isEmpty()) { + //substitute any existing attribute values + loadedTags.add( + enableSubstitution ? substitute(attributes, tag) + : tag + ); + } } } tags.addAll(loadedTags); } - private void addAll(final Map attributes) { - if (null == loadedProps) { - Map map = new HashMap<>(); - if (add != null && !"".equals(add.trim())) { - try { - Properties props = new Properties(); - props.load(new StringReader(add)); - for (String stringPropertyName : props.stringPropertyNames()) { - map.put(stringPropertyName, props.getProperty(stringPropertyName)); - } - } catch (IOException e) { - throw new IllegalArgumentException(); + static final Pattern PROP_REF_PATTERN = Pattern.compile("\\$\\{(?[^}]+)}"); + + public static void addAll(final Map attributes, String propString, boolean enableSubstitution) { + Map map = new HashMap<>(); + if (propString != null && !propString.trim().isEmpty()) { + try { + Properties props = new Properties(); + props.load(new StringReader(propString)); + for (String stringPropertyName : props.stringPropertyNames()) { + String value = props.getProperty(stringPropertyName); + map.put( + stringPropertyName, + enableSubstitution ? substitute(attributes, value) : + value + ); } + } catch (IOException e) { + throw new IllegalArgumentException(); } - loadedProps = map; } - attributes.putAll(loadedProps); + + attributes.putAll(map); + } + + /** + * Substitutes any references in the value with the values from the attributes map. + * + * @param attributes attributes + * @param value value string + * @return new string + */ + private static String substitute(Map attributes, String value) { + Matcher matcher = PROP_REF_PATTERN.matcher(value); + StringBuilder sb = new StringBuilder(); + while (matcher.find()) { + String name = matcher.group("name"); + String replacement = attributes.get(name); + matcher.appendReplacement(sb, Objects.requireNonNullElse(replacement, "")); + } + matcher.appendTail(sb); + return sb.toString(); } - private boolean matchesAll(final Map attributes) { + public static boolean matchesAll(final Map attributes, String matches) { boolean regexMatch = true; Map> comparisons = new HashMap<>(); - for (final String s : match.split("\r?\n")) { + for (final String s : matches.split("\r?\n")) { Matcher matcher = ComparisonPattern.matcher(s); if (matcher.matches()) { String key = matcher.group("key"); @@ -132,13 +170,15 @@ private boolean matchesAll(final Map attributes) { return true; } - private Predicate makePredicate(final String op, final String val) { + static Predicate makePredicate(final String op, final String val) { if ("==".equals(op)) { return val::equals; } else if ("!=".equals(op)) { return makePredicate("==", val).negate(); } else if ("!!".equals(op)) { return Objects::isNull; + } else if ("~~".equals(op)) { + return Predicate.not(Objects::isNull); } else if ("=~".equals(op)) { Pattern p = Pattern.compile(val); return (s) -> s != null && p.matcher(s).matches(); diff --git a/src/test/groovy/org/rundeck/plugins/nodes/attributes/AttributeNodeEnhancerSpec.groovy b/src/test/groovy/org/rundeck/plugins/nodes/attributes/AttributeNodeEnhancerSpec.groovy index 6470e33..293afd1 100644 --- a/src/test/groovy/org/rundeck/plugins/nodes/attributes/AttributeNodeEnhancerSpec.groovy +++ b/src/test/groovy/org/rundeck/plugins/nodes/attributes/AttributeNodeEnhancerSpec.groovy @@ -48,6 +48,60 @@ class AttributeNodeEnhancerSpec extends Specification{ } + def "test attribute value substitution"() { + + given: + def plugin = new AttributeNodeEnhancer() + plugin.match = "attb1==value1\nattb2~~" + plugin.add = 'attb2=a-${attb1}-b-${attb2}' + plugin.enableSubstitution = enabled + + + def node = new ModifiableNodeEntry("test1") + node.attributes = [attb1: "value1", attb2: "value2"] + + def project = "TestProject" + when: + + plugin.updateNode(project, node) + + then: + node.attributes.attb2 == result + + where: + enabled | result + true | "a-value1-b-value2" + false | 'a-${attb1}-b-${attb2}' + } + + def "test tag substitution"() { + + given: + def plugin = new AttributeNodeEnhancer() + plugin.match = "attb1==value1\nattb2~~" + plugin.addTags = 'tag1,a-${attb1}-b-${attb2}' + plugin.enableSubstitution = enabled + + + def node = new ModifiableNodeEntry("test1") + node.attributes = [attb1: "value1", attb2: "value2"] + node.tags = new HashSet<>(["tag0"]) + + def project = "TestProject" + when: + + plugin.updateNode(project, node) + + then: + node.tags == ["tag0", "tag1", result].toSet() + + where: + enabled | result + true | "a-value1-b-value2" + false | 'a-${attb1}-b-${attb2}' + + } + def "test multiples regex match"(){ given: @@ -75,6 +129,35 @@ class AttributeNodeEnhancerSpec extends Specification{ "attb1==value1\nattb2=value2" | null } + def "test present and not present match"() { + + given: + def plugin = new AttributeNodeEnhancer() + plugin.match = match + plugin.add = "result=valueResult" + + + def node = new ModifiableNodeEntry("test1") + node.attributes = [attb1: "value1", attb2: "value2"] + + def project = "TestProject" + when: + + plugin.updateNode(project, node) + + then: + node.attributes.result == result + + where: + match | result + "attb1~~" | "valueResult" + "attb1!!" | null + "attb2~~" | "valueResult" + "attb2!!" | null + "attb3~~" | null + "attb3!!" | "valueResult" + } + class ModifiableNodeEntry extends NodeEntryImpl implements IModifiableNodeEntry{ From 7e2314024ef2bbaf668b03ad55eeb047bbf25079 Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Tue, 8 Apr 2025 16:29:22 -0700 Subject: [PATCH 2/2] cleanup --- .../plugins/nodes/attributes/AttributeNodeEnhancer.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/rundeck/plugins/nodes/attributes/AttributeNodeEnhancer.java b/src/main/java/org/rundeck/plugins/nodes/attributes/AttributeNodeEnhancer.java index c84fa2d..affdb93 100644 --- a/src/main/java/org/rundeck/plugins/nodes/attributes/AttributeNodeEnhancer.java +++ b/src/main/java/org/rundeck/plugins/nodes/attributes/AttributeNodeEnhancer.java @@ -81,10 +81,10 @@ public void updateNode( addAllTags(node.getTags(), node.getAttributes(), addTags, enableSubstitution); } - public static void addAllTags(final Set tags, Map attributes, String addTags1, boolean enableSubstitution) { + public static void addAllTags(final Set tags, Map attributes, String newTags, boolean enableSubstitution) { Set loadedTags = new HashSet<>(); - if (addTags1 != null && !addTags1.trim().isEmpty()) { - String[] list = addTags1.split("\\s*,\\s*"); + if (newTags != null && !newTags.trim().isEmpty()) { + String[] list = newTags.split("\\s*,\\s*"); for (String tag : list) { if (!tag.trim().isEmpty()) { //substitute any existing attribute values @@ -116,7 +116,7 @@ public static void addAll(final Map attributes, String propStrin ); } } catch (IOException e) { - throw new IllegalArgumentException(); + throw new IllegalArgumentException("Unable to parse properties", e); } }