From 898f398b376915d20d9912ad2b11107ec128c368 Mon Sep 17 00:00:00 2001 From: Corporate Gadfly Date: Tue, 9 Jun 2026 18:44:00 -0400 Subject: [PATCH] Fix regex node lookup regression from PUP-11515 The LOOKAROUND_OPERATORS tokens introduced in 5d09d7f are uppercase, but TypeCollection#munge_name() lowercases all lookup keys, so regex nodes with group syntax (e.g. (?:-test)) were stored under a key that could never be found again, producing "Cannot find definition Node". Fix: downcase the generated synthetic name before storing it. Fixes: https://github.com/OpenVoxProject/openvox/issues/14 Signed-off-by: Corporate Gadfly Co-authored-by: GitHub Copilot --- lib/puppet/resource/type.rb | 10 ++++- spec/integration/parser/node_spec.rb | 59 +++++++++++++++++++++++++++- spec/unit/resource/type_spec.rb | 15 +++++++ 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/lib/puppet/resource/type.rb b/lib/puppet/resource/type.rb index afd66f2bc2..c1d036738f 100644 --- a/lib/puppet/resource/type.rb +++ b/lib/puppet/resource/type.rb @@ -206,11 +206,17 @@ def instantiate_resource(scope, resource) def name if type == :node && name_is_regex? - # Normalize lookarround regex patthern + # Normalize lookaround regex pattern to a unique, lookup-safe lowercase string. + # The LOOKAROUND_OPERATORS tokens are uppercased to keep them distinct from ordinary + # regex characters, but the final result MUST be downcased so it matches the key + # produced by munge_name() (which calls String#downcase) inside TypeCollection. + # Without this, a regex such as /^thing-(foo|bar)(?:-test)?-(\d+)/ produces a + # mixed-case synthetic key that is stored correctly but can never be found again + # via the downcased lookup path, causing "Cannot find definition Node" at runtime. internal_name = @name.source.downcase.gsub(/\(\?[^)]*\)/) do |str| str.gsub(/./) { |ch| LOOKAROUND_OPERATORS[ch] || ch } end - "__node_regexp__#{internal_name.gsub(/[^-\w:.]/, '').sub(/^\.+/, '')}" + "__node_regexp__#{internal_name.gsub(/[^-\w:.]/, '').sub(/^\.+/, '')}".downcase else @name end diff --git a/spec/integration/parser/node_spec.rb b/spec/integration/parser/node_spec.rb index b5fa1afbd8..571e760beb 100644 --- a/spec/integration/parser/node_spec.rb +++ b/spec/integration/parser/node_spec.rb @@ -85,7 +85,7 @@ class foo { end.not_to raise_error end - it 'does not raise an error with 2 regex node names are the same due to lookarround pattern' do + it 'does not raise an error with 2 regex node names are the same due to lookaround pattern' do expect do compile_to_catalog(<<-MANIFEST, Puppet::Node.new("async")) node /(? + # e.g. db01.example.com matches, db01.hosts.example.com does not + catalog = compile_to_catalog(<<-MANIFEST, Puppet::Node.new("db01.example.com")) + node /^(db01)\.(?!hosts).*$/ { notify { matched: } } + node default { notify { unmatched: } } + MANIFEST + + expect(catalog).not_to have_resource('Notify[unmatched]') + expect(catalog).to have_resource('Notify[matched]') + end + + it 'does not match a node excluded by a negative lookahead' do + catalog = compile_to_catalog(<<-MANIFEST, Puppet::Node.new("db01.hosts.example.com")) + node /^(db01)\.(?!hosts).*$/ { notify { matched: } } + node default { notify { unmatched: } } + MANIFEST + + expect(catalog).not_to have_resource('Notify[matched]') + expect(catalog).to have_resource('Notify[unmatched]') + end + it 'provides captures from the regex in the node body' do catalog = compile_to_catalog(<<-MANIFEST, Puppet::Node.new("nodename")) node /(.*)/ { notify { "$1": } } diff --git a/spec/unit/resource/type_spec.rb b/spec/unit/resource/type_spec.rb index 7120b447ff..3f5fdc3ebc 100644 --- a/spec/unit/resource/type_spec.rb +++ b/spec/unit/resource/type_spec.rb @@ -67,6 +67,21 @@ expect(Puppet::Resource::Type.new(:node, /W/).name).to eq("__node_regexp__w") end + # Regression coverage for issue #14: lookaround syntax must be encoded into a + # lowercase synthetic name so node lookups can find the stored regex node again. + it "should normalize lookaround syntax into a lowercase synthetic name" do + expect(Puppet::Resource::Type.new(:node, /(?