# frozen_string_literal: true
require 'erb'
require 'fileutils'
require_relative '../../puppet/util/autoload'
require_relative '../../puppet/generate/models/type/type'
module Puppet
module Generate
# Responsible for generating type definitions in Puppet
class Type
# Represents an input to the type generator
class Input
# Gets the path to the input.
attr_reader :path
# Gets the format to use for generating the output file.
attr_reader :format
# Initializes an input.
# @param base [String] The base path where the input is located.
# @param path [String] The path to the input file.
# @param format [Symbol] The format to use for generation.
# @return [void]
def initialize(base, path, format)
@base = base
@path = path
self.format = format
end
# Gets the expected resource type name for the input.
# @return [Symbol] Returns the expected resource type name for the input.
def type_name
File.basename(@path, '.rb').to_sym
end
# Sets the format to use for this input.
# @param format [Symbol] The format to use for generation.
# @return [Symbol] Returns the new format.
def format=(format)
format = format.to_sym
raise _("unsupported format '%{format}'.") % { format: format } unless self.class.supported_format?(format)
@format = format
end
# Determines if the output file is up-to-date with respect to the input file.
# @param [String, nil] The path to output to, or nil if determined by input
# @return [Boolean] Returns true if the output is up-to-date or false if not.
def up_to_date?(outputdir)
f = effective_output_path(outputdir)
Puppet::FileSystem::exist?(f) && (Puppet::FileSystem::stat(@path) <=> Puppet::FileSystem::stat(f)) <= 0
end
# Gets the filename of the output file.
# @return [String] Returns the name to the output file.
def output_name
@output_name ||=
case @format
when :pcore
"#{File.basename(@path, '.rb')}.pp"
else
raise _("unsupported format '%{format}'.") % { format: @format }
end
end
# Gets the path to the output file.
# @return [String] Returns the path to the output file.
def output_path
@output_path ||=
case @format
when :pcore
File.join(@base, 'pcore', 'types', output_name)
else
raise _("unsupported format '%{format}'.") % { format: @format }
end
end
# Sets the path to the output file.
# @param path [String] The new path to the output file.
# @return [String] Returns the new path to the output file.
def output_path=(path)
@output_path = path
end
# Returns the outputpath to use given an outputdir that may be nil
# If outputdir is not nil, the returned path is relative to that outpudir
# otherwise determined by this input.
# @param [String, nil] The outputdirectory to use, or nil if to be determined by this Input
def effective_output_path(outputdir)
outputdir ? File.join(outputdir, output_name) : output_path
end
# Gets the path to the template to use for this input.
# @return [String] Returns the path to the template.
def template_path
File.join(File.dirname(__FILE__), 'templates', 'type', "#{@format}.erb")
end
# Gets the string representation of the input.
# @return [String] Returns the string representation of the input.
def to_s
@path
end
# Determines if the given format is supported
# @param format [Symbol] The format to use for generation.
# @return [Boolean] Returns true if the format is supported or false if not.
def self.supported_format?(format)
[:pcore].include?(format)
end
end
# Finds the inputs for the generator.
# @param format [Symbol] The format to use.
# @param environment [Puppet::Node::Environment] The environment to search for inputs. Defaults to the current environment.
# @return [Array] Returns the array of inputs.
def self.find_inputs(format = :pcore, environment = Puppet.lookup(:current_environment))
Puppet.debug "Searching environment '#{environment.name}' for custom types."
inputs = []
environment.modules.each do |mod|
directory = File.join(Puppet::Util::Autoload.cleanpath(mod.plugin_directory), 'puppet', 'type')
unless Puppet::FileSystem.exist?(directory)
Puppet.debug "Skipping '#{mod.name}' module because it contains no custom types."
next
end
Puppet.debug "Searching '#{mod.name}' module for custom types."
Dir.glob("#{directory}/*.rb") do |file|
next unless Puppet::FileSystem.file?(file)
Puppet.debug "Found custom type source file '#{file}'."
inputs << Input.new(mod.path, file, format)
end
end
# Sort the inputs by path
inputs.sort_by! { |input| input.path }
end
def self.bad_input?
@bad_input
end
# Generates files for the given inputs.
# If a file is up to date (newer than input) it is kept.
# If a file is out of date it is regenerated.
# If there is a file for a non existing output in a given output directory it is removed.
# If using input specific output removal must be made by hand if input is removed.
#
# @param inputs [Array] The inputs to generate files for.
# @param outputdir [String, nil] the outputdir where all output should be generated, or nil if next to input
# @param force [Boolean] True to force the generation of the output files (skip up-to-date checks) or false if not.
# @return [void]
def self.generate(inputs, outputdir = nil, force = false)
# remove files for non existing inputs
unless outputdir.nil?
filenames_to_keep = inputs.map {|i| i.output_name }
existing_files = Puppet::FileSystem.children(outputdir).map {|f| Puppet::FileSystem.basename(f) }
files_to_remove = existing_files - filenames_to_keep
files_to_remove.each do |f|
Puppet::FileSystem.unlink(File.join(outputdir, f))
end
Puppet.notice(_("Removed output '%{files_to_remove}' for non existing inputs") % { files_to_remove: files_to_remove }) unless files_to_remove.empty?
end
if inputs.empty?
Puppet.notice _('No custom types were found.')
return nil
end
templates = {}
templates.default_proc = lambda { |hash, key|
raise _("template was not found at '%{key}'.") % { key: key } unless Puppet::FileSystem.file?(key)
template = Puppet::Util.create_erb(File.read(key))
template.filename = key
template
}
up_to_date = true
@bad_input = false
Puppet.notice _('Generating Puppet resource types.')
inputs.each do |input|
if !force && input.up_to_date?(outputdir)
Puppet.debug "Skipping '#{input}' because it is up-to-date."
next
end
up_to_date = false
type_name = input.type_name
Puppet.debug "Loading custom type '#{type_name}' in '#{input}'."
begin
require input.path
rescue SystemExit
raise
rescue Exception => e
# Log the exception and move on to the next input
@bad_input = true
Puppet.log_exception(e, _("Failed to load custom type '%{type_name}' from '%{input}': %{message}") % { type_name: type_name, input: input, message: e.message })
next
end
# HACK: there's no way to get a type without loading it (sigh); for now, just get the types hash directly
types ||= Puppet::Type.instance_variable_get('@types')
# Assume the type follows the naming convention
type = types[type_name]
unless type
Puppet.err _("Custom type '%{type_name}' was not defined in '%{input}'.") % { type_name: type_name, input: input }
next
end
# Create the model
begin
model = Models::Type::Type.new(type)
rescue Exception => e
@bad_input = true
# Move on to the next input
Puppet.log_exception(e, "#{input}: #{e.message}")
next
end
# Render the template
begin
result = model.render(templates[input.template_path])
rescue Exception => e
@bad_input = true
Puppet.log_exception(e)
raise
end
# Write the output file
begin
effective_output_path = input.effective_output_path(outputdir)
Puppet.notice _("Generating '%{effective_output_path}' using '%{format}' format.") % { effective_output_path: effective_output_path, format: input.format }
FileUtils.mkdir_p(File.dirname(effective_output_path))
Puppet::FileSystem.open(effective_output_path, nil, 'w:UTF-8') do |file|
file.write(result)
end
rescue Exception => e
@bad_input = true
Puppet.log_exception(e, _("Failed to generate '%{effective_output_path}': %{message}") % { effective_output_path: effective_output_path, message: e.message })
# Move on to the next input
next
end
end
Puppet.notice _('No files were generated because all inputs were up-to-date.') if up_to_date
end
end
end
end