1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
|
# frozen_string_literal: true
require_relative '../../puppet/util/logging'
class Puppet::Module
class Plan
class Error < Puppet::Error
attr_accessor :kind, :details
def initialize(message, kind, details = nil)
super(message)
@details = details || {}
@kind = kind
end
def to_h
{
msg: message,
kind: kind,
details: details
}
end
end
class InvalidName < Error
def initialize(name, msg)
super(msg, 'puppet.plans/invalid-name')
end
end
class InvalidFile < Error
def initialize(msg)
super(msg, 'puppet.plans/invalid-file')
end
end
class InvalidPlan < Error
end
class InvalidMetadata < Error
end
class PlanNotFound < Error
def initialize(plan_name, module_name)
msg = _("Plan %{plan_name} not found in module %{module_name}.") %
{ plan_name: plan_name, module_name: module_name }
super(msg, 'puppet.plans/plan-not-found', { 'name' => plan_name })
end
end
ALLOWED_EXTENSIONS = %w[.pp .yaml]
RESERVED_WORDS = %w[and application attr case class consumes default else
elsif environment false function if import in inherits node or private
produces site true type undef unless]
RESERVED_DATA_TYPES = %w[any array boolean catalogentry class collection
callable data default enum float hash integer numeric optional pattern
resource runtime scalar string struct tuple type undef variant]
def self.is_plan_name?(name)
return true if name =~ /^[a-z][a-z0-9_]*$/
false
end
# Determine whether a plan file has a legal name and extension
def self.is_plans_filename?(path)
name = File.basename(path, '.*')
ext = File.extname(path)
return [false, _("Plan names must start with a lowercase letter and be composed of only lowercase letters, numbers, and underscores")] unless is_plan_name?(name)
unless ALLOWED_EXTENSIONS.include? ext
return [false, _("Plan name cannot have extension %{ext}, must be .pp or .yaml") % { ext: ext }]
end
if RESERVED_WORDS.include?(name)
return [false, _("Plan name cannot be a reserved word, but was '%{name}'") % { name: name }]
end
if RESERVED_DATA_TYPES.include?(name)
return [false, _("Plan name cannot be a Puppet data type, but was '%{name}'") % { name: name }]
end
[true]
end
# Executables list should contain the full path of all possible implementation files
def self.find_implementations(name, plan_files)
basename = name.split('::')[1] || 'init'
# If implementations isn't defined, then we use executables matching the
# plan name, and only one may exist.
implementations = plan_files.select { |impl| File.basename(impl, '.*') == basename }
# Select .pp before .yaml, since .pp comes before .yaml alphabetically.
chosen = implementations.min
[{ "name" => File.basename(chosen), "path" => chosen }]
end
private_class_method :find_implementations
def self.find_files(name, plan_files)
find_implementations(name, plan_files)
end
def self.plans_in_module(pup_module)
# Search e.g. 'modules/<pup_module>/plans' for all plans
plan_files = Dir.glob(File.join(pup_module.plans_directory, '*'))
.keep_if { |f| valid, _ = is_plans_filename?(f); valid }
plans = plan_files.group_by { |f| plan_name_from_path(f) }
plans.map do |plan, plan_filenames|
new_with_files(pup_module, plan, plan_filenames)
end
end
attr_reader :name, :module, :metadata_file
# file paths must be relative to the modules plan directory
def initialize(pup_module, plan_name, plan_files)
valid, reason = Puppet::Module::Plan.is_plans_filename?(plan_files.first)
unless valid
raise InvalidName.new(plan_name, reason)
end
name = plan_name == "init" ? pup_module.name : "#{pup_module.name}::#{plan_name}"
@module = pup_module
@name = name
@metadata_file = metadata_file
@plan_files = plan_files || []
end
def metadata
# Nothing to go here unless plans eventually support metadata.
@metadata ||= {}
end
def files
@files ||= self.class.find_files(@name, @plan_files)
end
def validate
files
true
end
def ==(other)
name == other.name &&
self.module == other.module
end
def environment_name
@module.environment.respond_to?(:name) ? @module.environment.name : 'production'
end
private :environment_name
def self.new_with_files(pup_module, name, plan_files)
Puppet::Module::Plan.new(pup_module, name, plan_files)
end
private_class_method :new_with_files
# Abstracted here so we can add support for subdirectories later
def self.plan_name_from_path(path)
File.basename(path, '.*')
end
private_class_method :plan_name_from_path
end
end
|