Skip to content
This repository was archived by the owner on Feb 7, 2024. It is now read-only.

Commit 67678ff

Browse files
authored
Merge pull request #7 from lingua-pupuli/add-magic-comments
(GH-6) Add magic comments for Puppetfile parser
2 parents f76c4bb + d2a16e3 commit 67678ff

8 files changed

Lines changed: 402 additions & 16 deletions

File tree

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,42 @@ The parser converts the content of a Puppetfile into a document model (`Puppetfi
5454

5555
Currently only one Parser is available, `R10KEval`, which uses the same parsing logic as the [R10K Parser](https://github.com/puppetlabs/r10k/blob/master/lib/r10k/puppetfile.rb). In the future other parsers may be added, such as a [Ruby AST based parser](https://github.com/puppetlabs/r10k/pull/885).
5656

57+
#### R10KEval Parser
58+
59+
Sometimes it necessary to instruct the Resolver to ignore certain rules, for example ignoring the Puppet restrictions on a module because it has old metadata. The R10KEval Parser can read ruby comments which have special meanings, similar to how [rubocop](https://docs.rubocop.org/en/stable/) uses [magic comments](https://docs.rubocop.org/en/stable/configuration/#disabling-cops-within-source-code).
60+
61+
The R10KEval Parser uses the magic comment of `# resolver:disable` to disable tasks. For example;
62+
63+
You can set a comment for one module like this:
64+
65+
``` ruby
66+
mod 'puppetlabs-stdlib', :latest # resolver:disable Dependency/All
67+
```
68+
69+
or for many modules by wrapping the modules like this:
70+
71+
``` ruby
72+
# resolver:disable Dependency/All
73+
mod 'puppetlabs-stdlib', :latest
74+
mod 'puppetlabs-apache', '1.0.0'
75+
# resolver:ensable Dependency/All
76+
```
77+
78+
You can also set multiple items by using a comma like this:
79+
80+
``` ruby
81+
mod 'puppetlabs-stdlib', :latest # resolver:disable Dependency/All,Dependency/Puppet
82+
```
83+
84+
Note there is no space between items.
85+
86+
The available settings are:
87+
88+
| Setting Name | Description |
89+
| --------------------| ----------- |
90+
| `Dependency/Puppet` | Instructs the resolver to ignore any Puppet version in its dependency traversal for the specified modules. Useful for modules with outdated metadata.json information |
91+
| `Dependency/All` | Instructs the resolver to ignore any, and all, dependencies in its dependency traversal of the specified module. Useful for modules with outdated metadata.json information. |
92+
5793
### Puppetfile Document Validation
5894

5995
Even though a Puppetfile can be parsed, doesn't mean it's valid. For example, defining a module twice.

lib/puppetfile-resolver/models/module_specification.rb

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# frozen_string_literal: true
22

33
require 'puppetfile-resolver/models/module_dependency'
4+
require 'puppetfile-resolver/models/puppet_dependency'
5+
require 'puppetfile-resolver/puppetfile'
46

57
module PuppetfileResolver
68
module Models
@@ -9,6 +11,7 @@ class ModuleSpecification
911
attr_accessor :owner
1012
attr_accessor :version
1113
attr_accessor :origin # Same as R10K module :type
14+
attr_accessor :resolver_flags
1215

1316
def initialize(options = {})
1417
require 'semantic_puppet'
@@ -36,6 +39,7 @@ def initialize(options = {})
3639
@origin = options[:origin]
3740
@dependencies = nil
3841
@metadata = options[:metadata]
42+
@resolver_flags = options[:resolver_flags].nil? ? [] : options[:resolver_flags]
3943
@version = ::SemanticPuppet::Version.parse(options[:version]) unless options[:version].nil?
4044
end
4145

@@ -78,8 +82,9 @@ def metadata(_cache, _resolver_ui)
7882
def dependencies(cache, resolver_ui)
7983
return @dependencies unless @dependencies.nil?
8084

81-
meta = metadata(cache, resolver_ui)
85+
return (@dependencies = []) if resolver_flags.include?(PuppetfileResolver::Puppetfile::DISABLE_ALL_DEPENDENCIES_FLAG)
8286

87+
meta = metadata(cache, resolver_ui)
8388
@dependencies = []
8489
unless meta[:dependencies].nil? || meta[:dependencies].empty?
8590
@dependencies = meta[:dependencies].map do |dep|
@@ -90,14 +95,16 @@ def dependencies(cache, resolver_ui)
9095
end
9196
end
9297

93-
puppet_requirement = nil
94-
unless meta[:requirements].nil? || meta[:requirements].empty? # rubocop:disable Style/IfUnlessModifier
95-
puppet_requirement = meta[:requirements].find { |req| req[:name] == 'puppet' && !req[:version_requirement].nil? }
96-
end
97-
if puppet_requirement.nil?
98-
@dependencies << PuppetDependency.new('>= 0')
99-
else
100-
@dependencies << PuppetDependency.new(puppet_requirement[:version_requirement])
98+
unless resolver_flags.include?(PuppetfileResolver::Puppetfile::DISABLE_PUPPET_DEPENDENCY_FLAG)
99+
puppet_requirement = nil
100+
unless meta[:requirements].nil? || meta[:requirements].empty? # rubocop:disable Style/IfUnlessModifier
101+
puppet_requirement = meta[:requirements].find { |req| req[:name] == 'puppet' && !req[:version_requirement].nil? }
102+
end
103+
if puppet_requirement.nil?
104+
@dependencies << PuppetDependency.new('>= 0')
105+
else
106+
@dependencies << PuppetDependency.new(puppet_requirement[:version_requirement])
107+
end
101108
end
102109

103110
@dependencies

lib/puppetfile-resolver/puppetfile.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# frozen_string_literal: true
22

3+
module PuppetfileResolver
4+
module Puppetfile
5+
# Resolver Flags
6+
#
7+
# DISABLE_PUPPET_DEPENDENCY_FLAG - Instructs the resolver to not consider Puppet version in its dependency traversal. Useful for modules with outdated metadata.json information.
8+
# DISABLE_ALL_DEPENDENCIES_FLAG - Instructs the resolver to ignore any dependencies in its dependency traversal. Useful for modules with outdated metadata.json information.
9+
#
10+
DISABLE_PUPPET_DEPENDENCY_FLAG = :disable_puppet_dependency
11+
DISABLE_ALL_DEPENDENCIES_FLAG = :disable_all_dependencies
12+
end
13+
end
14+
315
require 'puppetfile-resolver/puppetfile/document'
416
require 'puppetfile-resolver/puppetfile/validation_errors'
517
require 'puppetfile-resolver/puppetfile/base_module'

lib/puppetfile-resolver/puppetfile/base_module.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,19 @@ class BaseModule
2727

2828
attr_reader :module_type
2929

30+
# Array of flags that will instruct the resolver to change its default behaviour. Current flags are
31+
# set out in the PuppetfileResolver::Puppetfile::..._FLAG constants
32+
# @api private
33+
# @return [Array[Symbol]] Array of flags that will instruct the resolver to change its default behaviour
34+
attr_accessor :resolver_flags
35+
3036
def initialize(title)
3137
@title = title
3238
unless title.nil? # rubocop:disable Style/IfUnlessModifier
3339
@owner, @name = parse_title(@title)
3440
end
3541
@location = DocumentLocation.new
42+
@resolver_flags = []
3643
end
3744

3845
def to_s

lib/puppetfile-resolver/puppetfile/parser/r10k_eval.rb

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22

3+
require 'puppetfile-resolver/puppetfile'
34
require 'puppetfile-resolver/puppetfile/parser/errors'
45
require 'puppetfile-resolver/puppetfile/document'
56
require 'puppetfile-resolver/puppetfile/parser/r10k_eval/dsl'
@@ -51,8 +52,81 @@ def self.parse(puppetfile_contents)
5152
raise err, e.backtrace
5253
end
5354

55+
# Post process magic comments
56+
post_process_flags!(document)
57+
58+
# Freeze the flags so they can't be modified
59+
document.modules.each { |mod| mod.resolver_flags.freeze }
60+
5461
document
5562
end
63+
64+
# Parses a Puppetfile and applies the "magic comments"
65+
def self.post_process_flags!(document)
66+
flag_ranges = {}
67+
document.content.lines.each_with_index do |line, index|
68+
if (matches = line.match(%r{^\s*# resolver:disable ([A-Za-z\/,]+)(?:\s|$)}))
69+
flags_from_line(matches[1]).each do |flag|
70+
# Start a flag range if there isn't already one going
71+
next unless flag_ranges[flag].nil?
72+
flag_ranges[flag] = index
73+
end
74+
elsif (matches = line.match(%r{# resolver:disable ([A-Za-z\/,]+)(?:\s|$)}))
75+
flags_from_line(matches[1]).each do |flag|
76+
# Assert the flag if we're not already within a range
77+
next unless flag_ranges[flag].nil?
78+
assert_resolver_flag(document, flag, index, index)
79+
end
80+
elsif (matches = line.match(%r{^\s*# resolver:enable ([A-Za-z\/,]+)(?:\s|$)}))
81+
flags_from_line(matches[1]).each do |flag|
82+
# End a flag range if there isn't already one going
83+
next if flag_ranges[flag].nil?
84+
assert_resolver_flag(document, flag, flag_ranges[flag], index)
85+
flag_ranges.delete(flag)
86+
end
87+
end
88+
end
89+
90+
return if flag_ranges.empty?
91+
# Any remaining flag ranges will be at the document end
92+
end_line = document.content.lines.count
93+
flag_ranges.each do |flag, start_line|
94+
assert_resolver_flag(document, flag, start_line, end_line)
95+
end
96+
end
97+
private_class_method :post_process_flags!
98+
99+
# Extracts the flags from the text based definitions
100+
# @return [Array[Symbol]]
101+
def self.flags_from_line(line)
102+
line.split(',').map do |flag_name|
103+
case flag_name.downcase
104+
when 'dependency/puppet'
105+
PuppetfileResolver::Puppetfile::DISABLE_PUPPET_DEPENDENCY_FLAG
106+
when 'dependency/all'
107+
PuppetfileResolver::Puppetfile::DISABLE_ALL_DEPENDENCIES_FLAG
108+
else # rubocop:disable Style/EmptyElse We will be adding something here later
109+
# TODO: Should we log a warning/info here?
110+
nil
111+
end
112+
end.compact
113+
end
114+
private_class_method :flags_from_line
115+
116+
# Sets the specified flag on modules which are between from_line to to_line
117+
def self.assert_resolver_flag(document, flag, from_line, to_line)
118+
document.modules.each do |mod|
119+
# If we don't know where the module is (?) then ignore it
120+
next if mod.location.start_line.nil? || mod.location.end_line.nil?
121+
122+
# If the module doesn't span the range we're looking for (from_line --> to_line) ignore it
123+
next unless mod.location.start_line >= from_line && mod.location.start_line <= to_line ||
124+
mod.location.end_line >= from_line && mod.location.end_line <= to_line
125+
mod.resolver_flags << flag unless mod.resolver_flags.include?(flag)
126+
end
127+
nil
128+
end
129+
private_class_method :assert_resolver_flag
56130
end
57131
end
58132
end

lib/puppetfile-resolver/resolution_provider.rb

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,23 +137,23 @@ def find_all_module_specifications(dependency)
137137
unless mod.nil?
138138
case mod.module_type
139139
when Puppetfile::FORGE_MODULE
140-
@module_info[dependency.name] = safe_spec_search { SpecSearchers::Forge.find_all(dependency, @cache, @resolver_ui) }
140+
@module_info[dependency.name] = safe_spec_search(dependency) { SpecSearchers::Forge.find_all(dependency, @cache, @resolver_ui) }
141141
when Puppetfile::GIT_MODULE
142-
@module_info[dependency.name] = safe_spec_search { SpecSearchers::Git.find_all(mod, dependency, @cache, @resolver_ui) }
142+
@module_info[dependency.name] = safe_spec_search(dependency) { SpecSearchers::Git.find_all(mod, dependency, @cache, @resolver_ui) }
143143
else # rubocop:disable Style/EmptyElse
144144
# Errr.... Nothing
145145
end
146146
end
147147
return @module_info[dependency.name] unless @module_info[dependency.name].empty?
148148

149149
# It's not in the Puppetfile, so perhaps it's in our modulepath?
150-
@module_info[dependency.name] = safe_spec_search { SpecSearchers::Local.find_all(mod, @puppet_module_paths, dependency, @cache, @resolver_ui) }
150+
@module_info[dependency.name] = safe_spec_search(dependency) { SpecSearchers::Local.find_all(mod, @puppet_module_paths, dependency, @cache, @resolver_ui) }
151151
return @module_info[dependency.name] unless @module_info[dependency.name].empty?
152152

153153
# It's not in the Puppetfile and not on disk, so perhaps it's on the Forge?
154154
# The forge needs an owner and name to be able to resolve
155155
if dependency.name && dependency.owner # rubocop:disable Style/IfUnlessModifier
156-
@module_info[dependency.name] = safe_spec_search { SpecSearchers::Forge.find_all(dependency, @cache, @resolver_ui) }
156+
@module_info[dependency.name] = safe_spec_search(dependency) { SpecSearchers::Forge.find_all(dependency, @cache, @resolver_ui) }
157157
end
158158

159159
# If we can't find any specifications for the module and we're allowing missing modules
@@ -164,8 +164,13 @@ def find_all_module_specifications(dependency)
164164
@module_info[dependency.name]
165165
end
166166

167-
def safe_spec_search
168-
yield
167+
def safe_spec_search(dependency)
168+
results = yield
169+
# The PuppetfileDependency has the resolver flags, so we need to inject them into the specifications
170+
return results unless dependency.is_a?(PuppetfileResolver::Models::PuppetfileDependency) || results.empty?
171+
results.each { |spec| spec.resolver_flags = dependency.puppetfile_module.resolver_flags }
172+
173+
results
169174
rescue StandardError => e
170175
if @allow_missing_modules
171176
@resolver_ui.debug { "Error while querying a specification searcher #{e.inspect}" }

0 commit comments

Comments
 (0)